mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-14 17:31:02 +08:00
355 lines
10 KiB
TypeScript
355 lines
10 KiB
TypeScript
import { h, inject, nextTick } from 'vue'
|
|
import { mount } from '@vue/test-utils'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { EVENT_CODE } from '@element-plus/constants'
|
|
import ElFocusTrap from '../src/focus-trap.vue'
|
|
import { FOCUS_TRAP_INJECTION_KEY } from '../src/tokens'
|
|
|
|
const AXIOM = 'rem is the best girl'
|
|
|
|
describe('<ElFocusTrap', () => {
|
|
const childKls = 'child-class'
|
|
const TrapChild = {
|
|
props: {
|
|
items: Number,
|
|
},
|
|
setup() {
|
|
const { focusTrapRef, onKeydown } = inject(
|
|
FOCUS_TRAP_INJECTION_KEY,
|
|
undefined
|
|
)!
|
|
return {
|
|
focusTrapRef,
|
|
onKeydown,
|
|
}
|
|
},
|
|
template: `
|
|
<span class="before-trap" tabindex="0"></span>
|
|
<div ref="focusTrapRef" tabindex="0" class="focus-container ${childKls}" @keydown="onKeydown">
|
|
<template v-if="!items">${AXIOM}</template>
|
|
<template v-else v-for="i in items">
|
|
<span class="item" tabindex="0">{{ i }}</span>
|
|
</template>
|
|
</div>
|
|
<span class="after-trap" tabindex="0"></span>
|
|
`,
|
|
}
|
|
|
|
const createComponent = (props = {}, items = 0) =>
|
|
mount(ElFocusTrap, {
|
|
props: {
|
|
trapped: true,
|
|
...props,
|
|
},
|
|
slots: {
|
|
default: () => h(TrapChild, { items }),
|
|
},
|
|
attachTo: document.body,
|
|
})
|
|
|
|
let wrapper: ReturnType<typeof createComponent>
|
|
const findFocusContainer = () => wrapper.find('.focus-container')
|
|
const findDescendants = () => wrapper.findAll('.item')
|
|
const findBeforeTrap = () => wrapper.find('.before-trap')
|
|
const findAfterTrap = () => wrapper.find('.after-trap')
|
|
|
|
afterEach(() => {
|
|
wrapper?.unmount()
|
|
document.body.innerHTML = ''
|
|
})
|
|
|
|
describe('render', () => {
|
|
it('should render correctly', async () => {
|
|
wrapper = createComponent()
|
|
await nextTick()
|
|
await nextTick()
|
|
|
|
const child = findFocusContainer()
|
|
expect(child.exists()).toBe(true)
|
|
expect(child.text()).toBe(AXIOM)
|
|
expect(document.activeElement).toBe(child.element)
|
|
})
|
|
|
|
it('should be able to focus on the first descendant item', async () => {
|
|
wrapper = createComponent(undefined, 3)
|
|
await nextTick()
|
|
await nextTick()
|
|
|
|
const descendants = findDescendants()
|
|
expect(descendants).toHaveLength(3)
|
|
expect(document.activeElement).toBe(descendants.at(0)?.element)
|
|
})
|
|
})
|
|
|
|
describe('events', () => {
|
|
it('should be able to dispatch focus on mount event', async () => {
|
|
const focusOnMount = vi.fn()
|
|
wrapper = createComponent({
|
|
onFocusAfterTrapped: focusOnMount,
|
|
})
|
|
await nextTick()
|
|
|
|
expect(focusOnMount).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should be able to dispatch focus on unmount', async () => {
|
|
const focusOnUnmount = vi.fn()
|
|
wrapper = createComponent({
|
|
onFocusAfterReleased: focusOnUnmount,
|
|
})
|
|
await nextTick()
|
|
await nextTick()
|
|
const child = findFocusContainer()
|
|
expect(document.activeElement).toBe(child.element)
|
|
|
|
wrapper.unmount()
|
|
expect(focusOnUnmount).toHaveBeenCalled()
|
|
expect(document.activeElement).toBe(document.body)
|
|
})
|
|
|
|
it('should be able to dispatch `release-requested` if escape key pressed while trapped', async () => {
|
|
wrapper = createComponent(
|
|
{
|
|
trapped: false,
|
|
loop: true,
|
|
},
|
|
1
|
|
)
|
|
await nextTick()
|
|
|
|
const focusContainer = findFocusContainer()
|
|
|
|
focusContainer?.trigger('keydown', {
|
|
key: EVENT_CODE.esc,
|
|
})
|
|
|
|
await nextTick()
|
|
await nextTick()
|
|
|
|
expect(wrapper.emitted('release-requested')).toBeFalsy()
|
|
|
|
await wrapper.setProps({ trapped: true })
|
|
|
|
await nextTick()
|
|
await nextTick()
|
|
|
|
const items = findDescendants()
|
|
const firstItem = items.at(0)
|
|
expect(document.activeElement).toBe(firstItem?.element)
|
|
|
|
// Expect no emit if esc while not trapped
|
|
expect(wrapper.emitted('release-requested')).toBeFalsy()
|
|
|
|
focusContainer?.trigger('keydown', {
|
|
key: EVENT_CODE.esc,
|
|
})
|
|
|
|
await nextTick()
|
|
await nextTick()
|
|
|
|
// Expect emit if esc while trapped
|
|
expect(wrapper.emitted('release-requested')?.length).toBe(1)
|
|
|
|
createComponent({ loop: true }, 3)
|
|
await nextTick()
|
|
await nextTick()
|
|
|
|
focusContainer?.trigger('keydown', {
|
|
key: EVENT_CODE.esc,
|
|
})
|
|
|
|
// Expect no emit if esc while layer paused
|
|
expect(wrapper.emitted('release-requested')?.length).toBe(1)
|
|
})
|
|
|
|
it('should be able to dispatch `focusout-prevented` when trab wraps due to trapped or is blocked', async () => {
|
|
wrapper = createComponent(undefined, 3)
|
|
await nextTick()
|
|
await nextTick()
|
|
|
|
const childComponent = findFocusContainer()
|
|
const items = findDescendants()
|
|
expect(document.activeElement).toBe(items.at(0)?.element)
|
|
|
|
expect(wrapper.emitted('focusout-prevented')).toBeFalsy()
|
|
await childComponent.trigger('keydown.shift', {
|
|
key: EVENT_CODE.tab,
|
|
})
|
|
expect(document.activeElement).toBe(items.at(0)?.element)
|
|
expect(wrapper.emitted('focusout-prevented')?.length).toBe(2)
|
|
;(items.at(2)?.element as HTMLElement).focus()
|
|
await childComponent.trigger('keydown', {
|
|
key: EVENT_CODE.tab,
|
|
})
|
|
expect(wrapper.emitted('focusout-prevented')?.length).toBe(4)
|
|
})
|
|
})
|
|
|
|
describe('features', () => {
|
|
it('should be able to navigate via keyboard', async () => {
|
|
wrapper = createComponent(undefined, 3)
|
|
await nextTick()
|
|
await nextTick()
|
|
|
|
const childComponent = findFocusContainer()
|
|
const items = findDescendants()
|
|
expect(document.activeElement).toBe(items.at(0)?.element)
|
|
|
|
/**
|
|
* NOTE:
|
|
* JSDOM does not support keyboard navigation simulation so that
|
|
* dispatching keyboard event with tab key is useless, we cannot test it
|
|
* it here, maybe turn to cypress for robust e2e test would be a better idea
|
|
*/
|
|
// when loop is off
|
|
await childComponent.trigger('keydown.shift', {
|
|
key: EVENT_CODE.tab,
|
|
})
|
|
expect(document.activeElement).toBe(items.at(0)?.element)
|
|
;(items.at(2)?.element as HTMLElement).focus()
|
|
expect(document.activeElement).toBe(items.at(2)?.element)
|
|
|
|
await childComponent.trigger('keydown', {
|
|
key: EVENT_CODE.tab,
|
|
})
|
|
expect(document.activeElement).toBe(items.at(2)?.element)
|
|
|
|
// set loop to true so that tab can tabbing from last to first and back forth
|
|
await wrapper.setProps({
|
|
loop: true,
|
|
})
|
|
|
|
await childComponent.trigger('keydown', {
|
|
key: EVENT_CODE.tab,
|
|
})
|
|
expect(document.activeElement).toBe(items.at(0)?.element)
|
|
|
|
await childComponent.trigger('keydown.shift', {
|
|
key: EVENT_CODE.tab,
|
|
})
|
|
expect(document.activeElement).toBe(items.at(2)?.element)
|
|
})
|
|
|
|
it('should not be able to navigate when no focusable element contained', async () => {
|
|
wrapper = createComponent()
|
|
await nextTick()
|
|
await nextTick()
|
|
|
|
const focusComponent = findFocusContainer()
|
|
expect(document.activeElement).toBe(focusComponent.element)
|
|
|
|
await focusComponent.trigger('keydown', {
|
|
key: EVENT_CODE.tab,
|
|
})
|
|
expect(document.activeElement).toBe(focusComponent.element)
|
|
})
|
|
|
|
it('should be able to navigate outside by keyboard when not trapped', async () => {
|
|
wrapper = createComponent(
|
|
{
|
|
trapped: false,
|
|
},
|
|
1
|
|
)
|
|
await nextTick()
|
|
await nextTick()
|
|
|
|
let isDefaultPrevented = false
|
|
const preventDefault = () => {
|
|
isDefaultPrevented = true
|
|
}
|
|
|
|
const focusContainer = findFocusContainer()
|
|
const items = findDescendants()
|
|
const beforeTrap = findBeforeTrap()
|
|
const afterTrap = findAfterTrap()
|
|
|
|
;(beforeTrap.element as HTMLElement).focus()
|
|
expect(document.activeElement).toBe(beforeTrap.element)
|
|
|
|
await focusContainer.trigger('keydown', {
|
|
key: EVENT_CODE.tab,
|
|
preventDefault,
|
|
})
|
|
if (!isDefaultPrevented) {
|
|
;(items.at(0)?.element as HTMLElement).focus()
|
|
}
|
|
|
|
expect(document.activeElement).toBe(items.at(0)?.element)
|
|
|
|
await focusContainer.trigger('keydown', {
|
|
key: EVENT_CODE.tab,
|
|
preventDefault,
|
|
})
|
|
if (!isDefaultPrevented) {
|
|
;(afterTrap.element as HTMLElement).focus()
|
|
}
|
|
|
|
expect(document.activeElement).toBe(afterTrap.element)
|
|
})
|
|
|
|
it('should not be able to navigate if the current layer is paused', async () => {
|
|
wrapper = createComponent(
|
|
{
|
|
loop: true,
|
|
},
|
|
3
|
|
)
|
|
await nextTick()
|
|
await nextTick()
|
|
|
|
const focusComponent = findFocusContainer()
|
|
const items = findDescendants()
|
|
expect(document.activeElement).toBe(items.at(0)?.element)
|
|
|
|
await focusComponent.trigger('keydown.shift', {
|
|
key: EVENT_CODE.tab,
|
|
})
|
|
expect(document.activeElement).toBe(items.at(2)?.element)
|
|
|
|
const newFocusTrap = createComponent({ loop: true }, 3)
|
|
await nextTick()
|
|
await nextTick()
|
|
expect(document.activeElement).toBe(newFocusTrap.find('.item').element)
|
|
|
|
await focusComponent.trigger('keydown', {
|
|
key: EVENT_CODE.tab,
|
|
})
|
|
expect(document.activeElement).not.toBe(items.at(0)?.element)
|
|
newFocusTrap.unmount()
|
|
await nextTick()
|
|
|
|
expect(document.activeElement).toBe(items.at(2)?.element)
|
|
|
|
await focusComponent.trigger('keydown', {
|
|
key: EVENT_CODE.tab,
|
|
})
|
|
expect(document.activeElement).toBe(items.at(0)?.element)
|
|
})
|
|
|
|
it('should steal focus when trapped', async () => {
|
|
wrapper = createComponent(
|
|
{
|
|
trapped: false,
|
|
loop: true,
|
|
},
|
|
1
|
|
)
|
|
await nextTick()
|
|
|
|
const beforeTrap = findBeforeTrap()
|
|
;(beforeTrap.element as HTMLElement).focus()
|
|
expect(document.activeElement).toBe(beforeTrap.element)
|
|
|
|
await wrapper.setProps({ trapped: true })
|
|
|
|
await nextTick()
|
|
await nextTick()
|
|
|
|
const items = findDescendants()
|
|
const firstItem = items.at(0)
|
|
expect(document.activeElement).toBe(firstItem?.element)
|
|
})
|
|
})
|
|
})
|