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

JedWatson / react-select / 0f984eea-c2ef-49b1-8ecc-1141116747e0

05 Oct 2025 07:02PM UTC coverage: 75.789%. Remained the same
0f984eea-c2ef-49b1-8ecc-1141116747e0

Pull #6004

circleci

web-flow
Merge branch 'master' into bugfix-menuShouldBlockScroll-prop
Pull Request #6004: Bugfix: menuShouldBlockScroll now applies the correct right-padding

659 of 1052 branches covered (62.64%)

1 of 2 new or added lines in 1 file covered. (50.0%)

1 existing line in 1 file now uncovered.

1033 of 1363 relevant lines covered (75.79%)

1933.28 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

356
interface FocusableOptionWithId<Option> {
357
  data: Option;
358
  id: string;
359
}
360

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

623
let instanceId = 1;
5✔
624

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

651
  // Misc. Instance Properties
652
  // ------------------------------
653

654
  blockOptionHover = false;
256✔
655
  isComposing = false;
256✔
656
  commonProps: any; // TODO
657
  initialTouchX = 0;
256✔
658
  initialTouchY = 0;
256✔
659
  openAfterFocus = false;
256✔
660
  scrollToFocusedOptionOnUpdate = false;
256✔
661
  userIsDragging?: boolean;
662

663
  // Refs
664
  // ------------------------------
665

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

683
  // Lifecycle
684
  // ------------------------------
685

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

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

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

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

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

767
    let newAriaSelection = ariaSelection;
825✔
768

769
    let hasKeptFocus = isFocused && prevWasFocused;
825✔
770

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

780
      hasKeptFocus = !prevWasFocused;
58✔
781
    }
782

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

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

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

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

810
    // Scroll focusedOption into view if menuIsOpen is set on mount (e.g. defaultMenuIsOpen)
811
    if (
256✔
812
      this.props.menuIsOpen &&
404✔
813
      this.state.focusedOption &&
814
      this.menuListRef &&
815
      this.focusedOptionRef
816
    ) {
817
      scrollIntoView(this.menuListRef, this.focusedOptionRef);
10✔
818
    }
819
    if (isAppleDevice()) {
256!
820
      // eslint-disable-next-line react/no-did-mount-set-state
821
      this.setState({ isAppleDevice: true });
×
822
    }
823
  }
