chore: [select] remove @ts-nocheck directive (#19098)

* chore: [select] remove @ts-nocheck directive

* Update packages/components/select/src/option.vue

Co-authored-by: qiang <qw13131wang@gmail.com>

* chore: delete useless type

* Update packages/components/select/src/useSelect.ts

Co-authored-by: qiang <qw13131wang@gmail.com>

* fix: type error

* fix: type error

* chore: use buildProps

* chore: add type

* Update packages/components/select/src/useSelect.ts

Co-authored-by: btea <2356281422@qq.com>

* chore: fix type

* chore: fix type

* chore: fix types

---------

Co-authored-by: warmthsea <2586244885@qq.com>
Co-authored-by: sea <45450994+warmthsea@users.noreply.github.com>
Co-authored-by: qiang <qw13131wang@gmail.com>
Co-authored-by: btea <2356281422@qq.com>
This commit is contained in:
dopamine
2025-03-29 17:58:52 +08:00
committed by GitHub
parent 36efad5eaf
commit 38cbfbb0b0
12 changed files with 254 additions and 138 deletions

View File

@@ -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<typeof Select> & {
@@ -18,3 +18,10 @@ export const ElOptionGroup: SFCWithInstall<typeof OptionGroup> =
withNoopInstall(OptionGroup)
export * from './src/token'
export * from './src/select'
export type {
SelectContext,
OptionPublicInstance as SelectOptionProxy,
OptionBasic,
} from './src/type'

View File

@@ -10,11 +10,11 @@
</template>
<script lang="ts">
// @ts-nocheck
import {
computed,
defineComponent,
getCurrentInstance,
isVNode,
onMounted,
provide,
reactive,
@@ -22,10 +22,13 @@ import {
toRefs,
} from 'vue'
import { useMutationObserver } from '@vueuse/core'
import { ensureArray } from '@element-plus/utils'
import { ensureArray, isArray } from '@element-plus/utils'
import { useNamespace } from '@element-plus/hooks'
import { selectGroupKey } from './token'
import type { Component, VNode, VNodeArrayChildren } from 'vue'
import type { OptionInternalInstance, OptionPublicInstance } from './type'
export default defineComponent({
name: 'ElOptionGroup',
componentName: 'ElOptionGroup',
@@ -42,9 +45,9 @@ export default defineComponent({
},
setup(props) {
const ns = useNamespace('select')
const groupRef = ref(null)
const instance = getCurrentInstance()
const children = ref([])
const groupRef = ref<HTMLElement>()
const instance = getCurrentInstance()!
const children = ref<OptionPublicInstance[]>([])
provide(
selectGroupKey,
@@ -57,18 +60,22 @@ export default defineComponent({
children.value.some((option) => option.visible === true)
)
const isOption = (node) =>
node.type?.name === 'ElOption' && !!node.component?.proxy
const isOption = (
node: VNode
): node is VNode & { component: OptionInternalInstance } =>
(node.type as Component).name === 'ElOption' && !!node.component?.proxy
// get all instances of options
const flattedChildren = (node) => {
const Nodes = ensureArray(node)
const children = []
const flattedChildren = (node: VNode | VNodeArrayChildren) => {
const nodes = ensureArray(node) as VNode[] | VNodeArrayChildren
const children: OptionPublicInstance[] = []
nodes.forEach((child) => {
if (!isVNode(child)) return
Nodes.forEach((child) => {
if (isOption(child)) {
children.push(child.component.proxy)
} else if (child.children?.length) {
} else if (isArray(child.children) && child.children.length) {
children.push(...flattedChildren(child.children))
} else if (child.component?.subTree) {
children.push(...flattedChildren(child.component.subTree))

View File

@@ -0,0 +1,23 @@
import { buildProps } from '@element-plus/utils'
export const COMPONENT_NAME = 'ElOption'
export const optionProps = buildProps({
/**
* @description value of option
*/
value: {
type: [String, Number, Boolean, Object],
required: true as const,
},
/**
* @description label of option, same as `value` if omitted
*/
label: {
type: [String, Number],
},
created: Boolean,
/**
* @description whether option is disabled
*/
disabled: Boolean,
})

View File

@@ -16,7 +16,6 @@
</template>
<script lang="ts">
// @ts-nocheck
import {
computed,
defineComponent,
@@ -29,30 +28,15 @@ import {
} from 'vue'
import { useId, useNamespace } from '@element-plus/hooks'
import { useOption } from './useOption'
import type { SelectOptionProxy } from './token'
import { COMPONENT_NAME, optionProps } from './option'
import type { OptionExposed, OptionInternalInstance, OptionStates } from './type'
export default defineComponent({
name: 'ElOption',
componentName: 'ElOption',
name: COMPONENT_NAME,
componentName: COMPONENT_NAME,
props: {
/**
* @description value of option
*/
value: {
required: true,
type: [String, Number, Boolean, Object],
},
/**
* @description label of option, same as `value` if omitted
*/
label: [String, Number],
created: Boolean,
/**
* @description whether option is disabled
*/
disabled: Boolean,
},
props: optionProps,
setup(props) {
const ns = useNamespace('select')
@@ -65,7 +49,7 @@ export default defineComponent({
ns.is('hovering', unref(hover)),
])
const states = reactive({
const states = reactive<OptionStates>({
index: -1,
groupDisabled: false,
visible: true,
@@ -83,7 +67,7 @@ export default defineComponent({
const { visible, hover } = toRefs(states)
const vm = getCurrentInstance().proxy as unknown as SelectOptionProxy
const vm = (getCurrentInstance()! as OptionInternalInstance).proxy
select.onOptionCreate(vm)
@@ -116,13 +100,14 @@ export default defineComponent({
itemSelected,
isDisabled,
select,
hoverItem,
updateOption,
visible,
hover,
selectOptionClick,
states,
}
hoverItem,
updateOption,
selectOptionClick,
} satisfies OptionExposed
},
})
</script>

View File

@@ -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

View File

@@ -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<OptionValue | OptionValue[]>([
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<typeof SelectProps>
export type SelectEmits = EmitFn<typeof selectEmits>
export type SelectInstance = InstanceType<typeof Select> & unknown

View File

@@ -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(() => {

View File

@@ -1,45 +1,8 @@
import type { ExtractPropTypes, InjectionKey } from 'vue'
import type { SelectProps } from './select'
interface SelectGroupContext {
disabled: boolean
}
export interface SelectContext {
props: ExtractPropTypes<typeof SelectProps>
states: any
expanded: boolean
selectRef: HTMLElement
optionsArray: any[]
setSelected(): void
onOptionCreate(vm: SelectOptionProxy): void
onOptionDestroy(
key: number | string | Record<string, string>,
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<SelectGroupContext> =
Symbol('ElSelectGroup')
export const selectKey: InjectionKey<SelectContext> = Symbol('ElSelect')
export interface SelectOptionProxy {
value: string | number | Record<string, string>
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<typeof SelectProps>

View File

@@ -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<OptionValue, OptionPublicInstance>
cachedOptions: Map<OptionValue, OptionPublicInstance>
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<typeof optionProps>
export interface OptionStates {
index: number
groupDisabled: boolean
visible: boolean
hover: boolean
}
export interface OptionExposed {
ns: unknown
id: unknown
containerKls: unknown
currentLabel: ComputedRef<string | number | boolean>
itemSelected: ComputedRef<boolean>
isDisabled: ComputedRef<boolean>
visible: Ref<boolean>
hover: Ref<boolean>
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']
}

View File

@@ -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: <el-select><el-option /></el-select/>')
}
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 = <T>(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(

View File

@@ -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<SelectStates>({
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<HTMLElement>(null)
const selectionRef = ref<HTMLElement>(null)
const tooltipRef = ref<TooltipInstance | null>(null)
const tagTooltipRef = ref<TooltipInstance | null>(null)
const inputRef = ref<HTMLInputElement | null>(null)
const prefixRef = ref<HTMLElement>(null)
const suffixRef = ref<HTMLElement>(null)
const menuRef = ref<HTMLElement>(null)
const tagMenuRef = ref<HTMLElement>(null)
const collapseItemRef = ref<HTMLElement>(null)
const scrollbarRef = ref<{
handleScroll: () => void
} | null>(null)
const selectRef = ref<HTMLElement>()
const selectionRef = ref<HTMLElement>()
const tooltipRef = ref<TooltipInstance>()
const tagTooltipRef = ref<TooltipInstance>()
const inputRef = ref<HTMLInputElement>()
const prefixRef = ref<HTMLElement>()
const suffixRef = ref<HTMLElement>()
const menuRef = ref<HTMLElement>()
const tagMenuRef = ref<HTMLElement>()
const collapseItemRef = ref<HTMLElement>()
const scrollbarRef = ref<ScrollbarInstance>()
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

View File

@@ -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)
}
})