@mrshmllw/smores-react 16.10.0 → 16.10.1

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,20 +41,22 @@ 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
62
  // oxlint-disable-next-line react/react-compiler
@@ -90,7 +93,7 @@ export const SearchInput = forwardRef(function SearchInput({ id: idProp, name =
90
93
  setHighlightedIndex(-1);
91
94
  if (query === null) {
92
95
  setSelectedValue(null);
93
- setShowOptions(false);
96
+ closeOptions();
94
97
  }
95
98
  else {
96
99
  setShowOptions(true);
@@ -102,10 +105,6 @@ export const SearchInput = forwardRef(function SearchInput({ id: idProp, name =
102
105
  updateSearchQuery(searchQuery);
103
106
  setShowOptions(true);
104
107
  }
105
- else if (selectedValue !== null && searchQuery === null) {
106
- setSearchQuery(selectedValueLabel || null);
107
- setShowOptions(true);
108
- }
109
108
  };
110
109
  const handleInputChange = (event) => {
111
110
  const nextValue = event.currentTarget.value;
@@ -122,35 +121,63 @@ export const SearchInput = forwardRef(function SearchInput({ id: idProp, name =
122
121
  onFound('');
123
122
  };
124
123
  const handleCaretClick = () => {
125
- setShowOptions(!showOptions);
124
+ if (showOptions) {
125
+ closeOptions();
126
+ }
127
+ else {
128
+ setShowOptions(true);
129
+ }
126
130
  };
131
+ const listboxId = `${id}-listbox`;
132
+ const getOptionId = (index) => `${id}-option-${index}`;
133
+ const hasResults = showOptions && filteredList.length > 0;
134
+ const hasHighlight = hasResults &&
135
+ highlightedIndex >= 0 &&
136
+ highlightedIndex < filteredList.length;
137
+ const noResults = showOptions && filteredList.length === 0;
127
138
  const handleKeyDown = (event) => {
128
- if (event.key === 'Enter' && highlightedIndex !== -1) {
139
+ if (event.key === 'Escape') {
140
+ if (!showOptions)
141
+ return;
129
142
  event.preventDefault();
130
- const focusedItem = filteredList[highlightedIndex];
131
- handleSelect(focusedItem);
143
+ event.stopPropagation();
144
+ closeOptions();
145
+ }
146
+ else if (event.key === 'Enter') {
147
+ if (hasHighlight) {
148
+ event.preventDefault();
149
+ handleSelect(filteredList[highlightedIndex]);
150
+ }
132
151
  }
133
152
  else if (event.key === 'ArrowDown') {
134
153
  event.preventDefault();
135
- const nextIndex = (highlightedIndex + 1) % filteredList.length;
136
- setHighlightedIndex(nextIndex);
154
+ if (!showOptions) {
155
+ setShowOptions(true);
156
+ }
157
+ else if (filteredList.length > 0) {
158
+ setHighlightedIndex((highlightedIndex + 1) % filteredList.length);
159
+ }
137
160
  }
138
161
  else if (event.key === 'ArrowUp') {
139
162
  event.preventDefault();
140
- const prevIndex = (highlightedIndex - 1 + filteredList.length) % filteredList.length;
141
- setHighlightedIndex(prevIndex);
163
+ if (showOptions && filteredList.length > 0) {
164
+ setHighlightedIndex((highlightedIndex - 1 + filteredList.length) % filteredList.length);
165
+ }
142
166
  }
143
167
  };
144
168
  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 ?? '') }))] }) }));
169
+ 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, onKeyDown: handleKeyDown, onBlur: (e) => {
170
+ onBlur?.(e);
171
+ } }), _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: {
172
+ rotate: showOptions ? '180deg' : '0deg',
173
+ }, 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
174
  });
151
175
  const Wrapper = styled(Box) `
152
176
  position: relative;
153
177
  `;
178
+ const LiveRegion = styled.div `
179
+ ${visuallyHidden}
180
+ `;
154
181
  const Line = styled(Box) `
155
182
  background: ${({ theme }) => theme.color.border.subtle};
156
183
  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 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 // 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 closeOptions()\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 }\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 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 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
  };
@@ -4,7 +4,7 @@ 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(() => {
@@ -25,16 +25,16 @@ export const SearchOptions = ({ displayedList, selectedValue, highlightedIndex,
25
25
  }
26
26
  setHighlightedIndex(-1);
27
27
  }, [searchTerm, setHighlightedIndex]);
28
- return (_jsx(BoxWithPositionRelative, { children: _jsx(StyledResultsContainer, { $positionRelative: positionRelative, children: _jsx(ResultsList, { ref: resultsListRef, $resultsBorder: resultsBorder, onKeyDown: onKeyDown, children: displayedList.length ? (
28
+ 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
29
  // oxlint-disable-next-line react/react-compiler
30
30
  displayedList.map((el, i) => {
31
31
  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: () => {
32
+ 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
33
  setHighlightedIndex(i);
34
34
  }, onFocus: () => {
35
35
  setHighlightedIndex(i);
36
36
  }, 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 })) }) }) }));
37
+ })) : (_jsx(EmptyResults, { onNotFound: onNotFound, onMessageChange: onEmptyMessageChange, searchTerm: searchTerm, notFoundComponent: notFoundComponent })) }) }) }));
38
38
  };
39
39
  const StyledResultsContainer = styled.div `
