@leafygreen-ui/combobox 1.0.3 → 1.2.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 (49) hide show
  1. package/CHANGELOG.md +59 -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 +1 -2
  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 +20 -10
  30. package/src/Chip.tsx +16 -9
  31. package/src/Combobox.spec.tsx +322 -139
  32. package/src/Combobox.story.tsx +274 -248
  33. package/src/Combobox.styles.ts +94 -24
  34. package/src/Combobox.tsx +446 -266
  35. package/src/Combobox.types.ts +43 -6
  36. package/src/ComboboxContext.tsx +2 -2
  37. package/src/ComboboxOption.tsx +34 -8
  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 -1
  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(
226
- () => allOptions.filter(isOptionVisible),
227
- [allOptions, isOptionVisible],
292
+ /**
293
+ * The array of visible options objects
294
+ */
295
+ const visibleOptions: Array<OptionObject> = useMemo(
296
+ () => allOptions.filter(shouldOptionBeVisible),
297
+ [allOptions, shouldOptionBeVisible],
228
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,17 +565,18 @@ 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;
@@ -516,12 +588,14 @@ export default function Combobox<M extends boolean>({
516
588
  }
517
589
  }
518
590
  }
519
- }, [focusedOption, getOptionRef]);
591
+ }, [highlightedOption, getOptionRef]);
520
592
 
521
593
  /**
522
- *
523
594
  * Rendering
524
- *
595
+ */
596
+
597
+ /**
598
+ * Callback to render the children as <InternalComboboxOption> elements
525
599
  */
