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

JedWatson / react-select / 9de42ca4-8c55-4031-8310-e7e0a2b0128b

26 Oct 2024 11:27AM CUT coverage: 75.844%. Remained the same
9de42ca4-8c55-4031-8310-e7e0a2b0128b

Pull #5880

circleci

lukebennett88
add box-sizing to border-box for RequiredInput

adding `required` would otherwise cause an extra (unstylable) component to be added which has some implicit padding from the user agent style sheet (inputs have padding) which could cause horizontal scrolling when the whole scroll field is 100% wide.
Pull Request #5880: add box-sizing to border-box for RequiredInput

658 of 1052 branches covered (62.55%)

1033 of 1362 relevant lines covered (75.84%)

1934.69 hits per line

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

87.62
/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 CategorizedOption<Option>[];
363
  index: number;
364
}
365

366
type CategorizedGroupOrOption<Option, Group extends GroupBase<Option>> =
367
  | CategorizedGroup<Option, Group>
368
  | CategorizedOption<Option>;
369

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

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

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

434
function buildFocusableOptionsFromCategorizedOptions<
435
  Option,
436
  Group extends GroupBase<Option>
437
>(categorizedOptions: readonly CategorizedGroupOrOption<Option, Group>[]) {
438
  return categorizedOptions.reduce<Option[]>(
1,675✔
439
    (optionsAccumulator, categorizedOption) => {
440
      if (categorizedOption.type === 'group') {
23,521✔
441
        optionsAccumulator.push(
88✔
442
          ...categorizedOption.options.map((option) => option.data)
492✔
443
        );
444
      } else {
445
        optionsAccumulator.push(categorizedOption.data);
23,433✔
446
      }
447
      return optionsAccumulator;
23,521✔
448
    },
449
    []
450
  );
451
}
452

453
function buildFocusableOptionsWithIds<Option, Group extends GroupBase<Option>>(
454
  categorizedOptions: readonly CategorizedGroupOrOption<Option, Group>[],
455
  optionId: string
456
) {
457
  return categorizedOptions.reduce<FocusableOptionWithId<Option>[]>(
133✔
458
    (optionsAccumulator, categorizedOption) => {
459
      if (categorizedOption.type === 'group') {
1,052✔
460
        optionsAccumulator.push(
12✔
461
          ...categorizedOption.options.map((option) => ({
58✔
462
            data: option.data,
463
            id: `${optionId}-${categorizedOption.index}-${option.index}`,
464
          }))
465
        );
466
      } else {
467
        optionsAccumulator.push({
1,040✔
468
          data: categorizedOption.data,
469
          id: `${optionId}-${categorizedOption.index}`,
470
        });
471
      }
472
      return optionsAccumulator;
1,052✔
473
    },
474
    []
475
  );
476
}
477

478
function buildFocusableOptions<
479
  Option,
480
  IsMulti extends boolean,
481
  Group extends GroupBase<Option>
482
>(props: Props<Option, IsMulti, Group>, selectValue: Options<Option>) {
483
  return buildFocusableOptionsFromCategorizedOptions(
112✔
484
    buildCategorizedOptions(props, selectValue)
485
  );
486
}
487

488
function isFocusable<
489
  Option,
490
  IsMulti extends boolean,
491
  Group extends GroupBase<Option>
492
>(
493
  props: Props<Option, IsMulti, Group>,
494
  categorizedOption: CategorizedOption<Option>
495
) {
496
  const { inputValue = '' } = props;
37,287!
497
  const { data, isSelected, label, value } = categorizedOption;
37,287✔
498

499
  return (
37,287✔
500
    (!shouldHideSelectedOptions(props) || !isSelected) &&
88,801✔
501
    filterOption(props, { label, value, data }, inputValue)
502
  );
503
}
504

505
function getNextFocusedValue<
506
  Option,
507
  IsMulti extends boolean,
508
  Group extends GroupBase<Option>
509
>(state: State<Option, IsMulti, Group>, nextSelectValue: Options<Option>) {
510
  const { focusedValue, selectValue: lastSelectValue } = state;
26✔
511
  const lastFocusedIndex = lastSelectValue.indexOf(focusedValue!);
26✔
512
  if (lastFocusedIndex > -1) {
26!
513
    const nextFocusedIndex = nextSelectValue.indexOf(focusedValue!);
×
514
    if (nextFocusedIndex > -1) {
×
515
      // the focused value is still in the selectValue, return it
516
      return focusedValue;
×
517
    } else if (lastFocusedIndex < nextSelectValue.length) {
×
518
      // the focusedValue is not present in the next selectValue array by
519
      // reference, so return the new value at the same index
520
      return nextSelectValue[lastFocusedIndex];
×
521
    }
522
  }
523
  return null;
26✔
524
}
525

526
function getNextFocusedOption<
527
  Option,
528
  IsMulti extends boolean,
529
  Group extends GroupBase<Option>
530
>(state: State<Option, IsMulti, Group>, options: Options<Option>) {
531
  const { focusedOption: lastFocusedOption } = state;
154✔
532
  return lastFocusedOption && options.indexOf(lastFocusedOption) > -1
154✔
533
    ? lastFocusedOption
534
    : options[0];
535
}
536

537
const getFocusedOptionId = <Option,>(
5✔
538
  focusableOptionsWithIds: FocusableOptionWithId<Option>[],
539
  focusedOption: Option
540
) => {
541
  const focusedOptionId = focusableOptionsWithIds.find(
511✔
542
    (option) => option.data === focusedOption
630✔
543
  )?.id;
544
  return focusedOptionId || null;
511✔
545
};
546

547
const getOptionLabel = <
5✔
548
  Option,
549
  IsMulti extends boolean,
550
  Group extends GroupBase<Option>
551
>(
552
  props: Props<Option, IsMulti, Group>,
553
  data: Option
554
): string => {
555
  return props.getOptionLabel(data);
46,163✔
556
};
557
const getOptionValue = <
5✔
558
  Option,
559
  IsMulti extends boolean,
560
  Group extends GroupBase<Option>
561
>(
562
  props: Props<Option, IsMulti, Group>,
563
  data: Option
564
): string => {
565
  return props.getOptionValue(data);
80,372✔
566
};
567

568
function isOptionDisabled<
569
  Option,
570
  IsMulti extends boolean,
571
  Group extends GroupBase<Option>
572
>(
573
  props: Props<Option, IsMulti, Group>,
574
  option: Option,
575
  selectValue: Options<Option>
576
): boolean {
577
  return typeof props.isOptionDisabled === 'function'
37,340!
578
    ? props.isOptionDisabled(option, selectValue)
579
    : false;
580
}
581
function isOptionSelected<
582
  Option,
583
  IsMulti extends boolean,
584
  Group extends GroupBase<Option>
585
>(
586
  props: Props<Option, IsMulti, Group>,
587
  option: Option,
588
  selectValue: Options<Option>
589
): boolean {
590
  if (selectValue.indexOf(option) > -1) return true;
37,314✔
591
  if (typeof props.isOptionSelected === 'function') {
37,002✔
592
    return props.isOptionSelected(option, selectValue);
102✔
593
  }
594
  const candidate = getOptionValue(props, option);
36,900✔
595
  return selectValue.some((i) => getOptionValue(props, i) === candidate);
36,900✔
596
}
597
function filterOption<
598
  Option,
599
  IsMulti extends boolean,
600
  Group extends GroupBase<Option>
601
>(
602
  props: Props<Option, IsMulti, Group>,
603
  option: FilterOptionOption<Option>,
604
  inputValue: string
605
) {
606
  return props.filterOption ? props.filterOption(option, inputValue) : true;
37,184✔
607
}
608

609
const shouldHideSelectedOptions = <
5✔
610
  Option,
611
  IsMulti extends boolean,
612
  Group extends GroupBase<Option>
613
>(
614
  props: Props<Option, IsMulti, Group>
615
) => {
616
  const { hideSelectedOptions, isMulti } = props;
37,287✔
617
  if (hideSelectedOptions === undefined) return isMulti;
37,287✔
618
  return hideSelectedOptions;
3,949✔
619
};
620

621
let instanceId = 1;
5✔
622

623
export default class Select<
624
  Option = unknown,
625
  IsMulti extends boolean = false,
626
  Group extends GroupBase<Option> = GroupBase<Option>
627
> extends Component<
628
  Props<Option, IsMulti, Group>,
629
  State<Option, IsMulti, Group>
