• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JedWatson / react-select / 709cb899-8707-41eb-8849-9acb620d1412

12 Aug 2024 12:33PM UTC coverage: 75.674% (-0.1%) from 75.789%
709cb899-8707-41eb-8849-9acb620d1412

Pull #6056

circleci

konstantinbabur
Nested groups support
Pull Request #6056: Nested groups support

660 of 1056 branches covered (62.5%)

18 of 23 new or added lines in 1 file covered. (78.26%)

116 existing lines in 2 files now uncovered.

1039 of 1373 relevant lines covered (75.67%)

1923.58 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.05
/packages/react-select/src/Select.tsx
1
import * as React from 'react';
2
import {
3
  AriaAttributes,
4
  Component,
5
  FocusEventHandler,
6
  FormEventHandler,
7
  KeyboardEventHandler,
8
  MouseEventHandler,
9
  ReactNode,
10
  RefCallback,
11
  TouchEventHandler,
12
} from 'react';
13
import { MenuPlacer } from './components/Menu';
14
import LiveRegion from './components/LiveRegion';
15

16
import { createFilter, FilterOptionOption } from './filters';
17
import { DummyInput, ScrollManager, RequiredInput } from './internal/index';
18
import { AriaLiveMessages, AriaSelection } from './accessibility/index';
19
import { isAppleDevice } from './accessibility/helpers';
20

21
import {
22
  classNames,
23
  cleanValue,
24
  isTouchCapable,
25
  isMobileDevice,
26
  noop,
27
  scrollIntoView,
28
  isDocumentElement,
29
  notNullish,
30
  valueTernary,
31
  multiValueAsValue,
32
  singleValueAsValue,
33
} from './utils';
34

35
import {
36
  formatGroupLabel as formatGroupLabelBuiltin,
37
  getOptionLabel as getOptionLabelBuiltin,
38
  getOptionValue as getOptionValueBuiltin,
39
  isOptionDisabled as isOptionDisabledBuiltin,
40
} from './builtins';
41

42
import { defaultComponents, SelectComponentsConfig } from './components/index';
43

44
import {
45
  ClassNamesConfig,
46
  defaultStyles,
47
  StylesConfig,
48
  StylesProps,
49
} from './styles';
50
import { defaultTheme, ThemeConfig } from './theme';
51

52
import {
53
  ActionMeta,
54
  FocusDirection,
55
  GetOptionLabel,
56
  GetOptionValue,
57
  GroupBase,
58
  InputActionMeta,
59
  MenuPlacement,
60
  MenuPosition,
61
  OnChangeValue,
62
  Options,
63
  OptionsOrGroups,
64
  PropsValue,
65
  SetValueAction,
66
} from './types';
67

68
export type FormatOptionLabelContext = 'menu' | 'value';
69
export interface FormatOptionLabelMeta<Option> {
70
  context: FormatOptionLabelContext;
71
  inputValue: string;
72
  selectValue: Options<Option>;
73
}
74

75
export interface Props<
76
  Option,
77
  IsMulti extends boolean,
78
  Group extends GroupBase<Option>
79
> {
80
  /** HTML ID of an element containing an error message related to the input**/
81
  'aria-errormessage'?: AriaAttributes['aria-errormessage'];
82
  /** Indicate if the value entered in the field is invalid **/
83
  'aria-invalid'?: AriaAttributes['aria-invalid'];
84
  /** Aria label (for assistive tech) */
85
  'aria-label'?: AriaAttributes['aria-label'];
86
  /** HTML ID of an element that should be used as the label (for assistive tech) */
87
  'aria-labelledby'?: AriaAttributes['aria-labelledby'];
88
  /** Used to set the priority with which screen reader should treat updates to live regions. The possible settings are: off, polite (default) or assertive */
89
  'aria-live'?: AriaAttributes['aria-live'];
90
  /** Customise the messages used by the aria-live component */
91
  ariaLiveMessages?: AriaLiveMessages<Option, IsMulti, Group>;
92
  /** Focus the control when it is mounted */
93
  autoFocus?: boolean;
94
  /** Remove the currently focused option when the user presses backspace when Select isClearable or isMulti */
95
  backspaceRemovesValue: boolean;
96
  /** Remove focus from the input when the user selects an option (handy for dismissing the keyboard on touch devices) */
97
  blurInputOnSelect: boolean;
98
  /** When the user reaches the top/bottom of the menu, prevent scroll on the scroll-parent  */
99
  captureMenuScroll: boolean;
100
  /** Sets a className attribute on the outer component */
101
  className?: string;
102
  /**
103
   * If provided, all inner components will be given a prefixed className attribute.
104
   *
105
   * This is useful when styling via CSS classes instead of the Styles API approach.
106
   */
107
  classNamePrefix?: string | null;
108
  /**
109
   * Provide classNames based on state for each inner component
110
   */
111
  classNames: ClassNamesConfig<Option, IsMulti, Group>;
112
  /** Close the select menu when the user selects an option */
113
  closeMenuOnSelect: boolean;
114
  /**
115
   * If `true`, close the select menu when the user scrolls the document/body.
116
   *
117
   * If a function, takes a standard javascript `ScrollEvent` you return a boolean:
118
   *
119
   * `true` => The menu closes
120
   *
121
   * `false` => The menu stays open
122
   *
123
   * This is useful when you have a scrollable modal and want to portal the menu out,
124
   * but want to avoid graphical issues.
125
   */
126
  closeMenuOnScroll: boolean | ((event: Event) => boolean);
127
  /**
128
   * This complex object includes all the compositional components that are used
129
   * in `react-select`. If you wish to overwrite a component, pass in an object
130
   * with the appropriate namespace.
131
   *
132
   * If you only wish to restyle a component, we recommend using the `styles` prop
133
   * instead. For a list of the components that can be passed in, and the shape
134
   * that will be passed to them, see [the components docs](/components)
135
   */
136
  components: SelectComponentsConfig<Option, IsMulti, Group>;
137
  /** Whether the value of the select, e.g. SingleValue, should be displayed in the control. */
138
  controlShouldRenderValue: boolean;
139
  /** Delimiter used to join multiple values into a single HTML Input value */
140
  delimiter?: string;
141
  /** Clear all values when the user presses escape AND the menu is closed */
142
  escapeClearsValue: boolean;
143
  /** Custom method to filter whether an option should be displayed in the menu */
144
  filterOption:
145
    | ((option: FilterOptionOption<Option>, inputValue: string) => boolean)
146
    | null;
147
  /**
148
   * Formats group labels in the menu as React components
149
   *
150
   * An example can be found in the [Replacing builtins](/advanced#replacing-builtins) documentation.
151
   */
152
  formatGroupLabel: (group: Group) => ReactNode;
153
  /** Formats option labels in the menu and control as React components */
154
  formatOptionLabel?: (
155
    data: Option,
156
    formatOptionLabelMeta: FormatOptionLabelMeta<Option>
157
  ) => ReactNode;
158
  /**
159
   * Resolves option data to a string to be displayed as the label by components
160
   *
161
   * Note: Failure to resolve to a string type can interfere with filtering and
162
   * screen reader support.
163
   */
164
  getOptionLabel: GetOptionLabel<Option>;
165
  /** Resolves option data to a string to compare options and specify value attributes */
166
  getOptionValue: GetOptionValue<Option>;
167
  /** Hide the selected option from the menu */
168
  hideSelectedOptions?: boolean;
169
  /** The id to set on the SelectContainer component. */
170
  id?: string;
171
  /** The value of the search input */
172
  inputValue: string;
173
  /** The id of the search input */
174
  inputId?: string;
175
  /** Define an id prefix for the select components e.g. {your-id}-value */
176
  instanceId?: number | string;
177
  /** Is the select value clearable */
178
  isClearable?: boolean;
179
  /** Is the select disabled */
180
  isDisabled: boolean;
181
  /** Is the select in a state of loading (async) */
182
  isLoading: boolean;
183
  /**
184
   * Override the built-in logic to detect whether an option is disabled
185
   *
186
   * An example can be found in the [Replacing builtins](/advanced#replacing-builtins) documentation.
187
   */
188
  isOptionDisabled: (option: Option, selectValue: Options<Option>) => boolean;
189
  /** Override the built-in logic to detect whether an option is selected */
190
  isOptionSelected?: (option: Option, selectValue: Options<Option>) => boolean;
191
  /** Support multiple selected options */
192
  isMulti: IsMulti;
193
  /** Is the select direction right-to-left */
194
  isRtl: boolean;
195
  /** Whether to enable search functionality */
196
  isSearchable: boolean;
197
  /** Async: Text to display when loading options */
198
  loadingMessage: (obj: { inputValue: string }) => ReactNode;
199
  /** Minimum height of the menu before flipping */
200
  minMenuHeight: number;
201
  /** Maximum height of the menu before scrolling */
202
  maxMenuHeight: number;
203
  /** Whether the menu is open */
204
  menuIsOpen: boolean;
205
  /**
206
   * Default placement of the menu in relation to the control. 'auto' will flip
207
   * when there isn't enough space below the control.
208
   */
209
  menuPlacement: MenuPlacement;
210
  /** The CSS position value of the menu, when "fixed" extra layout management is required */
211
  menuPosition: MenuPosition;
212
  /**
213
   * Whether the menu should use a portal, and where it should attach
214
   *
215
   * An example can be found in the [Portaling](/advanced#portaling) documentation
216
   */
217
  menuPortalTarget?: HTMLElement | null;
218
  /** Whether to block scroll events when the menu is open */
219
  menuShouldBlockScroll: boolean;
220
  /** Whether the menu should be scrolled into view when it opens */
221
  menuShouldScrollIntoView: boolean;
222
  /** Name of the HTML Input (optional - without this, no input will be rendered) */
223
  name?: string;
224
  /** Text to display when there are no options */
225
  noOptionsMessage: (obj: { inputValue: string }) => ReactNode;
226
  /** Handle blur events on the control */
227
  onBlur?: FocusEventHandler<HTMLInputElement>;
228
  /** Handle change events on the select */
229
  onChange: (
230
    newValue: OnChangeValue<Option, IsMulti>,
231
    actionMeta: ActionMeta<Option>
232
  ) => void;
233
  /** Handle focus events on the control */
234
  onFocus?: FocusEventHandler<HTMLInputElement>;
235
  /** Handle change events on the input */
236
  onInputChange: (newValue: string, actionMeta: InputActionMeta) => void;
237
  /** Handle key down events on the select */
238
  onKeyDown?: KeyboardEventHandler<HTMLDivElement>;
239
  /** Handle the menu opening */
240
  onMenuOpen: () => void;
241
  /** Handle the menu closing */
242
  onMenuClose: () => void;
243
  /** Fired when the user scrolls to the top of the menu */
244
  onMenuScrollToTop?: (event: WheelEvent | TouchEvent) => void;
245
  /** Fired when the user scrolls to the bottom of the menu */
246
  onMenuScrollToBottom?: (event: WheelEvent | TouchEvent) => void;
247
  /** Allows control of whether the menu is opened when the Select is focused */
248
  openMenuOnFocus: boolean;
249
  /** Allows control of whether the menu is opened when the Select is clicked */
250
  openMenuOnClick: boolean;
251
  /** Array of options that populate the select menu */
252
  options: OptionsOrGroups<Option, Group>;
253
  /** Number of options to jump in menu when page{up|down} keys are used */
254
  pageSize: number;
255
  /** Placeholder for the select value */
256
  placeholder: ReactNode;
257
  /** Status to relay to screen readers */
258
  screenReaderStatus: (obj: { count: number }) => string;
259
  /**
260
   * Style modifier methods
261
   *
262
   * A basic example can be found at the bottom of the [Replacing builtins](/advanced#replacing-builtins) documentation.
263
   */
264
  styles: StylesConfig<Option, IsMulti, Group>;
265
  /** Theme modifier method */
266
  theme?: ThemeConfig;
267
  /** Sets the tabIndex attribute on the input */
268
  tabIndex: number;
269
  /** Select the currently focused option when the user presses tab */
270
  tabSelectsValue: boolean;
271
  /** Remove all non-essential styles */
272
  unstyled: boolean;
273
  /** The value of the select; reflected by the selected option */
274
  value: PropsValue<Option>;
275
  /** Sets the form attribute on the input */
276
  form?: string;
277
  /** Marks the value-holding input as required for form validation */
278
  required?: boolean;
279
}
280

