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