diff --git a/packages/components/select/index.ts b/packages/components/select/index.ts index 7cab0e4485..e89e49e2bc 100644 --- a/packages/components/select/index.ts +++ b/packages/components/select/index.ts @@ -1,8 +1,8 @@ import { withInstall, withNoopInstall } from '@element-plus/utils' - import Select from './src/select.vue' import Option from './src/option.vue' import OptionGroup from './src/option-group.vue' + import type { SFCWithInstall } from '@element-plus/utils' export const ElSelect: SFCWithInstall & { @@ -18,3 +18,10 @@ export const ElOptionGroup: SFCWithInstall = withNoopInstall(OptionGroup) export * from './src/token' +export * from './src/select' + +export type { + SelectContext, + OptionPublicInstance as SelectOptionProxy, + OptionBasic, +} from './src/type' diff --git a/packages/components/select/src/option-group.vue b/packages/components/select/src/option-group.vue index db4e653998..76e5411d69 100644 --- a/packages/components/select/src/option-group.vue +++ b/packages/components/select/src/option-group.vue @@ -10,11 +10,11 @@ diff --git a/packages/components/select/src/options.ts b/packages/components/select/src/options.ts index 70dfd73b96..4d7052040c 100644 --- a/packages/components/select/src/options.ts +++ b/packages/components/select/src/options.ts @@ -2,17 +2,19 @@ import { defineComponent, inject } from 'vue' import { isEqual } from 'lodash-unified' import { isArray, isFunction, isString } from '@element-plus/utils' import { selectKey } from './token' + import type { Component, VNode, VNodeNormalizedChildren } from 'vue' +import type { OptionValue } from './type' export default defineComponent({ name: 'ElOptions', setup(_, { slots }) { const select = inject(selectKey) - let cachedValueList: any[] = [] + let cachedValueList: OptionValue[] = [] return () => { const children = slots.default?.()! - const valueList: any[] = [] + const valueList: OptionValue[] = [] function filterOptions(children?: VNodeNormalizedChildren) { if (!isArray(children)) return diff --git a/packages/components/select/src/select.ts b/packages/components/select/src/select.ts index 6b53720b09..6bc9cb0599 100644 --- a/packages/components/select/src/select.ts +++ b/packages/components/select/src/select.ts @@ -1,18 +1,29 @@ import { placements } from '@popperjs/core' +import { scrollbarEmits } from 'element-plus' import { useAriaProps, useEmptyValuesProps, useSizeProp, } from '@element-plus/hooks' -import { buildProps, definePropType, iconPropType } from '@element-plus/utils' +import { + EmitFn, + buildProps, + definePropType, + iconPropType, +} from '@element-plus/utils' import { useTooltipContentProps } from '@element-plus/components/tooltip' import { ArrowDown, CircleClose } from '@element-plus/icons-vue' import { tagProps } from '@element-plus/components/tag' +import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '@element-plus/constants' + +import type { ExtractPropTypes } from 'vue' +import type Select from './select.vue' import type { Options, Placement, PopperEffect, } from '@element-plus/components/popper' +import type { OptionValue } from './type' export const SelectProps = buildProps({ /** @@ -27,7 +38,13 @@ export const SelectProps = buildProps({ * @description binding value */ modelValue: { - type: [Array, String, Number, Boolean, Object], + type: definePropType([ + Array, + String, + Number, + Boolean, + Object, + ]), default: undefined, }, /** @@ -252,3 +269,19 @@ export const SelectProps = buildProps({ ...useEmptyValuesProps, ...useAriaProps(['ariaLabel']), }) +/* eslint-disable @typescript-eslint/no-unused-vars */ +export const selectEmits = { + [UPDATE_MODEL_EVENT]: (val: ISelectProps['modelValue']) => true, + [CHANGE_EVENT]: (val: ISelectProps['modelValue']) => true, + 'popup-scroll': scrollbarEmits.scroll, + 'remove-tag': (val: unknown) => true, + 'visible-change': (visible: boolean) => true, + focus: (evt: FocusEvent) => evt instanceof FocusEvent, + blur: (evt: FocusEvent) => evt instanceof FocusEvent, + clear: () => true, +} +/* eslint-enable @typescript-eslint/no-unused-vars */ + +export type ISelectProps = ExtractPropTypes +export type SelectEmits = EmitFn +export type SelectInstance = InstanceType & unknown diff --git a/packages/components/select/src/select.vue b/packages/components/select/src/select.vue index 802c00e69d..1595319fab 100644 --- a/packages/components/select/src/select.vue +++ b/packages/components/select/src/select.vue @@ -314,9 +314,9 @@ import ElSelectMenu from './select-dropdown.vue' import { useSelect } from './useSelect' import { selectKey } from './token' import ElOptions from './options' - import { SelectProps } from './select' -import type { SelectContext } from './token' + +import type { SelectContext } from './type' const COMPONENT_NAME = 'ElSelect' export default defineComponent({ @@ -370,13 +370,13 @@ export default defineComponent({ reactive({ props: _props, states: API.states, + selectRef: API.selectRef, optionsArray: API.optionsArray, + setSelected: API.setSelected, handleOptionSelect: API.handleOptionSelect, onOptionCreate: API.onOptionCreate, onOptionDestroy: API.onOptionDestroy, - selectRef: API.selectRef, - setSelected: API.setSelected, - }) as unknown as SelectContext + }) satisfies SelectContext ) const selectedLabel = computed(() => { diff --git a/packages/components/select/src/token.ts b/packages/components/select/src/token.ts index b976746ef9..df24d0ee84 100644 --- a/packages/components/select/src/token.ts +++ b/packages/components/select/src/token.ts @@ -1,45 +1,8 @@ -import type { ExtractPropTypes, InjectionKey } from 'vue' -import type { SelectProps } from './select' - -interface SelectGroupContext { - disabled: boolean -} - -export interface SelectContext { - props: ExtractPropTypes - states: any - expanded: boolean - selectRef: HTMLElement - optionsArray: any[] - setSelected(): void - onOptionCreate(vm: SelectOptionProxy): void - onOptionDestroy( - key: number | string | Record, - vm: SelectOptionProxy - ): void - handleOptionSelect(vm: SelectOptionProxy): void -} +import type { InjectionKey } from 'vue' +import type { SelectContext, SelectGroupContext } from './type' // For individual build sharing injection key, we had to make `Symbol` to string export const selectGroupKey: InjectionKey = Symbol('ElSelectGroup') export const selectKey: InjectionKey = Symbol('ElSelect') - -export interface SelectOptionProxy { - value: string | number | Record - label: string | number - created: boolean - disabled: boolean - currentLabel: string - itemSelected: boolean - isDisabled: boolean - select: SelectContext - hoverItem: () => void - updateOption: (query: string) => void - visible: boolean - hover: boolean - selectOptionClick: () => void -} - -export type ISelectProps = ExtractPropTypes diff --git a/packages/components/select/src/type.ts b/packages/components/select/src/type.ts new file mode 100644 index 0000000000..37dfc4b9b3 --- /dev/null +++ b/packages/components/select/src/type.ts @@ -0,0 +1,72 @@ +import type { + ComponentInternalInstance, + ComponentPublicInstance, + ComputedRef, + ExtractPropTypes, + Ref, +} from 'vue' +import type { ISelectProps } from './select' +import type { optionProps } from './option' +export interface SelectGroupContext { + disabled: boolean +} +export interface SelectContext { + props: ISelectProps + states: SelectStates + selectRef: HTMLElement | undefined + optionsArray: OptionPublicInstance[] + setSelected(): void + onOptionCreate(vm: OptionPublicInstance): void + onOptionDestroy(key: OptionValue, vm: OptionPublicInstance): void + handleOptionSelect(vm: OptionPublicInstance): void +} +export type SelectStates = { + inputValue: string + options: Map + cachedOptions: Map + optionValues: OptionValue[] + selected: OptionBasic[] + hoveringIndex: number + inputHovering: boolean + selectionWidth: number + collapseItemWidth: number + previousQuery: string | null + selectedLabel: string + menuVisibleOnFocus: boolean + isBeforeHide: boolean +} +export type OptionProps = ExtractPropTypes +export interface OptionStates { + index: number + groupDisabled: boolean + visible: boolean + hover: boolean +} +export interface OptionExposed { + ns: unknown + id: unknown + containerKls: unknown + currentLabel: ComputedRef + itemSelected: ComputedRef + isDisabled: ComputedRef + visible: Ref + hover: Ref + states: OptionStates + select: SelectContext + hoverItem: () => void + updateOption: (query: string) => void + selectOptionClick: () => void +} +export type OptionPublicInstance = ComponentPublicInstance< + OptionProps, + OptionExposed +> +export type OptionInternalInstance = ComponentInternalInstance & { + proxy: OptionPublicInstance +} +export type OptionValue = OptionProps['value'] +export type OptionBasic = { + value: OptionValue + currentLabel: OptionPublicInstance['currentLabel'] + isDisabled?: OptionPublicInstance['isDisabled'] +} diff --git a/packages/components/select/src/useOption.ts b/packages/components/select/src/useOption.ts index 1c7180ed1c..e7d321d76b 100644 --- a/packages/components/select/src/useOption.ts +++ b/packages/components/select/src/useOption.ts @@ -1,12 +1,22 @@ -// @ts-nocheck import { computed, getCurrentInstance, inject, toRaw, watch } from 'vue' import { get, isEqual } from 'lodash-unified' -import { ensureArray, escapeStringRegexp, isObject } from '@element-plus/utils' +import { + ensureArray, + escapeStringRegexp, + isObject, + throwError, +} from '@element-plus/utils' import { selectGroupKey, selectKey } from './token' +import { COMPONENT_NAME } from './option' -export function useOption(props, states) { +import type { OptionInternalInstance, OptionProps, OptionStates } from './type' + +export function useOption(props: OptionProps, states: OptionStates) { // inject const select = inject(selectKey) + if (!select) { + throwError(COMPONENT_NAME, 'usage: ') + } const selectGroup = inject(selectGroupKey, { disabled: false }) // computed @@ -39,9 +49,8 @@ export function useOption(props, states) { return props.disabled || states.groupDisabled || limitReached.value }) - const instance = getCurrentInstance() - - const contains = (arr = [], target) => { + const instance = getCurrentInstance()! as OptionInternalInstance + const contains = (arr: T[] = [], target: T) => { if (!isObject(props.value)) { return arr && arr.includes(target) } else { @@ -63,7 +72,7 @@ export function useOption(props, states) { const updateOption = (query: string) => { const regexp = new RegExp(escapeStringRegexp(query), 'i') - states.visible = regexp.test(currentLabel.value) || props.created + states.visible = regexp.test(String(currentLabel.value)) || props.created } watch( diff --git a/packages/components/select/src/useSelect.ts b/packages/components/select/src/useSelect.ts index ea0dde067f..f86a8d197d 100644 --- a/packages/components/select/src/useSelect.ts +++ b/packages/components/select/src/useSelect.ts @@ -1,5 +1,5 @@ -// @ts-nocheck import { + Component, computed, nextTick, onMounted, @@ -49,20 +49,22 @@ import { } from '@element-plus/components/form' import type { TooltipInstance } from '@element-plus/components/tooltip' -import type { ISelectProps, SelectOptionProxy } from './token' +import type { ScrollbarInstance } from '@element-plus/components/scrollbar' +import type { ISelectProps, SelectEmits } from './select' +import type { OptionPublicInstance, OptionValue, SelectStates } from './type' -export const useSelect = (props: ISelectProps, emit) => { +export const useSelect = (props: ISelectProps, emit: SelectEmits) => { const { t } = useLocale() const contentId = useId() const nsSelect = useNamespace('select') const nsInput = useNamespace('input') - const states = reactive({ + const states = reactive({ inputValue: '', options: new Map(), cachedOptions: new Map(), - optionValues: [] as any[], // sorted value of options - selected: [] as any[], + optionValues: [], // sorted value of options + selected: [], selectionWidth: 0, collapseItemWidth: 0, selectedLabel: '', @@ -74,19 +76,17 @@ export const useSelect = (props: ISelectProps, emit) => { }) // template refs - const selectRef = ref(null) - const selectionRef = ref(null) - const tooltipRef = ref(null) - const tagTooltipRef = ref(null) - const inputRef = ref(null) - const prefixRef = ref(null) - const suffixRef = ref(null) - const menuRef = ref(null) - const tagMenuRef = ref(null) - const collapseItemRef = ref(null) - const scrollbarRef = ref<{ - handleScroll: () => void - } | null>(null) + const selectRef = ref() + const selectionRef = ref() + const tooltipRef = ref() + const tagTooltipRef = ref() + const inputRef = ref() + const prefixRef = ref() + const suffixRef = ref() + const menuRef = ref() + const tagMenuRef = ref() + const collapseItemRef = ref() + const scrollbarRef = ref() const { isComposing, @@ -153,12 +153,14 @@ export const useSelect = (props: ISelectProps, emit) => { : props.suffixIcon ) const iconReverse = computed(() => - nsSelect.is('reverse', iconComponent.value && expanded.value) + nsSelect.is('reverse', !!(iconComponent.value && expanded.value)) ) const validateState = computed(() => formItem?.validateState || '') const validateIcon = computed( - () => ValidateComponentsMap[validateState.value] + () => + validateState.value && + (ValidateComponentsMap[validateState.value] as Component) ) const debounce = computed(() => (props.remote ? 300 : 0)) @@ -192,7 +194,7 @@ export const useSelect = (props: ISelectProps, emit) => { const optionsArray = computed(() => { const list = Array.from(states.options.values()) - const newList = [] + const newList: OptionPublicInstance[] = [] states.optionValues.forEach((item) => { const index = list.findIndex((i) => i.value === item) if (index > -1) { @@ -402,7 +404,7 @@ export const useSelect = (props: ISelectProps, emit) => { } else { states.selectedLabel = '' } - const result: any[] = [] + const result: SelectStates['selected'] = [] if (!isUndefined(props.modelValue)) { ensureArray(props.modelValue).forEach((value) => { result.push(getOption(value)) @@ -411,7 +413,7 @@ export const useSelect = (props: ISelectProps, emit) => { states.selected = result } - const getOption = (value) => { + const getOption = (value: OptionValue) => { let option const isObjectValue = isPlainObject(value) @@ -449,12 +451,12 @@ export const useSelect = (props: ISelectProps, emit) => { } const resetSelectionWidth = () => { - states.selectionWidth = selectionRef.value.getBoundingClientRect().width + states.selectionWidth = selectionRef.value!.getBoundingClientRect().width } const resetCollapseItemWidth = () => { states.collapseItemWidth = - collapseItemRef.value.getBoundingClientRect().width + collapseItemRef.value!.getBoundingClientRect().width } const updateTooltip = () => { @@ -472,8 +474,8 @@ export const useSelect = (props: ISelectProps, emit) => { handleQueryChange(states.inputValue) } - const onInput = (event) => { - states.inputValue = event.target.value + const onInput = (event: Event) => { + states.inputValue = (event.target as HTMLInputElement).value if (props.remote) { debouncedOnInputChange() } else { @@ -485,22 +487,22 @@ export const useSelect = (props: ISelectProps, emit) => { onInputChange() }, debounce.value) - const emitChange = (val) => { + const emitChange = (val: OptionValue | OptionValue[]) => { if (!isEqual(props.modelValue, val)) { emit(CHANGE_EVENT, val) } } - const getLastNotDisabledIndex = (value) => + const getLastNotDisabledIndex = (value: OptionValue[]) => findLastIndex(value, (it) => { const option = states.cachedOptions.get(it) return option && !option.disabled && !option.states.groupDisabled }) - const deletePrevTag = (e) => { + const deletePrevTag = (e: KeyboardEvent) => { if (!props.multiple) return if (e.code === EVENT_CODE.delete) return - if (e.target.value.length <= 0) { + if ((e.target as HTMLInputElement).value.length <= 0) { const value = ensureArray(props.modelValue).slice() const lastNotDisabledIndex = getLastNotDisabledIndex(value) if (lastNotDisabledIndex < 0) return @@ -512,7 +514,10 @@ export const useSelect = (props: ISelectProps, emit) => { } } - const deleteTag = (event, tag) => { + const deleteTag = ( + event: MouseEvent, + tag: OptionPublicInstance | SelectStates['selected'][0] + ) => { const index = states.selected.indexOf(tag) if (index > -1 && !selectDisabled.value) { const value = ensureArray(props.modelValue).slice() @@ -525,9 +530,9 @@ export const useSelect = (props: ISelectProps, emit) => { focus() } - const deleteSelected = (event) => { + const deleteSelected = (event: Event) => { event.stopPropagation() - const value: string | any[] = props.multiple ? [] : valueOnClear.value + const value = props.multiple ? [] : valueOnClear.value if (props.multiple) { for (const item of states.selected) { if (item.isDisabled) value.push(item.value) @@ -541,7 +546,7 @@ export const useSelect = (props: ISelectProps, emit) => { focus() } - const handleOptionSelect = (option) => { + const handleOptionSelect = (option: OptionPublicInstance) => { if (props.multiple) { const value = ensureArray(props.modelValue ?? []).slice() const optionIndex = getValueIndex(value, option) @@ -573,7 +578,7 @@ export const useSelect = (props: ISelectProps, emit) => { }) } - const getValueIndex = (arr: any[] = [], option) => { + const getValueIndex = (arr: OptionValue[], option: OptionPublicInstance) => { if (isUndefined(option)) return -1 if (!isObject(option.value)) return arr.indexOf(option.value) @@ -582,7 +587,12 @@ export const useSelect = (props: ISelectProps, emit) => { }) } - const scrollToOption = (option) => { + const scrollToOption = ( + option: + | OptionPublicInstance + | OptionPublicInstance[] + | SelectStates['selected'] + ) => { const targetOption = isArray(option) ? option[0] : option let target = null @@ -606,12 +616,12 @@ export const useSelect = (props: ISelectProps, emit) => { scrollbarRef.value?.handleScroll() } - const onOptionCreate = (vm: SelectOptionProxy) => { + const onOptionCreate = (vm: OptionPublicInstance) => { states.options.set(vm.value, vm) states.cachedOptions.set(vm.value, vm) } - const onOptionDestroy = (key, vm: SelectOptionProxy) => { + const onOptionDestroy = (key: OptionValue, vm: OptionPublicInstance) => { if (states.options.get(key) === vm) { states.options.delete(key) } @@ -689,7 +699,9 @@ export const useSelect = (props: ISelectProps, emit) => { } } - const getValueKey = (item) => { + const getValueKey = ( + item: OptionPublicInstance | SelectStates['selected'][0] + ) => { return isObject(item.value) ? get(item.value, props.valueKey) : item.value } @@ -717,7 +729,7 @@ export const useSelect = (props: ISelectProps, emit) => { : [] }) - const navigateOptions = (direction) => { + const navigateOptions = (direction: 'prev' | 'next') => { if (!expanded.value) { expanded.value = true return diff --git a/packages/components/tree-select/src/cache-options.ts b/packages/components/tree-select/src/cache-options.ts index b23afd2f39..0f84c53925 100644 --- a/packages/components/tree-select/src/cache-options.ts +++ b/packages/components/tree-select/src/cache-options.ts @@ -27,6 +27,9 @@ export default defineComponent({ () => { props.data.forEach((item) => { if (!select.states.cachedOptions.has(item.value)) { + // TODO: the type of 'item' is not compatible with the type of 'cachedOptions', + // which may indicate potential runtime issues. + // @ts-expect-error select.states.cachedOptions.set(item.value, item) } })