Skip to content

Commit 9f07df5

Browse files
feat: select virtual
1 parent e30ae71 commit 9f07df5

File tree

3 files changed

+36
-14
lines changed

3 files changed

+36
-14
lines changed

app/components/ui/select.stories.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { faker } from '@faker-js/faker';
2-
import { ComboboxButton } from '@headlessui/react';
2+
import {
3+
Combobox,
4+
ComboboxButton,
5+
ComboboxInput,
6+
ComboboxOptions,
7+
} from '@headlessui/react';
38
import { Meta } from '@storybook/react';
49
import { ArrowDown, CheckIcon } from 'lucide-react';
510
import { useState } from 'react';
611

712
import { cn } from '@/lib/tailwind/utils';
813

914
import { Button } from '@/components/ui/button';
15+
import { Input } from '@/components/ui/input';
1016
import { ComboboxOption, Select } from '@/components/ui/select';
1117

1218
export default {
@@ -216,7 +222,7 @@ export const Customization = () => {
216222
);
217223
};
218224

219-
const lotsOfOptions = Array.from({ length: 1_000 }, () => ({
225+
const lotsOfOptions = Array.from({ length: 10_000 }, () => ({
220226
label: `${faker.person.firstName()} ${faker.person.lastName()}`,
221227
id: window.crypto.randomUUID(),
222228
}));
@@ -225,6 +231,11 @@ export const LotsOfOptions = () => {
225231
const [bear, setBear] = useState<Bear | null>(null);
226232

227233
return (
228-
<Select options={lotsOfOptions} value={bear} onChange={(v) => setBear(v)} />
234+
<Select
235+
options={lotsOfOptions}
236+
value={bear}
237+
onChange={(v) => setBear(v)}
238+
mode="virtual"
239+
/>
229240
);
230241
};

app/components/ui/select.tsx

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
} from '@headlessui/react';
1111
import { ChevronDown, X } from 'lucide-react';
1212
import { ChangeEvent, ComponentProps, ReactNode, useState } from 'react';
13-
import { isNonNullish, isNullish } from 'remeda';
13+
import { isEmpty, isNonNullish, isNullish } from 'remeda';
1414

1515
import { cn } from '@/lib/tailwind/utils';
1616
import { getUiState } from '@/lib/ui-state';
@@ -34,7 +34,9 @@ type SelectProps<TValue extends TValueBase> = ComboboxProps<TValue, false> &
3434
renderEmpty?: (search: string) => ReactNode;
3535
/** Allow the user to provide a custom value */
3636
allowCustomValue?: boolean;
37+
/** Allow you to provide custom ComboboxOption */
3738
renderOption?: (option: OptionBase) => ReactNode;
39+
mode?: 'default' | 'virtual';
3840
};
3941

4042
export const Select = <TValue extends TValueBase>({
@@ -49,19 +51,13 @@ export const Select = <TValue extends TValueBase>({
4951
value,
5052
defaultValue,
5153
allowCustomValue = false,
54+
mode = 'default',
5255
...props
5356
}: SelectProps<TValue>) => {
54-
const [items, setItems] = useState(options);
5557
const [search, setSearch] = useState('');
5658

5759
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
5860
setSearch(event.target.value);
59-
60-
setItems(
61-
options.filter((item) =>
62-
item.label.toLowerCase().includes(event.target.value.toLowerCase())
63-
)
64-
);
6561
};
6662

6763
const handleOnValueChange: SelectProps<TValue>['onChange'] = (value) => {
@@ -74,9 +70,12 @@ export const Select = <TValue extends TValueBase>({
7470
*/
7571
const handleOnClose = () => {
7672
setSearch('');
77-
setItems(options);
7873
};
7974

75+
const items = options.filter((item) =>
76+
item.label.toLowerCase().includes(search.toLowerCase())
77+
);
78+
8079
const ui = getUiState((set) => {
8180
if (items.length === 0 && allowCustomValue && search.length > 0) {
8281
return set('create-search', { search });
@@ -90,6 +89,10 @@ export const Select = <TValue extends TValueBase>({
9089
return set('empty-override', { renderEmpty });
9190
}
9291

92+
if (mode === 'virtual' && !isEmpty(items)) {
93+
return set('virtual');
94+
}
95+
9396
if (items.length !== 0 && isNonNullish(renderOption)) {
9497
return set('render-option', { renderOption });
9598
}
@@ -107,6 +110,11 @@ export const Select = <TValue extends TValueBase>({
107110
value={value ?? null}
108111
onChange={(v) => handleOnValueChange(v)}
109112
onClose={handleOnClose}
113+
virtual={
114+
mode === 'virtual' && isNonNullish(items) && !isEmpty(items)
115+
? { options: items, disabled: (o) => o?.disabled }
116+
: undefined
117+
}
110118
{...props}
111119
>
112120
<div className="relative">
@@ -157,6 +165,9 @@ export const Select = <TValue extends TValueBase>({
157165
Create <span className="font-bold">{search}</span>
158166
</ComboboxOption>
159167
))
168+
.match('virtual', () => ({ option }: { option: OptionBase }) => (
169+
<ComboboxOption value={option}>{option.label}</ComboboxOption>
170+
))
160171
.match('render-option', ({ renderOption }) =>
161172
items.map((item) => renderOption(item))
162173
)
@@ -188,7 +199,7 @@ export function ComboboxOption({
188199
'flex cursor-pointer gap-1 rounded-sm px-3 py-1.5',
189200
'data-[focus]:bg-neutral-50 dark:data-[focus]:bg-neutral-800',
190201
'data-[selected]:bg-neutral-100 data-[selected]:font-medium dark:data-[selected]:bg-neutral-700',
191-
'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',
202+
'data-disabled:cursor-not-allowed data-disabled:opacity-50',
192203
'text-sm hover:bg-neutral-50 dark:hover:bg-neutral-800',
193204
props.className
194205
)}

app/lib/ui-state.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ type UiState<
2222
Extract<UiState<Status, Data>['state'], { __status: S }>,
2323
'__status'
2424
>
25-
) => React.ReactNode,
25+
) => React.ReactNode | ((...args: ExplicitAny[]) => React.ReactNode),
2626
__matched?: boolean,
2727
run?: () => React.ReactNode
2828
) => {

0 commit comments

Comments
 (0)