Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/reference/generated/autocomplete-root.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@
"description": "Filter function used to match items vs input query.",
"detailedType": "| ((\n itemValue: any,\n query: string,\n itemToString:\n | ((itemValue: any) => string)\n | undefined,\n ) => boolean)\n| ((\n itemValue: ItemValue,\n query: string,\n itemToString:\n | ((itemValue: ItemValue) => string)\n| null\n| undefined"
},
"filteredItems": {
"type": "any[] | Group[]",
"description": "Filtered items to display in the list.\nWhen provided, the list will use these items instead of filtering the `items` prop internally.\nUse when you want to control filtering logic externally with the `useFilter()` hook.",
"detailedType": "any[] | Group[] | undefined"
},
"grid": {
"type": "boolean",
"default": "false",
Expand Down
5 changes: 5 additions & 0 deletions docs/reference/generated/combobox-root.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@
"description": "Filter function used to match items vs input query.",
"detailedType": "| ((\n itemValue: Value,\n query: string,\n itemToString:\n | ((itemValue: Value) => string)\n | undefined,\n ) => boolean)\n| null\n| undefined"
},
"filteredItems": {
"type": "any[] | Group[]",
"description": "Filtered items to display in the list.\nWhen provided, the list will use these items instead of filtering the `items` prop internally.\nUse when you want to control filtering logic externally with the `useFilter()` hook.",
"detailedType": "any[] | Group[] | undefined"
},
"grid": {
"type": "boolean",
"default": "false",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ export default function ExampleVirtualizedAutocomplete() {
const [open, setOpen] = React.useState(false);
const [searchValue, setSearchValue] = React.useState('');

const deferredSearchValue = React.useDeferredValue(searchValue);

const scrollElementRef = React.useRef<HTMLDivElement | null>(null);

const { contains } = Autocomplete.useFilter({ sensitivity: 'base' });
const { contains } = Autocomplete.useFilter();

const filteredItems = React.useMemo(() => {
return virtualItems.filter((item) => contains(item, searchValue));
}, [contains, searchValue]);
return virtualizedItems.filter((item) => contains(item, deferredSearchValue, getItemLabel));
}, [contains, deferredSearchValue]);

