2020-09-09 21:18:08 +08:00
|
|
|
import { nextTick } from 'vue'
|
2020-09-02 10:26:32 +08:00
|
|
|
import { on, off } from '@element-plus/utils/dom'
|
2021-09-04 19:29:28 +08:00
|
|
|
import {
|
|
|
|
obtainAllFocusableElements,
|
|
|
|
EVENT_CODE,
|
|
|
|
} from '@element-plus/utils/aria'
|
2020-09-02 10:26:32 +08:00
|
|
|
|
|
|
|
import type { ObjectDirective } from 'vue'
|
|
|
|
|
|
|
|
export const FOCUSABLE_CHILDREN = '_trap-focus-children'
|
|
|
|
export const TRAP_FOCUS_HANDLER = '_trap-focus-handler'
|
|
|
|
|
|
|
|
export interface ITrapFocusElement extends HTMLElement {
|
|
|
|
[FOCUSABLE_CHILDREN]: HTMLElement[]
|
|
|
|
[TRAP_FOCUS_HANDLER]: (e: KeyboardEvent) => void
|
|
|
|
}
|
|
|
|
|
2021-01-14 17:01:37 +08:00
|
|
|
const FOCUS_STACK = []
|
2020-09-02 10:26:32 +08:00
|
|
|
|
2021-01-14 17:01:37 +08:00
|
|
|
const FOCUS_HANDLER = (e: KeyboardEvent) => {
|
|
|
|
// Getting the top layer.
|
|
|
|
if (FOCUS_STACK.length === 0) return
|
2021-09-04 19:29:28 +08:00
|
|
|
const focusableElement =
|
|
|
|
FOCUS_STACK[FOCUS_STACK.length - 1][FOCUSABLE_CHILDREN]
|
2021-01-14 17:01:37 +08:00
|
|
|
if (focusableElement.length > 0 && e.code === EVENT_CODE.tab) {
|
|
|
|
if (focusableElement.length === 1) {
|
|
|
|
e.preventDefault()
|
2021-06-25 16:25:54 +08:00
|
|
|
if (document.activeElement !== focusableElement[0]) {
|
2021-01-14 17:01:37 +08:00
|
|
|
focusableElement[0].focus()
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const goingBackward = e.shiftKey
|
|
|
|
const isFirst = e.target === focusableElement[0]
|
|
|
|
const isLast = e.target === focusableElement[focusableElement.length - 1]
|
|
|
|
if (isFirst && goingBackward) {
|
|
|
|
e.preventDefault()
|
|
|
|
focusableElement[focusableElement.length - 1].focus()
|
|
|
|
}
|
|
|
|
if (isLast && !goingBackward) {
|
|
|
|
e.preventDefault()
|
|
|
|
focusableElement[0].focus()
|
|
|
|
}
|
2020-09-02 10:26:32 +08:00
|
|
|
|
2021-01-14 17:01:37 +08:00
|
|
|
// the is critical since jsdom did not implement user actions, you can only mock it
|
|
|
|
// DELETE ME: when testing env switches to puppeteer
|
|
|
|
if (process.env.NODE_ENV === 'test') {
|
2021-09-04 19:29:28 +08:00
|
|
|
const index = focusableElement.findIndex(
|
|
|
|
(element: Element) => element === e.target
|
|
|
|
)
|
2021-01-14 17:01:37 +08:00
|
|
|
if (index !== -1) {
|
|
|
|
focusableElement[goingBackward ? index - 1 : index + 1]?.focus()
|
2020-09-02 10:26:32 +08:00
|
|
|
}
|
|
|
|
}
|
2021-01-14 17:01:37 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const TrapFocus: ObjectDirective = {
|
|
|
|
beforeMount(el: ITrapFocusElement) {
|
|
|
|
el[FOCUSABLE_CHILDREN] = obtainAllFocusableElements(el)
|
|
|
|
FOCUS_STACK.push(el)
|
|
|
|
if (FOCUS_STACK.length <= 1) {
|
|
|
|
on(document, 'keydown', FOCUS_HANDLER)
|
|
|
|
}
|
2020-09-02 10:26:32 +08:00
|
|
|
},
|
|
|
|
updated(el: ITrapFocusElement) {
|
2020-09-09 21:18:08 +08:00
|
|
|
nextTick(() => {
|
|
|
|
el[FOCUSABLE_CHILDREN] = obtainAllFocusableElements(el)
|
|
|
|
})
|
2020-09-02 10:26:32 +08:00
|
|
|
},
|
2021-01-14 17:01:37 +08:00
|
|
|
unmounted() {
|
|
|
|
FOCUS_STACK.shift()
|
|
|
|
if (FOCUS_STACK.length === 0) {
|
|
|
|
off(document, 'keydown', FOCUS_HANDLER)
|
|
|
|
}
|
2020-09-02 10:26:32 +08:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
export default TrapFocus
|