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

JedWatson / react-select / dfbda7c9-f865-42e7-9726-48c679f6fc87

10 Jan 2024 12:10AM CUT coverage: 76.25% (+0.4%) from 75.827%
dfbda7c9-f865-42e7-9726-48c679f6fc87

Pull #5842

circleci

itsdouges
chore: fix tests
Pull Request #5842: Fix event handling with react-beautiful-dnd

663 of 1046 branches covered (0.0%)

1037 of 1360 relevant lines covered (76.25%)

1939.0 hits per line

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

88.54
/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,321✔
381
  const isSelected = isOptionSelected(props, option, selectValue);
37,321✔
382
  const label = getOptionLabel(props, option);
37,321✔
383
  const value = getOptionValue(props, option);
37,321✔
384

385
  return {
37,321✔
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,375✔
405
    .map((groupOrOption, groupOrOptionIndex) => {
406
      if ('options' in groupOrOption) {
36,428✔
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,255✔
422
        props,
423
        groupOrOption,
424
        selectValue,
425
        groupOrOptionIndex
426
      );
427
      return isFocusable(props, categorizedOption)
36,255✔
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,677✔
439
    (optionsAccumulator, categorizedOption) => {
440
      if (categorizedOption.type === 'group') {
23,551✔
441
        optionsAccumulator.push(
88✔
442
          ...categorizedOption.options.map((option) => option.data)
492✔
443
        );
444
      } else {
445
        optionsAccumulator.push(categorizedOption.data);
23,463✔
446
      }
447
      return optionsAccumulator;
23,551✔
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,321!
497
  const { data, isSelected, label, value } = categorizedOption;
37,321✔
498

499
  return (
37,321✔
500
    (!shouldHideSelectedOptions(props) || !isSelected) &&
88,899✔
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(
513✔
542
    (option) => option.data === focusedOption
630✔
543
  )?.id;
544
  return focusedOptionId || null;
513✔
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,199✔
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,496✔
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,374!
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,348✔
591
  if (typeof props.isOptionSelected === 'function') {
37,032✔
592
    return props.isOptionSelected(option, selectValue);
102✔
593
  }
594
  const candidate = getOptionValue(props, option);
36,930✔
595
  return selectValue.some((i) => getOptionValue(props, i) === candidate);
36,930✔
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,214✔
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,321✔
617
  if (hideSelectedOptions === undefined) return isMulti;
37,321✔
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> = {
255✔
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;
255✔
652
  isComposing = false;
255✔
653
  commonProps: any; // TODO
654
  initialTouchX = 0;
255✔
655
  initialTouchY = 0;
255✔
656
  openAfterFocus = false;
255✔
657
  scrollToFocusedOptionOnUpdate = false;
255✔
658
  userIsDragging?: boolean;
659
  isAppleDevice = isAppleDevice();
255✔
660

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

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

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

684
  constructor(props: Props<Option, IsMulti, Group>) {
685
    super(props);
255✔
686
    this.state.instancePrefix =
255✔
687
      'react-select-' + (this.props.instanceId || ++instanceId);
504✔
688
    this.state.selectValue = cleanValue(props.value);
255✔
689
    // Set focusedOption if menuIsOpen is set on init (e.g. defaultMenuIsOpen)
690
    if (props.menuIsOpen && this.state.selectValue.length) {
255✔
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,876✔
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();
255✔
797
    this.startListeningToTouch();
255✔
798

799
    if (this.props.closeMenuOnScroll && document && document.addEventListener) {
255!
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) {
255✔
805
      this.focusInput();
5✔
806
    }
807

808
    // Scroll focusedOption into view if menuIsOpen is set on mount (e.g. defaultMenuIsOpen)
809
    if (
255✔
810
      this.props.menuIsOpen &&
403✔
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;
570✔
820
    const { isFocused } = this.state;
570✔
821

822
    if (
570✔
823
      // ensure focus is restored correctly when the control becomes enabled
824
      (isFocused && !isDisabled && prevProps.isDisabled) ||
1,674✔
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) {
570!
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 (
570!
836
      !isFocused &&
1,420!
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 (
570✔
848
      this.menuListRef &&
1,470✔
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();
255✔
858
    this.stopListeningToTouch();
255✔
859
    document.removeEventListener('scroll', this.onScroll, true);
255✔
860
  }
861

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

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

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

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

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

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

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

903
    if (!this.props.isMulti) {
30✔
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);
30!
912

913
    this.setState(
30✔
914
      {
915
        inputIsHiddenAfterUpdate: false,
916
        focusedValue: null,
917
        focusedOption: focusableOptions[openAtIndex],
918
        focusedOptionId: this.getFocusedOptionId(focusableOptions[openAtIndex]),
919
      },
920
      () => this.onMenuOpen()
30✔
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 = (
255✔
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 = (
255✔
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) => {
255✔
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) => {
255✔
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 = () => {
255✔
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 = () => {
255✔
1089
    const { isMulti } = this.props;
2✔
1090
    const { selectValue } = this.state;
2✔
1091
    const lastSelectedValue = selectValue[selectValue.length - 1];
2✔
1092
    const newValueArray = selectValue.slice(0, selectValue.length - 1);
2✔
1093
    const newValue = valueTernary(
2✔
1094
      isMulti,
1095
      newValueArray,
1096
      newValueArray[0] || null
3✔
1097
    );
1098

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

1105
  // ==============================
1106
  // Getters
1107
  // ==============================
1108

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

1128
  getFocusedOptionId = (focusedOption: Option) => {
255✔
1129
    return getFocusedOptionId(
338✔
1130
      this.state.focusableOptionsWithIds,
1131
      focusedOption
1132
    );
1133
  };
1134

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

1142
  getValue = () => this.state.selectValue;
255✔
1143

1144
  cx = (...args: any) => classNames(this.props.classNamePrefix, ...args);
17,847✔
1145

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

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

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

1209
  getComponents = () => {
255✔
1210
    return defaultComponents(this.props);
6,600✔
1211
  };
1212

1213
  buildCategorizedOptions = () =>
255✔
1214
    buildCategorizedOptions(this.props, this.state.selectValue);
2,130✔
1215
  getCategorizedOptions = () =>
255✔
1216
    this.props.menuIsOpen ? this.buildCategorizedOptions() : [];
565!
1217
  buildFocusableOptions = () =>
255✔
1218
    buildFocusableOptionsFromCategorizedOptions(this.buildCategorizedOptions());
1,565✔
1219
  getFocusableOptions = () =>
255✔
1220
    this.props.menuIsOpen ? this.buildFocusableOptions() : [];
1,736✔
1221

1222
  // ==============================
1223
  // Helpers
1224
  // ==============================
1225

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

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

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

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

1278
  // ==============================
1279
  // Mouse Handlers
1280
  // ==============================
1281

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

1379
  // ==============================
1380
  // Composition Handlers
1381
  // ==============================
1382

1383
  startListeningComposition() {
1384
    if (document && document.addEventListener) {
255!
1385
      document.addEventListener(
255✔
1386
        'compositionstart',
1387
        this.onCompositionStart,
1388
        false
1389
      );
1390
      document.addEventListener('compositionend', this.onCompositionEnd, false);
255✔
1391
    }
1392
  }
1393
  stopListeningComposition() {
1394
    if (document && document.removeEventListener) {
255!
1395
      document.removeEventListener('compositionstart', this.onCompositionStart);
255✔
1396
      document.removeEventListener('compositionend', this.onCompositionEnd);
255✔
1397
    }
1398
  }
1399
  onCompositionStart = () => {
255✔
1400
    this.isComposing = true;
×
1401
  };
1402
  onCompositionEnd = () => {
255✔
1403
    this.isComposing = false;
×
1404
  };
1405

1406
  // ==============================
1407
  // Touch Handlers
1408
  // ==============================
1409

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

1430
    this.initialTouchX = touch.clientX;
×
1431
    this.initialTouchY = touch.clientY;
×
1432
    this.userIsDragging = false;
×
1433
  };
1434
  onTouchMove = ({ touches }: TouchEvent) => {
255✔
1435
    const touch = touches && touches.item(0);
×
1436
    if (!touch) {
×
1437
      return;
×
1438
    }
1439

1440
    const deltaX = Math.abs(touch.clientX - this.initialTouchX);
×
1441
    const deltaY = Math.abs(touch.clientY - this.initialTouchY);
×
1442
    const moveThreshold = 5;
×
1443

1444
    this.userIsDragging = deltaX > moveThreshold || deltaY > moveThreshold;
×
1445
  };
1446
  onTouchEnd = (event: TouchEvent) => {
255✔
1447
    if (this.userIsDragging) return;
×
1448

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

1461
    // reset move vars
1462
    this.initialTouchX = 0;
×
1463
    this.initialTouchY = 0;
×
1464
  };
1465
  onControlTouchEnd: TouchEventHandler<HTMLDivElement> = (event) => {
255✔
1466
    if (this.userIsDragging) return;
×
1467
    this.onControlMouseDown(event);
×
1468
  };
1469
  onClearIndicatorTouchEnd: TouchEventHandler<HTMLDivElement> = (event) => {
255✔
1470
    if (this.userIsDragging) return;
×
1471

1472
    this.onClearIndicatorMouseDown(event);
×
1473
  };
1474
  onDropdownIndicatorTouchEnd: TouchEventHandler<HTMLDivElement> = (event) => {
255✔
1475
    if (this.userIsDragging) return;
×
1476

1477
    this.onDropdownIndicatorMouseDown(event);
×
1478
  };
1479

1480
  // ==============================
1481
  // Focus Handlers
1482
  // ==============================
1483

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

1538
  // If the hidden input gets focus through form submit,
1539
  // redirect focus to focusable input.
1540
  onValueInputFocus: FocusEventHandler = (e) => {
255✔
1541
    e.preventDefault();
×
1542
    e.stopPropagation();
×
1543

1544
    this.focus();
×
1545
  };
1546

1547
  // ==============================
1548
  // Keyboard Handlers
1549
  // ==============================
1550

1551
  onKeyDown: KeyboardEventHandler<HTMLDivElement> = (event) => {
255✔
1552
    const {
1553
      isMulti,
1554
      backspaceRemovesValue,
1555
      escapeClearsValue,
1556
      inputValue,
1557
      isClearable,
1558
      isDisabled,
1559
      menuIsOpen,
1560
      onKeyDown,
1561
      tabSelectsValue,
1562
      openMenuOnFocus,
1563
    } = this.props;
367✔
1564
    const { focusedOption, focusedValue, selectValue } = this.state;
367✔
1565

1566
    if (isDisabled) return;
367!
1567

1568
    if (typeof onKeyDown === 'function') {
367!
1569
      onKeyDown(event);
×
1570
      if (event.defaultPrevented) {
×
1571
        return;
×
1572
      }
1573
    }
1574

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

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

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

1708
    const id = inputId || this.getElementId('input');
825✔
1709

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

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

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

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

1800
    if (!this.hasValue() || !controlShouldRenderValue) {
825✔
1801
      return inputValue ? null : (
652✔
1802
        <Placeholder
1803
          {...commonProps}
1804
          key="placeholder"
1805
          isDisabled={isDisabled}
1806
          isFocused={isFocused}
1807
          innerProps={{ id: this.getElementId('placeholder') }}
1808
        >
1809
          {placeholder}
1810
        </Placeholder>
1811
      );
1812
    }
1813

1814
    if (isMulti) {
173✔
1815
      return selectValue.map((opt, index) => {
97✔
1816
        const isOptionFocused = opt === focusedValue;
120✔
1817
        const key = `${this.getOptionLabel(opt)}-${this.getOptionValue(opt)}`;
120✔
1818

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

1846
    if (inputValue) {
76✔
1847
      return null;
10✔
1848
    }
1849

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

1863
    if (
825✔
1864
      !this.isClearable() ||
2,028✔
1865
      !ClearIndicator ||
1866
      isDisabled ||
1867
      !this.hasValue() ||
1868
      isLoading
1869
    ) {
1870
      return null;
719✔
1871
    }
1872

1873
    const innerProps = {
106✔
1874
      onMouseDown: this.onClearIndicatorMouseDown,
1875
      onTouchEnd: this.onClearIndicatorTouchEnd,
1876
      'aria-hidden': 'true',
1877
    };
1878

1879
    return (
106✔
1880
      <ClearIndicator
1881
        {...commonProps}
1882
        innerProps={innerProps}
1883
        isFocused={isFocused}
1884
      />
1885
    );
1886
  }
1887
  renderLoadingIndicator() {
1888
    const { LoadingIndicator } = this.getComponents();
825✔
1889
    const { commonProps } = this;
825✔
1890
    const { isDisabled, isLoading } = this.props;
825✔
1891
    const { isFocused } = this.state;
825✔
1892

1893
    if (!LoadingIndicator || !isLoading) return null;
825✔
1894

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

1908
    // separator doesn't make sense without the dropdown indicator
1909
    if (!DropdownIndicator || !IndicatorSeparator) return null;
825!
1910

1911
    const { commonProps } = this;
825✔
1912
    const { isDisabled } = this.props;
825✔
1913
    const { isFocused } = this.state;
825✔
1914

1915
    return (
825✔
1916
      <IndicatorSeparator
1917
        {...commonProps}
1918
        isDisabled={isDisabled}
1919
        isFocused={isFocused}
1920
      />
1921
    );
1922
  }
1923
  renderDropdownIndicator() {
1924
    const { DropdownIndicator } = this.getComponents();
825✔
1925
    if (!DropdownIndicator) return null;
825!
1926
    const { commonProps } = this;
825✔
1927
    const { isDisabled } = this.props;
825✔
1928
    const { isFocused } = this.state;
825✔
1929

1930
    const innerProps = {
825✔
1931
      onMouseDown: this.onDropdownIndicatorMouseDown,
1932
      onTouchEnd: this.onDropdownIndicatorTouchEnd,
1933
      'aria-hidden': 'true',
1934
    };
1935

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

1976
    if (!menuIsOpen) return null;
825✔
1977

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

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

2014
    let menuUI: ReactNode;
2015

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

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

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

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

2128
    if (required && !this.hasValue() && !isDisabled) {
825✔
2129
      return <RequiredInput name={name} onFocus={this.onValueInputFocus} />;
6✔
2130
    }
2131

2132
    if (!name || isDisabled) return;
819✔
2133

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

2155
        return <div>{input}</div>;
332✔
2156
      }
2157
    } else {
2158
      const value = selectValue[0] ? this.getOptionValue(selectValue[0]) : '';
409✔
2159
      return <input name={name} type="hidden" value={value} />;
409✔
2160
    }
2161
  }
2162

2163
  renderLiveRegion() {
2164
    const { commonProps } = this;
825✔
2165
    const {
2166
      ariaSelection,
2167
      focusedOption,
2168
      focusedValue,
2169
      isFocused,
2170
      selectValue,
2171
    } = this.state;
825✔
2172

2173
    const focusableOptions = this.getFocusableOptions();
825✔
2174

2175
    return (
825✔
2176
      <LiveRegion
2177
        {...commonProps}
2178
        id={this.getElementId('live-region')}
2179
        ariaSelection={ariaSelection}
2180
        focusedOption={focusedOption}
2181
        focusedValue={focusedValue}
2182
        isFocused={isFocused}
2183
        selectValue={selectValue}
2184
        focusableOptions={focusableOptions}
2185
        isAppleDevice={this.isAppleDevice}
2186
      />
2187
    );
2188
  }
2189

2190
  render() {
2191
    const { Control, IndicatorsContainer, SelectContainer, ValueContainer } =
2192
      this.getComponents();
825✔
2193

2194
    const { className, id, isDisabled, menuIsOpen } = this.props;
825✔
2195
    const { isFocused } = this.state;
825✔
2196
    const commonProps = (this.commonProps = this.getCommonProps());
825✔
2197

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

2239
export type PublicBaseSelectProps<
2240
  Option,
2241
  IsMulti extends boolean,
2242
  Group extends GroupBase<Option>
2243
> = 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