getFocusableElements.ts 1.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. export interface FocusableOptions {
  3. ignoreTabindex?: boolean
  4. }
  5. const FOCUSABLE_QUERY =
  6. 'button, a[href]:not([href=""]), input, select, textarea, [tabindex]:not([tabindex="-1"])'
  7. export const isElementVisible = (el: HTMLElement) => {
  8. // In Vitest, a visibility check is unreliable due to the used JSDOM test environment.
  9. // Therefore, we always assume the element is visible.
  10. if (import.meta.env.VITEST) return true
  11. return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length) // from jQuery
  12. }
  13. const isNegativeTabIndex = (el: HTMLElement) => {
  14. const tabIndex = el.getAttribute('tabindex')
  15. return tabIndex && parseInt(tabIndex, 10) < 0
  16. }
  17. export const getFocusableElements = (
  18. container?: Maybe<HTMLElement>,
  19. options: FocusableOptions = {},
  20. ) => {
  21. return Array.from<HTMLElement>(
  22. container?.querySelectorAll(FOCUSABLE_QUERY) || [],
  23. ).filter(
  24. (el) =>
  25. isElementVisible(el) &&
  26. (options.ignoreTabindex || !isNegativeTabIndex(el)) &&
  27. !el.hasAttribute('disabled') &&
  28. el.getAttribute('aria-disabled') !== 'true',
  29. )
  30. }
  31. export const getFirstFocusableElement = (container?: Maybe<HTMLElement>) => {
  32. return getFocusableElements(container)[0]
  33. }
  34. export const getPreviousFocusableElement = (
  35. currentElement?: Maybe<HTMLElement>,
  36. ) => {
  37. if (!currentElement) return null
  38. const focusableElements = getFocusableElements(document.body)
  39. return focusableElements[focusableElements.indexOf(currentElement) - 1]
  40. }