@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
JavaScript
;
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