/** * 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, DropdownRender, } 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'; import warning from '../_util/warning'; 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.oneOfType([PropTypes.number, PropTypes.string]), // 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 { 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; 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?: DropdownRender; 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 | string; // Events onKeyup?: EventHandlerNonNull; onKeydown?: EventHandlerNonNull; onPopupScroll?: EventHandlerNonNull; onDropdownVisibleChange?: (open: boolean) => void; onSelect?: (value: SingleType, option: OptionsType[number]) => void; onDeselect?: (value: SingleType, 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 { prefixCls: string; components: { optionList: DefineComponent & { 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; /** Convert single raw value into { label, value } format. Will be called by each value */ getLabeledValue: GetLabeledValue>; filterOptions: FilterOptions; findValueOption: // Need still support legacy ts api | ((values: RawValueType[], options: FlattenOptionsType) => OptionsType) // New API add prevValueOptions support | (( values: RawValueType[], options: FlattenOptionsType, info?: { prevValueOptions?: OptionsType[] }, ) => OptionsType); /** Check if a value is disabled */ isValueDisabled: (value: RawValueType, options: FlattenOptionsType) => 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) { const { prefixCls: defaultPrefixCls, components: { optionList: OptionList }, convertChildrenToData, flattenOptions, getLabeledValue, filterOptions, isValueDisabled, findValueOption, warningProps, fillOptionsWithMissingValue, omitDOMProps, } = config as any; const Select = defineComponent>({ name: 'Select', setup(props: SelectProps) { const useInternalProps = computed( () => props.internalProps && props.internalProps.mark === INTERNAL_PROPS_MARK, ); warning( props.optionFilterProp !== 'children', 'Select', 'optionFilterProp not support children, please use label instead', ); 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(() => { 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(() => { 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; 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>(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; 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([...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([...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 => window.clearTimeout(timeoutId)); activeTimeoutIds.splice(0, activeTimeoutIds.length); }); onBeforeUnmount(() => { activeTimeoutIds.forEach(timeoutId => window.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 = window.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; // ============================= 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 = ( ); // ============================= 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 = ( × ); } // ============================= Arrow ============================== const mergedShowArrow = showArrow !== undefined ? showArrow : loading || (!isMultiple && mode !== 'combobox'); let arrowNode: VNode | JSX.Element; if (mergedShowArrow) { arrowNode = ( ); } // ============================ 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 (
{mockFocused && !mergedOpen && ( {/* Merge into one string to make screen reader work as expect */} {`${mergedRawValue.join(', ')}`} )} selectorDomRef.current} > {arrowNode} {clearNode}
); }, }); Select.inheritAttrs = false; Select.props = initDefaultProps(BaseProps(), {}); return Select; }