| import { matchSorter } from 'match-sorter';
|
|
|
| const FRUITS = [
|
| { value: 'Apple', emoji: '🍎' },
|
| { value: 'Grape', emoji: '🍇' },
|
| { value: 'Orange', emoji: '🍊' },
|
| { value: 'Strawberry', emoji: '🍓' },
|
| { value: 'Watermelon', emoji: '🍉' },
|
| { value: 'Banana', emoji: '🍌' },
|
| { value: 'Pineapple', emoji: '🍍' },
|
| { value: 'Peach', emoji: '🍑' },
|
| { value: 'Cherry', emoji: '🍒' },
|
| { value: 'Kiwi', emoji: '🥝' },
|
| { value: 'Mango', emoji: '🥭' },
|
| { value: 'Papaya', emoji: '🍈' },
|
| { value: 'Blueberry', emoji: '🫐' },
|
| { value: 'Coconut', emoji: '🥥' },
|
| ];
|
|
|
| function ComboboxExample() {
|
| const [{ searchValue, selectedValues, open, fruits }, dispatch] =
|
| React.useReducer(
|
| (state, action) => {
|
| switch (action.type) {
|
| case 'set': {
|
| const key = action.key;
|
| const value =
|
| typeof action.value === 'function'
|
| ? action.value(state[key])
|
| : action.value;
|
| return value === state[key] ? state : { ...state, [key]: value };
|
| }
|
| case 'createFruit': {
|
| const { fruits } = state;
|
| const searchValue = state.searchValue.trim();
|
| const value =
|
| searchValue.toLowerCase().charAt(0).toUpperCase() +
|
| searchValue.slice(1);
|
| const fruit = FRUITS.find((fruit) => fruit.value === value) || {
|
| value,
|
| emoji: '🍉',
|
| };
|
| return {
|
| fruits: fruits.some((fruit) => fruit.value === value)
|
| ? fruits
|
| : [...fruits, fruit],
|
| selectedValues: state.selectedValues.includes(value)
|
| ? state.selectedValues
|
| : [...state.selectedValues, value],
|
| open: false,
|
| searchValue: '',
|
| };
|
| }
|
| default:
|
| return state;
|
| }
|
| },
|
| {
|
| searchValue: '',
|
| selectedValues: [],
|
| open: false,
|
| fruits: [...FRUITS].slice(0, 3),
|
| },
|
| );
|
|
|
| const matches = React.useMemo(() => {
|
| return matchSorter(fruits, searchValue, {
|
| keys: ['value'],
|
| baseSort: (a, b) => (a.index < b.index ? -1 : 1),
|
| });
|
| }, [searchValue]);
|
|
|
| const hasSelectedItems = selectedValues.length > 0;
|
|
|
| return (
|
| <Combobox.Root
|
| selectionType="multiple"
|
| selectedValue={selectedValues}
|
| onSelectedValueChange={(value) =>
|
| dispatch({ type: 'set', key: 'selectedValues', value })
|
| }
|
| open={open}
|
| onOpenChange={(value) => dispatch({ type: 'set', key: 'open', value })}
|
| value={searchValue}
|
| onValueChange={(value) => {
|
| React.startTransition(() => {
|
| dispatch({ type: 'set', key: 'searchValue', value });
|
| });
|
| }}
|
| >
|
| <Flex direction="column" gap="2">
|
| <Flex direction="column" gap="1">
|
| <Combobox.Label>Your favorite fruit</Combobox.Label>
|
| <Combobox.Anchor>
|
| <Combobox.Input
|
| placeholder={
|
| hasSelectedItems
|
| ? `${selectedValues.length} items selected`
|
| : 'e.g., Apple, Banana'
|
| }
|
| />
|
| </Combobox.Anchor>
|
| <Combobox.Popover>
|
| <Combobox.Content>
|
| <Combobox.Header>
|
| <Text size="2">Here are some delicious fruits</Text>
|
| </Combobox.Header>
|
| <Combobox.ScrollArea>
|
| {matches.length > 0 ? (
|
| matches.map((match) => (
|
| <Combobox.Item key={match.value} value={match.value}>
|
| {match.emoji} {match.value}
|
| </Combobox.Item>
|
| ))
|
| ) : (
|
| <Flex align="center" gap="2" justify="between" px="2" py="1">
|
| <Text size="2">No results found</Text>
|
| </Flex>
|
| )}
|
| </Combobox.ScrollArea>
|
| <Combobox.Footer>
|
| {(() => {
|
| const cleanSearchValue = searchValue.trim();
|
| if (!cleanSearchValue) {
|
| return null;
|
| }
|
|
|
| const matched = matches.find(
|
| (match) =>
|
| match.value.toLowerCase() ===
|
| cleanSearchValue.toLowerCase(),
|
| );
|
|
|
| return (
|
| <Combobox.ActionItem disabled={!!matched} asChild>
|
| <button
|
| type="button"
|
| onClick={() =>
|
| !matched && dispatch({ type: 'createFruit' })
|
| }
|
| >
|
| <Text>
|
| Create <Text weight="bold">{searchValue}</Text>
|
| </Text>
|
| </button>
|
| </Combobox.ActionItem>
|
| );
|
| })()}
|
|
|
| <Combobox.ActionItem asChild>
|
| <a
|
| href="https://en.wikipedia.org/wiki/Fruit"
|
| target="_blank"
|
| rel="noopener noreferrer"
|
| >
|
| Read about fruits
|
| </a>
|
| </Combobox.ActionItem>
|
| <Combobox.ActionItem asChild>
|
| <button
|
| type="button"
|
| onClick={() =>
|
| dispatch({ type: 'set', key: 'open', value: false })
|
| }
|
| >
|
| Nevermind
|
| </button>
|
| </Combobox.ActionItem>
|
| </Combobox.Footer>
|
| </Combobox.Content>
|
| </Combobox.Popover>
|
| {selectedValues.length > 0 && (
|
| <Combobox.SelectionList>
|
| {selectedValues.map((value) => (
|
| <Combobox.SelectionListItem key={value} value={value}>
|
| {value}
|
| </Combobox.SelectionListItem>
|
| ))}
|
| </Combobox.SelectionList>
|
| )}
|
| </Flex>
|
| </Flex>
|
| </Combobox.Root>
|
| );
|
| }
|