UNPKG

@vectara/vectara-ui

Version:

Vectara's design system, codified as a React and Sass component library

182 lines (181 loc) 9.57 kB
var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useRef, useState, useEffect, useMemo } from "react"; import classNames from "classnames"; import { VuiIconButton } from "../button/IconButton"; import { BiSearch, BiX } from "react-icons/bi"; import { VuiIcon } from "../icon/Icon"; import { VuiSearchInputSuggestions } from "./SearchInputSuggestions"; import { createId } from "../../utils/createId"; import { VuiSpinner } from "../spinner/Spinner"; const SIZE = ["s", "m", "l"]; const sizeToIconSizeMap = { s: "xs", m: "s", l: "m" }; export const VuiSearchInput = (_a) => { var { className, size = "m", value, onChange, onKeyDown, placeholder, autoFocus, onSubmit, isClearable, onClear, suggestions, onSelectSuggestion, isLoading } = _a, rest = __rest(_a, ["className", "size", "value", "onChange", "onKeyDown", "placeholder", "autoFocus", "onSubmit", "isClearable", "onClear", "suggestions", "onSelectSuggestion", "isLoading"]); const classes = classNames("vuiSearchInput", `vuiSearchInput--${size}`, className); const inputRef = useRef(null); const containerRef = useRef(null); const [areSuggestionsVisible, setAreSuggestionsVisible] = useState(true); const suggestionRefs = useRef([]); const suppressNextFocus = useRef(false); // Reset suggestions visibility when suggestions change. const prevSuggestionsRef = useRef(suggestions); if (prevSuggestionsRef.current !== suggestions) { prevSuggestionsRef.current = suggestions; if (suggestions && suggestions.length > 0) { setAreSuggestionsVisible(true); } } const hasSuggestions = suggestions && suggestions.length > 0 && areSuggestionsVisible; const controlsId = useMemo(() => `searchSuggestions-${createId()}`, []); // Derive ghost text from the first suggestion if it extends the current value. const ghostText = useMemo(() => { var _a; if (!hasSuggestions || !value || !((_a = suggestions[0]) === null || _a === void 0 ? void 0 : _a.value)) return ""; const firstValue = suggestions[0].value; if (firstValue.startsWith(value) && firstValue !== value) { return firstValue.slice(value.length); } return ""; }, [hasSuggestions, value, suggestions]); useEffect(() => { const handleClickOutside = (event) => { if (containerRef.current && !containerRef.current.contains(event.target)) { setAreSuggestionsVisible(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, []); const handleInputFocus = () => { // Don't show suggestions if focus was triggered by Escape key if (suppressNextFocus.current) { suppressNextFocus.current = false; return; } // Show suggestions when input receives focus (if suggestions exist). if (suggestions && suggestions.length > 0) { setAreSuggestionsVisible(true); } }; const handleInputKeyDown = (e) => { var _a, _b, _c; switch (e.key) { case "ArrowDown": { e.preventDefault(); // Show suggestions if hidden, or move to first suggestion. if (suggestions && suggestions.length > 0) { if (!areSuggestionsVisible) { setAreSuggestionsVisible(true); } else { (_a = suggestionRefs.current[0]) === null || _a === void 0 ? void 0 : _a.focus(); } } break; } case "Escape": { e.preventDefault(); setAreSuggestionsVisible(false); suppressNextFocus.current = true; (_b = inputRef.current) === null || _b === void 0 ? void 0 : _b.focus(); break; } case "Tab": { // If there's a matching value suggestion, select it instead of default tab. if (hasSuggestions && ghostText && onSelectSuggestion && ((_c = suggestions[0]) === null || _c === void 0 ? void 0 : _c.value)) { e.preventDefault(); onSelectSuggestion(suggestions[0]); } else { setAreSuggestionsVisible(false); } break; } case "Enter": { // Prevent form submission from triggering a page refresh. if (!onSubmit) e.preventDefault(); break; } } }; const handleSuggestionKeyDown = (e, index) => { var _a, _b, _c, _d, _e, _f, _g; switch (e.key) { case "ArrowDown": { e.preventDefault(); // Move to next suggestion, or wrap to first. const nextIndex = index + 1; if (nextIndex < suggestionRefs.current.length) { (_a = suggestionRefs.current[nextIndex]) === null || _a === void 0 ? void 0 : _a.focus(); } else { (_b = suggestionRefs.current[0]) === null || _b === void 0 ? void 0 : _b.focus(); } break; } case "ArrowUp": { e.preventDefault(); if (index === 0) { // Move back to input. (_c = inputRef.current) === null || _c === void 0 ? void 0 : _c.focus(); } else { // Move to previous suggestion. (_d = suggestionRefs.current[index - 1]) === null || _d === void 0 ? void 0 : _d.focus(); } break; } case "Escape": { e.preventDefault(); setAreSuggestionsVisible(false); suppressNextFocus.current = true; (_e = inputRef.current) === null || _e === void 0 ? void 0 : _e.focus(); break; } case "Enter": { // For value suggestions, trigger selection, hide suggestions, and refocus input. if (suggestions && ((_f = suggestions[index]) === null || _f === void 0 ? void 0 : _f.value) && onSelectSuggestion) { e.preventDefault(); onSelectSuggestion(suggestions[index]); setAreSuggestionsVisible(false); (_g = inputRef.current) === null || _g === void 0 ? void 0 : _g.focus(); } break; } case "Tab": { // Hide suggestions and allow default tab behavior. setAreSuggestionsVisible(false); break; } } }; const inputClasses = classNames("vuiSearchInput__input", { "vuiSearchInput__input--hasSuggestions": hasSuggestions }); return (_jsx("form", Object.assign({ onSubmit: onSubmit, role: "search" }, { children: _jsxs("div", Object.assign({ ref: containerRef, className: classes, "aria-live": "polite", "aria-atomic": "true", "aria-busy": isLoading ? "true" : "false" }, { children: [_jsx("input", Object.assign({ ref: inputRef, className: inputClasses, type: "text", autoComplete: "off", autoCapitalize: "off", spellCheck: "false", autoFocus: autoFocus, placeholder: placeholder, value: value, onChange: onChange, onFocus: handleInputFocus, onKeyDown: (e) => { handleInputKeyDown(e); onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(e); }, "aria-autocomplete": "list", "aria-controls": hasSuggestions ? controlsId : undefined }, rest)), ghostText && (_jsxs("div", Object.assign({ className: "vuiSearchInput__ghostText", "aria-hidden": "true" }, { children: [_jsx("span", Object.assign({ className: "vuiSearchInput__ghostText--hidden" }, { children: value })), _jsx("span", Object.assign({ className: "vuiSearchInput__ghostText--visible" }, { children: ghostText }))] }))), _jsx("div", Object.assign({ className: "vuiSearchInput__icon" }, { children: isLoading ? (_jsx(VuiSpinner, { size: size === "m" ? "s" : "m" })) : (_jsx(VuiIcon, Object.assign({ color: "subdued", size: sizeToIconSizeMap[size] }, { children: _jsx(BiSearch, {}) }))) })), isClearable && value && (_jsx(VuiIconButton, { "aria-label": "Clear input", className: "vuiSearchInput__clearButton", icon: _jsx(VuiIcon, { children: _jsx(BiX, {}) }), onClick: (e) => { e.preventDefault(); onClear(); } })), hasSuggestions && (_jsx(VuiSearchInputSuggestions, { id: controlsId, suggestions: suggestions, onSuggestionKeyDown: handleSuggestionKeyDown, suggestionRefs: suggestionRefs, onSelectSuggestion: onSelectSuggestion }))] })) }))); };