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

JedWatson / react-select / 36c10f1a-e614-479e-8065-a6ee4ffe0d2e

11 Dec 2024 10:57PM UTC coverage: 75.844%. Remained the same
36c10f1a-e614-479e-8065-a6ee4ffe0d2e

Pull #5984

circleci

web-flow
Create five-ligers-beg.md
Pull Request #5984: Add peer version to include 19

658 of 1052 branches covered (62.55%)

1033 of 1362 relevant lines covered (75.84%)

1934.69 hits per line

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

87.62
/packages/react-select/src/Select.tsx
1
import * as React from 'react';
2
import {
3
  AriaAttributes,
4
  Component,
5
  FocusEventHandler,
6
  FormEventHandler,
7
  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
}
344

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

622
let instanceId = 1;
5✔
623

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

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

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

662
  // Refs
663
  // ------------------------------
664

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

682
  // Lifecycle
683
  // ------------------------------
684

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

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

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

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

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

766
    let newAriaSelection = ariaSelection;
825✔
767

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

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

779
      hasKeptFocus = !prevWasFocused;
58✔
780
    }
781

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

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

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

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

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

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

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

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

863
  // ==============================
864
  // Consumer Handlers
865
  // ==============================
866

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

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

882
  // ==============================
883
  // Methods
884
  // ==============================
885

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1108
  // ==============================
1109
  // Getters
1110
  // ==============================
1111

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

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

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

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

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

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

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

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

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

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

1225
  // ==============================
1226
  // Helpers
1227
  // ==============================
1228

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

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

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

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

1281
  // ==============================
1282
  // Mouse Handlers
1283
  // ==============================
1284

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

1385
  // ==============================
1386
  // Composition Handlers
1387
  // ==============================
1388

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

1412
  // ==============================
1413
  // Touch Handlers
1414
  // ==============================
1415

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

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

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

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

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

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

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

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

1486
  // ==============================
1487
  // Focus Handlers
1488
  // ==============================
1489

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

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

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

1553
  // ==============================
1554
  // Keyboard Handlers
1555
  // ==============================
1556

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

1572
    if (isDisabled) return;
368!
1573

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2020
    let menuUI: ReactNode;
2021

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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