mirror of
https://github.com/element-plus/element-plus.git
synced 2024-12-27 03:01:14 +08:00
fix(tree): fix keyboard navigation bug (#995)
* fix(tree): fix keyboard navigation bug * test(tree): add navigate test case
This commit is contained in:
parent
e93a728872
commit
eb5a04de6c
@ -1,7 +1,7 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { nextTick } from 'vue'
|
||||
import Tree from '../src/tree.vue'
|
||||
import { sleep } from '@element-plus/test-utils'
|
||||
import { sleep, defineGetter } from '@element-plus/test-utils'
|
||||
|
||||
const ALL_NODE_COUNT = 9
|
||||
|
||||
@ -836,4 +836,212 @@ describe('Tree.vue', () => {
|
||||
expect(treeWrappers[1].vm.getNode(4).data).toEqual(nodeData)
|
||||
})
|
||||
|
||||
test('navigate with defaultExpandAll', () => {
|
||||
const { wrapper } = getTreeVm(``, {
|
||||
template: `
|
||||
<div>
|
||||
<el-tree default-expand-all ref="tree1" :data="data" node-key="id" :props="defaultProps"></el-tree>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
const tree = wrapper.findComponent({ name: 'ElTree' })
|
||||
expect(Object.values(tree.vm.store.nodesMap).filter(item => item.canFocus).length).toBe(9)
|
||||
})
|
||||
|
||||
test('navigate up', async () => {
|
||||
const { wrapper } = getTreeVm(``, {
|
||||
template: `
|
||||
<div>
|
||||
<el-tree ref="tree1" :data="data" node-key="id" :props="defaultProps"></el-tree>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
let flag = false
|
||||
function handleFocus(){
|
||||
return () => (flag = true)
|
||||
}
|
||||
await nextTick()
|
||||
const tree = wrapper.findComponent({ name: 'ElTree' })
|
||||
const targetElement = wrapper.find('div[data-key="3"]').element
|
||||
const fromElement = wrapper.find('div[data-key="1"]').element
|
||||
defineGetter(targetElement, 'focus', handleFocus)
|
||||
tree.vm.setCurrentKey(1)
|
||||
expect(fromElement.classList.contains('is-focusable')).toBeTruthy()
|
||||
fromElement.dispatchEvent(new KeyboardEvent('keydown', { code: 'ArrowUp', bubbles: true, cancelable: false }))
|
||||
expect(flag).toBe(true)
|
||||
})
|
||||
|
||||
test('navigate down', async () => {
|
||||
const { wrapper } = getTreeVm(``, {
|
||||
template: `
|
||||
<div>
|
||||
<el-tree ref="tree1" :data="data" node-key="id" :props="defaultProps"></el-tree>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
let flag = false
|
||||
function handleFocus(){
|
||||
return () => (flag = true)
|
||||
}
|
||||
await nextTick()
|
||||
const tree = wrapper.findComponent({ name: 'ElTree' })
|
||||
const targetElement = wrapper.find('div[data-key="2"]').element
|
||||
const fromElement = wrapper.find('div[data-key="1"]').element
|
||||
defineGetter(targetElement, 'focus', handleFocus)
|
||||
tree.vm.setCurrentKey(1)
|
||||
expect(fromElement.classList.contains('is-focusable')).toBeTruthy()
|
||||
fromElement.dispatchEvent(new KeyboardEvent('keydown', { code: 'ArrowDown', bubbles: true, cancelable: false }))
|
||||
expect(flag).toBe(true)
|
||||
})
|
||||
|
||||
test('navigate with disabled', async () => {
|
||||
const wrapper = mount( {
|
||||
template: `
|
||||
<div>
|
||||
<el-tree ref="tree1" :data="data" node-key="id" :props="defaultProps"></el-tree>
|
||||
</div>
|
||||
`,
|
||||
components: {
|
||||
'el-tree': Tree,
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
data: [{
|
||||
id: 1,
|
||||
label: '一级 1',
|
||||
children: [{
|
||||
id: 11,
|
||||
label: '二级 1-1',
|
||||
children: [{
|
||||
id: 111,
|
||||
label: '三级 1-1',
|
||||
disabled: true,
|
||||
}],
|
||||
}],
|
||||
}, {
|
||||
id: 2,
|
||||
label: '一级 2',
|
||||
disabled: true,
|
||||
children: [{
|
||||
id: 21,
|
||||
label: '二级 2-1',
|
||||
}, {
|
||||
id: 22,
|
||||
label: '二级 2-2',
|
||||
}],
|
||||
}, {
|
||||
id: 3,
|
||||
label: '一级 3',
|
||||
children: [{
|
||||
id: 31,
|
||||
label: '二级 3-1',
|
||||
}, {
|
||||
id: 32,
|
||||
label: '二级 3-2',
|
||||
}],
|
||||
}],
|
||||
defaultProps: {
|
||||
children: 'children',
|
||||
label: 'label',
|
||||
disabled: 'disabled',
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
let flag = false
|
||||
function handleFocus(){
|
||||
return () => (flag = true)
|
||||
}
|
||||
await nextTick()
|
||||
const tree = wrapper.findComponent({ name: 'ElTree' })
|
||||
const targetElement = wrapper.find('div[data-key="3"]').element
|
||||
const fromElement = wrapper.find('div[data-key="1"]').element
|
||||
defineGetter(targetElement, 'focus', handleFocus)
|
||||
tree.vm.setCurrentKey(1)
|
||||
expect(fromElement.classList.contains('is-focusable')).toBeTruthy()
|
||||
fromElement.dispatchEvent(new KeyboardEvent('keydown', { code: 'ArrowDown', bubbles: true, cancelable: false }))
|
||||
expect(flag).toBe(true)
|
||||
})
|
||||
|
||||
test('navigate with lazy and without node-key', async () => {
|
||||
const wrapper = mount( {
|
||||
template: `
|
||||
<div>
|
||||
<el-tree
|
||||
:props="defaultProps"
|
||||
:load="loadNode"
|
||||
lazy
|
||||
show-checkbox>
|
||||
</el-tree>
|
||||
</div>
|
||||
`,
|
||||
components: {
|
||||
'el-tree': Tree,
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
defaultProps: {
|
||||
children: 'children',
|
||||
label: 'label',
|
||||
disabled: 'disabled',
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadNode(node, resolve) {
|
||||
if (node.level === 0) {
|
||||
return resolve([{ name: 'region1' }, { name: 'region2' }])
|
||||
}
|
||||
if (node.level > 3) return resolve([])
|
||||
|
||||
let hasChild
|
||||
if (node.data.name === 'region1') {
|
||||
hasChild = true
|
||||
} else if (node.data.name === 'region2') {
|
||||
hasChild = false
|
||||
} else {
|
||||
hasChild = false
|
||||
}
|
||||
|
||||
let data
|
||||
if (hasChild) {
|
||||
data = [{
|
||||
name: 'zone' + this.count++,
|
||||
}, {
|
||||
name: 'zone' + this.count++,
|
||||
}]
|
||||
} else {
|
||||
data = []
|
||||
}
|
||||
resolve(data)
|
||||
},
|
||||
},
|
||||
})
|
||||
let flag = false
|
||||
function handleFocus(){
|
||||
return () => (flag = !flag)
|
||||
}
|
||||
await nextTick()
|
||||
const tree = wrapper.findComponent({ name: 'ElTree' })
|
||||
const originElements = wrapper.findAll('div[data-key]')
|
||||
const region1 = originElements[0].element
|
||||
const region2 = originElements[1].element
|
||||
defineGetter(region2, 'focus', handleFocus)
|
||||
// expand
|
||||
region1.dispatchEvent(new MouseEvent('click'))
|
||||
expect(region1.classList.contains('is-focusable')).toBeTruthy()
|
||||
await sleep(100)
|
||||
expect(Object.values(tree.vm.store.nodesMap.length === 4)).toBeTruthy()
|
||||
expect(Object.values(Object.values(tree.vm.store.nodesMap).filter(item => item.canFocus).length === 4)).toBeTruthy()
|
||||
// collapse
|
||||
region1.dispatchEvent(new MouseEvent('click'))
|
||||
expect(Object.values(Object.values(tree.vm.store.nodesMap).filter(item => item.canFocus).length === 2)).toBeTruthy()
|
||||
// ArrowDown, region2 focus
|
||||
region1.dispatchEvent(new KeyboardEvent('keydown', { code: 'ArrowDown', bubbles: true, cancelable: false }))
|
||||
expect(flag).toBe(true)
|
||||
defineGetter(region1, 'focus', handleFocus)
|
||||
// ArrowDown, region1 focus
|
||||
region2.dispatchEvent(new KeyboardEvent('keydown', { code: 'ArrowDown', bubbles: true, cancelable: false }))
|
||||
expect(flag).toBe(false)
|
||||
})
|
||||
})
|
||||
|
@ -83,6 +83,7 @@ export default class Node {
|
||||
store: TreeStore;
|
||||
isLeafByUser: boolean;
|
||||
isLeaf: boolean;
|
||||
canFocus: boolean;
|
||||
|
||||
level: number;
|
||||
loaded: boolean;
|
||||
@ -99,6 +100,7 @@ export default class Node {
|
||||
this.parent = null
|
||||
this.visible = true
|
||||
this.isCurrent = false
|
||||
this.canFocus = false
|
||||
|
||||
for (const name in options) {
|
||||
if (options.hasOwnProperty(name)) {
|
||||
@ -135,6 +137,7 @@ export default class Node {
|
||||
|
||||
if (store.defaultExpandAll) {
|
||||
this.expanded = true
|
||||
this.canFocus = true
|
||||
}
|
||||
} else if (this.level > 0 && store.lazy && store.defaultExpandAll) {
|
||||
this.expand()
|
||||
@ -161,6 +164,7 @@ export default class Node {
|
||||
}
|
||||
|
||||
this.updateLeafState()
|
||||
if(this.parent && (this.level === 1 || this.parent.expanded === true)) this.canFocus = true
|
||||
}
|
||||
|
||||
setData(data: TreeNodeData): void {
|
||||
@ -323,6 +327,9 @@ export default class Node {
|
||||
}
|
||||
this.expanded = true
|
||||
if (callback) callback()
|
||||
this.childNodes.forEach(item => {
|
||||
item.canFocus = true
|
||||
})
|
||||
}
|
||||
|
||||
if (this.shouldLoadData()) {
|
||||
@ -349,6 +356,9 @@ export default class Node {
|
||||
|
||||
collapse(): void {
|
||||
this.expanded = false
|
||||
this.childNodes.forEach(item => {
|
||||
item.canFocus = false
|
||||
})
|
||||
}
|
||||
|
||||
shouldLoadData(): boolean {
|
||||
|
@ -163,10 +163,14 @@ export default class TreeStore {
|
||||
|
||||
registerNode(node: Node): void {
|
||||
const key = this.key
|
||||
if (!key || !node || !node.data) return
|
||||
if (!node || !node.data) return
|
||||
|
||||
const nodeKey = node.key
|
||||
if (nodeKey !== undefined) this.nodesMap[node.key] = node
|
||||
if(!key){
|
||||
this.nodesMap[node.id] = node
|
||||
}else {
|
||||
const nodeKey = node.key
|
||||
if (nodeKey !== undefined) this.nodesMap[node.key] = node
|
||||
}
|
||||
}
|
||||
|
||||
deregisterNode(node: Node): void {
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { onMounted, onUpdated, onBeforeUnmount, ref, watch, Ref } from 'vue'
|
||||
import { EVENT_CODE } from '@element-plus/utils/aria'
|
||||
import { on, off } from '@element-plus/utils/dom'
|
||||
import TreeStore from './tree-store'
|
||||
|
||||
interface UseKeydownOption {
|
||||
el$: Ref<HTMLElement>
|
||||
}
|
||||
export function useKeydown({ el$ }: UseKeydownOption) {
|
||||
export function useKeydown({ el$ }: UseKeydownOption, store: Ref<TreeStore>) {
|
||||
const treeItems = ref<Nullable<HTMLElement>[]>([])
|
||||
const checkboxItems = ref<Nullable<HTMLElement>[]>([])
|
||||
|
||||
@ -39,11 +40,35 @@ export function useKeydown({ el$ }: UseKeydownOption) {
|
||||
if ([EVENT_CODE.up, EVENT_CODE.down].indexOf(code) > -1) {
|
||||
ev.preventDefault()
|
||||
if (code === EVENT_CODE.up) {
|
||||
nextIndex = currentIndex !== 0 ? currentIndex - 1 : 0
|
||||
nextIndex = currentIndex === -1 ? 0 : currentIndex !== 0 ? currentIndex - 1 : treeItems.value.length - 1
|
||||
const startIndex = nextIndex
|
||||
while (true) {
|
||||
if(store.value.getNode(treeItems.value[nextIndex].dataset.key).canFocus) break
|
||||
nextIndex--
|
||||
if(nextIndex === startIndex) {
|
||||
nextIndex = -1
|
||||
break
|
||||
}
|
||||
if(nextIndex < 0) {
|
||||
nextIndex = treeItems.value.length - 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
nextIndex = (currentIndex < treeItems.value.length - 1) ? currentIndex + 1 : 0
|
||||
nextIndex = currentIndex === -1 ? 0 : (currentIndex < treeItems.value.length - 1) ? currentIndex + 1 : 0
|
||||
const startIndex = nextIndex
|
||||
while (true) {
|
||||
if(store.value.getNode(treeItems.value[nextIndex].dataset.key).canFocus) break
|
||||
nextIndex++
|
||||
if(nextIndex === startIndex) {
|
||||
nextIndex = -1
|
||||
break
|
||||
}
|
||||
if(nextIndex >= treeItems.value.length) {
|
||||
nextIndex = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
treeItems.value[nextIndex].focus()
|
||||
nextIndex !== -1 && treeItems.value[nextIndex].focus()
|
||||
}
|
||||
if ([EVENT_CODE.left, EVENT_CODE.right].indexOf(code) > -1) {
|
||||
ev.preventDefault()
|
||||
|
@ -16,6 +16,7 @@
|
||||
:aria-disabled="node.disabled"
|
||||
:aria-checked="node.checked"
|
||||
:draggable="tree.props.draggable"
|
||||
:data-key="getNodeKey(node)"
|
||||
@click.stop="handleClick"
|
||||
@contextmenu="handleContextMenu"
|
||||
@dragstart.stop="handleDragStart"
|
||||
|
@ -163,7 +163,7 @@ export default defineComponent({
|
||||
props, ctx, el$, dropIndicator$, store,
|
||||
})
|
||||
|
||||
useKeydown({ el$ })
|
||||
useKeydown({ el$ }, store)
|
||||
|
||||
const isEmpty = computed(() => {
|
||||
const { childNodes } = root.value
|
||||
|
Loading…
Reference in New Issue
Block a user