芝麻web文件管理V1.00
编辑当前文件:/home/freeclou/app.optimyar.com/backend/node_modules/react-select/src/Select.js
// @flow import React, { Component, type ElementRef, type Node } from 'react'; import { MenuPlacer } from './components/Menu'; import LiveRegion from './components/LiveRegion'; import { createFilter } from './filters'; import { DummyInput, ScrollManager } from './internal/index'; import { type AriaLiveProp, type AriaLiveMessagesProps, type AriaSelectionType, } from './accessibility/index'; import { classNames, cleanValue, isTouchCapable, isMobileDevice, noop, scrollIntoView, isDocumentElement, } from './utils'; import { formatGroupLabel as formatGroupLabelBuiltin, getOptionLabel as getOptionLabelBuiltin, getOptionValue as getOptionValueBuiltin, isOptionDisabled as isOptionDisabledBuiltin, } from './builtins'; import { defaultComponents, type PlaceholderOrValue, type SelectComponentsConfig, } from './components/index'; import { defaultStyles, type StylesConfig } from './styles'; import { defaultTheme, type ThemeConfig } from './theme'; import type { ActionMeta, ActionTypes, FocusDirection, FocusEventHandler, GroupType, InputActionMeta, KeyboardEventHandler, MenuPlacement, MenuPosition, OptionsType, OptionType, ValueType, } from './types'; type MouseOrTouchEvent = | SyntheticMouseEvent
| SyntheticTouchEvent
; type FormatOptionLabelContext = 'menu' | 'value'; type FormatOptionLabelMeta = { context: FormatOptionLabelContext, inputValue: string, selectValue: ValueType, }; export type Props = { /* Aria label (for assistive tech) */ 'aria-label'?: string, /* HTML ID of an element that should be used as the label (for assistive tech) */ 'aria-labelledby'?: string, /* Used to set the priority with which screen reader should treat updates to live regions. The possible settings are: off, polite (default) or assertive */ 'aria-live'?: AriaLiveProp, /* Customize the messages used by the aria-live component */ ariaLiveMessages?: AriaLiveMessagesProps, /* Focus the control when it is mounted */ autoFocus?: boolean, /* Remove the currently focused option when the user presses backspace when Select isClearable or isMulti */ backspaceRemovesValue: boolean, /* Remove focus from the input when the user selects an option (handy for dismissing the keyboard on touch devices) */ blurInputOnSelect: boolean, /* When the user reaches the top/bottom of the menu, prevent scroll on the scroll-parent */ captureMenuScroll: boolean, /* Sets a className attribute on the outer component */ className?: string, /* If provided, all inner components will be given a prefixed className attribute. This is useful when styling via CSS classes instead of the Styles API approach. */ classNamePrefix?: string | null, /* Close the select menu when the user selects an option */ closeMenuOnSelect: boolean, /* If `true`, close the select menu when the user scrolls the document/body. If a function, takes a standard javascript `ScrollEvent` you return a boolean: `true` => The menu closes `false` => The menu stays open This is useful when you have a scrollable modal and want to portal the menu out, but want to avoid graphical issues. */ closeMenuOnScroll: boolean | EventListener, /* This complex object includes all the compositional components that are used in `react-select`. If you wish to overwrite a component, pass in an object with the appropriate namespace. If you only wish to restyle a component, we recommend using the `styles` prop instead. For a list of the components that can be passed in, and the shape that will be passed to them, see [the components docs](/components) */ components: SelectComponentsConfig, /* Whether the value of the select, e.g. SingleValue, should be displayed in the control. */ controlShouldRenderValue: boolean, /* Delimiter used to join multiple values into a single HTML Input value */ delimiter?: string, /* Clear all values when the user presses escape AND the menu is closed */ escapeClearsValue: boolean, /* Custom method to filter whether an option should be displayed in the menu */ filterOption: | (({ label: string, value: string, data: OptionType }, string) => boolean) | null, /* Formats group labels in the menu as React components An example can be found in the [Replacing builtins](/advanced#replacing-builtins) documentation. */ formatGroupLabel: typeof formatGroupLabelBuiltin, /* Formats option labels in the menu and control as React components */ formatOptionLabel?: (OptionType, FormatOptionLabelMeta) => Node, /* Resolves option data to a string to be displayed as the label by components Note: Failure to resolve to a string type can interfere with filtering and screen reader support. */ getOptionLabel: typeof getOptionLabelBuiltin, /* Resolves option data to a string to compare options and specify value attributes */ getOptionValue: typeof getOptionValueBuiltin, /* Hide the selected option from the menu */ hideSelectedOptions?: boolean, /* The id to set on the SelectContainer component. */ id?: string, /* The value of the search input */ inputValue: string, /* The id of the search input */ inputId?: string, /* Define an id prefix for the select components e.g. {your-id}-value */ instanceId?: number | string, /* Is the select value clearable */ isClearable?: boolean, /* Is the select disabled */ isDisabled: boolean, /* Is the select in a state of loading (async) */ isLoading: boolean, /* Override the built-in logic to detect whether an option is disabled An example can be found in the [Replacing builtins](/advanced#replacing-builtins) documentation. */ isOptionDisabled: (OptionType, OptionsType) => boolean | false, /* Override the built-in logic to detect whether an option is selected */ isOptionSelected?: (OptionType, OptionsType) => boolean, /* Support multiple selected options */ isMulti: boolean, /* Is the select direction right-to-left */ isRtl: boolean, /* Whether to enable search functionality */ isSearchable: boolean, /* Async: Text to display when loading options */ loadingMessage: ({ inputValue: string }) => string | null, /* Minimum height of the menu before flipping */ minMenuHeight: number, /* Maximum height of the menu before scrolling */ maxMenuHeight: number, /* Whether the menu is open */ menuIsOpen: boolean, /* Default placement of the menu in relation to the control. 'auto' will flip when there isn't enough space below the control. */ menuPlacement: MenuPlacement, /* The CSS position value of the menu, when "fixed" extra layout management is required */ menuPosition: MenuPosition, /* Whether the menu should use a portal, and where it should attach An example can be found in the [Portaling](/advanced#portaling) documentation */ menuPortalTarget?: HTMLElement, /* Whether to block scroll events when the menu is open */ menuShouldBlockScroll: boolean, /* Whether the menu should be scrolled into view when it opens */ menuShouldScrollIntoView: boolean, /* Name of the HTML Input (optional - without this, no input will be rendered) */ name?: string, /* Text to display when there are no options */ noOptionsMessage: ({ inputValue: string }) => Node | null, /* Handle blur events on the control */ onBlur?: FocusEventHandler, /* Handle change events on the select */ onChange: (ValueType, ActionMeta) => void, /* Handle focus events on the control */ onFocus?: FocusEventHandler, /* Handle change events on the input */ onInputChange: (string, InputActionMeta) => void, /* Handle key down events on the select */ onKeyDown?: KeyboardEventHandler, /* Handle the menu opening */ onMenuOpen: () => void, /* Handle the menu closing */ onMenuClose: () => void, /* Fired when the user scrolls to the top of the menu */ onMenuScrollToTop?: (SyntheticEvent
) => void, /* Fired when the user scrolls to the bottom of the menu */ onMenuScrollToBottom?: (SyntheticEvent
) => void, /* Allows control of whether the menu is opened when the Select is focused */ openMenuOnFocus: boolean, /* Allows control of whether the menu is opened when the Select is clicked */ openMenuOnClick: boolean, /* Array of options that populate the select menu */ options: OptionsType, /* Number of options to jump in menu when page{up|down} keys are used */ pageSize: number, /* Placeholder for the select value */ placeholder: Node, /* Status to relay to screen readers */ screenReaderStatus: ({ count: number }) => string, /* Style modifier methods A basic example can be found at the bottom of the [Replacing builtins](/advanced#replacing-builtins) documentation. */ styles: StylesConfig, /* Theme modifier method */ theme?: ThemeConfig, /* Sets the tabIndex attribute on the input */ tabIndex: number | string, /* Select the currently focused option when the user presses tab */ tabSelectsValue: boolean, /* The value of the select; reflected by the selected option */ value: ValueType, /* Sets the form attribute on the input */ form?: string, }; export const defaultProps = { 'aria-live': 'polite', backspaceRemovesValue: true, blurInputOnSelect: isTouchCapable(), captureMenuScroll: !isTouchCapable(), closeMenuOnSelect: true, closeMenuOnScroll: false, components: {}, controlShouldRenderValue: true, escapeClearsValue: false, filterOption: createFilter(), formatGroupLabel: formatGroupLabelBuiltin, getOptionLabel: getOptionLabelBuiltin, getOptionValue: getOptionValueBuiltin, isDisabled: false, isLoading: false, isMulti: false, isRtl: false, isSearchable: true, isOptionDisabled: isOptionDisabledBuiltin, loadingMessage: () => 'Loading...', maxMenuHeight: 300, minMenuHeight: 140, menuIsOpen: false, menuPlacement: 'bottom', menuPosition: 'absolute', menuShouldBlockScroll: false, menuShouldScrollIntoView: !isMobileDevice(), noOptionsMessage: () => 'No options', openMenuOnFocus: false, openMenuOnClick: true, options: [], pageSize: 5, placeholder: 'Select...', screenReaderStatus: ({ count }: { count: number }) => `${count} result${count !== 1 ? 's' : ''} available`, styles: {}, tabIndex: '0', tabSelectsValue: true, }; type State = { ariaSelection: AriaSelectionType | null, inputIsHidden: boolean, isFocused: boolean, focusedOption: OptionType | null, focusedValue: OptionType | null, selectValue: OptionsType, clearFocusValueOnUpdate: boolean, inputIsHiddenAfterUpdate: ?boolean, prevProps: Props | void, }; type ElRef = ElementRef<*>; type CategorizedOption = { type: 'option', data: OptionType, isDisabled: boolean, isSelected: boolean, label: string, value: string, index: number, }; type CategorizedGroup = { type: 'group', data: GroupType, options: OptionsType, index: number, }; type CategorizedGroupOrOption = CategorizedGroup | CategorizedOption; function toCategorizedOption( props: Props, option: OptionType, selectValue: OptionsType, index: number ) { const isDisabled = isOptionDisabled(props, option, selectValue); const isSelected = isOptionSelected(props, option, selectValue); const label = getOptionLabel(props, option); const value = getOptionValue(props, option); return { type: 'option', data: option, isDisabled, isSelected, label, value, index, }; } function buildCategorizedOptions( props: Props, selectValue: OptionsType ): CategorizedGroupOrOption[] { return (props.options .map((groupOrOption, groupOrOptionIndex) => { if (groupOrOption.options) { const categorizedOptions = groupOrOption.options .map((option, optionIndex) => toCategorizedOption(props, option, selectValue, optionIndex) ) .filter(categorizedOption => isFocusable(props, categorizedOption)); return categorizedOptions.length > 0 ? { type: 'group', data: groupOrOption, options: categorizedOptions, index: groupOrOptionIndex, } : undefined; } const categorizedOption = toCategorizedOption( props, groupOrOption, selectValue, groupOrOptionIndex ); return isFocusable(props, categorizedOption) ? categorizedOption : undefined; }) // Flow limitation (see https://github.com/facebook/flow/issues/1414) .filter(categorizedOption => !!categorizedOption): any[]); } function buildFocusableOptionsFromCategorizedOptions( categorizedOptions: CategorizedGroupOrOption[] ) { return categorizedOptions.reduce((optionsAccumulator, categorizedOption) => { if (categorizedOption.type === 'group') { optionsAccumulator.push( ...categorizedOption.options.map(option => option.data) ); } else { optionsAccumulator.push(categorizedOption.data); } return optionsAccumulator; }, []); } function buildFocusableOptions(props: Props, selectValue: OptionsType) { return buildFocusableOptionsFromCategorizedOptions( buildCategorizedOptions(props, selectValue) ); } function isFocusable(props: Props, categorizedOption: CategorizedOption) { const { inputValue = '' } = props; const { data, isSelected, label, value } = categorizedOption; return ( (!shouldHideSelectedOptions(props) || !isSelected) && filterOption(props, { label, value, data }, inputValue) ); } function getNextFocusedValue(state: State, nextSelectValue: OptionsType) { const { focusedValue, selectValue: lastSelectValue } = state; const lastFocusedIndex = lastSelectValue.indexOf(focusedValue); if (lastFocusedIndex > -1) { const nextFocusedIndex = nextSelectValue.indexOf(focusedValue); if (nextFocusedIndex > -1) { // the focused value is still in the selectValue, return it return focusedValue; } else if (lastFocusedIndex < nextSelectValue.length) { // the focusedValue is not present in the next selectValue array by // reference, so return the new value at the same index return nextSelectValue[lastFocusedIndex]; } } return null; } function getNextFocusedOption(state: State, options: OptionsType) { const { focusedOption: lastFocusedOption } = state; return lastFocusedOption && options.indexOf(lastFocusedOption) > -1 ? lastFocusedOption : options[0]; } const getOptionLabel = (props: Props, data: OptionType): string => { return props.getOptionLabel(data); }; const getOptionValue = (props: Props, data: OptionType): string => { return props.getOptionValue(data); }; function isOptionDisabled( props: Props, option: OptionType, selectValue: OptionsType ): boolean { return typeof props.isOptionDisabled === 'function' ? props.isOptionDisabled(option, selectValue) : false; } function isOptionSelected( props: Props, option: OptionType, selectValue: OptionsType ): boolean { if (selectValue.indexOf(option) > -1) return true; if (typeof props.isOptionSelected === 'function') { return props.isOptionSelected(option, selectValue); } const candidate = getOptionValue(props, option); return selectValue.some(i => getOptionValue(props, i) === candidate); } function filterOption( props: Props, option: { label: string, value: string, data: OptionType }, inputValue: string ) { return props.filterOption ? props.filterOption(option, inputValue) : true; } const shouldHideSelectedOptions = (props: Props) => { const { hideSelectedOptions, isMulti } = props; if (hideSelectedOptions === undefined) return isMulti; return hideSelectedOptions; }; let instanceId = 1; export default class Select extends Component
{ static defaultProps = defaultProps; state = { ariaSelection: null, focusedOption: null, focusedValue: null, inputIsHidden: false, isFocused: false, selectValue: [], clearFocusValueOnUpdate: false, inputIsHiddenAfterUpdate: undefined, prevProps: undefined, }; // Misc. Instance Properties // ------------------------------ blockOptionHover: boolean = false; isComposing: boolean = false; commonProps: any; // TODO initialTouchX: number = 0; initialTouchY: number = 0; instancePrefix: string = ''; openAfterFocus: boolean = false; scrollToFocusedOptionOnUpdate: boolean = false; userIsDragging: ?boolean; // Refs // ------------------------------ controlRef: ElRef = null; getControlRef = (ref: ?HTMLElement) => { this.controlRef = ref; }; focusedOptionRef: ElRef = null; getFocusedOptionRef = (ref: ?HTMLElement) => { this.focusedOptionRef = ref; }; menuListRef: ElRef = null; getMenuListRef = (ref: ?HTMLElement) => { this.menuListRef = ref; }; inputRef: ElRef = null; getInputRef = (ref: ?HTMLElement) => { this.inputRef = ref; }; // Lifecycle // ------------------------------ constructor(props: Props) { super(props); this.instancePrefix = 'react-select-' + (this.props.instanceId || ++instanceId); this.state.selectValue = cleanValue(props.value); } static getDerivedStateFromProps(props: Props, state: State) { const { prevProps, clearFocusValueOnUpdate, inputIsHiddenAfterUpdate, } = state; const { options, value, menuIsOpen, inputValue } = props; let newMenuOptionsState = {}; if ( prevProps && (value !== prevProps.value || options !== prevProps.options || menuIsOpen !== prevProps.menuIsOpen || inputValue !== prevProps.inputValue) ) { const selectValue = cleanValue(value); const focusableOptions = menuIsOpen ? buildFocusableOptions(props, selectValue) : []; const focusedValue = clearFocusValueOnUpdate ? getNextFocusedValue(state, selectValue) : null; const focusedOption = getNextFocusedOption(state, focusableOptions); newMenuOptionsState = { selectValue, focusedOption, focusedValue, clearFocusValueOnUpdate: false, }; } // some updates should toggle the state of the input visibility const newInputIsHiddenState = inputIsHiddenAfterUpdate != null && props !== prevProps ? { inputIsHidden: inputIsHiddenAfterUpdate, inputIsHiddenAfterUpdate: undefined, } : {}; return { ...newMenuOptionsState, ...newInputIsHiddenState, prevProps: props, }; } componentDidMount() { this.startListeningComposition(); this.startListeningToTouch(); if (this.props.closeMenuOnScroll && document && document.addEventListener) { // Listen to all scroll events, and filter them out inside of 'onScroll' document.addEventListener('scroll', this.onScroll, true); } if (this.props.autoFocus) { this.focusInput(); } } componentDidUpdate(prevProps: Props) { const { isDisabled, menuIsOpen } = this.props; const { isFocused } = this.state; if ( // ensure focus is restored correctly when the control becomes enabled (isFocused && !isDisabled && prevProps.isDisabled) || // ensure focus is on the Input when the menu opens (isFocused && menuIsOpen && !prevProps.menuIsOpen) ) { this.focusInput(); } if (isFocused && isDisabled && !prevProps.isDisabled) { // ensure select state gets blurred in case Select is programatically disabled while focused this.setState({ isFocused: false }, this.onMenuClose); } // scroll the focused option into view if necessary if ( this.menuListRef && this.focusedOptionRef && this.scrollToFocusedOptionOnUpdate ) { scrollIntoView(this.menuListRef, this.focusedOptionRef); this.scrollToFocusedOptionOnUpdate = false; } } componentWillUnmount() { this.stopListeningComposition(); this.stopListeningToTouch(); document.removeEventListener('scroll', this.onScroll, true); } // ============================== // Consumer Handlers // ============================== onMenuOpen() { this.props.onMenuOpen(); } onMenuClose() { this.onInputChange('', { action: 'menu-close' }); this.props.onMenuClose(); } onInputChange(newValue: string, actionMeta: InputActionMeta) { this.props.onInputChange(newValue, actionMeta); } // ============================== // Methods // ============================== focusInput() { if (!this.inputRef) return; this.inputRef.focus(); } blurInput() { if (!this.inputRef) return; this.inputRef.blur(); } // aliased for consumers focus = this.focusInput; blur = this.blurInput; openMenu(focusOption: 'first' | 'last') { const { selectValue, isFocused } = this.state; const focusableOptions = this.buildFocusableOptions(); let openAtIndex = focusOption === 'first' ? 0 : focusableOptions.length - 1; if (!this.props.isMulti) { const selectedIndex = focusableOptions.indexOf(selectValue[0]); if (selectedIndex > -1) { openAtIndex = selectedIndex; } } // only scroll if the menu isn't already open this.scrollToFocusedOptionOnUpdate = !(isFocused && this.menuListRef); this.setState( { inputIsHiddenAfterUpdate: false, focusedValue: null, focusedOption: focusableOptions[openAtIndex], }, () => this.onMenuOpen() ); } focusValue(direction: 'previous' | 'next') { const { selectValue, focusedValue } = this.state; // Only multiselects support value focusing if (!this.props.isMulti) return; this.setState({ focusedOption: null, }); let focusedIndex = selectValue.indexOf(focusedValue); if (!focusedValue) { focusedIndex = -1; } const lastIndex = selectValue.length - 1; let nextFocus = -1; if (!selectValue.length) return; switch (direction) { case 'previous': if (focusedIndex === 0) { // don't cycle from the start to the end nextFocus = 0; } else if (focusedIndex === -1) { // if nothing is focused, focus the last value first nextFocus = lastIndex; } else { nextFocus = focusedIndex - 1; } break; case 'next': if (focusedIndex > -1 && focusedIndex < lastIndex) { nextFocus = focusedIndex + 1; } break; } this.setState({ inputIsHidden: nextFocus !== -1, focusedValue: selectValue[nextFocus], }); } focusOption(direction: FocusDirection = 'first') { const { pageSize } = this.props; const { focusedOption } = this.state; const options = this.getFocusableOptions(); if (!options.length) return; let nextFocus = 0; // handles 'first' let focusedIndex = options.indexOf(focusedOption); if (!focusedOption) { focusedIndex = -1; } if (direction === 'up') { nextFocus = focusedIndex > 0 ? focusedIndex - 1 : options.length - 1; } else if (direction === 'down') { nextFocus = (focusedIndex + 1) % options.length; } else if (direction === 'pageup') { nextFocus = focusedIndex - pageSize; if (nextFocus < 0) nextFocus = 0; } else if (direction === 'pagedown') { nextFocus = focusedIndex + pageSize; if (nextFocus > options.length - 1) nextFocus = options.length - 1; } else if (direction === 'last') { nextFocus = options.length - 1; } this.scrollToFocusedOptionOnUpdate = true; this.setState({ focusedOption: options[nextFocus], focusedValue: null, }); } onChange = (newValue: ValueType, actionMeta: ActionMeta) => { const { onChange, name } = this.props; actionMeta.name = name; this.ariaOnChange(newValue, actionMeta); onChange(newValue, actionMeta); }; setValue = ( newValue: ValueType, action: ActionTypes = 'set-value', option?: OptionType ) => { const { closeMenuOnSelect, isMulti } = this.props; this.onInputChange('', { action: 'set-value' }); if (closeMenuOnSelect) { this.setState({ inputIsHiddenAfterUpdate: !isMulti }); this.onMenuClose(); } // when the select value should change, we should reset focusedValue this.setState({ clearFocusValueOnUpdate: true }); this.onChange(newValue, { action, option }); }; selectOption = (newValue: OptionType) => { const { blurInputOnSelect, isMulti, name } = this.props; const { selectValue } = this.state; const deselected = isMulti && this.isOptionSelected(newValue, selectValue); const isDisabled = this.isOptionDisabled(newValue, selectValue); if (deselected) { const candidate = this.getOptionValue(newValue); this.setValue( selectValue.filter(i => this.getOptionValue(i) !== candidate), 'deselect-option', newValue ); } else if (!isDisabled) { // Select option if option is not disabled if (isMulti) { this.setValue([...selectValue, newValue], 'select-option', newValue); } else { this.setValue(newValue, 'select-option'); } } else { this.ariaOnChange(newValue, { action: 'select-option', name }); return; } if (blurInputOnSelect) { this.blurInput(); } }; removeValue = (removedValue: OptionType) => { const { isMulti } = this.props; const { selectValue } = this.state; const candidate = this.getOptionValue(removedValue); const newValueArray = selectValue.filter( i => this.getOptionValue(i) !== candidate ); const newValue = isMulti ? newValueArray : newValueArray[0] || null; this.onChange(newValue, { action: 'remove-value', removedValue }); this.focusInput(); }; clearValue = () => { const { selectValue } = this.state; this.onChange(this.props.isMulti ? [] : null, { action: 'clear', removedValues: selectValue, }); }; popValue = () => { const { isMulti } = this.props; const { selectValue } = this.state; const lastSelectedValue = selectValue[selectValue.length - 1]; const newValueArray = selectValue.slice(0, selectValue.length - 1); const newValue = isMulti ? newValueArray : newValueArray[0] || null; this.onChange(newValue, { action: 'pop-value', removedValue: lastSelectedValue, }); }; // ============================== // Getters // ============================== getTheme() { // Use the default theme if there are no customizations. if (!this.props.theme) { return defaultTheme; } // If the theme prop is a function, assume the function // knows how to merge the passed-in default theme with // its own modifications. if (typeof this.props.theme === 'function') { return this.props.theme(defaultTheme); } // Otherwise, if a plain theme object was passed in, // overlay it with the default theme. return { ...defaultTheme, ...this.props.theme, }; } getValue = () => this.state.selectValue; cx = (...args: any) => classNames(this.props.classNamePrefix, ...args); getCommonProps() { const { clearValue, cx, getStyles, getValue, selectOption, setValue, props, } = this; const { isMulti, isRtl, options } = props; const hasValue = this.hasValue(); return { clearValue, cx, getStyles, getValue, hasValue, isMulti, isRtl, options, selectOption, selectProps: props, setValue, theme: this.getTheme(), }; } getOptionLabel = (data: OptionType): string => { return getOptionLabel(this.props, data); }; getOptionValue = (data: OptionType): string => { return getOptionValue(this.props, data); }; getStyles = (key: string, props: {}): {} => { const base = defaultStyles[key](props); base.boxSizing = 'border-box'; const custom = this.props.styles[key]; return custom ? custom(base, props) : base; }; getElementId = (element: 'group' | 'input' | 'listbox' | 'option') => { return `${this.instancePrefix}-${element}`; }; getComponents = () => { return defaultComponents(this.props); }; buildCategorizedOptions = () => buildCategorizedOptions(this.props, this.state.selectValue); getCategorizedOptions = () => this.props.menuIsOpen ? this.buildCategorizedOptions() : []; buildFocusableOptions = () => buildFocusableOptionsFromCategorizedOptions(this.buildCategorizedOptions()); getFocusableOptions = () => this.props.menuIsOpen ? this.buildFocusableOptions() : []; // ============================== // Helpers // ============================== ariaOnChange = (value: ValueType, actionMeta: ActionMeta) => { this.setState({ ariaSelection: { value, ...actionMeta } }); }; hasValue() { const { selectValue } = this.state; return selectValue.length > 0; } hasOptions() { return !!this.getFocusableOptions().length; } isClearable(): boolean { const { isClearable, isMulti } = this.props; // single select, by default, IS NOT clearable // multi select, by default, IS clearable if (isClearable === undefined) return isMulti; return isClearable; } isOptionDisabled(option: OptionType, selectValue: OptionsType): boolean { return isOptionDisabled(this.props, option, selectValue); } isOptionSelected(option: OptionType, selectValue: OptionsType): boolean { return isOptionSelected(this.props, option, selectValue); } filterOption( option: { label: string, value: string, data: OptionType }, inputValue: string ) { return filterOption(this.props, option, inputValue); } formatOptionLabel(data: OptionType, context: FormatOptionLabelContext): Node { if (typeof this.props.formatOptionLabel === 'function') { const { inputValue } = this.props; const { selectValue } = this.state; return this.props.formatOptionLabel(data, { context, inputValue, selectValue, }); } else { return this.getOptionLabel(data); } } formatGroupLabel(data: GroupType) { return this.props.formatGroupLabel(data); } // ============================== // Mouse Handlers // ============================== onMenuMouseDown = (event: SyntheticMouseEvent
) => { if (event.button !== 0) { return; } event.stopPropagation(); event.preventDefault(); this.focusInput(); }; onMenuMouseMove = (event: SyntheticMouseEvent
) => { this.blockOptionHover = false; }; onControlMouseDown = (event: MouseOrTouchEvent) => { const { openMenuOnClick } = this.props; if (!this.state.isFocused) { if (openMenuOnClick) { this.openAfterFocus = true; } this.focusInput(); } else if (!this.props.menuIsOpen) { if (openMenuOnClick) { this.openMenu('first'); } } else { if ( // $FlowFixMe event.target.tagName !== 'INPUT' && event.target.tagName !== 'TEXTAREA' ) { this.onMenuClose(); } } if ( // $FlowFixMe event.target.tagName !== 'INPUT' && event.target.tagName !== 'TEXTAREA' ) { event.preventDefault(); } }; onDropdownIndicatorMouseDown = (event: MouseOrTouchEvent) => { // ignore mouse events that weren't triggered by the primary button if (event && event.type === 'mousedown' && event.button !== 0) { return; } if (this.props.isDisabled) return; const { isMulti, menuIsOpen } = this.props; this.focusInput(); if (menuIsOpen) { this.setState({ inputIsHiddenAfterUpdate: !isMulti }); this.onMenuClose(); } else { this.openMenu('first'); } event.preventDefault(); event.stopPropagation(); }; onClearIndicatorMouseDown = (event: MouseOrTouchEvent) => { // ignore mouse events that weren't triggered by the primary button if (event && event.type === 'mousedown' && event.button !== 0) { return; } this.clearValue(); event.stopPropagation(); this.openAfterFocus = false; if (event.type === 'touchend') { this.focusInput(); } else { setTimeout(() => this.focusInput()); } }; onScroll = (event: Event) => { if (typeof this.props.closeMenuOnScroll === 'boolean') { if ( event.target instanceof HTMLElement && isDocumentElement(event.target) ) { this.props.onMenuClose(); } } else if (typeof this.props.closeMenuOnScroll === 'function') { if (this.props.closeMenuOnScroll(event)) { this.props.onMenuClose(); } } }; // ============================== // Composition Handlers // ============================== startListeningComposition() { if (document && document.addEventListener) { document.addEventListener( 'compositionstart', this.onCompositionStart, false ); document.addEventListener('compositionend', this.onCompositionEnd, false); } } stopListeningComposition() { if (document && document.removeEventListener) { document.removeEventListener('compositionstart', this.onCompositionStart); document.removeEventListener('compositionend', this.onCompositionEnd); } } onCompositionStart = () => { this.isComposing = true; }; onCompositionEnd = () => { this.isComposing = false; }; // ============================== // Touch Handlers // ============================== startListeningToTouch() { if (document && document.addEventListener) { document.addEventListener('touchstart', this.onTouchStart, false); document.addEventListener('touchmove', this.onTouchMove, false); document.addEventListener('touchend', this.onTouchEnd, false); } } stopListeningToTouch() { if (document && document.removeEventListener) { document.removeEventListener('touchstart', this.onTouchStart); document.removeEventListener('touchmove', this.onTouchMove); document.removeEventListener('touchend', this.onTouchEnd); } } onTouchStart = ({ touches }: TouchEvent) => { const touch = touches && touches.item(0); if (!touch) { return; } this.initialTouchX = touch.clientX; this.initialTouchY = touch.clientY; this.userIsDragging = false; }; onTouchMove = ({ touches }: TouchEvent) => { const touch = touches && touches.item(0); if (!touch) { return; } const deltaX = Math.abs(touch.clientX - this.initialTouchX); const deltaY = Math.abs(touch.clientY - this.initialTouchY); const moveThreshold = 5; this.userIsDragging = deltaX > moveThreshold || deltaY > moveThreshold; }; onTouchEnd = (event: TouchEvent) => { if (this.userIsDragging) return; // close the menu if the user taps outside // we're checking on event.target here instead of event.currentTarget, because we want to assert information // on events on child elements, not the document (which we've attached this handler to). if ( this.controlRef && !this.controlRef.contains(event.target) && this.menuListRef && !this.menuListRef.contains(event.target) ) { this.blurInput(); } // reset move vars this.initialTouchX = 0; this.initialTouchY = 0; }; onControlTouchEnd = (event: SyntheticTouchEvent
) => { if (this.userIsDragging) return; this.onControlMouseDown(event); }; onClearIndicatorTouchEnd = (event: SyntheticTouchEvent
) => { if (this.userIsDragging) return; this.onClearIndicatorMouseDown(event); }; onDropdownIndicatorTouchEnd = (event: SyntheticTouchEvent
) => { if (this.userIsDragging) return; this.onDropdownIndicatorMouseDown(event); }; // ============================== // Focus Handlers // ============================== handleInputChange = (event: SyntheticKeyboardEvent
) => { const inputValue = event.currentTarget.value; this.setState({ inputIsHiddenAfterUpdate: false }); this.onInputChange(inputValue, { action: 'input-change' }); if (!this.props.menuIsOpen) { this.onMenuOpen(); } }; onInputFocus = (event: SyntheticFocusEvent
) => { if (this.props.onFocus) { this.props.onFocus(event); } this.setState({ inputIsHiddenAfterUpdate: false, isFocused: true, }); if (this.openAfterFocus || this.props.openMenuOnFocus) { this.openMenu('first'); } this.openAfterFocus = false; }; onInputBlur = (event: SyntheticFocusEvent
) => { if (this.menuListRef && this.menuListRef.contains(document.activeElement)) { this.inputRef.focus(); return; } if (this.props.onBlur) { this.props.onBlur(event); } this.onInputChange('', { action: 'input-blur' }); this.onMenuClose(); this.setState({ focusedValue: null, isFocused: false, }); }; onOptionHover = (focusedOption: OptionType) => { if (this.blockOptionHover || this.state.focusedOption === focusedOption) { return; } this.setState({ focusedOption }); }; shouldHideSelectedOptions = () => { return shouldHideSelectedOptions(this.props); }; // ============================== // Keyboard Handlers // ============================== onKeyDown = (event: SyntheticKeyboardEvent
) => { const { isMulti, backspaceRemovesValue, escapeClearsValue, inputValue, isClearable, isDisabled, menuIsOpen, onKeyDown, tabSelectsValue, openMenuOnFocus, } = this.props; const { focusedOption, focusedValue, selectValue } = this.state; if (isDisabled) return; if (typeof onKeyDown === 'function') { onKeyDown(event); if (event.defaultPrevented) { return; } } // Block option hover events when the user has just pressed a key this.blockOptionHover = true; switch (event.key) { case 'ArrowLeft': if (!isMulti || inputValue) return; this.focusValue('previous'); break; case 'ArrowRight': if (!isMulti || inputValue) return; this.focusValue('next'); break; case 'Delete': case 'Backspace': if (inputValue) return; if (focusedValue) { this.removeValue(focusedValue); } else { if (!backspaceRemovesValue) return; if (isMulti) { this.popValue(); } else if (isClearable) { this.clearValue(); } } break; case 'Tab': if (this.isComposing) return; if ( event.shiftKey || !menuIsOpen || !tabSelectsValue || !focusedOption || // don't capture the event if the menu opens on focus and the focused // option is already selected; it breaks the flow of navigation (openMenuOnFocus && this.isOptionSelected(focusedOption, selectValue)) ) { return; } this.selectOption(focusedOption); break; case 'Enter': if (event.keyCode === 229) { // ignore the keydown event from an Input Method Editor(IME) // ref. https://www.w3.org/TR/uievents/#determine-keydown-keyup-keyCode break; } if (menuIsOpen) { if (!focusedOption) return; if (this.isComposing) return; this.selectOption(focusedOption); break; } return; case 'Escape': if (menuIsOpen) { this.setState({ inputIsHiddenAfterUpdate: false }); this.onInputChange('', { action: 'menu-close' }); this.onMenuClose(); } else if (isClearable && escapeClearsValue) { this.clearValue(); } break; case ' ': // space if (inputValue) { return; } if (!menuIsOpen) { this.openMenu('first'); break; } if (!focusedOption) return; this.selectOption(focusedOption); break; case 'ArrowUp': if (menuIsOpen) { this.focusOption('up'); } else { this.openMenu('last'); } break; case 'ArrowDown': if (menuIsOpen) { this.focusOption('down'); } else { this.openMenu('first'); } break; case 'PageUp': if (!menuIsOpen) return; this.focusOption('pageup'); break; case 'PageDown': if (!menuIsOpen) return; this.focusOption('pagedown'); break; case 'Home': if (!menuIsOpen) return; this.focusOption('first'); break; case 'End': if (!menuIsOpen) return; this.focusOption('last'); break; default: return; } event.preventDefault(); }; // ============================== // Renderers // ============================== renderInput() { const { isDisabled, isSearchable, inputId, inputValue, tabIndex, form, } = this.props; const { Input } = this.getComponents(); const { inputIsHidden } = this.state; const { commonProps } = this; const id = inputId || this.getElementId('input'); // aria attributes makes the JSX "noisy", separated for clarity const ariaAttributes = { 'aria-autocomplete': 'list', 'aria-label': this.props['aria-label'], 'aria-labelledby': this.props['aria-labelledby'], }; if (!isSearchable) { // use a dummy input to maintain focus/blur functionality return (
); } return (
); } renderPlaceholderOrValue(): ?PlaceholderOrValue { const { MultiValue, MultiValueContainer, MultiValueLabel, MultiValueRemove, SingleValue, Placeholder, } = this.getComponents(); const { commonProps } = this; const { controlShouldRenderValue, isDisabled, isMulti, inputValue, placeholder, } = this.props; const { selectValue, focusedValue, isFocused } = this.state; if (!this.hasValue() || !controlShouldRenderValue) { return inputValue ? null : (
{placeholder}
); } if (isMulti) { const selectValues: Array
= selectValue.map((opt, index) => { const isOptionFocused = opt === focusedValue; return (
this.removeValue(opt), onTouchEnd: () => this.removeValue(opt), onMouseDown: e => { e.preventDefault(); e.stopPropagation(); }, }} data={opt} > {this.formatOptionLabel(opt, 'value')}
); }); return selectValues; } if (inputValue) { return null; } const singleValue = selectValue[0]; return (
{this.formatOptionLabel(singleValue, 'value')}
); } renderClearIndicator() { const { ClearIndicator } = this.getComponents(); const { commonProps } = this; const { isDisabled, isLoading } = this.props; const { isFocused } = this.state; if ( !this.isClearable() || !ClearIndicator || isDisabled || !this.hasValue() || isLoading ) { return null; } const innerProps = { onMouseDown: this.onClearIndicatorMouseDown, onTouchEnd: this.onClearIndicatorTouchEnd, 'aria-hidden': 'true', }; return (
); } renderLoadingIndicator() { const { LoadingIndicator } = this.getComponents(); const { commonProps } = this; const { isDisabled, isLoading } = this.props; const { isFocused } = this.state; if (!LoadingIndicator || !isLoading) return null; const innerProps = { 'aria-hidden': 'true' }; return (
); } renderIndicatorSeparator() { const { DropdownIndicator, IndicatorSeparator } = this.getComponents(); // separator doesn't make sense without the dropdown indicator if (!DropdownIndicator || !IndicatorSeparator) return null; const { commonProps } = this; const { isDisabled } = this.props; const { isFocused } = this.state; return (
); } renderDropdownIndicator() { const { DropdownIndicator } = this.getComponents(); if (!DropdownIndicator) return null; const { commonProps } = this; const { isDisabled } = this.props; const { isFocused } = this.state; const innerProps = { onMouseDown: this.onDropdownIndicatorMouseDown, onTouchEnd: this.onDropdownIndicatorTouchEnd, 'aria-hidden': 'true', }; return (
); } renderMenu() { const { Group, GroupHeading, Menu, MenuList, MenuPortal, LoadingMessage, NoOptionsMessage, Option, } = this.getComponents(); const { commonProps } = this; const { focusedOption } = this.state; const { captureMenuScroll, inputValue, isLoading, loadingMessage, minMenuHeight, maxMenuHeight, menuIsOpen, menuPlacement, menuPosition, menuPortalTarget, menuShouldBlockScroll, menuShouldScrollIntoView, noOptionsMessage, onMenuScrollToTop, onMenuScrollToBottom, } = this.props; if (!menuIsOpen) return null; // TODO: Internal Option Type here const render = (props: OptionType, id: string) => { const { type, data, isDisabled, isSelected, label, value } = props; const isFocused = focusedOption === data; const onHover = isDisabled ? undefined : () => this.onOptionHover(data); const onSelect = isDisabled ? undefined : () => this.selectOption(data); const optionId = `${this.getElementId('option')}-${id}`; const innerProps = { id: optionId, onClick: onSelect, onMouseMove: onHover, onMouseOver: onHover, tabIndex: -1, }; return (
{this.formatOptionLabel(props.data, 'menu')}
); }; let menuUI; if (this.hasOptions()) { menuUI = this.getCategorizedOptions().map(item => { if (item.type === 'group') { const { data, options, index: groupIndex } = item; const groupId = `${this.getElementId('group')}-${groupIndex}`; const headingId = `${groupId}-heading`; return (
{item.options.map(option => render(option, `${groupIndex}-${option.index}`) )}
); } else if (item.type === 'option') { return render(item, `${item.index}`); } }); } else if (isLoading) { const message = loadingMessage({ inputValue }); if (message === null) return null; menuUI =
{message}
; } else { const message = noOptionsMessage({ inputValue }); if (message === null) return null; menuUI =
{message}
; } const menuPlacementProps = { minMenuHeight, maxMenuHeight, menuPlacement, menuPosition, menuShouldScrollIntoView, }; const menuElement = (
{({ ref, placerProps: { placement, maxHeight } }) => (
{scrollTargetRef => (
{ this.getMenuListRef(instance); scrollTargetRef(instance); }} isLoading={isLoading} maxHeight={maxHeight} focusedOption={focusedOption} > {menuUI}
)}
)}
); // positioning behaviour is almost identical for portalled and fixed, // so we use the same component. the actual portalling logic is forked // within the component based on `menuPosition` return menuPortalTarget || menuPosition === 'fixed' ? (
{menuElement}
) : ( menuElement ); } renderFormField() { const { delimiter, isDisabled, isMulti, name } = this.props; const { selectValue } = this.state; if (!name || isDisabled) return; if (isMulti) { if (delimiter) { const value = selectValue .map(opt => this.getOptionValue(opt)) .join(delimiter); return
; } else { const input = selectValue.length > 0 ? ( selectValue.map((opt, i) => (
)) ) : (
); return
{input}
; } } else { const value = selectValue[0] ? this.getOptionValue(selectValue[0]) : ''; return
; } } renderLiveRegion() { const { commonProps } = this; const { ariaSelection, focusedOption, focusedValue, isFocused, selectValue, } = this.state; const focusableOptions = this.getFocusableOptions(); return (
); } render() { const { Control, IndicatorsContainer, SelectContainer, ValueContainer, } = this.getComponents(); const { className, id, isDisabled, menuIsOpen } = this.props; const { isFocused } = this.state; const commonProps = (this.commonProps = this.getCommonProps()); return (
{this.renderLiveRegion()}
{this.renderPlaceholderOrValue()} {this.renderInput()}
{this.renderClearIndicator()} {this.renderLoadingIndicator()} {this.renderIndicatorSeparator()} {this.renderDropdownIndicator()}
{this.renderMenu()} {this.renderFormField()}
); } }