From b705b362e4be357d4670c7f223d3d0fb4c0aa414 Mon Sep 17 00:00:00 2001 From: Brandon McConnell Date: Thu, 13 Mar 2025 12:45:45 -0400 Subject: [PATCH 01/10] Add support for optional `searchLabel` property in `SelectField` --- .changeset/late-shrimps-sniff.md | 5 ++++ .../src/lib/components/MultiSelect.svelte | 8 +++++- .../src/lib/components/SelectField.svelte | 7 ++++- packages/svelte-ux/src/lib/types/index.ts | 1 + .../components/MultiSelectMenu/+page.svelte | 28 +++++++++++++++++++ .../docs/components/SelectField/+page.svelte | 23 +++++++++++++++ 6 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 .changeset/late-shrimps-sniff.md diff --git a/.changeset/late-shrimps-sniff.md b/.changeset/late-shrimps-sniff.md new file mode 100644 index 000000000..b5919122a --- /dev/null +++ b/.changeset/late-shrimps-sniff.md @@ -0,0 +1,5 @@ +--- +'svelte-ux': patch +--- + +Add support for overriding searchable text in SelectField and MultiSelect components with new searchLabel property on the MenuOption type diff --git a/packages/svelte-ux/src/lib/components/MultiSelect.svelte b/packages/svelte-ux/src/lib/components/MultiSelect.svelte index edbe14b28..446ce3011 100644 --- a/packages/svelte-ux/src/lib/components/MultiSelect.svelte +++ b/packages/svelte-ux/src/lib/components/MultiSelect.svelte @@ -89,8 +89,14 @@ // Filter by search text function applyFilter(option: MenuOption, searchText: string) { + const words = searchText?.toLowerCase().split(' ') ?? []; + const formattedSearchLabel = Array.isArray(option.searchLabel) + ? option.searchLabel.join(' ') + : option.searchLabel; if (searchText) { - return option.label.toLowerCase().includes(searchText.toLowerCase()); + return words.every((word) => + (formattedSearchLabel ?? option.label).toLowerCase().includes(word) + ); } else { // show all if no search set return true; diff --git a/packages/svelte-ux/src/lib/components/SelectField.svelte b/packages/svelte-ux/src/lib/components/SelectField.svelte index 7b57f9088..5ebb6a21e 100644 --- a/packages/svelte-ux/src/lib/components/SelectField.svelte +++ b/packages/svelte-ux/src/lib/components/SelectField.svelte @@ -153,7 +153,12 @@ } else { const words = text?.toLowerCase().split(' ') ?? []; filteredOptions = options.filter((option) => { - return words.every((word) => option.label.toLowerCase().includes(word)); + const formattedSearchLabel = Array.isArray(option.searchLabel) + ? option.searchLabel.join(' ') + : option.searchLabel; + return words.every((word) => + (formattedSearchLabel ?? option.label).toLowerCase().includes(word) + ); }); } }; diff --git a/packages/svelte-ux/src/lib/types/index.ts b/packages/svelte-ux/src/lib/types/index.ts index a85dfd0e8..806d48ddc 100644 --- a/packages/svelte-ux/src/lib/types/index.ts +++ b/packages/svelte-ux/src/lib/types/index.ts @@ -13,6 +13,7 @@ export type MenuOption = { icon?: string; group?: string; disabled?: boolean; + searchLabel?: string | string[]; } & Record; export type LabelPlacement = 'inset' | 'float' | 'top' | 'left'; diff --git a/packages/svelte-ux/src/routes/docs/components/MultiSelectMenu/+page.svelte b/packages/svelte-ux/src/routes/docs/components/MultiSelectMenu/+page.svelte index bbdae0b33..bf897a5bc 100644 --- a/packages/svelte-ux/src/routes/docs/components/MultiSelectMenu/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/components/MultiSelectMenu/+page.svelte @@ -114,6 +114,34 @@ +

inlineSearch with searchLabel

+ + + + + {value.length} selected + ({ + ...o, + searchLabel: [o.label, String(o.value)], + }))} + {value} + on:change={(e) => { + // @ts-expect-error + value = e.detail.value; + }} + {open} + on:close={toggleOff} + inlineSearch + placeholder="Pick a number" + /> + + + + Options can be searched by their values ({options.map((o) => o.value).join(', ')}) + + +

maintainOrder

diff --git a/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte b/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte index f1284b771..669c41e93 100644 --- a/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte @@ -240,6 +240,29 @@ +

option slot with searchLabel

+ + + ({ ...o, searchLabel: [o.label, o.value] }))}> + + +
+
{option.label}
+
{option.value}
+
+
+
+
+
+

