UNPKG

@1771technologies/lytenyte-pro

Version:

Blazingly fast headless React data grid with 100s of features.

166 lines (165 loc) 6.4 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useControlled, useEvent } from "@1771technologies/lytenyte-core/internal"; import { Fragment, useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { SmartSelectProvider } from "./context.js"; import { Popover } from "../headless/popover/index.js"; import { useSlot } from "../../hooks/use-slot/use-slot.js"; import { SmartSelectContainer } from "./container.js"; import { Option } from "./option.js"; import { useAsyncOptions } from "./use-async-options.js"; import { getNearestMatching } from "@1771technologies/lytenyte-shared"; import { createPortal } from "react-dom"; export function SmartSelectRoot(p) { const [open, setOpen] = useControlled({ controlled: p.open, default: false }); const [query, setQuery] = useControlled({ controlled: p.query, default: "" }); const [triggerEl, setTriggerEl] = useState(null); const [activeId, setActiveId] = useState(null); const [containerEl, setContainerEl] = useState(null); const [rtl, setRtl] = useState(false); const triggerRef = useCallback((el) => { setTriggerEl(el); if (!el) return; const style = getComputedStyle(el); setRtl(style.direction === "rtl"); }, []); const onOpenChange = useEvent((b) => { setOpen(b); p.onOpenChange?.(b); }); const onQueryChange = useEvent((change) => { setQuery(change); p.onQueryChange?.(change); }); const normalizedValue = useMemo(() => { if (p.kind === "basic" || p.kind === "combo") return [p.value]; return p.value; }, [p.kind, p.value]); const clearOnSelect = p.clearOnSelect ?? true; const onOptionSelect = useEvent((change) => { if (p.kind === "basic" || p.kind === "combo") { const isSelected = p.value?.id === change.id; if (isSelected) p.onOptionChange(null); else p.onOptionChange(change); } else { const isSelected = p.value.find((x) => x.id === change.id); const next = isSelected ? p.value.filter((x) => x.id !== change.id) : [...p.value, change]; p.onOptionChange(next); } if (clearOnSelect) onQueryChange(""); }); const onOptionsChange = useEvent((change) => { if (p.kind === "basic" || p.kind === "combo") return; p.onOptionChange(change); }); const optValue = typeof p.options === "function" ? null : p.options; const basicSelectOptions = useMemo(() => { return optValue ?? []; }, [optValue]); const comboState = useAsyncOptions(typeof p.options === "function" ? p.options : null, p.clearOnQuery ?? false); const options = typeof p.options === "function" ? comboState.options : basicSelectOptions; const debounce = p.kind === "multi" || p.kind === "multi-combo" ? (p.searchDebounceMs ?? 200) : 0; const loadOptions = comboState.loadOptions; useEffect(() => { if (p.kind !== "multi-combo" && p.kind !== "combo") return; const t = setTimeout(() => { loadOptions(query); }, debounce); return () => clearTimeout(t); }, [loadOptions, debounce, p.kind, query]); useEffect(() => { if (!open) return; // eslint-disable-next-line react-hooks/set-state-in-effect setActiveId((prev) => { const active = prev ? (options.find((x) => x.id === prev) ?? null) : null; if (!active) queueMicrotask(() => setActiveId(options.at(0)?.id ?? null)); return prev; }); }, [comboState, open, options]); const renderedOptions = useMemo(() => { const render = p.children ?? DefaultChildren; return options.map((x) => { const selected = !!normalizedValue.find((v) => x.id === v?.id); const active = x.id === activeId; return _jsx(Fragment, { children: render({ option: x, selected, active }) }, x.id); }); }, [activeId, normalizedValue, options, p.children]); const trigger = useSlot({ slot: p.trigger }); const container = useSlot({ props: [{ children: renderedOptions }], slot: p.container ?? _jsx(SmartSelectContainer, {}), state: { children: renderedOptions, ...comboState, options, }, }); const inputRef = useRef(null); const value = useMemo(() => { return { onOpenChange, open, closeOnSelect: p.closeOnSelect ?? true, onOptionSelect, onOptionsChange, kindAndValue: { kind: p.kind, value: p.value }, options, inputRef, trigger: triggerEl, setTrigger: triggerRef, rtl, openKeys: p.openKeys ?? [" ", "Enter", "ArrowDown"], closeKeys: p.closeKeys ?? ["Escape"], activeId: activeId, setActiveId: setActiveId, container: containerEl, setContainer: setContainerEl, query, onQueryChange, openOnClick: p.openOnClick ?? true, preventNextOpen: { current: false }, comboState, }; }, [ activeId, comboState, containerEl, onOpenChange, onOptionSelect, onOptionsChange, onQueryChange, open, options, p.closeKeys, p.closeOnSelect, p.kind, p.openKeys, p.openOnClick, p.value, query, rtl, triggerEl, triggerRef, ]); const lightDismiss = useCallback((el) => { const closest = getNearestMatching(el, (el) => el === triggerEl); if (!closest) return true; return false; }, [triggerEl]); return (_jsx(SmartSelectProvider, { value: value, children: _jsxs(Popover, { open: open, onOpenChange: onOpenChange, onOpenChangeComplete: p.onOpenChangeComplete, focusTrap: false, modal: false, anchor: triggerEl, lightDismiss: lightDismiss, children: [trigger, triggerEl && createPortal(container, p.kind === "combo" ? triggerEl.parentElement : triggerEl)] }) })); } function DefaultChildren(p) { return _jsx(Option, { ...p }); }