281
export const defaultProps = {
5✔
282
  'aria-live': 'polite',
283
  backspaceRemovesValue: true,
284
  blurInputOnSelect: isTouchCapable(),
285
  captureMenuScroll: !isTouchCapable(),
286
  classNames: {},
287
  closeMenuOnSelect: true,
288
  closeMenuOnScroll: false,
289
  components: {},
290
  controlShouldRenderValue: true,
291
  escapeClearsValue: false,
292
  filterOption: createFilter(),
293
  formatGroupLabel: formatGroupLabelBuiltin,
294
  getOptionLabel: getOptionLabelBuiltin,
295
  getOptionValue: getOptionValueBuiltin,
296
  isDisabled: false,
297
  isLoading: false,
298
  isMulti: false,
299
  isRtl: false,
300
  isSearchable: true,
301
  isOptionDisabled: isOptionDisabledBuiltin,
302
  loadingMessage: () => 'Loading...',
8✔
303
  maxMenuHeight: 300,
304
  minMenuHeight: 140,
305
  menuIsOpen: false,
306
  menuPlacement: 'bottom',
307
  menuPosition: 'absolute',
308
  menuShouldBlockScroll: false,
309
  menuShouldScrollIntoView: !isMobileDevice(),
310
  noOptionsMessage: () => 'No options',
28✔
311
  openMenuOnFocus: false,
312
  openMenuOnClick: true,
313
  options: [],
314
  pageSize: 5,
315
  placeholder: 'Select...',
316
  screenReaderStatus: ({ count }: { count: number }) =>
317
    `${count} result${count !== 1 ? 's' : ''} available`,
581✔
318
  styles: {},
319
  tabIndex: 0,
320
  tabSelectsValue: true,
321
  unstyled: false,
322
};
323

324
interface State<
325
  Option,
326
  IsMulti extends boolean,
327
  Group extends GroupBase<Option>
328
> {
329
  ariaSelection: AriaSelection<Option, IsMulti> | null;
330
  inputIsHidden: boolean;
331
  isFocused: boolean;
332
  focusedOption: Option | null;
333
  focusedOptionId: string | null;
334
  focusableOptionsWithIds: FocusableOptionWithId<Option>[];
335
  focusedValue: Option | null;
336
  selectValue: Options<Option>;
337
  clearFocusValueOnUpdate: boolean;
338
  prevWasFocused: boolean;
339
  inputIsHiddenAfterUpdate: boolean | null | undefined;
340
  prevProps: Props<Option, IsMulti, Group> | void;
341
  instancePrefix: string;
342
}
343

344
interface CategorizedOption<Option> {
345
  type: 'option';
346
  data: Option;
347
  isDisabled: boolean;
348
  isSelected: boolean;
349
  label: string;
350
  value: string;
351
  index: number;
352
}
353

354
interface FocusableOptionWithId<Option> {
355
  data: Option;
356
  id: string;
357
}
358

359
interface CategorizedGroup<Option, Group extends GroupBase<Option>> {
360
  type: 'group';
361
  data: Group;
362
  options: readonly (
363
    | CategorizedOption<Option>
364
    | CategorizedGroup<Option, Group>
365
  )[];
366
  index: number;
367
}
368

369
type CategorizedGroupOrOption<Option, Group extends GroupBase<Option>> =
370
  | CategorizedGroup<Option, Group>
371
  | CategorizedOption<Option>;
372

373
function toCategorizedOption<
374
  Option,
375
  IsMulti extends boolean,
376
  Group extends GroupBase<Option>
377
>(
378
  props: Props<Option, IsMulti, Group>,
379
  option: Option,
380
  selectValue: Options<Option>,
381
  index: number
382
): CategorizedOption<Option> {
383
  const isDisabled = isOptionDisabled(props, option, selectValue);
37,287✔
384
  const isSelected = isOptionSelected(props, option, selectValue);
37,287✔
385
  const label = getOptionLabel(props, option);
37,287✔
386
  const value = getOptionValue(props, option);
37,287✔
387

388
  return {
37,287✔
389
    type: 'option',
390
    data: option,
391
    isDisabled,
392
    isSelected,
393
    label,
394
    value,
395
    index,
396
  };
397
}
398

399
function toCategorizedOptions<
400
  Option,
401
  IsMulti extends boolean,
402
  Group extends GroupBase<Option>
403
>(
404
  props: Props<Option, IsMulti, Group>,
405
  options: Props<Option, IsMulti, Group>['options'],
406
  selectValue: Options<Option>
407
): CategorizedGroupOrOption<Option, Group>[] {
408
  return options
2,546✔
409
    .map((groupOrOption, groupOrOptionIndex) => {
410
      if ('options' in groupOrOption) {
37,460✔
411
        const categorizedOptions = toCategorizedOptions(
173✔
412
          props,
413
          groupOrOption.options,
414
          selectValue
415
        );
416
        return categorizedOptions.length > 0
173✔
417
          ? {
418
              type: 'group' as const,
419
              data: groupOrOption,
420
              options: categorizedOptions,
421
              index: groupOrOptionIndex,
422
            }
423
          : undefined;
424
      }
425
      const categorizedOption = toCategorizedOption(
37,287✔
426
        props,
427
        groupOrOption,
428
        selectValue,
429
        groupOrOptionIndex
430
      );
431
      return isFocusable(props, categorizedOption)
37,287✔
432
        ? categorizedOption
433
        : undefined;
434
    })
435
    .filter(notNullish);
436
}
437

438
function buildCategorizedOptions<
439
  Option,
440
  IsMulti extends boolean,
441
  Group extends GroupBase<Option>
442
>(
443
  props: Props<Option, IsMulti, Group>,
444
  selectValue: Options<Option>
445
): CategorizedGroupOrOption<Option, Group>[] {
446
  return toCategorizedOptions(props, props.options, selectValue);
2,373✔
447
}
448

449
function buildFocusableOptionsFromCategorizedOptions<
450
  Option,
451
  Group extends GroupBase<Option>
452
>(categorizedOptions: readonly CategorizedGroupOrOption<Option, Group>[]) {
453
  return categorizedOptions.reduce<Option[]>(
1,675✔
454
    (optionsAccumulator, categorizedOption) => {
455
      if (categorizedOption.type === 'group') {
23,521✔
456
        optionsAccumulator.push(
88✔
457
          ...categorizedOption.options.reduce((options, option) => {
458
            if (option.type === 'group') {
492!
NEW
459
              options.push(
×
460
                ...buildFocusableOptionsFromCategorizedOptions(option.options)
461
              );
462

NEW
463
              return options;
×
464
            }
465

466
            options.push(option.data);
492✔
467

468
            return options;
492✔
469
          }, [] as Option[])
470
        );
471
      } else {
472
        optionsAccumulator.push(categorizedOption.data);
23,433✔
473
      }
474
      return optionsAccumulator;
23,521✔
475
    },
476
    []
477
  );
478
}
479

480
function buildFocusableOptionsWithIds<Option, Group extends GroupBase<Option>>(
481
  categorizedOptions: readonly CategorizedGroupOrOption<Option, Group>[],
482
  optionId: string
483
) {
484
  return categorizedOptions.reduce<FocusableOptionWithId<Option>[]>(
133✔
485
    (optionsAccumulator, categorizedOption) => {
486
      if (categorizedOption.type === 'group') {
1,052✔
487
        optionsAccumulator.push(
12✔
488
          ...categorizedOption.options.reduce((options, option) => {
489
            if (option.type === 'group') {
58!
NEW
490
              options.push(
×
491
                ...buildFocusableOptionsWithIds(option.options, optionId)
492
              );
NEW
493
              return options;
×
494
            }
495
            options.push({
58✔
496
              data: option.data,
497
              id: `${optionId}-${categorizedOption.index}-${option.index}`,
498
            });
499
            return options;
58✔
500
          }, [] as FocusableOptionWithId<Option>[])
501
        );
502
      } else {
503
        optionsAccumulator.push({
1,040✔
504
          data: categorizedOption.data,
505
          id: `${optionId}-${categorizedOption.index}`,
506
        });
507
      }
508
      return optionsAccumulator;
1,052✔
509
    },
510
    []
511
  );
512
}
513

514
function buildFocusableOptions<
515
  Option,
516
  IsMulti extends boolean,
517
  Group extends GroupBase<Option>
518
>(props: Props<Option, IsMulti, Group>, selectValue: Options<Option>) {
519
  return buildFocusableOptionsFromCategorizedOptions(
112✔
520
    buildCategorizedOptions(props, selectValue)
521
  );
522
}
523

524
function isFocusable<
525
  Option,
526
  IsMulti extends boolean,
527
  Group extends GroupBase<Option>
528
>(
529
  props: Props<Option, IsMulti, Group>,
530
  categorizedOption: CategorizedOption<Option>
531
) {
532
  const { inputValue = '' } = props;
37,287!
533
  const { data, isSelected, label, value } = categorizedOption;
37,287✔
534

535
  return (
37,287✔
536
    (!shouldHideSelectedOptions(props) || !isSelected) &&
88,801✔
537
    filterOption(props, { label, value, data }, inputValue)
538
  );
539
}
540

541
function getNextFocusedValue<
542
  Option,
543
  IsMulti extends boolean,
544
  Group extends GroupBase<Option>
545
>(state: State<Option, IsMulti, Group>, nextSelectValue: Options<Option>) {
546
  const { focusedValue, selectValue: lastSelectValue } = state;
26✔
547
  const lastFocusedIndex = lastSelectValue.indexOf(focusedValue!);
26✔
548
  if (lastFocusedIndex > -1) {
26!
UNCOV
549
    const nextFocusedIndex = nextSelectValue.indexOf(focusedValue!);
×
UNCOV
550
    if (nextFocusedIndex > -1) {
×
551
      // the focused value is still in the selectValue, return it
552
      return focusedValue;
×
UNCOV
553
    } else if (lastFocusedIndex < nextSelectValue.length) {
×
554
      // the focusedValue is not present in the next selectValue array by
555
      // reference, so return the new value at the same index
UNCOV
556
      return nextSelectValue[lastFocusedIndex];
×
557
    }
558
  }
559
  return null;
26✔
560
}
561