option slot with disabled

From c4bcd507f4e600471430ad2a1d30eb20c3f23f46 Mon Sep 17 00:00:00 2001 From: Brandon McConnell Date: Thu, 13 Mar 2025 13:00:45 -0400 Subject: [PATCH 02/10] Refine examples --- packages/svelte-ux/src/lib/types/index.ts | 2 +- .../routes/docs/components/MultiSelect/+page.svelte | 10 ++++++++++ .../docs/components/MultiSelectMenu/+page.svelte | 6 +++--- .../routes/docs/components/SelectField/+page.svelte | 3 +++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/svelte-ux/src/lib/types/index.ts b/packages/svelte-ux/src/lib/types/index.ts index 806d48ddc..3468fbcc1 100644 --- a/packages/svelte-ux/src/lib/types/index.ts +++ b/packages/svelte-ux/src/lib/types/index.ts @@ -13,7 +13,7 @@ export type MenuOption = { icon?: string; group?: string; disabled?: boolean; - searchLabel?: string | string[]; + searchLabel?: string | number | Array; } & Record; export type LabelPlacement = 'inset' | 'float' | 'top' | 'left'; diff --git a/packages/svelte-ux/src/routes/docs/components/MultiSelect/+page.svelte b/packages/svelte-ux/src/routes/docs/components/MultiSelect/+page.svelte index c331b71c6..ac3653466 100644 --- a/packages/svelte-ux/src/routes/docs/components/MultiSelect/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/components/MultiSelect/+page.svelte @@ -67,6 +67,16 @@ (value = e.detail.value)} inlineSearch /> +

inlineSearch with searchLabel

+ + +
+ Options can be searched by their values ({options.map((o) => o.value).join(', ')}) +
+ {value.length} selected + ({ ...o, searchLabel: [o.label, String(o.value)] }))} {value} on:change={(e) => (value = e.detail.value)} inlineSearch /> +
+

maintainOrder

diff --git a/packages/svelte-ux/src/routes/docs/components/MultiSelectMenu/+page.svelte b/packages/svelte-ux/src/routes/docs/components/MultiSelectMenu/+page.svelte index bf897a5bc..7654777aa 100644 --- a/packages/svelte-ux/src/routes/docs/components/MultiSelectMenu/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/components/MultiSelectMenu/+page.svelte @@ -117,6 +117,9 @@

inlineSearch with searchLabel

+
+ Options can be searched by their values ({options.map((o) => o.value).join(', ')}) +
{value.length} selected @@ -137,9 +140,6 @@ /> - - Options can be searched by their values ({options.map((o) => o.value).join(', ')}) -

maintainOrder

diff --git a/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte b/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte index 669c41e93..0e0aa7fa8 100644 --- a/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte @@ -243,6 +243,9 @@

option slot with searchLabel

+
+ Options can be searched by their values ({options.map((o) => o.value).join(', ')}) +
({ ...o, searchLabel: [o.label, o.value] }))}> Date: Thu, 13 Mar 2025 13:01:15 -0400 Subject: [PATCH 03/10] Format --- .../src/routes/docs/components/MultiSelect/+page.svelte | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/svelte-ux/src/routes/docs/components/MultiSelect/+page.svelte b/packages/svelte-ux/src/routes/docs/components/MultiSelect/+page.svelte index ac3653466..33d1f4af7 100644 --- a/packages/svelte-ux/src/routes/docs/components/MultiSelect/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/components/MultiSelect/+page.svelte @@ -74,7 +74,12 @@ Options can be searched by their values ({options.map((o) => o.value).join(', ')}) {value.length} selected - ({ ...o, searchLabel: [o.label, String(o.value)] }))} {value} on:change={(e) => (value = e.detail.value)} inlineSearch /> + ({ ...o, searchLabel: [o.label, String(o.value)] }))} + {value} + on:change={(e) => (value = e.detail.value)} + inlineSearch + />

maintainOrder

