UNPKG

@yamada-ui/react

Version:

React UI components of the Yamada, by the Yamada, for the Yamada built with React and Emotion

411 lines (407 loc) • 13.3 kB
"use client"; import { createContext as createContext$1 } from "../../utils/context.js"; import { isComposing, runKeyAction, visuallyHiddenAttributes } from "../../utils/dom.js"; import { useUpdateEffect } from "../../utils/effect.js"; import { mergeRefs } from "../../utils/ref.js"; import { utils_exports } from "../../utils/index.js"; import { useControllableState } from "../../hooks/use-controllable-state/index.js"; import { useI18n } from "../../providers/i18n-provider/i18n-provider.js"; import { useFieldProps } from "../field/use-field-props.js"; import { useCombobox, useComboboxItem } from "../../hooks/use-combobox/index.js"; import { cloneElement, isValidElement, useCallback, useMemo, useRef, useState } from "react"; import { jsxs } from "react/jsx-runtime"; //#region src/components/autocomplete/use-autocomplete.tsx const defaultRender = ({ count, focused, index, label, max, separator }) => { const last = count - 1 === index; return /* @__PURE__ */ jsxs("span", { style: { marginInlineEnd: "var(--gap)" }, children: [label, (!(0, utils_exports.isNumber)(max) || count < max) && focused || !last ? separator : null] }); }; const getInputValue = (item) => (0, utils_exports.isString)(item?.label) ? item.label : item?.query ?? ""; const defaultFilter = (inputValue, items, matcher) => { if (!inputValue.length) return items; return items.map((item) => { if ("items" in item) { const items$1 = item.items.filter((item$1) => { if ("query" in item$1) return matcher(inputValue, item$1.query); else if ((0, utils_exports.isString)(item$1.label)) return matcher(inputValue, item$1.label); }); if (items$1.length) return { ...item, items: items$1 }; } else if ("query" in item) { if (matcher(inputValue, item.query)) return item; } else if ((0, utils_exports.isString)(item.label)) { if (matcher(inputValue, item.label)) return item; } }).filter(Boolean); }; const defaultMatcher = (input, target) => target?.toLowerCase().includes(input.toLowerCase()) ?? false; const [AutocompleteContext, useAutocompleteContext] = createContext$1({ name: "AutocompleteContext" }); const useAutocomplete = (props = {}) => { const { t } = useI18n("autocomplete"); const { props: { id, ref, name, allowCustomValue = false, closeOnChange = false, multiple = false, closeOnSelect = !multiple, defaultInputValue, defaultValue = multiple ? [] : "", disabled, emptyMessage = t("No results found"), filter = defaultFilter, focusOnClear = true, inputValue: inputValueProp, items = [], matcher = defaultMatcher, max, openOnChange = true, openOnClick = true, openOnFocus = true, placeholder, readOnly, render = defaultRender, required, separator = ",", value: valueProp, onChange: onChangeProp, onInputChange: onInputChangeProp,...rest }, ariaProps, dataProps, eventProps } = useFieldProps(props); const fieldRef = useRef(null); const contentRef = useRef(null); const inputRef = useRef(null); const focusByClickRef = useRef(false); const valueMap = useMemo(() => { const valueMap$1 = {}; items.forEach((item) => { if ("items" in item) item.items.forEach((item$1) => { item$1.value ??= (0, utils_exports.isString)(item$1.label) ? item$1.label : void 0; if (!(0, utils_exports.isUndefined)(item$1.value)) valueMap$1[item$1.value] = item$1; }); else { item.value ??= (0, utils_exports.isString)(item.label) ? item.label : void 0; if (!(0, utils_exports.isUndefined)(item.value)) valueMap$1[item.value] = item; } }); return valueMap$1; }, [items]); const [focused, setFocused] = useState(false); const [value, setValue] = useControllableState({ defaultValue, value: valueProp, onChange: onChangeProp }); const [inputValue, setInputValue] = useControllableState({ defaultValue: defaultInputValue ?? getInputValue((0, utils_exports.isArray)(value) ? void 0 : valueMap[value]), value: inputValueProp, onChange: onInputChangeProp }); const onChange = useCallback((selectedValue) => { setValue((prev) => { if ((0, utils_exports.isArray)(prev)) if (prev.includes(selectedValue)) return prev.filter((prevValue) => prevValue !== selectedValue); else if (!(0, utils_exports.isNumber)(max) || prev.length < max) return [...prev, selectedValue]; else return prev; else return selectedValue; }); if ((0, utils_exports.isArray)(value)) setInputValue(""); else { const item = valueMap[selectedValue]; setInputValue(getInputValue(item)); } }, [ max, setInputValue, setValue, value, valueMap ]); const { activeDescendant, descendants, interactive, open, getContentProps: getComboboxContentProps, getSeparatorProps, getTriggerProps, popoverProps, onActiveDescendant, onClose, onOpen, onOpenWithActiveDescendant, onSelect } = useCombobox({ closeOnSelect, disabled, initialFocusValue: (0, utils_exports.isArray)(value) ? value[0] : value, openOnClick: false, openOnEnter: false, openOnSpace: false, readOnly, selectFocusRef: inputRef, selectOnSpace: false, onChange, ...ariaProps, ...dataProps, ...eventProps, ...rest }); const filteredItems = useMemo(() => { if (!items.length) return []; return filter(inputValue, items, matcher); }, [ filter, inputValue, items, matcher ]); const resolvedItems = useMemo(() => { return filteredItems.length ? filteredItems : [{ "data-empty": "", label: emptyMessage }]; }, [filteredItems, emptyMessage]); const empty = useMemo(() => !resolvedItems.filter(({ hidden }) => !hidden).length, [resolvedItems]); const children = useMemo(() => { if (!(0, utils_exports.isArray)(value)) return null; const count = value.length; return value.map((value$1, index) => { const item = valueMap[value$1] ?? { label: value$1, value: value$1 }; const onClear$1 = (ev) => { ev?.preventDefault(); ev?.stopPropagation(); if (item.value) onChange(item.value); }; const component = render({ count, focused, index, max, separator, onClear: onClear$1, ...item }); if (isValidElement(component)) return cloneElement(component, { ...component.props, key: index }); else return component; }); }, [ focused, max, onChange, render, separator, value, valueMap ]); const hasValues = (0, utils_exports.isArray)(value) && !!value.length; const onInputChange = useCallback((ev) => { if ((0, utils_exports.isArray)(value) && value.length === max) return; if ((0, utils_exports.runIfFn)(closeOnChange, ev)) onClose(); else if ((0, utils_exports.runIfFn)(openOnChange, ev)) onOpen(); activeDescendant.current = null; const inputValue$1 = ev.target.value; setInputValue(inputValue$1); if (inputValue$1.length || (0, utils_exports.isArray)(value)) return; setValue(""); }, [ activeDescendant, closeOnChange, max, onClose, onOpen, openOnChange, setInputValue, setValue, value ]); const onKeyDown = useCallback((ev) => { if (disabled || isComposing(ev)) return; const inputValue$1 = (0, utils_exports.cast)(ev.target).value; runKeyAction(ev, { Backspace: (ev$1) => { if (!(0, utils_exports.isArray)(value)) return; if (!!inputValue$1.length) return; ev$1.preventDefault(); setValue((prev) => prev.slice(0, -1)); }, Enter: (ev$1) => { if (!open || !inputValue$1.length || activeDescendant.current) return; const item = filteredItems[0]; if (!item) { if (!allowCustomValue || !(0, utils_exports.isArray)(value)) return; ev$1.preventDefault(); onSelect(inputValue$1); } else { ev$1.preventDefault(); if ("items" in item) onSelect(item.items[0]?.value); else onSelect(item.value); } } }, { preventDefault: false }); }, [ activeDescendant, allowCustomValue, disabled, filteredItems, onSelect, open, setValue, value ]); const onClick = useCallback(() => { if (!interactive) return; focusByClickRef.current = true; inputRef.current?.focus(); if (openOnClick) onOpenWithActiveDescendant(descendants.enabledFirstValue); }, [ descendants, interactive, onOpenWithActiveDescendant, openOnClick ]); const onMouseDown = useCallback((ev) => { if (!openOnFocus) return; ev.preventDefault(); ev.stopPropagation(); }, [openOnFocus]); const onFocus = useCallback((ev) => { ev.preventDefault(); ev.stopPropagation(); setFocused(true); if (openOnFocus && !focusByClickRef.current) onOpenWithActiveDescendant(descendants.enabledFirstValue); focusByClickRef.current = false; }, [ openOnFocus, onOpenWithActiveDescendant, descendants.enabledFirstValue ]); const onBlur = useCallback((ev) => { setFocused(false); if ((0, utils_exports.contains)(fieldRef.current, ev.relatedTarget) || (0, utils_exports.contains)(contentRef.current, ev.relatedTarget)) ev.preventDefault(); else if ((0, utils_exports.isArray)(value)) setInputValue(""); else if (allowCustomValue) { if (inputValue) setValue(inputValue); } else { const item = valueMap[value]; setInputValue(getInputValue(item)); } }, [ allowCustomValue, inputValue, setInputValue, setValue, value, valueMap ]); const onClear = useCallback(() => { if (!interactive) return; setValue((prev) => (0, utils_exports.isArray)(prev) ? [] : ""); setInputValue(""); if (focusOnClear) inputRef.current?.focus(); }, [ focusOnClear, interactive, setInputValue, setValue ]); useUpdateEffect(() => { if ((0, utils_exports.isArray)(valueProp)) return; setInputValue(getInputValue(valueProp ? valueMap[valueProp] : void 0)); }, [valueProp]); const getRootProps = useCallback((props$1) => ({ ...dataProps, ...props$1 }), [dataProps]); const getFieldProps = useCallback(({ ref: ref$1,...props$1 } = {}) => getTriggerProps({ ref: mergeRefs(ref$1, fieldRef), tabIndex: -1, ...props$1, onClick: (0, utils_exports.handlerAll)(props$1.onClick, onClick) }), [getTriggerProps, onClick]); const getInputProps = useCallback((props$1 = {}) => ({ id, ref: mergeRefs(props$1.ref, ref, inputRef), name, style: { ...!focused && (0, utils_exports.isArray)(value) && !!value.length ? visuallyHiddenAttributes.style : {}, ...props$1.style }, "data-max": (0, utils_exports.dataAttr)((0, utils_exports.isArray)(value) && (0, utils_exports.isNumber)(max) && value.length >= max), autoCapitalize: "off", autoComplete: "off", autoCorrect: "off", disabled, placeholder: hasValues ? void 0 : placeholder, readOnly, required, spellCheck: false, value: inputValue, ...dataProps, ...props$1, onBlur: (0, utils_exports.handlerAll)(props$1.onBlur, onBlur), onChange: (0, utils_exports.handlerAll)(props$1.onChange, onInputChange), onFocus: (0, utils_exports.handlerAll)(props$1.onFocus, onFocus), onKeyDown: (0, utils_exports.handlerAll)(props$1.onKeyDown, onKeyDown), onMouseDown: (0, utils_exports.handlerAll)(props$1.onMouseDown, onMouseDown) }), [ dataProps, disabled, focused, hasValues, id, inputValue, max, name, onBlur, onFocus, onInputChange, onKeyDown, onMouseDown, placeholder, readOnly, ref, required, value ]); const getContentProps = useCallback(({ ref: ref$1,...props$1 } = {}) => getComboboxContentProps({ ref: mergeRefs(ref$1, contentRef), hidden: empty, ...props$1 }), [empty, getComboboxContentProps]); const getIconProps = useCallback((props$1) => ({ ...dataProps, ...props$1 }), [dataProps]); return { children, descendants, inputValue, interactive, items: resolvedItems, max, open, setInputValue, setValue, value, valueMap, getClearIconProps: useCallback((props$1 = {}) => getIconProps({ "aria-disabled": (0, utils_exports.ariaAttr)(!interactive), "aria-label": t("Clear value"), role: "button", tabIndex: interactive ? 0 : -1, ...props$1, onClick: (0, utils_exports.handlerAll)(props$1.onClick, onClear), onKeyDown: (0, utils_exports.handlerAll)(props$1.onKeyDown, (ev) => runKeyAction(ev, { Enter: onClear, Space: onClear })) }), [ getIconProps, interactive, onClear, t ]), getContentProps, getFieldProps, getIconProps, getInputProps, getRootProps, getSeparatorProps, popoverProps, onActiveDescendant, onChange, onClose, onInputChange, onOpen, onSelect }; }; const useAutocompleteOption = ({ children, closeOnSelect, disabled, hidden, value,...rest } = {}) => { const { max, value: selectedValue } = useAutocompleteContext(); value ??= (0, utils_exports.isString)(children) ? children : void 0; const selected = (0, utils_exports.isArray)(selectedValue) ? !(0, utils_exports.isUndefined)(value) && selectedValue.includes(value) : selectedValue === value; const completed = (0, utils_exports.isNumber)(max) && (0, utils_exports.isArray)(selectedValue) && selectedValue.length >= max; const { getIndicatorProps, getItemProps } = useComboboxItem({ children, closeOnSelect, disabled: disabled || hidden || completed && !selected, hidden, selected, value, ...rest }); return { getIndicatorProps, getOptionProps: useCallback((props = {}) => getItemProps(props), [getItemProps]) }; }; //#endregion export { AutocompleteContext, useAutocomplete, useAutocompleteContext, useAutocompleteOption }; //# sourceMappingURL=use-autocomplete.js.map