From bbb4e46d58ca5f4b2e630d4e497f7ead117852ce Mon Sep 17 00:00:00 2001 From: hreyes Date: Wed, 11 Feb 2026 16:58:59 -0600 Subject: [PATCH] feature/search-select-search-in-trigger --- .../components/ui/combobox/combobox.svelte | 123 ------------------ .../src/lib/components/ui/combobox/index.ts | 1 - .../src/lib/components/ui/select/index.ts | 2 +- .../ui/select/select-content.svelte | 36 ++++- .../components/ui/select/select-item.svelte | 20 ++- .../components/ui/select/select-root.svelte | 33 +++++ .../ui/select/select-search-context.ts | 9 ++ .../ui/select/select-trigger.svelte | 15 +++ 8 files changed, 104 insertions(+), 135 deletions(-) delete mode 100644 frontend/src/lib/components/ui/combobox/combobox.svelte delete mode 100644 frontend/src/lib/components/ui/combobox/index.ts create mode 100644 frontend/src/lib/components/ui/select/select-root.svelte create mode 100644 frontend/src/lib/components/ui/select/select-search-context.ts diff --git a/frontend/src/lib/components/ui/combobox/combobox.svelte b/frontend/src/lib/components/ui/combobox/combobox.svelte deleted file mode 100644 index 7ac23e05..00000000 --- a/frontend/src/lib/components/ui/combobox/combobox.svelte +++ /dev/null @@ -1,123 +0,0 @@ - - - -
- - {selectedLabel} -
- {#if value && !disabled} - - {/if} - -
-
- - - -
- -
- -
- {#if filteredItems.length === 0} -
- {emptyMessage} -
- {:else} - {#each filteredItems as item (item.value)} - handleValueChange(item.value)} - class={cn( - 'data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50' - )} - > - - {item.label} - - {/each} - {/if} -
-
-
-
-
diff --git a/frontend/src/lib/components/ui/combobox/index.ts b/frontend/src/lib/components/ui/combobox/index.ts deleted file mode 100644 index a83ddc67..00000000 --- a/frontend/src/lib/components/ui/combobox/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as Combobox } from './combobox.svelte'; diff --git a/frontend/src/lib/components/ui/select/index.ts b/frontend/src/lib/components/ui/select/index.ts index 7e099815..8fc34655 100644 --- a/frontend/src/lib/components/ui/select/index.ts +++ b/frontend/src/lib/components/ui/select/index.ts @@ -3,6 +3,7 @@ import { Select as SelectPrimitive } from "bits-ui"; import Content from "./select-content.svelte"; import Item from "./select-item.svelte"; import Label from "./select-label.svelte"; +import Root from "./select-root.svelte"; import Trigger from "./select-trigger.svelte"; import Separator from "./select-separator.svelte"; import ScrollUpButton from "./select-scroll-up-button.svelte"; @@ -10,7 +11,6 @@ import ScrollDownButton from "./select-scroll-down-button.svelte"; import GroupHeading from "./select-group-heading.svelte"; import SearchableSelect from "./select-searchable.svelte"; -const Root = SelectPrimitive.Root; const Group = SelectPrimitive.Group; const Input = SelectPrimitive.Input; const Value = SelectPrimitive.Value; diff --git a/frontend/src/lib/components/ui/select/select-content.svelte b/frontend/src/lib/components/ui/select/select-content.svelte index 0a3ef421..8293adf1 100644 --- a/frontend/src/lib/components/ui/select/select-content.svelte +++ b/frontend/src/lib/components/ui/select/select-content.svelte @@ -3,7 +3,8 @@ import SelectScrollUpButton from "./select-scroll-up-button.svelte"; import SelectScrollDownButton from "./select-scroll-down-button.svelte"; import { cn, type WithoutChild } from "$lib/utils.js"; - import { setContext } from 'svelte'; + import { getContext } from "svelte"; + import { selectSearchContextKey, type SelectSearchContext } from "./select-search-context"; let { ref = $bindable(null), @@ -22,14 +23,38 @@ autoFocusSearch?: boolean; } = $props(); + const searchContext = getContext(selectSearchContextKey); let searchQuery = $state(''); + let isOpen = $state(false); let searchInputRef = $state(null); - // Provide search context to child items - setContext('select-search', { - get query() { return searchQuery; } + $effect(() => { + if (!searchContext) return; + const unsubscribe = searchContext.query.subscribe((value) => { + searchQuery = value; + }); + return unsubscribe; }); + $effect(() => { + if (!searchContext) return; + const unsubscribe = searchContext.open.subscribe((value) => { + isOpen = value; + }); + return unsubscribe; + }); + + $effect(() => { + if (!searchable || !autoFocusSearch || !isOpen || !searchInputRef) return; + searchInputRef.focus(); + searchInputRef.select(); + }); + + function updateQuery(next: string) { + searchQuery = next; + searchContext?.query.set(next); + } + function handleContentKeydown(event: KeyboardEvent) { if (!searchable || !searchInputRef) return; if (event.ctrlKey || event.metaKey || event.altKey) return; @@ -58,10 +83,11 @@ updateQuery((event.currentTarget as HTMLInputElement).value)} onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()} /> diff --git a/frontend/src/lib/components/ui/select/select-item.svelte b/frontend/src/lib/components/ui/select/select-item.svelte index f842920b..8295bd90 100644 --- a/frontend/src/lib/components/ui/select/select-item.svelte +++ b/frontend/src/lib/components/ui/select/select-item.svelte @@ -2,7 +2,8 @@ import CheckIcon from "@lucide/svelte/icons/check"; import { Select as SelectPrimitive } from "bits-ui"; import { cn, type WithoutChild } from "$lib/utils.js"; - import { getContext } from 'svelte'; + import { getContext } from "svelte"; + import { selectSearchContextKey, type SelectSearchContext } from "./select-search-context"; let { ref = $bindable(null), @@ -15,16 +16,25 @@ }: WithoutChild & { searchText?: string } = $props(); // Get search context - const searchContext = getContext<{ query: string } | undefined>('select-search'); + const searchContext = getContext(selectSearchContextKey); + let query = $state(''); + + $effect(() => { + if (!searchContext) return; + const unsubscribe = searchContext.query.subscribe((value) => { + query = value; + }); + return unsubscribe; + }); // Determine if item should be visible based on search const isVisible = $derived(() => { if (!searchContext) return true; - const query = searchContext.query.toLowerCase(); - if (!query) return true; + const normalizedQuery = query.toLowerCase(); + if (!normalizedQuery) return true; const itemLabel = (searchText || label || value || '').toString().toLowerCase(); - return itemLabel.includes(query); + return itemLabel.includes(normalizedQuery); }); diff --git a/frontend/src/lib/components/ui/select/select-root.svelte b/frontend/src/lib/components/ui/select/select-root.svelte new file mode 100644 index 00000000..e6b706ed --- /dev/null +++ b/frontend/src/lib/components/ui/select/select-root.svelte @@ -0,0 +1,33 @@ + + + + {@render children?.()} + diff --git a/frontend/src/lib/components/ui/select/select-search-context.ts b/frontend/src/lib/components/ui/select/select-search-context.ts new file mode 100644 index 00000000..d7a7e0b3 --- /dev/null +++ b/frontend/src/lib/components/ui/select/select-search-context.ts @@ -0,0 +1,9 @@ +import type { Writable } from "svelte/store"; + +export const selectSearchContextKey = Symbol("select-search"); + +export type SelectSearchContext = { + query: Writable; + open: Writable; + setOpen: (next: boolean) => void; +}; diff --git a/frontend/src/lib/components/ui/select/select-trigger.svelte b/frontend/src/lib/components/ui/select/select-trigger.svelte index d405187d..de709160 100644 --- a/frontend/src/lib/components/ui/select/select-trigger.svelte +++ b/frontend/src/lib/components/ui/select/select-trigger.svelte @@ -2,6 +2,8 @@ import { Select as SelectPrimitive } from "bits-ui"; import ChevronDownIcon from "@lucide/svelte/icons/chevron-down"; import { cn, type WithoutChild } from "$lib/utils.js"; + import { getContext } from "svelte"; + import { selectSearchContextKey, type SelectSearchContext } from "./select-search-context"; let { ref = $bindable(null), @@ -12,12 +14,25 @@ }: WithoutChild & { size?: "sm" | "default"; } = $props(); + + const searchContext = getContext(selectSearchContextKey); + + function handleTriggerKeydown(event: KeyboardEvent) { + if (!searchContext) return; + if (event.ctrlKey || event.metaKey || event.altKey) return; + if (event.key.length === 1) { + searchContext.setOpen(true); + searchContext.query.update((value) => value + event.key); + event.preventDefault(); + } + }