40
40
  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":";AAAA,OAAO,EAA4B,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAC9E,OAAO,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAA;AAE/C,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAC/B,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AACjC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AA+B7C,MAAM,CAAC,MAAM,aAAa,GAA2B,CAAC,EACpD,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,mBAAmB,EACnB,QAAQ,EACR,SAAS,EACT,SAAS,EACT,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,aAAa,EACb,UAAU,EACV,oBAAoB,EACpB,UAAU,EACV,iBAAiB,GAClB,EAAE,EAAE;IACH,MAAM,QAAQ,GAAG,MAAM,CAAoC,EAAE,CAAC,CAAA;IAC9D,MAAM,cAAc,GAAG,MAAM,CAAmB,IAAI,CAAC,CAAA;IAErD,SAAS,CAAC,GAAG,EAAE;QACb,QAAQ,CAAC,OAAO,GAAG,aAAa,CAAC,GAAG,CAClC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,SAAS,EAAiB,CAC5D,CAAA;IACH,CAAC,EAAE,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAA;IAE1B,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,eAAe,GAAG,QAAQ,EAAE,OAAO,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;QACpE,IAAI,gBAAgB,IAAI,CAAC,IAAI,eAAe,EAAE,CAAC;YAC7C,eAAe,CAAC,cAAc,CAAC;gBAC7B,KAAK,EAAE,SAAS;gBAChB,MAAM,EAAE,OAAO;aAChB,CAAC,CAAA;QACJ,CAAC;IACH,CAAC,EAAE,CAAC,gBAAgB,CAAC,CAAC,CAAA;IAEtB,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,cAAc,CAAC,OAAO,EAAE,CAAC;YAC3B,cAAc,CAAC,OAAO,CAAC,SAAS,GAAG,CAAC,CAAA;QACtC,CAAC;QACD,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAA;IACzB,CAAC,EAAE,CAAC,UAAU,EAAE,mBAAmB,CAAC,CAAC,CAAA;IAErC,OAAO,CACL,KAAC,uBAAuB,cACtB,KAAC,sBAAsB,IAAC,iBAAiB,EAAE,gBAAgB,YACzD,KAAC,WAAW,IACV,GAAG,EAAE,cAAc,EACnB,EAAE,EAAE,SAAS,EACb,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,gBACtC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,EAC3D,cAAc,EAAE,aAAa,EAC7B,SAAS,EAAE,SAAS,YAEnB,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;gBACtB,gDAAgD;gBAChD,aAAa,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE;oBAC1B,MAAM,UAAU,GACd,aAAa,KAAK,EAAE,CAAC,KAAK,IAAI,aAAa,KAAK,EAAE,CAAC,KAAK,CAAA;oBAE1D,OAAO,CACL,MAAC,UAAU,IAET,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,EAClB,IAAI,EAAC,QAAQ,mBACE,gBAAgB,KAAK,CAAC,EACrC,GAAG,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EACxB,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,EAC3B,WAAW,EAAE,UAAU,EACvB,OAAO,EAAE,gBAAgB,KAAK,CAAC,EAC/B,YAAY,EAAE,GAAG,EAAE;4BACjB,mBAAmB,CAAC,CAAC,CAAC,CAAA;wBACxB,CAAC,EACD,OAAO,EAAE,GAAG,EAAE;4BACZ,mBAAmB,CAAC,CAAC,CAAC,CAAA;wBACxB,CAAC,aAEA,EAAE,CAAC,KAAK,EACR,UAAU,IAAI,CACb,KAAC,IAAI,IACH,MAAM,EAAC,MAAM,EACb,IAAI,EAAE,EAAE,EACR,KAAK,EAAC,yBAAyB,GAC/B,CACH,KAtBI,EAAE,CAAC,KAAK,GAAG,YAAY,CAuBjB,CACd,CAAA;gBACH,CAAC,CAAC,CACH,CAAC,CAAC,CAAC,CACF,KAAC,YAAY,IACX,UAAU,EAAE,UAAU,EACtB,eAAe,EAAE,oBAAoB,EACrC,UAAU,EAAE,UAAU,EACtB,iBAAiB,EAAE,iBAAiB,GACpC,CACH,GACW,GACS,GACD,CAC3B,CAAA;AACH,CAAC,CAAA;AAED,MAAM,sBAAsB,GAAG,MAAM,CAAC,GAAG,CAExC;;;;aAIY,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC;IACxC,CAAC,EAAE,iBAAiB,EAAE,EAAE,EAAE,CAAC,CAAC,iBAAiB,IAAI,qBAAqB;;;;;;;;CAQzE,CAAA;AAED,MAAM,WAAW,GAAG,MAAM,CAAC,EAAE,CAE5B;;;;;;sBAMqB,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC;;;;sBAI5C,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM;;;;;;;;;;aAUjD,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI;;;CAGhD,CAAA;AAED,MAAM,UAAU,GAAG,MAAM,CAAC,EAAE,CAA4C;;;;;;;;;;;IAWpE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAChB,OAAO;IACP,GAAG,CAAA;0BACmB,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;KAC/D;;IAED,CAAC,EAAE,WAAW,EAAE,EAAE,EAAE,CACpB,WAAW;IACX,GAAG,CAAA;0BACmB,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;KACjE;CACJ,CAAA;AAED,MAAM,uBAAuB,GAAG,MAAM,CAAC,GAAG,CAAC,CAAA;;CAE1C,CAAA","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\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 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 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.1",
4
4
  "description": "Collection of React components used by Marshmallow Technology",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",