From cc139f53441294fb874839e41d3412cc689cc171 Mon Sep 17 00:00:00 2001 From: Brandon McConnell Date: Thu, 13 Mar 2025 14:47:56 -0400 Subject: [PATCH 04/10] Format as string, even if num typed --- packages/svelte-ux/src/lib/components/SelectField.svelte | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/svelte-ux/src/lib/components/SelectField.svelte b/packages/svelte-ux/src/lib/components/SelectField.svelte index 5ebb6a21e..f706306bd 100644 --- a/packages/svelte-ux/src/lib/components/SelectField.svelte +++ b/packages/svelte-ux/src/lib/components/SelectField.svelte @@ -147,15 +147,16 @@ export let search = async (text: string) => { logger.debug('search', { text, open }); + text = text.trim(); if (text === '') { // Reset options filteredOptions = options; } else { const words = text?.toLowerCase().split(' ') ?? []; filteredOptions = options.filter((option) => { - const formattedSearchLabel = Array.isArray(option.searchLabel) - ? option.searchLabel.join(' ') - : option.searchLabel; + const formattedSearchLabel = String( + Array.isArray(option.searchLabel) ? option.searchLabel.join(' ') : option.searchLabel + ); return words.every((word) => (formattedSearchLabel ?? option.label).toLowerCase().includes(word) ); From b1b5865876ee6c4cbdf64cc9735bba19e8fa8bc4 Mon Sep 17 00:00:00 2001 From: Brandon McConnell Date: Thu, 13 Mar 2025 16:34:35 -0400 Subject: [PATCH 05/10] Format as string, even if num typed (2/2) --- packages/svelte-ux/src/lib/components/MultiSelect.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/svelte-ux/src/lib/components/MultiSelect.svelte b/packages/svelte-ux/src/lib/components/MultiSelect.svelte index 446ce3011..e7640ffc1 100644 --- a/packages/svelte-ux/src/lib/components/MultiSelect.svelte +++ b/packages/svelte-ux/src/lib/components/MultiSelect.svelte @@ -90,9 +90,9 @@ // Filter by search text function applyFilter(option: MenuOption, searchText: string) { const words = searchText?.toLowerCase().split(' ') ?? []; - const formattedSearchLabel = Array.isArray(option.searchLabel) - ? option.searchLabel.join(' ') - : option.searchLabel; + const formattedSearchLabel = String( + Array.isArray(option.searchLabel) ? option.searchLabel.join(' ') : option.searchLabel + ); if (searchText) { return words.every((word) => (formattedSearchLabel ?? option.label).toLowerCase().includes(word) From 3b0f347b328fd8996480081d10a3f0f92c7b9704 Mon Sep 17 00:00:00 2001 From: Brandon McConnell Date: Fri, 14 Mar 2025 21:11:04 -0400 Subject: [PATCH 06/10] Refactor PR to update `search` logic (see description for details) - update `search` prop logic - remove `searchLabel` - rename `inlineSearch` -> `search` - add support in `MultiSelect` for passing custom search function into `search` prop --- .../src/lib/components/MultiSelect.svelte | 51 ++++++--- .../src/lib/components/MultiSelectMenu.svelte | 6 +- .../src/lib/components/SelectField.svelte | 68 ++++++------ packages/svelte-ux/src/lib/types/index.ts | 1 - .../docs/components/MultiSelect/+page.svelte | 79 +++++++++---- .../components/MultiSelectMenu/+page.svelte | 104 +++++++++++------- .../docs/components/SelectField/+page.svelte | 94 ++++++++++------ 7 files changed, 257 insertions(+), 146 deletions(-) diff --git a/packages/svelte-ux/src/lib/components/MultiSelect.svelte b/packages/svelte-ux/src/lib/components/MultiSelect.svelte index e7640ffc1..ec902ecca 100644 --- a/packages/svelte-ux/src/lib/components/MultiSelect.svelte +++ b/packages/svelte-ux/src/lib/components/MultiSelect.svelte @@ -8,6 +8,7 @@ import { dirtyStore, selectionStore, uniqueStore, changeStore } from '@layerstack/svelte-stores'; import { cls } from '@layerstack/tailwind'; + import { Logger } from '@layerstack/utils'; import Button from './Button.svelte'; import InfiniteScroll from './InfiniteScroll.svelte'; @@ -17,11 +18,12 @@ import type { MenuOption } from '../types/index.js'; import { getComponentClasses } from './theme.js'; + const logger = new Logger('MultiSelect'); + export let options: MenuOption[]; export let value: TValue[] = []; export let indeterminateSelected: typeof value = []; export let duration = 200; - export let inlineSearch = false; export let autoFocusSearch = false; export let placeholder = 'Search items'; export let optionProps: Partial> | undefined = undefined; @@ -88,23 +90,40 @@ $: [selectedOptions, unselectedOptions] = partition(options, (o) => value.includes(o.value)); // Filter by search text - function applyFilter(option: MenuOption, searchText: string) { - const words = searchText?.toLowerCase().split(' ') ?? []; - const formattedSearchLabel = String( - Array.isArray(option.searchLabel) ? option.searchLabel.join(' ') : option.searchLabel - ); - if (searchText) { - return words.every((word) => - (formattedSearchLabel ?? option.label).toLowerCase().includes(word) - ); + let defaultSearch = async (text: string, options: MenuOption[]) => { + logger.debug('search', { text, open }); + + if (text === '' || options.length === 0) { + // Reset options + return options; } else { - // show all if no search set - return true; + const words = text?.toLowerCase().split(' ') ?? []; + return options.filter((option) => { + const label = option.label.toLowerCase(); + return words.every((word) => label.includes(word)); + }); } + }; + let customSearch: typeof defaultSearch | boolean = false; + export { customSearch as search }; + $: search = typeof customSearch === 'boolean' ? defaultSearch : customSearch; + + let filteredOptions: MenuOption[] = [...(options ?? [])]; + let filteredSelectedOptions: MenuOption[] = [...(selectedOptions ?? [])]; + let filteredUnselectedOptions: MenuOption[] = [...(unselectedOptions ?? [])]; + async function updateFilteredOptions() { + [ + filteredOptions, + filteredSelectedOptions, + filteredUnselectedOptions, + ] = await Promise.all([ + search(searchText, options ?? []), + search(searchText, selectedOptions ?? []), + search(searchText, unselectedOptions ?? []), + ]); } - $: filteredOptions = options.filter((x) => applyFilter(x, searchText)); - $: filteredSelectedOptions = selectedOptions.filter((x) => applyFilter(x, searchText)); - $: filteredUnselectedOptions = unselectedOptions.filter((x) => applyFilter(x, searchText)); + // Re-filter options when `searchText` changes + $: (searchText, updateFilteredOptions()); const selection = selectionStore({ max }); // Only "subscribe" to value changes (not `$selection`) to fix correct `value` / topological ordering. Should be simplified with Svelte 5 @@ -141,7 +160,7 @@ } -{#if inlineSearch} +{#if search}
{ + // Filter by search text + export let search = async (text: string, options: MenuOption[]) => { logger.debug('search', { text, open }); - text = text.trim(); - if (text === '') { + if (text === '' || options.length === 0) { // Reset options - filteredOptions = options; + return options; } else { const words = text?.toLowerCase().split(' ') ?? []; - filteredOptions = options.filter((option) => { - const formattedSearchLabel = String( - Array.isArray(option.searchLabel) ? option.searchLabel.join(' ') : option.searchLabel - ); - return words.every((word) => - (formattedSearchLabel ?? option.label).toLowerCase().includes(word) - ); + return options.filter((option) => { + const label = option.label.toLowerCase(); + return words.every((word) => label.includes(word)); }); } }; @@ -204,32 +200,42 @@ } } + let previousSearchText = ''; + // Do not search if menu is not open / closing on selection $: if (open) { // Capture current highlighted item (attempt to restore after searching) const prevHighlightedOption = filteredOptions[highlightIndex]; // Do not search if menu is not open / closing on selection - search(searchText).then(() => { - // TODO: Find a way for scrollIntoView to still highlight after the menu height transition finishes - const selectedIndex = filteredOptions.findIndex((o) => o.value === value); - if (highlightIndex === -1) { - // Highlight selected if none currently - highlightIndex = selectedIndex === -1 ? nextOptionIndex(-1) : selectedIndex; - } else { - // Attempt to re-highlight previously highlighted option after search - const prevHighlightedOptionIndex = filteredOptions.findIndex( - (o) => o === prevHighlightedOption - ); - - if (prevHighlightedOptionIndex !== -1) { - // Maintain previously highlight index after filter update (option still available) - highlightIndex = prevHighlightedOptionIndex; + if (searchText.trim() && previousSearchText !== searchText) { + previousSearchText = searchText; + search(searchText, options ?? []).then((options) => { + // Update filtered options with new results + filteredOptions = options; + // TODO: Find a way for scrollIntoView to still highlight after the menu height transition finishes + const selectedIndex = options.findIndex((o) => o.value === value); + if (highlightIndex === -1) { + // Highlight selected if none currently + highlightIndex = selectedIndex === -1 ? nextOptionIndex(-1) : selectedIndex; } else { - // Highlight first option - highlightIndex = nextOptionIndex(-1); + // Attempt to re-highlight previously highlighted option after search + const prevHighlightedOptionIndex = options.findIndex( + (o) => o === prevHighlightedOption + ); + + if (prevHighlightedOptionIndex !== -1) { + // Maintain previously highlight index after filter update (option still available) + highlightIndex = prevHighlightedOptionIndex; + } else { + // Highlight first option + highlightIndex = nextOptionIndex(-1); + } } - } - }); + }); + } else if (searchText.trim() === '') { + // Restore options if cleared (show all options) + filteredOptions = options; + } } function onChange(e: ComponentEvents['change']) { @@ -657,4 +663,4 @@ {/if} {/if} -
+ \ No newline at end of file diff --git a/packages/svelte-ux/src/lib/types/index.ts b/packages/svelte-ux/src/lib/types/index.ts index 3468fbcc1..a85dfd0e8 100644 --- a/packages/svelte-ux/src/lib/types/index.ts +++ b/packages/svelte-ux/src/lib/types/index.ts @@ -13,7 +13,6 @@ export type MenuOption = { icon?: string; group?: string; disabled?: boolean; - searchLabel?: string | number | Array; } & Record; export type LabelPlacement = 'inset' | 'float' | 'top' | 'left'; diff --git a/packages/svelte-ux/src/routes/docs/components/MultiSelect/+page.svelte b/packages/svelte-ux/src/routes/docs/components/MultiSelect/+page.svelte index 33d1f4af7..664f2e71d 100644 --- a/packages/svelte-ux/src/routes/docs/components/MultiSelect/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/components/MultiSelect/+page.svelte @@ -60,26 +60,11 @@ />
-

inlineSearch

+

search

{value.length} selected - (value = e.detail.value)} inlineSearch /> - - -

inlineSearch with searchLabel

- - -
- Options can be searched by their values ({options.map((o) => o.value).join(', ')}) -
- {value.length} selected - ({ ...o, searchLabel: [o.label, String(o.value)] }))} - {value} - on:change={(e) => (value = e.detail.value)} - inlineSearch - /> + (value = e.detail.value)} search />

