UNPKG

@yamada-ui/autocomplete

Version:

Yamada UI autocomplete component

904 lines (902 loc) 25.9 kB
"use client" import { AutocompleteOptionGroup } from "./chunk-YHWMP7UU.mjs"; import { AutocompleteOption } from "./chunk-MNMEH7ZC.mjs"; import { useAutocompleteContext, useAutocompleteDescendants } from "./chunk-X5V76SN2.mjs"; // src/use-autocomplete.tsx import { layoutStyleProperties } from "@yamada-ui/core"; import { formControlProperties, useFormControlProps } from "@yamada-ui/form-control"; import { useControllableState } from "@yamada-ui/use-controllable-state"; import { useDisclosure } from "@yamada-ui/use-disclosure"; import { useOutsideClick } from "@yamada-ui/use-outside-click"; import { dataAttr, funcAll, getEventRelatedTarget, getValidChildren, handlerAll, isArray, isContains, isUndefined, mergeRefs, pickObject, splitObject, useSafeLayoutEffect, useUnmountEffect, useUpdateEffect } from "@yamada-ui/utils"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { jsx } from "react/jsx-runtime"; var kanaMap = { "\uFF61": "\u3002", "\uFF62": "\u300C", "\uFF63": "\u300D", "\uFF64": "\u3001", "\uFF65": "\u30FB", \uFF66: "\u30F2", \uFF66\uFF9E: "\u30FA", \uFF67: "\u30A1", \uFF68: "\u30A3", \uFF69: "\u30A5", \uFF6A: "\u30A7", \uFF6B: "\u30A9", \uFF6C: "\u30E3", \uFF6D: "\u30E5", \uFF6E: "\u30E7", \uFF6F: "\u30C3", \uFF70: "\u30FC", \uFF71: "\u30A2", \uFF72: "\u30A4", \uFF73: "\u30A6", \uFF73\uFF9E: "\u30F4", \uFF74: "\u30A8", \uFF75: "\u30AA", \uFF76: "\u30AB", \uFF76\uFF9E: "\u30AC", \uFF77: "\u30AD", \uFF77\uFF9E: "\u30AE", \uFF78: "\u30AF", \uFF78\uFF9E: "\u30B0", \uFF79: "\u30B1", \uFF79\uFF9E: "\u30B2", \uFF7A: "\u30B3", \uFF7A\uFF9E: "\u30B4", \uFF7B: "\u30B5", \uFF7B\uFF9E: "\u30B6", \uFF7C: "\u30B7", \uFF7C\uFF9E: "\u30B8", \uFF7D: "\u30B9", \uFF7D\uFF9E: "\u30BA", \uFF7E: "\u30BB", \uFF7E\uFF9E: "\u30BC", \uFF7F: "\u30BD", \uFF7F\uFF9E: "\u30BE", \uFF80: "\u30BF", \uFF80\uFF9E: "\u30C0", \uFF81: "\u30C1", \uFF81\uFF9E: "\u30C2", \uFF82: "\u30C4", \uFF82\uFF9E: "\u30C5", \uFF83: "\u30C6", \uFF83\uFF9E: "\u30C7", \uFF84: "\u30C8", \uFF84\uFF9E: "\u30C9", \uFF85: "\u30CA", \uFF86: "\u30CB", \uFF87: "\u30CC", \uFF88: "\u30CD", \uFF89: "\u30CE", \uFF8A: "\u30CF", \uFF8A\uFF9E: "\u30D0", \uFF8A\uFF9F: "\u30D1", \uFF8B: "\u30D2", \uFF8B\uFF9E: "\u30D3", \uFF8B\uFF9F: "\u30D4", \uFF8C: "\u30D5", \uFF8C\uFF9E: "\u30D6", \uFF8C\uFF9F: "\u30D7", \uFF8D: "\u30D8", \uFF8D\uFF9E: "\u30D9", \uFF8D\uFF9F: "\u30DA", \uFF8E: "\u30DB", \uFF8E\uFF9E: "\u30DC", \uFF8E\uFF9F: "\u30DD", \uFF8F: "\u30DE", \uFF90: "\u30DF", \uFF91: "\u30E0", \uFF92: "\u30E1", \uFF93: "\u30E2", \uFF94: "\u30E4", \uFF95: "\u30E6", \uFF96: "\u30E8", \uFF97: "\u30E9", \uFF98: "\u30EA", \uFF99: "\u30EB", \uFF9A: "\u30EC", \uFF9B: "\u30ED", \uFF9C: "\u30EF", \uFF9C\uFF9E: "\u30F7", \uFF9D: "\u30F3" }; var defaultFormat = (value) => { value = value.replace( /[!-~]/g, (v) => String.fromCharCode(v.charCodeAt(0) - 65248) ); const reg = new RegExp("(" + Object.keys(kanaMap).join("|") + ")", "g"); value = value.replace(reg, (v) => kanaMap[v]).replace(/゙/g, "\u309B").replace(/゚/g, "\u309C"); value = value.toUpperCase(); return value; }; var flattenItems = (items) => { const filterItems = (items2) => items2.map((item) => { var _a; const { disabled, focusable } = item; const trulyDisabled = !!disabled && !focusable; if (trulyDisabled) return; if ("items" in item) { return filterItems((_a = item.items) != null ? _a : []); } else { return item; } }).filter(Boolean); return filterItems(items).flat(Infinity); }; var useAutocomplete = (props) => { var _a; const { allowCreate = false, allowFree = false, animation, boundary, children, closeDelay, closeOnBlur = true, closeOnEsc = true, closeOnSelect = true, defaultIsOpen, defaultOpen, defaultValue, duration = 0.2, emptyMessage = "No results found", eventListeners, flip, format = defaultFormat, gutter, insertPositionItem = "first", isLazy, isOpen, items, lazy = isLazy, lazyBehavior, matchWidth = true, maxSelectValues, modifiers, offset, omitSelectedValues = false, open: openProp, openDelay, placeholder, placement = "bottom-start", preventOverflow, strategy, value: valueProp, optionProps, onChange: onChangeProp, onClose: onCloseProp, onCreate: onCreateProp, onKeyDown: onKeyDownProp, onOpen: onOpenProp, onSearch: onSearchProp, ...rest } = useFormControlProps(props); const descendants = useAutocompleteDescendants(); const containerRef = useRef(null); const inputRef = useRef(null); const timeoutIds = useRef(/* @__PURE__ */ new Set([])); const compositionRef = useRef(false); const prevValue = useRef(void 0); const [resolvedItems, setResolvedItems] = useState(items ? JSON.parse(JSON.stringify(items)) : void 0); const [value, setValue] = useControllableState({ defaultValue, value: valueProp, onChange: onChangeProp }); const [label, setLabel] = useState(void 0); const [inputValue, setInputValue] = useState(""); const [focusedIndex, setFocusedIndex] = useState(-1); const [allSelected, setAllSelected] = useState(false); const [hit, setHit] = useState(true); const { open, onClose, onOpen: onInternalOpen } = useDisclosure({ defaultIsOpen, defaultOpen, isOpen, open: openProp, onClose: onCloseProp, onOpen: onOpenProp }); const { "aria-readonly": _ariaReadonly, onFocus: onFocusProp, ...formControlProps } = pickObject(rest, formControlProperties); const [containerProps, inputProps] = splitObject(rest, layoutStyleProperties); const focused = focusedIndex > -1; const create = focusedIndex === -2 && allowCreate; const multi = isArray(value); const emptyValue = !multi ? !value : !value.length; const [firstInsertPositionItem, secondInsertPositionItem] = useMemo(() => { if (isArray(insertPositionItem)) { return insertPositionItem; } else { return [insertPositionItem, "first"]; } }, [insertPositionItem]); if (allowCreate && !isUndefined(children)) { console.warn( `${!multi ? "Autocomplete" : "MultiAutocomplete"}: ${!multi ? "Autocomplete" : "MultiAutocomplete"} internally prefers 'children'. If 'allowCreate' is true, it will not be reflected correctly. If want to reflect, please set 'items' in props.` ); } const selectedValues = descendants.enabledValues( ({ node }) => { var _a2; return multi && value.includes((_a2 = node.dataset.value) != null ? _a2 : ""); } ); const selectedIndexes = selectedValues.map(({ index }) => index); const enabledValues = descendants.enabledValues( ({ index, node }) => "target" in node.dataset && !selectedIndexes.includes(index) ); const activedescendantId = (_a = descendants.value(focusedIndex)) == null ? void 0 : _a.node.id; const validChildren = getValidChildren(children); const computedChildren = useMemo( () => resolvedItems == null ? void 0 : resolvedItems.map((item, index) => { if ("value" in item) { const { label: label2, value: value2, ...props2 } = item; return /* @__PURE__ */ jsx(AutocompleteOption, { value: value2, ...props2, children: label2 }, index); } else if ("items" in item) { const { items: items2 = [], label: label2, ...props2 } = item; return /* @__PURE__ */ jsx( AutocompleteOptionGroup, { label: label2, ...props2, children: items2.map(({ label: label3, value: value2, ...props3 }, index2) => /* @__PURE__ */ jsx(AutocompleteOption, { value: value2, ...props3, children: label3 }, index2)) }, index ); } }).filter(Boolean), [resolvedItems] ); const empty = !validChildren.length && !(computedChildren == null ? void 0 : computedChildren.length); const onOpen = useCallback(() => { if (formControlProps.disabled || formControlProps.readOnly) return; if (!allowCreate && (empty || allSelected)) return; onInternalOpen(); if (inputRef.current) inputRef.current.focus(); }, [allowCreate, formControlProps, allSelected, empty, onInternalOpen]); const onFocusFirst = useCallback(() => { const id = setTimeout(() => { var _a2; if (empty || allSelected) return; const first = descendants.enabledFirstValue( ({ node }) => "target" in node.dataset ); if (!first) return; if (!multi || !omitSelectedValues) { setFocusedIndex(first.index); } else { if (selectedIndexes.includes(first.index)) { const enabledFirst = enabledValues[0]; setFocusedIndex((_a2 = enabledFirst == null ? void 0 : enabledFirst.index) != null ? _a2 : -1); } else { setFocusedIndex(first.index); } } }); timeoutIds.current.add(id); }, [ descendants, enabledValues, allSelected, empty, multi, omitSelectedValues, selectedIndexes ]); const onFocusLast = useCallback(() => { const id = setTimeout(() => { var _a2; if (empty || allSelected) return; const last = descendants.enabledLastValue( ({ node }) => "target" in node.dataset ); if (!last) return; if (!multi || !omitSelectedValues) { setFocusedIndex(last.index); } else { if (selectedIndexes.includes(last.index)) { const enabledLast = enabledValues.reverse()[0]; setFocusedIndex((_a2 = enabledLast == null ? void 0 : enabledLast.index) != null ? _a2 : -1); } else { setFocusedIndex(last.index); } } }); timeoutIds.current.add(id); }, [ descendants, enabledValues, allSelected, empty, multi, omitSelectedValues, selectedIndexes ]); const onFocusSelected = useCallback(() => { const id = setTimeout(() => { const values = descendants.enabledValues(); const selected = values.find( ({ node }) => { var _a2; return !multi ? node.dataset.value === value : value.includes((_a2 = node.dataset.value) != null ? _a2 : ""); } ); if (selected) setFocusedIndex(selected.index); }); timeoutIds.current.add(id); }, [descendants, multi, value]); const onFocusNext = useCallback( (index = focusedIndex) => { const id = setTimeout(() => { var _a2, _b; const next = descendants.enabledNextValue( index, ({ node }) => "target" in node.dataset ); if (!next) return; if (!multi || !omitSelectedValues) { setFocusedIndex(next.index); } else { if (selectedIndexes.includes(next.index)) { const enabledNext = (_a2 = enabledValues.find(({ index: index2 }) => next.index < index2)) != null ? _a2 : enabledValues[0]; setFocusedIndex((_b = enabledNext == null ? void 0 : enabledNext.index) != null ? _b : -1); } else { setFocusedIndex(next.index); } } }); timeoutIds.current.add(id); }, [ descendants, enabledValues, focusedIndex, multi, omitSelectedValues, selectedIndexes ] ); const onFocusPrev = useCallback( (index = focusedIndex) => { const id = setTimeout(() => { var _a2, _b; const prev = descendants.enabledPrevValue( index, ({ node }) => "target" in node.dataset ); if (!prev) return; if (!multi || !omitSelectedValues) { setFocusedIndex(prev.index); } else { if (selectedIndexes.includes(prev.index)) { const enabledPrev = (_a2 = enabledValues.reverse().find(({ index: index2 }) => index2 < prev.index)) != null ? _a2 : enabledValues[0]; setFocusedIndex((_b = enabledPrev == null ? void 0 : enabledPrev.index) != null ? _b : -1); } else { setFocusedIndex(prev.index); } } }); timeoutIds.current.add(id); }, [ descendants, enabledValues, focusedIndex, multi, omitSelectedValues, selectedIndexes ] ); const onFocusFirstOrSelected = emptyValue || omitSelectedValues ? onFocusFirst : onFocusSelected; const onFocusLastOrSelected = emptyValue || omitSelectedValues ? onFocusLast : onFocusSelected; const pickOptions = useCallback( (value2) => { const values = descendants.values(); let hit2 = false; let focused2 = false; values.forEach(({ index, node }) => { var _a2; if (format((_a2 = node.textContent) != null ? _a2 : "").includes(value2)) { hit2 = true; const disabled = "disabled" in node.dataset; node.dataset.target = ""; if (!focused2 && !disabled) { focused2 = true; setFocusedIndex(index); } } else { delete node.dataset.target; } }); setHit(hit2); }, [descendants, format] ); const rebirthOptions = useCallback( (runFocus = true) => { const values = descendants.values(); values.forEach(({ node }) => { node.dataset.target = ""; }); if (runFocus) onFocusFirst(); setHit(true); }, [descendants, onFocusFirst] ); const onChangeLabel = useCallback( (newValue) => { const values = descendants.values(); if (!values.length) return; const resolvedValues = isArray(newValue) ? newValue : [newValue]; const selectedLabel = resolvedValues.map((value2) => { var _a2, _b; const { node } = (_a2 = values.find(({ node: node2 }) => node2.dataset.value === value2)) != null ? _a2 : {}; if (node) { const { textContent } = (_b = Array.from(node.children).find( (child) => child.getAttribute("data-label") !== null )) != null ? _b : {}; return textContent; } else { return allowFree ? value2 : void 0; } }).filter((label2) => !isUndefined(label2)); setLabel(!multi ? selectedLabel[0] : selectedLabel); }, [allowFree, descendants, multi] ); const onChange = useCallback( (newValue, runRebirth = true) => { setValue((prev) => { let next; if (!isArray(prev)) { next = newValue; } else { const selected = prev.includes(newValue); if (!selected) { next = [...prev, newValue]; } else { next = prev.filter((value2) => value2 !== newValue); } } prevValue.current = next; return next; }); setInputValue(""); if (multi && runRebirth) rebirthOptions(false); }, [multi, rebirthOptions, setValue] ); const onSelect = useCallback(() => { var _a2, _b; let enabledValue = descendants.value(focusedIndex); if ("disabled" in ((_a2 = enabledValue == null ? void 0 : enabledValue.node.dataset) != null ? _a2 : {})) enabledValue = void 0; if (!enabledValue) return; const value2 = (_b = enabledValue.node.dataset.value) != null ? _b : ""; onChange(value2); if (closeOnSelect) onClose(); if (omitSelectedValues) onFocusNext(); }, [ closeOnSelect, descendants, focusedIndex, omitSelectedValues, onChange, onClose, onFocusNext ]); const onSearch = useCallback( (ev) => { if (!open) onOpen(); onSearchProp == null ? void 0 : onSearchProp(ev); const value2 = ev.target.value; const computedValue = format(value2); if (computedValue) { pickOptions(computedValue); } else { rebirthOptions(); } setInputValue(value2); }, [open, onOpen, format, onSearchProp, pickOptions, rebirthOptions] ); const onCompositionStart = useCallback(() => { compositionRef.current = true; }, []); const onCompositionEnd = useCallback(() => { compositionRef.current = false; }, []); const onCreate = useCallback(() => { var _a2, _b; const newItem = { label: inputValue, value: inputValue }; let newItems = []; if (resolvedItems) newItems = [...resolvedItems]; if (firstInsertPositionItem === "first") { newItems = [newItem, ...newItems]; } else if (firstInsertPositionItem === "last") { newItems = [...newItems, newItem]; } else { const i = newItems.findIndex( ({ label: label2 }) => label2 === firstInsertPositionItem ); const targetItem = newItems[i]; if (i !== -1 && targetItem && "items" in targetItem) { if (secondInsertPositionItem === "first") { targetItem.items = [newItem, ...(_a2 = targetItem.items) != null ? _a2 : []]; } else { targetItem.items = [...(_b = targetItem.items) != null ? _b : [], newItem]; } newItems[i] = targetItem; } else { console.warn( `${!multi ? "Autocomplete" : "MultiAutocomplete"}: '${firstInsertPositionItem}' specified in insertPositionItem does not exist in the option group.` ); } } setResolvedItems(newItems); onChange(inputValue); rebirthOptions(false); const index = flattenItems(newItems).findIndex( ({ value: value2 }) => value2 === inputValue ); setFocusedIndex(index); onCreateProp == null ? void 0 : onCreateProp(newItem, newItems); }, [ inputValue, resolvedItems, firstInsertPositionItem, onChange, rebirthOptions, onCreateProp, secondInsertPositionItem, multi ]); const onClick = useCallback(() => { if (open) { if (inputRef.current) inputRef.current.focus(); } else { onOpen(); onFocusFirstOrSelected(); } }, [open, onFocusFirstOrSelected, onOpen]); const onFocus = useCallback(() => { if (open) return; onOpen(); onFocusFirstOrSelected(); }, [open, onFocusFirstOrSelected, onOpen]); const onBlur = useCallback( (ev) => { const relatedTarget = getEventRelatedTarget(ev); if (isContains(containerRef.current, relatedTarget)) return; if (!closeOnBlur && hit) return; if (allowFree && !!inputValue) onChange(inputValue, false); setInputValue(""); if (open) onClose(); }, [closeOnBlur, hit, open, inputValue, allowFree, onClose, onChange] ); const onDelete = useCallback(() => { if (!multi) { onChange(""); } else { onChange(value[value.length - 1]); } if (!open) onFocus(); }, [multi, open, onChange, onFocus, value]); const onClear = useCallback( (ev) => { ev.stopPropagation(); prevValue.current = []; setValue([]); setLabel(void 0); setInputValue(""); rebirthOptions(); if (open && inputRef.current) inputRef.current.focus(); }, [open, setLabel, setInputValue, setValue, rebirthOptions] ); const onKeyDown = useCallback( (ev) => { if (ev.key === " ") ev.key = ev.code; if (formControlProps.disabled || formControlProps.readOnly) return; if (compositionRef.current) return; const enabledDelete = label === inputValue || !inputValue.length; const actions = { ArrowDown: focused ? () => onFocusNext() : !open ? funcAll(onOpen, onFocusFirstOrSelected) : void 0, ArrowUp: focused ? () => onFocusPrev() : !open ? funcAll(onOpen, onFocusLastOrSelected) : void 0, Backspace: !emptyValue && enabledDelete ? onDelete : void 0, End: open ? onFocusLast : void 0, Enter: create ? onCreate : focused ? onSelect : !open ? funcAll(onOpen, onFocusFirstOrSelected) : allowFree && multi ? () => { if (inputValue) onChange(inputValue); setFocusedIndex(0); } : void 0, Escape: closeOnEsc ? onClose : void 0, Home: open ? onFocusFirst : void 0, Space: create ? onCreate : focused ? onSelect : !open ? funcAll(onOpen, onFocusFirstOrSelected) : void 0 }; const action = actions[ev.key]; if (!action) return; ev.preventDefault(); ev.stopPropagation(); action(ev); }, [ allowFree, formControlProps, label, inputValue, onOpen, focused, multi, onFocusFirstOrSelected, onFocusNext, onFocusLastOrSelected, onFocusPrev, create, onCreate, onSelect, open, onFocusFirst, onFocusLast, closeOnEsc, onClose, emptyValue, onDelete, onChange ] ); useEffect(() => { if (!multi) return; if (!omitSelectedValues && isUndefined(maxSelectValues)) return; const all = value.length > 0 && value.length === descendants.count(); const max = value.length === maxSelectValues; if (all || max) { onClose(); setAllSelected(true); } else { setAllSelected(false); } }, [omitSelectedValues, value, descendants, multi, onClose, maxSelectValues]); useSafeLayoutEffect(() => { onChangeLabel(value); }, [value]); useUpdateEffect(() => { if (open || allowFree) return; setFocusedIndex(-1); setInputValue(""); }, [open]); useUpdateEffect(() => { if (!hit) setFocusedIndex(-2); }, [hit]); useUpdateEffect(() => { setResolvedItems(items ? JSON.parse(JSON.stringify(items)) : void 0); }, [items]); useUnmountEffect(() => { timeoutIds.current.forEach((id) => clearTimeout(id)); timeoutIds.current.clear(); }); useOutsideClick({ ref: containerRef, enabled: open && (closeOnBlur || !hit), handler: onClose }); const getPopoverProps = useCallback( (props2) => ({ animation, boundary, closeDelay, closeOnBlur, duration, eventListeners, flip, gutter, lazy, lazyBehavior, matchWidth, modifiers, offset, openDelay, placement, preventOverflow, strategy, ...props2, closeOnButton: false, open, trigger: "never", onClose, onOpen }), [ closeOnBlur, openDelay, closeDelay, lazy, lazyBehavior, animation, duration, offset, gutter, preventOverflow, flip, matchWidth, boundary, eventListeners, strategy, placement, modifiers, open, onOpen, onClose ] ); const getContainerProps = useCallback( (props2 = {}, ref = null) => ({ ref: mergeRefs(containerRef, ref), ...containerProps, ...props2, ...formControlProps, onBlur: handlerAll(props2.onBlur, rest.onBlur, onBlur), onClick: handlerAll(props2.onClick, rest.onClick, onClick) }), [containerProps, formControlProps, onBlur, onClick, rest] ); const getFieldProps = useCallback( (props2 = {}, ref = null) => ({ ref, "aria-activedescendant": activedescendantId, "aria-autocomplete": "list", "aria-haspopup": "listbox", role: "combobox", tabIndex: -1, ...props2, ...formControlProps, "data-active": dataAttr(open), placeholder, onFocus: handlerAll(props2.onFocus, onFocusProp, onFocus), onKeyDown: handlerAll(props2.onKeyDown, onKeyDownProp, onKeyDown) }), [ activedescendantId, formControlProps, placeholder, open, onFocusProp, onFocus, onKeyDownProp, onKeyDown ] ); return { allowCreate, allowFree, allSelected, children: children != null ? children : computedChildren, closeOnSelect, descendants, empty, emptyMessage, focusedIndex, hit, inputRef, inputValue, label, omitSelectedValues, open, pickOptions, rebirthOptions, setFocusedIndex, value, formControlProps, getContainerProps, getFieldProps, getPopoverProps, inputProps, optionProps, onChange, onClear, onClose, onCompositionEnd, onCompositionStart, onCreate, onFocusFirst, onFocusLast, onFocusNext, onFocusPrev, onFocusSelected, onOpen, onSearch }; }; var useAutocompleteInput = () => { const { allSelected, inputRef, formControlProps, inputProps, onCompositionEnd, onCompositionStart, onSearch } = useAutocompleteContext(); useUpdateEffect(() => { if (allSelected && inputRef.current) inputRef.current.blur(); }, [allSelected]); const getInputProps = useCallback( (props = {}, ref = null) => { return { ref: mergeRefs(inputRef, ref), ...formControlProps, autoCapitalize: "none", autoComplete: "off", spellCheck: "false", tabIndex: allSelected ? -1 : 0, ...inputProps, ...props, cursor: formControlProps.readOnly ? "default" : "text", pointerEvents: formControlProps.disabled || allSelected ? "none" : "auto", onChange: handlerAll(props.onChange, onSearch), onCompositionEnd: handlerAll( props.onCompositionEnd, inputProps.onCompositionEnd, onCompositionEnd ), onCompositionStart: handlerAll( props.onCompositionStart, inputProps.onCompositionStart, onCompositionStart ) }; }, [ inputProps, inputRef, formControlProps, allSelected, onSearch, onCompositionStart, onCompositionEnd ] ); return { getInputProps }; }; export { useAutocomplete, useAutocompleteInput }; //# sourceMappingURL=chunk-NUGZZ2IV.mjs.map