UNPKG

@navinc/base-react-components

Version:
258 lines (255 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 } from '@navinc/utils'; import { useDebouncedCallback } from 'use-debounce'; import PropTypes from 'prop-types'; import isRebrand from './is-rebrand.js'; const id = (id) => id; const SearchInputContainer = styled.div.withConfig({ displayName: "brc-sc-SearchInputContainer", componentId: "brc-sc-1tkz17d" }) ` width: 100%; `; const ResultsContainer = styled.div.withConfig({ displayName: "brc-sc-ResultsContainer", componentId: "brc-sc-8e5pn8" }) ` position: relative; z-index: 2; & > ${Helper} { margin-left: ${({ theme }) => theme.gu(2)}; } `; const getResultContainerBackgroundColor = (isActive, theme) => { if (isRebrand(theme) && isActive) return `background-color: ${theme.navNeutral100}; border: 4px solid ${theme.navStatusPositive500}`; return isActive && `background-color: ${theme.neutral100};`; }; const ResultContainer = styled.div.withConfig({ displayName: "brc-sc-ResultContainer", componentId: "brc-sc-ed8c26" }) ` cursor: pointer; outline: none; padding: 16px; border-bottom: 1px solid ${({ theme }) => (isRebrand(theme) ? theme.navNeutral300 : theme.neutral300)}; ${({ isActive, theme }) => getResultContainerBackgroundColor(isActive, theme)}; &:hover { background-color: ${({ theme }) => (isRebrand(theme) ? theme.navNeutral100 : theme.neutral100)}; } &:focus { outline: ${({ theme }) => theme.focusOutline}; } `; const Results = styled.div.withConfig({ displayName: "brc-sc-Results", componentId: "brc-sc-1avlx1y" }) ` background-color: ${({ theme }) => (isRebrand(theme) ? theme.navSecondary100 : theme.white)}; border: solid 1px ${({ theme }) => (isRebrand(theme) ? theme.navNeutral300 : theme.neutral300)}; border-top: none; border-radius: ${({ theme }) => (isRebrand(theme) ? '0 0 8px 8px' : '0 0 4px 4px')}; 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-13fj46g" }) ` & ${Icon}, & ${LoadingDots} { position: absolute; right: 16px; top: 50%; transform: translateY(-50%); } & ${Icon} { width: 24px; } & ${LoadingDots} { 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-9i7w38" }) ` 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-f379za" }) ` color: ${({ theme }) => (isRebrand(theme) ? theme.navNeutral400 : theme.neutral400)}; margin-bottom: 8px; `; const DefaultNoResults = ({ query }) => (_jsxs(StyledNoResults, { children: [_jsx(NoResultsIcon, { name: "actions/circle-faq" }), _jsxs(Copy, { children: ["No results for \"", query, "\""] })] })); export const SearchInput = (_a) => { var { autoFocus, className, errors, helperText, isLoading, isInvalid, label, lede, name, NoResults, onBlur, onChange, onFocus, Result, required, results, resultsMaxHeight, resultToQuery, search, shouldPositionResultsRelative, touched, type, value } = _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"]); 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(() => { resultToQuery(value) && setQuery(resultToQuery(value) || ''); }, [resultToQuery, value]); useEffect(() => { if (autoFocus) { 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; 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(); inputRef.current.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.keyCode) { case 40: // Down Arrow if (shouldShowResults) { event.preventDefault(); goToNextResult(); } else { setCanBeOpen(true); } break; case 9: // Tab if (shouldShowResults) { event.preventDefault(); if (event.shiftKey) { goToPreviousResult(); } else { goToNextResult(); } } break; case 38: // Up Arrow if (shouldShowResults) { event.preventDefault(); goToPreviousResult(); } break; case 27: // Escape if (focusedResultIndex > -1) { setFocusedResultIndex(-1); } else { setQuery(''); } break; case 13: // Enter if (focusedResultIndex > -1) { event.preventDefault(); onSelectResult(); } break; } }; return (_jsxs(SearchInputContainer, Object.assign({ className: className }, { children: [_jsxs(StyledFieldWrapper, Object.assign({ shouldShowDropDown: shouldShowDropDown }, { children: [lede && _jsx(Copy, Object.assign({ bold: true }, { children: lede })), _jsxs(Field, Object.assign({ isInvalid: isInvalid, isVisited: isVisited, required: required, type: type }, { 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(LoadingDots, {}) : _jsx(Icon, { name: "system/search" }), _jsx(Label, Object.assign({ required: required, value: query }, { children: capitalize(label) }))] }))] })), _jsxs(ResultsContainer, { children: [shouldShowDropDown && (_jsxs(Results, Object.assign({ resultsMaxHeight: resultsMaxHeight, ref: resultsRef, shouldPositionResultsRelative: shouldPositionResultsRelative }, { children: [shouldShowNoResults && _jsx(RenderNoResults, { query: query }), shouldShowResults && results.map((result, index) => (_jsx(ResultContainer, Object.assign({ isActive: focusedResultIndex === index, onMouseDown: () => onSelectResult(result), tabIndex: 0 }, { children: _jsx(Result, { result: result }) }), JSON.stringify(result))))] }))), helperText && _jsx(Helper, { hasSpaceForHelper: true, helperText: helperText }), _jsx(Errors, Object.assign({ hasSpaceForErrors: true }, { children: !!errors.length && errors.map((err, i) => _jsx(Err, { children: err }, `err-${i}`)) }))] })] }))); }; SearchInput.defaultProps = { errors: [], onBlur: id, onChange: id, onFocus: id, results: [], resultToQuery: id, search: id, }; SearchInput.propTypes = { errors: PropTypes.array, isInvalid: PropTypes.bool, NoResults: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), onBlur: PropTypes.func, onChange: PropTypes.func, onFocus: PropTypes.func, results: PropTypes.array, resultsMaxHeight: PropTypes.string, resultToQuery: PropTypes.func.isRequired, Result: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired, search: PropTypes.func.isRequired, }; export default styled(SearchInput).withConfig({ componentId: "brc-sc-150ed28" }) ``; //# sourceMappingURL=search-input.js.map