utils.mjs 4.5 KB

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