chore(components): [select-v2] remove ts-nocheck comments in select-v2 (#16746)

* chore: remove ts-nocheck comments in select-v2

* take the review comments

* improve emits type

* the keys of emits use camelCase instead of kebab-case

* apply suggestions from code review

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

* reduce duplicate ESLint comments

* use more succinct syntax to define props

* revert emits to kebab-case

* fix: type checking failed

* chore: illustrate why return early in validateIcon

* fix: signature with duplicate parameter names

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

---------

Co-authored-by: btea <2356281422@qq.com>
Co-authored-by: qiang <qw13131wang@gmail.com>
This commit is contained in:
dopamine 2024-09-23 13:45:35 +08:00 committed by GitHub
parent 6f1f506bcb
commit cd517d6743
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 187 additions and 108 deletions

View File

@ -4,7 +4,13 @@ import {
useEmptyValuesProps,
useSizeProp,
} from '@element-plus/hooks'
import { buildProps, definePropType, iconPropType } from '@element-plus/utils'
import {
buildProps,
definePropType,
iconPropType,
isNumber,
} from '@element-plus/utils'
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '@element-plus/constants'
import { useTooltipContentProps } from '@element-plus/components/tooltip'
import { CircleClose } from '@element-plus/icons-vue'
import { tagProps } from '../../tag'
@ -12,6 +18,8 @@ import { defaultProps } from './useProps'
import type { Option, OptionType } from './select.types'
import type { Props } from './useProps'
import type { EmitFn } from '@element-plus/utils/vue/typescript'
import type { ExtractPropTypes } from 'vue'
import type {
Options,
Placement,
@ -269,3 +277,24 @@ export const OptionProps = buildProps({
selected: Boolean,
created: Boolean,
} as const)
/* eslint-disable @typescript-eslint/no-unused-vars */
export const selectEmits = {
[UPDATE_MODEL_EVENT]: (val: ISelectV2Props['modelValue']) => true,
[CHANGE_EVENT]: (val: ISelectV2Props['modelValue']) => true,
'remove-tag': (val: unknown) => true,
'visible-change': (visible: boolean) => true,
focus: (evt: FocusEvent) => evt instanceof FocusEvent,
blur: (evt: FocusEvent) => evt instanceof FocusEvent,
clear: () => true,
}
export const optionEmits = {
hover: (index?: number) => isNumber(index),
select: (val: Option, index?: number) => true,
}
/* eslint-enable @typescript-eslint/no-unused-vars */
export type ISelectV2Props = ExtractPropTypes<typeof SelectProps>
export type IOptionV2Props = ExtractPropTypes<typeof OptionProps>
export type SelectEmitFn = EmitFn<typeof selectEmits>
export type OptionEmitFn = EmitFn<typeof optionEmits>

View File

@ -1,16 +1,16 @@
<template>
<div
:class="ns.be('group', 'title')"
:style="[style, { lineHeight: `${height}px` }]"
:style="{ ...style, lineHeight: `${height}px` }"
>
{{ item.label }}
</div>
</template>
<script lang="ts">
// @ts-nocheck
import { defineComponent } from 'vue'
import { useNamespace } from '@element-plus/hooks'
import type { CSSProperties, PropType } from 'vue'
export default defineComponent({
props: {
@ -18,7 +18,9 @@ export default defineComponent({
type: Object,
required: true,
},
style: Object,
style: {
type: Object as PropType<CSSProperties>,
},
height: Number,
},
setup() {

View File

@ -23,12 +23,12 @@ import { defineComponent, inject } from 'vue'
import { useNamespace } from '@element-plus/hooks'
import { useOption } from './useOption'
import { useProps } from './useProps'
import { OptionProps } from './defaults'
import { OptionProps, optionEmits } from './defaults'
import { selectV2InjectionKey } from './token'
export default defineComponent({
props: OptionProps,
emits: ['select', 'hover'],
emits: optionEmits,
setup(props, { emit }) {
const select = inject(selectV2InjectionKey)!
const ns = useNamespace('select')

View File

@ -21,21 +21,44 @@ import { useProps } from './useProps'
import { selectV2InjectionKey } from './token'
import type { ItemProps } from '@element-plus/components/virtual-list'
import type {
DynamicSizeListInstance,
FixedSizeListInstance,
ItemProps,
} from '@element-plus/components/virtual-list'
import type { Option, OptionItemProps } from './select.types'
import type {
ComponentPublicInstance,
ComputedRef,
ExtractPropTypes,
Ref,
} from 'vue'
const props = {
loading: Boolean,
data: {
type: Array,
required: true as const,
},
hoveringIndex: Number,
width: Number,
}
interface SelectDropdownExposed {
listRef: Ref<FixedSizeListInstance | DynamicSizeListInstance | undefined>
isSized: ComputedRef<boolean>
isItemDisabled: (modelValue: any[] | any, selected: boolean) => boolean
isItemHovering: (target: number) => boolean
isItemSelected: (modelValue: any[] | any, target: Option) => boolean
scrollToItem: (index: number) => void
resetScrollTop: () => void
}
export type SelectDropdownInstance = ComponentPublicInstance<
ExtractPropTypes<typeof props>,
SelectDropdownExposed
>
export default defineComponent({
name: 'ElSelectDropdown',
props: {
loading: Boolean,
data: {
type: Array,
required: true,
},
hoveringIndex: Number,
width: Number,
},
props,
setup(props, { slots, expose }) {
const select = inject(selectV2InjectionKey)!
const ns = useNamespace('select')
@ -43,13 +66,13 @@ export default defineComponent({
const cachedHeights = ref<Array<number>>([])
const listRef = ref()
const listRef = ref<FixedSizeListInstance | DynamicSizeListInstance>()
const size = computed(() => props.data.length)
watch(
() => size.value,
() => {
select.tooltipRef.value.updatePopper?.()
select.tooltipRef.value!.updatePopper?.()
}
)
@ -94,14 +117,20 @@ export default defineComponent({
}
}
const isItemSelected = (modelValue: any[] | any, target: Option) => {
const isItemSelected: SelectDropdownExposed['isItemSelected'] = (
modelValue,
target
) => {
if (select.props.multiple) {
return contains(modelValue, getValue(target))
}
return isEqual(modelValue, getValue(target))
}
const isItemDisabled = (modelValue: any[] | any, selected: boolean) => {
const isItemDisabled: SelectDropdownExposed['isItemDisabled'] = (
modelValue,
selected
) => {
const { disabled, multiple, multipleLimit } = select.props
return (
disabled ||
@ -112,23 +141,23 @@ export default defineComponent({
)
}
const isItemHovering = (target: number) => props.hoveringIndex === target
const isItemHovering: SelectDropdownExposed['isItemHovering'] = (target) =>
props.hoveringIndex === target
const scrollToItem = (index: number) => {
const list = listRef.value as any
const scrollToItem: SelectDropdownExposed['scrollToItem'] = (index) => {
const list = listRef.value
if (list) {
list.scrollToItem(index)
}
}
const resetScrollTop = () => {
const list = listRef.value as any
const resetScrollTop: SelectDropdownExposed['resetScrollTop'] = () => {
const list = listRef.value
if (list) {
list.resetScrollTop()
}
}
expose({
const exposed: SelectDropdownExposed = {
listRef,
isSized,
@ -137,7 +166,8 @@ export default defineComponent({
isItemSelected,
scrollToItem,
resetScrollTop,
})
}
expose(exposed)
const Item = (itemProps: ItemProps<any>) => {
const { index, data, style } = itemProps
@ -151,7 +181,7 @@ export default defineComponent({
<GroupItem
item={item}
style={style}
height={(sized ? itemSize : estimatedSize) as number}
height={sized ? (itemSize as number) : estimatedSize}
/>
)
}
@ -190,7 +220,8 @@ export default defineComponent({
}
const onEscOrTab = () => {
select.expanded = false
// The following line actually doesn't work. Fixing it may introduce a small breaking change for some users, so just comment it out for now.
// select.expanded = false
}
const onKeydown = (e: KeyboardEvent) => {

View File

@ -13,3 +13,18 @@ export type OptionItemProps = {
index: number
disabled: boolean
}
export type SelectStates = {
inputValue: string
cachedOptions: Option[]
createdOptions: Option[]
hoveringIndex: number
inputHovering: boolean
selectionWidth: number
calculatorWidth: number
collapseItemWidth: number
previousQuery: string | null
previousValue: unknown
selectedLabel: string
menuVisibleOnFocus: boolean
isBeforeHide: boolean
}

View File

@ -280,10 +280,9 @@ import { ClickOutside } from '@element-plus/directives'
import ElTooltip from '@element-plus/components/tooltip'
import ElTag from '@element-plus/components/tag'
import ElIcon from '@element-plus/components/icon'
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '@element-plus/constants'
import ElSelectMenu from './select-dropdown'
import useSelect from './useSelect'
import { SelectProps } from './defaults'
import { SelectProps, selectEmits } from './defaults'
import { selectV2InjectionKey } from './token'
export default defineComponent({
@ -296,16 +295,7 @@ export default defineComponent({
},
directives: { ClickOutside },
props: SelectProps,
emits: [
UPDATE_MODEL_EVENT,
CHANGE_EVENT,
'remove-tag',
'clear',
'visible-change',
'focus',
'blur',
],
emits: selectEmits,
setup(props, { emit }) {
const modelValue = computed(() => {
const { modelValue: rawModelValue, multiple } = props
@ -325,19 +315,19 @@ export default defineComponent({
}),
emit
)
// TODO, remove the any cast to align the actual API.
provide(selectV2InjectionKey, {
props: reactive({
...toRefs(props),
height: API.popupHeight,
modelValue,
}),
expanded: API.expanded,
tooltipRef: API.tooltipRef,
onSelect: API.onSelect,
onHover: API.onHover,
onKeyboardNavigate: API.onKeyboardNavigate,
onKeyboardSelect: API.onKeyboardSelect,
} as any)
})
return {
...API,

View File

@ -1,14 +1,14 @@
import type { OptionProps, SelectProps } from './defaults'
import type { ExtractPropTypes, InjectionKey, Ref } from 'vue'
import type { IOptionV2Props, ISelectV2Props } from './defaults'
import type { InjectionKey, Ref } from 'vue'
import type { Option } from './select.types'
import type { TooltipInstance } from '@element-plus/components/tooltip'
export interface SelectV2Context {
props: ExtractPropTypes<typeof SelectProps>
expanded: boolean
tooltipRef: Ref<TooltipInstance>
props: ISelectV2Props
expanded: Ref<boolean>
tooltipRef: Ref<TooltipInstance | undefined>
onSelect: (option: Option) => void
onHover: (idx: number) => void
onHover: (idx?: number) => void
onKeyboardNavigate: (direction: 'forward' | 'backward') => void
onKeyboardSelect: () => void
}
@ -16,5 +16,4 @@ export interface SelectV2Context {
export const selectV2InjectionKey: InjectionKey<SelectV2Context> = Symbol(
'ElSelectV2Injection'
)
export type IOptionV2Props = ExtractPropTypes<typeof OptionProps>
export type ISelectV2Props = ExtractPropTypes<typeof SelectProps>
export type { ISelectV2Props, IOptionV2Props }

View File

@ -1,21 +1,20 @@
// @ts-nocheck
import { computed, ref } from 'vue'
import { useProps } from './useProps'
import type { ISelectV2Props } from './token'
import type { Option } from './select.types'
import type { Option, SelectStates } from './select.types'
export function useAllowCreate(props: ISelectV2Props, states) {
export function useAllowCreate(props: ISelectV2Props, states: SelectStates) {
const { aliasProps, getLabel, getValue } = useProps(props)
const createOptionCount = ref(0)
const cachedSelectedOption = ref<Option>(null)
const cachedSelectedOption = ref<Option>()
const enableAllowCreateMode = computed(() => {
return props.allowCreate && props.filterable
})
function hasExistingOption(query: string) {
const hasOption = (option) => getLabel(option) === query
const hasOption = (option: Option) => getLabel(option) === query
return (
(props.options && props.options.some(hasOption)) ||
states.createdOptions.some(hasOption)

View File

@ -1,7 +1,10 @@
// @ts-nocheck
import type { IOptionV2Props } from './token'
import type { OptionEmitFn } from './defaults'
export function useOption(props: IOptionV2Props, { emit }) {
export function useOption(
props: IOptionV2Props,
{ emit }: { emit: OptionEmitFn }
) {
return {
hoverItem: () => {
if (!props.disabled) {

View File

@ -1,4 +1,3 @@
// @ts-nocheck
import {
computed,
nextTick,
@ -45,13 +44,15 @@ import { ArrowDown } from '@element-plus/icons-vue'
import { useAllowCreate } from './useAllowCreate'
import { useProps } from './useProps'
import type ElTooltip from '@element-plus/components/tooltip'
import type { Option, OptionType } from './select.types'
import type { Option, OptionType, SelectStates } from './select.types'
import type { ISelectV2Props } from './token'
import type { SelectEmitFn } from './defaults'
import type { TooltipInstance } from '@element-plus/components/tooltip'
import type { SelectDropdownInstance } from './select-dropdown'
const MINIMUM_INPUT_WIDTH = 11
const useSelect = (props: ISelectV2Props, emit) => {
const useSelect = (props: ISelectV2Props, emit: SelectEmitFn) => {
// inject
const { t } = useLocale()
const nsSelect = useNamespace('select')
@ -64,10 +65,10 @@ const useSelect = (props: ISelectV2Props, emit) => {
useProps(props)
const { valueOnClear, isEmptyValue } = useEmptyValues(props)
const states = reactive({
const states: SelectStates = reactive({
inputValue: '',
cachedOptions: [] as Option[],
createdOptions: [] as Option[],
cachedOptions: [],
createdOptions: [],
hoveringIndex: -1,
inputHovering: false,
selectionWidth: 0,
@ -84,17 +85,17 @@ const useSelect = (props: ISelectV2Props, emit) => {
const popperSize = ref(-1)
// DOM & Component refs
const selectRef = ref<HTMLElement>(null)
const selectionRef = ref<HTMLElement>(null)
const tooltipRef = ref<InstanceType<typeof ElTooltip> | null>(null)
const tagTooltipRef = ref<InstanceType<typeof ElTooltip> | null>(null)
const inputRef = ref<HTMLElement>(null)
const calculatorRef = ref<HTMLElement>(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 selectRef = ref<HTMLElement>()
const selectionRef = ref<HTMLElement>()
const tooltipRef = ref<TooltipInstance>()
const tagTooltipRef = ref<TooltipInstance>()
const inputRef = ref<HTMLElement>()
const calculatorRef = ref<HTMLElement>()
const prefixRef = ref<HTMLElement>()
const suffixRef = ref<HTMLElement>()
const menuRef = ref<SelectDropdownInstance>()
const tagMenuRef = ref<HTMLElement>()
const collapseItemRef = ref<HTMLElement>()
const {
isComposing,
@ -127,8 +128,8 @@ const useSelect = (props: ISelectV2Props, emit) => {
},
})
const allOptions = ref([])
const filteredOptions = ref([])
const allOptions = ref<OptionType[]>([])
const filteredOptions = ref<OptionType[]>([])
// the controller of the expanded popup
const expanded = ref(false)
@ -163,9 +164,13 @@ const useSelect = (props: ISelectV2Props, emit) => {
)
const validateState = computed(() => elFormItem?.validateState || '')
const validateIcon = computed(
() => ValidateComponentsMap[validateState.value]
)
const validateIcon = computed(() => {
// When we use indexed access to get the type of an undeclared property,
// the unsafe type `any` will be inferred, which TypeScript throws an error to emphasize it.
// To avoid TypeScript complaining about it, we use truthiness narrowing to narrow the type of validateState.
if (!validateState.value) return
return ValidateComponentsMap[validateState.value]
})
const debounce = computed(() => (props.remote ? 300 : 0))
@ -191,7 +196,7 @@ const useSelect = (props: ISelectV2Props, emit) => {
return null
})
const filterOptions = (query) => {
const filterOptions = (query: string) => {
const isValidOption = (o: Option): boolean => {
if (props.filterable && isFunction(props.filterMethod)) return true
if (props.filterable && props.remote && isFunction(props.remoteMethod))
@ -228,8 +233,8 @@ const useSelect = (props: ISelectV2Props, emit) => {
}
const updateOptions = () => {
allOptions.value = filterOptions('') as OptionType[]
filteredOptions.value = filterOptions(states.inputValue) as OptionType[]
allOptions.value = filterOptions('')
filteredOptions.value = filterOptions(states.inputValue)
}
const allOptionsValueMap = computed(() => {
@ -449,7 +454,7 @@ const useSelect = (props: ISelectV2Props, emit) => {
states.previousValue = props.multiple ? String(val) : val
}
const getValueIndex = (arr = [], value: unknown) => {
const getValueIndex = (arr: unknown[] = [], value: unknown) => {
if (!isObject(value)) {
return arr.indexOf(value)
}
@ -474,16 +479,16 @@ const useSelect = (props: ISelectV2Props, emit) => {
}
const resetSelectionWidth = () => {
states.selectionWidth = selectionRef.value.getBoundingClientRect().width
states.selectionWidth = selectionRef.value!.getBoundingClientRect().width
}
const resetCalculatorWidth = () => {
states.calculatorWidth = calculatorRef.value.getBoundingClientRect().width
states.calculatorWidth = calculatorRef.value!.getBoundingClientRect().width
}
const resetCollapseItemWidth = () => {
states.collapseItemWidth =
collapseItemRef.value.getBoundingClientRect().width
collapseItemRef.value!.getBoundingClientRect().width
}
const updateTooltip = () => {
@ -569,7 +574,7 @@ const useSelect = (props: ISelectV2Props, emit) => {
}
}
const getLastNotDisabledIndex = (value) =>
const getLastNotDisabledIndex = (value: unknown[]) =>
findLastIndex(
value,
(it) =>
@ -618,8 +623,8 @@ const useSelect = (props: ISelectV2Props, emit) => {
const onKeyboardNavigate = (
direction: 'forward' | 'backward',
hoveringIndex: number = undefined
) => {
hoveringIndex: number | undefined = undefined
): void => {
const options = filteredOptions.value
if (
!['forward', 'backward'].includes(direction) ||
@ -671,8 +676,8 @@ const useSelect = (props: ISelectV2Props, emit) => {
}
}
const onHoverOption = (idx: number) => {
states.hoveringIndex = idx
const onHoverOption = (idx?: number) => {
states.hoveringIndex = idx ?? -1
}
const updateHoveringIndex = () => {
@ -683,14 +688,14 @@ const useSelect = (props: ISelectV2Props, emit) => {
} else {
states.hoveringIndex = filteredOptions.value.findIndex((item) =>
props.modelValue.some(
(modelValue) => getValueKey(modelValue) === getValueKey(item)
(modelValue: unknown) => getValueKey(modelValue) === getValueKey(item)
)
)
}
}
const onInput = (event) => {
states.inputValue = event.target.value
const onInput = (event: Event) => {
states.inputValue = (event.target as HTMLInputElement).value
if (props.remote) {
debouncedOnInputChange()
} else {
@ -713,10 +718,10 @@ const useSelect = (props: ISelectV2Props, emit) => {
}
const scrollToItem = (index: number) => {
menuRef.value.scrollToItem(index)
menuRef.value!.scrollToItem(index)
}
const getOption = (value: any, cachedOptions?: Option[]) => {
const getOption = (value: unknown, cachedOptions?: Option[]) => {
// match the option with the given value, if not found, create a new option
const selectValue = getValueKey(value)

View File

@ -4,6 +4,8 @@ export { default as FixedSizeGrid } from './src/components/fixed-size-grid'
export { default as DynamicSizeGrid } from './src/components/dynamic-size-grid'
export * from './src/props'
export type { FixedSizeListInstance } from './src/components/fixed-size-list'
export type { DynamicSizeListInstance } from './src/components/dynamic-size-list'
export type { GridInstance } from './src/builders/build-grid'
export type {
DynamicSizeGridInstance,

View File

@ -1,4 +1,3 @@
// @ts-nocheck
import {
Fragment,
computed,
@ -62,7 +61,7 @@ const createList = ({
const dynamicSizeCache = ref(initCache(props, instance))
const getItemStyleCache = useCache()
const getItemStyleCache = useCache<CSSProperties>()
// refs
// here windowRef and innerRef can be type of HTMLElement
// or user defined component type, depends on the type passed
@ -418,7 +417,7 @@ const createList = ({
})
onActivated(() => {
unref(windowRef).scrollTop = unref(states).scrollOffset
unref(windowRef)!.scrollTop = unref(states).scrollOffset
})
const api = {

View File

@ -254,4 +254,5 @@ const DynamicSizeList = createList({
},
})
export type DynamicSizeListInstance = InstanceType<typeof DynamicSizeList>
export default DynamicSizeList

View File

@ -125,4 +125,5 @@ const FixedSizeList = buildList({
},
})
export type FixedSizeListInstance = InstanceType<typeof FixedSizeList>
export default FixedSizeList

View File

@ -4,14 +4,15 @@ import memoOne from 'memoize-one'
import type { VirtualizedProps } from '../props'
export const useCache = () => {
export const useCache = <T>() => {
const vm = getCurrentInstance()!
const props = vm.proxy!.$props as VirtualizedProps
return computed(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _getItemStyleCache = (_: any, __: any, ___: any) => ({})
const _getItemStyleCache = (_: any, __: any, ___: any) =>
({} as Record<string, T>)
return props.perfMode
? memoize(_getItemStyleCache)
: memoOne(_getItemStyleCache)

View File

@ -1,7 +1,9 @@
import type { AppContext, Plugin } from 'vue'
import type { AppContext, EmitsOptions, Plugin, SetupContext } from 'vue'
export type SFCWithInstall<T> = T & Plugin
export type SFCInstallWithContext<T> = SFCWithInstall<T> & {
_context: AppContext | null
}
export type EmitFn<E extends EmitsOptions> = SetupContext<E>['emit']

4
typings/env.d.ts vendored
View File

@ -10,8 +10,8 @@ declare global {
namespace JSX {
interface IntrinsicAttributes {
class?: any
style?: any
class?: unknown
style?: unknown
}
}
}