utils.mjs 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import { focusElement } from "../../../utils/dom/aria.mjs";
  2. import { FOCUSOUT_PREVENTED, FOCUSOUT_PREVENTED_OPTS } from "./tokens.mjs";
  3. import { onBeforeUnmount, onMounted, ref } from "vue";
  4. //#region ../../packages/components/focus-trap/src/utils.ts
  5. const focusReason = ref();
  6. const lastUserFocusTimestamp = ref(0);
  7. const lastAutomatedFocusTimestamp = ref(0);
  8. let focusReasonUserCount = 0;
  9. const obtainAllFocusableElements = (element) => {
  10. const nodes = [];
  11. const walker = document.createTreeWalker(element, NodeFilter.SHOW_ELEMENT, { acceptNode: (node) => {
  12. const isHiddenInput = node.tagName === "INPUT" && node.type === "hidden";
  13. if (node.disabled || node.hidden || isHiddenInput) return NodeFilter.FILTER_SKIP;
  14. return node.tabIndex >= 0 || node === document.activeElement ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
  15. } });
  16. while (walker.nextNode()) nodes.push(walker.currentNode);
  17. return nodes;
  18. };
  19. const getVisibleElement = (elements, container) => {
  20. for (const element of elements) if (!isHidden(element, container)) return element;
  21. };
  22. const isHidden = (element, container) => {
  23. if (getComputedStyle(element).visibility === "hidden") return true;
  24. while (element) {
  25. if (container && element === container) return false;
  26. if (getComputedStyle(element).display === "none") return true;
  27. element = element.parentElement;
  28. }
  29. return false;
  30. };
  31. const getEdges = (container) => {
  32. const focusable = obtainAllFocusableElements(container);
  33. return [getVisibleElement(focusable, container), getVisibleElement(focusable.reverse(), container)];
  34. };
  35. const isSelectable = (element) => {
  36. return element instanceof HTMLInputElement && "select" in element;
  37. };
  38. const tryFocus = (element, shouldSelect) => {
  39. if (element) {
  40. const prevFocusedElement = document.activeElement;
  41. focusElement(element, { preventScroll: true });
  42. lastAutomatedFocusTimestamp.value = window.performance.now();
  43. if (element !== prevFocusedElement && isSelectable(element) && shouldSelect) element.select();
  44. }
  45. };
  46. function removeFromStack(list, item) {
  47. const copy = [...list];
  48. const idx = list.indexOf(item);
  49. if (idx !== -1) copy.splice(idx, 1);
  50. return copy;
  51. }
  52. const createFocusableStack = () => {
  53. let stack = [];
  54. const push = (layer) => {
  55. const currentLayer = stack[0];
  56. if (currentLayer && layer !== currentLayer) currentLayer.pause();
  57. stack = removeFromStack(stack, layer);
  58. stack.unshift(layer);
  59. };
  60. const remove = (layer) => {
  61. stack = removeFromStack(stack, layer);
  62. stack[0]?.resume?.();
  63. };
  64. return {
  65. push,
  66. remove
  67. };
  68. };
  69. const focusFirstDescendant = (elements, shouldSelect = false) => {
  70. const prevFocusedElement = document.activeElement;
  71. for (const element of elements) {
  72. tryFocus(element, shouldSelect);
  73. if (document.activeElement !== prevFocusedElement) return;
  74. }
  75. };
  76. const focusableStack = createFocusableStack();
  77. const isFocusCausedByUserEvent = () => {
  78. return lastUserFocusTimestamp.value > lastAutomatedFocusTimestamp.value;
  79. };
  80. const notifyFocusReasonPointer = () => {
  81. focusReason.value = "pointer";
  82. lastUserFocusTimestamp.value = window.performance.now();
  83. };
  84. const notifyFocusReasonKeydown = () => {
  85. focusReason.value = "keyboard";
  86. lastUserFocusTimestamp.value = window.performance.now();
  87. };
  88. const useFocusReason = () => {
  89. onMounted(() => {
  90. if (focusReasonUserCount === 0) {
  91. document.addEventListener("mousedown", notifyFocusReasonPointer);
  92. document.addEventListener("touchstart", notifyFocusReasonPointer);
  93. document.addEventListener("keydown", notifyFocusReasonKeydown);
  94. }
  95. focusReasonUserCount++;
  96. });
  97. onBeforeUnmount(() => {
  98. focusReasonUserCount--;
  99. if (focusReasonUserCount <= 0) {
  100. document.removeEventListener("mousedown", notifyFocusReasonPointer);
  101. document.removeEventListener("touchstart", notifyFocusReasonPointer);
  102. document.removeEventListener("keydown", notifyFocusReasonKeydown);
  103. }
  104. });
  105. return {
  106. focusReason,
  107. lastUserFocusTimestamp,
  108. lastAutomatedFocusTimestamp
  109. };
  110. };
  111. const createFocusOutPreventedEvent = (detail) => {
  112. return new CustomEvent(FOCUSOUT_PREVENTED, {
  113. ...FOCUSOUT_PREVENTED_OPTS,
  114. detail
  115. });
  116. };
  117. //#endregion
  118. export { createFocusOutPreventedEvent, focusFirstDescendant, focusableStack, getEdges, getVisibleElement, isFocusCausedByUserEvent, isHidden, obtainAllFocusableElements, tryFocus, useFocusReason };
  119. //# sourceMappingURL=utils.mjs.map