fix(components): [select] backspace delete disabled option (#11995)

* fix(components): [select] backspace delete disabled option

* fix(components): [select] findLastIndex

* fix(components): [select] simple polyfill findLastIndex in test file

* fix(components): [select] add test for backspace

* chore: lint
This commit is contained in:
井柏然 2023-08-13 21:41:13 +08:00 committed by GitHub
parent 5c1306127f
commit 067028ba3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 81 additions and 4 deletions

View File

@ -1914,10 +1914,10 @@ describe('Select', () => {
await nextTick() await nextTick()
expect(innerInputEl.placeholder).toBe('') expect(innerInputEl.placeholder).toBe('')
selectInput.trigger('keydown', { selectInput.trigger('keydown', {
key: EVENT_CODE.backspace, key: EVENT_CODE.backspace,
}) })
await nextTick() await nextTick()
expect(innerInputEl.placeholder).toBe(placeholder) expect(innerInputEl.placeholder).toBe(placeholder)
vi.useRealTimers() vi.useRealTimers()
@ -2297,5 +2297,67 @@ describe('Select', () => {
expect(vm.value).toBe(2) expect(vm.value).toBe(2)
expect(findInnerInput().value).toBe('z') expect(findInnerInput().value).toBe('z')
}) })
// fix: https://github.com/element-plus/element-plus/issues/11991
it('backspace key should not delete disabled options', async () => {
const options = [
{
value: 'Option1',
label: 'Option1',
disable: true,
},
{
value: 'Option2',
label: 'Option2',
disable: false,
},
]
const value = ['Option2', 'Option1']
const wrapper = _mount(
`
<el-select v-model="value"
multiple
filterable
>
<el-option
v-for="option in options"
:key="option.value"
:value="option.value"
:label="option.label"
:disabled="option.disable"
>
</el-option>
</el-select>
`,
() => ({
value,
options,
})
)
await nextTick()
const selectInput = wrapper.find('.el-select__input')
expect(wrapper.findAll('.el-tag').length).toBe(2)
// need trigger keydown twice because first keydown just select option, and second keydown is to delete
await selectInput.trigger('keydown', {
code: EVENT_CODE.backspace,
key: EVENT_CODE.backspace,
})
await selectInput.trigger('keydown', {
code: EVENT_CODE.backspace,
key: EVENT_CODE.backspace,
})
await nextTick()
expect(wrapper.findAll('.el-tag').length).toBe(1)
await selectInput.trigger('keydown', {
code: EVENT_CODE.backspace,
key: EVENT_CODE.backspace,
})
await selectInput.trigger('keydown', {
code: EVENT_CODE.backspace,
key: EVENT_CODE.backspace,
})
await nextTick()
// after the second deletion, an el-tag still exist
expect(wrapper.findAll('.el-tag').length).toBe(1)
})
}) })
}) })

View File

@ -11,7 +11,12 @@ import {
watch, watch,
} from 'vue' } from 'vue'
import { isObject, toRawType } from '@vue/shared' import { isObject, toRawType } from '@vue/shared'
import { get, isEqual, debounce as lodashDebounce } from 'lodash-unified' import {
findLastIndex,
get,
isEqual,
debounce as lodashDebounce,
} from 'lodash-unified'
import { import {
CHANGE_EVENT, CHANGE_EVENT,
EVENT_CODE, EVENT_CODE,
@ -40,6 +45,7 @@ export function useSelectStates(props) {
return reactive({ return reactive({
options: new Map(), options: new Map(),
cachedOptions: new Map(), cachedOptions: new Map(),
disabledOptions: new Map(),
createdLabel: null, createdLabel: null,
createdSelected: false, createdSelected: false,
selected: props.multiple ? [] : ({} as any), selected: props.multiple ? [] : ({} as any),
@ -627,11 +633,16 @@ export const useSelect = (props, states: States, ctx) => {
} }
} }
const getLastNotDisabledIndex = (value) =>
findLastIndex(value, (it) => !states.disabledOptions.has(it))
const deletePrevTag = (e) => { const deletePrevTag = (e) => {
if (e.code === EVENT_CODE.delete) return if (e.code === EVENT_CODE.delete) return
if (e.target.value.length <= 0 && !toggleLastOptionHitState()) { if (e.target.value.length <= 0 && !toggleLastOptionHitState()) {
const value = props.modelValue.slice() const value = props.modelValue.slice()
value.pop() const lastNotDisabledIndex = getLastNotDisabledIndex(value)
if (lastNotDisabledIndex < 0) return
value.splice(lastNotDisabledIndex, 1)
ctx.emit(UPDATE_MODEL_EVENT, value) ctx.emit(UPDATE_MODEL_EVENT, value)
emitChange(value) emitChange(value)
} }
@ -754,6 +765,7 @@ export const useSelect = (props, states: States, ctx) => {
states.filteredOptionsCount++ states.filteredOptionsCount++
states.options.set(vm.value, vm) states.options.set(vm.value, vm)
states.cachedOptions.set(vm.value, vm) states.cachedOptions.set(vm.value, vm)
vm.disabled && states.disabledOptions.set(vm.value, vm)
} }
const onOptionDestroy = (key, vm: SelectOptionProxy) => { const onOptionDestroy = (key, vm: SelectOptionProxy) => {
@ -772,7 +784,10 @@ export const useSelect = (props, states: States, ctx) => {
const toggleLastOptionHitState = (hit?: boolean) => { const toggleLastOptionHitState = (hit?: boolean) => {
if (!Array.isArray(states.selected)) return if (!Array.isArray(states.selected)) return
const option = states.selected[states.selected.length - 1] const lastNotDisabledIndex = getLastNotDisabledIndex(
states.selected.map((it) => it.value)
)
const option = states.selected[lastNotDisabledIndex]
if (!option) return if (!option) return
if (hit === true || hit === false) { if (hit === true || hit === false) {