1330 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			1330 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			TypeScript
		
	
	
| /**
 | ||
|  * To match accessibility requirement, we always provide an input in the component.
 | ||
|  * Other element will not set `tabindex` to avoid `onBlur` sequence problem.
 | ||
|  * For focused select, we set `aria-live="polite"` to update the accessibility content.
 | ||
|  *
 | ||
|  * ref:
 | ||
|  * - keyboard: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role#Keyboard_interactions
 | ||
|  */
 | ||
| 
 | ||
| import KeyCode from '../_util/KeyCode';
 | ||
| import classNames from '../_util/classNames';
 | ||
| import Selector from './Selector';
 | ||
| import SelectTrigger from './SelectTrigger';
 | ||
| import { RenderNode, Mode, RenderDOMFunc, OnActiveValue } from './interface';
 | ||
| import {
 | ||
|   GetLabeledValue,
 | ||
|   FilterOptions,
 | ||
|   FilterFunc,
 | ||
|   DefaultValueType,
 | ||
|   RawValueType,
 | ||
|   LabelValueType,
 | ||
|   Key,
 | ||
|   DisplayLabelValueType,
 | ||
|   FlattenOptionsType,
 | ||
|   SingleType,
 | ||
|   OnClear,
 | ||
|   INTERNAL_PROPS_MARK,
 | ||
|   SelectSource,
 | ||
|   CustomTagProps,
 | ||
| } from './interface/generator';
 | ||
| import { OptionListProps } from './OptionList';
 | ||
| import { toInnerValue, toOuterValues, removeLastEnabledValue, getUUID } from './utils/commonUtil';
 | ||
| import TransBtn from './TransBtn';
 | ||
| import useLock from './hooks/useLock';
 | ||
| import useDelayReset from './hooks/useDelayReset';
 | ||
| import { getSeparatedContent } from './utils/valueUtil';
 | ||
| import useSelectTriggerControl from './hooks/useSelectTriggerControl';
 | ||
| import useCacheDisplayValue from './hooks/useCacheDisplayValue';
 | ||
| import useCacheOptions from './hooks/useCacheOptions';
 | ||
| import {
 | ||
|   computed,
 | ||
|   CSSProperties,
 | ||
|   DefineComponent,
 | ||
|   defineComponent,
 | ||
|   onBeforeUnmount,
 | ||
|   onMounted,
 | ||
|   provide,
 | ||
|   ref,
 | ||
|   VNode,
 | ||
|   VNodeChild,
 | ||
|   watch,
 | ||
|   watchEffect,
 | ||
| } from 'vue';
 | ||
| import createRef from '../_util/createRef';
 | ||
| import PropTypes, { withUndefined } from '../_util/vue-types';
 | ||
| import initDefaultProps from '../_util/props-util/initDefaultProps';
 | ||
| 
 | ||
| const DEFAULT_OMIT_PROPS = [
 | ||
|   'children',
 | ||
|   'removeIcon',
 | ||
|   'placeholder',
 | ||
|   'autofocus',
 | ||
|   'maxTagCount',
 | ||
|   'maxTagTextLength',
 | ||
|   'maxTagPlaceholder',
 | ||
|   'choiceTransitionName',
 | ||
|   'onInputKeyDown',
 | ||
| ];
 | ||
| 
 | ||
| export const BaseProps = () => ({
 | ||
|   prefixCls: PropTypes.string,
 | ||
|   id: PropTypes.string,
 | ||
|   class: PropTypes.string,
 | ||
|   style: PropTypes.any,
 | ||
| 
 | ||
|   // Options
 | ||
|   options: PropTypes.array,
 | ||
|   mode: PropTypes.string,
 | ||
| 
 | ||
|   // Value
 | ||
|   value: PropTypes.any,
 | ||
|   defaultValue: PropTypes.any,
 | ||
|   labelInValue: PropTypes.looseBool,
 | ||
| 
 | ||
|   // Search
 | ||
|   inputValue: PropTypes.string,
 | ||
|   searchValue: PropTypes.string,
 | ||
|   optionFilterProp: PropTypes.string,
 | ||
|   /**
 | ||
|    * In Select, `false` means do nothing.
 | ||
|    * In TreeSelect, `false` will highlight match item.
 | ||
|    * It's by design.
 | ||
|    */
 | ||
|   filterOption: PropTypes.any,
 | ||
|   showSearch: PropTypes.looseBool,
 | ||
|   autoClearSearchValue: PropTypes.looseBool,
 | ||
|   onSearch: PropTypes.func,
 | ||
|   onClear: PropTypes.func,
 | ||
| 
 | ||
|   // Icons
 | ||
|   allowClear: PropTypes.looseBool,
 | ||
|   clearIcon: PropTypes.VNodeChild,
 | ||
|   showArrow: PropTypes.looseBool,
 | ||
|   inputIcon: PropTypes.VNodeChild,
 | ||
|   removeIcon: PropTypes.VNodeChild,
 | ||
|   menuItemSelectedIcon: PropTypes.VNodeChild,
 | ||
| 
 | ||
|   // Dropdown
 | ||
|   open: PropTypes.looseBool,
 | ||
|   defaultOpen: PropTypes.looseBool,
 | ||
|   listHeight: PropTypes.number,
 | ||
|   listItemHeight: PropTypes.number,
 | ||
|   dropdownStyle: PropTypes.object,
 | ||
|   dropdownClassName: PropTypes.string,
 | ||
|   dropdownMatchSelectWidth: withUndefined(PropTypes.oneOfType([Boolean, Number])),
 | ||
|   virtual: PropTypes.looseBool,
 | ||
|   dropdownRender: PropTypes.func,
 | ||
|   dropdownAlign: PropTypes.any,
 | ||
|   animation: PropTypes.string,
 | ||
|   transitionName: PropTypes.string,
 | ||
|   getPopupContainer: PropTypes.func,
 | ||
|   direction: PropTypes.string,
 | ||
| 
 | ||
|   // Others
 | ||
|   disabled: PropTypes.looseBool,
 | ||
|   loading: PropTypes.looseBool,
 | ||
|   autofocus: PropTypes.looseBool,
 | ||
|   defaultActiveFirstOption: PropTypes.looseBool,
 | ||
|   notFoundContent: PropTypes.VNodeChild,
 | ||
|   placeholder: PropTypes.VNodeChild,
 | ||
|   backfill: PropTypes.looseBool,
 | ||
|   getInputElement: PropTypes.func,
 | ||
|   optionLabelProp: PropTypes.string,
 | ||
|   maxTagTextLength: PropTypes.number,
 | ||
|   maxTagCount: PropTypes.number,
 | ||
|   maxTagPlaceholder: PropTypes.any,
 | ||
|   tokenSeparators: PropTypes.array,
 | ||
|   tagRender: PropTypes.func,
 | ||
|   showAction: PropTypes.array,
 | ||
|   tabindex: PropTypes.number,
 | ||
| 
 | ||
|   // Events
 | ||
|   onKeyup: PropTypes.func,
 | ||
|   onKeydown: PropTypes.func,
 | ||
|   onPopupScroll: PropTypes.func,
 | ||
|   onDropdownVisibleChange: PropTypes.func,
 | ||
|   onSelect: PropTypes.func,
 | ||
|   onDeselect: PropTypes.func,
 | ||
|   onInputKeyDown: PropTypes.func,
 | ||
|   onClick: PropTypes.func,
 | ||
|   onChange: PropTypes.func,
 | ||
|   onBlur: PropTypes.func,
 | ||
|   onFocus: PropTypes.func,
 | ||
|   onMousedown: PropTypes.func,
 | ||
|   onMouseenter: PropTypes.func,
 | ||
|   onMouseleave: PropTypes.func,
 | ||
| 
 | ||
|   // Motion
 | ||
|   choiceTransitionName: PropTypes.string,
 | ||
| 
 | ||
|   // Internal props
 | ||
|   /**
 | ||
|    * Only used in current version for internal event process.
 | ||
|    * Do not use in production environment.
 | ||
|    */
 | ||
|   internalProps: PropTypes.object,
 | ||
|   children: PropTypes.array,
 | ||
| });
 | ||