maintainOrder

@@ -148,7 +133,7 @@ -

many options w/ inlineSearch

+

many options w/ search

{value.length} selected @@ -157,7 +142,7 @@ options={manyOptions} {value} on:change={(e) => (value = e.detail.value)} - inlineSearch + search /> @@ -171,7 +156,7 @@ options={manyOptions} {value} on:change={(e) => (value = e.detail.value)} - inlineSearch + search infiniteScroll /> @@ -182,7 +167,7 @@ {value.length} selected
- (value = e.detail.value)} inlineSearch> + (value = e.detail.value)} search>
@@ -195,7 +180,7 @@ {value.length} selected
- (value = e.detail.value)} inlineSearch max={2}> + (value = e.detail.value)} search max={2}>
{#if selection.isMaxSelected()}
Maximum selection reached
@@ -210,7 +195,7 @@ {value.length} selected
- (value = e.detail.value)} inlineSearch> + (value = e.detail.value)} search> +

option slot with custom search

+ + +
+ Options can be searched by their values ({options.map((o) => o.value).join(', ')}) +
+ {value.length} selected + (value = e.detail.value)} + search={async (text, options) => { + text = text?.trim(); + if (!text || options.length === 0) { + return options; + } else { + const words = text?.toLowerCase().split(' ') ?? []; + return options.filter((option) => { + const searchableText = ([option.label, option.value].join(' ')).toLowerCase(); + return words.every((word) => searchableText.includes(word)); + }); + } + }} + > + + + {value} + + {label} + + +
+

