index.ts 2.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. import { nextTick } from 'vue'
  2. import { obtainAllFocusableElements } from '@element-plus/utils'
  3. import { EVENT_CODE } from '@element-plus/constants'
  4. import type { ObjectDirective } from 'vue'
  5. export const FOCUSABLE_CHILDREN = '_trap-focus-children'
  6. export const TRAP_FOCUS_HANDLER = '_trap-focus-handler'
  7. export interface TrapFocusElement extends HTMLElement {
  8. [FOCUSABLE_CHILDREN]: HTMLElement[]
  9. [TRAP_FOCUS_HANDLER]: (e: KeyboardEvent) => void
  10. }
  11. const FOCUS_STACK: TrapFocusElement[] = []
  12. const FOCUS_HANDLER = (e: KeyboardEvent) => {
  13. // Getting the top layer.
  14. if (FOCUS_STACK.length === 0) return
  15. const focusableElement =
  16. FOCUS_STACK[FOCUS_STACK.length - 1][FOCUSABLE_CHILDREN]
  17. if (focusableElement.length > 0 && e.code === EVENT_CODE.tab) {
  18. if (focusableElement.length === 1) {
  19. e.preventDefault()
  20. if (document.activeElement !== focusableElement[0]) {
  21. focusableElement[0].focus()
  22. }
  23. return
  24. }
  25. const goingBackward = e.shiftKey
  26. const isFirst = e.target === focusableElement[0]
  27. const isLast = e.target === focusableElement[focusableElement.length - 1]
  28. if (isFirst && goingBackward) {
  29. e.preventDefault()
  30. focusableElement[focusableElement.length - 1].focus()
  31. }
  32. if (isLast && !goingBackward) {
  33. e.preventDefault()
  34. focusableElement[0].focus()
  35. }
  36. // the is critical since jsdom did not implement user actions, you can only mock it
  37. // DELETE ME: when testing env switches to puppeteer
  38. if (process.env.NODE_ENV === 'test') {
  39. const index = focusableElement.indexOf(e.target as HTMLElement)
  40. if (index !== -1) {
  41. focusableElement[goingBackward ? index - 1 : index + 1]?.focus()
  42. }
  43. }
  44. }
  45. }
  46. const TrapFocus: ObjectDirective = {
  47. beforeMount(el: TrapFocusElement) {
  48. el[FOCUSABLE_CHILDREN] = obtainAllFocusableElements(el)
  49. FOCUS_STACK.push(el)
  50. if (FOCUS_STACK.length <= 1) {
  51. document.addEventListener('keydown', FOCUS_HANDLER)
  52. }
  53. },
  54. updated(el: TrapFocusElement) {
  55. nextTick(() => {
  56. el[FOCUSABLE_CHILDREN] = obtainAllFocusableElements(el)
  57. })
  58. },
  59. unmounted() {
  60. FOCUS_STACK.shift()
  61. if (FOCUS_STACK.length === 0) {
  62. document.removeEventListener('keydown', FOCUS_HANDLER)
  63. }
  64. },
  65. }
  66. export default TrapFocus