d2989b8a1dbce30fc1794b09ddf88c37c6cbf22c9aed1e253736a402c939344781a85bbc707fa05640a9b33bf0d2274dd83c4e6e32101a67f2e3d3d15403d3 4.5 KB

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