import { createEffect, Show, For, createMemo, type JSX } from "solid-js" import { Dialog } from "@kobalte/core/dialog" import { Icon, IconButton } from "@/ui" import { createStore } from "solid-js/store" import { entries, flatMap, groupBy, map, mapValues, pipe } from "remeda" import { createList } from "solid-list" import fuzzysort from "fuzzysort" interface SelectDialogProps { items: T[] key: (item: T) => string render: (item: T) => JSX.Element current?: T placeholder?: string filter?: | false | { keys: string[] } groupBy?: (x: T) => string onSelect?: (value: T | undefined) => void onClose?: () => void } export function SelectDialog(props: SelectDialogProps) { let scrollRef: HTMLDivElement | undefined const [store, setStore] = createStore({ filter: "", mouseActive: false, }) const grouped = createMemo(() => { const needle = store.filter.toLowerCase() const result = pipe( props.items, (x) => !needle || !props.filter ? x : fuzzysort.go(needle, x, { keys: props.filter && props.filter.keys }).map((x) => x.obj), groupBy((x) => (props.groupBy ? props.groupBy(x) : "")), mapValues((x) => x.sort((a, b) => props.key(a).localeCompare(props.key(b)))), entries(), map(([k, v]) => ({ category: k, items: v })), ) return result }) const flat = createMemo(() => { return pipe( grouped(), flatMap((x) => x.items), ) }) const list = createList({ items: () => flat().map(props.key), initialActive: props.current ? props.key(props.current) : undefined, loop: true, }) const resetSelection = () => list.setActive(props.key(flat()[0])) createEffect(() => { store.filter scrollRef?.scrollTo(0, 0) resetSelection() }) createEffect(() => { if (store.mouseActive) return if (list.active() === props.key(flat()[0])) { scrollRef?.scrollTo(0, 0) return } const element = scrollRef?.querySelector(`[data-key="${list.active()}"]`) element?.scrollIntoView({ block: "nearest", behavior: "smooth" }) }) const handleInput = (value: string) => { setStore("filter", value) resetSelection() } const handleSelect = (item: T) => { props.onSelect?.(item) props.onClose?.() } const handleKey = (e: KeyboardEvent) => { setStore("mouseActive", false) if (e.key === "Enter") { e.preventDefault() const selected = flat().find((x) => props.key(x) === list.active()) if (selected) handleSelect(selected) } else if (e.key === "Escape") { e.preventDefault() props.onClose?.() } else { list.onKeyDown(e) } } return ( open || props.onClose?.()}>
handleInput(e.currentTarget.value)} onKeyDown={handleKey} placeholder={props.placeholder} class="w-full pl-10 pr-4 py-2 rounded-t-md text-sm text-text placeholder-text-muted/70 focus:outline-none" autofocus spellcheck={false} autocorrect="off" autocomplete="off" autocapitalize="off" />
{/*
*/} { setStore("filter", "") resetSelection() }} >
(scrollRef = el)} class="relative flex-1 overflow-y-auto"> 0} fallback={
No results
} > {(group) => ( <>
{group.category}
{(item) => ( )}
)}
↑↓ Navigate Select ESC Close
{`${flat().length} results`}
) }