aria.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. const FOCUSABLE_ELEMENT_SELECTORS = `a[href],button:not([disabled]),button:not([hidden]),:not([tabindex="-1"]),input:not([disabled]),input:not([type="hidden"]),select:not([disabled]),textarea:not([disabled])`
  2. /**
  3. * Determine if the testing element is visible on screen no matter if its on the viewport or not
  4. */
  5. export const isVisible = (element: HTMLElement) => {
  6. if (process.env.NODE_ENV === 'test') return true
  7. const computed = getComputedStyle(element)
  8. // element.offsetParent won't work on fix positioned
  9. // WARNING: potential issue here, going to need some expert advices on this issue
  10. return computed.position === 'fixed' ? false : element.offsetParent !== null
  11. }
  12. export const obtainAllFocusableElements = (
  13. element: HTMLElement
  14. ): HTMLElement[] => {
  15. return Array.from(
  16. element.querySelectorAll<HTMLElement>(FOCUSABLE_ELEMENT_SELECTORS)
  17. ).filter((item: HTMLElement) => isFocusable(item) && isVisible(item))
  18. }
  19. /**
  20. * @desc Determine if target element is focusable
  21. * @param element {HTMLElement}
  22. * @returns {Boolean} true if it is focusable
  23. */
  24. export const isFocusable = (element: HTMLElement): boolean => {
  25. if (
  26. element.tabIndex > 0 ||
  27. (element.tabIndex === 0 && element.getAttribute('tabIndex') !== null)
  28. ) {
  29. return true
  30. }
  31. // HTMLButtonElement has disabled
  32. if ((element as HTMLButtonElement).disabled) {
  33. return false
  34. }
  35. switch (element.nodeName) {
  36. case 'A': {
  37. // casting current element to Specific HTMLElement in order to be more type precise
  38. return (
  39. !!(element as HTMLAnchorElement).href &&
  40. (element as HTMLAnchorElement).rel !== 'ignore'
  41. )
  42. }
  43. case 'INPUT': {
  44. return !(
  45. (element as HTMLInputElement).type === 'hidden' ||
  46. (element as HTMLInputElement).type === 'file'
  47. )
  48. }
  49. case 'BUTTON':
  50. case 'SELECT':
  51. case 'TEXTAREA': {
  52. return true
  53. }
  54. default: {
  55. return false
  56. }
  57. }
  58. }
  59. /**
  60. * @desc Set Attempt to set focus on the current node.
  61. * @param element
  62. * The node to attempt to focus on.
  63. * @returns
  64. * true if element is focused.
  65. */
  66. export const attemptFocus = (element: HTMLElement): boolean => {
  67. if (!isFocusable(element)) {
  68. return false
  69. }
  70. // Remove the old try catch block since there will be no error to be thrown
  71. element.focus?.()
  72. return document.activeElement === element
  73. }
  74. /**
  75. * Trigger an event
  76. * mouseenter, mouseleave, mouseover, keyup, change, click, etc.
  77. * @param {HTMLElement} elm
  78. * @param {String} name
  79. * @param {*} opts
  80. */
  81. export const triggerEvent = function (
  82. elm: HTMLElement,
  83. name: string,
  84. ...opts: Array<boolean>
  85. ): HTMLElement {
  86. let eventName: string
  87. if (name.includes('mouse') || name.includes('click')) {
  88. eventName = 'MouseEvents'
  89. } else if (name.includes('key')) {
  90. eventName = 'KeyboardEvent'
  91. } else {
  92. eventName = 'HTMLEvents'
  93. }
  94. const evt = document.createEvent(eventName)
  95. evt.initEvent(name, ...opts)
  96. elm.dispatchEvent(evt)
  97. return elm
  98. }
  99. export const isLeaf = (el: HTMLElement) => !el.getAttribute('aria-owns')
  100. export const getSibling = (
  101. el: HTMLElement,
  102. distance: number,
  103. elClass: string
  104. ) => {
  105. const { parentNode } = el
  106. if (!parentNode) return null
  107. const siblings = parentNode.querySelectorAll(elClass)
  108. const index = Array.prototype.indexOf.call(siblings, el)
  109. return siblings[index + distance] || null
  110. }
  111. export const focusNode = (el: HTMLElement) => {
  112. if (!el) return
  113. el.focus()
  114. !isLeaf(el) && el.click()
  115. }