UNPKG

@react-md/autocomplete

Version:

Create an accessible autocomplete component that allows a user to get real-time suggestions as they type within an input. This component can also be hooked up to a backend API that handles additional filtering or sorting.

302 lines 13.5 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.useAutoComplete = void 0; var react_1 = require("react"); var transition_1 = require("@react-md/transition"); var utils_1 = require("@react-md/utils"); var utils_2 = require("./utils"); /** * This hook handles all the autocomplete's "logic" and behavior. * * @internal */ function useAutoComplete(_a) { var _b; var suggestionsId = _a.suggestionsId, data = _a.data, propValue = _a.propValue, _c = _a.defaultValue, defaultValue = _c === void 0 ? "" : _c, filterFn = _a.filter, filterOptions = _a.filterOptions, filterOnNoValue = _a.filterOnNoValue, valueKey = _a.valueKey, getResultId = _a.getResultId, getResultValue = _a.getResultValue, onBlur = _a.onBlur, onFocus = _a.onFocus, onClick = _a.onClick, onChange = _a.onChange, onKeyDown = _a.onKeyDown, forwardedRef = _a.forwardedRef, onAutoComplete = _a.onAutoComplete, clearOnAutoComplete = _a.clearOnAutoComplete, anchor = _a.anchor, xMargin = _a.xMargin, yMargin = _a.yMargin, vwMargin = _a.vwMargin, vhMargin = _a.vhMargin, transformOrigin = _a.transformOrigin, listboxWidth = _a.listboxWidth, listboxStyle = _a.listboxStyle, preventOverlap = _a.preventOverlap, disableSwapping = _a.disableSwapping, disableVHBounds = _a.disableVHBounds, closeOnResize = _a.closeOnResize, closeOnScroll = _a.closeOnScroll, propDisableShowOnFocus = _a.disableShowOnFocus, isListAutocomplete = _a.isListAutocomplete, isInlineAutocomplete = _a.isInlineAutocomplete; var _d = utils_1.useEnsuredRef(forwardedRef), ref = _d[0], refHandler = _d[1]; var filter = utils_2.getFilterFunction(filterFn); var _e = react_1.useState(function () { var _a; var options = __assign(__assign({}, filterOptions), { valueKey: valueKey, getItemValue: getResultValue, startsWith: (_a = filterOptions === null || filterOptions === void 0 ? void 0 : filterOptions.startsWith) !== null && _a !== void 0 ? _a : isInlineAutocomplete }); var value = propValue !== null && propValue !== void 0 ? propValue : defaultValue; var filteredData = filterOnNoValue || value ? filter(value, data, options) : data; var match = value; if (isInlineAutocomplete && filteredData.length) { match = getResultValue(filteredData[0], valueKey); } return { value: value, match: match, filteredData: filteredData, }; }), _f = _e[0], stateValue = _f.value, match = _f.match, stateFilteredData = _f.filteredData, setState = _e[1]; var filteredData = filterFn === "none" ? data : stateFilteredData; var startsWith = (_b = filterOptions === null || filterOptions === void 0 ? void 0 : filterOptions.startsWith) !== null && _b !== void 0 ? _b : isInlineAutocomplete; var value = propValue !== null && propValue !== void 0 ? propValue : stateValue; var setValue = react_1.useCallback(function (nextValue) { var isBackspace = value.length > nextValue.length || (!!match && value.length === nextValue.length); var filtered = data; if (nextValue || filterOnNoValue) { var options = __assign(__assign({}, filterOptions), { valueKey: valueKey, getItemValue: getResultValue, startsWith: startsWith }); filtered = filter(nextValue, data, options); } var nextMatch = nextValue; if (isInlineAutocomplete && filtered.length && !isBackspace) { nextMatch = getResultValue(filtered[0], valueKey); var input = ref.current; if (input && !isBackspace) { input.value = nextMatch; input.setSelectionRange(nextValue.length, nextMatch.length); } } setState({ value: nextValue, match: nextMatch, filteredData: filtered }); }, [ ref, data, filter, filterOnNoValue, filterOptions, isInlineAutocomplete, getResultValue, value, match, startsWith, valueKey, ]); // this is really just a hacky way to make sure that once a value has been // autocompleted, the menu doesn't immediately re-appear due to the hook below // for showing when the value/ filtered data list change var autocompleted = react_1.useRef(false); var handleChange = react_1.useCallback(function (event) { if (onChange) { onChange(event); } autocompleted.current = false; setValue(event.currentTarget.value); }, [setValue, onChange]); var _g = utils_1.useToggle(false), visible = _g[0], show = _g[1], hide = _g[2]; var isTouch = utils_1.useIsUserInteractionMode("touch"); var disableShowOnFocus = propDisableShowOnFocus !== null && propDisableShowOnFocus !== void 0 ? propDisableShowOnFocus : isTouch; var focused = react_1.useRef(false); var handleBlur = react_1.useCallback(function (event) { if (onBlur) { onBlur(event); } focused.current = false; }, [onBlur]); var handleFocus = react_1.useCallback(function (event) { if (onFocus) { onFocus(event); } if (disableShowOnFocus) { return; } focused.current = true; if (isListAutocomplete && filteredData.length) { show(); } }, [filteredData, isListAutocomplete, onFocus, show, disableShowOnFocus]); var handleClick = react_1.useCallback(function (event) { if (onClick) { onClick(event); } // since click events also trigger focus events right beforehand, want to // skip the first click handler and require a second click to show it. // this is why the focused.current isn't set onFocus for // disableShowOnFocus if (disableShowOnFocus && !focused.current) { focused.current = true; return; } if (isListAutocomplete && filteredData.length) { show(); } }, [disableShowOnFocus, filteredData.length, isListAutocomplete, onClick, show]); var handleAutoComplete = react_1.useCallback(function (index) { var result = filteredData[index]; var resultValue = getResultValue(result, valueKey); if (onAutoComplete) { onAutoComplete({ value: resultValue, index: index, result: result, dataIndex: data.findIndex(function (datum) { return getResultValue(datum, valueKey) === resultValue; }), filteredData: filteredData, }); } setValue(clearOnAutoComplete ? "" : resultValue); autocompleted.current = true; }, [ clearOnAutoComplete, data, filteredData, getResultValue, onAutoComplete, valueKey, setValue, ]); var listboxRef = react_1.useRef(null); var _h = utils_1.useActiveDescendantMovement(__assign(__assign({}, utils_1.MovementPresets.VERTICAL_COMBOBOX), { getId: getResultId, items: filteredData, baseId: suggestionsId, onChange: function (_a, itemRefs) { var index = _a.index, items = _a.items, target = _a.target; // the default scroll into view behavior for aria-activedescendant // movement won't work here since the "target" element will actually be // the input element instead of the listbox. So need to implement the // scroll into view behavior manually from the listbox instead. var item = itemRefs[index] && itemRefs[index].current; var listbox = listboxRef.current; if (item && listbox && listbox.scrollHeight > listbox.offsetHeight) { utils_1.scrollIntoView(listbox, item); } if (!isInlineAutocomplete) { return; } var nextMatch = getResultValue(items[index], valueKey); target.value = nextMatch; target.setSelectionRange(0, nextMatch.length); setState(function (prevState) { return (__assign(__assign({}, prevState), { value: nextMatch, match: nextMatch })); }); }, onKeyDown: function (event) { if (onKeyDown) { onKeyDown(event); } var input = event.currentTarget; switch (event.key) { case "ArrowDown": if (isListAutocomplete && event.altKey && !visible && filteredData.length) { // don't want the cursor to move if there is text event.preventDefault(); event.stopPropagation(); show(); setFocusedIndex(-1); } break; case "ArrowUp": if (isListAutocomplete && event.altKey && visible) { // don't want the cursor to move if there is text event.preventDefault(); event.stopPropagation(); hide(); } break; case "Tab": event.stopPropagation(); hide(); break; case "ArrowRight": if (isInlineAutocomplete && input.selectionStart !== input.selectionEnd) { var index = focusedIndex !== -1 ? focusedIndex : 0; hide(); handleAutoComplete(index); } break; case "Enter": if (visible && focusedIndex >= 0) { event.stopPropagation(); handleAutoComplete(focusedIndex); hide(); } break; case "Escape": if (visible) { event.stopPropagation(); hide(); } else if (value) { event.stopPropagation(); setValue(""); } break; // no default } } })), activeId = _h.activeId, itemRefs = _h.itemRefs, handleKeyDown = _h.onKeyDown, focusedIndex = _h.focusedIndex, setFocusedIndex = _h.setFocusedIndex; utils_1.useCloseOnOutsideClick({ enabled: visible, element: ref.current, onOutsideClick: hide, }); var _j = transition_1.useFixedPositioning({ fixedTo: function () { return ref.current; }, anchor: anchor, onScroll: function (_event, _a) { var visible = _a.visible; if (closeOnScroll || !visible) { hide(); } }, onResize: closeOnResize ? hide : undefined, width: listboxWidth, xMargin: xMargin, yMargin: yMargin, vwMargin: vwMargin, vhMargin: vhMargin, transformOrigin: transformOrigin, preventOverlap: preventOverlap, disableSwapping: disableSwapping, disableVHBounds: disableVHBounds, }), style = _j.style, onEnter = _j.onEnter, onEntering = _j.onEntering, onEntered = _j.onEntered, onExited = _j.onExited, updateStyle = _j.updateStyle; react_1.useEffect(function () { if (!focused.current || autocompleted.current) { return; } if (filteredData.length && !visible && value.length && isListAutocomplete) { show(); } else if (!filteredData.length && visible) { hide(); } // this effect is just for toggling the visibility states as needed if the // value or filter data list changes // eslint-disable-next-line react-hooks/exhaustive-deps }, [filteredData, value]); react_1.useEffect(function () { if (!visible) { setFocusedIndex(-1); return; } updateStyle(); // only want to trigger on data changes and setFocusedIndex shouldn't change // anyways // eslint-disable-next-line react-hooks/exhaustive-deps }, [visible, filteredData]); return { ref: refHandler, value: value, match: match, visible: visible, activeId: activeId, itemRefs: itemRefs, filteredData: filteredData, fixedStyle: __assign(__assign({}, style), listboxStyle), transitionHooks: { onEnter: onEnter, onEntering: onEntering, onEntered: onEntered, onExited: onExited, }, listboxRef: listboxRef, handleBlur: handleBlur, handleFocus: handleFocus, handleClick: handleClick, handleChange: handleChange, handleKeyDown: handleKeyDown, handleAutoComplete: handleAutoComplete, }; } exports.useAutoComplete = useAutoComplete; //# sourceMappingURL=useAutoComplete.js.map