UNPKG

@navinc/base-react-components

Version:
266 lines (258 loc) 11.6 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 { useEffect, useRef, useState, } from 'react'; import { styled } from 'styled-components'; import { LoadingDots } from './loading-dots.js'; import { Icon } from './icon.js'; import { Err, Errors, Field, FieldWrapper, Helper, Input, Label } from './form-elements/shared.js'; import { Copy } from './copy.js'; import { focusWithoutScroll, capitalize, noop } from '@navinc/utils'; import { useDebouncedCallback } from 'use-debounce'; const StyledLoadingDots = styled(LoadingDots).withConfig({ displayName: "brc-sc-StyledLoadingDots", componentId: "brc-sc-1q0kt1i" }) ``; const id = (id) => id; const SearchInputContainer = styled.div.withConfig({ displayName: "brc-sc-SearchInputContainer", componentId: "brc-sc-1twra9w" }) ` width: 100%; `; const ResultsContainer = styled.div.withConfig({ displayName: "brc-sc-ResultsContainer", componentId: "brc-sc-1sa5k6m" }) ` position: relative; z-index: 2; & > ${Helper} { margin-left: ${({ theme }) => theme.gu(2)}; } `; const ResultContainer = styled.div.withConfig({ displayName: "brc-sc-ResultContainer", componentId: "brc-sc-e06mdg" }) ` cursor: pointer; outline: none; padding: 16px; border-bottom: 1px solid ${({ theme }) => theme.navNeutral300}; ${({ isActive, theme }) => isActive && `background-color: ${theme.navNeutral100}; border: 4px solid ${theme.navStatusPositive500};`} &:hover { background-color: ${({ theme }) => theme.navNeutral100}; } &:focus { outline: ${({ theme }) => theme.focusOutline}; } `; const Results = styled.div.withConfig({ displayName: "brc-sc-Results", componentId: "brc-sc-1uj7ufb" }) ` background-color: ${({ theme }) => theme.navNeutralLight}; border: solid 1px ${({ theme }) => theme.navNeutral300}; border-top: none; border-radius: 0 0 8px 8px; position: ${({ shouldPositionResultsRelative }) => (shouldPositionResultsRelative ? 'relative' : 'absolute')}; top: 0; overflow-y: auto; max-height: ${({ resultsMaxHeight }) => resultsMaxHeight || '300px'}; width: 100%; /* prettier-ignore */ ${ResultContainer}:last-child { border-bottom: none; } `; const StyledFieldWrapper = styled(FieldWrapper).withConfig({ displayName: "brc-sc-StyledFieldWrapper", componentId: "brc-sc-1c2gne6" }) ` & ${Icon}, & ${StyledLoadingDots} { position: absolute; right: 16px; top: 50%; transform: translateY(-50%); } & ${Icon} { width: 24px; } & ${StyledLoadingDots} { color: ${({ theme }) => theme.navPrimary}; width: 56px; } & ${Input} { text-overflow: ellipsis; padding-right: 40px; ${({ $shouldShowDropDown }) => $shouldShowDropDown && 'border-radius: 4px 4px 0 0;'} } `; const StyledNoResults = styled.div.withConfig({ displayName: "brc-sc-StyledNoResults", componentId: "brc-sc-18xezbb" }) ` align-items: center; display: flex; flex-direction: column; margin-bottom: 24px; margin-top: 24px; `; const NoResultsIcon = styled(Icon).withConfig({ displayName: "brc-sc-NoResultsIcon", componentId: "brc-sc-1u9q8iq" }) ` color: ${({ theme }) => theme.navNeutral400}; margin-bottom: 8px; `; const DefaultNoResults = ({ query }) => (_jsxs(StyledNoResults, { children: [_jsx(NoResultsIcon, { name: "actions/circle-faq" }), _jsxs(Copy, { children: ["No results for \"", query, "\""] })] })); const UnstyledSearchInput = (_a) => { var { autoFocus, className, errors = [], helperText, isLoading, isInvalid, label, lede, name, NoResults, onBlur = noop, onChange = noop, onFocus = noop, Result, required, results = [], resultsMaxHeight, resultToQuery = id, search, shouldPositionResultsRelative, touched, type, value, hasSpaceForErrors } = _a, inputProps = __rest(_a, ["autoFocus", "className", "errors", "helperText", "isLoading", "isInvalid", "label", "lede", "name", "NoResults", "onBlur", "onChange", "onFocus", "Result", "required", "results", "resultsMaxHeight", "resultToQuery", "search", "shouldPositionResultsRelative", "touched", "type", "value", "hasSpaceForErrors"]); const [focusedResultIndex, setFocusedResultIndex] = useState(-1); const [canBeOpen, setCanBeOpen] = useState(false); const [query, setQuery] = useState(''); const [lastSearchedQuery, setLastSearchedQuery] = useState(''); const inputRef = useRef(null); const resultsRef = useRef(null); const shouldShowDropDown = !!(query.length && canBeOpen && query === lastSearchedQuery && !isLoading); const shouldShowResults = !!(shouldShowDropDown && results.length); const shouldShowNoResults = !!(shouldShowDropDown && !results.length); const RenderNoResults = NoResults || DefaultNoResults; const isVisited = touched || !!value; useEffect(() => { if (!value) { return; } const newQuery = resultToQuery(value); if (!newQuery) { return; } setQuery(newQuery); }, [resultToQuery, value]); useEffect(() => { if (autoFocus && inputRef.current) { focusWithoutScroll(inputRef.current); } }, [autoFocus]); const searchDebounced = useDebouncedCallback((query) => { setLastSearchedQuery(query); search(query); setFocusedResultIndex(-1); }, 500); const handleChange = ({ target }) => { var _a; const newQuery = (_a = target.value) !== null && _a !== void 0 ? _a : ''; setQuery(newQuery); onChange({ target: { name, value: null, }, }); if (newQuery.length > 0) { searchDebounced(newQuery); setCanBeOpen(true); } }; const handleInputFocus = (event) => { setCanBeOpen(true); onFocus(event); }; const focusResult = (index) => { var _a, _b, _c; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- not sure if it is safe to remove the optional chaining const ref = (_b = (_a = resultsRef.current) === null || _a === void 0 ? void 0 : _a.children) === null || _b === void 0 ? void 0 : _b[index]; if (ref) { setFocusedResultIndex(index); ref.focus(); (_c = inputRef.current) === null || _c === void 0 ? void 0 : _c.focus(); } }; const goToNextResult = () => { const newIndex = focusedResultIndex + 1; if (newIndex < results.length) { focusResult(newIndex); } }; const goToPreviousResult = () => { const newIndex = focusedResultIndex - 1; if (newIndex > -2) { focusResult(newIndex); } }; const closeResults = () => { setCanBeOpen(false); setFocusedResultIndex(-1); }; const onSelectResult = (clickedResult) => { const result = clickedResult || results[focusedResultIndex]; closeResults(); onChange({ target: { name, value: result, }, }); }; const handleBlurOnInput = (event) => { if (!resultsRef.current || (resultsRef.current && !resultsRef.current.contains(event.relatedTarget))) { closeResults(); onBlur(event); } }; const handleKeyDownOnInput = (event) => { switch (event.key) { case 'ArrowDown': if (shouldShowResults) { event.preventDefault(); goToNextResult(); } else { setCanBeOpen(true); } break; case 'Tab': if (shouldShowResults) { event.preventDefault(); if (event.shiftKey) { goToPreviousResult(); } else { goToNextResult(); } } break; case 'ArrowUp': if (shouldShowResults) { event.preventDefault(); goToPreviousResult(); } break; case 'Escape': if (focusedResultIndex > -1) { setFocusedResultIndex(-1); } else { setQuery(''); } break; case 'Enter': if (focusedResultIndex > -1) { event.preventDefault(); onSelectResult(); } break; } }; return (_jsxs(SearchInputContainer, { className: className, children: [_jsxs(StyledFieldWrapper, { "$shouldShowDropDown": shouldShowDropDown, children: [lede && _jsx(Copy, { bold: true, children: lede }), _jsxs(Field, { isVisited: isVisited, children: [_jsx(Input, Object.assign({ autoComplete: "off", "data-testid": "search-input:input", name: name, onBlur: handleBlurOnInput, onChange: handleChange, onFocus: handleInputFocus, onKeyDown: handleKeyDownOnInput, type: type, ref: inputRef, required: required, value: query, isInvalid: isInvalid }, inputProps)), isLoading ? _jsx(StyledLoadingDots, {}) : _jsx(Icon, { name: "system/search" }), _jsx(Label, { required: required, children: capitalize(label) })] })] }), _jsxs(ResultsContainer, { children: [shouldShowDropDown && (_jsxs(Results, { resultsMaxHeight: resultsMaxHeight, ref: resultsRef, shouldPositionResultsRelative: shouldPositionResultsRelative, children: [shouldShowNoResults && _jsx(RenderNoResults, { query: query }), shouldShowResults && results.map((result, index) => (_jsx(ResultContainer, { isActive: focusedResultIndex === index, onMouseDown: () => onSelectResult(result), tabIndex: 0, children: _jsx(Result, { result: result }) }, JSON.stringify(result))))] })), helperText && _jsx(Helper, { helperText: helperText }), _jsx(Errors, { hasSpaceForErrors: hasSpaceForErrors, hasHelperText: !!helperText, children: !!errors.length && errors.map((err, i) => _jsx(Err, { children: err }, `err-${i}`)) })] })] })); }; /** All form-element components expect to be controlled components. This makes them immediately compatible with form managers like formik or redux-form, as well as within any stateful component. Use the `<SearchInput />` component just as you would for any `input` type that accepts textual input (`radio`, `checkbox` have their own respective components). All props get passed on to the underlying `input`. In addition to native form element attributes, all form-element components implement these props isInvalid: Boolean -- Indicates whether the value of the field is valid (`false`) or invalid (`true`) ``` <SearchInput NoResults={({ query }) => `look bro I got your ${query}`} Result={({ result }) => result} resultToQuery={(result) => result} results={[]} value='abc' /> ``` * * @deprecated This component is deprecated and will be removed in a future release. Avoid using it in new code. */ export const SearchInput = styled(UnstyledSearchInput) ``; //# sourceMappingURL=search-input.js.map