@redocly/theme 0.42.3 → 0.44.0

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.
Files changed (179) hide show
  1. package/lib/components/Buttons/CopyButton.js +2 -2
  2. package/lib/components/Buttons/EditPageButton.js +1 -1
  3. package/lib/components/Catalog/CatalogActions.js +1 -1
  4. package/lib/components/Dropdown/DropdownMenu.d.ts +2 -0
  5. package/lib/components/Dropdown/DropdownMenu.js +3 -1
  6. package/lib/components/Feedback/Comment.js +6 -6
  7. package/lib/components/Feedback/Mood.js +7 -7
  8. package/lib/components/Feedback/Rating.js +4 -4
  9. package/lib/components/Feedback/Reasons.js +3 -3
  10. package/lib/components/Feedback/Scale.js +10 -10
  11. package/lib/components/Feedback/Sentiment.js +5 -5
  12. package/lib/components/Filter/FilterContent.js +2 -2
  13. package/lib/components/Filter/FilterInput.js +1 -1
  14. package/lib/components/Filter/FilterPopover.js +2 -2
  15. package/lib/components/Filter/FilterSelect.js +1 -1
  16. package/lib/components/Footer/FooterCopyright.js +2 -2
  17. package/lib/components/LastUpdated/LastUpdated.js +1 -1
  18. package/lib/components/Loaders/SpinnerLoader.d.ts +5 -0
  19. package/lib/components/Loaders/SpinnerLoader.js +32 -0
  20. package/lib/components/PageNavigation/NextButton.js +1 -1
  21. package/lib/components/PageNavigation/PreviousButton.js +1 -1
  22. package/lib/components/Product/ProductPicker.js +1 -1
  23. package/lib/components/Search/FilterFields/SearchFilterFieldSelect.d.ts +12 -0
  24. package/lib/components/Search/FilterFields/SearchFilterFieldSelect.js +113 -0
  25. package/lib/components/Search/FilterFields/SearchFilterFieldTags.d.ts +10 -0
  26. package/lib/components/Search/FilterFields/SearchFilterFieldTags.js +37 -0
  27. package/lib/components/Search/Search.js +1 -1
  28. package/lib/components/Search/SearchDialog.js +113 -31
  29. package/lib/components/Search/SearchFilter.d.ts +11 -0
  30. package/lib/components/Search/SearchFilter.js +71 -0
  31. package/lib/components/Search/SearchFilterField.d.ts +11 -0
  32. package/lib/components/Search/SearchFilterField.js +43 -0
  33. package/lib/components/Search/SearchGroups.d.ts +9 -0
  34. package/lib/components/Search/SearchGroups.js +69 -0
  35. package/lib/components/Search/SearchHighlight.d.ts +1 -1
  36. package/lib/components/Search/SearchHighlight.js +28 -5
  37. package/lib/components/Search/SearchInput.d.ts +1 -1
  38. package/lib/components/Search/SearchInput.js +5 -2
  39. package/lib/components/Search/SearchItem.d.ts +2 -2
  40. package/lib/components/Search/SearchItem.js +24 -15
  41. package/lib/components/Search/SearchRecent.js +1 -1
  42. package/lib/components/Search/SearchSuggestedPages.js +1 -1
  43. package/lib/components/Search/SearchTrigger.js +2 -2
  44. package/lib/components/Search/variables.js +48 -2
  45. package/lib/components/Segmented/Segmented.d.ts +2 -5
  46. package/lib/components/Select/Select.d.ts +2 -36
  47. package/lib/components/Select/Select.js +136 -98
  48. package/lib/components/Select/SelectInput.d.ts +23 -0
  49. package/lib/components/Select/SelectInput.js +129 -0
  50. package/lib/components/Select/variables.js +12 -1
  51. package/lib/components/SidebarActions/ChangeViewButton.js +1 -1
  52. package/lib/components/SidebarActions/SidebarActions.js +2 -2
  53. package/lib/components/TableOfContent/TableOfContent.js +1 -1
  54. package/lib/components/Tag/Tag.d.ts +4 -2
  55. package/lib/components/Tag/Tag.js +40 -4
  56. package/lib/components/Tag/variables.dark.js +20 -5
  57. package/lib/components/Tag/variables.js +49 -17
  58. package/lib/components/UserMenu/LoginButton.js +1 -1
  59. package/lib/components/UserMenu/LogoutMenuItem.js +1 -1
  60. package/lib/components/UserMenu/UserMenu.js +1 -1
  61. package/lib/components/VersionPicker/VersionPicker.d.ts +2 -3
  62. package/lib/components/VersionPicker/VersionPicker.js +14 -31
  63. package/lib/core/hooks/__mocks__/index.d.ts +2 -1
  64. package/lib/core/hooks/__mocks__/index.js +2 -1
  65. package/lib/core/hooks/__mocks__/search/use-search-filter.d.ts +9 -0
  66. package/lib/core/hooks/__mocks__/search/use-search-filter.js +14 -0
  67. package/lib/core/hooks/__mocks__/use-theme-hooks.d.ts +6 -1
  68. package/lib/core/hooks/__mocks__/use-theme-hooks.js +6 -1
  69. package/lib/core/hooks/feedback/use-report-dialog.js +3 -3
  70. package/lib/core/hooks/index.d.ts +2 -1
  71. package/lib/core/hooks/index.js +2 -1
  72. package/lib/core/hooks/menu/use-mobile-menu-items.js +1 -1
  73. package/lib/core/hooks/menu/use-mobile-menu-levels.js +2 -2
  74. package/lib/core/hooks/search/use-recent-searches.js +2 -0
  75. package/lib/core/hooks/{use-search.d.ts → search/use-search-dialog.d.ts} +1 -1
  76. package/lib/core/hooks/{use-search.js → search/use-search-dialog.js} +5 -5
  77. package/lib/core/hooks/search/use-search-filter.d.ts +9 -0
  78. package/lib/core/hooks/search/use-search-filter.js +50 -0
  79. package/lib/core/types/hooks.d.ts +17 -4
  80. package/lib/core/types/index.d.ts +1 -1
  81. package/lib/core/types/index.js +1 -1
  82. package/lib/core/types/l10n.d.ts +1 -2
  83. package/lib/core/types/search.d.ts +42 -2
  84. package/lib/core/types/select.d.ts +31 -0
  85. package/lib/core/types/{select-option.js → select.js} +1 -1
  86. package/lib/core/utils/index.d.ts +1 -0
  87. package/lib/core/utils/index.js +1 -0
  88. package/lib/core/utils/menu.js +1 -1
  89. package/lib/core/utils/text-trimmer.d.ts +1 -0
  90. package/lib/core/utils/text-trimmer.js +16 -0
  91. package/lib/icons/ResetIcon/ResetIcon.d.ts +9 -0
  92. package/lib/icons/ResetIcon/ResetIcon.js +22 -0
  93. package/lib/icons/SettingsIcon/SettingsIcon.d.ts +9 -0
  94. package/lib/icons/SettingsIcon/SettingsIcon.js +23 -0
  95. package/lib/index.d.ts +8 -1
  96. package/lib/index.js +8 -1
  97. package/lib/layouts/Forbidden.js +2 -2
  98. package/lib/layouts/NotFound.js +3 -3
  99. package/lib/layouts/OIDCForbidden.js +1 -1
  100. package/lib/markdoc/tags/partial.js +1 -1
  101. package/package.json +9 -9
  102. package/src/components/Buttons/CopyButton.tsx +2 -2
  103. package/src/components/Buttons/EditPageButton.tsx +2 -2
  104. package/src/components/Catalog/CatalogActions.tsx +2 -2
  105. package/src/components/Dropdown/DropdownMenu.tsx +2 -1
  106. package/src/components/Feedback/Comment.tsx +8 -8
  107. package/src/components/Feedback/Mood.tsx +8 -8
  108. package/src/components/Feedback/Rating.tsx +5 -5
  109. package/src/components/Feedback/Reasons.tsx +4 -4
  110. package/src/components/Feedback/Scale.tsx +13 -13
  111. package/src/components/Feedback/Sentiment.tsx +6 -6
  112. package/src/components/Filter/FilterContent.tsx +3 -3
  113. package/src/components/Filter/FilterInput.tsx +1 -1
  114. package/src/components/Filter/FilterPopover.tsx +3 -3
  115. package/src/components/Filter/FilterSelect.tsx +5 -5
  116. package/src/components/Footer/FooterCopyright.tsx +3 -3
  117. package/src/components/LastUpdated/LastUpdated.tsx +1 -2
  118. package/src/components/Loaders/SpinnerLoader.tsx +31 -0
  119. package/src/components/PageNavigation/NextButton.tsx +1 -1
  120. package/src/components/PageNavigation/PreviousButton.tsx +1 -1
  121. package/src/components/Product/ProductPicker.tsx +2 -2
  122. package/src/components/Search/FilterFields/SearchFilterFieldSelect.tsx +135 -0
  123. package/src/components/Search/FilterFields/SearchFilterFieldTags.tsx +61 -0
  124. package/src/components/Search/Search.tsx +2 -2
  125. package/src/components/Search/SearchDialog.tsx +190 -51
  126. package/src/components/Search/SearchFilter.tsx +90 -0
  127. package/src/components/Search/SearchFilterField.tsx +84 -0
  128. package/src/components/Search/SearchGroups.tsx +81 -0
  129. package/src/components/Search/SearchHighlight.tsx +29 -2
  130. package/src/components/Search/SearchInput.tsx +9 -3
  131. package/src/components/Search/SearchItem.tsx +39 -24
  132. package/src/components/Search/SearchRecent.tsx +2 -2
  133. package/src/components/Search/SearchSuggestedPages.tsx +2 -2
  134. package/src/components/Search/SearchTrigger.tsx +2 -2
  135. package/src/components/Search/variables.ts +48 -2
  136. package/src/components/Segmented/Segmented.tsx +2 -2
  137. package/src/components/Select/Select.tsx +208 -157
  138. package/src/components/Select/SelectInput.tsx +201 -0
  139. package/src/components/Select/variables.ts +12 -1
  140. package/src/components/SidebarActions/ChangeViewButton.tsx +1 -1
  141. package/src/components/SidebarActions/SidebarActions.tsx +2 -2
  142. package/src/components/TableOfContent/TableOfContent.tsx +2 -2
  143. package/src/components/Tag/Tag.tsx +57 -6
  144. package/src/components/Tag/variables.dark.ts +20 -5
  145. package/src/components/Tag/variables.ts +49 -17
  146. package/src/components/UserMenu/LoginButton.tsx +2 -2
  147. package/src/components/UserMenu/LogoutMenuItem.tsx +2 -2
  148. package/src/components/UserMenu/UserMenu.tsx +2 -2
  149. package/src/components/VersionPicker/VersionPicker.tsx +18 -42
  150. package/src/core/hooks/__mocks__/index.ts +2 -1
  151. package/src/core/hooks/__mocks__/search/use-search-filter.ts +10 -0
  152. package/src/core/hooks/__mocks__/use-theme-hooks.ts +6 -1
  153. package/src/core/hooks/feedback/use-report-dialog.ts +3 -3
  154. package/src/core/hooks/index.ts +2 -1
  155. package/src/core/hooks/menu/use-mobile-menu-items.ts +1 -1
  156. package/src/core/hooks/menu/use-mobile-menu-levels.ts +2 -2
  157. package/src/core/hooks/search/use-recent-searches.ts +3 -0
  158. package/src/core/hooks/{use-search.ts → search/use-search-dialog.ts} +1 -1
  159. package/src/core/hooks/search/use-search-filter.ts +57 -0
  160. package/src/core/types/hooks.ts +25 -4
  161. package/src/core/types/index.ts +1 -1
  162. package/src/core/types/l10n.ts +169 -97
  163. package/src/core/types/search.ts +53 -2
  164. package/src/core/types/select.ts +33 -0
  165. package/src/core/utils/index.ts +1 -0
  166. package/src/core/utils/menu.ts +1 -1
  167. package/src/core/utils/text-trimmer.ts +7 -0
  168. package/src/icons/ResetIcon/ResetIcon.tsx +26 -0
  169. package/src/icons/SettingsIcon/SettingsIcon.tsx +30 -0
  170. package/src/index.ts +8 -1
  171. package/src/layouts/Forbidden.tsx +4 -9
  172. package/src/layouts/NotFound.tsx +6 -6
  173. package/src/layouts/OIDCForbidden.tsx +2 -2
  174. package/src/markdoc/tags/partial.ts +1 -1
  175. package/lib/core/types/select-option.d.ts +0 -4
  176. package/src/core/types/select-option.ts +0 -4
  177. /package/lib/components/{Loading → Loaders}/Loading.d.ts +0 -0
  178. /package/lib/components/{Loading → Loaders}/Loading.js +0 -0
  179. /package/src/components/{Loading → Loaders}/Loading.tsx +0 -0