Form integration

diff --git a/packages/svelte-ux/src/routes/docs/components/MultiSelectMenu/+page.svelte b/packages/svelte-ux/src/routes/docs/components/MultiSelectMenu/+page.svelte index 7654777aa..06562120c 100644 --- a/packages/svelte-ux/src/routes/docs/components/MultiSelectMenu/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/components/MultiSelectMenu/+page.svelte @@ -92,7 +92,7 @@
-

inlineSearch

+

search

@@ -107,35 +107,7 @@ }} {open} on:close={toggleOff} - inlineSearch - placeholder="Pick a number" - /> - - - - -

inlineSearch with searchLabel

- - -
- Options can be searched by their values ({options.map((o) => o.value).join(', ')}) -
- - - {value.length} selected - ({ - ...o, - searchLabel: [o.label, String(o.value)], - }))} - {value} - on:change={(e) => { - // @ts-expect-error - value = e.detail.value; - }} - {open} - on:close={toggleOff} - inlineSearch + search placeholder="Pick a number" /> @@ -164,7 +136,7 @@
-

maintainOrder w/ inlineSearch

+

maintainOrder w/ search

@@ -179,7 +151,7 @@ }} {open} on:close={toggleOff} - inlineSearch + search maintainOrder placeholder="Pick a number" /> @@ -208,7 +180,7 @@ -

