Skip to content

Commit a10dae0

Browse files
committed
[combobox][autocomplete] filteredItems prop
1 parent 1ee10fe commit a10dae0

File tree

10 files changed

+171
-76
lines changed

10 files changed

+171
-76
lines changed

docs/reference/generated/autocomplete-root.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@
6060
"description": "Filter function used to match items vs input query.",
6161
"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"
6262
},
63+
"filteredItems": {
64+
"type": "any[] | Group[]",
65+
"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.",
66+
"detailedType": "any[] | Group[] | undefined"
67+
},
6368
"grid": {
6469
"type": "boolean",
6570
"default": "false",

docs/reference/generated/combobox-root.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@
5959
"description": "Filter function used to match items vs input query.",
6060
"detailedType": "| ((\n itemValue: Value,\n query: string,\n itemToString:\n | ((itemValue: Value) => string)\n | undefined,\n ) => boolean)\n| null\n| undefined"
6161
},
62+
"filteredItems": {
63+
"type": "any[] | Group[]",
64+
"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.",
65+
"detailedType": "any[] | Group[] | undefined"
66+
},
6267
"grid": {
6368
"type": "boolean",
6469
"default": "false",

docs/src/app/(public)/(content)/react/components/autocomplete/demos/virtualized/css-modules/index.tsx

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ export default function ExampleVirtualizedAutocomplete() {
88
const [open, setOpen] = React.useState(false);
99
const [searchValue, setSearchValue] = React.useState('');
1010

11+
const deferredSearchValue = React.useDeferredValue(searchValue);
12+
1113
const scrollElementRef = React.useRef<HTMLDivElement | null>(null);
1214

13-
const { contains } = Autocomplete.useFilter({ sensitivity: 'base' });
15+
const { contains } = Autocomplete.useFilter();
1416

1517
const filteredItems = React.useMemo(() => {
16-
return virtualItems.filter((item) => contains(item, searchValue));
17-
}, [contains, searchValue]);
18+
return virtualizedItems.filter((item) => contains(item, deferredSearchValue, getItemLabel));
19+
}, [contains, deferredSearchValue]);
1820

