@xqmsg/ui-core 0.23.1-rc.3 → 0.23.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.
Files changed (192) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +40 -40
  3. package/dist/{78c9d6fd7766410f.svg → 89793640b494d7ea.svg} +9 -9
  4. package/dist/ui-core.cjs.development.js +1 -1
  5. package/dist/ui-core.cjs.development.js.map +1 -1
  6. package/dist/ui-core.cjs.production.min.js +1 -1
  7. package/dist/ui-core.cjs.production.min.js.map +1 -1
  8. package/dist/ui-core.esm.js +1 -1
  9. package/dist/ui-core.esm.js.map +1 -1
  10. package/package.json +118 -118
  11. package/src/components/banner/Banner.stories.tsx +100 -100
  12. package/src/components/banner/index.tsx +73 -73
  13. package/src/components/breadcrumbs/Breadcrumbs.stories.tsx +66 -66
  14. package/src/components/breadcrumbs/components/icon/index.tsx +38 -38
  15. package/src/components/breadcrumbs/components/label/index.tsx +20 -20
  16. package/src/components/breadcrumbs/index.tsx +48 -48
  17. package/src/components/button/Button.stories.tsx +140 -140
  18. package/src/components/button/google/GoogleButton.stories.tsx +23 -23
  19. package/src/components/button/google/index.tsx +29 -29
  20. package/src/components/button/index.tsx +51 -51
  21. package/src/components/button/microsoft/MicrosoftButton.stories.tsx +25 -25
  22. package/src/components/button/microsoft/index.tsx +29 -29
  23. package/src/components/button/spinner/SpinnerButton.stories.tsx +60 -60
  24. package/src/components/button/spinner/index.tsx +36 -36
  25. package/src/components/card/Card.stories.tsx +56 -56
  26. package/src/components/card/index.tsx +78 -78
  27. package/src/components/form/Form.stories.tsx +62 -62
  28. package/src/components/form/FormTypes.ts +20 -20
  29. package/src/components/form/hooks/useFormHandler.tsx +74 -74
  30. package/src/components/form/index.tsx +25 -25
  31. package/src/components/form/section/FormSection.stories.tsx +109 -109
  32. package/src/components/form/section/index.tsx +87 -87
  33. package/src/components/form/utils/formErrors.ts +34 -34
  34. package/src/components/icons/checkmark/checkmark.svg +3 -3
  35. package/src/components/icons/checkmark/index.tsx +13 -13
  36. package/src/components/icons/chevron/down/chevron-down.svg +3 -3
  37. package/src/components/icons/chevron/down/index.tsx +14 -14
  38. package/src/components/icons/chevron/right/chevron-right.svg +3 -3
  39. package/src/components/icons/chevron/right/index.tsx +13 -13
  40. package/src/components/icons/clock/clock.svg +3 -3
  41. package/src/components/icons/clock/index.tsx +13 -13
  42. package/src/components/icons/close/close.svg +3 -3
  43. package/src/components/icons/close/index.tsx +21 -21
  44. package/src/components/icons/dropdown/dropdown.svg +3 -3
  45. package/src/components/icons/dropdown/index.tsx +16 -16
  46. package/src/components/icons/error/error.svg +3 -3
  47. package/src/components/icons/error/index.tsx +13 -13
  48. package/src/components/icons/file/fill/file-fill.svg +4 -4
  49. package/src/components/icons/file/fill/index.tsx +13 -13
  50. package/src/components/icons/file/outline/file-outline.svg +3 -3
  51. package/src/components/icons/file/outline/index.tsx +13 -13
  52. package/src/components/icons/folder/add/fill/folder-add-fill.svg +3 -3
  53. package/src/components/icons/folder/add/fill/index.tsx +13 -13
  54. package/src/components/icons/folder/add/outline/folder-add-outline.svg +3 -3
  55. package/src/components/icons/folder/add/outline/index.tsx +15 -15
  56. package/src/components/icons/folder/fill/folder-fill-gradient.svg +33 -33
  57. package/src/components/icons/folder/fill/folder-fill.svg +4 -4
  58. package/src/components/icons/folder/fill/index.tsx +21 -21
  59. package/src/components/icons/folder/outline/folder-outline.svg +3 -3
  60. package/src/components/icons/folder/outline/index.tsx +13 -13
  61. package/src/components/icons/gear/GearIcon.tsx +36 -36
  62. package/src/components/icons/google/drive/index.tsx +13 -13
  63. package/src/components/icons/google/google.svg +13 -13
  64. package/src/components/icons/google/index.tsx +13 -13
  65. package/src/components/icons/group/group.svg +3 -3
  66. package/src/components/icons/group/index.tsx +13 -13
  67. package/src/components/icons/home/home.svg +3 -3
  68. package/src/components/icons/home/index.tsx +13 -13
  69. package/src/components/icons/image/image.svg +3 -3
  70. package/src/components/icons/image/index.tsx +13 -13
  71. package/src/components/icons/index.tsx +101 -101
  72. package/src/components/icons/link/index.tsx +13 -13
  73. package/src/components/icons/link/link.svg +4 -4
  74. package/src/components/icons/menu/index.tsx +13 -13
  75. package/src/components/icons/menu/menu.svg +3 -3
  76. package/src/components/icons/microsoft/index.tsx +13 -13
  77. package/src/components/icons/microsoft/microsoft.svg +9 -9
  78. package/src/components/icons/microsoft/onedrive/index.tsx +16 -16
  79. package/src/components/icons/neutral/index.tsx +14 -14
  80. package/src/components/icons/neutral/neutral.svg +3 -3
  81. package/src/components/icons/page/index.tsx +13 -13
  82. package/src/components/icons/page/page.svg +3 -3
  83. package/src/components/icons/positive/index.tsx +13 -13
  84. package/src/components/icons/positive/positive.svg +3 -3
  85. package/src/components/icons/question/index.tsx +13 -13
  86. package/src/components/icons/question/question.svg +3 -3
  87. package/src/components/icons/search/index.tsx +13 -13
  88. package/src/components/icons/search/search.svg +3 -3
  89. package/src/components/icons/services/index.tsx +13 -13
  90. package/src/components/icons/services/services.svg +3 -3
  91. package/src/components/icons/settings/index.tsx +14 -14
  92. package/src/components/icons/settings/settings.svg +6 -6
  93. package/src/components/icons/table/fill/index.tsx +13 -13
  94. package/src/components/icons/table/fill/table-fill.svg +3 -3
  95. package/src/components/icons/table/outline/index.tsx +13 -13
  96. package/src/components/icons/table/outline/table-outline.svg +3 -3
  97. package/src/components/icons/task/index.tsx +10 -10
  98. package/src/components/icons/task/task.svg +11 -11
  99. package/src/components/icons/trash/index.tsx +13 -13
  100. package/src/components/icons/trash/trash.svg +3 -3
  101. package/src/components/icons/vault/index.tsx +14 -14
  102. package/src/components/icons/video/index.tsx +13 -13
  103. package/src/components/icons/video/video.svg +3 -3
  104. package/src/components/icons/warning/index.tsx +13 -13
  105. package/src/components/icons/warning/warning.svg +3 -3
  106. package/src/components/icons/workspace/index.tsx +14 -14
  107. package/src/components/input/Input.stories.tsx +287 -287
  108. package/src/components/input/InputTypes.ts +77 -77
  109. package/src/components/input/StackedCheckbox/StackedCheckbox.tsx +44 -44
  110. package/src/components/input/StackedInput/StackedInput.tsx +60 -60
  111. package/src/components/input/StackedMultiSelect/index.tsx +349 -349
  112. package/src/components/input/StackedPilledInput/index.tsx +386 -375
  113. package/src/components/input/StackedRadio/StackedRadioGroup.tsx +38 -38
  114. package/src/components/input/StackedSelect/index.tsx +232 -232
  115. package/src/components/input/StackedSwitch/index.tsx +33 -33
  116. package/src/components/input/StackedTextarea/StackedTextarea.tsx +55 -55
  117. package/src/components/input/components/dropdown/index.tsx +111 -111
  118. package/src/components/input/components/label/index.tsx +35 -35
  119. package/src/components/input/components/token/Token.stories.tsx +25 -25
  120. package/src/components/input/components/token/index.tsx +45 -45
  121. package/src/components/input/index.tsx +298 -301
  122. package/src/components/layout/BorderedBox/index.tsx +30 -30
  123. package/src/components/layout/Layout.stories.tsx +40 -40
  124. package/src/components/layout/index.tsx +100 -100
  125. package/src/components/link/Link.stories.tsx +23 -23
  126. package/src/components/link/index.tsx +34 -34
  127. package/src/components/loading/LoadingIndicator.stories.tsx +45 -45
  128. package/src/components/loading/index.tsx +45 -45
  129. package/src/components/modal/Modal.stories.tsx +36 -36
  130. package/src/components/modal/components/action/index.tsx +37 -37
  131. package/src/components/modal/index.tsx +41 -41
  132. package/src/components/navigation/NavigationMenu.stories.tsx +85 -85
  133. package/src/components/navigation/components/header/index.tsx +27 -27
  134. package/src/components/navigation/components/items/index.tsx +76 -76
  135. package/src/components/navigation/index.tsx +87 -87
  136. package/src/components/select/index.tsx +140 -140
  137. package/src/components/table/Table.stories.tsx +63 -63
  138. package/src/components/table/TableTypes.ts +15 -15
  139. package/src/components/table/components/loading/index.tsx +45 -45
  140. package/src/components/table/components/text/index.tsx +23 -23
  141. package/src/components/table/empty/index.tsx +47 -47
  142. package/src/components/table/index.tsx +84 -84
  143. package/src/components/table/utils/generateTableColumns.ts +9 -9
  144. package/src/components/tabs/TabsWrapper.stories.tsx +85 -85
  145. package/src/components/tabs/index.tsx +39 -39
  146. package/src/components/text/Text.stories.tsx +59 -59
  147. package/src/components/text/index.tsx +16 -16
  148. package/src/components/toast/Toast.stories.tsx +52 -52
  149. package/src/components/toast/index.tsx +78 -78
  150. package/src/components/toolbar/Toolbar.stories.tsx +59 -59
  151. package/src/components/toolbar/components/actions/add/index.tsx +18 -18
  152. package/src/components/toolbar/components/actions/search/index.tsx +38 -38
  153. package/src/components/toolbar/components/actions/sort/index.tsx +49 -49
  154. package/src/components/toolbar/components/breadcrumbs/index.tsx +63 -63
  155. package/src/components/toolbar/components/breadcrumbs/item/index.tsx +72 -72
  156. package/src/components/toolbar/components/dropdown/index.tsx +107 -107
  157. package/src/components/toolbar/components/navigation/components/button/left/index.tsx +28 -28
  158. package/src/components/toolbar/components/navigation/components/button/left/left-arrow.svg +3 -3
  159. package/src/components/toolbar/components/navigation/components/button/right/index.tsx +27 -27
  160. package/src/components/toolbar/components/navigation/components/button/right/right-arrow.svg +3 -3
  161. package/src/components/toolbar/components/navigation/index.tsx +36 -36
  162. package/src/components/toolbar/index.tsx +55 -55
  163. package/src/hooks/useDeepEffect.tsx +22 -22
  164. package/src/hooks/useDidMountEffect.tsx +13 -13
  165. package/src/hooks/useOnOutsideClick.tsx +31 -31
  166. package/src/hooks/useToast.tsx +16 -16
  167. package/src/index.tsx +78 -78
  168. package/src/theme/components/alert.ts +60 -60
  169. package/src/theme/components/badge.ts +59 -59
  170. package/src/theme/components/button.ts +163 -163
  171. package/src/theme/components/checkbox.ts +28 -28
  172. package/src/theme/components/code.ts +16 -16
  173. package/src/theme/components/form-error.ts +31 -31
  174. package/src/theme/components/form-label.ts +17 -17
  175. package/src/theme/components/form.ts +29 -29
  176. package/src/theme/components/input.ts +65 -65
  177. package/src/theme/components/link.ts +118 -118
  178. package/src/theme/components/modal.ts +45 -45
  179. package/src/theme/components/select.ts +36 -36
  180. package/src/theme/components/switch.ts +89 -89
  181. package/src/theme/components/table.ts +42 -42
  182. package/src/theme/components/tabs.ts +255 -255
  183. package/src/theme/components/text.ts +93 -93
  184. package/src/theme/components/textarea.ts +42 -42
  185. package/src/theme/customXQChakraTheme.ts +54 -54
  186. package/src/theme/foundations/breakpoints.ts +18 -18
  187. package/src/theme/foundations/colors.ts +165 -165
  188. package/src/theme/foundations/shadows.ts +23 -23
  189. package/src/theme/foundations/typography.ts +62 -62
  190. package/src/theme/provider/index.tsx +21 -21
  191. package/src/theme/styles.ts +19 -19
  192. package/CHANGELOG.md +0 -456
