UNPKG

@drivy/cobalt

Version:

Opinionated design system for Drivy's projects.

184 lines (181 loc) 10.7 kB
import { jsxs, jsx } from 'react/jsx-runtime'; import { createListCollection, useCombobox, Combobox, Portal } from '@ark-ui/react'; import cx from 'classnames'; import { nanoid } from 'nanoid'; import { forwardRef, useState, useRef, useMemo, useEffect, useImperativeHandle } from 'react'; import { getA11yOnClick } from '../../../helpers/index.js'; import { useOnMountEffect } from '../../../hooks/useOnMountEffect.js'; import { Icon } from '../../Icon/index.js'; import { Hint } from '../Hint.js'; import CheckIcon from '../../Icon/__generated__/CheckIcon.js'; function sanitizeItem(item) { if (typeof item === "string") { // If the item is a string, create an object with label and value being the string return { label: item, value: item }; } else { // If the item is an object, ensure it has a label property const { value, label } = item; return { ...item, label: label !== null && label !== void 0 ? label : value }; } } function sanitizeItems(items) { return items.map((item) => sanitizeItem(item)); } const KEY_CODE_ENTER = 13; const ignoreItemValue = nanoid(); const _Autocomplete = forwardRef(({ id, className, label, hint, fullWidth, icon, status, focusOnInit = false, popoverClassName, minQueryLength = 1, onQueryChange, onSelectItem, renderItem, items, onKeyDown, disabled, defaultValue, onClearValue, allowCustomValue = true, ...inputProps }, ref) => { const [defaultInputValue, setDefaultInputValue] = useState(defaultValue); const [autocompleteKey, setAutocompleteKey] = useState(true); const isInputValidChangedProgrammatically = useRef(false); const sanitizedInitialItems = useMemo(() => sanitizeItems(items), [items]); const [value, setValue] = useState(defaultValue || ""); const [popoverItems, setPopoverItems] = useState(sanitizedInitialItems); // Allow to use the combobox without listening its changes const comboboxRef = useRef(null); const collection = useMemo(() => createListCollection({ items: popoverItems.length ? popoverItems : sanitizeItems([{ value: ignoreItemValue }]), // having an empty collection makes the component buggy. Best practice is to always have an "instruction" item when empty. This dummy item will not be displayed }), [popoverItems]); const handleInputChange = (details) => { if (isInputValidChangedProgrammatically.current) { isInputValidChangedProgrammatically.current = false; return; } onQueryChange ? onQueryChange(details.inputValue) : setPopoverItems(sanitizedInitialItems.filter((item) => item.label .toLowerCase() .includes(details.inputValue.toLowerCase()))); }; useEffect(() => { setPopoverItems(sanitizeItems(items)); }, [items]); const inputRef = useRef(null); const combobox = useCombobox({ ...(id ? { id } : {}), allowCustomValue: allowCustomValue, autoFocus: focusOnInit, value: [value], collection: collection, onInputValueChange: handleInputChange, openOnClick: minQueryLength === 0 && collection.items.length > 0, onValueChange: (details) => { isInputValidChangedProgrammatically.current = true; details.items[0] ? setValue(details.items[0].value) : setValue(""); }, onInteractOutside: () => { setTimeout(() => { var _a; return (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.blur(); }, 30); }, positioning: { gutter: -3, flip: false, }, }); useEffect(() => { comboboxRef.current = combobox; }, [combobox]); useImperativeHandle(ref, () => { return { query: combobox.inputValue, input: inputRef.current, open: () => { combobox.setOpen(true); }, focus: () => { combobox.focus(); }, clearValue: () => { combobox.clearValue(); setValue(""); setDefaultInputValue(""); onClearValue === null || onClearValue === void 0 ? void 0 : onClearValue(); onQueryChange === null || onQueryChange === void 0 ? void 0 : onQueryChange(""); }, setSelectedItem: (selectedItem) => { isInputValidChangedProgrammatically.current = true; const sanitizedItem = sanitizeItem(selectedItem); const existingItem = popoverItems.find((item) => item.value === sanitizedItem.value); if (!existingItem) { combobox.setOpen(false); setPopoverItems([]); setDefaultInputValue(sanitizedItem.value); setValue(sanitizedItem.value); setAutocompleteKey(!autocompleteKey); } else { setValue(sanitizedItem.value); combobox.setOpen(false); } }, }; }, [combobox, autocompleteKey, onClearValue, onQueryChange, popoverItems]); useOnMountEffect(() => { focusOnInit && combobox.setOpen(collection.items.length > 0); }); const validItems = useMemo(() => collection.items.filter((item) => item.value !== ignoreItemValue), [collection.items]); useEffect(() => { var _a, _b, _c; if (validItems.length) { if (!((_a = comboboxRef.current) === null || _a === void 0 ? void 0 : _a.open) && ((_b = comboboxRef.current) === null || _b === void 0 ? void 0 : _b.focused)) { comboboxRef.current.setOpen(true); } } else { if ((_c = comboboxRef.current) === null || _c === void 0 ? void 0 : _c.open) { comboboxRef.current.setOpen(false); } } }, [validItems]); const autocomplete = (jsxs(Combobox.RootProvider, { value: combobox, className: cx("cobalt-Autocomplete", className, { "cobalt-Autocomplete--empty": validItems.length === 0, }), children: [label && (jsx(Combobox.Label, { className: "cobalt-FormField__Label", children: label })), jsxs(Combobox.Control, { className: cx("cobalt-TextField", { "cobalt-TextField--error": status === "error", "cobalt-TextField--success": status === "success", "cobalt-TextField--withIcon": icon, "cobalt-TextField--withValue": inputRef.current && inputRef.current.value.length > 0, }), children: [jsx(Combobox.Input, { className: "cobalt-TextField__Input", ref: inputRef, disabled: disabled, defaultValue: defaultInputValue, ...inputProps, onKeyDown: (e) => { onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(e); if (onSelectItem && e.keyCode === KEY_CODE_ENTER && combobox.highlightedItem) { const processSelection = onSelectItem(combobox.highlightedItem, combobox.inputValue); if (!processSelection) { e.preventDefault(); e.stopPropagation(); } } } }), icon && (jsx(Icon, { source: icon, color: "primary", className: "cobalt-TextField__Icon" })), value.length > 0 && !disabled && (jsx(Combobox.ClearTrigger, { className: "cobalt-Autocomplete__clear-button", "data-testid": "clear", onClick: () => { setDefaultInputValue(""); setValue(""); setAutocompleteKey(!autocompleteKey); minQueryLength === 0 && setTimeout(() => { var _a; (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, 0); onClearValue === null || onClearValue === void 0 ? void 0 : onClearValue(); onQueryChange === null || onQueryChange === void 0 ? void 0 : onQueryChange(""); }, children: jsx(Icon, { source: "close", size: 16 }) }))] }), jsx(Portal, { children: jsx(Combobox.Positioner, { className: "cobalt-Autocomplete__positioner", children: jsx(Combobox.Content, { className: cx("cobalt-Autocomplete__content", popoverClassName, { "cobalt-Autocomplete__content--empty": validItems.length === 0, }), children: validItems.map((item, index) => (jsx(Combobox.Item, { item: item, className: "cobalt-Autocomplete__item", children: renderItem ? (jsx("div", { ...getA11yOnClick((e) => { if (onSelectItem) { const processSelection = onSelectItem(item, combobox.inputValue); if (!processSelection) { e.preventDefault(); e.stopPropagation(); } } }), children: renderItem(item, combobox.inputValue) })) : (jsxs("div", { className: cx("cobalt-Autocomplete__item-wrapper", { "cobalt-Autocomplete__item-wrapper--disabled": item.disabled, }), children: [icon && (jsx("span", { className: "cobalt-Autocomplete__item-icon", children: jsx(Icon, { source: icon, color: "primary" }) })), jsx(Combobox.ItemText, { className: "cobalt-Autocomplete__item-label", children: item.label }), jsx(Combobox.ItemIndicator, { className: "cobalt-Autocomplete_selected-item-indicator", children: jsx(CheckIcon, { size: 16 }) })] })) }, `${item.label}-${item.value}-${index}`))) }) }) })] }, `${autocompleteKey}`)); return label || hint ? (jsxs("div", { className: cx("cobalt-FormField", { "cobalt-FormField--withHint": hint, "cobalt-FormField--fullWidth": fullWidth, }), children: [autocomplete, hint && (jsx(Hint, { status: status, children: jsx("span", { dangerouslySetInnerHTML: { __html: hint } }) }))] })) : (autocomplete); }); _Autocomplete.displayName = "Autocomplete"; const Autocomplete = _Autocomplete; export { Autocomplete }; //# sourceMappingURL=index.js.map