@1771technologies/lytenyte-pro
Version:
Blazingly fast headless React data grid with 100s of features.
166 lines (165 loc) • 6.4 kB
JavaScript
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 });
}