562
function getNextFocusedOption<
563
  Option,
564
  IsMulti extends boolean,
565
  Group extends GroupBase<Option>
566
>(state: State<Option, IsMulti, Group>, options: Options<Option>) {
567
  const { focusedOption: lastFocusedOption } = state;
154✔
568
  return lastFocusedOption && options.indexOf(lastFocusedOption) > -1
154✔
569
    ? lastFocusedOption
570
    : options[0];
571
}
572

573
const getFocusedOptionId = <Option,>(
5✔
574
  focusableOptionsWithIds: FocusableOptionWithId<Option>[],
575
  focusedOption: Option
576
) => {
577
  const focusedOptionId = focusableOptionsWithIds.find(
511✔
578
    (option) => option.data === focusedOption
630✔
579
  )?.id;
580
  return focusedOptionId || null;
511✔
581
};
582

583
const getOptionLabel = <
5✔
584
  Option,
585
  IsMulti extends boolean,
586
  Group extends GroupBase<Option>
587
>(
588
  props: Props<Option, IsMulti, Group>,
589
  data: Option
590
): string => {
591
  return props.getOptionLabel(data);
46,159✔
592
};
593
const getOptionValue = <
5✔
594
  Option,
595
  IsMulti extends boolean,
596
  Group extends GroupBase<Option>
597
>(
598
  props: Props<Option, IsMulti, Group>,
599
  data: Option
600
): string => {
601
  return props.getOptionValue(data);
80,368✔
602
};
603

604
function isOptionDisabled<
605
  Option,
606
  IsMulti extends boolean,
607
  Group extends GroupBase<Option>
608
>(
609
  props: Props<Option, IsMulti, Group>,
610
  option: Option,
611
  selectValue: Options<Option>
612
): boolean {
613
  return typeof props.isOptionDisabled === 'function'
37,340!
614
    ? props.isOptionDisabled(option, selectValue)
615
    : false;
616
}
617
function isOptionSelected<
618
  Option,
619
  IsMulti extends boolean,
620
  Group extends GroupBase<Option>
621
>(
622
  props: Props<Option, IsMulti, Group>,
623
  option: Option,
624
  selectValue: Options<Option>
625
): boolean {
626
  if (selectValue.indexOf(option) > -1) return true;
37,314✔
627
  if (typeof props.isOptionSelected === 'function') {
37,002✔
628
    return props.isOptionSelected(option, selectValue);
102✔
629
  }
630
  const candidate = getOptionValue(props, option);
36,900✔
631
  return selectValue.some((i) => getOptionValue(props, i) === candidate);
36,900✔
632
}
633
function filterOption<
634
  Option,
635
  IsMulti extends boolean,
636
  Group extends GroupBase<Option>
637
>(
638
  props: Props<Option, IsMulti, Group>,
639
  option: FilterOptionOption<Option>,
640
  inputValue: string
641
) {
642
  return props.filterOption ? props.filterOption(option, inputValue) : true;
37,184✔
643
}
644

645
const shouldHideSelectedOptions = <
5✔
646
  Option,
647
  IsMulti extends boolean,
648
  Group extends GroupBase<Option>
649
>(
650
  props: Props<Option, IsMulti, Group>
651
) => {
652
  const { hideSelectedOptions, isMulti } = props;
37,287✔
653
  if (hideSelectedOptions === undefined) return isMulti;
37,287✔
654
  return hideSelectedOptions;
3,949✔
655
};
656

657
let instanceId = 1;
5✔
658

659
export default class Select<
660
  Option = unknown,
661
  IsMulti extends boolean = false,
662
  Group extends GroupBase<Option> = GroupBase<Option>
663
> extends Component<
664
  Props<Option, IsMulti, Group>,
665
  State<Option, IsMulti, Group>
