@leafygreen-ui/combobox 1.0.2 → 1.2.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 (49) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +2 -2
  3. package/dist/Chip.d.ts.map +1 -1
  4. package/dist/Combobox.d.ts +7 -1
  5. package/dist/Combobox.d.ts.map +1 -1
  6. package/dist/Combobox.styles.d.ts +7 -3
  7. package/dist/Combobox.styles.d.ts.map +1 -1
  8. package/dist/Combobox.types.d.ts +33 -6
  9. package/dist/Combobox.types.d.ts.map +1 -1
  10. package/dist/ComboboxContext.d.ts +1 -1
  11. package/dist/ComboboxContext.d.ts.map +1 -1
  12. package/dist/ComboboxOption.d.ts.map +1 -1
  13. package/dist/ComboboxTestUtils.d.ts +3 -4
  14. package/dist/ComboboxTestUtils.d.ts.map +1 -1
  15. package/dist/esm/index.js +1 -1
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/utils/OptionObjectUtils.d.ts +5 -0
  20. package/dist/utils/OptionObjectUtils.d.ts.map +1 -0
  21. package/dist/utils/flattenChildren.d.ts +11 -0
  22. package/dist/utils/flattenChildren.d.ts.map +1 -0
  23. package/dist/utils/getNameAndValue.d.ts +14 -0
  24. package/dist/utils/getNameAndValue.d.ts.map +1 -0
  25. package/dist/utils/index.d.ts +5 -0
  26. package/dist/utils/index.d.ts.map +1 -0
  27. package/dist/utils/wrapJSX.d.ts +14 -0
  28. package/dist/utils/wrapJSX.d.ts.map +1 -0
  29. package/package.json +22 -12
  30. package/src/Chip.tsx +16 -9
  31. package/src/Combobox.spec.tsx +336 -164
  32. package/src/Combobox.story.tsx +274 -248
  33. package/src/Combobox.styles.ts +94 -24
  34. package/src/Combobox.tsx +456 -279
  35. package/src/Combobox.types.ts +46 -8
  36. package/src/ComboboxContext.tsx +2 -2
  37. package/src/ComboboxOption.tsx +36 -11
  38. package/src/ComboboxTestUtils.tsx +22 -8
  39. package/src/utils/ComboboxUtils.spec.tsx +227 -0
  40. package/src/utils/OptionObjectUtils.ts +26 -0
  41. package/src/utils/flattenChildren.tsx +47 -0
  42. package/src/utils/getNameAndValue.ts +23 -0
  43. package/src/utils/index.ts +8 -0
  44. package/src/utils/wrapJSX.tsx +54 -0
  45. package/tsconfig.json +3 -0
  46. package/tsconfig.tsbuildinfo +1 -3977
  47. package/dist/util.d.ts +0 -53
  48. package/dist/util.d.ts.map +0 -1
  49. package/src/util.tsx +0 -117
package/src/Combobox.tsx CHANGED
@@ -9,48 +9,59 @@ import { clone, isArray, isEqual, isNull, isString, isUndefined } from 'lodash';
9
9
  import { Description, Label } from '@leafygreen-ui/typography';
10
10
  import Popover from '@leafygreen-ui/popover';
11
11
  import {
12
+ useAvailableSpace,
12
13
  useDynamicRefs,
13
14
  useEventListener,
14
15
  useIdAllocator,
15
16
  usePrevious,
16
- useViewportSize,
17
17
  } from '@leafygreen-ui/hooks';
18
- import InteractionRing from '@leafygreen-ui/interaction-ring';
19
18
  import Icon from '@leafygreen-ui/icon';
20
19
  import IconButton from '@leafygreen-ui/icon-button';
21
20
  import { cx } from '@leafygreen-ui/emotion';
22
21
  import { uiColors } from '@leafygreen-ui/palette';
23
- import { consoleOnce, isComponentType } from '@leafygreen-ui/lib';
22
+ import { consoleOnce, isComponentType, keyMap } from '@leafygreen-ui/lib';
24
23
  import {
25
24
  ComboboxProps,
26
25
  getNullSelection,
27
26
  onChangeType,
28
27
  SelectValueType,
28
+ OptionObject,
29
+ ComboboxElement,
30
+ ComboboxSize,
29
31
  } from './Combobox.types';
30
32
  import { ComboboxContext } from './ComboboxContext';
31
33
  import { InternalComboboxOption } from './ComboboxOption';
32
34
  import { Chip } from './Chip';
33
35
  import {
34
- clearButton,
36
+ clearButtonStyle,
37
+ clearButtonFocusOverrideStyles,
38
+ comboboxFocusStyle,
35
39
  comboboxParentStyle,
36
40
  comboboxStyle,
37
41
  endIcon,
38
42
  errorMessageStyle,
39
43
  inputElementStyle,
40
44
  inputWrapperStyle,
41
- interactionRingColor,
42
- interactionRingStyle,
43
45
  loadingIconStyle,
44
46
  menuList,
45
47
  menuMessage,
46
48
  menuStyle,
47
49
  menuWrapperStyle,
50
+ _tempLabelDescriptionOverrideStyle,
48
51
  } from './Combobox.styles';
49
52
  import { InternalComboboxGroup } from './ComboboxGroup';
50
- import { flattenChildren, getNameAndValue, OptionObject, keyMap } from './util';
53
+ import {
54
+ flattenChildren,
55
+ getOptionObjectFromValue,
56
+ getDisplayNameForValue,
57
+ getValueForDisplayName,
58
+ getNameAndValue,
59
+ } from './utils';
51
60
 
52
61
  /**
53
- * Component
62
+ * Combobox is a combination of a Select and TextInput,
63
+ * allowing the user to either type a value directly or select a value from the list.
64
+ * Can be configured to select a single or multiple options.
54
65
  */
