@blinkstrike/chakra-ui-autocomplete
Version:
An accessible autocomplete utility library built for chakra UI
423 lines (378 loc) • 14.9 kB
JavaScript
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