@@ -1,34 +1,14 @@
1
- import React, { useRef, useState } from 'react';
1
+ import React, { Fragment, useEffect, useId, useRef, useState } from 'react';
2
2
  import styled from 'styled-components';
3
3
 
4
- import { useOutsideClick } from '@redocly/theme/core/hooks';
5
- import { ChevronDownIcon } from '@redocly/theme/icons/ChevronDownIcon/ChevronDownIcon';
6
- import { ChevronUpIcon } from '@redocly/theme/icons/ChevronUpIcon/ChevronUpIcon';
7
- import { CheckmarkIcon } from '@redocly/theme/icons/CheckmarkIcon/CheckmarkIcon';
4
+ import type { SelectOption, SelectProps } from '@redocly/theme/core/types/select';
8
5
 
9
- export type SelectProps<T = any> = {
10
- value: T;
11
- options: {
12
- element: React.ReactNode | JSX.Element | string;
13
- value: T;
14
- label?: string;
15
- }[];
16
- dataAttributes?: Record<string, string>;
17
- className?: string;
18
- withArrow?: boolean;
19
- triggerEvent?: 'click' | 'hover';
20
- onChange?: (value: any) => void;
21
- placement?: 'top' | 'bottom';
22
- alignment?: 'start' | 'end';
23
- icon?: React.ReactNode;
24
- onlyIcon?: boolean;
25
- placeholder?: string;
26
- disabled?: boolean;
27
- hideCheckmarkIcon?: boolean;
28
- dataTestId?: string;
29
- renderInput?: (props: { isOpen: boolean }) => React.ReactElement;
30
- renderDivider?: () => React.ReactElement;
31
- };
6
+ import { CheckmarkIcon } from '@redocly/theme/icons/CheckmarkIcon/CheckmarkIcon';
7
+ import { SelectInput } from '@redocly/theme/components/Select/SelectInput';
8
+ import { Dropdown } from '@redocly/theme/components/Dropdown/Dropdown';
9
+ import { DropdownMenu } from '@redocly/theme/components/Dropdown/DropdownMenu';
10
+ import { DropdownMenuItem } from '@redocly/theme/components/Dropdown/DropdownMenuItem';
11
+ import { useOutsideClick } from '@redocly/theme/core/hooks';
32
12
 