@@ -1,349 +1,349 @@
1
- import React, {
2
- KeyboardEventHandler,
3
- useEffect,
4
- useMemo,
5
- useRef,
6
- useState,
7
- } from 'react';
8
- import { Box, Flex, Text, Input } from '@chakra-ui/react';
9
- import { debounce } from 'lodash';
10
- import {
11
- FieldOption,
12
- FieldOptions,
13
- ReactSelectFieldProps,
14
- } from '../InputTypes';
15
- import colors from '../../../theme/foundations/colors';
16
- import {
17
- Control,
18
- FieldValues,
19
- UseFormClearErrors,
20
- UseFormSetError,
21
- UseFormSetValue,
22
- useWatch,
23
- } from 'react-hook-form';
24
- import { Dropdown as DropdownIcon } from '../../icons/dropdown';
25
- import { Dropdown } from '../components/dropdown';
26
- import Token from '../components/token';
27
- import { useOnClickOutside } from '../../../hooks/useOnOutsideClick';
28
-
29
- export interface StackedMultiSelectProps extends ReactSelectFieldProps {
30
- options: FieldOptions;
31
- setValue: UseFormSetValue<FieldValues>;
32
- setError: UseFormSetError<FieldValues>;
33
- clearErrors: UseFormClearErrors<FieldValues>;
34
- control: Control<FieldValues, any>;
35
- }
36
-
37
- /**
38
- * A functional React component utilized to render the `StackedMultiSelect` component.
39
- */
40
- const StackedMultiSelect = React.forwardRef<
41
- HTMLInputElement,
42
- StackedMultiSelectProps
43
- >(({ options, setValue, control, name, placeholder, disabled }, _ref) => {
44
- const watchedValue = useWatch({ control, name: name as string });
45
- const dropdownRef = useRef<HTMLDivElement>(null);
46
- const dropdownMenuRef = useRef<HTMLDivElement>(null);
47
- const scrollRef = useRef<HTMLDivElement>(null);
48
- const inputRef = useRef<HTMLInputElement>(null);
49
-
50
- const [isInit, setIsInit] = useState(false);
51
- const [localValues, setLocalValues] = useState<FieldOptions>([]);
52
- const [localOptions, setLocalOptions] = useState<FieldOptions>(options);
53
- const [isFocussed, setIsFocussed] = useState(false);
54
- const [shouldSideScroll, setShouldSideScroll] = useState(false);
55
- const [optionIndex, setOptionIndex] = useState<number | null>(null);
56
-
57
- const [position, setPosition] = useState<'top' | 'bottom'>('top');
58
- const [searchValue, setSearchValue] = useState('');
59
- const [debouncedSearchValue, setDebouncedSearchValue] = useState('');
60
-
61
- const boundingClientRect = dropdownRef.current?.getBoundingClientRect() as DOMRect;
62
-
63
- useEffect(() => {
64
- if (window.innerHeight - (boundingClientRect?.y + 240) >= 0) {
65
- setPosition('top');
66
- } else {
67
- setPosition('bottom');
68
- }
69
- }, [boundingClientRect]);
70
-
71
- useOnClickOutside(dropdownRef, () => setIsFocussed(false));
72
-
73
- // gets latest watched form value (common delimited) from RHF state and creates a list
74
- useEffect(() => {
75
- if (watchedValue !== undefined && !watchedValue.length && !isInit) {
76
- setLocalValues([]);
77
- setIsInit(true);
78
- }
79
-
80
- if (watchedValue !== undefined && watchedValue?.length && !isInit) {
81
- if (shouldSideScroll) {
82
- (scrollRef.current as HTMLDivElement).scrollTo({
83
- left: scrollRef.current?.scrollWidth,
84
- behavior: 'smooth',
85
- });
86
- setShouldSideScroll(false);
87
- }
88
-
89
- if (isInit) return;
90
-
91
- setLocalValues(
92
- watchedValue
93
- .split(',')
94
- .filter(Boolean)
95
- .map((value: string) =>
96
- options.find(option => option.value === value)
97
- )
98
- );
99
- // Filter out options that are already selected
100
- setLocalOptions(prevLocalOptions =>
101
- prevLocalOptions.filter(
102
- localOption =>
103
- !watchedValue
104
- .split(',')
105
- .filter(Boolean)
106
- .map((value: string) =>
107
- options.find(option => option.value === value)
108
- )
109
- .includes(localOption)
110
- )
111
- );
112
-
113
- setIsInit(true);
114
- }
115
- }, [
116
- isInit,
117
- localOptions,
118
- localValues,
119
- options,
120
- shouldSideScroll,
121
- watchedValue,
122
- ]);
123
-
124
- const handleChange = (option: FieldOption) => {
125
- setShouldSideScroll(true);
126
- const newValue = [...localValues, option]
127
- .map(({ value }) => value)
128
- .join(',');
129
-
130
- setValue(name as string, newValue, {
131
- shouldValidate: true,
132
- shouldDirty: true,
133
- });
134
-
135
- setLocalOptions(prevLocalOptions =>
136
- prevLocalOptions.filter(prevLocalOption => prevLocalOption !== option)
137
- );
138
-
139
- setLocalValues(prevLocalValues => [...prevLocalValues, option]);
140
- };
141
-
142
- const handleDelete = (option: FieldOption) => {
143
- const newValue = localValues
144
- .filter(localValue => localValue !== option)
145
- .map(({ value }) => value)
146
- .join(',');
147
-
148
- setValue(name as string, newValue, {
149
- shouldValidate: true,
150
- shouldDirty: true,
151
- });
152
-
153
- setLocalOptions(prevLocalOptions =>
154
- [...prevLocalOptions, option].sort((a, b) => a.sortValue - b.sortValue)
155
- );
156
-
157
- setLocalValues(prevLocalValues =>
158
- prevLocalValues.filter(prevLocalValue => prevLocalValue !== option)
159
- );
160
- };
161
-
162
- const handleOnKeyDown: KeyboardEventHandler<HTMLInputElement> = e => {
163
- const initialOptionIndex = options[0].value === 'section_header' ? 1 : 0;
164
-
165
- if (
166
- !isFocussed &&
167
- (e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown')
168
- ) {
169
- setIsFocussed(true);
170
- return setOptionIndex(initialOptionIndex);
171
- }
172
-
173
- if (isFocussed) {
174
- if (
175
- optionIndex === null &&
176
- (e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown')
177
- ) {
178
- return setOptionIndex(initialOptionIndex);
179
- }
180
-
181
- if (e.key === 'ArrowUp' && optionIndex !== null && optionIndex > 0) {
182
- const incrementValue =
183
- localOptions[optionIndex - 1] &&
184
- localOptions[optionIndex - 1].value === 'section_header'
185
- ? 2
186
- : 1;
187
- setOptionIndex(optionIndex - incrementValue);
188
-
189
- return dropdownMenuRef.current?.scrollTo({
190
- top: optionIndex * 24,
191
- behavior: 'smooth',
192
- });
193
- }
194
-
195
- if (
196
- e.key === 'ArrowDown' &&
197
- optionIndex !== null &&
198
- optionIndex < localOptions.length
199
- ) {
200
- const incrementValue =
201
- localOptions[optionIndex + 1] &&
202
- localOptions[optionIndex + 1].value === 'section_header'
203
- ? 2
204
- : 1;
205
- setOptionIndex(optionIndex + incrementValue);
206
-
207
- return dropdownMenuRef.current?.scrollTo({
208
- top: optionIndex * 24,
209
- behavior: 'smooth',
210
- });
211
- }
212
-
213
- if (e.key === 'Enter' && optionIndex !== null) {
214
- const option = localOptions.find((_, idx) => optionIndex === idx);
215
- if (!option) return;
216
-
217
- handleChange(option);
218
-
219
- return setIsFocussed(false);
220
- }
221
-
222
- if (e.key === 'Tab') {
223
- return setIsFocussed(false);
224
- }
225
-
226
- return update(debouncedSearchValue.concat(e.key));
227
- }
228
- };
229
-
230
- useEffect(() => {
231
- if (searchValue.length) {
232
- const idx = options.findIndex(
233
- option =>
234
- option.label.substring(0, searchValue.length).toLowerCase() ===
235
- searchValue.toLowerCase()
236
- );
237
-
238
- dropdownMenuRef.current?.scrollTo({
239
- top: idx * 24,
240
- behavior: 'smooth',
241
- });
242
-
243
- setSearchValue('');
244
- setDebouncedSearchValue('');
245
- }
246
- }, [options, searchValue]);
247
-
248
- const updateSearchValue = useMemo(() => {
249
- return debounce(val => {
250
- setSearchValue(val);
251
- }, 1000);
252
- }, []);
253
-
254
- const update = (value: string) => {
255
- updateSearchValue(value);
256
- setDebouncedSearchValue(value);
257
- };
258
-
259
- return (
260
- <Box ref={dropdownRef} position="relative" onKeyDown={handleOnKeyDown}>
261
- <Flex
262
- fontSize="13px"
263
- h="26px"
264
- border={isFocussed ? '2px solid' : '.5px solid'}
265
- borderColor={isFocussed ? colors.border.focus : colors.border.default}
266
- py="5px"
267
- pl="8px"
268
- borderRadius="4px"
269
- alignItems="center"
270
- justifyContent="space-between"
271
- onClick={() => {
272
- if (!disabled) {
273
- if (isFocussed) {
274
- return setIsFocussed(false);
275
- }
276
-
277
- inputRef.current?.focus();
278
- setIsFocussed(true);
279
- }
280
- }}
281
- bg={disabled ? colors.fill.light.quaternary : '#ffffff'}
282
- cursor={disabled ? 'not-allowed' : 'pointer'}
283
- >
284
- <Flex
285
- alignItems="center"
286
- h="inherit"
287
- width="90%"
288
- overflowX="scroll"
289
- style={{
290
- scrollbarWidth: 'none' /* Firefox */,
291
- msOverflowStyle: 'none',
292
- }}
293
- sx={{
294
- '::-webkit-scrollbar': {
295
- display: 'none',
296
- },
297
- }}
298
- ref={scrollRef}
299
- >
300
- {localValues.length ? (
301
- localValues.map((option, idx) => (
302
- <Box
303
- key={idx}
304
- mr="4px"
305
- width="fit-content"
306
- h="16px"
307
- borderRadius="full"
308
- >
309
- <Token
310
- label={option.label}
311
- onDelete={() => handleDelete(option)}
312
- />
313
- </Box>
314
- ))
315
- ) : (
316
- <Text color={colors.label.secondary.light} fontSize="13px">
317
- {placeholder}
318
- </Text>
319
- )}
320
- </Flex>
321
- <Input
322
- padding={0}
323
- border="none"
324
- height="0"
325
- width="0"
326
- autoComplete="off"
327
- type="text"
328
- ref={inputRef}
329
- tabIndex={-1}
330
- _focus={{ boxShadow: 'none !important' }}
331
- />
332
- <Flex mr="4px" justifyContent="center" alignItems="center">
333
- <DropdownIcon boxSize="16px" disabled={disabled} />
334
- </Flex>
335
- </Flex>
336
- {isFocussed && (
337
- <Dropdown
338
- dropdownRef={dropdownMenuRef}
339
- onSelectItem={option => handleChange(option)}
340
- options={localOptions}
341
- position={position}
342
- optionIndex={optionIndex}
343
- />
344
- )}
345
- </Box>
346
- );
347
- });
348
-
349
- export default StackedMultiSelect;
1
+ import React, {
2
+ KeyboardEventHandler,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ } from 'react';
8
+ import { Box, Flex, Text, Input } from '@chakra-ui/react';
9
+ import { debounce } from 'lodash';
10
+ import {
11
+ FieldOption,
12
+ FieldOptions,
13
+ ReactSelectFieldProps,
14
+ } from '../InputTypes';
15
+ import colors from '../../../theme/foundations/colors';
16
+ import {
17
+ Control,
18
+ FieldValues,
19
+ UseFormClearErrors,
20
+ UseFormSetError,
21
+ UseFormSetValue,
22
+ useWatch,
23
+ } from 'react-hook-form';
24
+ import { Dropdown as DropdownIcon } from '../../icons/dropdown';
25
+ import { Dropdown } from '../components/dropdown';
26
+ import Token from '../components/token';
27
+ import { useOnClickOutside } from '../../../hooks/useOnOutsideClick';
28
+
29
+ export interface StackedMultiSelectProps extends ReactSelectFieldProps {
30
+ options: FieldOptions;
31
+ setValue: UseFormSetValue<FieldValues>;
32
+ setError: UseFormSetError<FieldValues>;
33
+ clearErrors: UseFormClearErrors<FieldValues>;
34
+ control: Control<FieldValues, any>;
35
+ }
36
+
37
+ /**
38
+ * A functional React component utilized to render the `StackedMultiSelect` component.
39
+ */
40
+ const StackedMultiSelect = React.forwardRef<
41
+ HTMLInputElement,
42
+ StackedMultiSelectProps
43
+ >(({ options, setValue, control, name, placeholder, disabled }, _ref) => {
44
+ const watchedValue = useWatch({ control, name: name as string });
45
+ const dropdownRef = useRef<HTMLDivElement>(null);
46
+ const dropdownMenuRef = useRef<HTMLDivElement>(null);
47
+ const scrollRef = useRef<HTMLDivElement>(null);
48
+ const inputRef = useRef<HTMLInputElement>(null);
49
+
50
+ const [isInit, setIsInit] = useState(false);
51
+ const [localValues, setLocalValues] = useState<FieldOptions>([]);
52
+ const [localOptions, setLocalOptions] = useState<FieldOptions>(options);
53
+ const [isFocussed, setIsFocussed] = useState(false);
54
+ const [shouldSideScroll, setShouldSideScroll] = useState(false);
55
+ const [optionIndex, setOptionIndex] = useState<number | null>(null);
56
+
57
+ const [position, setPosition] = useState<'top' | 'bottom'>('top');
58
+ const [searchValue, setSearchValue] = useState('');
59
+ const [debouncedSearchValue, setDebouncedSearchValue] = useState('');
60
+
61
+ const boundingClientRect = dropdownRef.current?.getBoundingClientRect() as DOMRect;
62
+
63
+ useEffect(() => {
64
+ if (window.innerHeight - (boundingClientRect?.y + 240) >= 0) {
65
+ setPosition('top');
66
+ } else {
67
+ setPosition('bottom');
68
+ }
69
+ }, [boundingClientRect]);
70
+
71
+ useOnClickOutside(dropdownRef, () => setIsFocussed(false));
72
+
73
+ // gets latest watched form value (common delimited) from RHF state and creates a list
74
+ useEffect(() => {
75
+ if (watchedValue !== undefined && !watchedValue.length && !isInit) {
76
+ setLocalValues([]);
77
+ setIsInit(true);
78
+ }
79
+
80
+ if (watchedValue !== undefined && watchedValue?.length && !isInit) {
81
+ if (shouldSideScroll) {
82
+ (scrollRef.current as HTMLDivElement).scrollTo({
83
+ left: scrollRef.current?.scrollWidth,
84
+ behavior: 'smooth',
85
+ });
86
+ setShouldSideScroll(false);
87
+ }
88
+
89
+ if (isInit) return;
90
+
91
+ setLocalValues(
92
+ watchedValue
93
+ .split(',')
94
+ .filter(Boolean)
95
+ .map((value: string) =>
96
+ options.find(option => option.value === value)
97
+ )
98
+ );
99
+ // Filter out options that are already selected
100
+ setLocalOptions(prevLocalOptions =>
101
+ prevLocalOptions.filter(
102
+ localOption =>
103
+ !watchedValue
104
+ .split(',')
105
+ .filter(Boolean)
106
+ .map((value: string) =>
107
+ options.find(option => option.value === value)
108
+ )
109
+ .includes(localOption)
110
+ )
111
+ );
112
+
113
+ setIsInit(true);
114
+ }
115
+ }, [
116
+ isInit,
117
+ localOptions,
118
+ localValues,
119
+ options,
120
+ shouldSideScroll,
121
+ watchedValue,
122
+ ]);
123
+
124
+ const handleChange = (option: FieldOption) => {
125
+ setShouldSideScroll(true);
126
+ const newValue = [...localValues, option]
127
+ .map(({ value }) => value)
128
+ .join(',');
129
+
130
+ setValue(name as string, newValue, {
131
+ shouldValidate: true,
132
+ shouldDirty: true,
133
+ });
134
+
135
+ setLocalOptions(prevLocalOptions =>
136
+ prevLocalOptions.filter(prevLocalOption => prevLocalOption !== option)
137
+ );
138
+
139
+ setLocalValues(prevLocalValues => [...prevLocalValues, option]);
140
+ };
141
+
142
+ const handleDelete = (option: FieldOption) => {
143
+ const newValue = localValues
144
+ .filter(localValue => localValue !== option)
145
+ .map(({ value }) => value)
146
+ .join(',');
147
+
148
+ setValue(name as string, newValue, {
149
+ shouldValidate: true,
150
+ shouldDirty: true,
151
+ });
152
+
153
+ setLocalOptions(prevLocalOptions =>
154
+ [...prevLocalOptions, option].sort((a, b) => a.sortValue - b.sortValue)
155
+ );
156
+
157
+ setLocalValues(prevLocalValues =>
158
+ prevLocalValues.filter(prevLocalValue => prevLocalValue !== option)
159
+ );
160
+ };
161
+
162
+ const handleOnKeyDown: KeyboardEventHandler<HTMLInputElement> = e => {
163
+ const initialOptionIndex = options[0].value === 'section_header' ? 1 : 0;
164
+
165
+ if (
166
+ !isFocussed &&
167
+ (e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown')
168
+ ) {
169
+ setIsFocussed(true);
170
+ return setOptionIndex(initialOptionIndex);
171
+ }
172
+
173
+ if (isFocussed) {
174
+ if (
175
+ optionIndex === null &&
176
+ (e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown')
177
+ ) {
178
+ return setOptionIndex(initialOptionIndex);
179
+ }
180
+
181
+ if (e.key === 'ArrowUp' && optionIndex !== null && optionIndex > 0) {
182
+ const incrementValue =
183
+ localOptions[optionIndex - 1] &&
184
+ localOptions[optionIndex - 1].value === 'section_header'
185
+ ? 2
186
+ : 1;
187
+ setOptionIndex(optionIndex - incrementValue);
188
+
189
+ return dropdownMenuRef.current?.scrollTo({
190
+ top: optionIndex * 24,
191
+ behavior: 'smooth',
192
+ });
193
+ }
194
+
195
+ if (
196
+ e.key === 'ArrowDown' &&
197
+ optionIndex !== null &&
198
+ optionIndex < localOptions.length
199
+ ) {
200
+ const incrementValue =
201
+ localOptions[optionIndex + 1] &&
202
+ localOptions[optionIndex + 1].value === 'section_header'
203
+ ? 2
204
+ : 1;
205
+ setOptionIndex(optionIndex + incrementValue);
206
+
207
+ return dropdownMenuRef.current?.scrollTo({
208
+ top: optionIndex * 24,
209
+ behavior: 'smooth',
210
+ });
211
+ }
212
+
213
+ if (e.key === 'Enter' && optionIndex !== null) {
214
+ const option = localOptions.find((_, idx) => optionIndex === idx);
215
+ if (!option) return;
216
+
217
+ handleChange(option);
218
+
219
+ return setIsFocussed(false);
220
+ }
221
+
222
+ if (e.key === 'Tab') {
223
+ return setIsFocussed(false);
224
+ }
225
+
226
+ return update(debouncedSearchValue.concat(e.key));
227
+ }
228
+ };
229
+
230
+ useEffect(() => {
231
+ if (searchValue.length) {
232
+ const idx = options.findIndex(
233
+ option =>
234
+ option.label.substring(0, searchValue.length).toLowerCase() ===
235
+ searchValue.toLowerCase()
236
+ );
237
+
238
+ dropdownMenuRef.current?.scrollTo({
239
+ top: idx * 24,
240
+ behavior: 'smooth',
241
+ });
242
+
243
+ setSearchValue('');
244
+ setDebouncedSearchValue('');
245
+ }
246
+ }, [options, searchValue]);
247
+
248
+ const updateSearchValue = useMemo(() => {
249
+ return debounce(val => {
250
+ setSearchValue(val);
251
+ }, 1000);
252
+ }, []);
253
+
254
+ const update = (value: string) => {
255
+ updateSearchValue(value);
256
+ setDebouncedSearchValue(value);
257
+ };
258
+
259
+ return (
260
+ <Box ref={dropdownRef} position="relative" onKeyDown={handleOnKeyDown}>
261
+ <Flex
262
+ fontSize="13px"
263
+ h="26px"
264
+ border={isFocussed ? '2px solid' : '.5px solid'}
265
+ borderColor={isFocussed ? colors.border.focus : colors.border.default}
266
+ py="5px"
267
+ pl="8px"
268
+ borderRadius="4px"
269
+ alignItems="center"
270
+ justifyContent="space-between"
271
+ onClick={() => {
272
+ if (!disabled) {
273
+ if (isFocussed) {
274
+ return setIsFocussed(false);
275
+ }
276
+
277
+ inputRef.current?.focus();
278
+ setIsFocussed(true);
279
+ }
280
+ }}
281
+ bg={disabled ? colors.fill.light.quaternary : '#ffffff'}
282
+ cursor={disabled ? 'not-allowed' : 'pointer'}
283
+ >
284
+ <Flex
285
+ alignItems="center"
286
+ h="inherit"
287
+ width="90%"
288
+ overflowX="scroll"
289
+ style={{
290
+ scrollbarWidth: 'none' /* Firefox */,
291
+ msOverflowStyle: 'none',
292
+ }}
293
+ sx={{
294
+ '::-webkit-scrollbar': {
295
+ display: 'none',
296
+ },
297
+ }}
298
+ ref={scrollRef}
299
+ >
300
+ {localValues.length ? (
301
+ localValues.map((option, idx) => (
302
+ <Box
303
+ key={idx}
304
+ mr="4px"
305
+ width="fit-content"
306
+ h="16px"
307
+ borderRadius="full"
308
+ >
309
+ <Token
310
+ label={option.label}
311
+ onDelete={() => handleDelete(option)}
312
+ />
313
+ </Box>
314
+ ))
315
+ ) : (
316
+ <Text color={colors.label.secondary.light} fontSize="13px">
317
+ {placeholder}
318
+ </Text>
319
+ )}
320
+ </Flex>
321
+ <Input
322
+ padding={0}
323
+ border="none"
324
+ height="0"
325
+ width="0"
326
+ autoComplete="off"
327
+ type="text"
328
+ ref={inputRef}
329
+ tabIndex={-1}
330
+ _focus={{ boxShadow: 'none !important' }}
331
+ />
332
+ <Flex mr="4px" justifyContent="center" alignItems="center">
333
+ <DropdownIcon boxSize="16px" disabled={disabled} />
334
+ </Flex>
335
+ </Flex>
336
+ {isFocussed && (
337
+ <Dropdown
338
+ dropdownRef={dropdownMenuRef}
339
+ onSelectItem={option => handleChange(option)}
340
+ options={localOptions}
341
+ position={position}
342
+ optionIndex={optionIndex}
343
+ />
344
+ )}
345
+ </Box>
346
+ );
347
+ });
348
+
349
+ export default StackedMultiSelect;