aria.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  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 = (element: HTMLElement): HTMLElement[] => {
  13. return Array.from(element.querySelectorAll<HTMLElement>(FOCUSABLE_ELEMENT_SELECTORS)).filter((item: HTMLElement) => isFocusable(item) && isVisible(item))
  14. }
  15. /**
  16. * @desc Determine if target element is focusable
  17. * @param element {HTMLElement}
  18. * @returns {Boolean} true if it is focusable
  19. */
  20. export const isFocusable = (element: HTMLElement): boolean => {
  21. if (element.tabIndex > 0 || (element.tabIndex === 0 && element.getAttribute('tabIndex') !== null)) {
  22. return true
  23. }
  24. // HTMLButtonElement has disabled
  25. if ((element as HTMLButtonElement).disabled) {
  26. return false
  27. }
  28. switch (element.nodeName) {
  29. case 'A': {
  30. // casting current element to Specific HTMLElement in order to be more type precise
  31. return !!(element as HTMLAnchorElement).href && (element as HTMLAnchorElement).rel !== 'ignore'
  32. }
  33. case 'INPUT': {
  34. return !((element as HTMLInputElement).type === 'hidden' || (element as HTMLInputElement).type === 'file')
  35. }
  36. case 'BUTTON':
  37. case 'SELECT':
  38. case 'TEXTAREA': {
  39. return true
  40. }
  41. default: {
  42. return false
  43. }
  44. }
  45. }
  46. /**
  47. * @desc Set Attempt to set focus on the current node.
  48. * @param element
  49. * The node to attempt to focus on.
  50. * @returns
  51. * true if element is focused.
  52. */
  53. export const attemptFocus = (element: HTMLElement): boolean => {
  54. if (!isFocusable(element)) {
  55. return false
  56. }
  57. // Remove the old try catch block since there will be no error to be thrown
  58. element.focus?.()
  59. return document.activeElement === element
  60. }
  61. /**
  62. * Trigger an event
  63. * mouseenter, mouseleave, mouseover, keyup, change, click, etc.
  64. * @param {HTMLElement} elm
  65. * @param {String} name
  66. * @param {*} opts
  67. */
  68. export const triggerEvent = function (elm: HTMLElement, name: string, ...opts: Array<boolean>): HTMLElement {
  69. let eventName: string
  70. if (name.includes('mouse') || name.includes('click')) {
  71. eventName = 'MouseEvents'
  72. } else if (name.includes('key')) {
  73. eventName = 'KeyboardEvent'
  74. } else {
  75. eventName = 'HTMLEvents'
  76. }
  77. const evt = document.createEvent(eventName)
  78. evt.initEvent(name, ...opts)
  79. elm.dispatchEvent(evt)
  80. return elm
  81. }
  82. export const isLeaf = (el: HTMLElement) => !el.getAttribute('aria-owns')
  83. export const getSibling = (el: HTMLElement, distance: number, elClass: string) => {
  84. const { parentNode } = el
  85. if (!parentNode) return null
  86. const siblings = parentNode.querySelectorAll(elClass)
  87. const index = Array.prototype.indexOf.call(siblings, el)
  88. return siblings[index + distance] || null
  89. }
  90. export const focusNode = (el: HTMLElement) => {
  91. if (!el) return
  92. el.focus()
  93. !isLeaf(el) && el.click()
  94. }