many options w/ inlineSearch

+

many options w/ search

@@ -224,7 +196,7 @@ {open} on:close={toggleOff} classes={{ menu: 'max-h-[360px] w-[360px]' }} - inlineSearch + search /> @@ -246,7 +218,7 @@ {open} on:close={toggleOff} classes={{ menu: 'max-h-[360px] w-[360px]' }} - inlineSearch + search infiniteScroll /> @@ -315,7 +287,7 @@ {open} on:close={toggleOff} classes={{ menu: 'w-[360px]' }} - inlineSearch + search >
@@ -341,7 +313,7 @@ {open} on:close={toggleOff} classes={{ menu: 'w-[360px]' }} - inlineSearch + search > + +

option slot with custom search

+ + +
+ Options can be searched by their values ({options.map((o) => o.value).join(', ')}) +
+ + + {value.length} selected + { + // @ts-expect-error + value = e.detail.value; + }} + {open} + on:close={toggleOff} + classes={{ menu: 'w-[360px]' }} + search={async (text, options) => { + text = text?.trim(); + if (!text || options.length === 0) { + return options; + } else { + const words = text?.toLowerCase().split(' ') ?? []; + return options.filter((option) => { + const searchableText = ([option.label, option.value].join(' ')).toLowerCase(); + return words.every((word) => searchableText.includes(word)); + }); + } + }} + > + + + {value} + + {label} + + + + +
\ No newline at end of file diff --git a/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte b/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte index 0e0aa7fa8..ee1604d13 100644 --- a/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte @@ -8,6 +8,7 @@ Form, MenuItem, SelectField, + State, TextField, Toggle, type MenuOption, @@ -240,32 +241,6 @@ -

option slot with searchLabel

- - -
- Options can be searched by their values ({options.map((o) => o.value).join(', ')}) -
- ({ ...o, searchLabel: [o.label, o.value] }))}> - - -
-
{option.label}
-
{option.value}
-
-
-
-
-
-

option slot with disabled

@@ -350,6 +325,49 @@ +

option slot with custom search

+ + +
+ Options can be searched by their values ({options.map((o) => o.value).join(', ')}) +
+ { + text = text?.trim(); + if (!text || options.length === 0) { + return options; + } else { + const words = text?.toLowerCase().split(' ') ?? []; + return options.filter((option) => { + const searchableText = ([option.label, option.value].join(' ')).toLowerCase(); + return words.every((word) => searchableText.includes(word)); + }); + } + }} + > + +
+
{option.label}
+
{option.value}
+
+
+
+
+

Prepend slot

@@ -511,17 +529,23 @@ -

Search

+

Custom search (case sensitive)

- { - console.log('search override...'); - await delay(1000); - console.log('search override done'); - }} - /> + + {loading ? 'Loading...' : 'Search'} + { + setLoading(true); + console.log('search override...'); + await delay(1000); + console.log('search override done'); + setLoading(false); + return options.filter((option) => option.label.includes(text)); + }} + /> +

