@drivy/cobalt
Version:
Opinionated design system for Drivy's projects.
184 lines (181 loc) • 10.7 kB
JavaScript
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