526
600
  const renderInternalOptions = useCallback(
527
601
  (_children: React.ReactNode) => {
@@ -529,17 +603,17 @@ export default function Combobox<M extends boolean>({
529
603
  if (isComponentType(child, 'ComboboxOption')) {
530
604
  const { value, displayName } = getNameAndValue(child.props);
531
605
 
532
- if (isOptionVisible(value)) {
533
- const { className, glyph } = child.props;
606
+ if (shouldOptionBeVisible(value)) {
607
+ const { className, glyph, disabled } = child.props;
534
608
  const index = allOptions.findIndex(opt => opt.value === value);
535
609
 
536
- const isFocused = focusedOption === value;
610
+ const isFocused = highlightedOption === value;
537
611
  const isSelected = isMultiselect(selection)
538
612
  ? selection.includes(value)
539
613
  : selection === value;
540
614
 
541
615
  const setSelected = () => {
542
- setFocusedOption(value);
616
+ sethighlightedOption(value);
543
617
  updateSelection(value);
544
618
  setInputFocus();
545
619
 
@@ -556,6 +630,7 @@ export default function Combobox<M extends boolean>({
556
630
  displayName={displayName}
557
631
  isFocused={isFocused}
558
632
  isSelected={isSelected}
633
+ disabled={disabled}
559
634
  setSelected={setSelected}
560
635
  glyph={glyph}
561
636
  className={className}
@@ -582,34 +657,46 @@ export default function Combobox<M extends boolean>({
582
657
  },
583
658
  [
584
659
  allOptions,
585
- focusedOption,
660
+ highlightedOption,
586
661
  getOptionRef,
587
662
  isMultiselect,
588
- isOptionVisible,
663
+ shouldOptionBeVisible,
589
664
  selection,
590
665
  setInputFocus,
591
666
  updateSelection,
592
667
  ],
593
668
  );
594
669
 
595
- const renderedOptions = useMemo(
670
+ /**
671
+ * The rendered JSX elements for the options
672
+ */
673
+ const renderedOptionsJSX = useMemo(
596
674
  () => renderInternalOptions(children),
597
675
  [children, renderInternalOptions],
598
676
  );
599
677
 
678
+ /**
679
+ * The rendered JSX for the selection Chips
680
+ */
600
681
  const renderedChips = useMemo(() => {
601
682
  if (isMultiselect(selection)) {
602
683
  return selection.filter(isValueValid).map((value, index) => {
603
- 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;
604
688
 
605
689
  const onRemove = () => {
606
- 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
+ }
607
697
  updateSelection(value);
608
698
  };
609
699
 
610
- const isFocused = focusedChip === value;
611
- const chipRef = getChipRef(value);
612
-
613
700
  const onFocus = () => {
614
701
  setFocusedChip(value);
615
702
  };
@@ -630,13 +717,17 @@ export default function Combobox<M extends boolean>({
630
717
  isMultiselect,
631
718
  selection,
632
719
  isValueValid,
633
- getDisplayNameForValue,
720
+ allOptions,
634
721
  focusedChip,
635
722
  getChipRef,
636
- updateFocusedChip,
637
723
  updateSelection,
724
+ setInputFocus,
725
+ updateFocusedChip,
638
726
  ]);
639
727
 
728
+ /**
729
+ * The rendered JSX for the input icons (clear, warn & caret)
730
+ */
640
731
  const renderedInputIcons = useMemo(() => {
641
732
  const handleClearButtonClick = (
642
733
  e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
@@ -661,7 +752,7 @@ export default function Combobox<M extends boolean>({
661
752
  ref={clearButtonRef}
662
753
  onClick={handleClearButtonClick}
663
754
  onFocus={handleClearButtonFocus}
664
- className={clearButton}
755
+ className={cx(clearButtonStyle, clearButtonFocusOverrideStyles)}
665
756
  >
666
757
  <Icon glyph="XWithCircle" />
667
758
  </IconButton>
@@ -684,7 +775,9 @@ export default function Combobox<M extends boolean>({
684
775
  isOpen,
685
776
  ]);
686
777
 
687
- // Do any of the options have an icon?
778
+ /**
779
+ * Flag to determine whether the rendered options have icons
780
+ */
688
781
  const withIcons = useMemo(
689
782
  () => allOptions.some(opt => opt.hasGlyph),
690
783
  [allOptions],
@@ -697,6 +790,7 @@ export default function Combobox<M extends boolean>({
697
790
  */
698
791
 
699
792
  const onCloseMenu = useCallback(() => {
793
+ // Single select, and no change to selection
700
794
  if (!isMultiselect(selection) && selection === prevSelection) {
701
795
  const exactMatchedOption = visibleOptions.find(
702
796
  option =>
@@ -710,12 +804,15 @@ export default function Combobox<M extends boolean>({
710
804
  } else {
711
805
  // Revert the value to the previous selection
712
806
  const displayName =
713
- getDisplayNameForValue(selection as SelectValueType<false>) ?? '';
807
+ getDisplayNameForValue(
808
+ selection as SelectValueType<false>,
809
+ allOptions,
810
+ ) ?? '';
714
811
  setInputValue(displayName);
715
812
  }
716
813
  }
717
814
  }, [
718
- getDisplayNameForValue,
815
+ allOptions,
719
816
  inputValue,
720
817
  isMultiselect,
721
818
  prevSelection,
@@ -728,20 +825,23 @@ export default function Combobox<M extends boolean>({
728
825
  if (doesSelectionExist) {
729
826
  if (isMultiselect(selection)) {
730
827
  // Scroll the wrapper to the end. No effect if not `overflow="scroll-x"`
731
- scrollToEnd();
828
+ scrollInputToEnd();
732
829
  } else if (!isMultiselect(selection)) {
733
830
  // Update the text input
734
831
  const displayName =
735
- getDisplayNameForValue(selection as SelectValueType<false>) ?? '';
832
+ getDisplayNameForValue(
833
+ selection as SelectValueType<false>,
834
+ allOptions,
835
+ ) ?? '';
736
836
  setInputValue(displayName);
737
837
  closeMenu();
738
838
  }
739
839
  } else {
740
840
  setInputValue('');
741
841
  }
742
- }, [doesSelectionExist, getDisplayNameForValue, isMultiselect, selection]);
842
+ }, [doesSelectionExist, allOptions, isMultiselect, selection]);
743
843
 
744
- // Set initialValue
844
+ // Set the initialValue
745
845
  useEffect(() => {
746
846
  if (initialValue) {
747
847
  if (isArray(initialValue)) {
@@ -787,27 +887,34 @@ export default function Combobox<M extends boolean>({
787
887
 
788
888
  // when the menu closes, update the value if needed
789
889
  useEffect(() => {
790
- if (!isOpen && prevOpenState !== isOpen) {
890
+ if (!isOpen && wasOpen) {
791
891
  onCloseMenu();
792
892
  }
793
- }, [isOpen, prevOpenState, onCloseMenu]);
893
+ }, [isOpen, wasOpen, onCloseMenu]);
794
894
 
795
895
  /**
796
896
  *
797
897
  * Menu management
798
898
  *
799
899
  */
800
- const closeMenu = () => setOpen(false);
801
- const openMenu = () => setOpen(true);
802
900
 
803
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
804
905
  useEffect(() => {
805
906
  setMenuWidth(comboboxRef.current?.clientWidth ?? 0);
806
- }, [comboboxRef, isOpen, focusedOption, selection]);
907
+ }, [comboboxRef, isOpen, highlightedOption, selection]);
908
+
909
+ // Handler fired when the manu has finished transitioning in/out
807
910
  const handleTransitionEnd = () => {
808
911
  setMenuWidth(comboboxRef.current?.clientWidth ?? 0);
809
912
  };
810
913
 
914
+ /**
915
+ * The rendered menu JSX contents
916
+ * Includes error, empty, search and default states
917
+ */
811
918
  const renderedMenuContents = useMemo((): JSX.Element => {
812
919
  switch (searchState) {
813
920
  case 'loading': {
@@ -834,51 +941,23 @@ export default function Combobox<M extends boolean>({
834
941
 
835
942
  case 'unset':
836
943
  default: {
837
- if (renderedOptions && renderedOptions.length > 0) {
838
- return <ul className={menuList}>{renderedOptions}</ul>;
944
+ if (renderedOptionsJSX && renderedOptionsJSX.length > 0) {
945
+ return <ul className={menuList}>{renderedOptionsJSX}</ul>;
839
946
  }
840
947
 
841
948
  return <span className={menuMessage}>{searchEmptyMessage}</span>;
842
949
  }
843
950
  }
844
951
  }, [
845
- renderedOptions,
952
+ renderedOptionsJSX,
846
953
  searchEmptyMessage,
847
954
  searchErrorMessage,
848
955
  searchLoadingMessage,
849
956
  searchState,
850
957
  ]);
851
958
 
852
- const viewportSize = useViewportSize();
853
-
854
- // Set the max height of the menu
855
- const maxHeight = useMemo(() => {
856
- // TODO - consolidate this hook with Select/ListMenu
857
- const maxMenuHeight = 274;
858
- const menuMargin = 8;
859
-
860
- if (viewportSize && comboboxRef.current && menuRef.current) {
861
- const { top: triggerTop, bottom: triggerBottom } =
862
- comboboxRef.current.getBoundingClientRect();
863
-
864
- // Find out how much space is available above or below the trigger
865
- const safeSpace = Math.max(
866
- viewportSize.height - triggerBottom,
867
- triggerTop,
868
- );
869
-
870
- // if there's more than enough space, set to maxMenuHeight
871
- // otherwise fill the space available
872
- return Math.min(maxMenuHeight, safeSpace - menuMargin);
873
- }
874
-
875
- return maxMenuHeight;
876
- }, [viewportSize, comboboxRef, menuRef]);
877
-
878
- // Scroll the menu when the focus changes
879
- useEffect(() => {
880
- // get the focused option
881
- }, [focusedOption]);
959
+ /** The max height of the menu element */
960
+ const maxHeight = Math.min(256, useAvailableSpace(comboboxRef));
882
961
 
883
962
  /**
884
963
  *
@@ -894,7 +973,9 @@ export default function Combobox<M extends boolean>({
894
973
  };
895
974
 
896
975
  // Set focus to the input element on click
897
- 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)
898
979
  if (e.target !== inputRef.current) {
899
980
  let cursorPos = 0;
900
981
 
@@ -909,10 +990,12 @@ export default function Combobox<M extends boolean>({
909
990
  }
910
991
  };
911
992
 
912
- // Fired when the wrapper gains focus
913
- const handleInputWrapperFocus = () => {
914
- scrollToEnd();
993
+ // Fired whenever the wrapper gains focus,
994
+ // and any time the focus within changes
995
+ const handleComboboxFocus = (e: React.FocusEvent) => {
996
+ scrollInputToEnd();
915
997
  openMenu();
998
+ trackFocusedElement(getNameFromElement(e.target));
916
999
  };
917
1000
 
918
1001
  // Fired onChange
@@ -925,7 +1008,7 @@ export default function Combobox<M extends boolean>({
925
1008
  };
926
1009
 
927
1010
  const handleClearButtonFocus = () => {
928
- setFocusedOption(null);
1011
+ sethighlightedOption(null);
929
1012
  };
930
1013
 
931
1014
  const handleKeyDown = (event: React.KeyboardEvent) => {
@@ -936,6 +1019,7 @@ export default function Combobox<M extends boolean>({
936
1019
 
937
1020
  const isFocusInComponent = isFocusOnCombobox || isFocusInMenu;
938
1021
 
1022
+ // Only run if the focus is in the component
939
1023
  if (isFocusInComponent) {
940
1024
  // No support for modifiers yet
941
1025
  // TODO - Handle support for multiple chip selection
@@ -943,15 +1027,13 @@ export default function Combobox<M extends boolean>({
943
1027
  return;
944
1028
  }
945
1029
 
946
- const focusedElement = getFocusedElementName();
947
-
948
1030
  switch (event.keyCode) {
949
1031
  case keyMap.Tab: {
950
- switch (focusedElement) {
1032
+ switch (focusedElementName) {
951
1033
  case 'Input': {
952
1034
  if (!doesSelectionExist) {
953
1035
  closeMenu();
954
- updateFocusedOption('first');
1036
+ updateHighlightedOption('first');
955
1037
  updateFocusedChip(null);
956
1038
  }
957
1039
  // else use default behavior
@@ -980,21 +1062,25 @@ export default function Combobox<M extends boolean>({
980
1062
 
981
1063
  case keyMap.Escape: {
982
1064
  closeMenu();
983
- updateFocusedOption('first');
1065
+ updateHighlightedOption('first');
984
1066
  break;
985
1067
  }
986
1068
 
987
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
988
1074
  if (
989
- // Focused on input element
990
- document.activeElement === inputRef.current &&
991
1075
  isOpen &&
992
- !isNull(focusedOption)
1076
+ focusedElementName === ComboboxElement.Input &&
1077
+ !isNull(highlightedOption) &&
1078
+ !isOptionDisabled(highlightedOption)
993
1079
  ) {
994
- updateSelection(focusedOption);
1080
+ updateSelection(highlightedOption);
995
1081
  } else if (
996
1082
  // Focused on clear button
997
- document.activeElement === clearButtonRef.current
1083
+ focusedElementName === ComboboxElement.ClearButton
998
1084
  ) {
999
1085
  updateSelection(null);
1000
1086
  setInputFocus();
@@ -1003,16 +1089,18 @@ export default function Combobox<M extends boolean>({
1003
1089
  }
1004
1090
 
1005
1091
  case keyMap.Backspace: {
1006
- // Backspace key focuses last chip
1007
- // Delete key does not
1008
- if (
1009
- isMultiselect(selection) &&
1010
- inputRef.current?.selectionStart === 0
1011
- ) {
1012
- updateFocusedChip('last');
1013
- } else {
1014
- 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
+ }
1015
1101
  }
1102
+ // Open the menu regardless
1103
+ openMenu();
1016
1104
  break;
1017
1105
  }
1018
1106
 
@@ -1020,9 +1108,11 @@ export default function Combobox<M extends boolean>({
1020
1108
  if (isOpen) {
1021
1109
  // Prevent the page from scrolling
1022
1110
  event.preventDefault();
1111
+ // only change option if the menu is already open
1112
+ updateHighlightedOption('next');
1113
+ } else {
1114
+ openMenu();
1023
1115
  }
1024
- openMenu();
1025
- updateFocusedOption('next');
1026
1116
  break;
1027
1117
  }
1028
1118
 
@@ -1030,8 +1120,11 @@ export default function Combobox<M extends boolean>({
1030
1120
  if (isOpen) {
1031
1121
  // Prevent the page from scrolling
1032
1122
  event.preventDefault();
1123
+ // only change option if the menu is already open
1124
+ updateHighlightedOption('prev');
1125
+ } else {
1126
+ openMenu();
1033
1127
  }
1034
- updateFocusedOption('prev');
1035
1128
  break;
1036
1129
  }
1037
1130
 
@@ -1059,19 +1152,44 @@ export default function Combobox<M extends boolean>({
1059
1152
  * Global Event Handler
1060
1153
  *
1061
1154
  */
1062
- // Global backdrop click handler
1063
- const handleBackdropClick = ({ target }: MouseEvent) => {
1064
- const isChildFocused =
1065
- menuRef.current?.contains(target as Node) ||
1066
- comboboxRef.current?.contains(target as Node) ||
1067
- false;
1068
-
1069
- if (!isChildFocused) {
1070
- setOpen(false);
1071
- }
1072
- };
1073
1155
 
1074
- 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
+ );
1075
1193
 
1076
1194
  const popoverProps = {
1077
1195
  popoverZIndex,
@@ -1107,64 +1225,69 @@ export default function Combobox<M extends boolean>({
1107
1225
  >
1108
1226
  <div>
1109
1227
  {label && (
1110
- <Label id={labelId} htmlFor={inputId}>
1228
+ <Label
1229
+ id={labelId}
1230
+ htmlFor={inputId}
1231
+ className={_tempLabelDescriptionOverrideStyle}
1232
+ >
1111
1233
  {label}
1112
1234
  </Label>
1113
1235
  )}
1114
- {description && <Description>{description}</Description>}
1236
+ {description && (
1237
+ <Description className={_tempLabelDescriptionOverrideStyle}>
1238
+ {description}
1239
+ </Description>
1240
+ )}
1115
1241
  </div>
1116
1242
 
1117
- <InteractionRing
1118
- className={interactionRingStyle}
1119
- disabled={disabled}
1120
- 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}
1121
1262
  >
1122
- {/* Disable eslint: onClick sets focus. Key events would already have focus */}
1123
- {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
1124
1263
  <div
1125
- ref={comboboxRef}
1126
- role="combobox"
1127
- aria-expanded={isOpen}
1128
- aria-controls={menuId}
1129
- aria-owns={menuId}
1130
- tabIndex={-1}
1131
- className={comboboxStyle}
1132
- onMouseDown={handleInputWrapperMousedown}
1133
- onClick={handleInputWrapperClick}
1134
- onFocus={handleInputWrapperFocus}
1135
- onKeyDown={handleKeyDown}
1136
- onTransitionEnd={handleTransitionEnd}
1137
- data-disabled={disabled}
1138
- data-state={state}
1264
+ ref={inputWrapperRef}
1265
+ className={inputWrapperStyle({
1266
+ overflow,
1267
+ isOpen,
1268
+ selection,
1269
+ size,
1270
+ value: inputValue,
1271
+ })}
1139
1272
  >
1140
- <div
1141
- ref={inputWrapperRef}
1142
- className={inputWrapperStyle({
1143
- overflow,
1144
- isOpen,
1145
- selection,
1146
- value: inputValue,
1147
- })}
1148
- >
1149
- {renderedChips}
1150
- <input
1151
- aria-label={ariaLabel ?? label}
1152
- aria-autocomplete="list"
1153
- aria-controls={menuId}
1154
- aria-labelledby={labelId}
1155
- ref={inputRef}
1156
- id={inputId}
1157
- className={inputElementStyle}
1158
- placeholder={placeholderValue}
1159
- disabled={disabled ?? undefined}
1160
- onChange={handleInputChange}
1161
- value={inputValue}
1162
- autoComplete="off"
1163
- />
1164
- </div>
1165
- {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
+ />
1166
1288
  </div>
1167
- </InteractionRing>
1289
+ {renderedInputIcons}
1290
+ </div>
1168
1291
 
1169
1292
  {state === 'error' && errorMessage && (
1170
1293
  <div className={errorMessageStyle}>{errorMessage}</div>
@@ -1198,4 +1321,61 @@ export default function Combobox<M extends boolean>({
1198
1321
  </div>
1199
1322
  </ComboboxContext.Provider>
1200
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
+ }
1201
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
+ */