mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-03 19:58:09 +08:00
* 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:
parent
4c698be458
commit
708b58d191
@ -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,
|
||||
})
|
||||
|
@ -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
|
||||
|
||||
|
@ -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'])
|
||||
})
|
||||
})
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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,
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user