@xqmsg/ui-core 0.24.3 → 0.24.5

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.
@@ -30,6 +30,7 @@ export interface StackedMultiSelectProps extends ReactSelectFieldProps {
30
30
  setError: UseFormSetError<FieldValues>;
31
31
  clearErrors: UseFormClearErrors<FieldValues>;
32
32
  control: Control<FieldValues, any>;
33
+ loadingOptions?: boolean;
33
34
  }
34
35
 
35
36
  /**
@@ -38,319 +39,330 @@ export interface StackedMultiSelectProps extends ReactSelectFieldProps {
38
39
  const StackedMultiSelect = React.forwardRef<
39
40
  HTMLInputElement,
40
41
  StackedMultiSelectProps
41
- >(({ options, setValue, control, name, placeholder, disabled }, _ref) => {
42
- const watchedValue = useWatch({ control, name: name as string });
43
- const dropdownRef = useRef<HTMLDivElement>(null);
44
- const dropdownMenuRef = useRef<HTMLDivElement>(null);
45
- const scrollRef = useRef<HTMLDivElement>(null);
46
- const inputRef = useRef<HTMLInputElement>(null);
47
-
48
- const [isInit, setIsInit] = useState(false);
49
- const [localValues, setLocalValues] = useState<FieldOptions>([]);
50
- const [localOptions, setLocalOptions] = useState<FieldOptions>(options);
51
- const [filteredOptions, setFilteredOptions] = useState<FieldOptions>(
52
- localOptions
53
- );
54
- const [isFocussed, setIsFocussed] = useState(false);
55
- const [shouldSideScroll, setShouldSideScroll] = useState(false);
56
- const [optionIndex, setOptionIndex] = useState<number | null>(null);
57
-
58
- const [position, setPosition] = useState<'top' | 'bottom'>('top');
59
- const [searchValue, setSearchValue] = useState('');
60
- // const [debouncedSearchValue, setDebouncedSearchValue] = useState('');
61
-
62
- const boundingClientRect = dropdownRef.current?.getBoundingClientRect() as DOMRect;
63
-
64
- useEffect(() => {
65
- if (window.innerHeight - (boundingClientRect?.y + 240) >= 0) {
66
- setPosition('top');
67
- } else {
68
- setPosition('bottom');
69
- }
70
- }, [boundingClientRect]);
71
-
72
- useOnClickOutside(dropdownRef, () => setIsFocussed(false));
73
-
74
- // gets latest watched form value (common delimited) from RHF state and creates a list
75
- useEffect(() => {
76
- if (watchedValue !== undefined && !watchedValue.length && !isInit) {
77
- setLocalValues([]);
78
- setIsInit(true);
79
- }
80
-
81
- if (watchedValue !== undefined && watchedValue?.length && !isInit) {
82
- if (shouldSideScroll) {
83
- (scrollRef.current as HTMLDivElement).scrollTo({
84
- left: scrollRef.current?.scrollWidth,
85
- behavior: 'smooth',
86
- });
87
- setShouldSideScroll(false);
42
+ >(
43
+ (
44
+ { options, setValue, control, name, placeholder, disabled, loadingOptions },
45
+ _ref
46
+ ) => {
47
+ const watchedValue = useWatch({ control, name: name as string });
48
+ const dropdownRef = useRef<HTMLDivElement>(null);
49
+ const dropdownMenuRef = useRef<HTMLDivElement>(null);
50
+ const scrollRef = useRef<HTMLDivElement>(null);
51
+ const inputRef = useRef<HTMLInputElement>(null);
52
+
53
+ const [isInit, setIsInit] = useState(false);
54
+ const [localValues, setLocalValues] = useState<FieldOptions>([]);
55
+ const [localOptions, setLocalOptions] = useState<FieldOptions>(options);
56
+ const [filteredOptions, setFilteredOptions] = useState<FieldOptions>(
57
+ localOptions
58
+ );
59
+ const [isFocussed, setIsFocussed] = useState(false);
60
+ const [shouldSideScroll, setShouldSideScroll] = useState(false);
61
+ const [optionIndex, setOptionIndex] = useState<number | null>(null);
62
+
63
+ const [position, setPosition] = useState<'top' | 'bottom'>('top');
64
+ const [searchValue, setSearchValue] = useState('');
65
+ // const [debouncedSearchValue, setDebouncedSearchValue] = useState('');
66
+
67
+ const boundingClientRect = dropdownRef.current?.getBoundingClientRect() as DOMRect;
68
+
69
+ useEffect(() => {
70
+ if (window.innerHeight - (boundingClientRect?.y + 240) >= 0) {
71
+ setPosition('top');
72
+ } else {
73
+ setPosition('bottom');
88
74
  }
75
+ }, [boundingClientRect]);
89
76
 
90
- if (isInit) return;
77
+ useOnClickOutside(dropdownRef, () => setIsFocussed(false));
91
78
 
92
- setLocalValues(
93
- watchedValue
94
- .split(',')
95
- .filter(Boolean)
96
- .map((value: string) =>
97
- options.find(option => option.value === value)
79
+ // gets latest watched form value (common delimited) from RHF state and creates a list
80
+ useEffect(() => {
81
+ if (watchedValue !== undefined && !watchedValue.length && !isInit) {
82
+ setLocalValues([]);
83
+ setIsInit(true);
84
+ }
85
+
86
+ if (watchedValue !== undefined && watchedValue?.length && !isInit) {
87
+ if (shouldSideScroll) {
88
+ (scrollRef.current as HTMLDivElement).scrollTo({
89
+ left: scrollRef.current?.scrollWidth,
90
+ behavior: 'smooth',
91
+ });
92
+ setShouldSideScroll(false);
93
+ }
94
+
95
+ if (isInit) return;
96
+
97
+ setLocalValues(
98
+ watchedValue
99
+ .split(',')
100
+ .filter(Boolean)
101
+ .map((value: string) =>
102
+ options.find(option => option.value === value)
103
+ )
104
+ );
105
+ // Filter out options that are already selected
106
+ setLocalOptions(prevLocalOptions =>
107
+ prevLocalOptions.filter(
108
+ localOption =>
109
+ !watchedValue
110
+ .split(',')
111
+ .filter(Boolean)
112
+ .map((value: string) =>
113
+ options.find(option => option.value === value)
114
+ )
115
+ .includes(localOption)
98
116
  )
99
- );
100
- // Filter out options that are already selected
101
- setLocalOptions(prevLocalOptions =>
102
- prevLocalOptions.filter(
103
- localOption =>
104
- !watchedValue
105
- .split(',')
106
- .filter(Boolean)
107
- .map((value: string) =>
108
- options.find(option => option.value === value)
109
- )
110
- .includes(localOption)
111
- )
112
- );
117
+ );
113
118
 
114
- setIsInit(true);
115
- }
116
- }, [
117
- isInit,
118
- localOptions,
119
- localValues,
120
- options,
121
- shouldSideScroll,
122
- watchedValue,
123
- ]);
124
-
125
- const handleChange = (option: FieldOption) => {
126
- setShouldSideScroll(true);
127
- const newValue = [...localValues, option]
128
- .map(({ value }) => value)
129
- .join(',');
130
-
131
- setValue(name as string, newValue, {
132
- shouldValidate: true,
133
- shouldDirty: true,
134
- });
135
-
136
- setLocalOptions(prevLocalOptions =>
137
- prevLocalOptions.filter(prevLocalOption => prevLocalOption !== option)
138
- );
119
+ setIsInit(true);
120
+ }
121
+ }, [
122
+ isInit,
123
+ localOptions,
124
+ localValues,
125
+ options,
126
+ shouldSideScroll,
127
+ watchedValue,
128
+ ]);
129
+
130
+ const handleChange = (option: FieldOption) => {
131
+ setShouldSideScroll(true);
132
+ const newValue = [...localValues, option]
133
+ .map(({ value }) => value)
134
+ .join(',');
135
+
136
+ setValue(name as string, newValue, {
137
+ shouldValidate: true,
138
+ shouldDirty: true,
139
+ });
139
140
 
140
- setLocalValues(prevLocalValues => [...prevLocalValues, option]);
141
+ setLocalOptions(prevLocalOptions =>
142
+ prevLocalOptions.filter(prevLocalOption => prevLocalOption !== option)
143
+ );
141
144
 
142
- // reset search on value select
143
- setSearchValue('');
144
- };
145
+ setLocalValues(prevLocalValues => [...prevLocalValues, option]);
145
146
 
146
- const handleDelete = (option: FieldOption) => {
147
- const newValue = localValues
148
- .filter(localValue => localValue !== option)
149
- .map(({ value }) => value)
150
- .join(',');
147
+ // reset search on value select
148
+ setSearchValue('');
149
+ };
151
150
 
152
- setValue(name as string, newValue, {
153
- shouldValidate: true,
154
- shouldDirty: true,
155
- });
151
+ const handleDelete = (option: FieldOption) => {
152
+ const newValue = localValues
153
+ .filter(localValue => localValue !== option)
154
+ .map(({ value }) => value)
155
+ .join(',');
156
156
 
157
- setLocalOptions(prevLocalOptions =>
158
- [...prevLocalOptions, option].sort((a, b) => a.sortValue - b.sortValue)
159
- );
157
+ setValue(name as string, newValue, {
158
+ shouldValidate: true,
159
+ shouldDirty: true,
160
+ });
160
161
 
161
- setLocalValues(prevLocalValues =>
162
- prevLocalValues.filter(prevLocalValue => prevLocalValue !== option)
163
- );
164
- };
162
+ setLocalOptions(prevLocalOptions =>
163
+ [...prevLocalOptions, option].sort((a, b) => a.sortValue - b.sortValue)
164
+ );
165
165
 
166
- const handleOnKeyDown: KeyboardEventHandler<HTMLInputElement> = e => {
167
- const initialOptionIndex = options[0].value === 'section_header' ? 1 : 0;
166
+ setLocalValues(prevLocalValues =>
167
+ prevLocalValues.filter(prevLocalValue => prevLocalValue !== option)
168
+ );
169
+ };
168
170
 
169
- if (
170
- !isFocussed &&
171
- (e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown')
172
- ) {
173
- setIsFocussed(true);
174
- return setOptionIndex(initialOptionIndex);
175
- }
171
+ const handleOnKeyDown: KeyboardEventHandler<HTMLInputElement> = e => {
172
+ const initialOptionIndex = options[0].value === 'section_header' ? 1 : 0;
176
173
 
177
- if (isFocussed) {
178
174
  if (
179
- optionIndex === null &&
175
+ !isFocussed &&
180
176
  (e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown')
181
177
  ) {
178
+ setIsFocussed(true);
182
179
  return setOptionIndex(initialOptionIndex);
183
180
  }
184
181
 
185
- if (e.key === 'ArrowUp' && optionIndex !== null && optionIndex > 0) {
186
- const incrementValue =
187
- filteredOptions[optionIndex - 1] &&
188
- filteredOptions[optionIndex - 1].value === 'section_header'
189
- ? 2
190
- : 1;
191
- setOptionIndex(optionIndex - incrementValue);
192
-
193
- return dropdownMenuRef.current?.scrollTo({
194
- top: optionIndex * 24,
195
- behavior: 'smooth',
196
- });
182
+ if (isFocussed) {
183
+ if (
184
+ optionIndex === null &&
185
+ (e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown')
186
+ ) {
187
+ return setOptionIndex(initialOptionIndex);
188
+ }
189
+
190
+ if (e.key === 'ArrowUp' && optionIndex !== null && optionIndex > 0) {
191
+ const incrementValue =
192
+ filteredOptions[optionIndex - 1] &&
193
+ filteredOptions[optionIndex - 1].value === 'section_header'
194
+ ? 2
195
+ : 1;
196
+ setOptionIndex(optionIndex - incrementValue);
197
+
198
+ return dropdownMenuRef.current?.scrollTo({
199
+ top: optionIndex * 24,
200
+ behavior: 'smooth',
201
+ });
202
+ }
203
+
204
+ if (
205
+ e.key === 'ArrowDown' &&
206
+ optionIndex !== null &&
207
+ optionIndex < filteredOptions.length
208
+ ) {
209
+ const incrementValue =
210
+ filteredOptions[optionIndex + 1] &&
211
+ filteredOptions[optionIndex + 1].value === 'section_header'
212
+ ? 2
213
+ : 1;
214
+ setOptionIndex(optionIndex + incrementValue);
215
+
216
+ return dropdownMenuRef.current?.scrollTo({
217
+ top: optionIndex * 24,
218
+ behavior: 'smooth',
219
+ });
220
+ }
221
+
222
+ if (e.key === 'Enter' && optionIndex !== null) {
223
+ const option = filteredOptions.find((_, idx) => optionIndex === idx);
224
+ if (!option) return;
225
+
226
+ handleChange(option);
227
+
228
+ return setIsFocussed(false);
229
+ }
230
+
231
+ if (e.key === 'Tab') {
232
+ return setIsFocussed(false);
233
+ }
197
234
  }
198
-
199
- if (
200
- e.key === 'ArrowDown' &&
201
- optionIndex !== null &&
202
- optionIndex < filteredOptions.length
203
- ) {
204
- const incrementValue =
205
- filteredOptions[optionIndex + 1] &&
206
- filteredOptions[optionIndex + 1].value === 'section_header'
207
- ? 2
208
- : 1;
209
- setOptionIndex(optionIndex + incrementValue);
210
-
211
- return dropdownMenuRef.current?.scrollTo({
212
- top: optionIndex * 24,
235
+ };
236
+
237
+ useEffect(() => {
238
+ if (searchValue.length) {
239
+ const idx = options.findIndex(
240
+ option =>
241
+ option.label.substring(0, searchValue.length).toLowerCase() ===
242
+ searchValue.toLowerCase()
243
+ );
244
+
245
+ dropdownMenuRef.current?.scrollTo({
246
+ top: idx * 27,
213
247
  behavior: 'smooth',
214
248
  });
215
249
  }
216
-
217
- if (e.key === 'Enter' && optionIndex !== null) {
218
- const option = filteredOptions.find((_, idx) => optionIndex === idx);
219
- if (!option) return;
220
-
221
- handleChange(option);
222
-
223
- return setIsFocussed(false);
224
- }
225
-
226
- if (e.key === 'Tab') {
227
- return setIsFocussed(false);
228
- }
229
- }
230
- };
231
-
232
- useEffect(() => {
233
- if (searchValue.length) {
234
- const idx = options.findIndex(
235
- option =>
236
- option.label.substring(0, searchValue.length).toLowerCase() ===
237
- searchValue.toLowerCase()
250
+ }, [options, searchValue]);
251
+
252
+ useEffect(() => {
253
+ setFilteredOptions(
254
+ localOptions.filter(element => {
255
+ return element.label
256
+ .toLowerCase()
257
+ .includes(searchValue.toLowerCase());
258
+ })
238
259
  );
239
-
240
- dropdownMenuRef.current?.scrollTo({
241
- top: idx * 27,
242
- behavior: 'smooth',
243
- });
244
- }
245
- }, [options, searchValue]);
246
-
247
- useEffect(() => {
248
- setFilteredOptions(
249
- localOptions.filter(element => {
250
- return element.label.toLowerCase().includes(searchValue.toLowerCase());
251
- })
252
- );
253
- }, [localOptions, searchValue]);
254
-
255
- const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
256
- console.log(e);
257
- const initialOptionIndex =
258
- filteredOptions[0]?.value === 'section_header' ? 1 : 0;
259
- setOptionIndex(initialOptionIndex);
260
- const { value } = e.target;
261
- setSearchValue(value);
262
- };
263
-
264
- return (
265
- <Box ref={dropdownRef} position="relative" onKeyDown={handleOnKeyDown}>
266
- <Flex
267
- fontSize="13px"
268
- h="26px"
269
- border={isFocussed ? '2px solid' : '.5px solid'}
270
- borderColor={isFocussed ? colors.border.focus : colors.border.default}
271
- py="5px"
272
- pl="8px"
273
- borderRadius="4px"
274
- alignItems="center"
275
- justifyContent="space-between"
276
- onClick={() => {
277
- if (!disabled) {
278
- if (isFocussed) {
279
- return setIsFocussed(false);
280
- }
281
-
282
- inputRef.current?.focus();
283
- setIsFocussed(true);
284
- }
285
- }}
286
- bg={disabled ? colors.fill.light.quaternary : '#ffffff'}
287
- cursor={disabled ? 'not-allowed' : 'pointer'}
288
- >
260
+ }, [localOptions, searchValue]);
261
+
262
+ const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
263
+ const initialOptionIndex =
264
+ filteredOptions[0]?.value === 'section_header' ? 1 : 0;
265
+ setOptionIndex(initialOptionIndex);
266
+ const { value } = e.target;
267
+ setSearchValue(value);
268
+ };
269
+
270
+ return (
271
+ <Box ref={dropdownRef} position="relative" onKeyDown={handleOnKeyDown}>
289
272
  <Flex
273
+ fontSize="13px"
274
+ h="26px"
275
+ border={isFocussed ? '2px solid' : '.5px solid'}
276
+ borderColor={isFocussed ? colors.border.focus : colors.border.default}
277
+ py="5px"
278
+ pl="8px"
279
+ borderRadius="4px"
290
280
  alignItems="center"
291
- h="inherit"
292
- width="90%"
293
- overflowX="scroll"
294
- style={{
295
- scrollbarWidth: 'none' /* Firefox */,
296
- msOverflowStyle: 'none',
297
- }}
298
- sx={{
299
- '::-webkit-scrollbar': {
300
- display: 'none',
301
- },
281
+ justifyContent="space-between"
282
+ onClick={() => {
283
+ if (!disabled) {
284
+ if (isFocussed) {
285
+ return setIsFocussed(false);
286
+ }
287
+
288
+ inputRef.current?.focus();
289
+ setIsFocussed(true);
290
+ }
302
291
  }}
303
- ref={scrollRef}
292
+ bg={disabled ? colors.fill.light.quaternary : '#ffffff'}
293
+ cursor={disabled ? 'not-allowed' : 'pointer'}
304
294
  >
305
- {localValues.length ? (
306
- localValues.map((option, idx) => (
307
- <Box
308
- key={idx}
309
- mr="4px"
310
- width="fit-content"
311
- h="16px"
312
- borderRadius="full"
313
- >
314
- <Token
315
- label={option.label}
316
- onDelete={() => handleDelete(option)}
317
- />
318
- </Box>
319
- ))
320
- ) : (
321
- <Text color={colors.label.secondary.light} fontSize="13px">
322
- {placeholder}
323
- </Text>
324
- )}
325
- </Flex>
326
- <Input
327
- padding={0}
328
- border="none"
329
- height="0"
330
- width="0"
331
- autoComplete="off"
332
- type="text"
333
- ref={inputRef}
334
- tabIndex={-1}
335
- _focus={{ boxShadow: 'none !important' }}
336
- />
337
- <Flex mr="4px" justifyContent="center" alignItems="center">
338
- <DropdownIcon boxSize="12px" disabled={disabled} />
295
+ <Flex
296
+ alignItems="center"
297
+ h="inherit"
298
+ width="90%"
299
+ overflowX="scroll"
300
+ style={{
301
+ scrollbarWidth: 'none' /* Firefox */,
302
+ msOverflowStyle: 'none',
303
+ }}
304
+ sx={{
305
+ '::-webkit-scrollbar': {
306
+ display: 'none',
307
+ },
308
+ }}
309
+ ref={scrollRef}
310
+ >
311
+ {localValues.length ? (
312
+ localValues.map((option, idx) => (
313
+ <Box
314
+ key={idx}
315
+ mr="4px"
316
+ width="fit-content"
317
+ h="16px"
318
+ borderRadius="full"
319
+ >
320
+ <Token
321
+ label={option.label}
322
+ onDelete={() => handleDelete(option)}
323
+ />
324
+ </Box>
325
+ ))
326
+ ) : (
327
+ <Text color={colors.label.secondary.light} fontSize="13px">
328
+ {placeholder}
329
+ </Text>
330
+ )}
331
+ </Flex>
332
+ <Input
333
+ padding={0}
334
+ border="none"
335
+ height="0"
336
+ width="0"
337
+ autoComplete="off"
338
+ type="text"
339
+ ref={inputRef}
340
+ tabIndex={-1}
341
+ _focus={{ boxShadow: 'none !important' }}
342
+ />
343
+ <Flex mr="4px" justifyContent="center" alignItems="center">
344
+ <DropdownIcon boxSize="12px" disabled={disabled} />
345
+ </Flex>
339
346
  </Flex>
340
- </Flex>
341
- {isFocussed && (
342
- <Dropdown
343
- dropdownRef={dropdownMenuRef}
344
- onSelectItem={option => handleChange(option)}
345
- options={filteredOptions}
346
- position={position}
347
- optionIndex={optionIndex}
348
- >
349
- <Input value={searchValue} onChange={handleInput} />
350
- </Dropdown>
351
- )}
352
- </Box>
353
- );
354
- });
347
+ {isFocussed && (
348
+ <Dropdown
349
+ dropdownRef={dropdownMenuRef}
350
+ onSelectItem={option => handleChange(option)}
351
+ options={filteredOptions}
352
+ position={position}
353
+ optionIndex={optionIndex}
354
+ loading={loadingOptions}
355
+ >
356
+ <Input
357
+ value={searchValue}
358
+ onChange={handleInput}
359
+ disabled={loadingOptions}
360
+ />
361
+ </Dropdown>
362
+ )}
363
+ </Box>
364
+ );
365
+ }
366
+ );
355
367
 
356
368
  export default StackedMultiSelect;