const virtualizer = useVirtualizer({
enabled: open,
Expand All @@ -24,10 +26,12 @@ export default function ExampleVirtualizedAutocomplete() {
overscan: 20,
paddingStart: 8,
paddingEnd: 8,
scrollPaddingEnd: 8,
scrollPaddingStart: 8,
});

const handleScrollElementRef = React.useCallback(
(element: HTMLDivElement) => {
(element: HTMLDivElement | null) => {
scrollElementRef.current = element;
if (element) {
virtualizer.measure();
Expand All @@ -42,12 +46,14 @@ export default function ExampleVirtualizedAutocomplete() {
return (
<Autocomplete.Root
virtualized
items={virtualItems}
items={virtualizedItems}
filteredItems={filteredItems}
open={open}
onOpenChange={setOpen}
value={searchValue}
onValueChange={setSearchValue}
openOnInputClick
itemToStringValue={getItemLabel}
onItemHighlighted={(item, { reason, index }) => {
if (!item) {
return;
Expand All @@ -64,7 +70,7 @@ export default function ExampleVirtualizedAutocomplete() {
}}
>
<label className={styles.Label}>
Search 10,000 items (virtualized)
Search 10,000 items
<Autocomplete.Input className={styles.Input} />
</label>

Expand Down Expand Up @@ -108,7 +114,7 @@ export default function ExampleVirtualizedAutocomplete() {
transform: `translateY(${virtualItem.start}px)`,
}}
>
{item}
{item.name}
</Autocomplete.Item>
);
})}
Expand All @@ -123,7 +129,17 @@ export default function ExampleVirtualizedAutocomplete() {
);
}

const virtualItems = Array.from({ length: 10000 }, (_, i) => {
const indexLabel = String(i + 1).padStart(4, '0');
return `Item ${indexLabel}`;
interface VirtualizedItem {
id: string;
name: string;
}

function getItemLabel(item: VirtualizedItem | null) {
return item ? item.name : '';
}

const virtualizedItems: VirtualizedItem[] = Array.from({ length: 10000 }, (_, index) => {
const id = String(index + 1);
const indexLabel = id.padStart(4, '0');
return { id, name: `Item ${indexLabel}` };
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use client';
import * as React from 'react';
import { Autocomplete } from '@base-ui-components/react/autocomplete';
import { useVirtualizer } from '@tanstack/react-virtual';
Expand All @@ -6,13 +7,15 @@ export default function ExampleVirtualizedAutocomplete() {
const [open, setOpen] = React.useState(false);
const [searchValue, setSearchValue] = React.useState('');

const scrollElementRef = React.useRef<HTMLDivElement>(null);
const deferredSearchValue = React.useDeferredValue(searchValue);

const { contains } = Autocomplete.useFilter({ sensitivity: 'base' });
const scrollElementRef = React.useRef<HTMLDivElement | null>(null);

const { contains } = Autocomplete.useFilter();

const filteredItems = React.useMemo(() => {
return virtualItems.filter((item) => contains(item, searchValue));
}, [contains, searchValue]);
return virtualizedItems.filter((item) => contains(item, deferredSearchValue, getItemLabel));
}, [contains, deferredSearchValue]);

const virtualizer = useVirtualizer({
enabled: open,
Expand All @@ -22,10 +25,12 @@ export default function ExampleVirtualizedAutocomplete() {
overscan: 20,
paddingStart: 8,
paddingEnd: 8,
scrollPaddingEnd: 8,
scrollPaddingStart: 8,
});

const handleScrollElementRef = React.useCallback(
(element: HTMLDivElement) => {
(element: HTMLDivElement | null) => {
scrollElementRef.current = element;
if (element) {
virtualizer.measure();
Expand All @@ -40,12 +45,14 @@ export default function ExampleVirtualizedAutocomplete() {
return (
<Autocomplete.Root
virtualized
items={virtualItems}
items={virtualizedItems}
filteredItems={filteredItems}
open={open}
onOpenChange={setOpen}
value={searchValue}
onValueChange={setSearchValue}
openOnInputClick
itemToStringValue={getItemLabel}
onItemHighlighted={(item, { reason, index }) => {
if (!item) {
return;
Expand All @@ -54,6 +61,7 @@ export default function ExampleVirtualizedAutocomplete() {
const isStart = index === 0;
const isEnd = index === filteredItems.length - 1;
const shouldScroll = reason === 'none' || (reason === 'keyboard' && (isStart || isEnd));

if (shouldScroll) {
queueMicrotask(() => {
virtualizer.scrollToIndex(index, { align: isEnd ? 'start' : 'end' });
Expand All @@ -62,7 +70,7 @@ export default function ExampleVirtualizedAutocomplete() {
}}
>
<label className="flex flex-col gap-1 text-sm leading-5 font-medium text-gray-900">
Search 10,000 items (virtualized)
Search 10,000 items
<Autocomplete.Input className="bg-[canvas] h-10 w-[16rem] md:w-[20rem] font-normal rounded-md border border-gray-200 pl-3.5 text-base text-gray-900 focus:outline focus:outline-2 focus:-outline-offset-1 focus:outline-blue-800" />
</label>

Expand All @@ -77,7 +85,7 @@ export default function ExampleVirtualizedAutocomplete() {
<div
role="presentation"
ref={handleScrollElementRef}
className="h-[min(22rem,var(--total-size))] max-h-[var(--available-height)] overflow-auto overscroll-contain scroll-pt-2"
className="h-[min(22rem,var(--total-size))] max-h-[var(--available-height)] overflow-auto overscroll-contain scroll-p-2"
style={{ '--total-size': totalSizePx } as React.CSSProperties}
>
<div
Expand Down Expand Up @@ -108,7 +116,7 @@ export default function ExampleVirtualizedAutocomplete() {
transform: `translateY(${virtualItem.start}px)`,
}}
>
{item}
{item.name}
</Autocomplete.Item>
);
})}
Expand All @@ -123,7 +131,17 @@ export default function ExampleVirtualizedAutocomplete() {
);
}

const virtualItems = Array.from({ length: 10000 }, (_, i) => {
const indexLabel = String(i + 1).padStart(4, '0');
return `Item ${indexLabel}`;
interface VirtualizedItem {
id: string;
name: string;
}

function getItemLabel(item: VirtualizedItem | null) {
return item ? item.name : '';
}

const virtualizedItems: VirtualizedItem[] = Array.from({ length: 10000 }, (_, index) => {
const id = String(index + 1);
const indexLabel = id.padStart(4, '0');
return { id, name: `Item ${indexLabel}` };
});
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,15 @@ Accepts all `Intl.CollatorOptions`, plus the following option:
type="return"
data={{
contains: {
type: '(itemValue: any, query: string) => boolean',
type: '(itemValue: any, query: string, itemToString?: (itemValue) => string) => boolean',
description: 'Returns whether the item matches the query anywhere.',
},
startsWith: {
type: '(itemValue: any, query: string) => boolean',
type: '(itemValue: any, query: string, itemToString?: (itemValue) => string) => boolean',
description: 'Returns whether the item starts with the query.',
},
endsWith: {
type: '(itemValue: any, query: string) => boolean',
type: '(itemValue: any, query: string, itemToString?: (itemValue) => string) => boolean',
description: 'Returns whether the item ends with the query.',
},
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ import styles from './index.module.css';
export default function ExampleVirtualizedCombobox() {
const [open, setOpen] = React.useState(false);
const [searchValue, setSearchValue] = React.useState('');
const [value, setValue] = React.useState<string | null>(null);
const [value, setValue] = React.useState<VirtualizedItem | null>(null);

const deferredSearchValue = React.useDeferredValue(searchValue);

const scrollElementRef = React.useRef<HTMLDivElement | null>(null);

const { contains } = Combobox.useFilter({ sensitivity: 'base', value });
const { contains } = Combobox.useFilter({ value });

const filteredItems = React.useMemo(() => {
return virtualItems.filter((item) => contains(item, searchValue));
}, [contains, searchValue]);
return virtualizedItems.filter((item) => contains(item, deferredSearchValue, getItemLabel));
}, [contains, deferredSearchValue]);

const virtualizer = useVirtualizer({
enabled: open,
Expand All @@ -30,7 +32,7 @@ export default function ExampleVirtualizedCombobox() {
});

const handleScrollElementRef = React.useCallback(
(element: HTMLDivElement) => {
(element: HTMLDivElement | null) => {
scrollElementRef.current = element;
if (element) {
virtualizer.measure();
Expand All @@ -45,17 +47,15 @@ export default function ExampleVirtualizedCombobox() {
return (
<Combobox.Root
virtualized
filter={contains}
items={virtualItems}
items={virtualizedItems}
filteredItems={filteredItems}
open={open}
onOpenChange={setOpen}
inputValue={searchValue}
onInputValueChange={setSearchValue}
value={value}
onValueChange={(newValue) => {
setValue(newValue);
setSearchValue(newValue ?? '');
}}
onValueChange={setValue}
itemToStringLabel={getItemLabel}
onItemHighlighted={(item, { reason, index }) => {
if (!item) {
return;
Expand All @@ -64,6 +64,7 @@ export default function ExampleVirtualizedCombobox() {
const isStart = index === 0;
const isEnd = index === filteredItems.length - 1;
const shouldScroll = reason === 'none' || (reason === 'keyboard' && (isStart || isEnd));

if (shouldScroll) {
queueMicrotask(() => {
virtualizer.scrollToIndex(index, { align: isEnd ? 'start' : 'end' });
Expand Down Expand Up @@ -119,7 +120,7 @@ export default function ExampleVirtualizedCombobox() {
<Combobox.ItemIndicator className={styles.ItemIndicator}>
<CheckIcon className={styles.ItemIndicatorIcon} />
</Combobox.ItemIndicator>
<div className={styles.ItemText}>{item}</div>
<div className={styles.ItemText}>{item.name}</div>
</Combobox.Item>
);
})}
Expand All @@ -142,7 +143,17 @@ function CheckIcon(props: React.ComponentProps<'svg'>) {
);
}

const virtualItems = Array.from({ length: 10000 }, (_, i) => {
const indexLabel = String(i + 1).padStart(4, '0');
return `Item ${indexLabel}`;
interface VirtualizedItem {
id: string;
name: string;
}

function getItemLabel(item: VirtualizedItem | null) {
return item ? item.name : '';
}

const virtualizedItems: VirtualizedItem[] = Array.from({ length: 10000 }, (_, index) => {
const id = String(index + 1);
const indexLabel = id.padStart(4, '0');
return { id, name: `Item ${indexLabel}` };
});
Loading
Loading