diff --git a/src/app/page.tsx b/src/app/page.tsx index 876f282..352cb13 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -41,15 +41,15 @@ import { import { MultiSelect, MultiSelectRef } from "@/components/multi-select"; const frameworksList = [ - { value: "next.js", label: "Next.js", icon: Icons.code }, + { value: "next.js", label: "Next.js", icon: Icons.code, keywords: ["fullstack"] }, { value: "react", label: "React", icon: Icons.zap }, { value: "vue", label: "Vue.js", icon: Icons.globe }, { value: "angular", label: "Angular", icon: Icons.target }, { value: "svelte", label: "Svelte", icon: Icons.star }, - { value: "nuxt.js", label: "Nuxt.js", icon: Icons.turtle }, - { value: "remix", label: "Remix", icon: Icons.rabbit }, - { value: "astro", label: "Astro", icon: Icons.fish }, - { value: "gatsby", label: "Gatsby", icon: Icons.dog }, + { value: "nuxt.js", label: "Nuxt.js", icon: Icons.turtle, keywords: ["fullstack"] }, + { value: "remix", label: "Remix", icon: Icons.rabbit, keywords: ["fullstack"] }, + { value: "astro", label: "Astro", icon: Icons.fish, keywords: ["fullstack"] }, + { value: "gatsby", label: "Gatsby", icon: Icons.dog, keywords: ["fullstack"] }, { value: "solid", label: "SolidJS", icon: Icons.cpu }, ]; @@ -1950,7 +1950,7 @@ function SimplifiedExample() { />
✅ Search enabled • ✅ Select all • ✅ Custom empty - state + state • ✅ Item keywords are searchable
@@ -1996,11 +1996,14 @@ function SimplifiedExample() {

Our search implementation is optimized for performance - with debounced input handling, efficient filtering - algorithms, and smooth animations that don't - block user interactions. + with debounced input handling and smooth animations that + don't block user interactions. Uses the underlying + Command filter approach, allowing users to supply + "hidden keywords" for matches, and also an entirely + custom filter function if needed. Try searching for + "fullstack" to see how "Next.js" matches.

-
+
Best Practices: {" "} @@ -2008,6 +2011,13 @@ function SimplifiedExample() { states, and consider disabling search for small option sets (≤10 items) to simplify the interface.
+
+ Pro Tip:{" "} + Add a keywords field to your options to extend + and improve matching beyond visible text and values. You + can also provide a fully custom filter function + for complete control over search behavior. +
diff --git a/src/components/multi-select.tsx b/src/components/multi-select.tsx index 0770a93..70e5779 100644 --- a/src/components/multi-select.tsx +++ b/src/components/multi-select.tsx @@ -93,6 +93,8 @@ interface MultiSelectOption { /** Gradient background for badge */ gradient?: string; }; + /** Optional keywords for improved searchability, passed on to `cmdk` `Command` */ + keywords?: string[]; } /** @@ -183,6 +185,12 @@ interface MultiSelectProps */ searchable?: boolean; + /** + * Optional filter function to customize how options are filtered based on search input. + * Takes the same signature as the underlying `cmdk` Command filter function. + */ + filter?: (value: string, search: string, keywords?: string[]) => number; + /** * Custom empty state message when no options match search. * Optional, defaults to "No results found." @@ -320,6 +328,7 @@ export const MultiSelect = React.forwardRef( className, hideSelectAll = false, searchable = true, + filter, emptyIndicator, autoSize = false, singleLine = false, @@ -580,30 +589,6 @@ export const MultiSelect = React.forwardRef( [getAllOptions] ); - const filteredOptions = React.useMemo(() => { - if (!searchable || !searchValue) return options; - if (options.length === 0) return []; - if (isGroupedOptions(options)) { - return options - .map((group) => ({ - ...group, - options: group.options.filter( - (option) => - option.label - .toLowerCase() - .includes(searchValue.toLowerCase()) || - option.value.toLowerCase().includes(searchValue.toLowerCase()) - ), - })) - .filter((group) => group.options.length > 0); - } - return options.filter( - (option) => - option.label.toLowerCase().includes(searchValue.toLowerCase()) || - option.value.toLowerCase().includes(searchValue.toLowerCase()) - ); - }, [options, searchValue, searchable, isGroupedOptions]); - const handleInputKeyDown = ( event: React.KeyboardEvent ) => { @@ -739,26 +724,6 @@ export const MultiSelect = React.forwardRef( } prevIsOpen.current = isPopoverOpen; } - - if ( - searchValue !== prevSearchValue.current && - searchValue !== undefined - ) { - if (searchValue && isPopoverOpen) { - const filteredCount = allOptions.filter( - (opt) => - opt.label.toLowerCase().includes(searchValue.toLowerCase()) || - opt.value.toLowerCase().includes(searchValue.toLowerCase()) - ).length; - - announce( - `${filteredCount} option${ - filteredCount === 1 ? "" : "s" - } found for "${searchValue}"` - ); - } - prevSearchValue.current = searchValue; - } }, [selectedValues, isPopoverOpen, searchValue, announce, getAllOptions]); return ( @@ -1019,7 +984,7 @@ export const MultiSelect = React.forwardRef( }} align="start" onEscapeKeyDown={() => setIsPopoverOpen(false)}> - + {searchable && ( ( )} - {isGroupedOptions(filteredOptions) ? ( - filteredOptions.map((group) => ( + {isGroupedOptions(options) ? ( + options.map((group) => ( {group.options.map((option) => { const isSelected = selectedValues.includes( @@ -1101,7 +1066,8 @@ export const MultiSelect = React.forwardRef( "cursor-pointer", option.disabled && "opacity-50 cursor-not-allowed" )} - disabled={option.disabled}> + disabled={option.disabled} + keywords={option.keywords}>
( )) ) : ( - {filteredOptions.map((option) => { + {options.map((option) => { const isSelected = selectedValues.includes(option.value); return ( ( "cursor-pointer", option.disabled && "opacity-50 cursor-not-allowed" )} - disabled={option.disabled}> + disabled={option.disabled} + keywords={option.keywords}>