824
  componentDidUpdate(prevProps: Props<Option, IsMulti, Group>) {
825
    const { isDisabled, menuIsOpen } = this.props;
569✔
826
    const { isFocused } = this.state;
569✔
827

828
    if (
569✔
829
      // ensure focus is restored correctly when the control becomes enabled
830
      (isFocused && !isDisabled && prevProps.isDisabled) ||
1,669✔
831
      // ensure focus is on the Input when the menu opens
832
      (isFocused && menuIsOpen && !prevProps.menuIsOpen)
833
    ) {
834
      this.focusInput();
27✔
835
    }
836

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

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

868
  // ==============================
869
  // Consumer Handlers
870
  // ==============================
871

872
  onMenuOpen() {
873
    this.props.onMenuOpen();
36✔
874
  }
875
  onMenuClose() {
876
    this.onInputChange('', {
95✔
877
      action: 'menu-close',
878
      prevInputValue: this.props.inputValue,
879
    });
880

881
    this.props.onMenuClose();
95✔
882
  }
883
  onInputChange(newValue: string, actionMeta: InputActionMeta) {
884
    this.props.onInputChange(newValue, actionMeta);
195✔
885
  }
886

887
  // ==============================
888
  // Methods
889
  // ==============================
890

891
  focusInput() {
892
    if (!this.inputRef) return;
79!
893
    this.inputRef.focus();
79✔
894
  }
895
  blurInput() {
896
    if (!this.inputRef) return;
50!
897
    this.inputRef.blur();
50✔
898
  }
899

900
  // aliased for consumers
901
  focus = this.focusInput;
256✔
902
  blur = this.blurInput;
256✔
903

904
  openMenu(focusOption: 'first' | 'last') {
905
    const { selectValue, isFocused } = this.state;
28✔
906
    const focusableOptions = this.buildFocusableOptions();
28✔
907
    let openAtIndex = focusOption === 'first' ? 0 : focusableOptions.length - 1;
28!
908

909
    if (!this.props.isMulti) {
28✔
910
      const selectedIndex = focusableOptions.indexOf(selectValue[0]);
13✔
911
      if (selectedIndex > -1) {
13!
912
        openAtIndex = selectedIndex;
×
913
      }
914
    }
915

916
    // only scroll if the menu isn't already open
917
    this.scrollToFocusedOptionOnUpdate = !(isFocused && this.menuListRef);
28!
918

919
    this.setState(
28✔
920
      {
921
        inputIsHiddenAfterUpdate: false,
922
        focusedValue: null,
923
        focusedOption: focusableOptions[openAtIndex],
924
        focusedOptionId: this.getFocusedOptionId(focusableOptions[openAtIndex]),
925
      },
926
      () => this.onMenuOpen()
28✔
927
    );
928
  }
929

930
  focusValue(direction: 'previous' | 'next') {
931
    const { selectValue, focusedValue } = this.state;
2✔
932

933
    // Only multiselects support value focusing
934
    if (!this.props.isMulti) return;
2!
935

936
    this.setState({
2✔
937
      focusedOption: null,
938
    });
939

940
    let focusedIndex = selectValue.indexOf(focusedValue!);
2✔
941
    if (!focusedValue) {
2✔
942
      focusedIndex = -1;
1✔
943
    }
944

945
    const lastIndex = selectValue.length - 1;
2✔
946
    let nextFocus = -1;
2✔
947
    if (!selectValue.length) return;
2!
948

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

973
  focusOption(direction: FocusDirection = 'first') {
×
974
    const { pageSize } = this.props;
302✔
975
    const { focusedOption } = this.state;
302✔
976
    const options = this.getFocusableOptions();
302✔
977

978
    if (!options.length) return;
302!
979
    let nextFocus = 0; // handles 'first'
302✔
980
    let focusedIndex = options.indexOf(focusedOption!);
302✔
981
    if (!focusedOption) {
302✔
982
      focusedIndex = -1;
44✔
983
    }
984

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

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

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

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

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

1105
    if (lastSelectedValue) {
3✔
1106
      this.onChange(newValue, {
2✔
1107
        action: 'pop-value',
1108
        removedValue: lastSelectedValue,
1109
      });
1110
    }
1111
  };
1112

1113
  // ==============================
1114
  // Getters
1115
  // ==============================
1116

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

1136
  getFocusedOptionId = (focusedOption: Option) => {
256✔
1137
    return getFocusedOptionId(
336✔
1138
      this.state.focusableOptionsWithIds,
1139
      focusedOption
1140
    );
1141
  };
1142

1143
  getFocusableOptionsWithIds = () => {
256✔
1144
    return buildFocusableOptionsWithIds(
21✔
1145
      buildCategorizedOptions(this.props, this.state.selectValue),
1146
      this.getElementId('option')
1147
    );
1148
  };
1149

1150
  getValue = () => this.state.selectValue;
256✔
1151

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

1154
  getCommonProps() {
1155
    const {
1156
      clearValue,
1157
      cx,
1158
      getStyles,
1159
      getClassNames,
1160
      getValue,
1161
      selectOption,
1162
      setValue,
1163
      props,
1164
    } = this;
825✔
1165
    const { isMulti, isRtl, options } = props;
825✔
1166
    const hasValue = this.hasValue();
825✔
1167

1168
    return {
825✔
1169
      clearValue,
1170
      cx,
1171
      getStyles,
1172
      getClassNames,
1173
      getValue,
1174
      hasValue,
1175
      isMulti,
1176
      isRtl,
1177
      options,
1178
      selectOption,
1179
      selectProps: props,
1180
      setValue,
1181
      theme: this.getTheme(),
1182
    };
1183
  }
1184

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

1217
  getComponents = () => {
256✔
1218
    return defaultComponents(this.props);
6,600✔
1219
  };
1220

1221
  buildCategorizedOptions = () =>
256✔
1222
    buildCategorizedOptions(this.props, this.state.selectValue);
2,128✔
1223
  getCategorizedOptions = () =>
256✔
1224
    this.props.menuIsOpen ? this.buildCategorizedOptions() : [];
565!
1225
  buildFocusableOptions = () =>
256✔
1226
    buildFocusableOptionsFromCategorizedOptions(this.buildCategorizedOptions());
1,563✔
1227
  getFocusableOptions = () =>
256✔
1228
    this.props.menuIsOpen ? this.buildFocusableOptions() : [];
1,736✔
1229

1230
  // ==============================
1231
  // Helpers
1232
  // ==============================
1233

1234
  ariaOnChange = (
256✔
1235
    value: OnChangeValue<Option, IsMulti>,
1236
    actionMeta: ActionMeta<Option>
1237
  ) => {
1238
    this.setState({ ariaSelection: { value, ...actionMeta } });
61✔
1239
  };
1240

1241
  hasValue() {
1242
    const { selectValue } = this.state;
2,850✔
1243
    return selectValue.length > 0;
2,850✔
1244
  }
1245
  hasOptions() {
1246
    return !!this.getFocusableOptions().length;
603✔
1247
  }
1248
  isClearable(): boolean {
1249
    const { isClearable, isMulti } = this.props;
825✔
1250

1251
    // single select, by default, IS NOT clearable
1252
    // multi select, by default, IS clearable
1253
    if (isClearable === undefined) return isMulti;
825✔
1254

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

1286
  // ==============================
1287
  // Mouse Handlers
1288
  // ==============================
1289

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

1390
  // ==============================
1391
  // Composition Handlers
1392
  // ==============================
1393

1394
  startListeningComposition() {
1395
    if (document && document.addEventListener) {
256!
1396
      document.addEventListener(
256✔
1397
        'compositionstart',
1398
        this.onCompositionStart,
1399
        false
1400
      );
1401
      document.addEventListener('compositionend', this.onCompositionEnd, false);
256✔
1402
    }
1403
  }
1404
  stopListeningComposition() {
1405
    if (document && document.removeEventListener) {
256!
1406
      document.removeEventListener('compositionstart', this.onCompositionStart);
256✔
1407
      document.removeEventListener('compositionend', this.onCompositionEnd);
256✔
1408
    }
1409
  }
1410
  onCompositionStart = () => {
256✔
1411
    this.isComposing = true;
×
1412
  };
1413
  onCompositionEnd = () => {
256✔
1414
    this.isComposing = false;
×
1415
  };
1416

1417
  // ==============================
1418
  // Touch Handlers
1419
  // ==============================
1420

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

1441
    this.initialTouchX = touch.clientX;
×
1442
    this.initialTouchY = touch.clientY;
×
1443
    this.userIsDragging = false;
×
1444
  };
1445
  onTouchMove = ({ touches }: TouchEvent) => {
256✔
1446
    const touch = touches && touches.item(0);
×
1447
    if (!touch) {
×
1448
      return;
×
1449
    }
1450

1451
    const deltaX = Math.abs(touch.clientX - this.initialTouchX);
×
1452
    const deltaY = Math.abs(touch.clientY - this.initialTouchY);
×
1453
    const moveThreshold = 5;
×
1454

1455
    this.userIsDragging = deltaX > moveThreshold || deltaY > moveThreshold;
×
1456
  };
1457
  onTouchEnd = (event: TouchEvent) => {
256✔
1458
    if (this.userIsDragging) return;
×
1459

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

1472
    // reset move vars
1473
    this.initialTouchX = 0;
×
1474
    this.initialTouchY = 0;
×
1475
  };
1476
  onControlTouchEnd: TouchEventHandler<HTMLDivElement> = (event) => {
256✔
1477
    if (this.userIsDragging) return;
×
1478
    this.onControlMouseDown(event);
×
1479
  };
1480
  onClearIndicatorTouchEnd: TouchEventHandler<HTMLDivElement> = (event) => {
256✔
1481
    if (this.userIsDragging) return;
×
1482

1483
    this.onClearIndicatorMouseDown(event);
×
1484
  };
1485
  onDropdownIndicatorTouchEnd: TouchEventHandler<HTMLDivElement> = (event) => {
256✔
1486
    if (this.userIsDragging) return;
×
1487

1488
    this.onDropdownIndicatorMouseDown(event);
×
1489
  };
1490

1491
  // ==============================
1492
  // Focus Handlers
1493
  // ==============================
1494

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

1549
  // If the hidden input gets focus through form submit,
1550
  // redirect focus to focusable input.
1551
  onValueInputFocus: FocusEventHandler = (e) => {
256✔
1552
    e.preventDefault();
×
1553
    e.stopPropagation();
×
1554

1555
    this.focus();
×
1556
  };
1557

1558
  // ==============================
1559
  // Keyboard Handlers
1560
  // ==============================
1561

1562
  onKeyDown: KeyboardEventHandler<HTMLDivElement> = (event) => {
256✔
1563
    const {
1564
      isMulti,
1565
      backspaceRemovesValue,
1566
      escapeClearsValue,
1567
      inputValue,
1568
      isClearable,
1569
      isDisabled,
1570
      menuIsOpen,
1571
      onKeyDown,
1572
      tabSelectsValue,
1573
      openMenuOnFocus,
1574
    } = this.props;
368✔
1575
    const { focusedOption, focusedValue, selectValue } = this.state;
368✔
1576

1577
    if (isDisabled) return;
368!
1578

1579
    if (typeof onKeyDown === 'function') {
368!
1580
      onKeyDown(event);
×
1581
      if (event.defaultPrevented) {
×
1582
        return;
×
1583
      }
1584
    }
1585

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

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

1701
  // ==============================
1702
  // Renderers
1703
  // ==============================
1704
  renderInput() {
1705
    const {
1706
      isDisabled,
1707
      isSearchable,
1708
      inputId,
1709
      inputValue,
1710
      tabIndex,
1711
      form,
1712
      menuIsOpen,
1713
      required,
1714
    } = this.props;
825✔
1715
    const { Input } = this.getComponents();
825✔
1716
    const { inputIsHidden, ariaSelection } = this.state;
825✔
1717
    const { commonProps } = this;
825✔
1718

1719
    const id = inputId || this.getElementId('input');
825✔
1720

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

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

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

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

1811
    if (!this.hasValue() || !controlShouldRenderValue) {
825✔
1812
      return inputValue ? null : (
651✔
1813
        <Placeholder
1814
          {...commonProps}
1815
          key="placeholder"
1816
          isDisabled={isDisabled}
1817
          isFocused={isFocused}
1818
          innerProps={{ id: this.getElementId('placeholder') }}
1819
        >
1820
          {placeholder}
1821
        </Placeholder>
1822
      );
1823
    }
1824

1825
    if (isMulti) {
174✔
1826
      return selectValue.map((opt, index) => {
98✔
1827
        const isOptionFocused = opt === focusedValue;
119✔
1828
        const key = `${this.getOptionLabel(opt)}-${this.getOptionValue(opt)}`;
119✔
1829

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

1857
    if (inputValue) {
76✔
1858
      return null;
10✔
1859
    }
1860

1861
    const singleValue = selectValue[0];
66✔
1862
    return (
66✔
1863
      <SingleValue {...commonProps} data={singleValue} isDisabled={isDisabled}>
1864
        {this.formatOptionLabel(singleValue, 'value')}
1865
      </SingleValue>
1866
    );
1867
  }
1868
  renderClearIndicator() {
1869
    const { ClearIndicator } = this.getComponents();
825✔
1870
    const { commonProps } = this;
825✔
1871
    const { isDisabled, isLoading } = this.props;
825✔
1872
    const { isFocused } = this.state;
825✔
1873

1874
    if (
825✔
1875
      !this.isClearable() ||
2,029✔
1876
      !ClearIndicator ||
1877
      isDisabled ||
1878
      !this.hasValue() ||
1879
      isLoading
1880
    ) {
1881
      return null;
718✔
1882
    }
1883

1884
    const innerProps = {
107✔
1885
      onMouseDown: this.onClearIndicatorMouseDown,
1886
      onTouchEnd: this.onClearIndicatorTouchEnd,
1887
      'aria-hidden': 'true',
1888
    };
1889

1890
    return (
107✔
1891
      <ClearIndicator
1892
        {...commonProps}
1893
        innerProps={innerProps}
1894
        isFocused={isFocused}
1895
      />
1896
    );
1897
  }
1898
  renderLoadingIndicator() {
1899
    const { LoadingIndicator } = this.getComponents();
825✔
1900
    const { commonProps } = this;
825✔
1901
    const { isDisabled, isLoading } = this.props;
825✔
1902
    const { isFocused } = this.state;
825✔
1903

1904
    if (!LoadingIndicator || !isLoading) return null;
825✔
1905

1906
    const innerProps = { 'aria-hidden': 'true' };
10✔
1907
    return (
10✔
1908
      <LoadingIndicator
1909
        {...commonProps}
1910
        innerProps={innerProps}
1911
        isDisabled={isDisabled}
1912
        isFocused={isFocused}
1913
      />
1914
    );
1915
  }
1916
  renderIndicatorSeparator() {
1917
    const { DropdownIndicator, IndicatorSeparator } = this.getComponents();
825✔
1918

1919
    // separator doesn't make sense without the dropdown indicator
1920
    if (!DropdownIndicator || !IndicatorSeparator) return null;
825!
1921

1922
    const { commonProps } = this;
825✔
1923
    const { isDisabled } = this.props;
825✔
1924
    const { isFocused } = this.state;
825✔
1925

1926
    return (
825✔
1927
      <IndicatorSeparator
1928
        {...commonProps}
1929
        isDisabled={isDisabled}
1930
        isFocused={isFocused}
1931
      />
1932
    );
1933
  }
1934
  renderDropdownIndicator() {
1935
    const { DropdownIndicator } = this.getComponents();
825✔
1936
    if (!DropdownIndicator) return null;
825!
1937
    const { commonProps } = this;
825✔
1938
    const { isDisabled } = this.props;
825✔
1939
    const { isFocused } = this.state;
825✔
1940

1941
    const innerProps = {
825✔
1942
      onMouseDown: this.onDropdownIndicatorMouseDown,
1943
      onTouchEnd: this.onDropdownIndicatorTouchEnd,
1944
      'aria-hidden': 'true',
1945
    };
1946

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

1987
    if (!menuIsOpen) return null;
825✔
1988

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

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

2025
    let menuUI: ReactNode;
2026

2027
    if (this.hasOptions()) {
603✔
2028
      menuUI = this.getCategorizedOptions().map((item) => {
565✔
2029
        if (item.type === 'group') {
8,439✔
2030
          const { data, options, index: groupIndex } = item;
30✔
2031
          const groupId = `${this.getElementId('group')}-${groupIndex}`;
30✔
2032
          const headingId = `${groupId}-heading`;
30✔
2033

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

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

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

2139
    if (required && !this.hasValue() && !isDisabled) {
825✔
2140
      return <RequiredInput name={name} onFocus={this.onValueInputFocus} />;
6✔
2141
    }
2142

2143
    if (!name || isDisabled) return;
819✔
2144

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

2166
        return <div>{input}</div>;
332✔
2167
      }
2168
    } else {
2169
      const value = selectValue[0] ? this.getOptionValue(selectValue[0]) : '';
409✔
2170
      return <input name={name} type="hidden" value={value} />;
409✔
2171
    }
2172
  }
2173

2174
  renderLiveRegion() {
2175
    const { commonProps } = this;
825✔
2176
    const {
2177
      ariaSelection,
2178
      focusedOption,
2179
      focusedValue,
2180
      isFocused,
2181
      selectValue,
2182
    } = this.state;
825✔
2183

2184
    const focusableOptions = this.getFocusableOptions();
825✔
2185

2186
    return (
825✔
2187
      <LiveRegion
2188
        {...commonProps}
2189
        id={this.getElementId('live-region')}
2190
        ariaSelection={ariaSelection}
2191
        focusedOption={focusedOption}
2192
        focusedValue={focusedValue}
2193
        isFocused={isFocused}
2194
        selectValue={selectValue}
2195
        focusableOptions={focusableOptions}
2196
        isAppleDevice={this.state.isAppleDevice}
2197
      />
2198
    );
2199
  }
2200

2201
  render() {
2202
    const { Control, IndicatorsContainer, SelectContainer, ValueContainer } =
2203
      this.getComponents();
825✔
2204

2205
    const { className, id, isDisabled, menuIsOpen } = this.props;
825✔
2206
    const { isFocused } = this.state;
825✔
2207
    const commonProps = (this.commonProps = this.getCommonProps());
825✔
2208

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

2250
export type PublicBaseSelectProps<
2251
  Option,
2252
  IsMulti extends boolean,
2253
  Group extends GroupBase<Option>
2254
> = 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