630
> {
631
  static defaultProps = defaultProps;
5✔
632
  state: State<Option, IsMulti, Group> = {
256✔
633
    ariaSelection: null,
634
    focusedOption: null,
635
    focusedOptionId: null,
636
    focusableOptionsWithIds: [],
637
    focusedValue: null,
638
    inputIsHidden: false,
639
    isFocused: false,
640
    selectValue: [],
641
    clearFocusValueOnUpdate: false,
642
    prevWasFocused: false,
643
    inputIsHiddenAfterUpdate: undefined,
644
    prevProps: undefined,
645
    instancePrefix: '',
646
  };
647

648
  // Misc. Instance Properties
649
  // ------------------------------
650

651
  blockOptionHover = false;
256✔
652
  isComposing = false;
256✔
653
  commonProps: any; // TODO
654
  initialTouchX = 0;
256✔
655
  initialTouchY = 0;
256✔
656
  openAfterFocus = false;
256✔
657
  scrollToFocusedOptionOnUpdate = false;
256✔
658
  userIsDragging?: boolean;
659
  isAppleDevice = isAppleDevice();
256✔
660

661
  // Refs
662
  // ------------------------------
663

664
  controlRef: HTMLDivElement | null = null;
256✔
665
  getControlRef: RefCallback<HTMLDivElement> = (ref) => {
256✔
666
    this.controlRef = ref;
504✔
667
  };
668
  focusedOptionRef: HTMLDivElement | null = null;
256✔
669
  getFocusedOptionRef: RefCallback<HTMLDivElement> = (ref) => {
256✔
670
    this.focusedOptionRef = ref;
792✔
671
  };
672
  menuListRef: HTMLDivElement | null = null;
256✔
673
  getMenuListRef: RefCallback<HTMLDivElement> = (ref) => {
256✔
674
    this.menuListRef = ref;
1,536✔
675
  };
676
  inputRef: HTMLInputElement | null = null;
256✔
677
  getInputRef: RefCallback<HTMLInputElement> = (ref) => {
256✔
678
    this.inputRef = ref;
502✔
679
  };
680

681
  // Lifecycle
682
  // ------------------------------
683

684
  constructor(props: Props<Option, IsMulti, Group>) {
685
    super(props);
256✔
686
    this.state.instancePrefix =
256✔
687
      'react-select-' + (this.props.instanceId || ++instanceId);
506✔
688
    this.state.selectValue = cleanValue(props.value);
256✔
689
    // Set focusedOption if menuIsOpen is set on init (e.g. defaultMenuIsOpen)
690
    if (props.menuIsOpen && this.state.selectValue.length) {
256✔
691
      const focusableOptionsWithIds: FocusableOptionWithId<Option>[] =
692
        this.getFocusableOptionsWithIds();
21✔
693
      const focusableOptions = this.buildFocusableOptions();
21✔
694
      const optionIndex = focusableOptions.indexOf(this.state.selectValue[0]);
21✔
695
      this.state.focusableOptionsWithIds = focusableOptionsWithIds;
21✔
696
      this.state.focusedOption = focusableOptions[optionIndex];
21✔
697
      this.state.focusedOptionId = getFocusedOptionId(
21✔
698
        focusableOptionsWithIds,
699
        focusableOptions[optionIndex]
700
      );
701
    }
702
  }
703

704
  static getDerivedStateFromProps(
705
    props: Props<unknown, boolean, GroupBase<unknown>>,
706
    state: State<unknown, boolean, GroupBase<unknown>>
707
  ) {
708
    const {
709
      prevProps,
710
      clearFocusValueOnUpdate,
711
      inputIsHiddenAfterUpdate,
712
      ariaSelection,
713
      isFocused,
714
      prevWasFocused,
715
      instancePrefix,
716
    } = state;
825✔
717
    const { options, value, menuIsOpen, inputValue, isMulti } = props;
825✔
718
    const selectValue = cleanValue(value);
825✔
719
    let newMenuOptionsState = {};
825✔
720
    if (
825✔
721
      prevProps &&
2,872✔
722
      (value !== prevProps.value ||
723
        options !== prevProps.options ||
724
        menuIsOpen !== prevProps.menuIsOpen ||
725
        inputValue !== prevProps.inputValue)
726
    ) {
727
      const focusableOptions = menuIsOpen
154✔
728
        ? buildFocusableOptions(props, selectValue)
729
        : [];
730

731
      const focusableOptionsWithIds = menuIsOpen
154✔
732
        ? buildFocusableOptionsWithIds(
733
            buildCategorizedOptions(props, selectValue),
734
            `${instancePrefix}-option`
735
          )
736
        : [];
737

738
      const focusedValue = clearFocusValueOnUpdate
154✔
739
        ? getNextFocusedValue(state, selectValue)
740
        : null;
741
      const focusedOption = getNextFocusedOption(state, focusableOptions);
154✔
742
      const focusedOptionId = getFocusedOptionId(
154✔
743
        focusableOptionsWithIds,
744
        focusedOption
745
      );
746

747
      newMenuOptionsState = {
154✔
748
        selectValue,
749
        focusedOption,
750
        focusedOptionId,
751
        focusableOptionsWithIds,
752
        focusedValue,
753
        clearFocusValueOnUpdate: false,
754
      };
755
    }
756
    // some updates should toggle the state of the input visibility
757
    const newInputIsHiddenState =
758
      inputIsHiddenAfterUpdate != null && props !== prevProps
825✔
759
        ? {
760
            inputIsHidden: inputIsHiddenAfterUpdate,
761
            inputIsHiddenAfterUpdate: undefined,
762
          }
763
        : {};
764

765
    let newAriaSelection = ariaSelection;
825✔
766

767
    let hasKeptFocus = isFocused && prevWasFocused;
825✔
768

769
    if (isFocused && !hasKeptFocus) {
825✔
770
      // If `value` or `defaultValue` props are not empty then announce them
771
      // when the Select is initially focused
772
      newAriaSelection = {
58✔
773
        value: valueTernary(isMulti, selectValue, selectValue[0] || null),
108✔
774
        options: selectValue,
775
        action: 'initial-input-focus',
776
      };
777

778
      hasKeptFocus = !prevWasFocused;
58✔
779
    }
780

781
    // If the 'initial-input-focus' action has been set already
782
    // then reset the ariaSelection to null
783
    if (ariaSelection?.action === 'initial-input-focus') {
825✔
784
      newAriaSelection = null;
31✔
785
    }
786

787
    return {
825✔
788
      ...newMenuOptionsState,
789
      ...newInputIsHiddenState,
790
      prevProps: props,
791
      ariaSelection: newAriaSelection,
792
      prevWasFocused: hasKeptFocus,
793
    };
794
  }
795
  componentDidMount() {
796
    this.startListeningComposition();
256✔
797
    this.startListeningToTouch();
256✔
798

799
    if (this.props.closeMenuOnScroll && document && document.addEventListener) {
256!
800
      // Listen to all scroll events, and filter them out inside of 'onScroll'
801
      document.addEventListener('scroll', this.onScroll, true);
×
802
    }
803

804
    if (this.props.autoFocus) {
256✔
805
      this.focusInput();
5✔
806
    }
807

808
    // Scroll focusedOption into view if menuIsOpen is set on mount (e.g. defaultMenuIsOpen)
809
    if (
256✔
810
      this.props.menuIsOpen &&
404✔
811
      this.state.focusedOption &&
812
      this.menuListRef &&
813
      this.focusedOptionRef
814
    ) {
815
      scrollIntoView(this.menuListRef, this.focusedOptionRef);
10✔
816
    }
817
  }
818
  componentDidUpdate(prevProps: Props<Option, IsMulti, Group>) {
819
    const { isDisabled, menuIsOpen } = this.props;
569✔
820
    const { isFocused } = this.state;
569✔
821

822
    if (
569✔
823
      // ensure focus is restored correctly when the control becomes enabled
824
      (isFocused && !isDisabled && prevProps.isDisabled) ||
1,669✔
825
      // ensure focus is on the Input when the menu opens
826
      (isFocused && menuIsOpen && !prevProps.menuIsOpen)
827
    ) {
828
      this.focusInput();
27✔
829
    }
830

831
    if (isFocused && isDisabled && !prevProps.isDisabled) {
569!
832
      // ensure select state gets blurred in case Select is programmatically disabled while focused
833
      // eslint-disable-next-line react/no-did-update-set-state
834
      this.setState({ isFocused: false }, this.onMenuClose);
×
835
    } else if (
569!
836
      !isFocused &&
1,419!
837
      !isDisabled &&
838
      prevProps.isDisabled &&
839
      this.inputRef === document.activeElement
840
    ) {
841
      // ensure select state gets focused in case Select is programatically re-enabled while focused (Firefox)
842
      // eslint-disable-next-line react/no-did-update-set-state
843
      this.setState({ isFocused: true });
×
844
    }
845

846
    // scroll the focused option into view if necessary
847
    if (
569✔
848
      this.menuListRef &&
1,469✔
849
      this.focusedOptionRef &&
850
      this.scrollToFocusedOptionOnUpdate
851
    ) {
852
      scrollIntoView(this.menuListRef, this.focusedOptionRef);
328✔
853
      this.scrollToFocusedOptionOnUpdate = false;
328✔
854
    }
855
  }
856
  componentWillUnmount() {
857
    this.stopListeningComposition();
256✔
858
    this.stopListeningToTouch();
256✔
859
    document.removeEventListener('scroll', this.onScroll, true);
256✔
860
  }
861

862
  // ==============================
863
  // Consumer Handlers
864
  // ==============================
865

866
  onMenuOpen() {
867
    this.props.onMenuOpen();
36✔
868
  }
869
  onMenuClose() {
870
    this.onInputChange('', {
95✔
871
      action: 'menu-close',
872
      prevInputValue: this.props.inputValue,
873
    });
874

875
    this.props.onMenuClose();
95✔
876
  }
877
  onInputChange(newValue: string, actionMeta: InputActionMeta) {
878
    this.props.onInputChange(newValue, actionMeta);
195✔
879
  }
880

881
  // ==============================
882
  // Methods
883
  // ==============================
884

885
  focusInput() {
886
    if (!this.inputRef) return;
79!
887
    this.inputRef.focus();
79✔
888
  }
889
  blurInput() {
890
    if (!this.inputRef) return;
50!
891
    this.inputRef.blur();
50✔
892
  }
893

894
  // aliased for consumers
895
  focus = this.focusInput;
256✔
896
  blur = this.blurInput;
256✔
897

898
  openMenu(focusOption: 'first' | 'last') {
899
    const { selectValue, isFocused } = this.state;
28✔
900
    const focusableOptions = this.buildFocusableOptions();
28✔
901
    let openAtIndex = focusOption === 'first' ? 0 : focusableOptions.length - 1;
28!
902

903
    if (!this.props.isMulti) {
28✔
904
      const selectedIndex = focusableOptions.indexOf(selectValue[0]);
13✔
905
      if (selectedIndex > -1) {
13!
906
        openAtIndex = selectedIndex;
×
907
      }
908
    }
909

910
    // only scroll if the menu isn't already open
911
    this.scrollToFocusedOptionOnUpdate = !(isFocused && this.menuListRef);
28!
912

913
    this.setState(
28✔
914
      {
915
        inputIsHiddenAfterUpdate: false,
916
        focusedValue: null,
917
        focusedOption: focusableOptions[openAtIndex],
918
        focusedOptionId: this.getFocusedOptionId(focusableOptions[openAtIndex]),
919
      },
920
      () => this.onMenuOpen()
28✔
921
    );
922
  }
923

924
  focusValue(direction: 'previous' | 'next') {
925
    const { selectValue, focusedValue } = this.state;
2✔
926

927
    // Only multiselects support value focusing
928
    if (!this.props.isMulti) return;
2!
929

930
    this.setState({
2✔
931
      focusedOption: null,
932
    });
933

934
    let focusedIndex = selectValue.indexOf(focusedValue!);
2✔
935
    if (!focusedValue) {
2✔
936
      focusedIndex = -1;
1✔
937
    }
938

939
    const lastIndex = selectValue.length - 1;
2✔
940
    let nextFocus = -1;
2✔
941
    if (!selectValue.length) return;
2!
942

943
    switch (direction) {
2!
944
      case 'previous':
945
        if (focusedIndex === 0) {
2!
946
          // don't cycle from the start to the end
947
          nextFocus = 0;
×
948
        } else if (focusedIndex === -1) {
2✔
949
          // if nothing is focused, focus the last value first
950
          nextFocus = lastIndex;
1✔
951
        } else {
952
          nextFocus = focusedIndex - 1;
1✔
953
        }
954
        break;
2✔
955
      case 'next':
956
        if (focusedIndex > -1 && focusedIndex < lastIndex) {
×
957
          nextFocus = focusedIndex + 1;
×
958
        }
959
        break;
×
960
    }
961
    this.setState({
2✔
962
      inputIsHidden: nextFocus !== -1,
963
      focusedValue: selectValue[nextFocus],
964
    });
965
  }
966

967
  focusOption(direction: FocusDirection = 'first') {
×
968
    const { pageSize } = this.props;
302✔
969
    const { focusedOption } = this.state;
302✔
970
    const options = this.getFocusableOptions();
302✔
971

972
    if (!options.length) return;
302!
973
    let nextFocus = 0; // handles 'first'
302✔
974
    let focusedIndex = options.indexOf(focusedOption!);
302✔
975
    if (!focusedOption) {
302✔
976
      focusedIndex = -1;
44✔
977
    }
978

979
    if (direction === 'up') {
302✔
980
      nextFocus = focusedIndex > 0 ? focusedIndex - 1 : options.length - 1;
17✔
981
    } else if (direction === 'down') {
285✔
982
      nextFocus = (focusedIndex + 1) % options.length;
243✔
983
    } else if (direction === 'pageup') {
42✔
984
      nextFocus = focusedIndex - pageSize;
8✔
985
      if (nextFocus < 0) nextFocus = 0;
8✔
986
    } else if (direction === 'pagedown') {
34✔
987
      nextFocus = focusedIndex + pageSize;
16✔
988
      if (nextFocus > options.length - 1) nextFocus = options.length - 1;
16✔
989
    } else if (direction === 'last') {
18✔
990
      nextFocus = options.length - 1;
10✔
991
    }
992
    this.scrollToFocusedOptionOnUpdate = true;
302✔
993
    this.setState({
302✔
994
      focusedOption: options[nextFocus],
995
      focusedValue: null,
996
      focusedOptionId: this.getFocusedOptionId(options[nextFocus]),
997
    });
998
  }
999
  onChange = (
256✔
1000
    newValue: OnChangeValue<Option, IsMulti>,
1001
    actionMeta: ActionMeta<Option>
1002
  ) => {
1003
    const { onChange, name } = this.props;
59✔
1004
    actionMeta.name = name;
59✔
1005

1006
    this.ariaOnChange(newValue, actionMeta);
59✔
1007
    onChange(newValue, actionMeta);
59✔
1008
  };
1009
  setValue = (
256✔
1010
    newValue: OnChangeValue<Option, IsMulti>,
1011
    action: SetValueAction,
1012
    option?: Option
1013
  ) => {
1014
    const { closeMenuOnSelect, isMulti, inputValue } = this.props;
51✔
1015
    this.onInputChange('', { action: 'set-value', prevInputValue: inputValue });
51✔
1016
    if (closeMenuOnSelect) {
51✔
1017
      this.setState({
50✔
1018
        inputIsHiddenAfterUpdate: !isMulti,
1019
      });
1020
      this.onMenuClose();
50✔
1021
    }
1022
    // when the select value should change, we should reset focusedValue
1023
    this.setState({ clearFocusValueOnUpdate: true });
51✔
1024
    this.onChange(newValue, { action, option });
51✔
1025
  };
1026
  selectOption = (newValue: Option) => {
256✔
1027
    const { blurInputOnSelect, isMulti, name } = this.props;
53✔
1028
    const { selectValue } = this.state;
53✔
1029
    const deselected = isMulti && this.isOptionSelected(newValue, selectValue);
53✔
1030
    const isDisabled = this.isOptionDisabled(newValue, selectValue);
53✔
1031

1032
    if (deselected) {
53✔
1033
      const candidate = this.getOptionValue(newValue);
6✔
1034
      this.setValue(
6✔
1035
        multiValueAsValue(
1036
          selectValue.filter((i) => this.getOptionValue(i) !== candidate)
6✔
1037
        ),
1038
        'deselect-option',
1039
        newValue
1040
      );
1041
    } else if (!isDisabled) {
47✔
1042
      // Select option if option is not disabled
1043
      if (isMulti) {
45✔
1044
        this.setValue(
21✔
1045
          multiValueAsValue([...selectValue, newValue]),
1046
          'select-option',
1047
          newValue
1048
        );
1049
      } else {
1050
        this.setValue(singleValueAsValue(newValue), 'select-option');
24✔
1051
      }
1052
    } else {
1053
      this.ariaOnChange(singleValueAsValue(newValue), {
2✔
1054
        action: 'select-option',
1055
        option: newValue,
1056
        name,
1057
      });
1058
      return;
2✔
1059
    }
1060

1061
    if (blurInputOnSelect) {
51✔
1062
      this.blurInput();
50✔
1063
    }
1064
  };
1065
  removeValue = (removedValue: Option) => {
256✔
1066
    const { isMulti } = this.props;
1✔
1067
    const { selectValue } = this.state;
1✔
1068
    const candidate = this.getOptionValue(removedValue);
1✔
1069
    const newValueArray = selectValue.filter(
1✔
1070
      (i) => this.getOptionValue(i) !== candidate
3✔
1071
    );
1072
    const newValue = valueTernary(
1✔
1073
      isMulti,
1074
      newValueArray,
1075
      newValueArray[0] || null
1!
1076
    );
1077

1078
    this.onChange(newValue, { action: 'remove-value', removedValue });
1✔
1079
    this.focusInput();
1✔
1080
  };
1081
  clearValue = () => {
256✔
1082
    const { selectValue } = this.state;
5✔
1083
    this.onChange(valueTernary(this.props.isMulti, [], null), {
5✔
1084
      action: 'clear',
1085
      removedValues: selectValue,
1086
    });
1087
  };
1088
  popValue = () => {
256✔
1089
    const { isMulti } = this.props;
3✔
1090
    const { selectValue } = this.state;
3✔
1091
    const lastSelectedValue = selectValue[selectValue.length - 1];
3✔
1092
    const newValueArray = selectValue.slice(0, selectValue.length - 1);
3✔
1093
    const newValue = valueTernary(
3✔
1094
      isMulti,
1095
      newValueArray,
1096
      newValueArray[0] || null
5✔
1097
    );
1098

1099
    if (lastSelectedValue) {
3✔
1100
      this.onChange(newValue, {
2✔
1101
        action: 'pop-value',
1102
        removedValue: lastSelectedValue,
1103
      });
1104
    }
1105
  };
1106

1107
  // ==============================
1108
  // Getters
1109
  // ==============================
1110

1111
  getTheme() {
1112
    // Use the default theme if there are no customisations.
1113
    if (!this.props.theme) {
825✔
1114
      return defaultTheme;
824✔
1115
    }
1116
    // If the theme prop is a function, assume the function
1117
    // knows how to merge the passed-in default theme with
1118
    // its own modifications.
1119
    if (typeof this.props.theme === 'function') {
1!
1120
      return this.props.theme(defaultTheme);
1✔
1121
    }
1122
    // Otherwise, if a plain theme object was passed in,
1123
    // overlay it with the default theme.
1124
    return {
×
1125
      ...defaultTheme,
1126
      ...this.props.theme,
1127
    };
1128
  }
1129

1130
  getFocusedOptionId = (focusedOption: Option) => {
256✔
1131
    return getFocusedOptionId(
336✔
1132
      this.state.focusableOptionsWithIds,
1133
      focusedOption
1134
    );
1135
  };
1136

1137
  getFocusableOptionsWithIds = () => {
256✔
1138
    return buildFocusableOptionsWithIds(
21✔
1139
      buildCategorizedOptions(this.props, this.state.selectValue),
1140
      this.getElementId('option')
1141
    );
1142
  };
1143

1144
  getValue = () => this.state.selectValue;
256✔
1145

1146
  cx = (...args: any) => classNames(this.props.classNamePrefix, ...args);
17,844✔
1147

1148
  getCommonProps() {
1149
    const {
1150
      clearValue,
1151
      cx,
1152
      getStyles,
1153
      getClassNames,
1154
      getValue,
1155
      selectOption,
1156
      setValue,
1157
      props,
1158
    } = this;
825✔
1159
    const { isMulti, isRtl, options } = props;
825✔
1160
    const hasValue = this.hasValue();
825✔
1161

1162
    return {
825✔
1163
      clearValue,
1164
      cx,
1165
      getStyles,
1166
      getClassNames,
1167
      getValue,
1168
      hasValue,
1169
      isMulti,
1170
      isRtl,
1171
      options,
1172
      selectOption,
1173
      selectProps: props,
1174
      setValue,
1175
      theme: this.getTheme(),
1176
    };
1177
  }
1178

1179
  getOptionLabel = (data: Option): string => {
256✔
1180
    return getOptionLabel(this.props, data);
8,876✔
1181
  };
1182
  getOptionValue = (data: Option): string => {
256✔
1183
    return getOptionValue(this.props, data);
330✔
1184
  };
1185
  getStyles = <Key extends keyof StylesProps<Option, IsMulti, Group>>(
256✔
1186
    key: Key,
1187
    props: StylesProps<Option, IsMulti, Group>[Key]
1188
  ) => {
1189
    const { unstyled } = this.props;
17,033✔
1190
    const base = defaultStyles[key](props as any, unstyled);
17,033✔
1191
    base.boxSizing = 'border-box';
17,033✔
1192
    const custom = this.props.styles[key];
17,033✔
1193
    return custom ? custom(base, props as any) : base;
17,033!
1194
  };
1195
  getClassNames = <Key extends keyof StylesProps<Option, IsMulti, Group>>(
256✔
1196
    key: Key,
1197
    props: StylesProps<Option, IsMulti, Group>[Key]
1198
  ) => this.props.classNames[key]?.(props as any);
17,033✔
1199
  getElementId = (
256✔
1200
    element:
1201
      | 'group'
1202
      | 'input'
1203
      | 'listbox'
1204
      | 'option'
1205
      | 'placeholder'
1206
      | 'live-region'
1207
  ) => {
1208
    return `${this.state.instancePrefix}-${element}`;
12,893✔
1209
  };
1210

1211
  getComponents = () => {
256✔
1212
    return defaultComponents(this.props);
6,600✔
1213
  };
1214

1215
  buildCategorizedOptions = () =>
256✔
1216
    buildCategorizedOptions(this.props, this.state.selectValue);
2,128✔
1217
  getCategorizedOptions = () =>
256✔
1218
    this.props.menuIsOpen ? this.buildCategorizedOptions() : [];
565!
1219
  buildFocusableOptions = () =>
256✔
1220
    buildFocusableOptionsFromCategorizedOptions(this.buildCategorizedOptions());
1,563✔
1221
  getFocusableOptions = () =>
256✔
1222
    this.props.menuIsOpen ? this.buildFocusableOptions() : [];
1,736✔
1223

1224
  // ==============================
1225
  // Helpers
1226
  // ==============================
1227

1228
  ariaOnChange = (
256✔
1229
    value: OnChangeValue<Option, IsMulti>,
1230
    actionMeta: ActionMeta<Option>
1231
  ) => {
1232
    this.setState({ ariaSelection: { value, ...actionMeta } });
61✔
1233
  };
1234

1235
  hasValue() {
1236
    const { selectValue } = this.state;
2,850✔
1237
    return selectValue.length > 0;
2,850✔
1238
  }
1239
  hasOptions() {
1240
    return !!this.getFocusableOptions().length;
603✔
1241
  }
1242
  isClearable(): boolean {
1243
    const { isClearable, isMulti } = this.props;
825✔
1244

1245
    // single select, by default, IS NOT clearable
1246
    // multi select, by default, IS clearable
1247
    if (isClearable === undefined) return isMulti;
825✔
1248

1249
    return isClearable;
20✔
1250
  }
1251
  isOptionDisabled(option: Option, selectValue: Options<Option>): boolean {
1252
    return isOptionDisabled(this.props, option, selectValue);
53✔
1253
  }
1254
  isOptionSelected(option: Option, selectValue: Options<Option>): boolean {
1255
    return isOptionSelected(this.props, option, selectValue);
27✔
1256
  }
1257
  filterOption(option: FilterOptionOption<Option>, inputValue: string) {
1258
    return filterOption(this.props, option, inputValue);
×
1259
  }
1260
  formatOptionLabel(
1261
    data: Option,
1262
    context: FormatOptionLabelContext
1263
  ): ReactNode {
1264
    if (typeof this.props.formatOptionLabel === 'function') {
8,759✔
1265
      const { inputValue } = this.props;
2✔
1266
      const { selectValue } = this.state;
2✔
1267
      return this.props.formatOptionLabel(data, {
2✔
1268
        context,
1269
        inputValue,
1270
        selectValue,
1271
      });
1272
    } else {
1273
      return this.getOptionLabel(data);
8,757✔
1274
    }
1275
  }
1276
  formatGroupLabel(data: Group) {
1277
    return this.props.formatGroupLabel(data);
30✔
1278
  }
1279

1280
  // ==============================
1281
  // Mouse Handlers
1282
  // ==============================
1283

1284
  onMenuMouseDown: MouseEventHandler<HTMLDivElement> = (event) => {
256✔
1285
    if (event.button !== 0) {
8!
1286
      return;
×
1287
    }
1288
    event.stopPropagation();
8✔
1289
    event.preventDefault();
8✔
1290
    this.focusInput();
8✔
1291
  };
1292
  onMenuMouseMove: MouseEventHandler<HTMLDivElement> = (event) => {
256✔
1293
    this.blockOptionHover = false;
8✔
1294
  };
1295
  onControlMouseDown = (
256✔
1296
    event: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
1297
  ) => {
1298
    // Event captured by dropdown indicator
1299
    if (event.defaultPrevented) {
42✔
1300
      return;
41✔
1301
    }
1302
    const { openMenuOnClick } = this.props;
1✔
1303
    if (!this.state.isFocused) {
1!
1304
      if (openMenuOnClick) {
1!
1305
        this.openAfterFocus = true;
×
1306
      }
1307
      this.focusInput();
1✔
1308
    } else if (!this.props.menuIsOpen) {
×
1309
      if (openMenuOnClick) {
×
1310
        this.openMenu('first');
×
1311
      }
1312
    } else {
1313
      if (
×
1314
        (event.target as HTMLElement).tagName !== 'INPUT' &&
×
1315
        (event.target as HTMLElement).tagName !== 'TEXTAREA'
1316
      ) {
1317
        this.onMenuClose();
×
1318
      }
1319
    }
1320
    if (
1!
1321
      (event.target as HTMLElement).tagName !== 'INPUT' &&
1!
1322
      (event.target as HTMLElement).tagName !== 'TEXTAREA'
1323
    ) {
1324
      event.preventDefault();
×
1325
    }
1326
  };
1327
  onDropdownIndicatorMouseDown = (
256✔
1328
    event: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
1329
  ) => {
1330
    // ignore mouse events that weren't triggered by the primary button
1331
    if (
37!
1332
      event &&
111✔
1333
      event.type === 'mousedown' &&
1334
      (event as React.MouseEvent<HTMLDivElement>).button !== 0
1335
    ) {
1336
      return;
×
1337
    }
1338
    if (this.props.isDisabled) return;
37!
1339
    const { isMulti, menuIsOpen } = this.props;
37✔
1340
    this.focusInput();
37✔
1341
    if (menuIsOpen) {
37✔
1342
      this.setState({ inputIsHiddenAfterUpdate: !isMulti });
9✔
1343
      this.onMenuClose();
9✔
1344
    } else {
1345
      this.openMenu('first');
28✔
1346
    }
1347
    event.preventDefault();
37✔
1348
  };
1349
  onClearIndicatorMouseDown = (
256✔
1350
    event: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
1351
  ) => {
1352
    // ignore mouse events that weren't triggered by the primary button
1353
    if (
3!
1354
      event &&
9✔
1355
      event.type === 'mousedown' &&
1356
      (event as React.MouseEvent<HTMLDivElement>).button !== 0
1357
    ) {
1358
      return;
×
1359
    }
1360
    this.clearValue();
3✔
1361
    event.preventDefault();
3✔
1362
    this.openAfterFocus = false;
3✔
1363
    if (event.type === 'touchend') {
3!
1364
      this.focusInput();
×
1365
    } else {
1366
      setTimeout(() => this.focusInput());
3✔
1367
    }
1368
  };
1369
  onScroll = (event: Event) => {
256✔
1370
    if (typeof this.props.closeMenuOnScroll === 'boolean') {
×
1371
      if (
×
1372
        event.target instanceof HTMLElement &&
×
1373
        isDocumentElement(event.target)
1374
      ) {
1375
        this.props.onMenuClose();
×
1376
      }
1377
    } else if (typeof this.props.closeMenuOnScroll === 'function') {
×
1378
      if (this.props.closeMenuOnScroll(event)) {
×
1379
        this.props.onMenuClose();
×
1380
      }
1381
    }
1382
  };
1383

1384
  // ==============================
1385
  // Composition Handlers
1386
  // ==============================
1387

1388
  startListeningComposition() {
1389
    if (document && document.addEventListener) {
256!
1390
      document.addEventListener(
256✔
1391
        'compositionstart',
1392
        this.onCompositionStart,
1393
        false
1394
      );
1395
      document.addEventListener('compositionend', this.onCompositionEnd, false);
256✔
1396
    }
1397
  }
1398
  stopListeningComposition() {
1399
    if (document && document.removeEventListener) {
256!
1400
      document.removeEventListener('compositionstart', this.onCompositionStart);
256✔
1401
      document.removeEventListener('compositionend', this.onCompositionEnd);
256✔
1402
    }
1403
  }
1404
  onCompositionStart = () => {
256✔
1405
    this.isComposing = true;
×
1406
  };
1407
  onCompositionEnd = () => {
256✔
1408
    this.isComposing = false;
×
1409
  };
1410

1411
  // ==============================
1412
  // Touch Handlers
1413
  // ==============================
1414

1415
  startListeningToTouch() {
1416
    if (document && document.addEventListener) {
256!
1417
      document.addEventListener('touchstart', this.onTouchStart, false);
256✔
1418
      document.addEventListener('touchmove', this.onTouchMove, false);
256✔
1419
      document.addEventListener('touchend', this.onTouchEnd, false);
256✔
1420
    }
1421
  }
1422
  stopListeningToTouch() {
1423
    if (document && document.removeEventListener) {
256!
1424
      document.removeEventListener('touchstart', this.onTouchStart);
256✔
1425
      document.removeEventListener('touchmove', this.onTouchMove);
256✔
1426
      document.removeEventListener('touchend', this.onTouchEnd);
256✔
1427
    }
1428
  }
1429
  onTouchStart = ({ touches }: TouchEvent) => {
256✔
1430
    const touch = touches && touches.item(0);
×
1431
    if (!touch) {
×
1432
      return;
×
1433
    }
1434

1435
    this.initialTouchX = touch.clientX;
×
1436
    this.initialTouchY = touch.clientY;
×
1437
    this.userIsDragging = false;
×
1438
  };
1439
  onTouchMove = ({ touches }: TouchEvent) => {
256✔
1440
    const touch = touches && touches.item(0);
×
1441
    if (!touch) {
×
1442
      return;
×
1443
    }
1444

1445
    const deltaX = Math.abs(touch.clientX - this.initialTouchX);
×
1446
    const deltaY = Math.abs(touch.clientY - this.initialTouchY);
×
1447
    const moveThreshold = 5;
×
1448

1449
    this.userIsDragging = deltaX > moveThreshold || deltaY > moveThreshold;
×
1450
  };
1451
  onTouchEnd = (event: TouchEvent) => {
256✔
1452
    if (this.userIsDragging) return;
×
1453

1454
    // close the menu if the user taps outside
1455
    // we're checking on event.target here instead of event.currentTarget, because we want to assert information
1456
    // on events on child elements, not the document (which we've attached this handler to).
1457
    if (
×
1458
      this.controlRef &&
×
1459
      !this.controlRef.contains(event.target as Node) &&
1460
      this.menuListRef &&
1461
      !this.menuListRef.contains(event.target as Node)
1462
    ) {
1463
      this.blurInput();
×
1464
    }
1465

1466
    // reset move vars
1467
    this.initialTouchX = 0;
×
1468
    this.initialTouchY = 0;
×
1469
  };
1470
  onControlTouchEnd: TouchEventHandler<HTMLDivElement> = (event) => {
256✔
1471
    if (this.userIsDragging) return;
×
1472
    this.onControlMouseDown(event);
×
1473
  };
1474
  onClearIndicatorTouchEnd: TouchEventHandler<HTMLDivElement> = (event) => {
256✔
1475
    if (this.userIsDragging) return;
×
1476

1477
    this.onClearIndicatorMouseDown(event);
×
1478
  };
1479
  onDropdownIndicatorTouchEnd: TouchEventHandler<HTMLDivElement> = (event) => {
256✔
1480
    if (this.userIsDragging) return;
×
1481

1482
    this.onDropdownIndicatorMouseDown(event);
×
1483
  };
1484

1485
  // ==============================
1486
  // Focus Handlers
1487
  // ==============================
1488

1489
  handleInputChange: FormEventHandler<HTMLInputElement> = (event) => {
256✔
1490
    const { inputValue: prevInputValue } = this.props;
13✔
1491
    const inputValue = event.currentTarget.value;
13✔
1492
    this.setState({ inputIsHiddenAfterUpdate: false });
13✔
1493
    this.onInputChange(inputValue, { action: 'input-change', prevInputValue });
13✔
1494
    if (!this.props.menuIsOpen) {
13✔
1495
      this.onMenuOpen();
8✔
1496
    }
1497
  };
1498
  onInputFocus: FocusEventHandler<HTMLInputElement> = (event) => {
256✔
1499
    if (this.props.onFocus) {
60✔
1500
      this.props.onFocus(event);
4✔
1501
    }
1502
    this.setState({
60✔
1503
      inputIsHiddenAfterUpdate: false,
1504
      isFocused: true,
1505
    });
1506
    if (this.openAfterFocus || this.props.openMenuOnFocus) {
60!
1507
      this.openMenu('first');
×
1508
    }
1509
    this.openAfterFocus = false;
60✔
1510
  };
1511
  onInputBlur: FocusEventHandler<HTMLInputElement> = (event) => {
256✔
1512
    const { inputValue: prevInputValue } = this.props;
31✔
1513
    if (this.menuListRef && this.menuListRef.contains(document.activeElement)) {
31!
1514
      this.inputRef!.focus();
×
1515
      return;
×
1516
    }
1517
    if (this.props.onBlur) {
31✔
1518
      this.props.onBlur(event);
4✔
1519
    }
1520
    this.onInputChange('', { action: 'input-blur', prevInputValue });
31✔
1521
    this.onMenuClose();
31✔
1522
    this.setState({
31✔
1523
      focusedValue: null,
1524
      isFocused: false,
1525
    });
1526
  };
1527
  onOptionHover = (focusedOption: Option) => {
256✔
1528
    if (this.blockOptionHover || this.state.focusedOption === focusedOption) {
12✔
1529
      return;
6✔
1530
    }
1531
    const options = this.getFocusableOptions();
6✔
1532
    const focusedOptionIndex = options.indexOf(focusedOption!);
6✔
1533
    this.setState({
6✔
1534
      focusedOption,
1535
      focusedOptionId:
1536
        focusedOptionIndex > -1 ? this.getFocusedOptionId(focusedOption) : null,
6!
1537
    });
1538
  };
1539
  shouldHideSelectedOptions = () => {
256✔
1540
    return shouldHideSelectedOptions(this.props);
×
1541
  };
1542

1543
  // If the hidden input gets focus through form submit,
1544
  // redirect focus to focusable input.
1545
  onValueInputFocus: FocusEventHandler = (e) => {
256✔
1546
    e.preventDefault();
×
1547
    e.stopPropagation();
×
1548

1549
    this.focus();
×
1550
  };
1551

1552
  // ==============================
1553
  // Keyboard Handlers
1554
  // ==============================
1555

1556
  onKeyDown: KeyboardEventHandler<HTMLDivElement> = (event) => {
256✔
1557
    const {
1558
      isMulti,
1559
      backspaceRemovesValue,
1560
      escapeClearsValue,
1561
      inputValue,
1562
      isClearable,
1563
      isDisabled,
1564
      menuIsOpen,
1565
      onKeyDown,
1566
      tabSelectsValue,
1567
      openMenuOnFocus,
1568
    } = this.props;
368✔
1569
    const { focusedOption, focusedValue, selectValue } = this.state;
368✔
1570

1571
    if (isDisabled) return;
368!
1572

1573
    if (typeof onKeyDown === 'function') {
368!
1574
      onKeyDown(event);
×
1575
      if (event.defaultPrevented) {
×
1576
        return;
×
1577
      }
1578
    }
1579

1580
    // Block option hover events when the user has just pressed a key
1581
    this.blockOptionHover = true;
368✔
1582
    switch (event.key) {
368!
1583
      case 'ArrowLeft':
1584
        if (!isMulti || inputValue) return;
2!
1585
        this.focusValue('previous');
2✔
1586
        break;
2✔
1587
      case 'ArrowRight':
1588
        if (!isMulti || inputValue) return;
×
1589
        this.focusValue('next');
×
1590
        break;
×
1591
      case 'Delete':
1592
      case 'Backspace':
1593
        if (inputValue) return;
6!
1594
        if (focusedValue) {
6!
1595
          this.removeValue(focusedValue);
×
1596
        } else {
1597
          if (!backspaceRemovesValue) return;
6✔
1598
          if (isMulti) {
5✔
1599
            this.popValue();
3✔
1600
          } else if (isClearable) {
2✔
1601
            this.clearValue();
1✔
1602
          }
1603
        }
1604
        break;
5✔
1605
      case 'Tab':
1606
        if (this.isComposing) return;
4!
1607

1608
        if (
4✔
1609
          event.shiftKey ||
18!
1610
          !menuIsOpen ||
1611
          !tabSelectsValue ||
1612
          !focusedOption ||
1613
          // don't capture the event if the menu opens on focus and the focused
1614
          // option is already selected; it breaks the flow of navigation
1615
          (openMenuOnFocus && this.isOptionSelected(focusedOption, selectValue))
1616
        ) {
1617
          return;
1✔
1618
        }
1619
        this.selectOption(focusedOption);
3✔
1620
        break;
3✔
1621
      case 'Enter':
1622
        if (event.keyCode === 229) {
33✔
1623
          // ignore the keydown event from an Input Method Editor(IME)
1624
          // ref. https://www.w3.org/TR/uievents/#determine-keydown-keyup-keyCode
1625
          break;
1✔
1626
        }
1627
        if (menuIsOpen) {
32✔
1628
          if (!focusedOption) return;
31✔
1629
          if (this.isComposing) return;
29!
1630
          this.selectOption(focusedOption);
29✔
1631
          break;
29✔
1632
        }
1633
        return;
1✔
1634
      case 'Escape':
1635
        if (menuIsOpen) {
9✔
1636
          this.setState({
5✔
1637
            inputIsHiddenAfterUpdate: false,
1638
          });
1639
          this.onInputChange('', {
5✔
1640
            action: 'menu-close',
1641
            prevInputValue: inputValue,
1642
          });
1643
          this.onMenuClose();
5✔
1644
        } else if (isClearable && escapeClearsValue) {
4✔
1645
          this.clearValue();
1✔
1646
        }
1647
        break;
9✔
1648
      case ' ': // space
1649
        if (inputValue) {
4!
1650
          return;
×
1651
        }
1652
        if (!menuIsOpen) {
4!
1653
          this.openMenu('first');
×
1654
          break;
×
1655
        }
1656
        if (!focusedOption) return;
4!
1657
        this.selectOption(focusedOption);
4✔
1658
        break;
4✔
1659
      case 'ArrowUp':
1660
        if (menuIsOpen) {
17!
1661
          this.focusOption('up');
17✔
1662
        } else {
1663
          this.openMenu('last');
×
1664
        }
1665
        break;
17✔
1666
      case 'ArrowDown':
1667
        if (menuIsOpen) {
243!
1668
          this.focusOption('down');
243✔
1669
        } else {
1670
          this.openMenu('first');
×
1671
        }
1672
        break;
243✔
1673
      case 'PageUp':
1674
        if (!menuIsOpen) return;
8!
1675
        this.focusOption('pageup');
8✔
1676
        break;
8✔
1677
      case 'PageDown':
1678
        if (!menuIsOpen) return;
16!
1679
        this.focusOption('pagedown');
16✔
1680
        break;
16✔
1681
      case 'Home':
1682
        if (!menuIsOpen) return;
8!
1683
        this.focusOption('first');
8✔
1684
        break;
8✔
1685
      case 'End':
1686
        if (!menuIsOpen) return;
10!
1687
        this.focusOption('last');
10✔
1688
        break;
10✔
1689
      default:
1690
        return;
8✔
1691
    }
1692
    event.preventDefault();
355✔
1693
  };
1694

1695
  // ==============================
1696
  // Renderers
1697
  // ==============================
1698
  renderInput() {
1699
    const {
1700
      isDisabled,
1701
      isSearchable,
1702
      inputId,
1703
      inputValue,
1704
      tabIndex,
1705
      form,
1706
      menuIsOpen,
1707
      required,
1708
    } = this.props;
825✔
1709
    const { Input } = this.getComponents();
825✔
1710
    const { inputIsHidden, ariaSelection } = this.state;
825✔
1711
    const { commonProps } = this;
825✔
1712

1713
    const id = inputId || this.getElementId('input');
825✔
1714

1715
    // aria attributes makes the JSX "noisy", separated for clarity
1716
    const ariaAttributes = {
825✔
1717
      'aria-autocomplete': 'list' as const,
1718
      'aria-expanded': menuIsOpen,
1719
      'aria-haspopup': true,
1720
      'aria-errormessage': this.props['aria-errormessage'],
1721
      'aria-invalid': this.props['aria-invalid'],
1722
      'aria-label': this.props['aria-label'],
1723
      'aria-labelledby': this.props['aria-labelledby'],
1724
      'aria-required': required,
1725
      role: 'combobox',
1726
      'aria-activedescendant': this.isAppleDevice
825!
1727
        ? undefined
1728
        : this.state.focusedOptionId || '',
1,476✔
1729

1730
      ...(menuIsOpen && {
1,428✔
1731
        'aria-controls': this.getElementId('listbox'),
1732
      }),
1733
      ...(!isSearchable && {
834✔
1734
        'aria-readonly': true,
1735
      }),
1736
      ...(this.hasValue()
825✔
1737
        ? ariaSelection?.action === 'initial-input-focus' && {
182✔
1738
            'aria-describedby': this.getElementId('live-region'),
1739
          }
1740
        : {
1741
            'aria-describedby': this.getElementId('placeholder'),
1742
          }),
1743
    };
1744

1745
    if (!isSearchable) {
825✔
1746
      // use a dummy input to maintain focus/blur functionality
1747
      return (
9✔
1748
        <DummyInput
1749
          id={id}
1750
          innerRef={this.getInputRef}
1751
          onBlur={this.onInputBlur}
1752
          onChange={noop}
1753
          onFocus={this.onInputFocus}
1754
          disabled={isDisabled}
1755
          tabIndex={tabIndex}
1756
          inputMode="none"
1757
          form={form}
1758
          value=""
1759
          {...ariaAttributes}
1760
        />
1761
      );
1762
    }
1763

1764
    return (
816✔
1765
      <Input
1766
        {...commonProps}
1767
        autoCapitalize="none"
1768
        autoComplete="off"
1769
        autoCorrect="off"
1770
        id={id}
1771
        innerRef={this.getInputRef}
1772
        isDisabled={isDisabled}
1773
        isHidden={inputIsHidden}
1774
        onBlur={this.onInputBlur}
1775
        onChange={this.handleInputChange}
1776
        onFocus={this.onInputFocus}
1777
        spellCheck="false"
1778
        tabIndex={tabIndex}
1779
        form={form}
1780
        type="text"
1781
        value={inputValue}
1782
        {...ariaAttributes}
1783
      />
1784
    );
1785
  }
1786
  renderPlaceholderOrValue() {
1787
    const {
1788
      MultiValue,
1789
      MultiValueContainer,
1790
      MultiValueLabel,
1791
      MultiValueRemove,
1792
      SingleValue,
1793
      Placeholder,
1794
    } = this.getComponents();
825✔
1795
    const { commonProps } = this;
825✔
1796
    const {
1797
      controlShouldRenderValue,
1798
      isDisabled,
1799
      isMulti,
1800
      inputValue,
1801
      placeholder,
1802
    } = this.props;
825✔
1803
    const { selectValue, focusedValue, isFocused } = this.state;
825✔
1804

1805
    if (!this.hasValue() || !controlShouldRenderValue) {
825✔
1806
      return inputValue ? null : (
651✔
1807
        <Placeholder
1808
          {...commonProps}
1809
          key="placeholder"
1810
          isDisabled={isDisabled}
1811
          isFocused={isFocused}
1812
          innerProps={{ id: this.getElementId('placeholder') }}
1813
        >
1814
          {placeholder}
1815
        </Placeholder>
1816
      );
1817
    }
1818

1819
    if (isMulti) {
174✔
1820
      return selectValue.map((opt, index) => {
98✔
1821
        const isOptionFocused = opt === focusedValue;
119✔
1822
        const key = `${this.getOptionLabel(opt)}-${this.getOptionValue(opt)}`;
119✔
1823

1824
        return (
119✔
1825
          <MultiValue
1826
            {...commonProps}
1827
            components={{
1828
              Container: MultiValueContainer,
1829
              Label: MultiValueLabel,
1830
              Remove: MultiValueRemove,
1831
            }}
1832
            isFocused={isOptionFocused}
1833
            isDisabled={isDisabled}
1834
            key={key}
1835
            index={index}
1836
            removeProps={{
1837
              onClick: () => this.removeValue(opt),
1✔
1838
              onTouchEnd: () => this.removeValue(opt),
×
1839
              onMouseDown: (e) => {
1840
                e.preventDefault();
1✔
1841
              },
1842
            }}
1843
            data={opt}
1844
          >
1845
            {this.formatOptionLabel(opt, 'value')}
1846
          </MultiValue>
1847
        );
1848
      });
1849
    }
1850

1851
    if (inputValue) {
76✔
1852
      return null;
10✔
1853
    }
1854

1855
    const singleValue = selectValue[0];
66✔
1856
    return (
66✔
1857
      <SingleValue {...commonProps} data={singleValue} isDisabled={isDisabled}>
1858
        {this.formatOptionLabel(singleValue, 'value')}
1859
      </SingleValue>
1860
    );
1861
  }
1862
  renderClearIndicator() {
1863
    const { ClearIndicator } = this.getComponents();
825✔
1864
    const { commonProps } = this;
825✔
1865
    const { isDisabled, isLoading } = this.props;
825✔
1866
    const { isFocused } = this.state;
825✔
1867

1868
    if (
825✔
1869
      !this.isClearable() ||
2,029✔
1870
      !ClearIndicator ||
1871
      isDisabled ||
1872
      !this.hasValue() ||
1873
      isLoading
1874
    ) {
1875
      return null;
718✔
1876
    }
1877

1878
    const innerProps = {
107✔
1879
      onMouseDown: this.onClearIndicatorMouseDown,
1880
      onTouchEnd: this.onClearIndicatorTouchEnd,
1881
      'aria-hidden': 'true',
1882
    };
1883

1884
    return (
107✔
1885
      <ClearIndicator
1886
        {...commonProps}
1887
        innerProps={innerProps}
1888
        isFocused={isFocused}
1889
      />
1890
    );
1891
  }
1892
  renderLoadingIndicator() {
1893
    const { LoadingIndicator } = this.getComponents();
825✔
1894
    const { commonProps } = this;
825✔
1895
    const { isDisabled, isLoading } = this.props;
825✔
1896
    const { isFocused } = this.state;
825✔
1897

1898
    if (!LoadingIndicator || !isLoading) return null;
825✔
1899

1900
    const innerProps = { 'aria-hidden': 'true' };
10✔
1901
    return (
10✔
1902
      <LoadingIndicator
1903
        {...commonProps}
1904
        innerProps={innerProps}
1905
        isDisabled={isDisabled}
1906
        isFocused={isFocused}
1907
      />
1908
    );
1909
  }
1910
  renderIndicatorSeparator() {
1911
    const { DropdownIndicator, IndicatorSeparator } = this.getComponents();
825✔
1912

1913
    // separator doesn't make sense without the dropdown indicator
1914
    if (!DropdownIndicator || !IndicatorSeparator) return null;
825!
1915

1916
    const { commonProps } = this;
825✔
1917
    const { isDisabled } = this.props;
825✔
1918
    const { isFocused } = this.state;
825✔
1919

1920
    return (
825✔
1921
      <IndicatorSeparator
1922
        {...commonProps}
1923
        isDisabled={isDisabled}
1924
        isFocused={isFocused}
1925
      />
1926
    );
1927
  }
1928
  renderDropdownIndicator() {
1929
    const { DropdownIndicator } = this.getComponents();
825✔
1930
    if (!DropdownIndicator) return null;
825!
1931
    const { commonProps } = this;
825✔
1932
    const { isDisabled } = this.props;
825✔
1933
    const { isFocused } = this.state;
825✔
1934

1935
    const innerProps = {
825✔
1936
      onMouseDown: this.onDropdownIndicatorMouseDown,
1937
      onTouchEnd: this.onDropdownIndicatorTouchEnd,
1938
      'aria-hidden': 'true',
1939
    };
1940

1941
    return (
825✔
1942
      <DropdownIndicator
1943
        {...commonProps}
1944
        innerProps={innerProps}
1945
        isDisabled={isDisabled}
1946
        isFocused={isFocused}
1947
      />
1948
    );
1949
  }
1950
  renderMenu() {
1951
    const {
1952
      Group,
1953
      GroupHeading,
1954
      Menu,
1955
      MenuList,
1956
      MenuPortal,
1957
      LoadingMessage,
1958
      NoOptionsMessage,
1959
      Option,
1960
    } = this.getComponents();
825✔
1961
    const { commonProps } = this;
825✔
1962
    const { focusedOption } = this.state;
825✔
1963
    const {
1964
      captureMenuScroll,
1965
      inputValue,
1966
      isLoading,
1967
      loadingMessage,
1968
      minMenuHeight,
1969
      maxMenuHeight,
1970
      menuIsOpen,
1971
      menuPlacement,
1972
      menuPosition,
1973
      menuPortalTarget,
1974
      menuShouldBlockScroll,
1975
      menuShouldScrollIntoView,
1976
      noOptionsMessage,
1977
      onMenuScrollToTop,
1978
      onMenuScrollToBottom,
1979
    } = this.props;
825✔
1980

1981
    if (!menuIsOpen) return null;
825✔
1982

1983
    // TODO: Internal Option Type here
1984
    const render = (props: CategorizedOption<Option>, id: string) => {
603✔
1985
      const { type, data, isDisabled, isSelected, label, value } = props;
8,574✔
1986
      const isFocused = focusedOption === data;
8,574✔
1987
      const onHover = isDisabled ? undefined : () => this.onOptionHover(data);
8,574✔
1988
      const onSelect = isDisabled ? undefined : () => this.selectOption(data);
8,574✔
1989
      const optionId = `${this.getElementId('option')}-${id}`;
8,574✔
1990
      const innerProps = {
8,574✔
1991
        id: optionId,
1992
        onClick: onSelect,
1993
        onMouseMove: onHover,
1994
        onMouseOver: onHover,
1995
        tabIndex: -1,
1996
        role: 'option',
1997
        'aria-selected': this.isAppleDevice ? undefined : isSelected, // is not supported on Apple devices
8,574!
1998
      };
1999

2000
      return (
8,574✔
2001
        <Option
2002
          {...commonProps}
2003
          innerProps={innerProps}
2004
          data={data}
2005
          isDisabled={isDisabled}
2006
          isSelected={isSelected}
2007
          key={optionId}
2008
          label={label}
2009
          type={type}
2010
          value={value}
2011
          isFocused={isFocused}
2012
          innerRef={isFocused ? this.getFocusedOptionRef : undefined}
8,574✔
2013
        >
2014
          {this.formatOptionLabel(props.data, 'menu')}
2015
        </Option>
2016
      );
2017
    };
2018

2019
    let menuUI: ReactNode;
2020

2021
    if (this.hasOptions()) {
603✔
2022
      menuUI = this.getCategorizedOptions().map((item) => {
565✔
2023
        if (item.type === 'group') {
8,439✔
2024
          const { data, options, index: groupIndex } = item;
30✔
2025
          const groupId = `${this.getElementId('group')}-${groupIndex}`;
30✔
2026
          const headingId = `${groupId}-heading`;
30✔
2027

2028
          return (
30✔
2029
            <Group
2030
              {...commonProps}
2031
              key={groupId}
2032
              data={data}
2033
              options={options}
2034
              Heading={GroupHeading}
2035
              headingProps={{
2036
                id: headingId,
2037
                data: item.data,
2038
              }}
2039
              label={this.formatGroupLabel(item.data)}
2040
            >
2041
              {item.options.map((option) =>
2042
                render(option, `${groupIndex}-${option.index}`)
165✔
2043
              )}
2044
            </Group>
2045
          );
2046
        } else if (item.type === 'option') {
8,409!
2047
          return render(item, `${item.index}`);
8,409✔
2048
        }
2049
      });
2050
    } else if (isLoading) {
38✔
2051
      const message = loadingMessage({ inputValue });
8✔
2052
      if (message === null) return null;
8!
2053
      menuUI = <LoadingMessage {...commonProps}>{message}</LoadingMessage>;
8✔
2054
    } else {
2055
      const message = noOptionsMessage({ inputValue });
30✔
2056
      if (message === null) return null;
30!
2057
      menuUI = <NoOptionsMessage {...commonProps}>{message}</NoOptionsMessage>;
30✔
2058
    }
2059
    const menuPlacementProps = {
603✔
2060
      minMenuHeight,
2061
      maxMenuHeight,
2062
      menuPlacement,
2063
      menuPosition,
2064
      menuShouldScrollIntoView,
2065
    };
2066

2067
    const menuElement = (
2068
      <MenuPlacer {...commonProps} {...menuPlacementProps}>
603✔
2069
        {({ ref, placerProps: { placement, maxHeight } }) => (
2070
          <Menu
769✔
2071
            {...commonProps}
2072
            {...menuPlacementProps}
2073
            innerRef={ref}
2074
            innerProps={{
2075
              onMouseDown: this.onMenuMouseDown,
2076
              onMouseMove: this.onMenuMouseMove,
2077
            }}
2078
            isLoading={isLoading}
2079
            placement={placement}
2080
          >
2081
            <ScrollManager
2082
              captureEnabled={captureMenuScroll}
2083
              onTopArrive={onMenuScrollToTop}
2084
              onBottomArrive={onMenuScrollToBottom}
2085
              lockEnabled={menuShouldBlockScroll}
2086
            >
2087
              {(scrollTargetRef) => (
2088
                <MenuList
768✔
2089
                  {...commonProps}
2090
                  innerRef={(instance) => {
2091
                    this.getMenuListRef(instance);
1,536✔
2092
                    scrollTargetRef(instance);
1,536✔
2093
                  }}
2094
                  innerProps={{
2095
                    role: 'listbox',
2096
                    'aria-multiselectable': commonProps.isMulti,
2097
                    id: this.getElementId('listbox'),
2098
                  }}
2099
                  isLoading={isLoading}
2100
                  maxHeight={maxHeight}
2101
                  focusedOption={focusedOption}
2102
                >
2103
                  {menuUI}
2104
                </MenuList>
2105
              )}
2106
            </ScrollManager>
2107
          </Menu>
2108
        )}
2109
      </MenuPlacer>
2110
    );
2111

2112
    // positioning behaviour is almost identical for portalled and fixed,
2113
    // so we use the same component. the actual portalling logic is forked
2114
    // within the component based on `menuPosition`
2115
    return menuPortalTarget || menuPosition === 'fixed' ? (
603!
2116
      <MenuPortal
2117
        {...commonProps}
2118
        appendTo={menuPortalTarget}
2119
        controlElement={this.controlRef}
2120
        menuPlacement={menuPlacement}
2121
        menuPosition={menuPosition}
2122
      >
2123
        {menuElement}
2124
      </MenuPortal>
2125
    ) : (
2126
      menuElement
2127
    );
2128
  }
2129
  renderFormField() {
2130
    const { delimiter, isDisabled, isMulti, name, required } = this.props;
825✔
2131
    const { selectValue } = this.state;
825✔
2132

2133
    if (required && !this.hasValue() && !isDisabled) {
825✔
2134
      return <RequiredInput name={name} onFocus={this.onValueInputFocus} />;
6✔
2135
    }
2136

2137
    if (!name || isDisabled) return;
819✔
2138

2139
    if (isMulti) {
756✔
2140
      if (delimiter) {
347✔
2141
        const value = selectValue
15✔
2142
          .map((opt) => this.getOptionValue(opt))
15✔
2143
          .join(delimiter);
2144
        return <input name={name} type="hidden" value={value} />;
15✔
2145
      } else {
2146
        const input =
2147
          selectValue.length > 0 ? (
332✔
2148
            selectValue.map((opt, i) => (
2149
              <input
104✔
2150
                key={`i-${i}`}
2151
                name={name}
2152
                type="hidden"
2153
                value={this.getOptionValue(opt)}
2154
              />
2155
            ))
2156
          ) : (
2157
            <input name={name} type="hidden" value="" />
2158
          );
2159

2160
        return <div>{input}</div>;
332✔
2161
      }
2162
    } else {
2163
      const value = selectValue[0] ? this.getOptionValue(selectValue[0]) : '';
409✔
2164
      return <input name={name} type="hidden" value={value} />;
409✔
2165
    }
2166
  }
2167

2168
  renderLiveRegion() {
2169
    const { commonProps } = this;
825✔
2170
    const {
2171
      ariaSelection,
2172
      focusedOption,
2173
      focusedValue,
2174
      isFocused,
2175
      selectValue,
2176
    } = this.state;
825✔
2177

2178
    const focusableOptions = this.getFocusableOptions();
825✔
2179

2180
    return (
825✔
2181
      <LiveRegion
2182
        {...commonProps}
2183
        id={this.getElementId('live-region')}
2184
        ariaSelection={ariaSelection}
2185
        focusedOption={focusedOption}
2186
        focusedValue={focusedValue}
2187
        isFocused={isFocused}
2188
        selectValue={selectValue}
2189
        focusableOptions={focusableOptions}
2190
        isAppleDevice={this.isAppleDevice}
2191
      />
2192
    );
2193
  }
2194

2195
  render() {
2196
    const { Control, IndicatorsContainer, SelectContainer, ValueContainer } =
2197
      this.getComponents();
825✔
2198

2199
    const { className, id, isDisabled, menuIsOpen } = this.props;
825✔
2200
    const { isFocused } = this.state;
825✔
2201
    const commonProps = (this.commonProps = this.getCommonProps());
825✔
2202

2203
    return (
825✔
2204
      <SelectContainer
2205
        {...commonProps}
2206
        className={className}
2207
        innerProps={{
2208
          id: id,
2209
          onKeyDown: this.onKeyDown,
2210
        }}
2211
        isDisabled={isDisabled}
2212
        isFocused={isFocused}
2213
      >
2214
        {this.renderLiveRegion()}
2215
        <Control
2216
          {...commonProps}
2217
          innerRef={this.getControlRef}
2218
          innerProps={{
2219
            onMouseDown: this.onControlMouseDown,
2220
            onTouchEnd: this.onControlTouchEnd,
2221
          }}
2222
          isDisabled={isDisabled}
2223
          isFocused={isFocused}
2224
          menuIsOpen={menuIsOpen}
2225
        >
2226
          <ValueContainer {...commonProps} isDisabled={isDisabled}>
2227
            {this.renderPlaceholderOrValue()}
2228
            {this.renderInput()}
2229
          </ValueContainer>
2230
          <IndicatorsContainer {...commonProps} isDisabled={isDisabled}>
2231
            {this.renderClearIndicator()}
2232
            {this.renderLoadingIndicator()}
2233
            {this.renderIndicatorSeparator()}
2234
            {this.renderDropdownIndicator()}
2235
          </IndicatorsContainer>
2236
        </Control>
2237
        {this.renderMenu()}
2238
        {this.renderFormField()}
2239
      </SelectContainer>
2240
    );
2241
  }
2242
}
2243

2244
export type PublicBaseSelectProps<
2245
  Option,
2246
  IsMulti extends boolean,
2247
  Group extends GroupBase<Option>
2248
> = 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