55
66
  export default function Combobox<M extends boolean>({
56
67
  children,
@@ -59,7 +70,7 @@ export default function Combobox<M extends boolean>({
59
70
  placeholder = 'Select',
60
71
  'aria-label': ariaLabel,
61
72
  disabled = false,
62
- size = 'default',
73
+ size = ComboboxSize.Default,
63
74
  darkMode = false,
64
75
  state = 'none',
65
76
  errorMessage,
@@ -100,8 +111,10 @@ export default function Combobox<M extends boolean>({
100
111
  const menuRef = useRef<HTMLDivElement>(null);
101
112
 
102
113
  const [isOpen, setOpen] = useState(false);
103
- const prevOpenState = usePrevious(isOpen);
104
- const [focusedOption, setFocusedOption] = useState<string | null>(null);
114
+ const wasOpen = usePrevious(isOpen);
115
+ const [highlightedOption, sethighlightedOption] = useState<string | null>(
116
+ null,
117
+ );
105
118
  const [selection, setSelection] = useState<SelectValueType<M> | null>(null);
106
119
  const prevSelection = usePrevious(selection);
107
120
  const [inputValue, setInputValue] = useState<string>('');
@@ -112,7 +125,25 @@ export default function Combobox<M extends boolean>({
112
125
  !isNull(selection) &&
113
126
  ((isArray(selection) && selection.length > 0) || isString(selection));
114
127
 
115
- // Tells typescript that selection is multiselect
128
+ const placeholderValue =
129
+ multiselect && isArray(selection) && selection.length > 0
130
+ ? undefined
131
+ : placeholder;
132
+
133
+ const closeMenu = () => setOpen(false);
134
+ const openMenu = () => setOpen(true);
135
+
136
+ /**
137
+ * Array of all of the options objects
138
+ */
139
+ const allOptions: Array<OptionObject> = useMemo(
140
+ () => flattenChildren(children),
141
+ [children],
142
+ );
143
+
144
+ /**
145
+ * Utility function that tells Typescript whether selection is multiselect
146
+ */
116
147
  const isMultiselect = useCallback(
117
148
  <T extends string>(val?: Array<T> | T | null): val is Array<T> => {
118
149
  if (multiselect && (typeof val == 'string' || typeof val == 'number')) {
@@ -130,7 +161,10 @@ export default function Combobox<M extends boolean>({
130
161
  [multiselect],
131
162
  );
132
163
 
133
- // Force focus of input box
164
+ /**
165
+ * Forces focus of input box
166
+ * @param cursorPos index the cursor should be set to
167
+ */
134
168
  const setInputFocus = useCallback(
135
169
  (cursorPos?: number) => {
136
170
  if (!disabled && inputRef && inputRef.current) {
@@ -143,7 +177,11 @@ export default function Combobox<M extends boolean>({
143
177
  [disabled],
144
178
  );
145
179
 
146
- // Update selection differently in mulit & single select
180
+ /**
181
+ * Update selection.
182
+ * This behaves differently in multi. vs single select.
183
+ * @param value option value the selection should be set to
184
+ */
147
185
  const updateSelection = useCallback(
148
186
  (value: string | null) => {
149
187
  if (isMultiselect(selection)) {
@@ -177,33 +215,50 @@ export default function Combobox<M extends boolean>({
177
215
  [isMultiselect, onChange, selection],
178
216
  );
179
217
 
180
- // Scrolls the combobox to the far right
181
- // Used when `overflow == 'scroll-x'`
182
- const scrollToEnd = () => {
183
- if (inputWrapperRef && inputWrapperRef.current) {
184
- // TODO - consider converting to .scrollTo(). This is not yet wuppoted in IE or jsdom
185
- inputWrapperRef.current.scrollLeft = inputWrapperRef.current.scrollWidth;
186
- }
187
- };
188
-
189
- const placeholderValue =
190
- multiselect && isArray(selection) && selection.length > 0
191
- ? undefined
192
- : placeholder;
193
-
194
- const allOptions = useMemo(() => flattenChildren(children), [children]);
218
+ /**
219
+ * Returns whether a given value is included in, or equal to, the current selection
220
+ * @param value the option value to check
221
+ */
222
+ const isValueCurrentSelection = useCallback(
223
+ (value: string): boolean => {
224
+ return isMultiselect(selection)
225
+ ? selection.includes(value)
226
+ : value === selection;
227
+ },
228
+ [isMultiselect, selection],
229
+ );
195
230
 
196
- const getDisplayNameForValue = useCallback(
197
- (value: string | null): string => {
198
- return value
199
- ? allOptions.find(opt => opt.value === value)?.displayName ?? value
200
- : '';
231
+ /**
232
+ * Returns whether given text is included in, or equal to, the current selection.
233
+ * Similar to `isValueCurrentSelection`, but assumes the text argument is the `displayName` for the selection
234
+ * @param text the text to check
235
+ */
236
+ const isTextCurrentSelection = useCallback(
237
+ (text: string): boolean => {
238
+ const value = getValueForDisplayName(text, allOptions);
239
+ return isValueCurrentSelection(value);
201
240
  },
202
- [allOptions],
241
+ [allOptions, isValueCurrentSelection],
203
242
  );
204
243
 
205
- // Computes whether the option is visible based on the current input
206
- const isOptionVisible = useCallback(
244
+ /**
245
+ * Returns whether the provided option is disabled
246
+ * @param option the option value or OptionObject to check
247
+ */
248
+ const isOptionDisabled = (option: string | OptionObject): boolean => {
249
+ if (typeof option === 'string') {
250
+ const optionObj = getOptionObjectFromValue(option, allOptions);
251
+ return !!optionObj?.isDisabled;
252
+ } else {
253
+ return !!option.isDisabled;
254
+ }
255
+ };
256
+
257
+ /**
258
+ * Computes whether the option is visible based on the current input
259
+ * @param option the option value or OptionObject to compute
260
+ */
261
+ const shouldOptionBeVisible = useCallback(
207
262
  (option: string | OptionObject): boolean => {
208
263
  const value = typeof option === 'string' ? option : option.value;
209
264
 
@@ -212,21 +267,40 @@ export default function Combobox<M extends boolean>({
212
267
  return filteredOptions.includes(value);
213
268
  }
214
269
 
270
+ // If the text input value is the current selection
271
+ // (or included in the selection)
272
+ // then all options should be visible
273
+ if (isTextCurrentSelection(inputValue)) {
274
+ return true;
275
+ }
276
+
215
277
  // otherwise, we do our own filtering
216
278
  const displayName =
217
279
  typeof option === 'string'
218
- ? getDisplayNameForValue(value)
280
+ ? getDisplayNameForValue(value, allOptions)
219
281
  : option.displayName;
220
- return displayName.toLowerCase().includes(inputValue.toLowerCase());
282
+
283
+ const isValueInDisplayName = displayName
284
+ .toLowerCase()
285
+ .includes(inputValue.toLowerCase());
286
+
287
+ return isValueInDisplayName;
221
288
  },
222
- [filteredOptions, getDisplayNameForValue, inputValue],
289
+ [filteredOptions, isTextCurrentSelection, inputValue, allOptions],
223
290
  );
224
291
 
225
- const visibleOptions = useMemo(() => allOptions.filter(isOptionVisible), [
226
- allOptions,
227
- isOptionVisible,
228
- ]);
292
+ /**
293
+ * The array of visible options objects
294
+ */
295
+ const visibleOptions: Array<OptionObject> = useMemo(
296
+ () => allOptions.filter(shouldOptionBeVisible),
297
+ [allOptions, shouldOptionBeVisible],
298
+ );
229
299
 
300
+ /**
301
+ * Returns whether the given value is in the options array
302
+ * @param value the value to check
303
+ */
230
304
  const isValueValid = useCallback(
231
305
  (value: string | null): boolean => {
232
306
  return value ? !!allOptions.find(opt => opt.value === value) : false;
@@ -234,6 +308,10 @@ export default function Combobox<M extends boolean>({
234
308
  [allOptions],
235
309
  );
236
310
 
311
+ /**
312
+ * Returns the index of a given value in the array of visible (filtered) options
313
+ * @param value the option value to get the index of
314
+ */
237
315
  const getIndexOfValue = useCallback(
238
316
  (value: string | null): number => {
239
317
  return visibleOptions
@@ -243,6 +321,10 @@ export default function Combobox<M extends boolean>({
243
321
  [visibleOptions],
244
322
  );
245
323
 
324
+ /**
325
+ * Returns the option value of a given index in the array of visible (filtered) options
326
+ * @param index the option index to get the value of
327
+ */
246
328
  const getValueAtIndex = useCallback(
247
329
  (index: number): string | undefined => {
248
330
  if (visibleOptions && visibleOptions.length >= index) {
@@ -253,6 +335,9 @@ export default function Combobox<M extends boolean>({
253
335
  [visibleOptions],
254
336
  );
255
337
 
338
+ /**
339
+ * Returns the index of the active chip in the selection array
340
+ */
256
341
  const getActiveChipIndex = useCallback(
257
342
  () =>
258
343
  isMultiselect(selection)
@@ -269,46 +354,21 @@ export default function Combobox<M extends boolean>({
269
354
  *
270
355
  */
271
356
 
272
- const getFocusedElementName = useCallback(() => {
273
- const isFocusOn = {
274
- Input: inputRef.current?.contains(document.activeElement),
275
- ClearButton: clearButtonRef.current?.contains(document.activeElement),
276
- Chip:
277
- isMultiselect(selection) &&
278
- selection.some(value =>
279
- getChipRef(value)?.current?.contains(document.activeElement),
280
- ),
281
- };
282
- const getActiveChipIndex = () =>
283
- isMultiselect(selection)
284
- ? selection.findIndex(value =>
285
- getChipRef(value)?.current?.contains(document.activeElement),
286
- )
287
- : -1;
288
-
289
- if (isMultiselect(selection) && isFocusOn.Chip) {
290
- if (getActiveChipIndex() === 0) {
291
- return 'FirstChip';
292
- } else if (getActiveChipIndex() === selection.length - 1) {
293
- return 'LastChip';
294
- }
295
-
296
- return 'MiddleChip';
297
- } else if (isFocusOn.Input) {
298
- return 'Input';
299
- } else if (isFocusOn.ClearButton) {
300
- return 'ClearButton';
301
- } else if (comboboxRef.current?.contains(document.activeElement)) {
302
- return 'Combobox';
303
- }
304
- }, [getChipRef, isMultiselect, selection]);
357
+ const [focusedElementName, trackFocusedElement] = useState<
358
+ ComboboxElement | undefined
359
+ >();
305
360
 
306
361
  type Direction = 'next' | 'prev' | 'first' | 'last';
307
- const updateFocusedOption = useCallback(
362
+
363
+ /**
364
+ * Updates the highlighted menu option based on the provided direction
365
+ * @param direction the direction to move the focus. `'next' | 'prev' | 'first' | 'last'`
366
+ */
367
+ const updateHighlightedOption = useCallback(
308
368
  (direction: Direction) => {
309
369
  const optionsCount = visibleOptions?.length ?? 0;
310
370
  const lastIndex = optionsCount - 1 > 0 ? optionsCount - 1 : 0;
311
- const indexOfFocus = getIndexOfValue(focusedOption);
371
+ const indexOfHighlight = getIndexOfValue(highlightedOption);
312
372
 
313
373
  // Remove focus from chip
314
374
  if (direction && isOpen) {
@@ -319,39 +379,39 @@ export default function Combobox<M extends boolean>({
319
379
  switch (direction) {
320
380
  case 'next': {
321
381
  const newValue =
322
- indexOfFocus + 1 < optionsCount
323
- ? getValueAtIndex(indexOfFocus + 1)
382
+ indexOfHighlight + 1 < optionsCount
383
+ ? getValueAtIndex(indexOfHighlight + 1)
324
384
  : getValueAtIndex(0);
325
385
 
326
- setFocusedOption(newValue ?? null);
386
+ sethighlightedOption(newValue ?? null);
327
387
  break;
328
388
  }
329
389
 
330
390
  case 'prev': {
331
391
  const newValue =
332
- indexOfFocus - 1 >= 0
333
- ? getValueAtIndex(indexOfFocus - 1)
392
+ indexOfHighlight - 1 >= 0
393
+ ? getValueAtIndex(indexOfHighlight - 1)
334
394
  : getValueAtIndex(lastIndex);
335
395
 
336
- setFocusedOption(newValue ?? null);
396
+ sethighlightedOption(newValue ?? null);
337
397
  break;
338
398
  }
339
399
 
340
400
  case 'last': {
341
401
  const newValue = getValueAtIndex(lastIndex);
342
- setFocusedOption(newValue ?? null);
402
+ sethighlightedOption(newValue ?? null);
343
403
  break;
344
404
  }
345
405
 
346
406
  case 'first':
347
407
  default: {
348
408
  const newValue = getValueAtIndex(0);
349
- setFocusedOption(newValue ?? null);
409
+ sethighlightedOption(newValue ?? null);
350
410
  }
351
411
  }
352
412
  },
353
413
  [
354
- focusedOption,
414
+ highlightedOption,
355
415
  getIndexOfValue,
356
416
  getValueAtIndex,
357
417
  isOpen,
@@ -360,6 +420,11 @@ export default function Combobox<M extends boolean>({
360
420
  ],
361
421
  );
362
422
 
423
+ /**
424
+ * Updates the focused chip based on the provided direction
425
+ * @param direction the direction to move the focus. `'next' | 'prev' | 'first' | 'last'`
426
+ * @param relativeToIndex the chip index to move focus relative to
427
+ */
363
428
  const updateFocusedChip = useCallback(
364
429
  (direction: Direction | null, relativeToIndex?: number) => {
365
430
  if (isMultiselect(selection)) {
@@ -409,17 +474,18 @@ export default function Combobox<M extends boolean>({
409
474
  [getActiveChipIndex, isMultiselect, selection],
410
475
  );
411
476
 
477
+ /**
478
+ * Handles an arrow key press
479
+ */
412
480
  const handleArrowKey = useCallback(
413
481
  (direction: 'left' | 'right', event: React.KeyboardEvent<Element>) => {
414
482
  // Remove focus from menu
415
- if (direction) setFocusedOption(null);
416
-
417
- const focusedElementName = getFocusedElementName();
483
+ if (direction) sethighlightedOption(null);
418
484
 
419
485
  switch (direction) {
420
486
  case 'right':
421
487
  switch (focusedElementName) {
422
- case 'Input': {
488
+ case ComboboxElement.Input: {
423
489
  // If cursor is at the end of the input
424
490
  if (
425
491
  inputRef.current?.selectionEnd ===
@@ -430,21 +496,26 @@ export default function Combobox<M extends boolean>({
430
496
  break;
431
497
  }
432
498
 
433
- case 'LastChip': {
434
- // if focus is on last chip, go to input
435
- event.preventDefault();
436
- setInputFocus(0);
437
- updateFocusedChip(null);
438
- break;
439
- }
440
-
441
- case 'FirstChip':
442
- case 'MiddleChip': {
499
+ case ComboboxElement.FirstChip:
500
+ case ComboboxElement.MiddleChip:
501
+ case ComboboxElement.LastChip: {
502
+ if (
503
+ focusedElementName === ComboboxElement.LastChip ||
504
+ // the first chip is also the last chip (i.e. only one)
505
+ selection?.length === 1
506
+ ) {
507
+ // if focus is on last chip, go to input
508
+ setInputFocus(0);
509
+ updateFocusedChip(null);
510
+ event.preventDefault();
511
+ break;
512
+ }
513
+ // First/middle chips
443
514
  updateFocusedChip('next');
444
515
  break;
445
516
  }
446
517
 
447
- case 'ClearButton':
518
+ case ComboboxElement.ClearButton:
448
519
  default:
449
520
  break;
450
521
  }
@@ -452,19 +523,19 @@ export default function Combobox<M extends boolean>({
452
523
 
453
524
  case 'left':
454
525
  switch (focusedElementName) {
455
- case 'ClearButton': {
526
+ case ComboboxElement.ClearButton: {
456
527
  event.preventDefault();
457
528
  setInputFocus(inputRef?.current?.value.length);
458
529
  break;
459
530
  }
460
531
 
461
- case 'Input':
462
- case 'MiddleChip':
463
- case 'LastChip': {
532
+ case ComboboxElement.Input:
533
+ case ComboboxElement.MiddleChip:
534
+ case ComboboxElement.LastChip: {
464
535
  if (isMultiselect(selection)) {
465
536
  // Break if cursor is not at the start of the input
466
537
  if (
467
- focusedElementName === 'Input' &&
538
+ focusedElementName === ComboboxElement.Input &&
468
539
  inputRef.current?.selectionStart !== 0
469
540
  ) {
470
541
  break;
@@ -475,7 +546,7 @@ export default function Combobox<M extends boolean>({
475
546
  break;
476
547
  }
477
548
 
478
- case 'FirstChip':
549
+ case ComboboxElement.FirstChip:
479
550
  default:
480
551
  break;
481
552
  }
@@ -486,7 +557,7 @@ export default function Combobox<M extends boolean>({
486
557
  }
487
558
  },
488
559
  [
489
- getFocusedElementName,
560
+ focusedElementName,
490
561
  isMultiselect,
491
562
  selection,
492
563
  setInputFocus,
@@ -494,36 +565,37 @@ export default function Combobox<M extends boolean>({
494
565
  ],
495
566
  );
496
567
 
497
- // Update the focused option when the inputValue changes
568
+ // When the input value changes (or when the menu opens)
569
+ // Update the focused option
498
570
  useEffect(() => {
499
571
  if (inputValue !== prevValue) {
500
- updateFocusedOption('first');
572
+ updateHighlightedOption('first');
501
573
  }
502
- }, [inputValue, isOpen, prevValue, updateFocusedOption]);
574
+ }, [inputValue, isOpen, prevValue, updateHighlightedOption]);
503
575
 
504
- // When the focused option chenges, update the menu scroll if necessary
576
+ // When the focused option changes, update the menu scroll if necessary
505
577
  useEffect(() => {
506
- if (focusedOption) {
507
- const focusedElementRef = getOptionRef(focusedOption);
578
+ if (highlightedOption) {
579
+ const focusedElementRef = getOptionRef(highlightedOption);
508
580
 
509
581
  if (focusedElementRef && focusedElementRef.current && menuRef.current) {
510
582
  const { offsetTop: optionTop } = focusedElementRef.current;
511
- const {
512
- scrollTop: menuScroll,
513
- offsetHeight: menuHeight,
514
- } = menuRef.current;
583
+ const { scrollTop: menuScroll, offsetHeight: menuHeight } =
584
+ menuRef.current;
515
585
 
516
586
  if (optionTop > menuHeight || optionTop < menuScroll) {
517
587
  menuRef.current.scrollTop = optionTop;
518
588
  }
519
589
  }
520
590
  }
521
- }, [focusedOption, getOptionRef]);
591
+ }, [highlightedOption, getOptionRef]);
522
592
 
523
593
  /**
524
- *
525
594
  * Rendering
526
- *
595
+ */
596
+
597
+ /**
598
+ * Callback to render the children as <InternalComboboxOption> elements
527
599
  */
528
600
  const renderInternalOptions = useCallback(
529
601
  (_children: React.ReactNode) => {
@@ -531,17 +603,17 @@ export default function Combobox<M extends boolean>({
531
603
  if (isComponentType(child, 'ComboboxOption')) {
532
604
  const { value, displayName } = getNameAndValue(child.props);
533
605
 
534
- if (isOptionVisible(value)) {
535
- const { className, glyph } = child.props;
606
+ if (shouldOptionBeVisible(value)) {
607
+ const { className, glyph, disabled } = child.props;
536
608
  const index = allOptions.findIndex(opt => opt.value === value);
537
609
 
538
- const isFocused = focusedOption === value;
610
+ const isFocused = highlightedOption === value;
539
611
  const isSelected = isMultiselect(selection)
540
612
  ? selection.includes(value)
541
613
  : selection === value;
542
614
 
543
615
  const setSelected = () => {
544
- setFocusedOption(value);
616
+ sethighlightedOption(value);
545
617
  updateSelection(value);
546
618
  setInputFocus();
547
619
 
@@ -558,6 +630,7 @@ export default function Combobox<M extends boolean>({
558
630
  displayName={displayName}
559
631
  isFocused={isFocused}
560
632
  isSelected={isSelected}
633
+ disabled={disabled}
561
634
  setSelected={setSelected}
562
635
  glyph={glyph}
563
636
  className={className}
@@ -584,34 +657,46 @@ export default function Combobox<M extends boolean>({
584
657
  },
585
658
  [
586
659
  allOptions,
587
- focusedOption,
660
+ highlightedOption,
588
661
  getOptionRef,
589
662
  isMultiselect,
590
- isOptionVisible,
663
+ shouldOptionBeVisible,
591
664
  selection,
592
665
  setInputFocus,
593
666
  updateSelection,
594
667
  ],
595
668
  );
596
669
 
597
- const renderedOptions = useMemo(() => renderInternalOptions(children), [
598
- children,
599
- renderInternalOptions,
600
- ]);
670
+ /**
671
+ * The rendered JSX elements for the options
672
+ */
673
+ const renderedOptionsJSX = useMemo(
674
+ () => renderInternalOptions(children),
675
+ [children, renderInternalOptions],
676
+ );
601
677
 
678
+ /**
679
+ * The rendered JSX for the selection Chips
680
+ */
602
681
  const renderedChips = useMemo(() => {
603
682
  if (isMultiselect(selection)) {
604
683
  return selection.filter(isValueValid).map((value, index) => {
605
- const displayName = getDisplayNameForValue(value);
684
+ const displayName = getDisplayNameForValue(value, allOptions);
685
+ const isFocused = focusedChip === value;
686
+ const chipRef = getChipRef(value);
687
+ const isLastChip = index >= selection.length - 1;
606
688
 
607
689
  const onRemove = () => {
608
- updateFocusedChip('next', index);
690
+ if (isLastChip) {
691
+ // Focus the input if this is the last chip in the set
692
+ setInputFocus();
693
+ updateFocusedChip(null);
694
+ } else {
695
+ updateFocusedChip('next', index);
696
+ }
609
697
  updateSelection(value);
610
698
  };
611
699
 
612
- const isFocused = focusedChip === value;
613
- const chipRef = getChipRef(value);
614
-
615
700
  const onFocus = () => {
616
701
  setFocusedChip(value);
617
702
  };
@@ -632,13 +717,17 @@ export default function Combobox<M extends boolean>({
632
717
  isMultiselect,
633
718
  selection,
634
719
  isValueValid,
635
- getDisplayNameForValue,
720
+ allOptions,
636
721
  focusedChip,
637
722
  getChipRef,
638
- updateFocusedChip,
639
723
  updateSelection,
724
+ setInputFocus,
725
+ updateFocusedChip,
640
726
  ]);
641
727
 
728
+ /**
729
+ * The rendered JSX for the input icons (clear, warn & caret)
730
+ */
642
731
  const renderedInputIcons = useMemo(() => {
643
732
  const handleClearButtonClick = (
644
733
  e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
@@ -663,7 +752,7 @@ export default function Combobox<M extends boolean>({
663
752
  ref={clearButtonRef}
664
753
  onClick={handleClearButtonClick}
665
754
  onFocus={handleClearButtonFocus}
666
- className={clearButton}
755
+ className={cx(clearButtonStyle, clearButtonFocusOverrideStyles)}
667
756
  >
668
757
  <Icon glyph="XWithCircle" />
669
758
  </IconButton>
@@ -686,10 +775,13 @@ export default function Combobox<M extends boolean>({
686
775
  isOpen,
687
776
  ]);
688
777
 
689
- // Do any of the options have an icon?
690
- const withIcons = useMemo(() => allOptions.some(opt => opt.hasGlyph), [
691
- allOptions,
692
- ]);
778
+ /**
779
+ * Flag to determine whether the rendered options have icons
780
+ */
781
+ const withIcons = useMemo(
782
+ () => allOptions.some(opt => opt.hasGlyph),
783
+ [allOptions],
784
+ );
693
785
 
694
786
  /**
695
787
  *
@@ -698,6 +790,7 @@ export default function Combobox<M extends boolean>({
698
790
  */
699
791
 
700
792
  const onCloseMenu = useCallback(() => {
793
+ // Single select, and no change to selection
701
794
  if (!isMultiselect(selection) && selection === prevSelection) {
702
795
  const exactMatchedOption = visibleOptions.find(
703
796
  option =>
@@ -711,12 +804,15 @@ export default function Combobox<M extends boolean>({
711
804
  } else {
712
805
  // Revert the value to the previous selection
713
806
  const displayName =
714
- getDisplayNameForValue(selection as SelectValueType<false>) ?? '';
807
+ getDisplayNameForValue(
808
+ selection as SelectValueType<false>,
809
+ allOptions,
810
+ ) ?? '';
715
811
  setInputValue(displayName);
716
812
  }
717
813
  }
718
814
  }, [
719
- getDisplayNameForValue,
815
+ allOptions,
720
816
  inputValue,
721
817
  isMultiselect,
722
818
  prevSelection,
@@ -729,20 +825,23 @@ export default function Combobox<M extends boolean>({
729
825
  if (doesSelectionExist) {
730
826
  if (isMultiselect(selection)) {
731
827
  // Scroll the wrapper to the end. No effect if not `overflow="scroll-x"`
732
- scrollToEnd();
828
+ scrollInputToEnd();
733
829
  } else if (!isMultiselect(selection)) {
734
830
  // Update the text input
735
831
  const displayName =
736
- getDisplayNameForValue(selection as SelectValueType<false>) ?? '';
832
+ getDisplayNameForValue(
833
+ selection as SelectValueType<false>,
834
+ allOptions,
835
+ ) ?? '';
737
836
  setInputValue(displayName);
738
837
  closeMenu();
739
838
  }
740
839
  } else {
741
840
  setInputValue('');
742
841
  }
743
- }, [doesSelectionExist, getDisplayNameForValue, isMultiselect, selection]);
842
+ }, [doesSelectionExist, allOptions, isMultiselect, selection]);
744
843
 
745
- // Set initialValue
844
+ // Set the initialValue
746
845
  useEffect(() => {
747
846
  if (initialValue) {
748
847
  if (isArray(initialValue)) {
@@ -788,27 +887,34 @@ export default function Combobox<M extends boolean>({
788
887
 
789
888
  // when the menu closes, update the value if needed
790
889
  useEffect(() => {
791
- if (!isOpen && prevOpenState !== isOpen) {
890
+ if (!isOpen && wasOpen) {
792
891
  onCloseMenu();
793
892
  }
794
- }, [isOpen, prevOpenState, onCloseMenu]);
893
+ }, [isOpen, wasOpen, onCloseMenu]);
795
894
 
796
895
  /**
797
896
  *
798
897
  * Menu management
799
898
  *
800
899
  */
801
- const closeMenu = () => setOpen(false);
802
- const openMenu = () => setOpen(true);
803
900
 
804
901
  const [menuWidth, setMenuWidth] = useState(0);
902
+
903
+ // When the menu opens, or the selection changes, or the focused option changes
904
+ // update the menu width
805
905
  useEffect(() => {
806
906
  setMenuWidth(comboboxRef.current?.clientWidth ?? 0);
807
- }, [comboboxRef, isOpen, focusedOption, selection]);
907
+ }, [comboboxRef, isOpen, highlightedOption, selection]);
908
+
909
+ // Handler fired when the manu has finished transitioning in/out
808
910
  const handleTransitionEnd = () => {
809
911
  setMenuWidth(comboboxRef.current?.clientWidth ?? 0);
810
912
  };
811
913
 
914
+ /**
915
+ * The rendered menu JSX contents
916
+ * Includes error, empty, search and default states
917
+ */
812
918
  const renderedMenuContents = useMemo((): JSX.Element => {
813
919
  switch (searchState) {
814
920
  case 'loading': {
@@ -835,53 +941,23 @@ export default function Combobox<M extends boolean>({
835
941
 
836
942
  case 'unset':
837
943
  default: {
838
- if (renderedOptions && renderedOptions.length > 0) {
839
- return <ul className={menuList}>{renderedOptions}</ul>;
944
+ if (renderedOptionsJSX && renderedOptionsJSX.length > 0) {
945
+ return <ul className={menuList}>{renderedOptionsJSX}</ul>;
840
946
  }
841
947
 
842
948
  return <span className={menuMessage}>{searchEmptyMessage}</span>;
843
949
  }
844
950
  }
845
951
  }, [
846
- renderedOptions,
952
+ renderedOptionsJSX,
847
953
  searchEmptyMessage,
848
954
  searchErrorMessage,
849
955
  searchLoadingMessage,
850
956
  searchState,
851
957
  ]);
852
958
 
853
- const viewportSize = useViewportSize();
854
-
855
- // Set the max height of the menu
856
- const maxHeight = useMemo(() => {
857
- // TODO - consolidate this hook with Select/ListMenu
858
- const maxMenuHeight = 274;
859
- const menuMargin = 8;
860
-
861
- if (viewportSize && comboboxRef.current && menuRef.current) {
862
- const {
863
- top: triggerTop,
864
- bottom: triggerBottom,
865
- } = comboboxRef.current.getBoundingClientRect();
866
-
867
- // Find out how much space is available above or below the trigger
868
- const safeSpace = Math.max(
869
- viewportSize.height - triggerBottom,
870
- triggerTop,
871
- );
872
-
873
- // if there's more than enough space, set to maxMenuHeight
874
- // otherwise fill the space available
875
- return Math.min(maxMenuHeight, safeSpace - menuMargin);
876
- }
877
-
878
- return maxMenuHeight;
879
- }, [viewportSize, comboboxRef, menuRef]);
880
-
881
- // Scroll the menu when the focus changes
882
- useEffect(() => {
883
- // get the focused option
884
- }, [focusedOption]);
959
+ /** The max height of the menu element */
960
+ const maxHeight = Math.min(256, useAvailableSpace(comboboxRef));
885
961
 
886
962
  /**
887
963
  *
@@ -897,7 +973,9 @@ export default function Combobox<M extends boolean>({
897
973
  };
898
974
 
899
975
  // Set focus to the input element on click
900
- const handleInputWrapperClick = (e: React.MouseEvent) => {
976
+ const handleComboboxClick = (e: React.MouseEvent) => {
977
+ // If we clicked the wrapper, not the input itself.
978
+ // (Focus is set automatically if the click is on the input)
901
979
  if (e.target !== inputRef.current) {
902
980
  let cursorPos = 0;
903
981
 
@@ -912,10 +990,12 @@ export default function Combobox<M extends boolean>({
912
990
  }
913
991
  };
914
992
 
915
- // Fired when the wrapper gains focus
916
- const handleInputWrapperFocus = () => {
917
- scrollToEnd();
993
+ // Fired whenever the wrapper gains focus,
994
+ // and any time the focus within changes
995
+ const handleComboboxFocus = (e: React.FocusEvent) => {
996
+ scrollInputToEnd();
918
997
  openMenu();
998
+ trackFocusedElement(getNameFromElement(e.target));
919
999
  };
920
1000
 
921
1001
  // Fired onChange
@@ -928,7 +1008,7 @@ export default function Combobox<M extends boolean>({
928
1008
  };
929
1009
 
930
1010
  const handleClearButtonFocus = () => {
931
- setFocusedOption(null);
1011
+ sethighlightedOption(null);
932
1012
  };
933
1013
 
934
1014
  const handleKeyDown = (event: React.KeyboardEvent) => {
@@ -939,6 +1019,7 @@ export default function Combobox<M extends boolean>({
939
1019
 
940
1020
  const isFocusInComponent = isFocusOnCombobox || isFocusInMenu;
941
1021
 
1022
+ // Only run if the focus is in the component
942
1023
  if (isFocusInComponent) {
943
1024
  // No support for modifiers yet
944
1025
  // TODO - Handle support for multiple chip selection
@@ -946,15 +1027,13 @@ export default function Combobox<M extends boolean>({
946
1027
  return;
947
1028
  }
948
1029
 
949
- const focusedElement = getFocusedElementName();
950
-
951
1030
  switch (event.keyCode) {
952
1031
  case keyMap.Tab: {
953
- switch (focusedElement) {
1032
+ switch (focusedElementName) {
954
1033
  case 'Input': {
955
1034
  if (!doesSelectionExist) {
956
1035
  closeMenu();
957
- updateFocusedOption('first');
1036
+ updateHighlightedOption('first');
958
1037
  updateFocusedChip(null);
959
1038
  }
960
1039
  // else use default behavior
@@ -983,21 +1062,25 @@ export default function Combobox<M extends boolean>({
983
1062
 
984
1063
  case keyMap.Escape: {
985
1064
  closeMenu();
986
- updateFocusedOption('first');
1065
+ updateHighlightedOption('first');
987
1066
  break;
988
1067
  }
989
1068
 
990
1069
  case keyMap.Enter: {
1070
+ // Select the highlighed option iff
1071
+ // the menu is open
1072
+ // we're focused on input element,
1073
+ // and the highlighted option is not disabled
991
1074
  if (
992
- // Focused on input element
993
- document.activeElement === inputRef.current &&
994
1075
  isOpen &&
995
- !isNull(focusedOption)
1076
+ focusedElementName === ComboboxElement.Input &&
1077
+ !isNull(highlightedOption) &&
1078
+ !isOptionDisabled(highlightedOption)
996
1079
  ) {
997
- updateSelection(focusedOption);
1080
+ updateSelection(highlightedOption);
998
1081
  } else if (
999
1082
  // Focused on clear button
1000
- document.activeElement === clearButtonRef.current
1083
+ focusedElementName === ComboboxElement.ClearButton
1001
1084
  ) {
1002
1085
  updateSelection(null);
1003
1086
  setInputFocus();
@@ -1006,16 +1089,18 @@ export default function Combobox<M extends boolean>({
1006
1089
  }
1007
1090
 
1008
1091
  case keyMap.Backspace: {
1009
- // Backspace key focuses last chip
1010
- // Delete key does not
1011
- if (
1012
- isMultiselect(selection) &&
1013
- inputRef.current?.selectionStart === 0
1014
- ) {
1015
- updateFocusedChip('last');
1016
- } else {
1017
- openMenu();
1092
+ // Backspace key focuses last chip if the input is focused
1093
+ // Note: Chip removal behavior is handled in `onRemove` defined in `renderChips`
1094
+ if (isMultiselect(selection)) {
1095
+ if (
1096
+ focusedElementName === 'Input' &&
1097
+ inputRef.current?.selectionStart === 0
1098
+ ) {
1099
+ updateFocusedChip('last');
1100
+ }
1018
1101
  }
1102
+ // Open the menu regardless
1103
+ openMenu();
1019
1104
  break;
1020
1105
  }
1021
1106
 
@@ -1023,9 +1108,11 @@ export default function Combobox<M extends boolean>({
1023
1108
  if (isOpen) {
1024
1109
  // Prevent the page from scrolling
1025
1110
  event.preventDefault();
1111
+ // only change option if the menu is already open
1112
+ updateHighlightedOption('next');
1113
+ } else {
1114
+ openMenu();
1026
1115
  }
1027
- openMenu();
1028
- updateFocusedOption('next');
1029
1116
  break;
1030
1117
  }
1031
1118
 
@@ -1033,8 +1120,11 @@ export default function Combobox<M extends boolean>({
1033
1120
  if (isOpen) {
1034
1121
  // Prevent the page from scrolling
1035
1122
  event.preventDefault();
1123
+ // only change option if the menu is already open
1124
+ updateHighlightedOption('prev');
1125
+ } else {
1126
+ openMenu();
1036
1127
  }
1037
- updateFocusedOption('prev');
1038
1128
  break;
1039
1129
  }
1040
1130
 
@@ -1062,19 +1152,44 @@ export default function Combobox<M extends boolean>({
1062
1152
  * Global Event Handler
1063
1153
  *
1064
1154
  */
1065
- // Global backdrop click handler
1066
- const handleBackdropClick = ({ target }: MouseEvent) => {
1067
- const isChildFocused =
1068
- menuRef.current?.contains(target as Node) ||
1069
- comboboxRef.current?.contains(target as Node) ||
1070
- false;
1071
1155
 
1072
- if (!isChildFocused) {
1073
- setOpen(false);
1074
- }
1075
- };
1076
-
1077
- useEventListener('mousedown', handleBackdropClick);
1156
+ /**
1157
+ * We add two event handlers to the document to handle the backdrop click behavior.
1158
+ * Intended behavior is to close the menu, and keep focus on the Combobox.
1159
+ * No other click event handlers should fire on backdrop click
1160
+ *
1161
+ * 1. Mousedown event fires
1162
+ * 2. We prevent `mousedown`'s default behavior, to prevent focus from being applied to the body (or other target)
1163
+ * 3. Click event fires
1164
+ * 4. We handle this event on _capture_, and stop propagation before the `click` event propagates all the way to any other element.
1165
+ * This ensures that even if we click on a button, that handler is not fired
1166
+ * 5. Then we call `closeMenu`, setting `isOpen = false`, and rerender the component
1167
+ */
1168
+ useEventListener(
1169
+ 'mousedown',
1170
+ (mousedown: MouseEvent) => {
1171
+ if (!doesComponentContainEventTarget(mousedown)) {
1172
+ mousedown.preventDefault(); // Prevent focus from being applied to body
1173
+ mousedown.stopPropagation(); // Stop any other mousedown events from firing
1174
+ }
1175
+ },
1176
+ {
1177
+ enabled: isOpen,
1178
+ },
1179
+ );
1180
+ useEventListener(
1181
+ 'click',
1182
+ (click: MouseEvent) => {
1183
+ if (!doesComponentContainEventTarget(click)) {
1184
+ click.stopPropagation(); // Stop any other click events from firing
1185
+ closeMenu();
1186
+ }
1187
+ },
1188
+ {
1189
+ options: { capture: true },
1190
+ enabled: isOpen,
1191
+ },
1192
+ );
1078
1193
 
1079
1194
  const popoverProps = {
1080
1195
  popoverZIndex,
@@ -1110,64 +1225,69 @@ export default function Combobox<M extends boolean>({
1110
1225
  >
1111
1226
  <div>
1112
1227
  {label && (
1113
- <Label id={labelId} htmlFor={inputId}>
1228
+ <Label
1229
+ id={labelId}
1230
+ htmlFor={inputId}
1231
+ className={_tempLabelDescriptionOverrideStyle}
1232
+ >
1114
1233
  {label}
1115
1234
  </Label>
1116
1235
  )}
1117
- {description && <Description>{description}</Description>}
1236
+ {description && (
1237
+ <Description className={_tempLabelDescriptionOverrideStyle}>
1238
+ {description}
1239
+ </Description>
1240
+ )}
1118
1241
  </div>
1119
1242
 
1120
- <InteractionRing
1121
- className={interactionRingStyle}
1122
- disabled={disabled}
1123
- color={interactionRingColor({ state, darkMode })}
1243
+ {/* Disable eslint: onClick sets focus. Key events would already have focus */}
1244
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
1245
+ <div
1246
+ ref={comboboxRef}
1247
+ role="combobox"
1248
+ aria-expanded={isOpen}
1249
+ aria-controls={menuId}
1250
+ aria-owns={menuId}
1251
+ tabIndex={-1}
1252
+ className={cx(comboboxStyle, {
1253
+ [comboboxFocusStyle]: focusedElementName === ComboboxElement.Input,
1254
+ })}
1255
+ onMouseDown={handleInputWrapperMousedown}
1256
+ onClick={handleComboboxClick}
1257
+ onFocus={handleComboboxFocus}
1258
+ onKeyDown={handleKeyDown}
1259
+ onTransitionEnd={handleTransitionEnd}
1260
+ data-disabled={disabled}
1261
+ data-state={state}
1124
1262
  >
1125
- {/* Disable eslint: onClick sets focus. Key events would already have focus */}
1126
- {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
1127
1263
  <div
1128
- ref={comboboxRef}
1129
- role="combobox"
1130
- aria-expanded={isOpen}
1131
- aria-controls={menuId}
1132
- aria-owns={menuId}
1133
- tabIndex={-1}
1134
- className={comboboxStyle}
1135
- onMouseDown={handleInputWrapperMousedown}
1136
- onClick={handleInputWrapperClick}
1137
- onFocus={handleInputWrapperFocus}
1138
- onKeyDown={handleKeyDown}
1139
- onTransitionEnd={handleTransitionEnd}
1140
- data-disabled={disabled}
1141
- data-state={state}
1264
+ ref={inputWrapperRef}
1265
+ className={inputWrapperStyle({
1266
+ overflow,
1267
+ isOpen,
1268
+ selection,
1269
+ size,
1270
+ value: inputValue,
1271
+ })}
1142
1272
  >
1143
- <div
1144
- ref={inputWrapperRef}
1145
- className={inputWrapperStyle({
1146
- overflow,
1147
- isOpen,
1148
- selection,
1149
- value: inputValue,
1150
- })}
1151
- >
1152
- {renderedChips}
1153
- <input
1154
- aria-label={ariaLabel ?? label}
1155
- aria-autocomplete="list"
1156
- aria-controls={menuId}
1157
- aria-labelledby={labelId}
1158
- ref={inputRef}
1159
- id={inputId}
1160
- className={inputElementStyle}
1161
- placeholder={placeholderValue}
1162
- disabled={disabled ?? undefined}
1163
- onChange={handleInputChange}
1164
- value={inputValue}
1165
- autoComplete="off"
1166
- />
1167
- </div>
1168
- {renderedInputIcons}
1273
+ {renderedChips}
1274
+ <input
1275
+ aria-label={ariaLabel ?? label}
1276
+ aria-autocomplete="list"
1277
+ aria-controls={menuId}
1278
+ aria-labelledby={labelId}
1279
+ ref={inputRef}
1280
+ id={inputId}
1281
+ className={inputElementStyle}
1282
+ placeholder={placeholderValue}
1283
+ disabled={disabled ?? undefined}
1284
+ onChange={handleInputChange}
1285
+ value={inputValue}
1286
+ autoComplete="off"
1287
+ />
1169
1288
  </div>
1170
- </InteractionRing>
1289
+ {renderedInputIcons}
1290
+ </div>
1171
1291
 
1172
1292
  {state === 'error' && errorMessage && (
1173
1293
  <div className={errorMessageStyle}>{errorMessage}</div>
@@ -1201,4 +1321,61 @@ export default function Combobox<M extends boolean>({
1201
1321
  </div>
1202
1322
  </ComboboxContext.Provider>
1203
1323
  );
1324
+
1325
+ // Closure-dependant utils
1326
+
1327
+ /**
1328
+ * Returns whether the event target is a Combobox element
1329
+ */
1330
+ function doesComponentContainEventTarget({ target }: MouseEvent): boolean {
1331
+ return (
1332
+ menuRef.current?.contains(target as Node) ||
1333
+ comboboxRef.current?.contains(target as Node) ||
1334
+ false
1335
+ );
1336
+ }
1337
+
1338
+ /**
1339
+ * Scrolls the combobox to the far right.
1340
+ * Used when `overflow == 'scroll-x'`.
1341
+ * Has no effect otherwise
1342
+ */
1343
+ function scrollInputToEnd() {
1344
+ if (inputWrapperRef && inputWrapperRef.current) {
1345
+ // TODO - consider converting to .scrollTo(). This is not yet suppoted in IE or jsdom
1346
+ inputWrapperRef.current.scrollLeft = inputWrapperRef.current.scrollWidth;
1347
+ }
1348
+ }
1349
+
1350
+ /**
1351
+ * Returns the provided element as a ComboboxElement string
1352
+ */
1353
+ function getNameFromElement(
1354
+ element?: Element | null,
1355
+ ): ComboboxElement | undefined {
1356
+ if (!element) return;
1357
+ if (inputRef.current?.contains(element)) return ComboboxElement.Input;
1358
+ if (clearButtonRef.current?.contains(element))
1359
+ return ComboboxElement.ClearButton;
1360
+
1361
+ const activeChipIndex = isMultiselect(selection)
1362
+ ? selection.findIndex(value =>
1363
+ getChipRef(value)?.current?.contains(element),
1364
+ )
1365
+ : -1;
1366
+
1367
+ if (isMultiselect(selection)) {
1368
+ if (activeChipIndex === 0) return ComboboxElement.FirstChip;
1369
+ if (activeChipIndex === selection.length - 1)
1370
+ return ComboboxElement.LastChip;
1371
+ if (activeChipIndex > 0) return ComboboxElement.MiddleChip;
1372
+ }
1373
+
1374
+ if (menuRef.current?.contains(element)) return ComboboxElement.Menu;
1375
+ if (comboboxRef.current?.contains(element)) return ComboboxElement.Combobox;
1376
+ }
1204
1377
  }
1378
+ /**
1379
+ * Why'd you have to go and make things so complicated?
1380
+ * - Avril; and also me to myself about this component
1381
+ */