From 067028ba3cbb3cb02363bbd38e9c3a66da8da893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=95=E6=9F=8F=E7=84=B6?= <993967177@qq.com> Date: Sun, 13 Aug 2023 21:41:13 +0800 Subject: [PATCH] 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 --- .../select/__tests__/select.test.ts | 64 ++++++++++++++++++- packages/components/select/src/useSelect.ts | 21 +++++- 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/packages/components/select/__tests__/select.test.ts b/packages/components/select/__tests__/select.test.ts index fd891fec1d..14a5c08e77 100644 --- a/packages/components/select/__tests__/select.test.ts +++ b/packages/components/select/__tests__/select.test.ts @@ -1914,10 +1914,10 @@ describe('Select', () => { await nextTick() expect(innerInputEl.placeholder).toBe('') - selectInput.trigger('keydown', { key: EVENT_CODE.backspace, }) + await nextTick() expect(innerInputEl.placeholder).toBe(placeholder) vi.useRealTimers() @@ -2297,5 +2297,67 @@ describe('Select', () => { expect(vm.value).toBe(2) 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( + ` + + + + + `, + () => ({ + 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) + }) }) }) diff --git a/packages/components/select/src/useSelect.ts b/packages/components/select/src/useSelect.ts index 9b857ae413..d5a118a2a8 100644 --- a/packages/components/select/src/useSelect.ts +++ b/packages/components/select/src/useSelect.ts @@ -11,7 +11,12 @@ import { watch, } from 'vue' 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 { CHANGE_EVENT, EVENT_CODE, @@ -40,6 +45,7 @@ export function useSelectStates(props) { return reactive({ options: new Map(), cachedOptions: new Map(), + disabledOptions: new Map(), createdLabel: null, createdSelected: false, 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) => { if (e.code === EVENT_CODE.delete) return if (e.target.value.length <= 0 && !toggleLastOptionHitState()) { 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) emitChange(value) } @@ -754,6 +765,7 @@ export const useSelect = (props, states: States, ctx) => { states.filteredOptionsCount++ states.options.set(vm.value, vm) states.cachedOptions.set(vm.value, vm) + vm.disabled && states.disabledOptions.set(vm.value, vm) } const onOptionDestroy = (key, vm: SelectOptionProxy) => { @@ -772,7 +784,10 @@ export const useSelect = (props, states: States, ctx) => { const toggleLastOptionHitState = (hit?: boolean) => { 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 (hit === true || hit === false) {