@mrshmllw/smores-react 16.10.0 → 16.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,7 @@ import { useOnClickOutside } from '../hooks';
9
9
  import { useUniqueId } from '../utils/id';
10
10
  import { useControllableState } from '../utils/useControlledState';
11
11
  import { SearchOptions } from './components/SearchOptions';
12
+ import { visuallyHidden } from '../utils/visuallyHidden';
12
13
  import { IconContainer } from '../sharedStyles/shared.styles';
13
14
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
14
15
  import { faXmark, faChevronDown, faSearch, } from '@awesome.me/kit-46ca99185c/icons/classic/regular';
@@ -40,40 +41,53 @@ export const SearchInput = forwardRef(function SearchInput({ id: idProp, name =
40
41
  });
41
42
  const [searchQuery, setSearchQuery] = useState(null);
42
43
  const [highlightedIndex, setHighlightedIndex] = useState(-1);
44
+ const [emptyMessage, setEmptyMessage] = useState('');
43
45
  const selectedValueLabel = searchList.find((option) => option.label === selectedValue || option.value === selectedValue)?.label;
44
46
  const handleBlur = () => {
45
- if (selectedValue) {
46
- setSearchQuery(selectedValueLabel ?? null);
47
- }
48
- else if (!selectedValue) {
49
- setSearchQuery(null);
50
- }
47
+ setSearchQuery(null);
48
+ };
49
+ // Every way of closing the list goes through here, so a reopened list
50
+ // never starts with a stale highlighted option.
51
+ const closeOptions = () => {
52
+ setShowOptions(false);
53
+ setHighlightedIndex(-1);
51
54
  };
52
55
  useOnClickOutside({
53
56
  ref: wrapperRef,
54
57
  callback: () => {
55
58
  handleBlur();
56
- setShowOptions(false);
59
+ closeOptions();
57
60
  },
58
61
  });
59
- // oxlint-disable-next-line react/react-compiler
62
+ const isFuzzySearch = enableFuzzySearch || !!fuzzySearchOptions;
60
63
  const fuse = useMemo(() => {
64
+ if (!isFuzzySearch) {
65
+ return null;
66
+ }
61
67
  return new Fuse(searchList, {
62
68
  ...defaultFuzzySearchOptions,
63
69
  ...fuzzySearchOptions,
64
70
  });
65
- }, [searchList]);
66
- // oxlint-disable-next-line react/react-compiler
71
+ }, [isFuzzySearch, searchList, fuzzySearchOptions]);
67
72
  const filteredList = useMemo(() => {
68
73
  if (searchQuery === null || searchQuery === '') {
69
74
  return searchList;
70
75
  }
71
- if (enableFuzzySearch || !!fuzzySearchOptions) {
76
+ if (fuse) {
72
77
  return fuse.search(searchQuery).map(({ item }) => item);
73
78
  }
74
79
  return searchList.filter(({ label }) => label.toLowerCase().includes(searchQuery.toLocaleLowerCase()));
75
- // oxlint-disable-next-line react/react-compiler
76
- }, [searchQuery, enableFuzzySearch, !!fuzzySearchOptions]);
80
+ }, [searchQuery, searchList, fuse]);
81
+ // If the options change under a highlighted one (e.g. searchList is
82
+ // swapped), clear the highlight so Enter can't pick a different option.
83
+ // Compare values, not identity, so an inline searchList that re-renders
84
+ // with the same options keeps the highlight.
85
+ const optionsKey = useMemo(() => filteredList.map((option) => option.value).join('\n'), [filteredList]);
86
+ const [highlightedOptionsKey, setHighlightedOptionsKey] = useState(optionsKey);
87
+ if (optionsKey !== highlightedOptionsKey) {
88
+ setHighlightedOptionsKey(optionsKey);
89
+ setHighlightedIndex(-1);
90
+ }
77
91
  const getDisplayedInputText = () => {
78
92
  if (searchQuery !== null) {
79
93
  return searchQuery;
@@ -90,22 +104,33 @@ export const SearchInput = forwardRef(function SearchInput({ id: idProp, name =
90
104
  setHighlightedIndex(-1);
91
105
  if (query === null) {
92
106
  setSelectedValue(null);
93
- setShowOptions(false);
107
+ closeOptions();
94
108
  }
95
109
  else {
96
110
  setShowOptions(true);
97
111
  }
98
112
  };
99
- const handleClick = () => {
113
+ // Reopening a field with a selection usually means changing it, so
114
+ // select the label and let typing replace it rather than append to it
115
+ const shouldSelectLabel = !showOptions && searchQuery === null && selectedValue !== null;
116
+ const handleMouseDown = (event) => {
117
+ // Focus and select ourselves, so the browser doesn't place a caret
118
+ // (Safari can otherwise undo the selection on mouseup)
119
+ const input = event.currentTarget;
120
+ if (shouldSelectLabel && document.activeElement !== input) {
121
+ event.preventDefault();
122
+ input.focus();
123
+ }
124
+ };
125
+ const handleClick = (event) => {
126
+ if (shouldSelectLabel) {
127
+ event.currentTarget.select();
128
+ }
100
129
  setShowOptions(true);
101
130
  if (searchQuery !== null) {
102
131
  updateSearchQuery(searchQuery);
103
132
  setShowOptions(true);
104
133
  }
105
- else if (selectedValue !== null && searchQuery === null) {
106
- setSearchQuery(selectedValueLabel || null);
107
- setShowOptions(true);
108
- }
109
134
  };
110
135
  const handleInputChange = (event) => {
111
136
  const nextValue = event.currentTarget.value;
@@ -122,35 +147,68 @@ export const SearchInput = forwardRef(function SearchInput({ id: idProp, name =
122
147
  onFound('');
123
148
  };
124
149
  const handleCaretClick = () => {
125
- setShowOptions(!showOptions);
150
+ if (showOptions) {
151
+ closeOptions();
152
+ }
153
+ else {
154
+ setShowOptions(true);
155
+ }
126
156
  };
157
+ const listboxId = `${id}-listbox`;
158
+ const getOptionId = (index) => `${id}-option-${index}`;
159
+ const hasResults = showOptions && filteredList.length > 0;
160
+ const hasHighlight = hasResults &&
161
+ highlightedIndex >= 0 &&
162
+ highlightedIndex < filteredList.length;
163
+ const noResults = showOptions && filteredList.length === 0;
127
164
  const handleKeyDown = (event) => {
128
- if (event.key === 'Enter' && highlightedIndex !== -1) {
165
+ if (event.key === 'Escape') {
166
+ if (!showOptions)
167
+ return;
129
168
  event.preventDefault();
130
- const focusedItem = filteredList[highlightedIndex];
131
- handleSelect(focusedItem);
169
+ event.stopPropagation();
170
+ closeOptions();
171
+ }
172
+ else if (event.key === 'Enter') {
173
+ if (hasHighlight) {
174
+ event.preventDefault();
175
+ handleSelect(filteredList[highlightedIndex]);
176
+ }
132
177
  }
133
178
  else if (event.key === 'ArrowDown') {
134
179
  event.preventDefault();
135
- const nextIndex = (highlightedIndex + 1) % filteredList.length;
136
- setHighlightedIndex(nextIndex);
180
+ if (!showOptions) {
181
+ setShowOptions(true);
182
+ }
183
+ else if (filteredList.length > 0) {
184
+ setHighlightedIndex((highlightedIndex + 1) % filteredList.length);
185
+ }
137
186
  }
138
187
  else if (event.key === 'ArrowUp') {
139
188
  event.preventDefault();
140
- const prevIndex = (highlightedIndex - 1 + filteredList.length) % filteredList.length;
141
- setHighlightedIndex(prevIndex);
189
+ if (showOptions && filteredList.length > 0) {
190
+ setHighlightedIndex((highlightedIndex - 1 + filteredList.length) % filteredList.length);
191
+ }
142
192
  }
143
193
  };
144
194
  const showClearSearchButton = !!clearSearch && (!!value || !!selectedValue || !!searchQuery);
145
- return (_jsx(Wrapper, { ref: wrapperRef, children: _jsxs(Field, { className: className, renderAsTitle: renderAsTitle, htmlFor: id, ...otherProps, children: [_jsxs(Box, { flex: true, alignItems: "center", justifyContent: "flex-start", children: [showIcon && (_jsx(InputLeadingIconContainer, { $size: 20, children: _jsx(FontAwesomeIcon, { icon: faSearch, color: theme.color.text.subtle }) })), _jsx(Input, { id: id, name: name, ref: ref, placeholder: placeholder, $error: otherProps.error, $frontIcon: showIcon, $fallbackStyle: fallbackStyle, autoComplete: "off", value: displayedInputText, onFocus: handleClick, onChange: handleInputChange, selected: isSelected, onClick: handleClick, onKeyDown: handleKeyDown, onBlur: (e) => {
146
- onBlur?.(e);
147
- } }), _jsxs(Icons, { flex: true, alignItems: "center", gap: "space.100", $clearSearch: showClearSearchButton, children: [showClearSearchButton && (_jsx(IconContainer, { title: "Clear search", onClick: handleClearSearch, type: "button", as: "button", $size: 20, style: { cursor: 'pointer' }, children: _jsx(FontAwesomeIcon, { icon: faXmark, color: theme.color.illustration.neutral[400] }) })), _jsx(Line, {}), _jsx(IconContainer, { type: "button", title: "icon-button", as: "button", onClick: handleCaretClick, $size: 20, children: _jsx(FontAwesomeIcon, { style: {
148
- rotate: showOptions ? '180deg' : '0deg',
149
- }, icon: faChevronDown, color: theme.color.illustration.neutral[400] }) })] })] }), showOptions && (_jsx(SearchOptions, { displayedList: filteredList, selectedValue: selectedValue, highlightedIndex: highlightedIndex, setHighlightedIndex: setHighlightedIndex, onKeyDown: handleKeyDown, searchTerm: searchQuery || '', onSelect: handleSelect, positionRelative: resultsRelativePosition, resultsBorder: resultsBorder, onNotFound: onNotFound, notFoundComponent: notFoundComponent?.(searchQuery ?? '') }))] }) }));
195
+ return (_jsxs(Wrapper, { ref: wrapperRef, children: [_jsxs(Field, { className: className, renderAsTitle: renderAsTitle, htmlFor: id, ...otherProps, children: [_jsxs(Box, { flex: true, alignItems: "center", justifyContent: "flex-start", children: [showIcon && (_jsx(InputLeadingIconContainer, { $size: 20, children: _jsx(FontAwesomeIcon, { icon: faSearch, color: theme.color.text.subtle }) })), _jsx(Input, { id: id, name: name, ref: ref, placeholder: placeholder, $error: otherProps.error, $frontIcon: showIcon, $fallbackStyle: fallbackStyle, autoComplete: "off", role: "combobox", "aria-autocomplete": "list", "aria-expanded": hasResults, "aria-controls": hasResults ? listboxId : undefined, "aria-activedescendant": hasHighlight ? getOptionId(highlightedIndex) : undefined, value: displayedInputText, onFocus: handleClick, onChange: handleInputChange, selected: isSelected, onClick: handleClick, onMouseDown: handleMouseDown, onKeyDown: (event) => {
196
+ if (event.key === 'ArrowDown' && shouldSelectLabel) {
197
+ event.currentTarget.select();
198
+ }
199
+ handleKeyDown(event);
200
+ }, onBlur: (e) => {
201
+ onBlur?.(e);
202
+ } }), _jsxs(Icons, { flex: true, alignItems: "center", gap: "space.100", $clearSearch: showClearSearchButton, children: [showClearSearchButton && (_jsx(IconContainer, { title: "Clear search", onClick: handleClearSearch, type: "button", as: "button", $size: 20, style: { cursor: 'pointer' }, children: _jsx(FontAwesomeIcon, { icon: faXmark, color: theme.color.illustration.neutral[400] }) })), _jsx(Line, {}), _jsx(IconContainer, { type: "button", title: "icon-button", as: "button", onClick: handleCaretClick, $size: 20, children: _jsx(FontAwesomeIcon, { style: {
203
+ rotate: showOptions ? '180deg' : '0deg',
204
+ }, icon: faChevronDown, color: theme.color.illustration.neutral[400] }) })] })] }), showOptions && (_jsx(SearchOptions, { displayedList: filteredList, selectedValue: selectedValue, highlightedIndex: highlightedIndex, setHighlightedIndex: setHighlightedIndex, onKeyDown: handleKeyDown, listboxId: listboxId, listboxLabel: otherProps.label ?? placeholder ?? 'Search results', getOptionId: getOptionId, searchTerm: searchQuery || '', onSelect: handleSelect, positionRelative: resultsRelativePosition, resultsBorder: resultsBorder, onNotFound: onNotFound, onEmptyMessageChange: setEmptyMessage, notFoundComponent: notFoundComponent?.(searchQuery ?? '') }))] }), _jsx(LiveRegion, { role: "status", children: noResults ? emptyMessage : '' })] }));
150
205
  });
151
206
  const Wrapper = styled(Box) `
152
207
  position: relative;
153
208
  `;
209
+ const LiveRegion = styled.div `
210
+ ${visuallyHidden}
211
+ `;
154
212
  const Line = styled(Box) `
155
213
  background: ${({ theme }) => theme.color.border.subtle};
156
214
  height: 24px;
@@ -1 +1 @@
1
- {"version":3,"file":"SearchInput.js","sourceRoot":"","sources":["../../src/SearchInput/SearchInput.tsx"],"names":[],"mappings":"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","sourcesContent":["import Fuse, { IFuseOptions } from 'fuse.js'\nimport {\n ChangeEvent,\n FocusEvent,\n ReactNode,\n forwardRef,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport styled, { useTheme } from 'styled-components'\nimport { Box } from '../Box'\nimport { Field } from '../fields/Field'\nimport { CommonFieldProps } from '../fields/commonFieldTypes'\nimport {\n Input,\n InputLeadingIconContainer,\n} from '../fields/components/CommonInput'\nimport { useOnClickOutside } from '../hooks'\nimport { useUniqueId } from '../utils/id'\nimport { useControllableState } from '../utils/useControlledState'\nimport { SearchOptions } from './components/SearchOptions'\nimport { IconContainer } from '../sharedStyles/shared.styles'\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome'\nimport {\n faXmark,\n faChevronDown,\n faSearch,\n} from '@awesome.me/kit-46ca99185c/icons/classic/regular'\n\nexport type SearchInputItem = {\n label: string\n value: string\n tags?: any[]\n}\n\nexport interface SearchInputProps extends CommonFieldProps {\n /** Optional input className */\n name?: string\n /** Optional placeholder text */\n placeholder?: string\n /** List of input items to search on*/\n searchList: SearchInputItem[]\n /** callback to handle found item click */\n onFound: (element: string) => void\n /** optional callback to run when no results found */\n onNotFound?: (searchTerm: string) => void\n /** optional Component to render when no results found */\n notFoundComponent?: (searchTerm: string) => ReactNode\n /** optional boolean to show search icon */\n showIcon?: boolean\n /** optional boolean to show a clear search button */\n clearSearch?: boolean\n /** Optional callback to run on blur */\n onBlur?: (e: FocusEvent<HTMLInputElement>) => void\n /** Optional default value for input */\n value?: string\n /** Optional boolean to move results to a realtive position */\n resultsRelativePosition?: boolean\n /** optional boolean to add border to results */\n resultsBorder?: boolean\n /** optional boolean to enable fuzzy search via fuse.js */\n enableFuzzySearch?: boolean\n /** optional config of fuzzy search\n * passing a value to this prop, automatically enables fuzzy search\n */\n fuzzySearchOptions?: IFuseOptions<SearchInputItem>\n}\n\nconst defaultFuzzySearchOptions = {\n keys: [\n {\n name: 'label',\n weight: 0.6,\n },\n {\n name: 'tags',\n weight: 0.4,\n },\n ],\n findAllMatches: true,\n minMatchCharLength: 1,\n location: 0,\n threshold: 0.45,\n distance: 55,\n}\n\nexport const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(\n function SearchInput(\n {\n id: idProp,\n name = 'search_input',\n className = '',\n placeholder,\n searchList,\n showIcon = false,\n renderAsTitle = false,\n value,\n onBlur,\n onFound,\n onNotFound,\n notFoundComponent,\n fallbackStyle,\n resultsRelativePosition = false,\n resultsBorder = true,\n enableFuzzySearch = false,\n fuzzySearchOptions,\n clearSearch,\n ...otherProps\n },\n ref,\n ) {\n const wrapperRef = useRef(null)\n const theme = useTheme()\n const id = useUniqueId(idProp)\n const [showOptions, setShowOptions] = useState(false)\n const [selectedValue, setSelectedValue] = useControllableState<\n string | null\n >({\n initialState: null,\n stateProp: value,\n })\n const [searchQuery, setSearchQuery] = useState<string | null>(null)\n const [highlightedIndex, setHighlightedIndex] = useState(-1)\n\n const selectedValueLabel = searchList.find(\n (option) =>\n option.label === selectedValue || option.value === selectedValue,\n )?.label\n\n const handleBlur = () => {\n if (selectedValue) {\n setSearchQuery(selectedValueLabel ?? null)\n } else if (!selectedValue) {\n setSearchQuery(null)\n }\n }\n\n useOnClickOutside({\n ref: wrapperRef,\n callback: () => {\n handleBlur()\n setShowOptions(false)\n },\n })\n\n // oxlint-disable-next-line react/react-compiler\n const fuse = useMemo(() => {\n return new Fuse(searchList, {\n ...defaultFuzzySearchOptions,\n ...fuzzySearchOptions,\n })\n }, [searchList])\n\n // oxlint-disable-next-line react/react-compiler\n const filteredList = useMemo(() => {\n if (searchQuery === null || searchQuery === '') {\n return searchList\n }\n\n if (enableFuzzySearch || !!fuzzySearchOptions) {\n return fuse.search(searchQuery).map(({ item }) => item)\n }\n\n return searchList.filter(({ label }) =>\n label.toLowerCase().includes(searchQuery.toLocaleLowerCase()),\n )\n // oxlint-disable-next-line react/react-compiler\n }, [searchQuery, enableFuzzySearch, !!fuzzySearchOptions])\n\n const getDisplayedInputText = () => {\n if (searchQuery !== null) {\n return searchQuery\n }\n if (selectedValue !== null) {\n return selectedValueLabel || ''\n }\n return ''\n }\n\n const isSelected = selectedValue !== null\n const displayedInputText = getDisplayedInputText()\n\n const updateSearchQuery = (query: string | null) => {\n setSearchQuery(query)\n setHighlightedIndex(-1)\n\n if (query === null) {\n setSelectedValue(null)\n setShowOptions(false)\n } else {\n setShowOptions(true)\n }\n }\n\n const handleClick = () => {\n setShowOptions(true)\n if (searchQuery !== null) {\n updateSearchQuery(searchQuery)\n setShowOptions(true)\n } else if (selectedValue !== null && searchQuery === null) {\n setSearchQuery(selectedValueLabel || null)\n setShowOptions(true)\n }\n }\n\n const handleInputChange = (event: ChangeEvent<HTMLInputElement>): void => {\n const nextValue = event.currentTarget.value\n updateSearchQuery(nextValue)\n }\n\n const handleSelect = (nextValue: SearchInputItem): void => {\n updateSearchQuery(null)\n setSelectedValue(nextValue.label)\n onFound(nextValue.value)\n }\n\n const handleClearSearch = () => {\n updateSearchQuery(null)\n setSelectedValue(null)\n onFound('')\n }\n\n const handleCaretClick = () => {\n setShowOptions(!showOptions)\n }\n\n const handleKeyDown = (event: {\n key: string\n preventDefault: () => void\n }) => {\n if (event.key === 'Enter' && highlightedIndex !== -1) {\n event.preventDefault()\n const focusedItem = filteredList[highlightedIndex]\n handleSelect(focusedItem)\n } else if (event.key === 'ArrowDown') {\n event.preventDefault()\n const nextIndex = (highlightedIndex + 1) % filteredList.length\n setHighlightedIndex(nextIndex)\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n const prevIndex =\n (highlightedIndex - 1 + filteredList.length) % filteredList.length\n setHighlightedIndex(prevIndex)\n }\n }\n\n const showClearSearchButton =\n !!clearSearch && (!!value || !!selectedValue || !!searchQuery)\n\n return (\n <Wrapper ref={wrapperRef}>\n <Field\n className={className}\n renderAsTitle={renderAsTitle}\n htmlFor={id}\n {...otherProps}\n >\n <Box flex alignItems=\"center\" justifyContent=\"flex-start\">\n {showIcon && (\n <InputLeadingIconContainer $size={20}>\n <FontAwesomeIcon\n icon={faSearch}\n color={theme.color.text.subtle}\n />\n </InputLeadingIconContainer>\n )}\n <Input\n id={id}\n name={name}\n ref={ref}\n placeholder={placeholder}\n $error={otherProps.error}\n $frontIcon={showIcon}\n $fallbackStyle={fallbackStyle}\n autoComplete=\"off\"\n value={displayedInputText}\n onFocus={handleClick}\n onChange={handleInputChange}\n selected={isSelected}\n onClick={handleClick}\n onKeyDown={handleKeyDown}\n onBlur={(e) => {\n onBlur?.(e)\n }}\n />\n <Icons\n flex\n alignItems=\"center\"\n gap=\"space.100\"\n $clearSearch={showClearSearchButton}\n >\n {showClearSearchButton && (\n <IconContainer\n title=\"Clear search\"\n onClick={handleClearSearch}\n type=\"button\"\n as=\"button\"\n $size={20}\n style={{ cursor: 'pointer' }}\n >\n <FontAwesomeIcon\n icon={faXmark}\n color={theme.color.illustration.neutral[400]}\n />\n </IconContainer>\n )}\n <Line />\n <IconContainer\n type=\"button\"\n title=\"icon-button\"\n as=\"button\"\n onClick={handleCaretClick}\n $size={20}\n >\n <FontAwesomeIcon\n style={{\n rotate: showOptions ? '180deg' : '0deg',\n }}\n icon={faChevronDown}\n color={theme.color.illustration.neutral[400]}\n />\n </IconContainer>\n </Icons>\n </Box>\n\n {showOptions && (\n <SearchOptions\n displayedList={filteredList}\n selectedValue={selectedValue}\n highlightedIndex={highlightedIndex}\n setHighlightedIndex={setHighlightedIndex}\n onKeyDown={handleKeyDown}\n searchTerm={searchQuery || ''}\n onSelect={handleSelect}\n positionRelative={resultsRelativePosition}\n resultsBorder={resultsBorder}\n onNotFound={onNotFound}\n notFoundComponent={notFoundComponent?.(searchQuery ?? '')}\n />\n )}\n </Field>\n </Wrapper>\n )\n },\n)\n\nconst Wrapper = styled(Box)`\n position: relative;\n`\n\nconst Line = styled(Box)`\n background: ${({ theme }) => theme.color.border.subtle};\n height: 24px;\n width: 1px;\n`\n\nconst Icons = styled(Box)<{ $clearSearch: boolean }>`\n position: relative;\n right: ${({ $clearSearch }) => ($clearSearch ? '80px' : '48px')};\n margin-right: ${({ $clearSearch }) => ($clearSearch ? '-80px' : '-48px')};\n`\n"]}
1
+ {"version":3,"file":"SearchInput.js","sourceRoot":"","sources":["../../src/SearchInput/SearchInput.tsx"],"names":[],"mappings":"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","sourcesContent":["import Fuse, { IFuseOptions } from 'fuse.js'\nimport {\n ChangeEvent,\n FocusEvent,\n MouseEvent,\n ReactNode,\n forwardRef,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport styled, { useTheme } from 'styled-components'\nimport { Box } from '../Box'\nimport { Field } from '../fields/Field'\nimport { CommonFieldProps } from '../fields/commonFieldTypes'\nimport {\n Input,\n InputLeadingIconContainer,\n} from '../fields/components/CommonInput'\nimport { useOnClickOutside } from '../hooks'\nimport { useUniqueId } from '../utils/id'\nimport { useControllableState } from '../utils/useControlledState'\nimport { SearchKeyEvent, SearchOptions } from './components/SearchOptions'\nimport { visuallyHidden } from '../utils/visuallyHidden'\nimport { IconContainer } from '../sharedStyles/shared.styles'\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome'\nimport {\n faXmark,\n faChevronDown,\n faSearch,\n} from '@awesome.me/kit-46ca99185c/icons/classic/regular'\n\nexport type SearchInputItem = {\n label: string\n value: string\n tags?: any[]\n}\n\nexport interface SearchInputProps extends CommonFieldProps {\n /** Optional input className */\n name?: string\n /** Optional placeholder text */\n placeholder?: string\n /** List of input items to search on*/\n searchList: SearchInputItem[]\n /** callback to handle found item click */\n onFound: (element: string) => void\n /** optional callback to run when no results found */\n onNotFound?: (searchTerm: string) => void\n /** optional Component to render when no results found */\n notFoundComponent?: (searchTerm: string) => ReactNode\n /** optional boolean to show search icon */\n showIcon?: boolean\n /** optional boolean to show a clear search button */\n clearSearch?: boolean\n /** Optional callback to run on blur */\n onBlur?: (e: FocusEvent<HTMLInputElement>) => void\n /** Optional default value for input */\n value?: string\n /** Optional boolean to move results to a realtive position */\n resultsRelativePosition?: boolean\n /** optional boolean to add border to results */\n resultsBorder?: boolean\n /** optional boolean to enable fuzzy search via fuse.js */\n enableFuzzySearch?: boolean\n /** optional config of fuzzy search\n * passing a value to this prop, automatically enables fuzzy search\n */\n fuzzySearchOptions?: IFuseOptions<SearchInputItem>\n}\n\nconst defaultFuzzySearchOptions = {\n keys: [\n {\n name: 'label',\n weight: 0.6,\n },\n {\n name: 'tags',\n weight: 0.4,\n },\n ],\n findAllMatches: true,\n minMatchCharLength: 1,\n location: 0,\n threshold: 0.45,\n distance: 55,\n}\n\nexport const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(\n function SearchInput(\n {\n id: idProp,\n name = 'search_input',\n className = '',\n placeholder,\n searchList,\n showIcon = false,\n renderAsTitle = false,\n value,\n onBlur,\n onFound,\n onNotFound,\n notFoundComponent,\n fallbackStyle,\n resultsRelativePosition = false,\n resultsBorder = true,\n enableFuzzySearch = false,\n fuzzySearchOptions,\n clearSearch,\n ...otherProps\n },\n ref,\n ) {\n const wrapperRef = useRef(null)\n const theme = useTheme()\n const id = useUniqueId(idProp)\n const [showOptions, setShowOptions] = useState(false)\n const [selectedValue, setSelectedValue] = useControllableState<\n string | null\n >({\n initialState: null,\n stateProp: value,\n })\n const [searchQuery, setSearchQuery] = useState<string | null>(null)\n const [highlightedIndex, setHighlightedIndex] = useState(-1)\n const [emptyMessage, setEmptyMessage] = useState('')\n\n const selectedValueLabel = searchList.find(\n (option) =>\n option.label === selectedValue || option.value === selectedValue,\n )?.label\n\n const handleBlur = () => {\n setSearchQuery(null)\n }\n\n // Every way of closing the list goes through here, so a reopened list\n // never starts with a stale highlighted option.\n const closeOptions = () => {\n setShowOptions(false)\n setHighlightedIndex(-1)\n }\n\n useOnClickOutside({\n ref: wrapperRef,\n callback: () => {\n handleBlur()\n closeOptions()\n },\n })\n\n const isFuzzySearch = enableFuzzySearch || !!fuzzySearchOptions\n\n const fuse = useMemo(() => {\n if (!isFuzzySearch) {\n return null\n }\n return new Fuse(searchList, {\n ...defaultFuzzySearchOptions,\n ...fuzzySearchOptions,\n })\n }, [isFuzzySearch, searchList, fuzzySearchOptions])\n\n const filteredList = useMemo(() => {\n if (searchQuery === null || searchQuery === '') {\n return searchList\n }\n\n if (fuse) {\n return fuse.search(searchQuery).map(({ item }) => item)\n }\n\n return searchList.filter(({ label }) =>\n label.toLowerCase().includes(searchQuery.toLocaleLowerCase()),\n )\n }, [searchQuery, searchList, fuse])\n\n // If the options change under a highlighted one (e.g. searchList is\n // swapped), clear the highlight so Enter can't pick a different option.\n // Compare values, not identity, so an inline searchList that re-renders\n // with the same options keeps the highlight.\n const optionsKey = useMemo(\n () => filteredList.map((option) => option.value).join('\\n'),\n [filteredList],\n )\n const [highlightedOptionsKey, setHighlightedOptionsKey] =\n useState(optionsKey)\n if (optionsKey !== highlightedOptionsKey) {\n setHighlightedOptionsKey(optionsKey)\n setHighlightedIndex(-1)\n }\n\n const getDisplayedInputText = () => {\n if (searchQuery !== null) {\n return searchQuery\n }\n if (selectedValue !== null) {\n return selectedValueLabel || ''\n }\n return ''\n }\n\n const isSelected = selectedValue !== null\n const displayedInputText = getDisplayedInputText()\n\n const updateSearchQuery = (query: string | null) => {\n setSearchQuery(query)\n setHighlightedIndex(-1)\n\n if (query === null) {\n setSelectedValue(null)\n closeOptions()\n } else {\n setShowOptions(true)\n }\n }\n\n // Reopening a field with a selection usually means changing it, so\n // select the label and let typing replace it rather than append to it\n const shouldSelectLabel =\n !showOptions && searchQuery === null && selectedValue !== null\n\n const handleMouseDown = (event: MouseEvent<HTMLInputElement>) => {\n // Focus and select ourselves, so the browser doesn't place a caret\n // (Safari can otherwise undo the selection on mouseup)\n const input = event.currentTarget\n if (shouldSelectLabel && document.activeElement !== input) {\n event.preventDefault()\n input.focus()\n }\n }\n\n const handleClick = (event: { currentTarget: HTMLInputElement }) => {\n if (shouldSelectLabel) {\n event.currentTarget.select()\n }\n setShowOptions(true)\n if (searchQuery !== null) {\n updateSearchQuery(searchQuery)\n setShowOptions(true)\n }\n }\n\n const handleInputChange = (event: ChangeEvent<HTMLInputElement>): void => {\n const nextValue = event.currentTarget.value\n updateSearchQuery(nextValue)\n }\n\n const handleSelect = (nextValue: SearchInputItem): void => {\n updateSearchQuery(null)\n setSelectedValue(nextValue.label)\n onFound(nextValue.value)\n }\n\n const handleClearSearch = () => {\n updateSearchQuery(null)\n setSelectedValue(null)\n onFound('')\n }\n\n const handleCaretClick = () => {\n if (showOptions) {\n closeOptions()\n } else {\n setShowOptions(true)\n }\n }\n\n const listboxId = `${id}-listbox`\n const getOptionId = (index: number) => `${id}-option-${index}`\n const hasResults = showOptions && filteredList.length > 0\n const hasHighlight =\n hasResults &&\n highlightedIndex >= 0 &&\n highlightedIndex < filteredList.length\n const noResults = showOptions && filteredList.length === 0\n\n const handleKeyDown = (event: SearchKeyEvent) => {\n if (event.key === 'Escape') {\n if (!showOptions) return\n event.preventDefault()\n event.stopPropagation()\n closeOptions()\n } else if (event.key === 'Enter') {\n if (hasHighlight) {\n event.preventDefault()\n handleSelect(filteredList[highlightedIndex])\n }\n } else if (event.key === 'ArrowDown') {\n event.preventDefault()\n if (!showOptions) {\n setShowOptions(true)\n } else if (filteredList.length > 0) {\n setHighlightedIndex((highlightedIndex + 1) % filteredList.length)\n }\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n if (showOptions && filteredList.length > 0) {\n setHighlightedIndex(\n (highlightedIndex - 1 + filteredList.length) % filteredList.length,\n )\n }\n }\n }\n\n const showClearSearchButton =\n !!clearSearch && (!!value || !!selectedValue || !!searchQuery)\n\n return (\n <Wrapper ref={wrapperRef}>\n <Field\n className={className}\n renderAsTitle={renderAsTitle}\n htmlFor={id}\n {...otherProps}\n >\n <Box flex alignItems=\"center\" justifyContent=\"flex-start\">\n {showIcon && (\n <InputLeadingIconContainer $size={20}>\n <FontAwesomeIcon\n icon={faSearch}\n color={theme.color.text.subtle}\n />\n </InputLeadingIconContainer>\n )}\n <Input\n id={id}\n name={name}\n ref={ref}\n placeholder={placeholder}\n $error={otherProps.error}\n $frontIcon={showIcon}\n $fallbackStyle={fallbackStyle}\n autoComplete=\"off\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded={hasResults}\n aria-controls={hasResults ? listboxId : undefined}\n aria-activedescendant={\n hasHighlight ? getOptionId(highlightedIndex) : undefined\n }\n value={displayedInputText}\n onFocus={handleClick}\n onChange={handleInputChange}\n selected={isSelected}\n onClick={handleClick}\n onMouseDown={handleMouseDown}\n onKeyDown={(event) => {\n if (event.key === 'ArrowDown' && shouldSelectLabel) {\n event.currentTarget.select()\n }\n handleKeyDown(event)\n }}\n onBlur={(e) => {\n onBlur?.(e)\n }}\n />\n <Icons\n flex\n alignItems=\"center\"\n gap=\"space.100\"\n $clearSearch={showClearSearchButton}\n >\n {showClearSearchButton && (\n <IconContainer\n title=\"Clear search\"\n onClick={handleClearSearch}\n type=\"button\"\n as=\"button\"\n $size={20}\n style={{ cursor: 'pointer' }}\n >\n <FontAwesomeIcon\n icon={faXmark}\n color={theme.color.illustration.neutral[400]}\n />\n </IconContainer>\n )}\n <Line />\n <IconContainer\n type=\"button\"\n title=\"icon-button\"\n as=\"button\"\n onClick={handleCaretClick}\n $size={20}\n >\n <FontAwesomeIcon\n style={{\n rotate: showOptions ? '180deg' : '0deg',\n }}\n icon={faChevronDown}\n color={theme.color.illustration.neutral[400]}\n />\n </IconContainer>\n </Icons>\n </Box>\n\n {showOptions && (\n <SearchOptions\n displayedList={filteredList}\n selectedValue={selectedValue}\n highlightedIndex={highlightedIndex}\n setHighlightedIndex={setHighlightedIndex}\n onKeyDown={handleKeyDown}\n listboxId={listboxId}\n listboxLabel={otherProps.label ?? placeholder ?? 'Search results'}\n getOptionId={getOptionId}\n searchTerm={searchQuery || ''}\n onSelect={handleSelect}\n positionRelative={resultsRelativePosition}\n resultsBorder={resultsBorder}\n onNotFound={onNotFound}\n onEmptyMessageChange={setEmptyMessage}\n notFoundComponent={notFoundComponent?.(searchQuery ?? '')}\n />\n )}\n </Field>\n <LiveRegion role=\"status\">{noResults ? emptyMessage : ''}</LiveRegion>\n </Wrapper>\n )\n },\n)\n\nconst Wrapper = styled(Box)`\n position: relative;\n`\n\nconst LiveRegion = styled.div`\n ${visuallyHidden}\n`\n\nconst Line = styled(Box)`\n background: ${({ theme }) => theme.color.border.subtle};\n height: 24px;\n width: 1px;\n`\n\nconst Icons = styled(Box)<{ $clearSearch: boolean }>`\n position: relative;\n right: ${({ $clearSearch }) => ($clearSearch ? '80px' : '48px')};\n margin-right: ${({ $clearSearch }) => ($clearSearch ? '-80px' : '-48px')};\n`\n"]}
@@ -1,6 +1,7 @@
1
1
  import { FC, ReactNode } from 'react';
2
2
  interface Props {
3
3
  onNotFound?: (searchTerm: string) => void;
4
+ onMessageChange: (message: string) => void;
4
5
  searchTerm: string;
5
6
  notFoundComponent?: ReactNode;
6
7
  }
@@ -1,12 +1,45 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useEffect } from 'react';
3
- export const EmptyResults = ({ onNotFound, searchTerm, notFoundComponent, }) => {
2
+ import { useEffect, useLayoutEffect, useRef } from 'react';
3
+ const DEFAULT_MESSAGE = 'No results';
4
+ const BLOCK_SELECTOR = 'address, article, aside, blockquote, dd, div, dl, dt, figcaption, footer, h1, h2, h3, h4, h5, h6, header, li, ol, p, section, ul';
5
+ // Reads the element's text as a screen reader would: skipping hidden parts,
6
+ // and adding a space where it moves to another block or line, so a heading
7
+ // and a hint don't run together.
8
+ const getText = (element) => {
9
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
10
+ let text = '';
11
+ let previousBlock = null;
12
+ while (walker.nextNode()) {
13
+ const node = walker.currentNode;
14
+ if (node.nodeName === 'BR') {
15
+ text += ' ';
16
+ continue;
17
+ }
18
+ if (node.nodeType !== Node.TEXT_NODE)
19
+ continue;
20
+ const parent = node.parentElement;
21
+ if (parent?.closest('[aria-hidden="true"], [hidden]'))
22
+ continue;
23
+ const block = parent?.closest(BLOCK_SELECTOR) ?? null;
24
+ if (previousBlock && block !== previousBlock) {
25
+ text += ' ';
26
+ }
27
+ text += node.textContent ?? '';
28
+ previousBlock = block;
29
+ }
30
+ return text.replace(/\s+/g, ' ').trim();
31
+ };
32
+ export const EmptyResults = ({ onNotFound, onMessageChange, searchTerm, notFoundComponent, }) => {
33
+ const ref = useRef(null);
4
34
  useEffect(() => {
5
35
  onNotFound?.(searchTerm);
6
36
  }, []);
7
- if (notFoundComponent) {
8
- return _jsx("li", { children: notFoundComponent });
9
- }
10
- return _jsx("li", { children: "No results" });
37
+ // Report the text people see, so the live region announces a custom
38
+ // notFoundComponent too. A layout effect updates it before paint.
39
+ useLayoutEffect(() => {
40
+ const text = ref.current ? getText(ref.current) : '';
41
+ onMessageChange(text || DEFAULT_MESSAGE);
42
+ }, [searchTerm, notFoundComponent, onMessageChange]);
43
+ return _jsx("li", { ref: ref, children: notFoundComponent || DEFAULT_MESSAGE });
11
44
  };
12
45
  //# sourceMappingURL=EmptyResults.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"EmptyResults.js","sourceRoot":"","sources":["../../../src/SearchInput/components/EmptyResults.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAQhD,MAAM,CAAC,MAAM,YAAY,GAAc,CAAC,EACtC,UAAU,EACV,UAAU,EACV,iBAAiB,GAClB,EAAE,EAAE;IACH,SAAS,CAAC,GAAG,EAAE;QACb,UAAU,EAAE,CAAC,UAAU,CAAC,CAAA;IAC1B,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,IAAI,iBAAiB,EAAE,CAAC;QACtB,OAAO,uBAAK,iBAAiB,GAAM,CAAA;IACrC,CAAC;IAED,OAAO,sCAAmB,CAAA;AAC5B,CAAC,CAAA","sourcesContent":["import { FC, ReactNode, useEffect } from 'react'\n\ninterface Props {\n onNotFound?: (searchTerm: string) => void\n searchTerm: string\n notFoundComponent?: ReactNode\n}\n\nexport const EmptyResults: FC<Props> = ({\n onNotFound,\n searchTerm,\n notFoundComponent,\n}) => {\n useEffect(() => {\n onNotFound?.(searchTerm)\n }, [])\n\n if (notFoundComponent) {\n return <li>{notFoundComponent}</li>\n }\n\n return <li>No results</li>\n}\n"]}
1
+ {"version":3,"file":"EmptyResults.js","sourceRoot":"","sources":["../../../src/SearchInput/components/EmptyResults.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAiB,SAAS,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAEzE,MAAM,eAAe,GAAG,YAAY,CAAA;AASpC,MAAM,cAAc,GAClB,kIAAkI,CAAA;AAEpI,4EAA4E;AAC5E,2EAA2E;AAC3E,iCAAiC;AACjC,MAAM,OAAO,GAAG,CAAC,OAAoB,EAAE,EAAE;IACvC,MAAM,MAAM,GAAG,QAAQ,CAAC,gBAAgB,CACtC,OAAO,EACP,UAAU,CAAC,SAAS,GAAG,UAAU,CAAC,YAAY,CAC/C,CAAA;IACD,IAAI,IAAI,GAAG,EAAE,CAAA;IACb,IAAI,aAAa,GAAmB,IAAI,CAAA;IACxC,OAAO,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAA;QAC/B,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,EAAE,CAAC;YAC3B,IAAI,IAAI,GAAG,CAAA;YACX,SAAQ;QACV,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,SAAS;YAAE,SAAQ;QAE9C,MAAM,MAAM,GAAG,IAAI,CAAC,aAAa,CAAA;QACjC,IAAI,MAAM,EAAE,OAAO,CAAC,gCAAgC,CAAC;YAAE,SAAQ;QAE/D,MAAM,KAAK,GAAG,MAAM,EAAE,OAAO,CAAC,cAAc,CAAC,IAAI,IAAI,CAAA;QACrD,IAAI,aAAa,IAAI,KAAK,KAAK,aAAa,EAAE,CAAC;YAC7C,IAAI,IAAI,GAAG,CAAA;QACb,CAAC;QACD,IAAI,IAAI,IAAI,CAAC,WAAW,IAAI,EAAE,CAAA;QAC9B,aAAa,GAAG,KAAK,CAAA;IACvB,CAAC;IACD,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAA;AACzC,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,YAAY,GAAc,CAAC,EACtC,UAAU,EACV,eAAe,EACf,UAAU,EACV,iBAAiB,GAClB,EAAE,EAAE;IACH,MAAM,GAAG,GAAG,MAAM,CAAgB,IAAI,CAAC,CAAA;IAEvC,SAAS,CAAC,GAAG,EAAE;QACb,UAAU,EAAE,CAAC,UAAU,CAAC,CAAA;IAC1B,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,oEAAoE;IACpE,kEAAkE;IAClE,eAAe,CAAC,GAAG,EAAE;QACnB,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;QACpD,eAAe,CAAC,IAAI,IAAI,eAAe,CAAC,CAAA;IAC1C,CAAC,EAAE,CAAC,UAAU,EAAE,iBAAiB,EAAE,eAAe,CAAC,CAAC,CAAA;IAEpD,OAAO,aAAI,GAAG,EAAE,GAAG,YAAG,iBAAiB,IAAI,eAAe,GAAM,CAAA;AAClE,CAAC,CAAA","sourcesContent":["import { FC, ReactNode, useEffect, useLayoutEffect, useRef } from 'react'\n\nconst DEFAULT_MESSAGE = 'No results'\n\ninterface Props {\n onNotFound?: (searchTerm: string) => void\n onMessageChange: (message: string) => void\n searchTerm: string\n notFoundComponent?: ReactNode\n}\n\nconst BLOCK_SELECTOR =\n 'address, article, aside, blockquote, dd, div, dl, dt, figcaption, footer, h1, h2, h3, h4, h5, h6, header, li, ol, p, section, ul'\n\n// Reads the element's text as a screen reader would: skipping hidden parts,\n// and adding a space where it moves to another block or line, so a heading\n// and a hint don't run together.\nconst getText = (element: HTMLElement) => {\n const walker = document.createTreeWalker(\n element,\n NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,\n )\n let text = ''\n let previousBlock: Element | null = null\n while (walker.nextNode()) {\n const node = walker.currentNode\n if (node.nodeName === 'BR') {\n text += ' '\n continue\n }\n if (node.nodeType !== Node.TEXT_NODE) continue\n\n const parent = node.parentElement\n if (parent?.closest('[aria-hidden=\"true\"], [hidden]')) continue\n\n const block = parent?.closest(BLOCK_SELECTOR) ?? null\n if (previousBlock && block !== previousBlock) {\n text += ' '\n }\n text += node.textContent ?? ''\n previousBlock = block\n }\n return text.replace(/\\s+/g, ' ').trim()\n}\n\nexport const EmptyResults: FC<Props> = ({\n onNotFound,\n onMessageChange,\n searchTerm,\n notFoundComponent,\n}) => {\n const ref = useRef<HTMLLIElement>(null)\n\n useEffect(() => {\n onNotFound?.(searchTerm)\n }, [])\n\n // Report the text people see, so the live region announces a custom\n // notFoundComponent too. A layout effect updates it before paint.\n useLayoutEffect(() => {\n const text = ref.current ? getText(ref.current) : ''\n onMessageChange(text || DEFAULT_MESSAGE)\n }, [searchTerm, notFoundComponent, onMessageChange])\n\n return <li ref={ref}>{notFoundComponent || DEFAULT_MESSAGE}</li>\n}\n"]}
@@ -3,19 +3,25 @@ type Option = {
3
3
  label: string;
4
4
  value: string;
5
5
  };
6
+ export type SearchKeyEvent = {
7
+ key: string;
8
+ preventDefault: () => void;
9
+ stopPropagation: () => void;
10
+ };
6
11
  type SearchOptionsProps = {
7
12
  displayedList: Array<Option>;
8
13
  selectedValue: string | null;
9
14
  highlightedIndex: number;
10
15
  setHighlightedIndex: (arg: number) => void;
11
16
  onSelect: (option: Option) => void;
12
- onKeyDown: (e: {
13
- key: string;
14
- preventDefault: () => void;
15
- }) => void;
17
+ onKeyDown: (e: SearchKeyEvent) => void;
18
+ listboxId: string;
19
+ listboxLabel: string;
20
+ getOptionId: (index: number) => string;
16
21
  positionRelative: boolean;
17
22
  resultsBorder: boolean;
18
23
  onNotFound?: (searchTerm: string) => void;
24
+ onEmptyMessageChange: (message: string) => void;
19
25
  searchTerm: string;
20
26
  notFoundComponent?: ReactNode;
21
27
  };
@@ -1,10 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { createRef, useEffect, useRef } from 'react';
2
+ import { createRef, useEffect, useLayoutEffect, useRef, } from 'react';
3
3
  import styled, { css } from 'styled-components';
4
4
  import { Box } from '../../Box';
5
5
  import { Icon } from '../../Icon';
6
6
  import { EmptyResults } from './EmptyResults';
7
- export const SearchOptions = ({ displayedList, selectedValue, highlightedIndex, setHighlightedIndex, onSelect, onKeyDown, positionRelative, resultsBorder, onNotFound, searchTerm, notFoundComponent, }) => {
7
+ export const SearchOptions = ({ displayedList, selectedValue, highlightedIndex, setHighlightedIndex, onSelect, onKeyDown, listboxId, listboxLabel, getOptionId, positionRelative, resultsBorder, onNotFound, onEmptyMessageChange, searchTerm, notFoundComponent, }) => {
8
8
  const itemRefs = useRef([]);
9
9
  const resultsListRef = useRef(null);
10
10
  useEffect(() => {
@@ -19,22 +19,43 @@ export const SearchOptions = ({ displayedList, selectedValue, highlightedIndex,
19
19
  });
20
20
  }
21
21
  }, [highlightedIndex]);
22
+ // Only reset the scroll when the search term changes, not when the list
23
+ // opens, so the selected option stays centred
24
+ const scrollResetForSearchTerm = useRef(searchTerm);
22
25
  useEffect(() => {
23
- if (resultsListRef.current) {
26
+ if (resultsListRef.current &&
27
+ scrollResetForSearchTerm.current !== searchTerm) {
24
28
  resultsListRef.current.scrollTop = 0;
25
29
  }
30
+ scrollResetForSearchTerm.current = searchTerm;
26
31
  setHighlightedIndex(-1);
27
32
  }, [searchTerm, setHighlightedIndex]);
28
- return (_jsx(BoxWithPositionRelative, { children: _jsx(StyledResultsContainer, { $positionRelative: positionRelative, children: _jsx(ResultsList, { ref: resultsListRef, $resultsBorder: resultsBorder, onKeyDown: onKeyDown, children: displayedList.length ? (
33
+ // When the list opens, centre the selected option in it. This sets the
34
+ // list's own scrollTop so the page doesn't scroll.
35
+ const hasScrolledToSelected = useRef(false);
36
+ useLayoutEffect(() => {
37
+ if (hasScrolledToSelected.current)
38
+ return;
39
+ hasScrolledToSelected.current = true;
40
+ const list = resultsListRef.current;
41
+ const selectedIndex = displayedList.findIndex((el) => selectedValue === el.label || selectedValue === el.value);
42
+ const selectedItem = list?.children[selectedIndex];
43
+ if (list && selectedItem) {
44
+ list.scrollTop =
45
+ selectedItem.offsetTop -
46
+ (list.clientHeight - selectedItem.offsetHeight) / 2;
47
+ }
48
+ }, [displayedList, selectedValue]);
49
+ return (_jsx(BoxWithPositionRelative, { children: _jsx(StyledResultsContainer, { $positionRelative: positionRelative, children: _jsx(ResultsList, { ref: resultsListRef, id: listboxId, role: displayedList.length ? 'listbox' : undefined, "aria-label": displayedList.length ? listboxLabel : undefined, $resultsBorder: resultsBorder, onKeyDown: onKeyDown, children: displayedList.length ? (
29
50
  // oxlint-disable-next-line react/react-compiler
30
51
  displayedList.map((el, i) => {
31
52
  const isSelected = selectedValue === el.label || selectedValue === el.value;
32
- return (_jsxs(ListButton, { "aria-label": el.label + '_list_item', ref: itemRefs.current[i], onClick: () => onSelect(el), $isSelected: isSelected, $showBg: highlightedIndex === i, onMouseEnter: () => {
53
+ return (_jsxs(ListButton, { id: getOptionId(i), role: "option", "aria-selected": highlightedIndex === i, ref: itemRefs.current[i], onClick: () => onSelect(el), $isSelected: isSelected, $showBg: highlightedIndex === i, onMouseEnter: () => {
33
54
  setHighlightedIndex(i);
34
55
  }, onFocus: () => {
35
56
  setHighlightedIndex(i);
36
57
  }, children: [el.label, isSelected && (_jsx(Icon, { render: "tick", size: 16, color: "color.surface.brand.300" }))] }, el.label + '_list_item'));
37
- })) : (_jsx(EmptyResults, { onNotFound: onNotFound, searchTerm: searchTerm, notFoundComponent: notFoundComponent })) }) }) }));
58
+ })) : (_jsx(EmptyResults, { onNotFound: onNotFound, onMessageChange: onEmptyMessageChange, searchTerm: searchTerm, notFoundComponent: notFoundComponent })) }) }) }));
38
59
  };
39
60
  const StyledResultsContainer = styled.div `
40
61
  box-sizing: border-box;
@@ -1 +1 @@
1
- {"version":3,"file":"SearchOptions.js","sourceRoot":"","sources":["../../../src/SearchInput/components/SearchOptions.tsx"],"names":[],"mappings":"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","sourcesContent":["import { FC, ReactNode, RefObject, createRef, useEffect, useRef } from 'react'\nimport styled, { css } from 'styled-components'\nimport { TransientProps } from '../../utils/utilTypes'\nimport { Box } from '../../Box'\nimport { Icon } from '../../Icon'\nimport { EmptyResults } from './EmptyResults'\n\ntype Option = {\n label: string\n value: string\n}\n\ntype SearchOptionsProps = {\n displayedList: Array<Option>\n selectedValue: string | null\n highlightedIndex: number\n setHighlightedIndex: (arg: number) => void\n onSelect: (option: Option) => void\n onKeyDown: (e: { key: string; preventDefault: () => void }) => void\n positionRelative: boolean\n resultsBorder: boolean\n onNotFound?: (searchTerm: string) => void\n searchTerm: string\n notFoundComponent?: ReactNode\n}\n\nexport const SearchOptions: FC<SearchOptionsProps> = ({\n displayedList,\n selectedValue,\n highlightedIndex,\n setHighlightedIndex,\n onSelect,\n onKeyDown,\n positionRelative,\n resultsBorder,\n onNotFound,\n searchTerm,\n notFoundComponent,\n}) => {\n const itemRefs = useRef<RefObject<HTMLLIElement | null>[]>([])\n const resultsListRef = useRef<HTMLUListElement>(null)\n\n useEffect(() => {\n itemRefs.current = displayedList.map(\n (_, i) => itemRefs.current[i] ?? createRef<HTMLLIElement>(),\n )\n }, [displayedList.length])\n\n useEffect(() => {\n const focusedListItem = itemRefs?.current[highlightedIndex]?.current\n if (highlightedIndex >= 0 && focusedListItem) {\n focusedListItem.scrollIntoView({\n block: 'nearest',\n inline: 'start',\n })\n }\n }, [highlightedIndex])\n\n useEffect(() => {\n if (resultsListRef.current) {\n resultsListRef.current.scrollTop = 0\n }\n setHighlightedIndex(-1)\n }, [searchTerm, setHighlightedIndex])\n\n return (\n <BoxWithPositionRelative>\n <StyledResultsContainer $positionRelative={positionRelative}>\n <ResultsList\n ref={resultsListRef}\n $resultsBorder={resultsBorder}\n onKeyDown={onKeyDown}\n >\n {displayedList.length ? (\n // oxlint-disable-next-line react/react-compiler\n displayedList.map((el, i) => {\n const isSelected =\n selectedValue === el.label || selectedValue === el.value\n\n return (\n <ListButton\n key={el.label + '_list_item'}\n aria-label={el.label + '_list_item'}\n ref={itemRefs.current[i]}\n onClick={() => onSelect(el)}\n $isSelected={isSelected}\n $showBg={highlightedIndex === i}\n onMouseEnter={() => {\n setHighlightedIndex(i)\n }}\n onFocus={() => {\n setHighlightedIndex(i)\n }}\n >\n {el.label}\n {isSelected && (\n <Icon\n render=\"tick\"\n size={16}\n color=\"color.surface.brand.300\"\n />\n )}\n </ListButton>\n )\n })\n ) : (\n <EmptyResults\n onNotFound={onNotFound}\n searchTerm={searchTerm}\n notFoundComponent={notFoundComponent}\n />\n )}\n </ResultsList>\n </StyledResultsContainer>\n </BoxWithPositionRelative>\n )\n}\n\nconst StyledResultsContainer = styled.div<\n TransientProps<Pick<SearchOptionsProps, 'positionRelative'>>\n>`\n box-sizing: border-box;\n overflow: hidden;\n margin: -16px;\n padding: ${({ theme }) => theme.space[200]};\n ${({ $positionRelative }) => !$positionRelative && 'position: absolute;'}\n width: calc(100% + 32px);\n left: 0px;\n top: -8px;\n\n ul {\n max-height: 192px;\n }\n`\n\nconst ResultsList = styled.ul<\n TransientProps<Pick<SearchOptionsProps, 'resultsBorder'>>\n>`\n position: relative;\n list-style: none;\n overflow-y: auto;\n padding: 0;\n margin: 0;\n background-color: ${({ theme }) => theme.color.background['000']};\n border-radius: 12px;\n margin-top: 14px;\n z-index: 1000;\n border: 1px solid ${({ theme }) => theme.color.border.subtle};\n box-shadow:\n 0px 10px 18px rgba(18, 18, 23, 0.03),\n 0px 5px 8px rgba(18, 18, 23, 0.04),\n 0px 2px 4px rgba(18, 18, 23, 0.04);\n\n li {\n padding: 16px 14px;\n box-sizing: border-box;\n font-size: 16px;\n color: ${({ theme }) => theme.color.text.base};\n cursor: pointer;\n }\n`\n\nconst ListButton = styled.li<{ $isSelected: boolean; $showBg: boolean }>`\n display: flex;\n justify-content: space-between;\n\n &:focus {\n outline: none;\n }\n &:focus-visible {\n outline: none;\n }\n\n ${({ $showBg }) =>\n $showBg &&\n css`\n background-color: ${({ theme }) => theme.color.background[200]};\n `}\n\n ${({ $isSelected }) =>\n $isSelected &&\n css`\n background-color: ${({ theme }) => theme.color.surface.base[300]};\n `}\n`\n\nconst BoxWithPositionRelative = styled(Box)`\n position: relative;\n`\n"]}
1
+ {"version":3,"file":"SearchOptions.js","sourceRoot":"","sources":["../../../src/SearchInput/components/SearchOptions.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n FC,\n ReactNode,\n RefObject,\n createRef,\n useEffect,\n useLayoutEffect,\n useRef,\n} from 'react'\nimport styled, { css } from 'styled-components'\nimport { TransientProps } from '../../utils/utilTypes'\nimport { Box } from '../../Box'\nimport { Icon } from '../../Icon'\nimport { EmptyResults } from './EmptyResults'\n\ntype Option = {\n label: string\n value: string\n}\n\nexport type SearchKeyEvent = {\n key: string\n preventDefault: () => void\n stopPropagation: () => void\n}\n\ntype SearchOptionsProps = {\n displayedList: Array<Option>\n selectedValue: string | null\n highlightedIndex: number\n setHighlightedIndex: (arg: number) => void\n onSelect: (option: Option) => void\n onKeyDown: (e: SearchKeyEvent) => void\n listboxId: string\n listboxLabel: string\n getOptionId: (index: number) => string\n positionRelative: boolean\n resultsBorder: boolean\n onNotFound?: (searchTerm: string) => void\n onEmptyMessageChange: (message: string) => void\n searchTerm: string\n notFoundComponent?: ReactNode\n}\n\nexport const SearchOptions: FC<SearchOptionsProps> = ({\n displayedList,\n selectedValue,\n highlightedIndex,\n setHighlightedIndex,\n onSelect,\n onKeyDown,\n listboxId,\n listboxLabel,\n getOptionId,\n positionRelative,\n resultsBorder,\n onNotFound,\n onEmptyMessageChange,\n searchTerm,\n notFoundComponent,\n}) => {\n const itemRefs = useRef<RefObject<HTMLLIElement | null>[]>([])\n const resultsListRef = useRef<HTMLUListElement>(null)\n\n useEffect(() => {\n itemRefs.current = displayedList.map(\n (_, i) => itemRefs.current[i] ?? createRef<HTMLLIElement>(),\n )\n }, [displayedList.length])\n\n useEffect(() => {\n const focusedListItem = itemRefs?.current[highlightedIndex]?.current\n if (highlightedIndex >= 0 && focusedListItem) {\n focusedListItem.scrollIntoView({\n block: 'nearest',\n inline: 'start',\n })\n }\n }, [highlightedIndex])\n\n // Only reset the scroll when the search term changes, not when the list\n // opens, so the selected option stays centred\n const scrollResetForSearchTerm = useRef(searchTerm)\n useEffect(() => {\n if (\n resultsListRef.current &&\n scrollResetForSearchTerm.current !== searchTerm\n ) {\n resultsListRef.current.scrollTop = 0\n }\n scrollResetForSearchTerm.current = searchTerm\n setHighlightedIndex(-1)\n }, [searchTerm, setHighlightedIndex])\n\n // When the list opens, centre the selected option in it. This sets the\n // list's own scrollTop so the page doesn't scroll.\n const hasScrolledToSelected = useRef(false)\n useLayoutEffect(() => {\n if (hasScrolledToSelected.current) return\n hasScrolledToSelected.current = true\n\n const list = resultsListRef.current\n const selectedIndex = displayedList.findIndex(\n (el) => selectedValue === el.label || selectedValue === el.value,\n )\n const selectedItem = list?.children[selectedIndex] as\n HTMLElement | undefined\n if (list && selectedItem) {\n list.scrollTop =\n selectedItem.offsetTop -\n (list.clientHeight - selectedItem.offsetHeight) / 2\n }\n }, [displayedList, selectedValue])\n\n return (\n <BoxWithPositionRelative>\n <StyledResultsContainer $positionRelative={positionRelative}>\n <ResultsList\n ref={resultsListRef}\n id={listboxId}\n role={displayedList.length ? 'listbox' : undefined}\n aria-label={displayedList.length ? listboxLabel : undefined}\n $resultsBorder={resultsBorder}\n onKeyDown={onKeyDown}\n >\n {displayedList.length ? (\n // oxlint-disable-next-line react/react-compiler\n displayedList.map((el, i) => {\n const isSelected =\n selectedValue === el.label || selectedValue === el.value\n\n return (\n <ListButton\n key={el.label + '_list_item'}\n id={getOptionId(i)}\n role=\"option\"\n aria-selected={highlightedIndex === i}\n ref={itemRefs.current[i]}\n onClick={() => onSelect(el)}\n $isSelected={isSelected}\n $showBg={highlightedIndex === i}\n onMouseEnter={() => {\n setHighlightedIndex(i)\n }}\n onFocus={() => {\n setHighlightedIndex(i)\n }}\n >\n {el.label}\n {isSelected && (\n <Icon\n render=\"tick\"\n size={16}\n color=\"color.surface.brand.300\"\n />\n )}\n </ListButton>\n )\n })\n ) : (\n <EmptyResults\n onNotFound={onNotFound}\n onMessageChange={onEmptyMessageChange}\n searchTerm={searchTerm}\n notFoundComponent={notFoundComponent}\n />\n )}\n </ResultsList>\n </StyledResultsContainer>\n </BoxWithPositionRelative>\n )\n}\n\nconst StyledResultsContainer = styled.div<\n TransientProps<Pick<SearchOptionsProps, 'positionRelative'>>\n>`\n box-sizing: border-box;\n overflow: hidden;\n margin: -16px;\n padding: ${({ theme }) => theme.space[200]};\n ${({ $positionRelative }) => !$positionRelative && 'position: absolute;'}\n width: calc(100% + 32px);\n left: 0px;\n top: -8px;\n\n ul {\n max-height: 192px;\n }\n`\n\nconst ResultsList = styled.ul<\n TransientProps<Pick<SearchOptionsProps, 'resultsBorder'>>\n>`\n position: relative;\n list-style: none;\n overflow-y: auto;\n padding: 0;\n margin: 0;\n background-color: ${({ theme }) => theme.color.background['000']};\n border-radius: 12px;\n margin-top: 14px;\n z-index: 1000;\n border: 1px solid ${({ theme }) => theme.color.border.subtle};\n box-shadow:\n 0px 10px 18px rgba(18, 18, 23, 0.03),\n 0px 5px 8px rgba(18, 18, 23, 0.04),\n 0px 2px 4px rgba(18, 18, 23, 0.04);\n\n li {\n padding: 16px 14px;\n box-sizing: border-box;\n font-size: 16px;\n color: ${({ theme }) => theme.color.text.base};\n cursor: pointer;\n }\n`\n\nconst ListButton = styled.li<{ $isSelected: boolean; $showBg: boolean }>`\n display: flex;\n justify-content: space-between;\n\n &:focus {\n outline: none;\n }\n &:focus-visible {\n outline: none;\n }\n\n ${({ $showBg }) =>\n $showBg &&\n css`\n background-color: ${({ theme }) => theme.color.background[200]};\n `}\n\n ${({ $isSelected }) =>\n $isSelected &&\n css`\n background-color: ${({ theme }) => theme.color.surface.base[300]};\n `}\n`\n\nconst BoxWithPositionRelative = styled(Box)`\n position: relative;\n`\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrshmllw/smores-react",
3
- "version": "16.10.0",
3
+ "version": "16.10.2",
4
4
  "description": "Collection of React components used by Marshmallow Technology",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",