| 
 | ||
| export interface SelectProps<OptionsType extends object[], ValueType> {
 | ||
|   prefixCls?: string;
 | ||
|   id?: string;
 | ||
|   class?: string;
 | ||
|   style?: CSSProperties;
 | ||
| 
 | ||
|   // Options
 | ||
|   options?: OptionsType;
 | ||
|   children?: any[];
 | ||
|   mode?: Mode;
 | ||
| 
 | ||
|   // Value
 | ||
|   value?: ValueType;
 | ||
|   defaultValue?: ValueType;
 | ||
|   labelInValue?: boolean;
 | ||
| 
 | ||
|   // Search
 | ||
|   inputValue?: string;
 | ||
|   searchValue?: string;
 | ||
|   optionFilterProp?: string;
 | ||
|   /**
 | ||
|    * In Select, `false` means do nothing.
 | ||
|    * In TreeSelect, `false` will highlight match item.
 | ||
|    * It's by design.
 | ||
|    */
 | ||
|   filterOption?: boolean | FilterFunc<OptionsType[number]>;
 | ||
|   showSearch?: boolean;
 | ||
|   autoClearSearchValue?: boolean;
 | ||
|   onSearch?: (value: string) => void;
 | ||
|   onClear?: OnClear;
 | ||
| 
 | ||
|   // Icons
 | ||
|   allowClear?: boolean;
 | ||
|   clearIcon?: VNodeChild;
 | ||
|   showArrow?: boolean;
 | ||
|   inputIcon?: RenderNode;
 | ||
|   removeIcon?: VNodeChild;
 | ||
|   menuItemSelectedIcon?: RenderNode;
 | ||
| 
 | ||
|   // Dropdown
 | ||
|   open?: boolean;
 | ||
|   defaultOpen?: boolean;
 | ||
|   listHeight?: number;
 | ||
|   listItemHeight?: number;
 | ||
|   dropdownStyle?: CSSProperties;
 | ||
|   dropdownClassName?: string;
 | ||
|   dropdownMatchSelectWidth?: boolean | number;
 | ||
|   virtual?: boolean;
 | ||
|   dropdownRender?: (menu: VNodeChild | JSX.Element) => VNodeChild;
 | ||
|   dropdownAlign?: any;
 | ||
|   animation?: string;
 | ||
|   transitionName?: string;
 | ||
|   getPopupContainer?: RenderDOMFunc;
 | ||
|   direction?: string;
 | ||
| 
 | ||
|   // Others
 | ||
|   disabled?: boolean;
 | ||
|   loading?: boolean;
 | ||
|   autofocus?: boolean;
 | ||
|   defaultActiveFirstOption?: boolean;
 | ||
|   notFoundContent?: VNodeChild;
 | ||
|   placeholder?: VNodeChild;
 | ||
|   backfill?: boolean;
 | ||
|   getInputElement?: () => VNodeChild | JSX.Element;
 | ||
|   optionLabelProp?: string;
 | ||
|   maxTagTextLength?: number;
 | ||
|   maxTagCount?: number;
 | ||
|   maxTagPlaceholder?: VNodeChild | ((omittedValues: LabelValueType[]) => VNodeChild);
 | ||
|   tokenSeparators?: string[];
 | ||
|   tagRender?: (props: CustomTagProps) => VNodeChild;
 | ||
|   showAction?: ('focus' | 'click')[];
 | ||
|   tabindex?: number;
 | ||
| 
 | ||
|   // Events
 | ||
|   onKeyup?: EventHandlerNonNull;
 | ||
|   onKeydown?: EventHandlerNonNull;
 | ||
|   onPopupScroll?: EventHandlerNonNull;
 | ||
|   onDropdownVisibleChange?: (open: boolean) => void;
 | ||
|   onSelect?: (value: SingleType<ValueType>, option: OptionsType[number]) => void;
 | ||
|   onDeselect?: (value: SingleType<ValueType>, option: OptionsType[number]) => void;
 | ||
|   onInputKeyDown?: EventHandlerNonNull;
 | ||
|   onClick?: EventHandlerNonNull;
 | ||
|   onChange?: (value: ValueType, option: OptionsType[number] | OptionsType) => void;
 | ||
|   onBlur?: EventHandlerNonNull;
 | ||
|   onFocus?: EventHandlerNonNull;
 | ||
|   onMousedown?: EventHandlerNonNull;
 | ||
|   onMouseenter?: EventHandlerNonNull;
 | ||
|   onMouseleave?: EventHandlerNonNull;
 | ||
| 
 | ||
|   // Motion
 | ||
|   choiceTransitionName?: string;
 | ||
| 
 | ||
|   // Internal props
 | ||
|   /**
 | ||
|    * Only used in current version for internal event process.
 | ||
|    * Do not use in production environment.
 | ||
|    */
 | ||
|   internalProps?: {
 | ||
|     mark?: string;
 | ||
|     onClear?: OnClear;
 | ||
|     skipTriggerChange?: boolean;
 | ||
|     skipTriggerSelect?: boolean;
 | ||
|     onRawSelect?: (value: RawValueType, option: OptionsType[number], source: SelectSource) => void;
 | ||
|     onRawDeselect?: (
 | ||
|       value: RawValueType,
 | ||
|       option: OptionsType[number],
 | ||
|       source: SelectSource,
 | ||
|     ) => void;
 | ||
|   };
 | ||
| }
 | ||
| 
 | ||
| export interface GenerateConfig<OptionsType extends object[]> {
 | ||
|   prefixCls: string;
 | ||
|   components: {
 | ||
|     optionList: DefineComponent<Omit<OptionListProps, 'options'> & { options?: OptionsType }>;
 | ||
|   };
 | ||
|   /** Convert jsx tree into `OptionsType` */
 | ||
|   convertChildrenToData: (children: VNodeChild | JSX.Element) => OptionsType;
 | ||
|   /** Flatten nest options into raw option list */
 | ||
|   flattenOptions: (options: OptionsType, props: any) => FlattenOptionsType<OptionsType>;
 | ||
|   /** Convert single raw value into { label, value } format. Will be called by each value */
 | ||
|   getLabeledValue: GetLabeledValue<FlattenOptionsType<OptionsType>>;
 | ||
|   filterOptions: FilterOptions<OptionsType>;
 | ||
|   findValueOption: // Need still support legacy ts api
 | ||
|   | ((values: RawValueType[], options: FlattenOptionsType<OptionsType>) => OptionsType)
 | ||
|     // New API add prevValueOptions support
 | ||
|     | ((
 | ||
|         values: RawValueType[],
 | ||
|         options: FlattenOptionsType<OptionsType>,
 | ||
|         info?: { prevValueOptions?: OptionsType[] },
 | ||
|       ) => OptionsType);
 | ||
|   /** Check if a value is disabled */
 | ||
|   isValueDisabled: (value: RawValueType, options: FlattenOptionsType<OptionsType>) => boolean;
 | ||
|   warningProps?: (props: any) => void;
 | ||
|   fillOptionsWithMissingValue?: (
 | ||
|     options: OptionsType,
 | ||
|     value: DefaultValueType,
 | ||
|     optionLabelProp: string,
 | ||
|     labelInValue: boolean,
 | ||
|   ) => OptionsType;
 | ||
|   omitDOMProps?: (props: object) => object;
 | ||
| }
 | ||