666
> {
667
  static defaultProps = defaultProps;
5✔
668
  state: State<Option, IsMulti, Group> = {
255✔
669
    ariaSelection: null,
670
    focusedOption: null,
671
    focusedOptionId: null,
672
    focusableOptionsWithIds: [],
673
    focusedValue: null,
674
    inputIsHidden: false,
675
    isFocused: false,
676
    selectValue: [],
677
    clearFocusValueOnUpdate: false,
678
    prevWasFocused: false,
679
    inputIsHiddenAfterUpdate: undefined,
680
    prevProps: undefined,
681
    instancePrefix: '',
682
  };
683

684
  // Misc. Instance Properties
685
  // ------------------------------
686

687
  blockOptionHover = false;
255✔
688
  isComposing = false;
255✔
689
  commonProps: any; // TODO
690
  initialTouchX = 0;
255✔
691
  initialTouchY = 0;
255✔
692
  openAfterFocus = false;
255✔
693
  scrollToFocusedOptionOnUpdate = false;
255✔
694
  userIsDragging?: boolean;
695
  isAppleDevice = isAppleDevice();
255✔
696

697
  // Refs
698
  // ------------------------------
699

700
  controlRef: HTMLDivElement | null = null;
255✔
701
  getControlRef: RefCallback<HTMLDivElement> = (ref) => {
255✔
702
    this.controlRef = ref;
502✔
703
  };
704
  focusedOptionRef: HTMLDivElement | null = null;
255✔
705
  getFocusedOptionRef: RefCallback<HTMLDivElement> = (ref) => {
255✔
706
    this.focusedOptionRef = ref;
792✔
707
  };
708
  menuListRef: HTMLDivElement | null = null;
255✔
709
  getMenuListRef: RefCallback<HTMLDivElement> = (ref) => {
255✔
710
    this.menuListRef = ref;
1,536✔
711
  };
712
  inputRef: HTMLInputElement | null = null;
255✔
713
  getInputRef: RefCallback<HTMLInputElement> = (ref) => {
255✔
714
    this.inputRef = ref;
500✔
715
  };
716

717
  // Lifecycle
718
  // ------------------------------
719

720
  constructor(props: Props<Option, IsMulti, Group>) {
721
    super(props);
255✔
722
    this.state.instancePrefix =
255✔
723
      'react-select-' + (this.props.instanceId || ++instanceId);
504✔
724
    this.state.selectValue = cleanValue(props.value);
255✔
725
    // Set focusedOption if menuIsOpen is set on init (e.g. defaultMenuIsOpen)
726
    if (props.menuIsOpen && this.state.selectValue.length) {
255✔
727
      const focusableOptionsWithIds: FocusableOptionWithId<Option>[] =
728
        this.getFocusableOptionsWithIds();
21✔
729
      const focusableOptions = this.buildFocusableOptions();
21✔
730
      const optionIndex = focusableOptions.indexOf(this.state.selectValue[0]);
21✔
731
      this.state.focusableOptionsWithIds = focusableOptionsWithIds;
21✔
732
      this.state.focusedOption = focusableOptions[optionIndex];
21✔
733
      this.state.focusedOptionId = getFocusedOptionId(
21✔
734
        focusableOptionsWithIds,
735
        focusableOptions[optionIndex]
736
      );
737
    }
738
  }
739

740
  static getDerivedStateFromProps(
741
    props: Props<unknown, boolean, GroupBase<unknown>>,
742
    state: State<unknown, boolean, GroupBase<unknown>>
743
  ) {
744
    const {
745
      prevProps,
746
      clearFocusValueOnUpdate,
747
      inputIsHiddenAfterUpdate,
748
      ariaSelection,
749
      isFocused,
750
      prevWasFocused,
751
      instancePrefix,
752
    } = state;
824✔
753
    const { options, value, menuIsOpen, inputValue, isMulti } = props;
824✔
754
    const selectValue = cleanValue(value);
824✔
755
    let newMenuOptionsState = {};
824✔
756
    if (
824✔
757
      prevProps &&
2,871✔
758
      (value !== prevProps.value ||
759
        options !== prevProps.options ||
760
        menuIsOpen !== prevProps.menuIsOpen ||
761
        inputValue !== prevProps.inputValue)
762
    ) {
763
      const focusableOptions = menuIsOpen
154✔
764
        ? buildFocusableOptions(props, selectValue)
765
        : [];
766

767
      const focusableOptionsWithIds = menuIsOpen
154✔
768
        ? buildFocusableOptionsWithIds(
769
            buildCategorizedOptions(props, selectValue),
770
            `${instancePrefix}-option`
771
          )
772
        : [];
773

774
      const focusedValue = clearFocusValueOnUpdate
154✔
775
        ? getNextFocusedValue(state, selectValue)
776
        : null;
777
      const focusedOption = getNextFocusedOption(state, focusableOptions);
154✔
778
      const focusedOptionId = getFocusedOptionId(
154✔
779
        focusableOptionsWithIds,
780
        focusedOption
781
      );
782

783
      newMenuOptionsState = {
154✔
784
        selectValue,
785
        focusedOption,
786
        focusedOptionId,
787
        focusableOptionsWithIds,
788
        focusedValue,
789
        clearFocusValueOnUpdate: false,
790
      };
791
    }
792
    // some updates should toggle the state of the input visibility
793
    const newInputIsHiddenState =
794
      inputIsHiddenAfterUpdate != null && props !== prevProps
824✔
795
        ? {
796
            inputIsHidden: inputIsHiddenAfterUpdate,
797
            inputIsHiddenAfterUpdate: undefined,
798
          }
799
        : {};
800

801
    let newAriaSelection = ariaSelection;
824✔
802

803
    let hasKeptFocus = isFocused && prevWasFocused;
824✔
804

805
    if (isFocused && !hasKeptFocus) {
824✔
806
      // If `value` or `defaultValue` props are not empty then announce them
807
      // when the Select is initially focused
808
      newAriaSelection = {
58✔
809
        value: valueTernary(isMulti, selectValue, selectValue[0] || null),
108✔
810
        options: selectValue,
811
        action: 'initial-input-focus',
812
      };
813

814
      hasKeptFocus = !prevWasFocused;
58✔
815
    }
816

817
    // If the 'initial-input-focus' action has been set already
818
    // then reset the ariaSelection to null
819
    if (ariaSelection?.action === 'initial-input-focus') {
824✔
820
      newAriaSelection = null;
31✔
821
    }
822

823
    return {
824✔
824
      ...newMenuOptionsState,
825
      ...newInputIsHiddenState,
826
      prevProps: props,
827
      ariaSelection: newAriaSelection,
828
      prevWasFocused: hasKeptFocus,
829
    };
830
  }
831
  componentDidMount() {
832
    this.startListeningComposition();
255✔
833
    this.startListeningToTouch();
255✔
834

835
    if (this.props.closeMenuOnScroll && document && document.addEventListener) {
255!
836
      // Listen to all scroll events, and filter them out inside of 'onScroll'
UNCOV
837
      document.addEventListener('scroll', this.onScroll, true);
×
838
    }
839

840
    if (this.props.autoFocus) {
255✔
841
      this.focusInput();
5✔
842
    }
843

844
    // Scroll focusedOption into view if menuIsOpen is set on mount (e.g. defaultMenuIsOpen)
845
    if (
255✔
846
      this.props.menuIsOpen &&
403✔
847
      this.state.focusedOption &&
848
      this.menuListRef &&
849
      this.focusedOptionRef
850
    ) {
851
      scrollIntoView(this.menuListRef, this.focusedOptionRef);
10✔
852
    }
853
  }
854
  componentDidUpdate(prevProps: Props<Option, IsMulti, Group>) {
855
    const { isDisabled, menuIsOpen } = this.props;
569✔
856
    const { isFocused } = this.state;
569✔
857

858
    if (
569✔
859
      // ensure focus is restored correctly when the control becomes enabled
860
      (isFocused && !isDisabled && prevProps.isDisabled) ||
1,669✔
861
      // ensure focus is on the Input when the menu opens
862
      (isFocused && menuIsOpen && !prevProps.menuIsOpen)
863
    ) {
864
      this.focusInput();
27✔
865
    }
866

867
    if (isFocused && isDisabled && !prevProps.isDisabled) {
569!
868
      // ensure select state gets blurred in case Select is programmatically disabled while focused
869
      // eslint-disable-next-line react/no-did-update-set-state
UNCOV
870
      this.setState({ isFocused: false }, this.onMenuClose);
×
871
    } else if (
569!
872
      !isFocused &&
1,419!
873
      !isDisabled &&
874
      prevProps.isDisabled &&
875
      this.inputRef === document.activeElement
876
    ) {
877
      // ensure select state gets focused in case Select is programatically re-enabled while focused (Firefox)
878
      // eslint-disable-next-line react/no-did-update-set-state
UNCOV
879
      this.setState({ isFocused: true });
×
880
    }
881

882
    // scroll the focused option into view if necessary
883
    if (
569✔
884
      this.menuListRef &&
1,469✔
885
      this.focusedOptionRef &&
886
      this.scrollToFocusedOptionOnUpdate
887
    ) {
888
      scrollIntoView(this.menuListRef, this.focusedOptionRef);
328✔
889
      this.scrollToFocusedOptionOnUpdate = false;
328✔
890
    }
891
  }
892
  componentWillUnmount() {
893
    this.stopListeningComposition();
255✔
894
    this.stopListeningToTouch();
255✔
895
    document.removeEventListener('scroll', this.onScroll, true);
255✔
896
  }
897

898
  // ==============================
899
  // Consumer Handlers
900
  // ==============================
901

902
  onMenuOpen() {
903
    this.props.onMenuOpen();
36✔
904
  }
905
  onMenuClose() {
906
    this.onInputChange('', {
95✔
907
      action: 'menu-close',
908
      prevInputValue: this.props.inputValue,
909
    });
910

911
    this.props.onMenuClose();
95✔
912
  }
913
  onInputChange(newValue: string, actionMeta: InputActionMeta) {
914
    this.props.onInputChange(newValue, actionMeta);
195✔
915
  }
916

917
  // ==============================
918
  // Methods
919
  // ==============================
920

921
  focusInput() {
922
    if (!this.inputRef) return;
79!
923
    this.inputRef.focus();
79✔
924
  }
925
  blurInput() {
926
    if (!this.inputRef) return;
50!
927
    this.inputRef.blur();
50✔
928
  }
929

930
  // aliased for consumers
931
  focus = this.focusInput;
255✔
932
  blur = this.blurInput;
255✔
933

934
  openMenu(focusOption: 'first' | 'last') {
935
    const { selectValue, isFocused } = this.state;
28✔
936
    const focusableOptions = this.buildFocusableOptions();
28✔
937
    let openAtIndex = focusOption === 'first' ? 0 : focusableOptions.length - 1;
28!
938

939
    if (!this.props.isMulti) {
28✔
940
      const selectedIndex = focusableOptions.indexOf(selectValue[0]);
13✔
941
      if (selectedIndex > -1) {
13!
UNCOV
942
        openAtIndex = selectedIndex;
×
943
      }
944
    }
945

946
    // only scroll if the menu isn't already open
947
    this.scrollToFocusedOptionOnUpdate = !(isFocused && this.menuListRef);
28!
948

949
    this.setState(
28✔
950
      {
951
        inputIsHiddenAfterUpdate: false,
952
        focusedValue: null,
953
        focusedOption: focusableOptions[openAtIndex],
954
        focusedOptionId: this.getFocusedOptionId(focusableOptions[openAtIndex]),
955
      },
956
      () => this.onMenuOpen()
28✔
957
    );
958
  }
959

960
  focusValue(direction: 'previous' | 'next') {
961
    const { selectValue, focusedValue } = this.state;
2✔
962

963
    // Only multiselects support value focusing
964
    if (!this.props.isMulti) return;
2!
965

966
    this.setState({
2✔
967
      focusedOption: null,
968
    });
969

970
    let focusedIndex = selectValue.indexOf(focusedValue!);
2✔
971
    if (!focusedValue) {
2✔
972
      focusedIndex = -1;
1✔
973
    }
974

975
    const lastIndex = selectValue.length - 1;
2✔
976
    let nextFocus = -1;
2✔
977
    if (!selectValue.length) return;
2!
978

979
    switch (direction) {
2!
980
      case 'previous':
981
        if (focusedIndex === 0) {
2!
982
          // don't cycle from the start to the end
UNCOV
983
          nextFocus = 0;
×
984
        } else if (focusedIndex === -1) {
2✔
985
          // if nothing is focused, focus the last value first
986
          nextFocus = lastIndex;
1✔
987
        } else {
988
          nextFocus = focusedIndex - 1;
1✔
989
        }
990
        break;
2✔
991
      case 'next':
UNCOV
992
        if (focusedIndex > -1 && focusedIndex < lastIndex) {
×
UNCOV
993
          nextFocus = focusedIndex + 1;
×
994
        }
UNCOV
995
        break;
×
996
    }
997
    this.setState({
2✔
998
      inputIsHidden: nextFocus !== -1,
999
      focusedValue: selectValue[nextFocus],
1000
    });
1001
  }
1002

1003
  focusOption(direction: FocusDirection = 'first') {
×
1004
    const { pageSize } = this.props;
302✔
1005
    const { focusedOption } = this.state;
302✔
1006
    const options = this.getFocusableOptions();
302✔
1007

1008
    if (!options.length) return;
302!
1009
    let nextFocus = 0; // handles 'first'
302✔
1010
    let focusedIndex = options.indexOf(focusedOption!);
302✔
1011
    if (!focusedOption) {
302✔
1012
      focusedIndex = -1;
44✔
1013
    }
1014

1015
    if (direction === 'up') {
302✔
1016
      nextFocus = focusedIndex > 0 ? focusedIndex - 1 : options.length - 1;
17✔
1017
    } else if (direction === 'down') {
285✔
1018
      nextFocus = (focusedIndex + 1) % options.length;
243✔
1019
    } else if (direction === 'pageup') {
42✔
1020
      nextFocus = focusedIndex - pageSize;
8✔
1021
      if (nextFocus < 0) nextFocus = 0;
8✔
1022
    } else if (direction === 'pagedown') {
34✔
1023
      nextFocus = focusedIndex + pageSize;
16✔
1024
      if (nextFocus > options.length - 1) nextFocus = options.length - 1;
16✔
1025
    } else if (direction === 'last') {
18✔
1026
      nextFocus = options.length - 1;
10✔
1027
    }
1028
    this.scrollToFocusedOptionOnUpdate = true;
302✔
1029
    this.setState({
302✔
1030
      focusedOption: options[nextFocus],
1031
      focusedValue: null,
1032
      focusedOptionId: this.getFocusedOptionId(options[nextFocus]),
1033
    });
1034
  }
1035
  onChange = (
255✔
1036
    newValue: OnChangeValue<Option, IsMulti>,
1037
    actionMeta: ActionMeta<Option>
1038
  ) => {
1039
    const { onChange, name } = this.props;
59✔
1040
    actionMeta.name = name;
59✔
1041

1042
    this.ariaOnChange(newValue, actionMeta);
59✔
1043
    onChange(newValue, actionMeta);
59✔
1044
  };
1045
  setValue = (
255✔
1046
    newValue: OnChangeValue<Option, IsMulti>,
1047
    action: SetValueAction,
1048
    option?: Option
1049
  ) => {
1050
    const { closeMenuOnSelect, isMulti, inputValue } = this.props;
51✔
1051
    this.onInputChange('', { action: 'set-value', prevInputValue: inputValue });
51✔
1052
    if (closeMenuOnSelect) {
51✔
1053
      this.setState({
50✔
1054
        inputIsHiddenAfterUpdate: !isMulti,
1055
      });
1056
      this.onMenuClose();
50✔
1057
    }
1058
    // when the select value should change, we should reset focusedValue
1059
    this.setState({ clearFocusValueOnUpdate: true });
51✔
1060
    this.onChange(newValue, { action, option });
51✔
1061
  };
1062
  selectOption = (newValue: Option) => {
255✔
1063
    const { blurInputOnSelect, isMulti, name } = this.props;
53✔
1064
    const { selectValue } = this.state;
53✔
1065
    const deselected = isMulti && this.isOptionSelected(newValue, selectValue);
53✔
1066
    const isDisabled = this.isOptionDisabled(newValue, selectValue);
53✔
1067

1068
    if (deselected) {
53✔
1069
      const candidate = this.getOptionValue(newValue);
6✔
1070
      this.setValue(
6✔
1071
        multiValueAsValue(
1072
          selectValue.filter((i) => this.getOptionValue(i) !== candidate)
6✔
1073
        ),
1074
        'deselect-option',
1075
        newValue
1076
      );
1077
    } else if (!isDisabled) {
47✔
1078
      // Select option if option is not disabled
1079
      if (isMulti) {
45✔
1080
        this.setValue(
21✔
1081
          multiValueAsValue([...selectValue, newValue]),
1082
          'select-option',
1083
          newValue
1084
        );
1085
      } else {
1086
        this.setValue(singleValueAsValue(newValue), 'select-option');
24✔
1087
      }
1088
    } else {
1089
      this.ariaOnChange(singleValueAsValue(newValue), {
2✔
1090
        action: 'select-option',
1091
        option: newValue,
1092
        name,
1093
      });
1094
      return;
2✔
1095
    }
1096

1097
    if (blurInputOnSelect) {
51✔
1098
      this.blurInput();
50✔
1099
    }
1100
  };
1101
  removeValue = (removedValue: Option) => {
255✔
1102
    const { isMulti } = this.props;
1✔
1103
    const { selectValue } = this.state;
1✔
1104
    const candidate = this.getOptionValue(removedValue);
1✔
1105
    const newValueArray = selectValue.filter(
1✔
1106
      (i) => this.getOptionValue(i) !== candidate
3✔
1107
    );
1108
    const newValue = valueTernary(
1✔
1109
      isMulti,
1110
      newValueArray,
1111
      newValueArray[0] || null
1!
1112
    );
1113

1114
    this.onChange(newValue, { action: 'remove-value', removedValue });
1✔
1115
    this.focusInput();
1✔
1116
  };
1117
  clearValue = () => {
255✔
1118
    const { selectValue } = this.state;
5✔
1119
    this.onChange(valueTernary(this.props.isMulti, [], null), {
5✔
1120
      action: 'clear',
1121
      removedValues: selectValue,
1122
    });
1123
  };
1124
  popValue = () => {
255✔
1125
    const { isMulti } = this.props;
2✔
1126
    const { selectValue } = this.state;
2✔
1127
    const lastSelectedValue = selectValue[selectValue.length - 1];
2✔
1128
    const newValueArray = selectValue.slice(0, selectValue.length - 1);
2✔
1129
    const newValue = valueTernary(
2✔
1130
      isMulti,
1131
      newValueArray,
1132
      newValueArray[0] || null
3✔
1133
    );
1134

1135
    this.onChange(newValue, {
2✔
1136
      action: 'pop-value',
1137
      removedValue: lastSelectedValue,
1138
    });
1139
  };
1140

1141
  // ==============================
1142
  // Getters
1143
  // ==============================
1144

1145
  getTheme() {
1146
    // Use the default theme if there are no customisations.
1147
    if (!this.props.theme) {
824✔
1148
      return defaultTheme;
823✔
1149
    }
1150
    // If the theme prop is a function, assume the function
1151
    // knows how to merge the passed-in default theme with
1152
    // its own modifications.
1153
    if (typeof this.props.theme === 'function') {
1!
1154
      return this.props.theme(defaultTheme);
1✔
1155
    }
1156
    // Otherwise, if a plain theme object was passed in,
1157
    // overlay it with the default theme.
UNCOV
1158
    return {
×
1159
      ...defaultTheme,
1160
      ...this.props.theme,
1161
    };
1162
  }
1163

1164
  getFocusedOptionId = (focusedOption: Option) => {
255✔
1165
    return getFocusedOptionId(
336✔
1166
      this.state.focusableOptionsWithIds,
1167
      focusedOption
1168
    );
1169
  };
1170

1171
  getFocusableOptionsWithIds = () => {
255✔
1172
    return buildFocusableOptionsWithIds(
21✔
1173
      buildCategorizedOptions(this.props, this.state.selectValue),
1174
      this.getElementId('option')
1175
    );
1176
  };
1177

1178
  getValue = () => this.state.selectValue;
255✔
1179

1180
  cx = (...args: any) => classNames(this.props.classNamePrefix, ...args);
17,829✔
1181

1182
  getCommonProps() {
1183
    const {
1184
      clearValue,
1185
      cx,
1186
      getStyles,
1187
      getClassNames,
1188
      getValue,
1189
      selectOption,
1190
      setValue,
1191
      props,
1192
    } = this;
824✔
1193
    const { isMulti, isRtl, options } = props;
824✔
1194
    const hasValue = this.hasValue();
824✔
1195

1196
    return {
824✔
1197
      clearValue,
1198
      cx,
1199
      getStyles,
1200
      getClassNames,
1201
      getValue,
1202
      hasValue,
1203
      isMulti,
1204
      isRtl,
1205
      options,
1206
      selectOption,
1207
      selectProps: props,
1208
      setValue,
1209
      theme: this.getTheme(),
1210
    };
1211
  }
1212

1213
  getOptionLabel = (data: Option): string => {
255✔
1214
    return getOptionLabel(this.props, data);
8,872✔
1215
  };
1216
  getOptionValue = (data: Option): string => {
255✔
1217
    return getOptionValue(this.props, data);
326✔
1218
  };
1219
  getStyles = <Key extends keyof StylesProps<Option, IsMulti, Group>>(
255✔
1220
    key: Key,
1221
    props: StylesProps<Option, IsMulti, Group>[Key]
1222
  ) => {
1223
    const { unstyled } = this.props;
17,019✔
1224
    const base = defaultStyles[key](props as any, unstyled);
17,019✔
1225
    base.boxSizing = 'border-box';
17,019✔
1226
    const custom = this.props.styles[key];
17,019✔
1227
    return custom ? custom(base, props as any) : base;
17,019!
1228
  };
1229
  getClassNames = <Key extends keyof StylesProps<Option, IsMulti, Group>>(
255✔
1230
    key: Key,
1231
    props: StylesProps<Option, IsMulti, Group>[Key]
1232
  ) => this.props.classNames[key]?.(props as any);
17,019✔
1233
  getElementId = (
255✔
1234
    element:
1235
      | 'group'
1236
      | 'input'
1237
      | 'listbox'
1238
      | 'option'
1239
      | 'placeholder'
1240
      | 'live-region'
1241
  ) => {
1242
    return `${this.state.instancePrefix}-${element}`;
12,893✔
1243
  };
1244

1245
  getComponents = () => {
255✔
1246
    return defaultComponents(this.props);
6,592✔
1247
  };
1248

1249
  buildCategorizedOptions = () =>
255✔
1250
    buildCategorizedOptions(this.props, this.state.selectValue);
2,128✔
1251
  getCategorizedOptions = () =>
255✔
1252
    this.props.menuIsOpen ? this.buildCategorizedOptions() : [];
565!
1253
  buildFocusableOptions = () =>
255✔
1254
    buildFocusableOptionsFromCategorizedOptions(this.buildCategorizedOptions());
1,563✔
1255
  getFocusableOptions = () =>
255✔
1256
    this.props.menuIsOpen ? this.buildFocusableOptions() : [];
1,735✔
1257

1258
  // ==============================
1259
  // Helpers
1260
  // ==============================
1261

1262
  ariaOnChange = (
255✔
1263
    value: OnChangeValue<Option, IsMulti>,
1264
    actionMeta: ActionMeta<Option>
1265
  ) => {
1266
    this.setState({ ariaSelection: { value, ...actionMeta } });
61✔
1267
  };
1268

1269
  hasValue() {
1270
    const { selectValue } = this.state;
2,846✔
1271
    return selectValue.length > 0;
2,846✔
1272
  }
1273
  hasOptions() {
1274
    return !!this.getFocusableOptions().length;
603✔
1275
  }
1276
  isClearable(): boolean {
1277
    const { isClearable, isMulti } = this.props;
824✔
1278

1279
    // single select, by default, IS NOT clearable
1280
    // multi select, by default, IS clearable
1281
    if (isClearable === undefined) return isMulti;
824✔
1282

1283
    return isClearable;
19✔
1284
  }
1285
  isOptionDisabled(option: Option, selectValue: Options<Option>): boolean {
1286
    return isOptionDisabled(this.props, option, selectValue);
53✔
1287
  }
1288
  isOptionSelected(option: Option, selectValue: Options<Option>): boolean {
1289
    return isOptionSelected(this.props, option, selectValue);
27✔
1290
  }
1291
  filterOption(option: FilterOptionOption<Option>, inputValue: string) {
UNCOV
1292
    return filterOption(this.props, option, inputValue);
×
1293
  }
1294
  formatOptionLabel(
1295
    data: Option,
1296
    context: FormatOptionLabelContext
1297
  ): ReactNode {
1298
    if (typeof this.props.formatOptionLabel === 'function') {
8,757✔
1299
      const { inputValue } = this.props;
2✔
1300
      const { selectValue } = this.state;
2✔
1301
      return this.props.formatOptionLabel(data, {
2✔
1302
        context,
1303
        inputValue,
1304
        selectValue,
1305
      });
1306
    } else {
1307
      return this.getOptionLabel(data);
8,755✔
1308
    }
1309
  }
1310
  formatGroupLabel(data: Group) {
1311
    return this.props.formatGroupLabel(data);
30✔
1312
  }
1313

1314
  // ==============================
1315
  // Mouse Handlers
1316
  // ==============================
1317

1318
  onMenuMouseDown: MouseEventHandler<HTMLDivElement> = (event) => {
255✔
1319
    if (event.button !== 0) {
8!
UNCOV
1320
      return;
×
1321
    }
1322
    event.stopPropagation();
8✔
1323
    event.preventDefault();
8✔
1324
    this.focusInput();
8✔
1325
  };
1326
  onMenuMouseMove: MouseEventHandler<HTMLDivElement> = (event) => {
255✔
1327
    this.blockOptionHover = false;
8✔
1328
  };
1329
  onControlMouseDown = (
255✔
1330
    event: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
1331
  ) => {
1332
    // Event captured by dropdown indicator
1333
    if (event.defaultPrevented) {
42✔
1334
      return;
41✔
1335
    }
1336
    const { openMenuOnClick } = this.props;
1✔
1337
    if (!this.state.isFocused) {
1!
1338
      if (openMenuOnClick) {
1!
UNCOV
1339
        this.openAfterFocus = true;
×
1340
      }
1341
      this.focusInput();
1✔
UNCOV
1342
    } else if (!this.props.menuIsOpen) {
×
UNCOV
1343
      if (openMenuOnClick) {
×
UNCOV
1344
        this.openMenu('first');
×
1345
      }
1346
    } else {
1347
      if (
×
1348
        (event.target as HTMLElement).tagName !== 'INPUT' &&
×
1349
        (event.target as HTMLElement).tagName !== 'TEXTAREA'
1350
      ) {
1351
        this.onMenuClose();
×
1352
      }
1353
    }
1354
    if (
1!
1355
      (event.target as HTMLElement).tagName !== 'INPUT' &&
1!
1356
      (event.target as HTMLElement).tagName !== 'TEXTAREA'
1357
    ) {
UNCOV
1358
      event.preventDefault();
×
1359
    }
1360
  };
1361
  onDropdownIndicatorMouseDown = (
255✔
1362
    event: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
1363
  ) => {
1364
    // ignore mouse events that weren't triggered by the primary button
1365
    if (
37!
1366
      event &&
111✔
1367
      event.type === 'mousedown' &&
1368
      (event as React.MouseEvent<HTMLDivElement>).button !== 0
1369
    ) {
UNCOV
1370
      return;
×
1371
    }
1372
    if (this.props.isDisabled) return;
37!
1373
    const { isMulti, menuIsOpen } = this.props;
37✔
1374
    this.focusInput();
37✔
1375
    if (menuIsOpen) {
37✔
1376
      this.setState({ inputIsHiddenAfterUpdate: !isMulti });
9✔
1377
      this.onMenuClose();
9✔
1378
    } else {
1379
      this.openMenu('first');
28✔
1380
    }
1381
    event.preventDefault();
37✔
1382
  };
1383
  onClearIndicatorMouseDown = (
255✔
1384
    event: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
1385
  ) => {
1386
    // ignore mouse events that weren't triggered by the primary button
1387
    if (
3!
1388
      event &&
9✔
1389
      event.type === 'mousedown' &&
1390
      (event as React.MouseEvent<HTMLDivElement>).button !== 0
1391
    ) {
UNCOV
1392
      return;
×
1393
    }
1394
    this.clearValue();
3✔
1395
    event.preventDefault();
3✔
1396
    this.openAfterFocus = false;
3✔
1397
    if (event.type === 'touchend') {
3!
UNCOV
1398
      this.focusInput();
×
1399
    } else {
1400
      setTimeout(() => this.focusInput());
3✔
1401
    }
1402
  };
1403
  onScroll = (event: Event) => {
255✔
UNCOV
1404
    if (typeof this.props.closeMenuOnScroll === 'boolean') {
×
UNCOV
1405
      if (
×
1406
        event.target instanceof HTMLElement &&
×
1407
        isDocumentElement(event.target)
1408
      ) {
UNCOV
1409
        this.props.onMenuClose();
×
1410
      }
UNCOV
1411
    } else if (typeof this.props.closeMenuOnScroll === 'function') {
×
1412
      if (this.props.closeMenuOnScroll(event)) {
×
1413
        this.props.onMenuClose();
×
1414
      }
1415
    }
1416
  };
1417

1418
  // ==============================
1419
  // Composition Handlers
1420
  // ==============================
1421

1422
  startListeningComposition() {
1423
    if (document && document.addEventListener) {
255!
1424
      document.addEventListener(
255✔
1425
        'compositionstart',
1426
        this.onCompositionStart,
1427
        false
1428
      );
1429
      document.addEventListener('compositionend', this.onCompositionEnd, false);
255✔
1430
    }
1431
  }
1432
  stopListeningComposition() {
1433
    if (document && document.removeEventListener) {
255!
1434
      document.removeEventListener('compositionstart', this.onCompositionStart);
255✔
1435
      document.removeEventListener('compositionend', this.onCompositionEnd);
255✔
1436
    }
1437
  }
1438
  onCompositionStart = () => {
255✔
UNCOV
1439
    this.isComposing = true;
×
1440
  };
1441
  onCompositionEnd = () => {
255✔
UNCOV
1442
    this.isComposing = false;
×
1443
  };
1444

1445
  // ==============================
1446
  // Touch Handlers
1447
  // ==============================
1448

1449
  startListeningToTouch() {
1450
    if (document && document.addEventListener) {
255!
1451
      document.addEventListener('touchstart', this.onTouchStart, false);
255✔
1452
      document.addEventListener('touchmove', this.onTouchMove, false);
255✔
1453
      document.addEventListener('touchend', this.onTouchEnd, false);
255✔
1454
    }
1455
  }
1456
  stopListeningToTouch() {
1457
    if (document && document.removeEventListener) {
255!
1458
      document.removeEventListener('touchstart', this.onTouchStart);
255✔
1459
      document.removeEventListener('touchmove', this.onTouchMove);
255✔
1460
      document.removeEventListener('touchend', this.onTouchEnd);
255✔
1461
    }
1462
  }
1463
  onTouchStart = ({ touches }: TouchEvent) => {
255✔
UNCOV
1464
    const touch = touches && touches.item(0);
×
UNCOV
1465
    if (!touch) {
×
UNCOV
1466
      return;
×
1467
    }
1468

UNCOV
1469
    this.initialTouchX = touch.clientX;
×
UNCOV
1470
    this.initialTouchY = touch.clientY;
×
UNCOV
1471
    this.userIsDragging = false;
×
1472
  };
1473
  onTouchMove = ({ touches }: TouchEvent) => {
255✔
1474
    const touch = touches && touches.item(0);
×
UNCOV
1475
    if (!touch) {
×
UNCOV
1476
      return;
×
1477
    }
1478

1479
    const deltaX = Math.abs(touch.clientX - this.initialTouchX);
×
UNCOV
1480
    const deltaY = Math.abs(touch.clientY - this.initialTouchY);
×
UNCOV
1481
    const moveThreshold = 5;
×
1482

1483
    this.userIsDragging = deltaX > moveThreshold || deltaY > moveThreshold;
×
1484
  };
1485
  onTouchEnd = (event: TouchEvent) => {
255✔
UNCOV
1486
    if (this.userIsDragging) return;
×
1487

1488
    // close the menu if the user taps outside
1489
    // we're checking on event.target here instead of event.currentTarget, because we want to assert information
1490
    // on events on child elements, not the document (which we've attached this handler to).
1491
    if (
×
1492
      this.controlRef &&
×
1493
      !this.controlRef.contains(event.target as Node) &&
1494
      this.menuListRef &&
1495
      !this.menuListRef.contains(event.target as Node)
1496
    ) {
UNCOV
1497
      this.blurInput();
×
1498
    }
1499

1500
    // reset move vars
UNCOV
1501
    this.initialTouchX = 0;
×
UNCOV
1502
    this.initialTouchY = 0;
×
1503
  };
1504
  onControlTouchEnd: TouchEventHandler<HTMLDivElement> = (event) => {
255✔
1505
    if (this.userIsDragging) return;
×
UNCOV
1506
    this.onControlMouseDown(event);
×
1507
  };
1508
  onClearIndicatorTouchEnd: TouchEventHandler<HTMLDivElement> = (event) => {
255✔
1509
    if (this.userIsDragging) return;
×
1510

UNCOV
1511
    this.onClearIndicatorMouseDown(event);
×
1512
  };
1513
  onDropdownIndicatorTouchEnd: TouchEventHandler<HTMLDivElement> = (event) => {
255✔
1514
    if (this.userIsDragging) return;
×
1515

UNCOV
1516
    this.onDropdownIndicatorMouseDown(event);
×
1517
  };
1518

1519
  // ==============================
1520
  // Focus Handlers
1521
  // ==============================
1522

1523
  handleInputChange: FormEventHandler<HTMLInputElement> = (event) => {
255✔
1524
    const { inputValue: prevInputValue } = this.props;
13✔
1525
    const inputValue = event.currentTarget.value;
13✔
1526
    this.setState({ inputIsHiddenAfterUpdate: false });
13✔
1527
    this.onInputChange(inputValue, { action: 'input-change', prevInputValue });
13✔
1528
    if (!this.props.menuIsOpen) {
13✔
1529
      this.onMenuOpen();
8✔
1530
    }
1531
  };
1532
  onInputFocus: FocusEventHandler<HTMLInputElement> = (event) => {
255✔
1533
    if (this.props.onFocus) {
60✔
1534
      this.props.onFocus(event);
4✔
1535
    }
1536
    this.setState({
60✔
1537
      inputIsHiddenAfterUpdate: false,
1538
      isFocused: true,
1539
    });
1540
    if (this.openAfterFocus || this.props.openMenuOnFocus) {
60!
UNCOV
1541
      this.openMenu('first');
×
1542
    }
1543
    this.openAfterFocus = false;
60✔
1544
  };
1545
  onInputBlur: FocusEventHandler<HTMLInputElement> = (event) => {
255✔
1546
    const { inputValue: prevInputValue } = this.props;
31✔
1547
    if (this.menuListRef && this.menuListRef.contains(document.activeElement)) {
31!
UNCOV
1548
      this.inputRef!.focus();
×
1549
      return;
×
1550
    }
1551
    if (this.props.onBlur) {
31✔
1552
      this.props.onBlur(event);
4✔
1553
    }
1554
    this.onInputChange('', { action: 'input-blur', prevInputValue });
31✔
1555
    this.onMenuClose();
31✔
1556
    this.setState({
31✔
1557
      focusedValue: null,
1558
      isFocused: false,
1559
    });
1560
  };
1561
  onOptionHover = (focusedOption: Option) => {
255✔
1562
    if (this.blockOptionHover || this.state.focusedOption === focusedOption) {
12✔
1563
      return;
6✔
1564
    }
1565
    const options = this.getFocusableOptions();
6✔
1566
    const focusedOptionIndex = options.indexOf(focusedOption!);
6✔
1567
    this.setState({
6✔
1568
      focusedOption,
1569
      focusedOptionId:
1570
        focusedOptionIndex > -1 ? this.getFocusedOptionId(focusedOption) : null,
6!
1571
    });
1572
  };
1573
  shouldHideSelectedOptions = () => {
255✔
UNCOV
1574
    return shouldHideSelectedOptions(this.props);
×
1575
  };
1576

1577
  // If the hidden input gets focus through form submit,
1578
  // redirect focus to focusable input.
1579
  onValueInputFocus: FocusEventHandler = (e) => {
255✔
UNCOV
1580
    e.preventDefault();
×
UNCOV
1581
    e.stopPropagation();
×
1582

UNCOV
1583
    this.focus();
×
1584
  };
1585

1586
  // ==============================
1587
  // Keyboard Handlers
1588
  // ==============================
1589

1590
  onKeyDown: KeyboardEventHandler<HTMLDivElement> = (event) => {
255✔
1591
    const {
1592
      isMulti,
1593
      backspaceRemovesValue,
1594
      escapeClearsValue,
1595
      inputValue,
1596
      isClearable,
1597
      isDisabled,
1598
      menuIsOpen,
1599
      onKeyDown,
1600
      tabSelectsValue,
1601
      openMenuOnFocus,
1602
    } = this.props;
367✔
1603
    const { focusedOption, focusedValue, selectValue } = this.state;
367✔
1604

1605
    if (isDisabled) return;
367!
1606

1607
    if (typeof onKeyDown === 'function') {
367!
UNCOV
1608
      onKeyDown(event);
×
UNCOV
1609
      if (event.defaultPrevented) {
×
UNCOV
1610
        return;
×
1611
      }
1612
    }
1613

1614
    // Block option hover events when the user has just pressed a key
1615
    this.blockOptionHover = true;
367✔
1616
    switch (event.key) {
367!
1617
      case 'ArrowLeft':
1618
        if (!isMulti || inputValue) return;
2!
1619
        this.focusValue('previous');
2✔
1620
        break;
2✔
1621
      case 'ArrowRight':
UNCOV
1622
        if (!isMulti || inputValue) return;
×
UNCOV
1623
        this.focusValue('next');
×
UNCOV
1624
        break;
×
1625
      case 'Delete':
1626
      case 'Backspace':
1627
        if (inputValue) return;
5!
1628
        if (focusedValue) {
5!
UNCOV
1629
          this.removeValue(focusedValue);
×
1630
        } else {
1631
          if (!backspaceRemovesValue) return;
5✔
1632
          if (isMulti) {
4✔
1633
            this.popValue();
2✔
1634
          } else if (isClearable) {
2✔
1635
            this.clearValue();
1✔
1636
          }
1637
        }
1638
        break;
4✔
1639
      case 'Tab':
1640
        if (this.isComposing) return;
4!
1641

1642
        if (
4✔
1643
          event.shiftKey ||
18!
1644
          !menuIsOpen ||
1645
          !tabSelectsValue ||
1646
          !focusedOption ||
1647
          // don't capture the event if the menu opens on focus and the focused
1648
          // option is already selected; it breaks the flow of navigation
1649
          (openMenuOnFocus && this.isOptionSelected(focusedOption, selectValue))
1650
        ) {
1651
          return;
1✔
1652
        }
1653
        this.selectOption(focusedOption);
3✔
1654
        break;
3✔
1655
      case 'Enter':
1656
        if (event.keyCode === 229) {
33✔
1657
          // ignore the keydown event from an Input Method Editor(IME)
1658
          // ref. https://www.w3.org/TR/uievents/#determine-keydown-keyup-keyCode
1659
          break;
1✔
1660
        }
1661
        if (menuIsOpen) {
32✔
1662
          if (!focusedOption) return;
31✔
1663
          if (this.isComposing) return;
29!
1664
          this.selectOption(focusedOption);
29✔
1665
          break;
29✔
1666
        }
1667
        return;
1✔
1668
      case 'Escape':
1669
        if (menuIsOpen) {
9✔
1670
          this.setState({
5✔
1671
            inputIsHiddenAfterUpdate: false,
1672
          });
1673
          this.onInputChange('', {
5✔
1674
            action: 'menu-close',
1675
            prevInputValue: inputValue,
1676
          });
1677
          this.onMenuClose();
5✔
1678
        } else if (isClearable && escapeClearsValue) {
4✔
1679
          this.clearValue();
1✔
1680
        }
1681
        break;
9✔
1682
      case ' ': // space
1683
        if (inputValue) {
4!
UNCOV
1684
          return;
×
1685
        }
1686
        if (!menuIsOpen) {
4!
UNCOV
1687
          this.openMenu('first');
×
UNCOV
1688
          break;
×
1689
        }
1690
        if (!focusedOption) return;
4!
1691
        this.selectOption(focusedOption);
4✔
1692
        break;
4✔
1693
      case 'ArrowUp':
1694
        if (menuIsOpen) {
17!
1695
          this.focusOption('up');
17✔
1696
        } else {
UNCOV
1697
          this.openMenu('last');
×
1698
        }
1699
        break;
17✔
1700
      case 'ArrowDown':
1701
        if (menuIsOpen) {
243!
1702
          this.focusOption('down');
243✔
1703
        } else {
UNCOV
1704
          this.openMenu('first');
×
1705
        }
1706
        break;
243✔
1707
      case 'PageUp':
1708
        if (!menuIsOpen) return;
8!
1709
        this.focusOption('pageup');
8✔
1710
        break;
8✔
1711
      case 'PageDown':
1712
        if (!menuIsOpen) return;
16!
1713
        this.focusOption('pagedown');
16✔
1714
        break;
16✔
1715
      case 'Home':
1716
        if (!menuIsOpen) return;
8!
1717
        this.focusOption('first');
8✔
1718
        break;
8✔
1719
      case 'End':
1720
        if (!menuIsOpen) return;
10!
1721
        this.focusOption('last');
10✔
1722
        break;
10✔
1723
      default:
1724
        return;
8✔
1725
    }
1726
    event.preventDefault();
354✔
1727
  };
1728

1729
  // ==============================
1730
  // Renderers
1731
  // ==============================
1732
  renderInput() {
1733
    const {
1734
      isDisabled,
1735
      isSearchable,
1736
      inputId,
1737
      inputValue,
1738
      tabIndex,
1739
      form,
1740
      menuIsOpen,
1741
      required,
1742
    } = this.props;
824✔
1743
    const { Input } = this.getComponents();
824✔
1744
    const { inputIsHidden, ariaSelection } = this.state;
824✔
1745
    const { commonProps } = this;
824✔
1746

1747
    const id = inputId || this.getElementId('input');
824✔
1748

1749
    // aria attributes makes the JSX "noisy", separated for clarity
1750
    const ariaAttributes = {
824✔
1751
      'aria-autocomplete': 'list' as const,
1752
      'aria-expanded': menuIsOpen,
1753
      'aria-haspopup': true,
1754
      'aria-errormessage': this.props['aria-errormessage'],
1755
      'aria-invalid': this.props['aria-invalid'],
1756
      'aria-label': this.props['aria-label'],
1757
      'aria-labelledby': this.props['aria-labelledby'],
1758
      'aria-required': required,
1759
      role: 'combobox',
1760
      'aria-activedescendant': this.isAppleDevice
824!
1761
        ? undefined
1762
        : this.state.focusedOptionId || '',
1,474✔
1763

1764
      ...(menuIsOpen && {
1,427✔
1765
        'aria-controls': this.getElementId('listbox'),
1766
      }),
1767
      ...(!isSearchable && {
833✔
1768
        'aria-readonly': true,
1769
      }),
1770
      ...(this.hasValue()
824✔
1771
        ? ariaSelection?.action === 'initial-input-focus' && {
180✔
1772
            'aria-describedby': this.getElementId('live-region'),
1773
          }
1774
        : {
1775
            'aria-describedby': this.getElementId('placeholder'),
1776
          }),
1777
    };
1778

1779
    if (!isSearchable) {
824✔
1780
      // use a dummy input to maintain focus/blur functionality
1781
      return (
9✔
1782
        <DummyInput
1783
          id={id}
1784
          innerRef={this.getInputRef}
1785
          onBlur={this.onInputBlur}
1786
          onChange={noop}
1787
          onFocus={this.onInputFocus}
1788
          disabled={isDisabled}
1789
          tabIndex={tabIndex}
1790
          inputMode="none"
1791
          form={form}
1792
          value=""
1793
          {...ariaAttributes}
1794
        />
1795
      );
1796
    }
1797

1798
    return (
815✔
1799
      <Input
1800
        {...commonProps}
1801
        autoCapitalize="none"
1802
        autoComplete="off"
1803
        autoCorrect="off"
1804
        id={id}
1805
        innerRef={this.getInputRef}
1806
        isDisabled={isDisabled}
1807
        isHidden={inputIsHidden}
1808
        onBlur={this.onInputBlur}
1809
        onChange={this.handleInputChange}
1810
        onFocus={this.onInputFocus}
1811
        spellCheck="false"
1812
        tabIndex={tabIndex}
1813
        form={form}
1814
        type="text"
1815
        value={inputValue}
1816
        {...ariaAttributes}
1817
      />
1818
    );
1819
  }
1820
  renderPlaceholderOrValue() {
1821
    const {
1822
      MultiValue,
1823
      MultiValueContainer,
1824
      MultiValueLabel,
1825
      MultiValueRemove,
1826
      SingleValue,
1827
      Placeholder,
1828
    } = this.getComponents();
824✔
1829
    const { commonProps } = this;
824✔
1830
    const {
1831
      controlShouldRenderValue,
1832
      isDisabled,
1833
      isMulti,
1834
      inputValue,
1835
      placeholder,
1836
    } = this.props;
824✔
1837
    const { selectValue, focusedValue, isFocused } = this.state;
824✔
1838

1839
    if (!this.hasValue() || !controlShouldRenderValue) {
824✔
1840
      return inputValue ? null : (
652✔
1841
        <Placeholder
1842
          {...commonProps}
1843
          key="placeholder"
1844
          isDisabled={isDisabled}
1845
          isFocused={isFocused}
1846
          innerProps={{ id: this.getElementId('placeholder') }}
1847
        >
1848
          {placeholder}
1849
        </Placeholder>
1850
      );
1851
    }
1852

1853
    if (isMulti) {
172✔
1854
      return selectValue.map((opt, index) => {
96✔
1855
        const isOptionFocused = opt === focusedValue;
117✔
1856
        const key = `${this.getOptionLabel(opt)}-${this.getOptionValue(opt)}`;
117✔
1857

1858
        return (
117✔
1859
          <MultiValue
1860
            {...commonProps}
1861
            components={{
1862
              Container: MultiValueContainer,
1863
              Label: MultiValueLabel,
1864
              Remove: MultiValueRemove,
1865
            }}
1866
            isFocused={isOptionFocused}
1867
            isDisabled={isDisabled}
1868
            key={key}
1869
            index={index}
1870
            removeProps={{
1871
              onClick: () => this.removeValue(opt),
1✔
UNCOV
1872
              onTouchEnd: () => this.removeValue(opt),
×
1873
              onMouseDown: (e) => {
1874
                e.preventDefault();
1✔
1875
              },
1876
            }}
1877
            data={opt}
1878
          >
1879
            {this.formatOptionLabel(opt, 'value')}
1880
          </MultiValue>
1881
        );
1882
      });
1883
    }
1884

1885
    if (inputValue) {
76✔
1886
      return null;
10✔
1887
    }
1888

1889
    const singleValue = selectValue[0];
66✔
1890
    return (
66✔
1891
      <SingleValue {...commonProps} data={singleValue} isDisabled={isDisabled}>
1892
        {this.formatOptionLabel(singleValue, 'value')}
1893
      </SingleValue>
1894
    );
1895
  }
1896
  renderClearIndicator() {
1897
    const { ClearIndicator } = this.getComponents();
824✔
1898
    const { commonProps } = this;
824✔
1899
    const { isDisabled, isLoading } = this.props;
824✔
1900
    const { isFocused } = this.state;
824✔
1901

1902
    if (
824✔
1903
      !this.isClearable() ||
2,023✔
1904
      !ClearIndicator ||
1905
      isDisabled ||
1906
      !this.hasValue() ||
1907
      isLoading
1908
    ) {
1909
      return null;
719✔
1910
    }
1911

1912
    const innerProps = {
105✔
1913
      onMouseDown: this.onClearIndicatorMouseDown,
1914
      onTouchEnd: this.onClearIndicatorTouchEnd,
1915
      'aria-hidden': 'true',
1916
    };
1917

1918
    return (
105✔
1919
      <ClearIndicator
1920
        {...commonProps}
1921
        innerProps={innerProps}
1922
        isFocused={isFocused}
1923
      />
1924
    );
1925
  }
1926
  renderLoadingIndicator() {
1927
    const { LoadingIndicator } = this.getComponents();
824✔
1928
    const { commonProps } = this;
824✔
1929
    const { isDisabled, isLoading } = this.props;
824✔
1930
    const { isFocused } = this.state;
824✔
1931

1932
    if (!LoadingIndicator || !isLoading) return null;
824✔
1933

1934
    const innerProps = { 'aria-hidden': 'true' };
10✔
1935
    return (
10✔
1936
      <LoadingIndicator
1937
        {...commonProps}
1938
        innerProps={innerProps}
1939
        isDisabled={isDisabled}
1940
        isFocused={isFocused}
1941
      />
1942
    );
1943
  }
1944
  renderIndicatorSeparator() {
1945
    const { DropdownIndicator, IndicatorSeparator } = this.getComponents();
824✔
1946

1947
    // separator doesn't make sense without the dropdown indicator
1948
    if (!DropdownIndicator || !IndicatorSeparator) return null;
824!
1949

1950
    const { commonProps } = this;
824✔
1951
    const { isDisabled } = this.props;
824✔
1952
    const { isFocused } = this.state;
824✔
1953

1954
    return (
824✔
1955
      <IndicatorSeparator
1956
        {...commonProps}
1957
        isDisabled={isDisabled}
1958
        isFocused={isFocused}
1959
      />
1960
    );
1961
  }
1962
  renderDropdownIndicator() {
1963
    const { DropdownIndicator } = this.getComponents();
824✔
1964
    if (!DropdownIndicator) return null;
824!
1965
    const { commonProps } = this;
824✔
1966
    const { isDisabled } = this.props;
824✔
1967
    const { isFocused } = this.state;
824✔
1968

1969
    const innerProps = {
824✔
1970
      onMouseDown: this.onDropdownIndicatorMouseDown,
1971
      onTouchEnd: this.onDropdownIndicatorTouchEnd,
1972
      'aria-hidden': 'true',
1973
    };
1974

1975
    return (
824✔
1976
      <DropdownIndicator
1977
        {...commonProps}
1978
        innerProps={innerProps}
1979
        isDisabled={isDisabled}
1980
        isFocused={isFocused}
1981
      />
1982
    );
1983
  }
1984
  renderMenu() {
1985
    const {
1986
      Group,
1987
      GroupHeading,
1988
      Menu,
1989
      MenuList,
1990
      MenuPortal,
1991
      LoadingMessage,
1992
      NoOptionsMessage,
1993
      Option,
1994
    } = this.getComponents();
824✔
1995
    const { commonProps } = this;
824✔
1996
    const { focusedOption } = this.state;
824✔
1997
    const {
1998
      captureMenuScroll,
1999
      inputValue,
2000
      isLoading,
2001
      loadingMessage,
2002
      minMenuHeight,
2003
      maxMenuHeight,
2004
      menuIsOpen,
2005
      menuPlacement,
2006
      menuPosition,
2007
      menuPortalTarget,
2008
      menuShouldBlockScroll,
2009
      menuShouldScrollIntoView,
2010
      noOptionsMessage,
2011
      onMenuScrollToTop,
2012
      onMenuScrollToBottom,
2013
    } = this.props;
824✔
2014

2015
    if (!menuIsOpen) return null;
824✔
2016

2017
    // TODO: Internal Option Type here
2018
    const render = (props: CategorizedOption<Option>, id: string) => {
603✔
2019
      const { type, data, isDisabled, isSelected, label, value } = props;
8,574✔
2020
      const isFocused = focusedOption === data;
8,574✔
2021
      const onHover = isDisabled ? undefined : () => this.onOptionHover(data);
8,574✔
2022
      const onSelect = isDisabled ? undefined : () => this.selectOption(data);
8,574✔
2023
      const optionId = `${this.getElementId('option')}-${id}`;
8,574✔
2024
      const innerProps = {
8,574✔
2025
        id: optionId,
2026
        onClick: onSelect,
2027
        onMouseMove: onHover,
2028
        onMouseOver: onHover,
2029
        tabIndex: -1,
2030
        role: 'option',
2031
        'aria-selected': this.isAppleDevice ? undefined : isSelected, // is not supported on Apple devices
8,574!
2032
      };
2033

2034
      return (
8,574✔
2035
        <Option
2036
          {...commonProps}
2037
          innerProps={innerProps}
2038
          data={data}
2039
          isDisabled={isDisabled}
2040
          isSelected={isSelected}
2041
          key={optionId}
2042
          label={label}
2043
          type={type}
2044
          value={value}
2045
          isFocused={isFocused}
2046
          innerRef={isFocused ? this.getFocusedOptionRef : undefined}
8,574✔
2047
        >
2048
          {this.formatOptionLabel(props.data, 'menu')}
2049
        </Option>
2050
      );
2051
    };
2052

2053
    const renderGroup = (item: CategorizedGroup<Option, Group>) => {
603✔
2054
      const { data, options, index: groupIndex } = item;
30✔
2055
      const groupId = `${this.getElementId('group')}-${groupIndex}`;
30✔
2056
      const headingId = `${groupId}-heading`;
30✔
2057

2058
      return (
30✔
2059
        <Group
2060
          {...commonProps}
2061
          key={groupId}
2062
          data={data}
2063
          options={options}
2064
          Heading={GroupHeading}
2065
          headingProps={{
2066
            id: headingId,
2067
            data: item.data,
2068
          }}
2069
          label={this.formatGroupLabel(item.data)}
2070
        >
2071
          {item.options.map((option) => {
2072
            if (option.type === 'group') {
165!
NEW
2073
              return renderGroup(option);
×
2074
            } else if (option.type === 'option') {
165!
2075
              return render(option, `${groupIndex}-${option.index}`);
165✔
2076
            }
2077
          })}
2078
        </Group>
2079
      );
2080
    };
2081

2082
    let menuUI: ReactNode;
2083

2084
    if (this.hasOptions()) {
603✔
2085
      menuUI = this.getCategorizedOptions().map((item) => {
565✔
2086
        if (item.type === 'group') {
8,439✔
2087
          return renderGroup(item);
30✔
2088
        } else if (item.type === 'option') {
8,409!
2089
          return render(item, `${item.index}`);
8,409✔
2090
        }
2091
      });
2092
    } else if (isLoading) {
38✔
2093
      const message = loadingMessage({ inputValue });
8✔
2094
      if (message === null) return null;
8!
2095
      menuUI = <LoadingMessage {...commonProps}>{message}</LoadingMessage>;
8✔
2096
    } else {
2097
      const message = noOptionsMessage({ inputValue });
30✔
2098
      if (message === null) return null;
30!
2099
      menuUI = <NoOptionsMessage {...commonProps}>{message}</NoOptionsMessage>;
30✔
2100
    }
2101
    const menuPlacementProps = {
603✔
2102
      minMenuHeight,
2103
      maxMenuHeight,
2104
      menuPlacement,
2105
      menuPosition,
2106
      menuShouldScrollIntoView,
2107
    };
2108

2109
    const menuElement = (
2110
      <MenuPlacer {...commonProps} {...menuPlacementProps}>
603✔
2111
        {({ ref, placerProps: { placement, maxHeight } }) => (
2112
          <Menu
769✔
2113
            {...commonProps}
2114
            {...menuPlacementProps}
2115
            innerRef={ref}
2116
            innerProps={{
2117
              onMouseDown: this.onMenuMouseDown,
2118
              onMouseMove: this.onMenuMouseMove,
2119
            }}
2120
            isLoading={isLoading}
2121
            placement={placement}
2122
          >
2123
            <ScrollManager
2124
              captureEnabled={captureMenuScroll}
2125
              onTopArrive={onMenuScrollToTop}
2126
              onBottomArrive={onMenuScrollToBottom}
2127
              lockEnabled={menuShouldBlockScroll}
2128
            >
2129
              {(scrollTargetRef) => (
2130
                <MenuList
768✔
2131
                  {...commonProps}
2132
                  innerRef={(instance) => {
2133
                    this.getMenuListRef(instance);
1,536✔
2134
                    scrollTargetRef(instance);
1,536✔
2135
                  }}
2136
                  innerProps={{
2137
                    role: 'listbox',
2138
                    'aria-multiselectable': commonProps.isMulti,
2139
                    id: this.getElementId('listbox'),
2140
                  }}
2141
                  isLoading={isLoading}
2142
                  maxHeight={maxHeight}
2143
                  focusedOption={focusedOption}
2144
                >
2145
                  {menuUI}
2146
                </MenuList>
2147
              )}
2148
            </ScrollManager>
2149
          </Menu>
2150
        )}
2151
      </MenuPlacer>
2152
    );
2153

2154
    // positioning behaviour is almost identical for portalled and fixed,
2155
    // so we use the same component. the actual portalling logic is forked
2156
    // within the component based on `menuPosition`
2157
    return menuPortalTarget || menuPosition === 'fixed' ? (
603!
2158
      <MenuPortal
2159
        {...commonProps}
2160
        appendTo={menuPortalTarget}
2161
        controlElement={this.controlRef}
2162
        menuPlacement={menuPlacement}
2163
        menuPosition={menuPosition}
2164
      >
2165
        {menuElement}
2166
      </MenuPortal>
2167
    ) : (
2168
      menuElement
2169
    );
2170
  }
2171
  renderFormField() {
2172
    const { delimiter, isDisabled, isMulti, name, required } = this.props;
824✔
2173
    const { selectValue } = this.state;
824✔
2174

2175
    if (required && !this.hasValue() && !isDisabled) {
824✔
2176
      return <RequiredInput name={name} onFocus={this.onValueInputFocus} />;
6✔
2177
    }
2178

2179
    if (!name || isDisabled) return;
818✔
2180

2181
    if (isMulti) {
755✔
2182
      if (delimiter) {
346✔
2183
        const value = selectValue
15✔
2184
          .map((opt) => this.getOptionValue(opt))
15✔
2185
          .join(delimiter);
2186
        return <input name={name} type="hidden" value={value} />;
15✔
2187
      } else {
2188
        const input =
2189
          selectValue.length > 0 ? (
331✔
2190
            selectValue.map((opt, i) => (
2191
              <input
102✔
2192
                key={`i-${i}`}
2193
                name={name}
2194
                type="hidden"
2195
                value={this.getOptionValue(opt)}
2196
              />
2197
            ))
2198
          ) : (
2199
            <input name={name} type="hidden" value="" />
2200
          );
2201

2202
        return <div>{input}</div>;
331✔
2203
      }
2204
    } else {
2205
      const value = selectValue[0] ? this.getOptionValue(selectValue[0]) : '';
409✔
2206
      return <input name={name} type="hidden" value={value} />;
409✔
2207
    }
2208
  }
2209

2210
  renderLiveRegion() {
2211
    const { commonProps } = this;
824✔
2212
    const {
2213
      ariaSelection,
2214
      focusedOption,
2215
      focusedValue,
2216
      isFocused,
2217
      selectValue,
2218
    } = this.state;
824✔
2219

2220
    const focusableOptions = this.getFocusableOptions();
824✔
2221

2222
    return (
824✔
2223
      <LiveRegion
2224
        {...commonProps}
2225
        id={this.getElementId('live-region')}
2226
        ariaSelection={ariaSelection}
2227
        focusedOption={focusedOption}
2228
        focusedValue={focusedValue}
2229
        isFocused={isFocused}
2230
        selectValue={selectValue}
2231
        focusableOptions={focusableOptions}
2232
        isAppleDevice={this.isAppleDevice}
2233
      />
2234
    );
2235
  }
2236

2237
  render() {
2238
    const { Control, IndicatorsContainer, SelectContainer, ValueContainer } =
2239
      this.getComponents();
824✔
2240

2241
    const { className, id, isDisabled, menuIsOpen } = this.props;
824✔
2242
    const { isFocused } = this.state;
824✔
2243
    const commonProps = (this.commonProps = this.getCommonProps());
824✔
2244

2245
    return (
824✔
2246
      <SelectContainer
2247
        {...commonProps}
2248
        className={className}
2249
        innerProps={{
2250
          id: id,
2251
          onKeyDown: this.onKeyDown,
2252
        }}
2253
        isDisabled={isDisabled}
2254
        isFocused={isFocused}
2255
      >
2256
        {this.renderLiveRegion()}
2257
        <Control
2258
          {...commonProps}
2259
          innerRef={this.getControlRef}
2260
          innerProps={{
2261
            onMouseDown: this.onControlMouseDown,
2262
            onTouchEnd: this.onControlTouchEnd,
2263
          }}
2264
          isDisabled={isDisabled}
2265
          isFocused={isFocused}
2266
          menuIsOpen={menuIsOpen}
2267
        >
2268
          <ValueContainer {...commonProps} isDisabled={isDisabled}>
2269
            {this.renderPlaceholderOrValue()}
2270
            {this.renderInput()}
2271
          </ValueContainer>
2272
          <IndicatorsContainer {...commonProps} isDisabled={isDisabled}>
2273
            {this.renderClearIndicator()}
2274
            {this.renderLoadingIndicator()}
2275
            {this.renderIndicatorSeparator()}
2276
            {this.renderDropdownIndicator()}
2277
          </IndicatorsContainer>
2278
        </Control>
2279
        {this.renderMenu()}
2280
        {this.renderFormField()}
2281
      </SelectContainer>
2282
    );
2283
  }
2284
}
2285

2286
export type PublicBaseSelectProps<
2287
  Option,
2288
  IsMulti extends boolean,
2289
  Group extends GroupBase<Option>
2290
> = JSX.LibraryManagedAttributes<typeof Select, Props<Option, IsMulti, Group>>;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc