UNPKG

@drivy/cobalt

Version:

Opinionated design system for Drivy's projects.

188 lines (185 loc) 9.81 kB
import React, { forwardRef, useState, useRef, useMemo, useEffect, useImperativeHandle } from 'react'; import cx from 'classnames'; import { Icon } from '../../Icon/index.js'; import { createListCollection, useCombobox, Combobox, Portal } from '@ark-ui/react'; import { Hint } from '../Hint.js'; import { nanoid } from 'nanoid'; 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); 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, }, }); useImperativeHandle(ref, () => { return { query: combobox.inputValue, input: inputRef.current, open: () => { combobox.setOpen(true); }, focus: () => { combobox.focus(); }, clearValue: () => { combobox.clearValue(); setValue(""); setDefaultInputValue(""); onClearValue && onClearValue(); onQueryChange && 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]); useEffect(() => { focusOnInit && combobox.setOpen(collection.items.length > 0); }, []); const validItems = collection.items.filter((item) => item.value !== ignoreItemValue); useEffect(() => { validItems.length === 0 && combobox.setOpen(false); combobox.focused && collection.items.length > 0 && !combobox.open && combobox.setOpen(true); }, [collection.items.length]); const autocomplete = (React.createElement(Combobox.RootProvider, { value: combobox, className: cx("cobalt-Autocomplete", className, { "cobalt-Autocomplete--empty": validItems.length === 0, }), key: "" + autocompleteKey }, label && (React.createElement(Combobox.Label, { className: "cobalt-FormField__Label" }, label)), React.createElement(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, }) }, React.createElement(Combobox.Input, { className: "cobalt-TextField__Input", ref: inputRef, disabled: disabled, defaultValue: defaultInputValue, ...inputProps, onKeyDown: (e) => { onKeyDown && 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 && (React.createElement(Icon, { source: icon, color: "primary", className: "cobalt-TextField__Icon" })), value.length > 0 && !disabled && (React.createElement(Combobox.ClearTrigger, { className: "cobalt-Autocomplete__clear-button", role: "clear", onClick: () => { setDefaultInputValue(""); setValue(""); setAutocompleteKey(!autocompleteKey); minQueryLength === 0 && setTimeout(() => { var _a; (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, 0); onClearValue && onClearValue(); onQueryChange && onQueryChange(""); } }, React.createElement(Icon, { source: "close", size: 16 })))), React.createElement(Portal, null, React.createElement(Combobox.Positioner, { className: "cobalt-Autocomplete__positioner" }, React.createElement(Combobox.Content, { className: cx("cobalt-Autocomplete__content", popoverClassName, { "cobalt-Autocomplete__content--empty": validItems.length === 0, }) }, validItems.map((item, index) => (React.createElement(Combobox.Item, { key: index, item: item, className: "cobalt-Autocomplete__item" }, renderItem ? (React.createElement("div", { onClick: (e) => { if (onSelectItem) { const processSelection = onSelectItem(item, combobox.inputValue); if (!processSelection) { e.preventDefault(); e.stopPropagation(); } } } }, renderItem(item, combobox.inputValue))) : (React.createElement("div", { className: cx("cobalt-Autocomplete__item-wrapper", { "cobalt-Autocomplete__item-wrapper--disabled": item.disabled, }) }, icon && (React.createElement("span", { className: "cobalt-Autocomplete__item-icon" }, React.createElement(Icon, { source: icon, color: "primary" }))), React.createElement(Combobox.ItemText, { className: "cobalt-Autocomplete__item-label" }, item.label), React.createElement(Combobox.ItemIndicator, { className: "cobalt-Autocomplete_selected-item-indicator" }, React.createElement(CheckIcon, { size: 16 })))))))))))); return label || hint ? (React.createElement("div", { className: cx("cobalt-FormField", { "cobalt-FormField--withHint": hint, "cobalt-FormField--fullWidth": fullWidth, }) }, autocomplete, hint && (React.createElement(Hint, { status: status }, React.createElement("span", { dangerouslySetInnerHTML: { __html: hint } }))))) : (React.createElement(React.Fragment, null, autocomplete)); }); Autocomplete.displayName = "Autocomplete"; export { Autocomplete }; //# sourceMappingURL=index.js.map