| type ValueType = DefaultValueType;
 | ||
| /**
 | ||
|  * This function is in internal usage.
 | ||
|  * Do not use it in your prod env since we may refactor this.
 | ||
|  */
 | ||
| export default function generateSelector<
 | ||
|   OptionsType extends {
 | ||
|     value?: RawValueType;
 | ||
|     label?: VNodeChild;
 | ||
|     key?: Key;
 | ||
|     disabled?: boolean;
 | ||
|   }[]
 | ||
| >(config: GenerateConfig<OptionsType>) {
 | ||
|   const {
 | ||
|     prefixCls: defaultPrefixCls,
 | ||
|     components: { optionList: OptionList },
 | ||
|     convertChildrenToData,
 | ||
|     flattenOptions,
 | ||
|     getLabeledValue,
 | ||
|     filterOptions,
 | ||
|     isValueDisabled,
 | ||
|     findValueOption,
 | ||
|     warningProps,
 | ||
|     fillOptionsWithMissingValue,
 | ||
|     omitDOMProps,
 | ||
|   } = config as any;
 | ||
|   const Select = defineComponent<SelectProps<OptionsType, ValueType>>({
 | ||
|     name: 'Select',
 | ||
|     setup(props: SelectProps<OptionsType, ValueType>) {
 | ||
|       const useInternalProps = computed(
 | ||
|         () => props.internalProps && props.internalProps.mark === INTERNAL_PROPS_MARK,
 | ||
|       );
 | ||
| 
 | ||
|       const containerRef = ref(null);
 | ||
|       const triggerRef = ref(null);
 | ||
|       const selectorRef = ref(null);
 | ||
|       const listRef = ref(null);
 | ||
|       const tokenWithEnter = computed(() =>
 | ||
|         (props.tokenSeparators || []).some(tokenSeparator =>
 | ||
|           ['\n', '\r\n'].includes(tokenSeparator),
 | ||
|         ),
 | ||
|       );
 | ||
| 
 | ||
|       /** Used for component focused management */
 | ||
|       const [mockFocused, setMockFocused, cancelSetMockFocused] = useDelayReset();
 | ||
| 
 | ||
|       const mergedId = computed(() => props.id || `rc_select_${getUUID()}`);
 | ||
| 
 | ||
|       // optionLabelProp
 | ||
|       const mergedOptionLabelProp = computed(() => {
 | ||
|         let mergedOptionLabelProp = props.optionLabelProp;
 | ||
|         if (mergedOptionLabelProp === undefined) {
 | ||
|           mergedOptionLabelProp = props.options ? 'label' : 'children';
 | ||
|         }
 | ||
|         return mergedOptionLabelProp;
 | ||
|       });
 | ||
| 
 | ||
|       // labelInValue
 | ||
|       const mergedLabelInValue = computed(() =>
 | ||
|         props.mode === 'combobox' ? false : props.labelInValue,
 | ||
|       );
 | ||
| 
 | ||
|       const isMultiple = computed(() => props.mode === 'tags' || props.mode === 'multiple');
 | ||
| 
 | ||
|       const mergedShowSearch = computed(() =>
 | ||
|         props.showSearch !== undefined
 | ||
|           ? props.showSearch
 | ||
|           : isMultiple.value || props.mode === 'combobox',
 | ||
|       );
 | ||
| 
 | ||
|       // ============================== Ref ===============================
 | ||
|       const selectorDomRef = createRef();
 | ||
| 
 | ||
|       const mergedValue = ref(undefined);
 | ||
|       watch(
 | ||
|         computed(() => [props.value, props.defaultValue]),
 | ||
|         () => {
 | ||
|           mergedValue.value = props.value !== undefined ? props.value : props.defaultValue;
 | ||
|         },
 | ||
|         { immediate: true },
 | ||
|       );
 | ||
|       // ============================= Value ==============================
 | ||
| 
 | ||
|       /** Unique raw values */
 | ||
|       const mergedRawValue = computed(() =>
 | ||
|         toInnerValue(mergedValue.value, {
 | ||
|           labelInValue: mergedLabelInValue.value,
 | ||
|           combobox: props.mode === 'combobox',
 | ||
|         }),
 | ||
|       );
 | ||
|       /** We cache a set of raw values to speed up check */
 | ||
|       const rawValues = computed(() => new Set(mergedRawValue.value));
 | ||
| 
 | ||
|       // ============================= Option =============================
 | ||
|       // Set by option list active, it will merge into search input when mode is `combobox`
 | ||
|       const activeValue = ref(null);
 | ||
|       const setActiveValue = (val: string) => {
 | ||
|         activeValue.value = val;
 | ||
|       };
 | ||
|       const innerSearchValue = ref('');
 | ||
|       const setInnerSearchValue = (val: string) => {
 | ||
|         innerSearchValue.value = val;
 | ||
|       };
 | ||
| 
 | ||
|       const mergedSearchValue = computed(() => {
 | ||
|         let mergedSearchValue = innerSearchValue.value;
 | ||
|         if (props.mode === 'combobox' && mergedValue.value !== undefined) {
 | ||
|           mergedSearchValue = mergedValue.value as string;
 | ||
|         } else if (props.searchValue !== undefined) {
 | ||
|           mergedSearchValue = props.searchValue;
 | ||
|         } else if (props.inputValue) {
 | ||
|           mergedSearchValue = props.inputValue;
 | ||
|         }
 | ||
|         return mergedSearchValue;
 | ||
|       });
 | ||
| 
 | ||
|       const mergedOptions = computed(
 | ||
|         (): OptionsType => {
 | ||
|           let newOptions = props.options;
 | ||
|           if (newOptions === undefined) {
 | ||
|             newOptions = convertChildrenToData(props.children);
 | ||
|           }
 | ||
| 
 | ||
|           /**
 | ||
|            * `tags` should fill un-list item.
 | ||
|            * This is not cool here since TreeSelect do not need this
 | ||
|            */
 | ||
|           if (props.mode === 'tags' && fillOptionsWithMissingValue) {
 | ||
|             newOptions = fillOptionsWithMissingValue(
 | ||
|               newOptions,
 | ||
|               mergedValue.value,
 | ||
|               mergedOptionLabelProp.value,
 | ||
|               props.labelInValue,
 | ||
|             );
 | ||
|           }
 | ||
| 
 | ||
|           return newOptions || ([] as OptionsType);
 | ||
|         },
 | ||
|       );
 | ||
| 
 | ||
|       const mergedFlattenOptions = computed(() => flattenOptions(mergedOptions.value, props));
 | ||
| 
 | ||
|       const getValueOption = useCacheOptions(mergedRawValue.value, mergedFlattenOptions);
 | ||
| 
 | ||
|       // Display options for OptionList
 | ||
|       const displayOptions = computed<OptionsType>(() => {
 | ||
|         if (!mergedSearchValue.value || !mergedShowSearch.value) {
 | ||
|           return [...mergedOptions.value] as OptionsType;
 | ||
|         }
 | ||
|         const { optionFilterProp = 'value', mode, filterOption } = props;
 | ||
|         const filteredOptions: OptionsType = filterOptions(
 | ||
|           mergedSearchValue.value,
 | ||
|           mergedOptions.value,
 | ||
|           {
 | ||
|             optionFilterProp,
 | ||
|             filterOption:
 | ||
|               mode === 'combobox' && filterOption === undefined ? () => true : filterOption,
 | ||
|           },
 | ||
|         );
 | ||
|         if (
 | ||
|           mode === 'tags' &&
 | ||
|           filteredOptions.every(opt => opt[optionFilterProp] !== mergedSearchValue.value)
 | ||
|         ) {
 | ||
|           filteredOptions.unshift({
 | ||
|             value: mergedSearchValue.value,
 | ||
|             label: mergedSearchValue.value,
 | ||
|             key: '__RC_SELECT_TAG_PLACEHOLDER__',
 | ||
|           });
 | ||
|         }
 | ||
| 
 | ||
|         return filteredOptions;
 | ||
|       });
 | ||
| 
 | ||
|       const displayFlattenOptions = computed(() => flattenOptions(displayOptions.value, props));
 | ||
|       onMounted(() => {
 | ||
|         watch(
 | ||
|           mergedSearchValue,
 | ||
|           () => {
 | ||
|             if (listRef.value && listRef.value.scrollTo) {
 | ||
|               listRef.value.scrollTo(0);
 | ||
|             }
 | ||
|           },
 | ||
|           { flush: 'post', immediate: true },
 | ||
|         );
 | ||
|       });
 | ||
| 
 | ||
|       // ============================ Selector ============================
 | ||
|       let displayValues = computed<DisplayLabelValueType[]>(() => {
 | ||
|         const tmpValues = mergedRawValue.value.map((val: RawValueType) => {
 | ||
|           const valueOptions = getValueOption([val]);
 | ||
|           const displayValue = getLabeledValue(val, {
 | ||
|             options: valueOptions,
 | ||
|             prevValue: mergedValue.value,
 | ||
|             labelInValue: mergedLabelInValue.value,
 | ||
|             optionLabelProp: mergedOptionLabelProp.value,
 | ||
|           });
 | ||
| 
 | ||
|           return {
 | ||
|             ...displayValue,
 | ||
|             disabled: isValueDisabled(val, valueOptions),
 | ||
|           };
 | ||
|         });
 | ||
| 
 | ||
|         if (
 | ||
|           !props.mode &&
 | ||
|           tmpValues.length === 1 &&
 | ||
|           tmpValues[0].value === null &&
 | ||
|           tmpValues[0].label === null
 | ||
|         ) {
 | ||
|           return [];
 | ||
|         }
 | ||
| 
 | ||
|         return tmpValues;
 | ||
|       });
 | ||
| 
 | ||
|       // Polyfill with cache label
 | ||
|       displayValues = useCacheDisplayValue(displayValues);
 | ||
| 
 | ||
|       const triggerSelect = (newValue: RawValueType, isSelect: boolean, source: SelectSource) => {
 | ||
|         const newValueOption = getValueOption([newValue]);
 | ||
|         const outOption = findValueOption([newValue], newValueOption)[0];
 | ||
|         const { internalProps = {} } = props;
 | ||
|         if (!internalProps.skipTriggerSelect) {
 | ||
|           // Skip trigger `onSelect` or `onDeselect` if configured
 | ||
|           const selectValue = (mergedLabelInValue.value
 | ||
|             ? getLabeledValue(newValue, {
 | ||
|                 options: newValueOption,
 | ||
|                 prevValue: mergedValue.value,
 | ||
|                 labelInValue: mergedLabelInValue.value,
 | ||
|                 optionLabelProp: mergedOptionLabelProp.value,
 | ||
|               })
 | ||
|             : newValue) as SingleType<ValueType>;
 | ||
| 
 | ||
|           if (isSelect && props.onSelect) {
 | ||
|             props.onSelect(selectValue, outOption);
 | ||
|           } else if (!isSelect && props.onDeselect) {
 | ||
|             props.onDeselect(selectValue, outOption);
 | ||
|           }
 | ||
|         }
 | ||
| 
 | ||
|         // Trigger internal event
 | ||
|         if (useInternalProps.value) {
 | ||
|           if (isSelect && internalProps.onRawSelect) {
 | ||
|             internalProps.onRawSelect(newValue, outOption, source);
 | ||
|           } else if (!isSelect && internalProps.onRawDeselect) {
 | ||
|             internalProps.onRawDeselect(newValue, outOption, source);
 | ||
|           }
 | ||
|         }
 | ||
|       };
 | ||
| 
 | ||
|       // We need cache options here in case user update the option list
 | ||
|       const prevValueOptions = ref([]);
 | ||
|       const setPrevValueOptions = (val: any[]) => {
 | ||
|         prevValueOptions.value = val;
 | ||
|       };
 | ||
|       const triggerChange = (newRawValues: RawValueType[]) => {
 | ||
|         if (
 | ||
|           useInternalProps.value &&
 | ||
|           props.internalProps &&
 | ||
|           props.internalProps.skipTriggerChange
 | ||
|         ) {
 | ||
|           return;
 | ||
|         }
 | ||
|         const newRawValuesOptions = getValueOption(newRawValues);
 | ||
|         const outValues = toOuterValues<FlattenOptionsType<OptionsType>>(Array.from(newRawValues), {
 | ||
|           labelInValue: mergedLabelInValue.value,
 | ||
|           options: newRawValuesOptions,
 | ||
|           getLabeledValue,
 | ||
|           prevValue: mergedValue.value,
 | ||
|           optionLabelProp: mergedOptionLabelProp.value,
 | ||
|         });
 | ||
| 
 | ||
|         const outValue: ValueType = (isMultiple.value ? outValues : outValues[0]) as ValueType;
 | ||
|         // Skip trigger if prev & current value is both empty
 | ||
|         if (
 | ||
|           props.onChange &&
 | ||
|           (mergedRawValue.value.length !== 0 || (outValues as []).length !== 0)
 | ||
|         ) {
 | ||
|           const outOptions = findValueOption(newRawValues, newRawValuesOptions, {
 | ||
|             prevValueOptions: prevValueOptions.value,
 | ||
|           });
 | ||
| 
 | ||
|           // We will cache option in case it removed by ajax
 | ||
|           setPrevValueOptions(
 | ||
|             outOptions.map((option, index) => {
 | ||
|               const clone = { ...option };
 | ||
|               Object.defineProperty(clone, '_INTERNAL_OPTION_VALUE_', {
 | ||
|                 get: () => newRawValues[index],
 | ||
|               });
 | ||
|               return clone;
 | ||
|             }),
 | ||
|           );
 | ||
| 
 | ||
|           props.onChange(outValue, isMultiple.value ? outOptions : outOptions[0]);
 | ||
|         }
 | ||
|         mergedValue.value = outValue;
 | ||
|       };
 | ||
| 
 | ||
|       const onInternalSelect = (
 | ||
|         newValue: RawValueType,
 | ||
|         { selected, source }: { selected: boolean; source: 'option' | 'selection' },
 | ||
|       ) => {
 | ||
|         const { autoClearSearchValue = true } = props;
 | ||
|         if (props.disabled) {
 | ||
|           return;
 | ||
|         }
 | ||
| 
 | ||
|         let newRawValue: Set<RawValueType>;
 | ||
| 
 | ||
|         if (isMultiple.value) {
 | ||
|           newRawValue = new Set(mergedRawValue.value);
 | ||
|           if (selected) {
 | ||
|             newRawValue.add(newValue);
 | ||
|           } else {
 | ||
|             newRawValue.delete(newValue);
 | ||
|           }
 | ||
|         } else {
 | ||
|           newRawValue = new Set();
 | ||
|           newRawValue.add(newValue);
 | ||
|         }
 | ||
| 
 | ||
|         // Multiple always trigger change and single should change if value changed
 | ||
|         if (
 | ||
|           isMultiple.value ||
 | ||
|           (!isMultiple.value && Array.from(mergedRawValue.value)[0] !== newValue)
 | ||
|         ) {
 | ||
|           triggerChange(Array.from(newRawValue));
 | ||
|         }
 | ||
| 
 | ||
|         // Trigger `onSelect`. Single mode always trigger select
 | ||
|         triggerSelect(newValue, !isMultiple.value || selected, source);
 | ||
| 
 | ||
|         // Clean search value if single or configured
 | ||
|         if (props.mode === 'combobox') {
 | ||
|           setInnerSearchValue(String(newValue));
 | ||
|           setActiveValue('');
 | ||
|         } else if (!isMultiple.value || autoClearSearchValue) {
 | ||
|           setInnerSearchValue('');
 | ||
|           setActiveValue('');
 | ||
|         }
 | ||
|       };
 | ||
| 
 | ||
|       const onInternalOptionSelect = (newValue: RawValueType, info: { selected: boolean }) => {
 | ||
|         onInternalSelect(newValue, { ...info, source: 'option' });
 | ||
|       };
 | ||
| 
 | ||
|       const onInternalSelectionSelect = (newValue: RawValueType, info: { selected: boolean }) => {
 | ||
|         onInternalSelect(newValue, { ...info, source: 'selection' });
 | ||
|       };
 | ||
| 
 | ||
|       // ============================== Open ==============================
 | ||
|       const initOpen = props.open !== undefined ? props.open : props.defaultOpen;
 | ||
|       const innerOpen = ref(initOpen);
 | ||
|       const mergedOpen = ref(initOpen);
 | ||
|       const setInnerOpen = (val: boolean) => {
 | ||
|         innerOpen.value = props.open !== undefined ? props.open : val;
 | ||
|         mergedOpen.value = innerOpen.value;
 | ||
|       };
 | ||
|       watch(
 | ||
|         () => props.open,
 | ||
|         () => {
 | ||
|           setInnerOpen(props.open);
 | ||
|         },
 | ||
|       );
 | ||
| 
 | ||
|       // Not trigger `open` in `combobox` when `notFoundContent` is empty
 | ||
|       const emptyListContent = computed(
 | ||
|         () => !props.notFoundContent && !displayOptions.value.length,
 | ||
|       );
 | ||
| 
 | ||
|       watchEffect(() => {
 | ||
|         mergedOpen.value = innerOpen.value;
 | ||
|         if (
 | ||
|           props.disabled ||
 | ||
|           (emptyListContent.value && mergedOpen.value && props.mode === 'combobox')
 | ||
|         ) {
 | ||
|           mergedOpen.value = false;
 | ||
|         }
 | ||
|       });
 | ||
| 
 | ||
|       const triggerOpen = computed(() => (emptyListContent.value ? false : mergedOpen.value));
 | ||
| 
 | ||
|       const onToggleOpen = (newOpen?: boolean) => {
 | ||
|         const nextOpen = newOpen !== undefined ? newOpen : !mergedOpen.value;
 | ||
| 
 | ||
|         if (innerOpen.value !== nextOpen && !props.disabled) {
 | ||
|           setInnerOpen(nextOpen);
 | ||
|           if (props.onDropdownVisibleChange) {
 | ||
|             props.onDropdownVisibleChange(nextOpen);
 | ||
|           }
 | ||
|         }
 | ||
|       };
 | ||
| 
 | ||
|       useSelectTriggerControl([containerRef, triggerRef], triggerOpen, onToggleOpen);
 | ||
| 
 | ||
|       // ============================= Search =============================
 | ||
|       const triggerSearch = (searchText: string, fromTyping: boolean, isCompositing: boolean) => {
 | ||
|         let ret = true;
 | ||
|         let newSearchText = searchText;
 | ||
|         const preSearchValue = mergedSearchValue.value;
 | ||
|         setActiveValue(null);
 | ||
| 
 | ||
|         // Check if match the `tokenSeparators`
 | ||
|         const patchLabels: string[] = isCompositing
 | ||
|           ? null
 | ||
|           : getSeparatedContent(searchText, props.tokenSeparators);
 | ||
|         let patchRawValues: RawValueType[] = patchLabels;
 | ||
| 
 | ||
|         if (props.mode === 'combobox') {
 | ||
|           // Only typing will trigger onChange
 | ||
|           if (fromTyping) {
 | ||
|             triggerChange([newSearchText]);
 | ||
|           }
 | ||
|         } else if (patchLabels) {
 | ||
|           newSearchText = '';
 | ||
| 
 | ||
|           if (props.mode !== 'tags') {
 | ||
|             patchRawValues = patchLabels
 | ||
|               .map(label => {
 | ||
|                 const item = mergedFlattenOptions.value.find(
 | ||
|                   ({ data }) => data[mergedOptionLabelProp.value] === label,
 | ||
|                 );
 | ||
|                 return item ? item.data.value : null;
 | ||
|               })
 | ||
|               .filter((val: RawValueType) => val !== null);
 | ||
|           }
 | ||
| 
 | ||
|           const newRawValues = Array.from(
 | ||
|             new Set<RawValueType>([...mergedRawValue.value, ...patchRawValues]),
 | ||
|           );
 | ||
|           triggerChange(newRawValues);
 | ||
|           newRawValues.forEach(newRawValue => {
 | ||
|             triggerSelect(newRawValue, true, 'input');
 | ||
|           });
 | ||
| 
 | ||
|           // Should close when paste finish
 | ||
|           onToggleOpen(false);
 | ||
| 
 | ||
|           // Tell Selector that break next actions
 | ||
|           ret = false;
 | ||
|         }
 | ||
| 
 | ||
|         setInnerSearchValue(newSearchText);
 | ||
| 
 | ||
|         if (props.onSearch && preSearchValue !== newSearchText) {
 | ||
|           props.onSearch(newSearchText);
 | ||
|         }
 | ||
| 
 | ||
|         return ret;
 | ||
|       };
 | ||
| 
 | ||
|       // Only triggered when menu is closed & mode is tags
 | ||
|       // If menu is open, OptionList will take charge
 | ||
|       // If mode isn't tags, press enter is not meaningful when you can't see any option
 | ||
|       const onSearchSubmit = (searchText: string) => {
 | ||
|         const newRawValues = Array.from(
 | ||
|           new Set<RawValueType>([...mergedRawValue.value, searchText]),
 | ||
|         );
 | ||
|         triggerChange(newRawValues);
 | ||
|         newRawValues.forEach(newRawValue => {
 | ||
|           triggerSelect(newRawValue, true, 'input');
 | ||
|         });
 | ||
|         setInnerSearchValue('');
 | ||
|       };
 | ||
| 
 | ||
|       // Close dropdown when disabled change
 | ||
| 
 | ||
|       watch(
 | ||
|         computed(() => props.disabled),
 | ||
|         () => {
 | ||
|           if (innerOpen.value && !!props.disabled) {
 | ||
|             setInnerOpen(false);
 | ||
|           }
 | ||
|         },
 | ||
|         { immediate: true },
 | ||
|       );
 | ||
| 
 | ||
|       // Close will clean up single mode search text
 | ||
|       watch(
 | ||
|         mergedOpen,
 | ||
|         () => {
 | ||
|           if (!mergedOpen.value && !isMultiple.value && props.mode !== 'combobox') {
 | ||
|             triggerSearch('', false, false);
 | ||
|           }
 | ||
|         },
 | ||
|         { immediate: true },
 | ||
|       );
 | ||
|       // ============================ Keyboard ============================
 | ||
|       /**
 | ||
|        * We record input value here to check if can press to clean up by backspace
 | ||
|        * - null: Key is not down, this is reset by key up
 | ||
|        * - true: Search text is empty when first time backspace down
 | ||
|        * - false: Search text is not empty when first time backspace down
 | ||
|        */
 | ||
|       const [getClearLock, setClearLock] = useLock();
 | ||
| 
 | ||
|       // KeyDown
 | ||
|       const onInternalKeyDown = (event: KeyboardEvent) => {
 | ||
|         const clearLock = getClearLock();
 | ||
|         const { which } = event;
 | ||
|         // We only manage open state here, close logic should handle by list component
 | ||
|         if (!mergedOpen.value && which === KeyCode.ENTER) {
 | ||
|           onToggleOpen(true);
 | ||
|         }
 | ||
| 
 | ||
|         setClearLock(!!mergedSearchValue.value);
 | ||
| 
 | ||
|         // Remove value by `backspace`
 | ||
|         if (
 | ||
|           which === KeyCode.BACKSPACE &&
 | ||
|           !clearLock &&
 | ||
|           isMultiple.value &&
 | ||
|           !mergedSearchValue.value &&
 | ||
|           mergedRawValue.value.length
 | ||
|         ) {
 | ||
|           const removeInfo = removeLastEnabledValue(displayValues.value, mergedRawValue.value);
 | ||
| 
 | ||
|           if (removeInfo.removedValue !== null) {
 | ||
|             triggerChange(removeInfo.values);
 | ||
|             triggerSelect(removeInfo.removedValue, false, 'input');
 | ||
|           }
 | ||
|         }
 | ||
| 
 | ||
|         if (mergedOpen.value && listRef.value) {
 | ||
|           listRef.value.onKeydown(event);
 | ||
|         }
 | ||
| 
 | ||
|         if (props.onKeydown) {
 | ||
|           props.onKeydown(event);
 | ||
|         }
 | ||
|       };
 | ||
| 
 | ||
|       // KeyUp
 | ||
|       const onInternalKeyUp = (event: Event) => {
 | ||
|         if (mergedOpen.value && listRef.value) {
 | ||
|           listRef.value.onKeyup(event);
 | ||
|         }
 | ||
| 
 | ||
|         if (props.onKeyup) {
 | ||
|           props.onKeyup(event);
 | ||
|         }
 | ||
|       };
 | ||
| 
 | ||
|       // ========================== Focus / Blur ==========================
 | ||
|       /** Record real focus status */
 | ||
|       const focusRef = ref(false);
 | ||
| 
 | ||
|       const onContainerFocus = (...args: any[]) => {
 | ||
|         setMockFocused(true);
 | ||
| 
 | ||
|         if (!props.disabled) {
 | ||
|           if (props.onFocus && !focusRef.value) {
 | ||
|             props.onFocus(args[0]);
 | ||
|           }
 | ||
| 
 | ||
|           // `showAction` should handle `focus` if set
 | ||
|           if (props.showAction && props.showAction.includes('focus')) {
 | ||
|             onToggleOpen(true);
 | ||
|           }
 | ||
|         }
 | ||
| 
 | ||
|         focusRef.value = true;
 | ||
|       };
 | ||
| 
 | ||
|       const onContainerBlur = (...args: any[]) => {
 | ||
|         setMockFocused(false, () => {
 | ||
|           focusRef.value = false;
 | ||
|           onToggleOpen(false);
 | ||
|         });
 | ||
| 
 | ||
|         if (props.disabled) {
 | ||
|           return;
 | ||
|         }
 | ||
|         const serachVal = mergedSearchValue.value;
 | ||
|         if (serachVal) {
 | ||
|           // `tags` mode should move `searchValue` into values
 | ||
|           if (props.mode === 'tags') {
 | ||
|             triggerSearch('', false, false);
 | ||
|             triggerChange(Array.from(new Set([...mergedRawValue.value, serachVal])));
 | ||
|           } else if (props.mode === 'multiple') {
 | ||
|             // `multiple` mode only clean the search value but not trigger event
 | ||
|             setInnerSearchValue('');
 | ||
|           }
 | ||
|         }
 | ||
| 
 | ||
|         if (props.onBlur) {
 | ||
|           props.onBlur(args[0]);
 | ||
|         }
 | ||
|       };
 | ||
|       provide('VCSelectContainerEvent', {
 | ||
|         focus: onContainerFocus,
 | ||
|         blur: onContainerBlur,
 | ||
|       });
 | ||
|       const activeTimeoutIds: number[] = [];
 | ||
| 
 | ||
|       onMounted(() => {
 | ||
|         activeTimeoutIds.forEach(timeoutId => clearTimeout(timeoutId));
 | ||
|         activeTimeoutIds.splice(0, activeTimeoutIds.length);
 | ||
|       });
 | ||
|       onBeforeUnmount(() => {
 | ||
|         activeTimeoutIds.forEach(timeoutId => clearTimeout(timeoutId));
 | ||
|         activeTimeoutIds.splice(0, activeTimeoutIds.length);
 | ||
|       });
 | ||
| 
 | ||
|       const onInternalMouseDown = (event: MouseEvent) => {
 | ||
|         const { target } = event;
 | ||
|         const popupElement: HTMLDivElement = triggerRef.value && triggerRef.value.getPopupElement();
 | ||
| 
 | ||
|         // We should give focus back to selector if clicked item is not focusable
 | ||
|         if (popupElement && popupElement.contains(target as HTMLElement)) {
 | ||
|           const timeoutId = setTimeout(() => {
 | ||
|             const index = activeTimeoutIds.indexOf(timeoutId);
 | ||
|             if (index !== -1) {
 | ||
|               activeTimeoutIds.splice(index, 1);
 | ||
|             }
 | ||
| 
 | ||
|             cancelSetMockFocused();
 | ||
| 
 | ||
|             if (!popupElement.contains(document.activeElement)) {
 | ||
|               selectorRef.value.focus();
 | ||
|             }
 | ||
|           });
 | ||
| 
 | ||
|           activeTimeoutIds.push(timeoutId);
 | ||
|         }
 | ||
| 
 | ||
|         if (props.onMousedown) {
 | ||
|           props.onMousedown(event);
 | ||
|         }
 | ||
|       };
 | ||
| 
 | ||
|       // ========================= Accessibility ==========================
 | ||
|       const accessibilityIndex = ref(0);
 | ||
|       const mergedDefaultActiveFirstOption = computed(() =>
 | ||
|         props.defaultActiveFirstOption !== undefined
 | ||
|           ? props.defaultActiveFirstOption
 | ||
|           : props.mode !== 'combobox',
 | ||
|       );
 | ||
| 
 | ||
|       const onActiveValue: OnActiveValue = (active, index, { source = 'keyboard' } = {}) => {
 | ||
|         accessibilityIndex.value = index;
 | ||
| 
 | ||
|         if (
 | ||
|           props.backfill &&
 | ||
|           props.mode === 'combobox' &&
 | ||
|           active !== null &&
 | ||
|           source === 'keyboard'
 | ||
|         ) {
 | ||
|           setActiveValue(String(active));
 | ||
|         }
 | ||
|       };
 | ||
| 
 | ||
|       // ============================= Popup ==============================
 | ||
|       const containerWidth = ref(null);
 | ||
|       onMounted(() => {
 | ||
|         watch(
 | ||
|           triggerOpen,
 | ||
|           () => {
 | ||
|             if (triggerOpen.value) {
 | ||
|               const newWidth = Math.ceil(containerRef.value.offsetWidth);
 | ||
|               if (containerWidth.value !== newWidth) {
 | ||
|                 containerWidth.value = newWidth;
 | ||
|               }
 | ||
|             }
 | ||
|           },
 | ||
|           { immediate: true },
 | ||
|         );
 | ||
|       });
 | ||
| 
 | ||
|       const focus = () => {
 | ||
|         selectorRef.value.focus();
 | ||
|       };
 | ||
|       const blur = () => {
 | ||
|         selectorRef.value.blur();
 | ||
|       };
 | ||
|       return {
 | ||
|         focus,
 | ||
|         blur,
 | ||
|         tokenWithEnter,
 | ||
|         mockFocused,
 | ||
|         mergedId,
 | ||
|         containerWidth,
 | ||
|         onActiveValue,
 | ||
|         accessibilityIndex,
 | ||
|         mergedDefaultActiveFirstOption,
 | ||
|         onInternalMouseDown,
 | ||
|         onContainerFocus,
 | ||
|         onContainerBlur,
 | ||
|         onInternalKeyDown,
 | ||
|         isMultiple,
 | ||
|         mergedOpen,
 | ||
|         displayOptions,
 | ||
|         displayFlattenOptions,
 | ||
|         rawValues,
 | ||
|         onInternalOptionSelect,
 | ||
|         onToggleOpen,
 | ||
|         mergedSearchValue,
 | ||
|         useInternalProps,
 | ||
|         triggerChange,
 | ||
|         triggerSearch,
 | ||
|         mergedRawValue,
 | ||
|         mergedShowSearch,
 | ||
|         onInternalKeyUp,
 | ||
|         triggerOpen,
 | ||
|         mergedOptions,
 | ||
|         onInternalSelectionSelect,
 | ||
|         selectorDomRef,
 | ||
|         displayValues,
 | ||
|         activeValue,
 | ||
|         onSearchSubmit,
 | ||
|         containerRef,
 | ||
|         listRef,
 | ||
|         triggerRef,
 | ||
|         selectorRef,
 | ||
|       };
 | ||
|     },
 | ||
|     methods: {
 | ||
|       // We need force update here since popup dom is render async
 | ||
|       onPopupMouseEnter() {
 | ||
|         this.$forceUpdate();
 | ||
|       },
 | ||
|     },
 | ||
|     render() {
 | ||
|       const {
 | ||
|         tokenWithEnter,
 | ||
|         mockFocused,
 | ||
|         mergedId,
 | ||
|         containerWidth,
 | ||
|         onActiveValue,
 | ||
|         accessibilityIndex,
 | ||
|         mergedDefaultActiveFirstOption,
 | ||
|         onInternalMouseDown,
 | ||
|         onInternalKeyDown,
 | ||
|         isMultiple,
 | ||
|         mergedOpen,
 | ||
|         displayOptions,
 | ||
|         displayFlattenOptions,
 | ||
|         rawValues,
 | ||
|         onInternalOptionSelect,
 | ||
|         onToggleOpen,
 | ||
|         mergedSearchValue,
 | ||
|         onPopupMouseEnter,
 | ||
|         useInternalProps,
 | ||
|         triggerChange,
 | ||
|         triggerSearch,
 | ||
|         mergedRawValue,
 | ||
|         mergedShowSearch,
 | ||
|         onInternalKeyUp,
 | ||
|         triggerOpen,
 | ||
|         mergedOptions,
 | ||
|         onInternalSelectionSelect,
 | ||
|         selectorDomRef,
 | ||
|         displayValues,
 | ||
|         activeValue,
 | ||
|         onSearchSubmit,
 | ||
|       } = this as any;
 | ||
|       const {
 | ||
|         prefixCls = defaultPrefixCls,
 | ||
|         class: className,
 | ||
|         id,
 | ||
| 
 | ||
|         open,
 | ||
|         defaultOpen,
 | ||
|         options,
 | ||
|         children,
 | ||
| 
 | ||
|         mode,
 | ||
|         value,
 | ||
|         defaultValue,
 | ||
|         labelInValue,
 | ||
| 
 | ||
|         // Search related
 | ||
|         showSearch,
 | ||
|         inputValue,
 | ||
|         searchValue,
 | ||
|         filterOption,
 | ||
|         optionFilterProp,
 | ||
|         autoClearSearchValue,
 | ||
|         onSearch,
 | ||
| 
 | ||
|         // Icons
 | ||
|         allowClear,
 | ||
|         clearIcon,
 | ||
|         showArrow,
 | ||
|         inputIcon,
 | ||
|         menuItemSelectedIcon,
 | ||
| 
 | ||
|         // Others
 | ||
|         disabled,
 | ||
|         loading,
 | ||
|         defaultActiveFirstOption,
 | ||
|         notFoundContent = 'Not Found',
 | ||
|         optionLabelProp,
 | ||
|         backfill,
 | ||
|         getInputElement,
 | ||
|         getPopupContainer,
 | ||
| 
 | ||
|         // Dropdown
 | ||
|         listHeight = 200,
 | ||
|         listItemHeight = 20,
 | ||
|         animation,
 | ||
|         transitionName,
 | ||
|         virtual,
 | ||
|         dropdownStyle,
 | ||
|         dropdownClassName,
 | ||
|         dropdownMatchSelectWidth,
 | ||
|         dropdownRender,
 | ||
|         dropdownAlign,
 | ||
|         showAction,
 | ||
|         direction,
 | ||
| 
 | ||
|         // Tags
 | ||
|         tokenSeparators,
 | ||
|         tagRender,
 | ||
| 
 | ||
|         // Events
 | ||
|         onPopupScroll,
 | ||
|         onDropdownVisibleChange,
 | ||
|         onFocus,
 | ||
|         onBlur,
 | ||
|         onKeyup,
 | ||
|         onKeydown,
 | ||
|         onMousedown,
 | ||
| 
 | ||
|         onChange,
 | ||
|         onSelect,
 | ||
|         onDeselect,
 | ||
|         onClear,
 | ||
| 
 | ||
|         internalProps = {},
 | ||
| 
 | ||
|         ...restProps
 | ||
|       } = this.$props as SelectProps<OptionsType, ValueType>;
 | ||
| 
 | ||
|       // ============================= Input ==============================
 | ||
|       // Only works in `combobox`
 | ||
|       const customizeInputElement: VNodeChild | JSX.Element =
 | ||
|         (mode === 'combobox' && getInputElement && getInputElement()) || null;
 | ||
| 
 | ||
|       const domProps = omitDOMProps ? omitDOMProps(restProps) : restProps;
 | ||
|       DEFAULT_OMIT_PROPS.forEach(prop => {
 | ||
|         delete domProps[prop];
 | ||
|       });
 | ||
|       const popupNode = (
 | ||
|         <OptionList
 | ||
|           ref="listRef"
 | ||
|           prefixCls={prefixCls}
 | ||
|           id={mergedId}
 | ||
|           open={mergedOpen}
 | ||
|           childrenAsData={!options}
 | ||
|           options={displayOptions}
 | ||
|           flattenOptions={displayFlattenOptions}
 | ||
|           multiple={isMultiple}
 | ||
|           values={rawValues}
 | ||
|           height={listHeight}
 | ||
|           itemHeight={listItemHeight}
 | ||
|           onSelect={onInternalOptionSelect}
 | ||
|           onToggleOpen={onToggleOpen}
 | ||
|           onActiveValue={onActiveValue}
 | ||
|           defaultActiveFirstOption={mergedDefaultActiveFirstOption}
 | ||
|           notFoundContent={notFoundContent}
 | ||
|           onScroll={onPopupScroll}
 | ||
|           searchValue={mergedSearchValue}
 | ||
|           menuItemSelectedIcon={menuItemSelectedIcon}
 | ||
|           virtual={virtual !== false && dropdownMatchSelectWidth !== false}
 | ||
|           onMouseenter={onPopupMouseEnter}
 | ||
|         />
 | ||
|       );
 | ||
| 
 | ||
|       // ============================= Clear ==============================
 | ||
|       let clearNode: VNode | JSX.Element;
 | ||
|       const onClearMouseDown = () => {
 | ||
|         // Trigger internal `onClear` event
 | ||
|         if (useInternalProps && internalProps.onClear) {
 | ||
|           internalProps.onClear();
 | ||
|         }
 | ||
| 
 | ||
|         if (onClear) {
 | ||
|           onClear();
 | ||
|         }
 | ||
| 
 | ||
|         triggerChange([]);
 | ||
|         triggerSearch('', false, false);
 | ||
|       };
 | ||
| 
 | ||
|       if (!disabled && allowClear && (mergedRawValue.length || mergedSearchValue)) {
 | ||
|         clearNode = (
 | ||
|           <TransBtn
 | ||
|             class={`${prefixCls}-clear`}
 | ||
|             onMousedown={onClearMouseDown}
 | ||
|             customizeIcon={clearIcon}
 | ||
|           >
 | ||
|             Ã
 | ||
|           </TransBtn>
 | ||
|         );
 | ||
|       }
 | ||
| 
 | ||
|       // ============================= Arrow ==============================
 | ||
|       const mergedShowArrow =
 | ||
|         showArrow !== undefined ? showArrow : loading || (!isMultiple && mode !== 'combobox');
 | ||
|       let arrowNode: VNode | JSX.Element;
 | ||
| 
 | ||
|       if (mergedShowArrow) {
 | ||
|         arrowNode = (
 | ||
|           <TransBtn
 | ||
|             class={classNames(`${prefixCls}-arrow`, {
 | ||
|               [`${prefixCls}-arrow-loading`]: loading,
 | ||
|             })}
 | ||
|             customizeIcon={inputIcon}
 | ||
|             customizeIconProps={{
 | ||
|               loading,
 | ||
|               searchValue: mergedSearchValue,
 | ||
|               open: mergedOpen,
 | ||
|               focused: mockFocused,
 | ||
|               showSearch: mergedShowSearch,
 | ||
|             }}
 | ||
|           />
 | ||
|         );
 | ||
|       }
 | ||
| 
 | ||
|       // ============================ Warning =============================
 | ||
|       if (process.env.NODE_ENV !== 'production' && warningProps) {
 | ||
|         warningProps(this.$props);
 | ||
|       }
 | ||
| 
 | ||
|       // ============================= Render =============================
 | ||
|       const mergedClassName = classNames(prefixCls, className, {
 | ||
|         [`${prefixCls}-focused`]: mockFocused,
 | ||
|         [`${prefixCls}-multiple`]: isMultiple,
 | ||
|         [`${prefixCls}-single`]: !isMultiple,
 | ||
|         [`${prefixCls}-allow-clear`]: allowClear,
 | ||
|         [`${prefixCls}-show-arrow`]: mergedShowArrow,
 | ||
|         [`${prefixCls}-disabled`]: disabled,
 | ||
|         [`${prefixCls}-loading`]: loading,
 | ||
|         [`${prefixCls}-open`]: mergedOpen,
 | ||
|         [`${prefixCls}-customize-input`]: customizeInputElement,
 | ||
|         [`${prefixCls}-show-search`]: mergedShowSearch,
 | ||
|       });
 | ||
| 
 | ||
|       return (
 | ||
|         <div
 | ||
|           class={mergedClassName}
 | ||
|           {...domProps}
 | ||
|           ref="containerRef"
 | ||
|           onMousedown={onInternalMouseDown}
 | ||
|           onKeydown={onInternalKeyDown}
 | ||
|           onKeyup={onInternalKeyUp}
 | ||
|           // onFocus={onContainerFocus} // trigger by input
 | ||
|           // onBlur={onContainerBlur} // trigger by input
 | ||
|         >
 | ||
|           {mockFocused && !mergedOpen && (
 | ||
|             <span
 | ||
|               style={{
 | ||
|                 width: 0,
 | ||
|                 height: 0,
 | ||
|                 display: 'flex',
 | ||
|                 overflow: 'hidden',
 | ||
|                 opacity: 0,
 | ||
|               }}
 | ||
|               aria-live="polite"
 | ||
|             >
 | ||
|               {/* Merge into one string to make screen reader work as expect */}
 | ||
|               {`${mergedRawValue.join(', ')}`}
 | ||
|             </span>
 | ||
|           )}
 | ||
|           <SelectTrigger
 | ||
|             ref="triggerRef"
 | ||
|             disabled={disabled}
 | ||
|             prefixCls={prefixCls}
 | ||
|             visible={triggerOpen}
 | ||
|             popupElement={popupNode}
 | ||
|             containerWidth={containerWidth}
 | ||
|             animation={animation}
 | ||
|             transitionName={transitionName}
 | ||
|             dropdownStyle={dropdownStyle}
 | ||
|             dropdownClassName={dropdownClassName}
 | ||
|             direction={direction}
 | ||
|             dropdownMatchSelectWidth={dropdownMatchSelectWidth}
 | ||
|             dropdownRender={dropdownRender}
 | ||
|             dropdownAlign={dropdownAlign}
 | ||
|             getPopupContainer={getPopupContainer}
 | ||
|             empty={!mergedOptions.length}
 | ||
|             getTriggerDOMNode={() => selectorDomRef.current as any}
 | ||
|           >
 | ||
|             <Selector
 | ||
|               {...this.$props}
 | ||
|               domRef={selectorDomRef}
 | ||
|               prefixCls={prefixCls}
 | ||
|               inputElement={customizeInputElement}
 | ||
|               ref="selectorRef"
 | ||
|               id={mergedId}
 | ||
|               showSearch={mergedShowSearch}
 | ||
|               mode={mode}
 | ||
|               accessibilityIndex={accessibilityIndex}
 | ||
|               multiple={isMultiple}
 | ||
|               tagRender={tagRender}
 | ||
|               values={displayValues}
 | ||
|               open={mergedOpen}
 | ||
|               onToggleOpen={onToggleOpen}
 | ||
|               searchValue={mergedSearchValue}
 | ||
|               activeValue={activeValue}
 | ||
|               onSearch={triggerSearch}
 | ||
|               onSearchSubmit={onSearchSubmit}
 | ||
|               onSelect={onInternalSelectionSelect}
 | ||
|               tokenWithEnter={tokenWithEnter}
 | ||
|             />
 | ||
|           </SelectTrigger>
 | ||
| 
 | ||
|           {arrowNode}
 | ||
|           {clearNode}
 | ||
|         </div>
 | ||
|       );
 | ||
|     },
 | ||
|   });
 | ||
|   Select.inheritAttrs = false;
 | ||
|   Select.props = initDefaultProps(BaseProps(), {});
 | ||
|   return Select;
 | ||
| }
 |