UNPKG

@blinkstrike/chakra-ui-autocomplete

Version:

An accessible autocomplete utility library built for chakra UI

423 lines (378 loc) 14.9 kB
import React, { useState, useRef, useEffect } from 'react'; import { useMultipleSelection, useCombobox } from 'downshift'; import { matchSorter } from 'match-sorter'; import Highlighter from 'react-highlight-words/index'; import useDeepCompareEffect from 'react-use/lib/useDeepCompareEffect'; import { useColorMode, Stack, FormLabel, Tag, TagLabel, TagCloseButton, Input, Button, Text, Box, List, ListItem, ListIcon } from '@chakra-ui/react'; import { ArrowDownIcon, CheckCircleIcon } from '@chakra-ui/icons'; function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } /** * Default option filter function to match and sort items based on label and value. */ function defaultOptionFilterFunc(items, inputValue) { return matchSorter(items, inputValue, { keys: ['value', 'label'] }); } /** * Default renderer for creating a new item. */ function defaultCreateItemRenderer(value) { return React.createElement(Text, null, React.createElement(Box, { as: "span" }, "Create"), ' ', React.createElement(Box, { as: "span", bg: "yellow.300", fontWeight: "bold" }, "\"", value, "\"")); } /** * Chakra UI Autocomplete Component. * * @component * @example * // Basic usage * <CUIAutoComplete * items={[]} * placeholder="Search..." * hideToggleButton={true} // Hide the dropdown button * label="Select or Create Items" * onCreateItem={(item) => console.log('Created:', item)} * onClearAll={() => console.log('Cleared all items')} * clearAll={true} // Enable the clear all button * /> * * @param {CUIAutoCompleteProps} props - Component properties. * @returns {React.ReactElement} - Autocomplete component. */ var CUIAutoComplete = function CUIAutoComplete(props) { var items = props.items, _props$optionFilterFu = props.optionFilterFunc, optionFilterFunc = _props$optionFilterFu === void 0 ? defaultOptionFilterFunc : _props$optionFilterFu, itemRenderer = props.itemRenderer, _props$highlightItemB = props.highlightItemBg, highlightItemBg = _props$highlightItemB === void 0 ? 'gray.100' : _props$highlightItemB, placeholder = props.placeholder, label = props.label, listStyleProps = props.listStyleProps, labelStyleProps = props.labelStyleProps, inputStyleProps = props.inputStyleProps, toggleButtonStyleProps = props.toggleButtonStyleProps, tagStyleProps = props.tagStyleProps, selectedIconProps = props.selectedIconProps, onClearAll = props.onClearAll, _props$clearAll = props.clearAll, clearAll = _props$clearAll === void 0 ? false : _props$clearAll, _props$keyboardShortc = props.keyboardShortcuts, keyboardShortcuts = _props$keyboardShortc === void 0 ? true : _props$keyboardShortc, onCreateItem = props.onCreateItem, CustomIcon = props.icon, _props$hideToggleButt = props.hideToggleButton, hideToggleButton = _props$hideToggleButt === void 0 ? false : _props$hideToggleButt, _props$disableCreateI = props.disableCreateItem, disableCreateItem = _props$disableCreateI === void 0 ? false : _props$disableCreateI, _props$createItemRend = props.createItemRenderer, createItemRenderer = _props$createItemRend === void 0 ? defaultCreateItemRenderer : _props$createItemRend, renderCustomInput = props.renderCustomInput, downshiftProps = _objectWithoutPropertiesLoose(props, ["items", "optionFilterFunc", "itemRenderer", "highlightItemBg", "placeholder", "label", "listStyleProps", "labelStyleProps", "inputStyleProps", "toggleButtonStyleProps", "tagStyleProps", "selectedIconProps", "listItemStyleProps", "onClearAll", "clearAll", "keyboardShortcuts", "onCreateItem", "icon", "hideToggleButton", "disableCreateItem", "createItemRenderer", "renderCustomInput"]); var _useState = useState(false), isCreating = _useState[0], setIsCreating = _useState[1]; var _useState2 = useState(''), inputValue = _useState2[0], setInputValue = _useState2[1]; var _useState3 = useState(items), inputItems = _useState3[0], setInputItems = _useState3[1]; var _useState4 = useState(''), error = _useState4[0], setError = _useState4[1]; var disclosureRef = useRef(null); var _useColorMode = useColorMode(), colorMode = _useColorMode.colorMode; // Check the color mode for the CUI instance (light or dark) var borderColor = colorMode === 'dark' ? 'whiteAlpha.400' : 'gray.300'; var _useMultipleSelection = useMultipleSelection(downshiftProps), getSelectedItemProps = _useMultipleSelection.getSelectedItemProps, getDropdownProps = _useMultipleSelection.getDropdownProps, addSelectedItem = _useMultipleSelection.addSelectedItem, removeSelectedItem = _useMultipleSelection.removeSelectedItem, selectedItems = _useMultipleSelection.selectedItems; var selectedItemValues = selectedItems.map(function (item) { return item.value; }); var _useCombobox = useCombobox({ inputValue: inputValue, selectedItem: undefined, items: inputItems, onInputValueChange: function onInputValueChange(_ref) { var inputValue = _ref.inputValue, selectedItem = _ref.selectedItem; var filteredItems = optionFilterFunc(items, inputValue || ''); if (isCreating && filteredItems.length > 0) { setIsCreating(false); } if (!selectedItem) { setInputItems(filteredItems); } }, stateReducer: function stateReducer(state, actionAndChanges) { var changes = actionAndChanges.changes, type = actionAndChanges.type; switch (type) { case useCombobox.stateChangeTypes.InputBlur: return _extends({}, changes, { isOpen: false }); case useCombobox.stateChangeTypes.InputKeyDownEnter: case useCombobox.stateChangeTypes.ItemClick: return _extends({}, changes, { highlightedIndex: state.highlightedIndex, inputValue: inputValue, isOpen: true }); case useCombobox.stateChangeTypes.FunctionSelectItem: return _extends({}, changes, { inputValue: inputValue }); default: return changes; } }, onStateChange: function onStateChange(_ref2) { var inputValue = _ref2.inputValue, type = _ref2.type, selectedItem = _ref2.selectedItem; switch (type) { case useCombobox.stateChangeTypes.InputChange: setInputValue(inputValue || ''); break; case useCombobox.stateChangeTypes.InputKeyDownEnter: case useCombobox.stateChangeTypes.ItemClick: if (selectedItem) { // Validate input value if (selectedItemValues.includes(selectedItem.value)) { setError('Item already selected.'); } else { setError(''); } if (selectedItemValues.includes(selectedItem.value)) { removeSelectedItem(selectedItem); } else { if (onCreateItem && isCreating) { onCreateItem(selectedItem); setIsCreating(false); setInputItems(items); setInputValue(''); } else { addSelectedItem(selectedItem); } } selectItem(null); } break; } } }), isOpen = _useCombobox.isOpen, getToggleButtonProps = _useCombobox.getToggleButtonProps, getLabelProps = _useCombobox.getLabelProps, getMenuProps = _useCombobox.getMenuProps, getInputProps = _useCombobox.getInputProps, highlightedIndex = _useCombobox.highlightedIndex, getItemProps = _useCombobox.getItemProps, openMenu = _useCombobox.openMenu, selectItem = _useCombobox.selectItem, setHighlightedIndex = _useCombobox.setHighlightedIndex; var clearSelection = function clearSelection() { setInputValue(''); setInputItems(items); setHighlightedIndex(0); selectItem(null); onClearAll && onClearAll(); }; useEffect(function () { if (inputItems.length === 0 && !disableCreateItem) { setIsCreating(true); setInputItems([{ label: "" + inputValue, value: inputValue }]); setHighlightedIndex(0); } }, [inputItems, setIsCreating, setHighlightedIndex, inputValue, disableCreateItem]); useDeepCompareEffect(function () { setInputItems(items); }, [items]); function defaultItemRenderer(selected) { return selected.label; } var handleKeyDown = function handleKeyDown(e) { if (keyboardShortcuts === false) return; if (e.key === 'Escape') { // Handle escape key (e.g., close dropdown) if (isOpen) { // Close the dropdown setInputValue(''); setIsCreating(false); } } else if (e.key === 'Enter') { // Handle enter key (e.g., select highlighted item or create new item) if (highlightedIndex !== null) { var selectedItem = inputItems[highlightedIndex]; if (selectedItem) { if (selectedItemValues.includes(selectedItem.value)) { removeSelectedItem(selectedItem); } else { if (onCreateItem && isCreating) { onCreateItem(selectedItem); setIsCreating(false); setInputItems(items); setInputValue(''); } else { addSelectedItem(selectedItem); } } } } else if (isCreating) { // Create a new item with the current input value var newItem = { label: inputValue, value: inputValue }; if (onCreateItem) { onCreateItem(newItem); setInputValue(''); } } } else if (e.key === 'ArrowDown') { // Handle arrow down key (e.g., navigate to the next item) //@ts-ignore setHighlightedIndex(function (prevIndex) { return prevIndex === null || prevIndex === inputItems.length - 1 ? 0 : prevIndex + 1; }); } else if (e.key === 'ArrowUp') { // Handle arrow up key (e.g., navigate to the previous item) //@ts-ignore setHighlightedIndex(function (prevIndex) { return prevIndex === null || prevIndex === 0 ? inputItems.length - 1 : prevIndex - 1; }); } else if (e.key === 'Tab') { // Handle tab key (e.g., close dropdown if no item is highlighted) if (isOpen && highlightedIndex === null) { // Close the dropdown setInputValue(''); setIsCreating(false); } } }; return React.createElement(Stack, null, React.createElement(FormLabel, Object.assign({}, getLabelProps(_extends({}, labelStyleProps))), label), selectedItems && React.createElement(Stack, { spacing: 2, isInline: true, flexWrap: "wrap" }, selectedItems.map(function (selectedItem, index) { return React.createElement(Tag, Object.assign({ mb: 1 }, tagStyleProps, { key: "selected-item-" + index }, getSelectedItemProps({ selectedItem: selectedItem, index: index })), React.createElement(TagLabel, null, selectedItem.label), React.createElement(TagCloseButton, { onClick: function onClick(e) { e.stopPropagation(); removeSelectedItem(selectedItem); }, "aria-label": "Remove menu selection badge" })); })), React.createElement(Stack, Object.assign({}, getInputProps({ onFocus: function onFocus() { return openMenu(); } })), renderCustomInput ? renderCustomInput(_extends({}, inputStyleProps, getInputProps(getDropdownProps({ placeholder: placeholder, onClick: isOpen ? function () {} : openMenu, onFocus: isOpen ? function () {} : openMenu, ref: disclosureRef }))), _extends({}, toggleButtonStyleProps, getToggleButtonProps(), { 'aria-label': 'toggle menu' })) : React.createElement(React.Fragment, null, React.createElement(Input, Object.assign({}, inputStyleProps, getInputProps(getDropdownProps({ placeholder: placeholder, onClick: isOpen ? function () {} : openMenu, onFocus: isOpen ? function () {} : openMenu, onKeyDown: handleKeyDown, ref: disclosureRef })))), !hideToggleButton && React.createElement(Button, Object.assign({}, toggleButtonStyleProps, getToggleButtonProps(), { "aria-label": "toggle menu" }), React.createElement(ArrowDownIcon, null)))), clearAll && selectedItems.length > 0 && React.createElement(Button, { onClick: clearSelection, variant: "link", color: "blue.500" }, "Clear All"), error && React.createElement(Text, { color: "red.500" }, error), React.createElement(Box, { pb: 4, mb: 4 }, React.createElement(List, Object.assign({ bg: "white", borderRadius: "4px", // @ts-ignore border: isOpen && '1px solid rgba(0,0,0,0.1)', borderColor: borderColor, boxShadow: "6px 5px 8px rgba(0,50,30,0.02)" }, listStyleProps, getMenuProps()), isOpen && inputItems.map(function (item, index) { return React.createElement(ListItem, Object.assign({ px: 2, py: 1, borderBottom: "1px solid rgba(0,0,0,0.01)", _hover: { bg: 'grey.900' }, bg: highlightedIndex === index ? highlightItemBg : 'inherit', key: "" + item.value + index }, getItemProps({ item: item, index: index })), isCreating ? createItemRenderer(item.label) : React.createElement(Box, { display: "inline-flex", alignItems: "center" }, selectedItemValues.includes(item.value) && React.createElement(ListIcon, Object.assign({ as: CustomIcon || CheckCircleIcon, color: "green.500", role: "img", display: "inline", "aria-label": "Selected" }, selectedIconProps)), itemRenderer ? itemRenderer(item) : //@ts-expect-error This is a valid package but showing its not. React.createElement(Highlighter, { autoEscape: true, searchWords: [inputValue || ''], textToHighlight: defaultItemRenderer(item) }))); })))); }; export { CUIAutoComplete }; //# sourceMappingURL=chakra-ui-autocomplete.esm.js.map