1921
const virtualizer = useVirtualizer({
2022
enabled: open,
@@ -24,10 +26,12 @@ export default function ExampleVirtualizedAutocomplete() {
2426
overscan: 20,
2527
paddingStart: 8,
2628
paddingEnd: 8,
29+
scrollPaddingEnd: 8,
30+
scrollPaddingStart: 8,
2731
});
2832

2933
const handleScrollElementRef = React.useCallback(
30-
(element: HTMLDivElement) => {
34+
(element: HTMLDivElement | null) => {
3135
scrollElementRef.current = element;
3236
if (element) {
3337
virtualizer.measure();
@@ -42,12 +46,14 @@ export default function ExampleVirtualizedAutocomplete() {
4246
return (
4347
<Autocomplete.Root
4448
virtualized
45-
items={virtualItems}
49+
items={virtualizedItems}
50+
filteredItems={filteredItems}
4651
open={open}
4752
onOpenChange={setOpen}
4853
value={searchValue}
4954
onValueChange={setSearchValue}
5055
openOnInputClick
56+
itemToStringValue={getItemLabel}
5157
onItemHighlighted={(item, { reason, index }) => {
5258
if (!item) {
5359
return;
@@ -64,7 +70,7 @@ export default function ExampleVirtualizedAutocomplete() {
6470
}}
6571
>
6672
<label className={styles.Label}>
67-
Search 10,000 items (virtualized)
73+
Search 10,000 items
6874
<Autocomplete.Input className={styles.Input} />
6975
</label>
7076

@@ -108,7 +114,7 @@ export default function ExampleVirtualizedAutocomplete() {
108114
transform: `translateY(${virtualItem.start}px)`,
109115
}}
110116
>
111-
{item}
117+
{item.name}
112118
</Autocomplete.Item>
113119
);
114120
})}
@@ -123,7 +129,17 @@ export default function ExampleVirtualizedAutocomplete() {
123129
);
124130
}
125131

126-
const virtualItems = Array.from({ length: 10000 }, (_, i) => {
127-
const indexLabel = String(i + 1).padStart(4, '0');
128-
return `Item ${indexLabel}`;
132+
interface VirtualizedItem {
133+
id: string;
134+
name: string;
135+
}
136+
137+
function getItemLabel(item: VirtualizedItem | null) {
138+
return item ? item.name : '';
139+
}
140+
141+
const virtualizedItems: VirtualizedItem[] = Array.from({ length: 10000 }, (_, index) => {
142+
const id = String(index + 1);
143+
const indexLabel = id.padStart(4, '0');
144+
return { id, name: `Item ${indexLabel}` };
129145
});

docs/src/app/(public)/(content)/react/components/autocomplete/demos/virtualized/tailwind/index.tsx

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
'use client';
12
import * as React from 'react';
23
import { Autocomplete } from '@base-ui-components/react/autocomplete';
34
import { useVirtualizer } from '@tanstack/react-virtual';
@@ -6,13 +7,15 @@ export default function ExampleVirtualizedAutocomplete() {
67
const [open, setOpen] = React.useState(false);
78
const [searchValue, setSearchValue] = React.useState('');
89

9-
const scrollElementRef = React.useRef<HTMLDivElement>(null);
10+
const deferredSearchValue = React.useDeferredValue(searchValue);
1011

11-
const { contains } = Autocomplete.useFilter({ sensitivity: 'base' });
12+
const scrollElementRef = React.useRef<HTMLDivElement | null>(null);
13+
14+
const { contains } = Autocomplete.useFilter();
1215

1316
const filteredItems = React.useMemo(() => {
14-
return virtualItems.filter((item) => contains(item, searchValue));
15-
}, [contains, searchValue]);
17+
return virtualizedItems.filter((item) => contains(item, deferredSearchValue, getItemLabel));
18+
}, [contains, deferredSearchValue]);
1619

1720
const virtualizer = useVirtualizer({
1821
enabled: open,
@@ -22,10 +25,12 @@ export default function ExampleVirtualizedAutocomplete() {
2225
overscan: 20,
2326
paddingStart: 8,
2427
paddingEnd: 8,
28+
scrollPaddingEnd: 8,
29+
scrollPaddingStart: 8,
2530
});
2631

2732
const handleScrollElementRef = React.useCallback(
28-
(element: HTMLDivElement) => {
33+
(element: HTMLDivElement | null) => {
2934
scrollElementRef.current = element;
3035
if (element) {
3136
virtualizer.measure();
@@ -40,12 +45,14 @@ export default function ExampleVirtualizedAutocomplete() {
4045
return (
4146
<Autocomplete.Root
4247
virtualized
43-
items={virtualItems}
48+
items={virtualizedItems}
49+
filteredItems={filteredItems}
4450
open={open}
4551
onOpenChange={setOpen}
4652
value={searchValue}
4753
onValueChange={setSearchValue}
4854
openOnInputClick
55+
itemToStringValue={getItemLabel}
4956
onItemHighlighted={(item, { reason, index }) => {
5057
if (!item) {
5158
return;
@@ -54,6 +61,7 @@ export default function ExampleVirtualizedAutocomplete() {
5461
const isStart = index === 0;
5562
const isEnd = index === filteredItems.length - 1;
5663
const shouldScroll = reason === 'none' || (reason === 'keyboard' && (isStart || isEnd));
64+
5765
if (shouldScroll) {
5866
queueMicrotask(() => {
5967
virtualizer.scrollToIndex(index, { align: isEnd ? 'start' : 'end' });
@@ -62,7 +70,7 @@ export default function ExampleVirtualizedAutocomplete() {
6270
}}
6371
>
6472
<label className="flex flex-col gap-1 text-sm leading-5 font-medium text-gray-900">
65-
Search 10,000 items (virtualized)
73+
Search 10,000 items
6674
<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" />
6775
</label>
6876

@@ -77,7 +85,7 @@ export default function ExampleVirtualizedAutocomplete() {
7785
<div
7886
role="presentation"
7987
ref={handleScrollElementRef}
80-
className="h-[min(22rem,var(--total-size))] max-h-[var(--available-height)] overflow-auto overscroll-contain scroll-pt-2"
88+
className="h-[min(22rem,var(--total-size))] max-h-[var(--available-height)] overflow-auto overscroll-contain scroll-p-2"
8189
style={{ '--total-size': totalSizePx } as React.CSSProperties}
8290
>
8391
<div
@@ -108,7 +116,7 @@ export default function ExampleVirtualizedAutocomplete() {
108116
transform: `translateY(${virtualItem.start}px)`,
109117
}}
110118
>
111-
{item}
119+
{item.name}
112120
</Autocomplete.Item>
113121
);
114122
})}
@@ -123,7 +131,17 @@ export default function ExampleVirtualizedAutocomplete() {
123131
);
124132
}
125133

126-
const virtualItems = Array.from({ length: 10000 }, (_, i) => {
127-
const indexLabel = String(i + 1).padStart(4, '0');
128-
return `Item ${indexLabel}`;
134+
interface VirtualizedItem {
135+
id: string;
136+
name: string;
137+
}
138+
139+
function getItemLabel(item: VirtualizedItem | null) {
140+
return item ? item.name : '';
141+
}
142+
143+
const virtualizedItems: VirtualizedItem[] = Array.from({ length: 10000 }, (_, index) => {
144+
const id = String(index + 1);
145+
const indexLabel = id.padStart(4, '0');
146+
return { id, name: `Item ${indexLabel}` };
129147
});

docs/src/app/(public)/(content)/react/components/autocomplete/page.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,15 @@ Accepts all `Intl.CollatorOptions`, plus the following option:
8888
type="return"
8989
data={{
9090
contains: {
91-
type: '(itemValue: any, query: string) => boolean',
91+
type: '(itemValue: any, query: string, itemToString?: (itemValue) => string) => boolean',
9292
description: 'Returns whether the item matches the query anywhere.',
9393
},
9494
startsWith: {
95-
type: '(itemValue: any, query: string) => boolean',
95+
type: '(itemValue: any, query: string, itemToString?: (itemValue) => string) => boolean',
9696
description: 'Returns whether the item starts with the query.',
9797
},
9898
endsWith: {
99-
type: '(itemValue: any, query: string) => boolean',
99+
type: '(itemValue: any, query: string, itemToString?: (itemValue) => string) => boolean',
100100
description: 'Returns whether the item ends with the query.',
101101
},
102102
}}

docs/src/app/(public)/(content)/react/components/combobox/demos/virtualized/css-modules/index.tsx

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@ import styles from './index.module.css';
77
export default function ExampleVirtualizedCombobox() {
88
const [open, setOpen] = React.useState(false);
99
const [searchValue, setSearchValue] = React.useState('');
10-
const [value, setValue] = React.useState<string | null>(null);
10+
const [value, setValue] = React.useState<VirtualizedItem | null>(null);
11+
12+
const deferredSearchValue = React.useDeferredValue(searchValue);
1113

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

14-
const { contains } = Combobox.useFilter({ sensitivity: 'base', value });
16+
const { contains } = Combobox.useFilter({ value });
1517

1618
const filteredItems = React.useMemo(() => {
17-
return virtualItems.filter((item) => contains(item, searchValue));
18-
}, [contains, searchValue]);
19+
return virtualizedItems.filter((item) => contains(item, deferredSearchValue, getItemLabel));
20+
}, [contains, deferredSearchValue]);
1921

2022
const virtualizer = useVirtualizer({
2123
enabled: open,
@@ -30,7 +32,7 @@ export default function ExampleVirtualizedCombobox() {
3032
});
3133

3234
const handleScrollElementRef = React.useCallback(
33-
(element: HTMLDivElement) => {
35+
(element: HTMLDivElement | null) => {
3436
scrollElementRef.current = element;
3537
if (element) {
3638
virtualizer.measure();
@@ -45,17 +47,15 @@ export default function ExampleVirtualizedCombobox() {
4547
return (
4648
<Combobox.Root
4749
virtualized
48-
filter={contains}
49-
items={virtualItems}
50+
items={virtualizedItems}
51+
filteredItems={filteredItems}
5052
open={open}
5153
onOpenChange={setOpen}
5254
inputValue={searchValue}
5355
onInputValueChange={setSearchValue}
5456
value={value}
55-
onValueChange={(newValue) => {
56-
setValue(newValue);
57-
setSearchValue(newValue ?? '');
58-
}}
57+
onValueChange={setValue}
58+
itemToStringLabel={getItemLabel}
5959
onItemHighlighted={(item, { reason, index }) => {
6060
if (!item) {
6161
return;
@@ -64,6 +64,7 @@ export default function ExampleVirtualizedCombobox() {
6464
const isStart = index === 0;
6565
const isEnd = index === filteredItems.length - 1;
6666
const shouldScroll = reason === 'none' || (reason === 'keyboard' && (isStart || isEnd));
67+
6768
if (shouldScroll) {
6869
queueMicrotask(() => {
6970
virtualizer.scrollToIndex(index, { align: isEnd ? 'start' : 'end' });
@@ -119,7 +120,7 @@ export default function ExampleVirtualizedCombobox() {
119120
<Combobox.ItemIndicator className={styles.ItemIndicator}>
120121
<CheckIcon className={styles.ItemIndicatorIcon} />
121122
</Combobox.ItemIndicator>
122-
<div className={styles.ItemText}>{item}</div>
123+
<div className={styles.ItemText}>{item.name}</div>
123124
</Combobox.Item>
124125
);
125126
})}
@@ -142,7 +143,17 @@ function CheckIcon(props: React.ComponentProps<'svg'>) {
142143
);
143144
}
144145

145-
const virtualItems = Array.from({ length: 10000 }, (_, i) => {
146-
const indexLabel = String(i + 1).padStart(4, '0');
147-
return `Item ${indexLabel}`;
146+
interface VirtualizedItem {
147+
id: string;
148+
name: string;
149+
}
150+
151+
function getItemLabel(item: VirtualizedItem | null) {
152+
return item ? item.name : '';
153+
}
154+
155+
const virtualizedItems: VirtualizedItem[] = Array.from({ length: 10000 }, (_, index) => {
156+
const id = String(index + 1);
157+
const indexLabel = id.padStart(4, '0');
158+
return { id, name: `Item ${indexLabel}` };
148159
});

0 commit comments

Comments
 (0)