Placement

From fc585ff7fee5f338c1721c7bdfa91fa12ffdeacd Mon Sep 17 00:00:00 2001 From: Brandon McConnell Date: Fri, 14 Mar 2025 21:15:12 -0400 Subject: [PATCH 07/10] Format --- .../svelte-ux/src/lib/components/MultiSelect.svelte | 8 ++------ .../svelte-ux/src/lib/components/SelectField.svelte | 6 ++---- .../routes/docs/components/MultiSelect/+page.svelte | 13 +++++-------- .../docs/components/MultiSelectMenu/+page.svelte | 8 +++++--- .../routes/docs/components/SelectField/+page.svelte | 2 +- 5 files changed, 15 insertions(+), 22 deletions(-) diff --git a/packages/svelte-ux/src/lib/components/MultiSelect.svelte b/packages/svelte-ux/src/lib/components/MultiSelect.svelte index ec902ecca..7d30c52bc 100644 --- a/packages/svelte-ux/src/lib/components/MultiSelect.svelte +++ b/packages/svelte-ux/src/lib/components/MultiSelect.svelte @@ -112,18 +112,14 @@ let filteredSelectedOptions: MenuOption[] = [...(selectedOptions ?? [])]; let filteredUnselectedOptions: MenuOption[] = [...(unselectedOptions ?? [])]; async function updateFilteredOptions() { - [ - filteredOptions, - filteredSelectedOptions, - filteredUnselectedOptions, - ] = await Promise.all([ + [filteredOptions, filteredSelectedOptions, filteredUnselectedOptions] = await Promise.all([ search(searchText, options ?? []), search(searchText, selectedOptions ?? []), search(searchText, unselectedOptions ?? []), ]); } // Re-filter options when `searchText` changes - $: (searchText, updateFilteredOptions()); + $: searchText, updateFilteredOptions(); const selection = selectionStore({ max }); // Only "subscribe" to value changes (not `$selection`) to fix correct `value` / topological ordering. Should be simplified with Svelte 5 diff --git a/packages/svelte-ux/src/lib/components/SelectField.svelte b/packages/svelte-ux/src/lib/components/SelectField.svelte index d066b2a18..860ad9ff4 100644 --- a/packages/svelte-ux/src/lib/components/SelectField.svelte +++ b/packages/svelte-ux/src/lib/components/SelectField.svelte @@ -219,9 +219,7 @@ highlightIndex = selectedIndex === -1 ? nextOptionIndex(-1) : selectedIndex; } else { // Attempt to re-highlight previously highlighted option after search - const prevHighlightedOptionIndex = options.findIndex( - (o) => o === prevHighlightedOption - ); + const prevHighlightedOptionIndex = options.findIndex((o) => o === prevHighlightedOption); if (prevHighlightedOptionIndex !== -1) { // Maintain previously highlight index after filter update (option still available) @@ -663,4 +661,4 @@ {/if} {/if} -
\ No newline at end of file +
diff --git a/packages/svelte-ux/src/routes/docs/components/MultiSelect/+page.svelte b/packages/svelte-ux/src/routes/docs/components/MultiSelect/+page.svelte index 664f2e71d..89bfed545 100644 --- a/packages/svelte-ux/src/routes/docs/components/MultiSelect/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/components/MultiSelect/+page.svelte @@ -138,12 +138,7 @@ {value.length} selected
- (value = e.detail.value)} - search - /> + (value = e.detail.value)} search />
@@ -282,7 +277,7 @@ } else { const words = text?.toLowerCase().split(' ') ?? []; return options.filter((option) => { - const searchableText = ([option.label, option.value].join(' ')).toLowerCase(); + const searchableText = [option.label, option.value].join(' ').toLowerCase(); return words.every((word) => searchableText.includes(word)); }); } @@ -304,7 +299,9 @@ container: 'flex items-center gap-1', }} > - + {value} {label} diff --git a/packages/svelte-ux/src/routes/docs/components/MultiSelectMenu/+page.svelte b/packages/svelte-ux/src/routes/docs/components/MultiSelectMenu/+page.svelte index 06562120c..2c2263148 100644 --- a/packages/svelte-ux/src/routes/docs/components/MultiSelectMenu/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/components/MultiSelectMenu/+page.svelte @@ -363,7 +363,7 @@ } else { const words = text?.toLowerCase().split(' ') ?? []; return options.filter((option) => { - const searchableText = ([option.label, option.value].join(' ')).toLowerCase(); + const searchableText = [option.label, option.value].join(' ').toLowerCase(); return words.every((word) => searchableText.includes(word)); }); } @@ -385,7 +385,9 @@ container: 'flex items-center gap-1', }} > - + {value} {label} @@ -393,4 +395,4 @@ - \ No newline at end of file + diff --git a/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte b/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte index ee1604d13..4cd0d2222 100644 --- a/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/components/SelectField/+page.svelte @@ -340,7 +340,7 @@ } else { const words = text?.toLowerCase().split(' ') ?? []; return options.filter((option) => { - const searchableText = ([option.label, option.value].join(' ')).toLowerCase(); + const searchableText = [option.label, option.value].join(' ').toLowerCase(); return words.every((word) => searchableText.includes(word)); }); } From 529241aefc3a988bb42b1db8047152ac0744c5a2 Mon Sep 17 00:00:00 2001 From: Brandon McConnell Date: Sat, 15 Mar 2025 00:54:06 -0400 Subject: [PATCH 08/10] Fix errors --- .../src/lib/components/MultiSelect.svelte | 15 +++++++++------ .../src/lib/components/MultiSelectMenu.svelte | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/svelte-ux/src/lib/components/MultiSelect.svelte b/packages/svelte-ux/src/lib/components/MultiSelect.svelte index 7d30c52bc..188895d3d 100644 --- a/packages/svelte-ux/src/lib/components/MultiSelect.svelte +++ b/packages/svelte-ux/src/lib/components/MultiSelect.svelte @@ -107,16 +107,19 @@ let customSearch: typeof defaultSearch | boolean = false; export { customSearch as search }; $: search = typeof customSearch === 'boolean' ? defaultSearch : customSearch; + $: usingSearch = customSearch !== false; let filteredOptions: MenuOption[] = [...(options ?? [])]; let filteredSelectedOptions: MenuOption[] = [...(selectedOptions ?? [])]; let filteredUnselectedOptions: MenuOption[] = [...(unselectedOptions ?? [])]; async function updateFilteredOptions() { - [filteredOptions, filteredSelectedOptions, filteredUnselectedOptions] = await Promise.all([ - search(searchText, options ?? []), - search(searchText, selectedOptions ?? []), - search(searchText, unselectedOptions ?? []), - ]); + if (usingSearch) { + [filteredOptions, filteredSelectedOptions, filteredUnselectedOptions] = await Promise.all([ + search(searchText, options ?? []), + search(searchText, selectedOptions ?? []), + search(searchText, unselectedOptions ?? []), + ]); + } } // Re-filter options when `searchText` changes $: searchText, updateFilteredOptions(); @@ -156,7 +159,7 @@ } -{#if search} +{#if customSearch}
Date: Sat, 15 Mar 2025 01:10:11 -0400 Subject: [PATCH 09/10] Update changeset --- .changeset/late-shrimps-sniff.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/late-shrimps-sniff.md b/.changeset/late-shrimps-sniff.md index b5919122a..171976637 100644 --- a/.changeset/late-shrimps-sniff.md +++ b/.changeset/late-shrimps-sniff.md @@ -2,4 +2,4 @@ 'svelte-ux': patch --- -Add support for overriding searchable text in SelectField and MultiSelect components with new searchLabel property on the MenuOption type +Unify and enhance search functionality of SelectField and MultiSelect components, enabling new custom search capability in MultiSelect. From 1b17d697b376d0690dee1652f0118d53086f8757 Mon Sep 17 00:00:00 2001 From: Brandon McConnell Date: Sat, 15 Mar 2025 01:11:36 -0400 Subject: [PATCH 10/10] Fix bug --- packages/svelte-ux/src/lib/components/MultiSelect.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte-ux/src/lib/components/MultiSelect.svelte b/packages/svelte-ux/src/lib/components/MultiSelect.svelte index 188895d3d..0afabe15c 100644 --- a/packages/svelte-ux/src/lib/components/MultiSelect.svelte +++ b/packages/svelte-ux/src/lib/components/MultiSelect.svelte @@ -159,7 +159,7 @@ } -{#if customSearch} +{#if usingSearch}