33
13
  export function Select<T>(props: SelectProps<T>): JSX.Element {
34
14
  const {
@@ -38,7 +18,6 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
38
18
  dataAttributes,
39
19
  withArrow = true,
40
20
  triggerEvent = 'click',
41
- onChange,
42
21
  placement,
43
22
  alignment,
44
23
  icon,
@@ -46,102 +25,229 @@ export function Select<T>(props: SelectProps<T>): JSX.Element {
46
25
  disabled,
47
26
  placeholder,
48
27
  hideCheckmarkIcon,
28
+ checkmarkIconPosition,
29
+ dataTestId = 'select',
30
+ multiple,
31
+ searchable,
32
+ clearable,
33
+ footer,
34
+ onChange,
35
+ onSearch,
49
36
  renderInput,
50
37
  renderDivider,
51
- dataTestId = 'select',
52
38
  } = props;
53
39
 
54
- const containerRef = useRef<HTMLDivElement | null>(null);
40
+ const getSelectedOptionsFromPropsValue = () => {
41
+ const values = Array.isArray(value) ? value : [value];
42
+ return values
43
+ .map((value) => {
44
+ const selectedOption = options.find((option) => option.value === value);
45
+ return selectedOption || typeof value === 'string'
46
+ ? ({ value } as SelectOption<T>)
47
+ : (value as SelectOption<T>);
48
+ })
49
+ .filter((option) => !!option);
50
+ };
55
51
 
56
- const [isOpen, setIsOpen] = useState<boolean>(false);
57
- // const [selectedIdx, setSelectedIdx] = useState<React.ReactNode | string>(selected);
52
+ const [selectedOptions, setSelectedOptions] = useState<SelectOption<T>[]>(
53
+ getSelectedOptionsFromPropsValue(),
54
+ );
55
+ const selectRef = useRef<HTMLDivElement | null>(null);
56
+ const [searchValue, setSearchValue] = useState<string | null>(null);
57
+ const [dropdownActive, setDropdownActive] = useState<boolean | undefined>(false);
58
+ const [filteredOptions, setFilteredOptions] = useState<SelectOption<T>[]>(options);
59
+ const [stickyInputValue, setStickyInputValue] = useState<string>(placeholder || '');
60
+ const inputId = useId();
58
61
 
59
- const handleOpen = () => {
60
- setIsOpen(true);
62
+ useOutsideClick(selectRef, () => {
63
+ setDropdownActive(false);
64
+ });
65
+
66
+ useEffect(() => {
67
+ setFilteredOptions(options);
68
+ }, [options]);
69
+
70
+ useEffect(() => {
71
+ setSelectedOptions(getSelectedOptionsFromPropsValue());
72
+ // eslint-disable-next-line react-hooks/exhaustive-deps
73
+ }, [multiple, value]);
74
+
75
+ useEffect(() => {
76
+ if (onSearch) {
77
+ onSearch?.(searchValue as T);
78
+ } else {
79
+ if (typeof searchValue === 'string') {
80
+ const filteredOptions = options.filter((option) => {
81
+ const valueForSearch = String(option.label ?? option.value ?? '');
82
+ return (
83
+ !valueForSearch || valueForSearch.toLowerCase().indexOf(searchValue.toLowerCase()) > -1
84
+ );
85
+ });
86
+ setFilteredOptions(filteredOptions);
87
+ } else {
88
+ setFilteredOptions(options);
89
+ }
90
+ }
91
+ // eslint-disable-next-line react-hooks/exhaustive-deps
92
+ }, [searchValue]);
93
+
94
+ const selectHandler = (selectedOption: SelectOption<T>) => {
95
+ const newSelectedOptions = multiple
96
+ ? isSelected(selectedOption)
97
+ ? selectedOptions.filter((option) => option.value !== selectedOption.value)
98
+ : [...selectedOptions, selectedOption]
99
+ : [selectedOption];
100
+
101
+ const newSelectedValues = newSelectedOptions.length
102
+ ? multiple
103
+ ? newSelectedOptions.map((o) => o.value)
104
+ : newSelectedOptions[0].value
105
+ : multiple
106
+ ? []
107
+ : ('' as T);
108
+
109
+ setSelectedOptions(newSelectedOptions);
110
+ onChange?.(newSelectedValues);
111
+ setSearchValue(null);
112
+ setDropdownActive(false);
113
+ if (!multiple) {
114
+ setStickyInputValue('');
115
+ }
61
116
  };
62
117
 
63
- const handleClose = () => {
64
- setIsOpen(false);
118
+ const searchHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
119
+ const targetValue = e.target.value;
120
+ setSearchValue(targetValue);
121
+ setDropdownActive(true);
65
122
  };
66
123
 
67
- const handleToggle = () => {
68
- setIsOpen(!isOpen);
124
+ const clearHandler = (option?: SelectOption<T>) => {
125
+ if (option) {
126
+ selectHandler(option);
127
+ } else {
128
+ if (!multiple) {
129
+ setStickyInputValue('');
130
+ }
131
+ setSelectedOptions([]);
132
+ onChange?.(multiple ? [] : ('' as T));
133
+ }
69
134
  };
70
135
 
71
- const handleSelect = (value: T) => {
72
- // setSelectedIdx(options.findIndex(o => o.value === value));
73
- setIsOpen(false);
74
- onChange?.(value);
136
+ const inputBlurHandler = (e: React.FocusEvent) => {
137
+ const relatedTarget = e.relatedTarget as HTMLElement;
138
+ const isDropdownItem =
139
+ relatedTarget?.attributes.getNamedItem('data-component-name')?.value ===
140
+ 'Dropdown/DropdownMenuItem';
141
+ if (!isDropdownItem) {
142
+ if (!e.relatedTarget || e.relatedTarget?.id !== inputId) {
143
+ setDropdownActive(false);
144
+ }
145
+ setSearchValue(null);
146
+ setStickyInputValue('');
147
+ }
75
148
  };
76
149
 
77
- useOutsideClick(containerRef, handleClose);
78
-
79
- // useEffect(() => {
80
- // handleSelect(selected);
81
- // // eslint-disable-next-line react-hooks/exhaustive-deps
82
- // }, [selected]);
83
-
84
- const selectedOption = options.find((option) => option.value === value);
85
-
86
- const renderDefaultInput = () => (
87
- <SelectInput>
88
- {!onlyIcon && (
89
- <SelectInputValue>
90
- {selectedOption?.label || selectedOption?.element || placeholder}
91
- </SelectInputValue>
92
- )}
93
- {icon}
94
- {withArrow ? isOpen ? <ChevronUpIcon size="14px" /> : <ChevronDownIcon size="14px" /> : null}
95
- </SelectInput>
96
- );
150
+ const inputFocusHandler = () => {
151
+ if (!multiple && selectedOptions.length) {
152
+ setStickyInputValue(selectedOptions[0].label || (selectedOptions[0].value as string));
153
+ }
154
+ };
155
+
156
+ const inputClickHandler = () => {
157
+ setDropdownActive(!dropdownActive);
158
+ };
159
+
160
+ const isSelected = (option: SelectOption<T>) => {
161
+ return !!selectedOptions.find(
162
+ (selectOption) => selectOption.value === option.value || selectOption.value === option.label,
163
+ );
164
+ };
165
+
166
+ const renderDefaultInput = () => {
167
+ return (
168
+ <SelectInput
169
+ id={inputId}
170
+ selectedOptions={selectedOptions}
171
+ searchValue={searchValue}
172
+ placeholder={placeholder}
173
+ stickyValue={stickyInputValue}
174
+ multiple={multiple}
175
+ searchable={searchable}
176
+ clearable={clearable}
177
+ customIcon={icon}
178
+ onlyIcon={onlyIcon}
179
+ clearHandler={clearHandler}
180
+ searchHandler={searchHandler}
181
+ inputBlurHandler={inputBlurHandler}
182
+ inputFocusHandler={inputFocusHandler}
183
+ clickHandler={inputClickHandler}
184
+ />
185
+ );
186
+ };
97
187
 
98
188
  return (
99
- <SelectContainer
189
+ <SelectWrapper
100
190
  data-component-name="Select/Select"
191
+ ref={selectRef}
101
192
  {...dataAttributes}
102
193
  disabled={disabled}
103
194
  data-testid={dataTestId}
104
195
  className={className}
105
- ref={containerRef}
106
- onPointerEnter={triggerEvent === 'hover' ? handleOpen : undefined}
107
- onPointerLeave={triggerEvent === 'hover' ? handleClose : undefined}
108
- onClick={triggerEvent === 'click' ? handleToggle : undefined}
109
196
  >
110
- {renderInput ? renderInput({ isOpen }) : renderDefaultInput()}
111
- {isOpen && (
112
- <SelectList placement={placement} alignment={alignment}>
113
- {options.map((option, index) => {
114
- const selected = option.value === value;
115
- return (
116
- <>
117
- <SelectListItem
118
- key={index}
119
- onClick={() => handleSelect(option.value)}
120
- selected={selected}
121
- >
122
- {typeof option.element === 'string' ? (
123
- <div>{option.element}</div>
124
- ) : (
125
- option.element
126
- )}
127
- {!hideCheckmarkIcon && selected && <CheckmarkIcon />}
128
- </SelectListItem>
129
- {renderDivider && index !== options.length - 1 ? renderDivider() : null}
130
- </>
131
- );
132
- })}
133
- </SelectList>
134
- )}
135
- </SelectContainer>
197
+ <SelectDropdown
198
+ closeOnClick={!multiple}
199
+ withArrow={withArrow}
200
+ trigger={renderInput ? renderInput() : renderDefaultInput()}
201
+ triggerEvent={triggerEvent}
202
+ placement={placement}
203
+ alignment={alignment}
204
+ active={!renderInput ? dropdownActive : undefined}
205
+ >
206
+ <SelectDropdownMenu footer={footer}>
207
+ {filteredOptions.length === 0 ? (
208
+ <DropdownMenuItem disabled>No results</DropdownMenuItem>
209
+ ) : (
210
+ filteredOptions.map((option, index) => {
211
+ return (
212
+ <Fragment key={index}>
213
+ <DropdownMenuItem
214
+ onAction={() => selectHandler(option)}
215
+ prefix={
216
+ !hideCheckmarkIcon &&
217
+ (checkmarkIconPosition ? checkmarkIconPosition === 'start' : false) &&
218
+ isSelected(option) && <CheckmarkIcon />
219
+ }
220
+ suffix={
221
+ !hideCheckmarkIcon &&
222
+ (checkmarkIconPosition ? checkmarkIconPosition === 'end' : true) &&
223
+ isSelected(option) && <CheckmarkIcon />
224
+ }
225
+ >
226
+ {typeof option.element === 'string' ? (
227
+ <div>{option.element}</div>
228
+ ) : (
229
+ option.element
230
+ )}
231
+ </DropdownMenuItem>
232
+ {renderDivider && index !== options.length - 1 ? renderDivider() : null}
233
+ </Fragment>
234
+ );
235
+ }) || 'No results'
236
+ )}
237
+ </SelectDropdownMenu>
238
+ </SelectDropdown>
239
+ </SelectWrapper>
136
240
  );
137
241
  }
138
242
 
139
- export const SelectContainer = styled.div<{ disabled?: boolean }>`
243
+ export const SelectWrapper = styled.div<{ disabled?: boolean }>`
244
+ display: flex;
140
245
  position: relative;
141
246
  font-size: var(--select-font-size);
142
247
  font-weight: var(--select-font-weight);
143
248
  line-height: var(--select-line-height);
144
249
  border-radius: var(--select-border-radius);
250
+ border: var(--select-border);
145
251
  color: var(--select-text-color);
146
252
  min-width: 0;
147
253
 
@@ -151,71 +257,16 @@ export const SelectContainer = styled.div<{ disabled?: boolean }>`
151
257
  opacity: 0.59;
152
258
  pointer-events: none;
153
259
  `}
154
- a {
155
- display: block;
156
- text-decoration: none;
157
- color: var(--select-text-color);
158
- }
159
- `;
160
-
161
- export const SelectInput = styled.div`
162
- display: flex;
163
- align-items: center;
164
- justify-content: space-between;
165
- border-radius: var(--select-input-border-radius);
166
- padding: var(--select-input-padding);
167
- cursor: pointer;
168
- gap: 8px;
169
260
  `;
170
261
 
171
- export const SelectInputValue = styled.div`
172
- pointer-events: none;
173
- min-width: 0;
174
- text-overflow: ellipsis;
175
- overflow: hidden;
176
- `;
177
-
178
- export const SelectList = styled.ul<{ placement?: string; alignment?: string }>`
179
- position: absolute;
180
- top: ${({ placement }) => (placement === 'top' ? 'auto' : '100%')};
181
- bottom: ${({ placement }) => (placement === 'top' ? '100%' : 'auto')};
182
- left: ${({ alignment }) => (alignment === 'start' ? '0' : 'auto')};
183
- right: ${({ alignment }) => (alignment === 'end' ? '0' : 'auto')};
184
- margin: 0;
185
- min-width: var(--select-list-min-width);
186
- max-width: var(--select-list-max-width);
187
- padding: var(--select-list-padding);
188
- background-color: var(--select-list-bg-color);
189
- border-radius: var(--select-list-border-radius);
190
- box-shadow: var(--select-list-box-shadow);
191
- list-style-type: none;
192
- cursor: pointer;
193
- white-space: nowrap;
194
- overflow: hidden;
195
- z-index: 1;
196
- `;
262
+ const SelectDropdown = styled(Dropdown)`
263
+ width: 100%;
197
264
 
198
- export const SelectListItem = styled.li<{ selected: boolean }>`
199
- display: flex;
200
- align-items: center;
201
- justify-content: space-between;
202
- border-radius: var(--select-list-item-border-radius);
203
- gap: var(--select-list-item-gap);
204
- padding: var(--select-list-item-padding);
205
- ${({ selected }) =>
206
- selected &&
207
- `
208
- font-weight: var(--select-list-item-font-weight-active);
209
- background-color: var(--select-list-item-bg-color-active);
210
- &:hover { background-color: var(--select-list-item-bg-color-active)};
211
- `}
212
-
213
- & > * {
214
- overflow: hidden;
215
- text-overflow: ellipsis;
265
+ > * {
266
+ width: 100%;
216
267
  }
268
+ `;
217
269
 
218
- &:hover {
219
- background-color: var(--select-list-item-bg-color-hover);
220
- }
270
+ const SelectDropdownMenu = styled(DropdownMenu)`
271
+ width: 100%;
221
272
  `;
@@ -0,0 +1,201 @@
1
+ import styled from 'styled-components';
2
+ import React, { useRef } from 'react';
3
+
4
+ import type { SelectOption } from '@redocly/theme/core/types/select';
5
+
6
+ import { Tag } from '@redocly/theme/components/Tag/Tag';
7
+ import { CloseIcon } from '@redocly/theme/icons/CloseIcon/CloseIcon';
8
+ import { Button } from '@redocly/theme/components/Button/Button';
9
+
10
+ type SelectInputProps<T> = {
11
+ id?: string;
12
+ selectedOptions: SelectOption<T>[];
13
+ searchValue: any;
14
+ stickyValue: any;
15
+ onlyIcon?: boolean;
16
+ icon?: React.ReactNode;
17
+ customIcon?: React.ReactNode;
18
+ placeholder?: string;
19
+ multiple?: boolean;
20
+ searchable?: boolean;
21
+ clearable?: boolean;
22
+ clearHandler?: (value?: any) => void;
23
+ inputBlurHandler?: (e?: any) => void;
24
+ inputFocusHandler?: (e?: any) => void;
25
+ searchHandler?: (e?: any) => void;
26
+ clickHandler?: (e?: any) => void;
27
+ };
28
+
29
+ export function SelectInput<T>(props: SelectInputProps<T>): React.ReactNode {
30
+ const {
31
+ id,
32
+ onlyIcon,
33
+ icon,
34
+ customIcon,
35
+ selectedOptions,
36
+ placeholder,
37
+ stickyValue,
38
+ multiple,
39
+ searchable,
40
+ clearable,
41
+ clearHandler,
42
+ searchHandler,
43
+ clickHandler,
44
+ searchValue,
45
+ inputBlurHandler,
46
+ inputFocusHandler,
47
+ } = props;
48
+ const inputRef = useRef<HTMLInputElement | null>(null);
49
+
50
+ const onChangeHandler = (e: any) => {
51
+ searchHandler?.(e);
52
+ inputRef.current?.focus();
53
+ };
54
+
55
+ const onKeyDownHandler = (e: any) => {
56
+ e.stopPropagation();
57
+ if (e.key === 'Backspace' && !searchValue && selectedOptions.length) {
58
+ clearHandler?.(selectedOptions[selectedOptions.length - 1]);
59
+ inputRef.current?.focus();
60
+ }
61
+ };
62
+
63
+ const onClickHandler = (e: React.MouseEvent) => {
64
+ clickHandler?.(e);
65
+ };
66
+
67
+ const onFocusHandler = (e: React.FocusEvent<HTMLDivElement>) => {
68
+ inputFocusHandler?.(e);
69
+ inputRef.current?.focus();
70
+ };
71
+
72
+ const onBlurHandler = (e: React.FocusEvent<HTMLDivElement>) => {
73
+ inputBlurHandler?.(e);
74
+ };
75
+
76
+ const onClearAllHandler = (e: React.MouseEvent) => {
77
+ e.stopPropagation();
78
+ clearHandler?.();
79
+ };
80
+
81
+ const selectTags = selectedOptions.map((option, index) => (
82
+ <SelectInputTag
83
+ closable
84
+ key={index}
85
+ onClose={() => {
86
+ clearHandler?.(option);
87
+ inputRef.current?.focus();
88
+ }}
89
+ >
90
+ {option.label || (option.value as string) || option.element}
91
+ </SelectInputTag>
92
+ ));
93
+
94
+ const selectInput = (
95
+ <SelectInternalInput
96
+ value={
97
+ searchValue ||
98
+ (!multiple && !stickyValue && selectedOptions.length
99
+ ? selectedOptions[0].label || selectedOptions[0].value
100
+ : '')
101
+ }
102
+ placeholder={
103
+ searchValue || (multiple && selectedOptions.length) ? '' : stickyValue || placeholder
104
+ }
105
+ onChange={onChangeHandler}
106
+ onKeyDown={onKeyDownHandler}
107
+ onBlur={onBlurHandler}
108
+ ref={inputRef}
109
+ width={multiple ? (!searchValue && selectedOptions.length ? '10px' : 'auto') : '100%'}
110
+ />
111
+ );
112
+
113
+ const simpleValue = selectedOptions.length ? (
114
+ selectedOptions[0].label || selectedOptions[0].element || (selectedOptions[0].value as string)
115
+ ) : (
116
+ <SelectInternalInputPlaceholder>{placeholder}</SelectInternalInputPlaceholder>
117
+ );
118
+
119
+ const multipleValues = selectedOptions.length ? (
120
+ selectTags
121
+ ) : (
122
+ <SelectInternalInputPlaceholder>{placeholder}</SelectInternalInputPlaceholder>
123
+ );
124
+
125
+ return (
126
+ <SelectInputWrapper {...props} id={id} onFocus={onFocusHandler} onClick={onClickHandler}>
127
+ {!onlyIcon && (
128
+ <>
129
+ <SelectInputValue>
130
+ {multiple ? (
131
+ searchable ? (
132
+ <>
133
+ {selectTags}
134
+ {selectInput}
135
+ </>
136
+ ) : (
137
+ multipleValues
138
+ )
139
+ ) : searchable ? (
140
+ selectInput
141
+ ) : (
142
+ simpleValue
143
+ )}
144
+ </SelectInputValue>
145
+ {!!(clearable && selectedOptions.length) && (
146
+ <Button size="small" variant="text" icon={<CloseIcon />} onClick={onClearAllHandler} />
147
+ )}
148
+ </>
149
+ )}
150
+ {customIcon || icon}
151
+ </SelectInputWrapper>
152
+ );
153
+ }
154
+
155
+ export const SelectInputWrapper = styled.div`
156
+ width: 100%;
157
+ display: flex;
158
+ align-items: center;
159
+ justify-content: space-between;
160
+ border-radius: var(--select-input-border-radius);
161
+ padding: var(--select-input-padding);
162
+ cursor: pointer;
163
+ gap: var(--select-input-gap);
164
+ `;
165
+
166
+ const SelectInputValue = styled.div`
167
+ width: calc(100% - 20px);
168
+ display: flex;
169
+ min-width: 0;
170
+ text-overflow: ellipsis;
171
+ overflow: hidden;
172
+ flex-wrap: wrap;
173
+ gap: var(--select-input-value-gap);
174
+ `;
175
+
176
+ const SelectInputTag = styled(Tag)`
177
+ --tag-content-padding: 0;
178
+ `;
179
+
180
+ const SelectInternalInput = styled.input.attrs(() => ({
181
+ type: 'text',
182
+ }))<{ width?: string }>`
183
+ outline: none;
184
+ border-radius: var(--select-input-border-radius);
185
+ border: none;
186
+ font-size: var(--select-input-font-size);
187
+ font-weight: var(--select-input-font-weight);
188
+ line-height: var(--select-input-line-height);
189
+ background-color: var(--select-input-bg-color);
190
+
191
+ &::placeholder {
192
+ color: var(--select-input-placeholder-color);
193
+ }
194
+
195
+ width: ${({ width }) => width || 'auto'};
196
+ `;
197
+
198
+ const SelectInternalInputPlaceholder = styled.div`
199
+ color: var(--select-input-placeholder-color);
200
+ padding-left: 8px;
201
+ `;
@@ -9,10 +9,19 @@ export const select = css`
9
9
  --select-line-height: var(--line-height-base); // @presenter LineHeight
10
10
  --select-text-color: var(--text-color-secondary); // @presenter Color
11
11
  --select-border-radius: var(--border-radius); // @presenter BorderRadius
12
+ --select-border: var(--border-width) var(--border-style) var(--border-color-primary); // @presenter Border
13
+
12
14
  --select-input-padding-vertical: 6px; // @presenter Spacing
13
- --select-input-padding-horizontal: 16px; // @presenter Spacing
15
+ --select-input-padding-horizontal: 6px; // @presenter Spacing
14
16
  --select-input-padding: var(--select-input-padding-vertical) var(--select-input-padding-horizontal);
17
+ --select-input-gap: var(--spacing-xs); // @presenter Spacing
18
+ --select-input-value-gap: var(--spacing-unit); // @presenter Spacing
15
19
  --select-input-border-radius: var(--border-radius); // @presenter BorderRadius
20
+ --select-input-font-size: var(--font-size-base); // @presenter FontSize
21
+ --select-input-font-weight: var(--font-weight-regular); // @presenter FontWeight
22
+ --select-input-line-height: var(--line-height-base); // @presenter LineHeight
23
+ --select-input-bg-color: var(--bg-color); // @presenter Color
24
+ --select-input-placeholder-color: var(--input-content-placeholder-color); // @presenter Color
16
25
 
17
26
  --select-list-min-width: 100px;
18
27
  --select-list-max-width: 300px;
@@ -30,5 +39,7 @@ export const select = css`
30
39
  --select-list-item-bg-color-active: transparent; // @presenter Color
31
40
  --select-list-item-bg-color-hover: var(--menu-item-bg-color-hover); // @presenter Color
32
41
  --select-list-item-font-weight-active: var(--font-weight-medium); // @presenter Color
42
+
43
+ --select-placeholder-color: var(--color-warm-grey-6); // @presenter Color
33
44
  // @tokens End
34
45
  `;
@@ -25,7 +25,7 @@ export const ChangeViewButton = ({
25
25
 
26
26
  return (
27
27
  <StyledChangeViewButton
28
- title={translate('theme.sidebar.actions.changeLayout', 'Change layout')}
28
+ title={translate('sidebar.actions.changeLayout', 'Change layout')}
29
29
  onClick={onClick}
30
30
  collapsedSidebar={collapsedSidebar}
31
31
  >
@@ -53,8 +53,8 @@ export const SidebarActions = ({
53
53
  }}
54
54
  title={
55
55
  collapsedSidebar
56
- ? translate('theme.sidebar.actions.show', 'Show sidebar')
57
- : translate('theme.sidebar.actions.hide', 'Hide sidebar')
56
+ ? translate('sidebar.actions.show', 'Show sidebar')
57
+ : translate('sidebar.actions.hide', 'Hide sidebar')
58
58
  }
59
59
  size="small"
60
60
  variant="outlined"
@@ -48,8 +48,8 @@ export function TableOfContent(props: TableOfContentProps): JSX.Element | null {
48
48
  <TableOfContentMenu data-component-name="TableOfContent/TableOfContent" className={className}>
49
49
  <TableOfContentItems ref={sidebar}>
50
50
  {displayedHeadings.length ? (
51
- <TableOfContentHeader data-translation-key="theme.toc.header">
52
- {translate('theme.toc.header', toc.header || 'On this page')}
51
+ <TableOfContentHeader data-translation-key="toc.header">
52
+ {translate('toc.header', toc.header || 'On this page')}
53
53
  </TableOfContentHeader>
54
54
  ) : null}
55
55
  {displayedHeadings.map((heading: MdHeading | null, idx: number) => {