fix(components): [el-cascader] can not use keyboard select node(#3254) (#3260)

* fix(components): [el-cascader] can not use keyboard select node(#3254)

* fix(components): [el-cascader] add filterable keyboard selection test

* fix: switch case block
This commit is contained in:
SongWuKong 2021-10-31 00:25:24 +08:00 committed by GitHub
parent 4c698be458
commit 708b58d191
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 109 additions and 58 deletions

View File

@ -26,7 +26,7 @@ import {
watch,
} from 'vue'
import isEqual from 'lodash/isEqual'
import { EVENT_CODE } from '@element-plus/utils/aria'
import { EVENT_CODE, focusNode, getSibling } from '@element-plus/utils/aria'
import { UPDATE_MODEL_EVENT, CHANGE_EVENT } from '@element-plus/utils/constants'
import isServer from '@element-plus/utils/isServer'
import scrollIntoView from '@element-plus/utils/scroll-into-view'
@ -41,13 +41,7 @@ import ElCascaderMenu from './menu.vue'
import Store from './store'
import Node, { ExpandTrigger } from './node'
import { CommonProps, useCascaderConfig } from './config'
import {
checkNode,
focusNode,
getMenuIndex,
getSibling,
sortByOriginalOrder,
} from './utils'
import { checkNode, getMenuIndex, sortByOriginalOrder } from './utils'
import { CASCADER_PANEL_INJECTION_KEY } from './types'
import type { PropType, Ref } from 'vue'
@ -99,36 +93,23 @@ export default defineComponent({
)
const renderLabelFn = computed(() => props.renderLabel || slots.default)
let oldConfig: typeof config
let oldOptions: CascaderOption[]
const initStore = () => {
const { options } = props
const cfg = config.value
const configTemp = config
if (
oldOptions === undefined ||
oldOptions !== options ||
oldConfig === undefined ||
configTemp !== oldConfig
) {
manualChecked = false
store.value = new Store(options, cfg)
menus.value = [store.value.getNodes()]
manualChecked = false
store.value = new Store(options, cfg)
menus.value = [store.value.getNodes()]
if (cfg.lazy && isEmpty(props.options)) {
initialLoaded = false
lazyLoad(null, () => {
initialLoaded = true
syncCheckedValue(false, true)
})
} else {
if (cfg.lazy && isEmpty(props.options)) {
initialLoaded = false
lazyLoad(null, () => {
initialLoaded = true
syncCheckedValue(false, true)
}
})
} else {
syncCheckedValue(false, true)
}
oldConfig = configTemp
oldOptions = options
}
const lazyLoad: ElCascaderPanelContext['lazyLoad'] = (node, cb) => {
@ -294,7 +275,9 @@ export default defineComponent({
case EVENT_CODE.up:
case EVENT_CODE.down: {
const distance = code === EVENT_CODE.up ? -1 : 1
focusNode(getSibling(target, distance))
focusNode(
getSibling(target, distance, '.el-cascader-node[tabindex="-1"]')
)
break
}
case EVENT_CODE.left: {
@ -337,7 +320,7 @@ export default defineComponent({
})
)
watch([config, () => props.options], () => initStore(), {
watch([config, () => props.options], initStore, {
deep: true,
immediate: true,
})

View File

@ -1,35 +1,12 @@
import type { Nullable } from '@element-plus/utils/types'
import { isLeaf } from '@element-plus/utils/aria'
import type { default as CascaderNode } from './node'
export const isLeaf = (el: HTMLElement) => !el.getAttribute('aria-owns')
export const getSibling = (
el: HTMLElement,
distance: number
): Nullable<Element> => {
const { parentNode } = el
if (!parentNode) return null
const siblings = parentNode.querySelectorAll(
'.el-cascader-node[tabindex="-1"]'
)
const index = Array.prototype.indexOf.call(siblings, el)
return siblings[index + distance] || null
}
export const getMenuIndex = (el: HTMLElement) => {
if (!el) return 0
const pieces = el.id.split('-')
return Number(pieces[pieces.length - 2])
}
export const focusNode = (el) => {
if (!el) return
el.focus()
!isLeaf(el) && el.click()
}
export const checkNode = (el) => {
if (!el) return

View File

@ -1,5 +1,7 @@
import { nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import { EVENT_CODE } from '@element-plus/utils/aria'
import { triggerEvent } from '@element-plus/test-utils'
import { ArrowDown, Check, CircleClose } from '@element-plus/icons'
import Cascader from '../src/index.vue'
@ -26,6 +28,8 @@ const TRIGGER = '.el-cascader'
const NODE = '.el-cascader-node'
const TAG = '.el-tag'
const SUGGESTION_ITEM = '.el-cascader__suggestion-item'
const SUGGESTION_PANEL = '.el-cascader__suggestion-panel'
const DROPDOWN = '.el-cascader__dropdown'
const _mount: typeof mount = (options) =>
mount(
@ -326,4 +330,41 @@ describe('Cascader.vue', () => {
expect(filterMethod).toBeCalled()
expect(hzSuggestion.textContent).toBe('Zhejiang / Hangzhou')
})
test('filterable keyboard selection', async () => {
const wrapper = _mount({
template: `
<cascader
v-model="value"
:options="options"
filterable
/>
`,
data() {
return {
options: OPTIONS,
value: [],
}
},
})
const input = wrapper.find('input')
const dropdown = document.querySelector(DROPDOWN)
input.element.value = 'h'
await input.trigger('input')
const suggestionsPanel = document.querySelector(
SUGGESTION_PANEL
) as HTMLDivElement
const suggestions = dropdown.querySelectorAll(
SUGGESTION_ITEM
) as NodeListOf<HTMLElement>
const hzSuggestion = suggestions[0]
triggerEvent(suggestionsPanel, 'keydown', EVENT_CODE.down)
expect(document.activeElement.textContent).toBe('Zhejiang / Hangzhou')
triggerEvent(hzSuggestion, 'keydown', EVENT_CODE.down)
expect(document.activeElement.textContent).toBe('Zhejiang / Ningbo')
triggerEvent(hzSuggestion, 'keydown', EVENT_CODE.enter)
await nextTick()
expect(wrapper.vm.value).toEqual(['zhejiang', 'hangzhou'])
})
})

View File

@ -117,6 +117,7 @@
tag="ul"
class="el-cascader__suggestion-panel"
view-class="el-cascader__suggestion-list"
@keydown="handleSuggestionKeyDown"
>
<template v-if="suggestions.length">
<li
@ -170,7 +171,7 @@ import { elFormKey, elFormItemKey } from '@element-plus/tokens'
import { ClickOutside as Clickoutside } from '@element-plus/directives'
import { useLocaleInject } from '@element-plus/hooks'
import { EVENT_CODE } from '@element-plus/utils/aria'
import { EVENT_CODE, focusNode, getSibling } from '@element-plus/utils/aria'
import { UPDATE_MODEL_EVENT, CHANGE_EVENT } from '@element-plus/utils/constants'
import isServer from '@element-plus/utils/isServer'
import { useGlobalConfig } from '@element-plus/utils/util'
@ -564,6 +565,33 @@ export default defineComponent({
}
}
const handleSuggestionKeyDown = (e: KeyboardEvent) => {
const target = e.target as HTMLElement
const { code } = e
switch (code) {
case EVENT_CODE.up:
case EVENT_CODE.down: {
const distance = code === EVENT_CODE.up ? -1 : 1
focusNode(
getSibling(
target,
distance,
'.el-cascader__suggestion-item[tabindex="-1"]'
)
)
break
}
case EVENT_CODE.enter:
target.click()
break
case EVENT_CODE.esc:
case EVENT_CODE.tab:
togglePopperVisible(false)
break
}
}
const handleDelete = () => {
const tags = presentTags.value
const lastTag = tags[tags.length - 1]
@ -664,6 +692,7 @@ export default defineComponent({
handleComposition,
handleClear,
handleSuggestionClick,
handleSuggestionKeyDown,
handleDelete,
handleInput,
}

View File

@ -1,3 +1,4 @@
import type { Nullable } from '@element-plus/utils/types'
export const EVENT_CODE = {
tab: 'Tab',
enter: 'Enter',
@ -120,6 +121,26 @@ export const triggerEvent = function (
return elm
}
export const isLeaf = (el: HTMLElement) => !el.getAttribute('aria-owns')
export const getSibling = (
el: HTMLElement,
distance: number,
elClass: string
): Nullable<Element> => {
const { parentNode } = el
if (!parentNode) return null
const siblings = parentNode.querySelectorAll(elClass)
const index = Array.prototype.indexOf.call(siblings, el)
return siblings[index + distance] || null
}
export const focusNode = (el) => {
if (!el) return
el.focus()
!isLeaf(el) && el.click()
}
const Utils = {
IgnoreUtilFocusChanges: false,
/**