index.mjs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. import $45QHv$babelruntimehelpersesmextends from "@babel/runtime/helpers/esm/extends";
  2. import {forwardRef as $45QHv$forwardRef, useState as $45QHv$useState, useRef as $45QHv$useRef, useEffect as $45QHv$useEffect, useCallback as $45QHv$useCallback, createElement as $45QHv$createElement} from "react";
  3. import {useComposedRefs as $45QHv$useComposedRefs} from "@radix-ui/react-compose-refs";
  4. import {Primitive as $45QHv$Primitive} from "@radix-ui/react-primitive";
  5. import {useCallbackRef as $45QHv$useCallbackRef} from "@radix-ui/react-use-callback-ref";
  6. const $d3863c46a17e8a28$var$AUTOFOCUS_ON_MOUNT = 'focusScope.autoFocusOnMount';
  7. const $d3863c46a17e8a28$var$AUTOFOCUS_ON_UNMOUNT = 'focusScope.autoFocusOnUnmount';
  8. const $d3863c46a17e8a28$var$EVENT_OPTIONS = {
  9. bubbles: false,
  10. cancelable: true
  11. };
  12. /* -------------------------------------------------------------------------------------------------
  13. * FocusScope
  14. * -----------------------------------------------------------------------------------------------*/ const $d3863c46a17e8a28$var$FOCUS_SCOPE_NAME = 'FocusScope';
  15. const $d3863c46a17e8a28$export$20e40289641fbbb6 = /*#__PURE__*/ $45QHv$forwardRef((props, forwardedRef)=>{
  16. const { loop: loop = false , trapped: trapped = false , onMountAutoFocus: onMountAutoFocusProp , onUnmountAutoFocus: onUnmountAutoFocusProp , ...scopeProps } = props;
  17. const [container1, setContainer] = $45QHv$useState(null);
  18. const onMountAutoFocus = $45QHv$useCallbackRef(onMountAutoFocusProp);
  19. const onUnmountAutoFocus = $45QHv$useCallbackRef(onUnmountAutoFocusProp);
  20. const lastFocusedElementRef = $45QHv$useRef(null);
  21. const composedRefs = $45QHv$useComposedRefs(forwardedRef, (node)=>setContainer(node)
  22. );
  23. const focusScope = $45QHv$useRef({
  24. paused: false,
  25. pause () {
  26. this.paused = true;
  27. },
  28. resume () {
  29. this.paused = false;
  30. }
  31. }).current; // Takes care of trapping focus if focus is moved outside programmatically for example
  32. $45QHv$useEffect(()=>{
  33. if (trapped) {
  34. function handleFocusIn(event) {
  35. if (focusScope.paused || !container1) return;
  36. const target = event.target;
  37. if (container1.contains(target)) lastFocusedElementRef.current = target;
  38. else $d3863c46a17e8a28$var$focus(lastFocusedElementRef.current, {
  39. select: true
  40. });
  41. }
  42. function handleFocusOut(event) {
  43. if (focusScope.paused || !container1) return;
  44. const relatedTarget = event.relatedTarget; // A `focusout` event with a `null` `relatedTarget` will happen in at least two cases:
  45. //
  46. // 1. When the user switches app/tabs/windows/the browser itself loses focus.
  47. // 2. In Google Chrome, when the focused element is removed from the DOM.
  48. //
  49. // We let the browser do its thing here because:
  50. //
  51. // 1. The browser already keeps a memory of what's focused for when the page gets refocused.
  52. // 2. In Google Chrome, if we try to focus the deleted focused element (as per below), it
  53. // throws the CPU to 100%, so we avoid doing anything for this reason here too.
  54. if (relatedTarget === null) return; // If the focus has moved to an actual legitimate element (`relatedTarget !== null`)
  55. // that is outside the container, we move focus to the last valid focused element inside.
  56. if (!container1.contains(relatedTarget)) $d3863c46a17e8a28$var$focus(lastFocusedElementRef.current, {
  57. select: true
  58. });
  59. } // When the focused element gets removed from the DOM, browsers move focus
  60. // back to the document.body. In this case, we move focus to the container
  61. // to keep focus trapped correctly.
  62. function handleMutations(mutations) {
  63. const focusedElement = document.activeElement;
  64. if (focusedElement !== document.body) return;
  65. for (const mutation of mutations)if (mutation.removedNodes.length > 0) $d3863c46a17e8a28$var$focus(container1);
  66. }
  67. document.addEventListener('focusin', handleFocusIn);
  68. document.addEventListener('focusout', handleFocusOut);
  69. const mutationObserver = new MutationObserver(handleMutations);
  70. if (container1) mutationObserver.observe(container1, {
  71. childList: true,
  72. subtree: true
  73. });
  74. return ()=>{
  75. document.removeEventListener('focusin', handleFocusIn);
  76. document.removeEventListener('focusout', handleFocusOut);
  77. mutationObserver.disconnect();
  78. };
  79. }
  80. }, [
  81. trapped,
  82. container1,
  83. focusScope.paused
  84. ]);
  85. $45QHv$useEffect(()=>{
  86. if (container1) {
  87. $d3863c46a17e8a28$var$focusScopesStack.add(focusScope);
  88. const previouslyFocusedElement = document.activeElement;
  89. const hasFocusedCandidate = container1.contains(previouslyFocusedElement);
  90. if (!hasFocusedCandidate) {
  91. const mountEvent = new CustomEvent($d3863c46a17e8a28$var$AUTOFOCUS_ON_MOUNT, $d3863c46a17e8a28$var$EVENT_OPTIONS);
  92. container1.addEventListener($d3863c46a17e8a28$var$AUTOFOCUS_ON_MOUNT, onMountAutoFocus);
  93. container1.dispatchEvent(mountEvent);
  94. if (!mountEvent.defaultPrevented) {
  95. $d3863c46a17e8a28$var$focusFirst($d3863c46a17e8a28$var$removeLinks($d3863c46a17e8a28$var$getTabbableCandidates(container1)), {
  96. select: true
  97. });
  98. if (document.activeElement === previouslyFocusedElement) $d3863c46a17e8a28$var$focus(container1);
  99. }
  100. }
  101. return ()=>{
  102. container1.removeEventListener($d3863c46a17e8a28$var$AUTOFOCUS_ON_MOUNT, onMountAutoFocus); // We hit a react bug (fixed in v17) with focusing in unmount.
  103. // We need to delay the focus a little to get around it for now.
  104. // See: https://github.com/facebook/react/issues/17894
  105. setTimeout(()=>{
  106. const unmountEvent = new CustomEvent($d3863c46a17e8a28$var$AUTOFOCUS_ON_UNMOUNT, $d3863c46a17e8a28$var$EVENT_OPTIONS);
  107. container1.addEventListener($d3863c46a17e8a28$var$AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus);
  108. container1.dispatchEvent(unmountEvent);
  109. if (!unmountEvent.defaultPrevented) $d3863c46a17e8a28$var$focus(previouslyFocusedElement !== null && previouslyFocusedElement !== void 0 ? previouslyFocusedElement : document.body, {
  110. select: true
  111. });
  112. // we need to remove the listener after we `dispatchEvent`
  113. container1.removeEventListener($d3863c46a17e8a28$var$AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus);
  114. $d3863c46a17e8a28$var$focusScopesStack.remove(focusScope);
  115. }, 0);
  116. };
  117. }
  118. }, [
  119. container1,
  120. onMountAutoFocus,
  121. onUnmountAutoFocus,
  122. focusScope
  123. ]); // Takes care of looping focus (when tabbing whilst at the edges)
  124. const handleKeyDown = $45QHv$useCallback((event)=>{
  125. if (!loop && !trapped) return;
  126. if (focusScope.paused) return;
  127. const isTabKey = event.key === 'Tab' && !event.altKey && !event.ctrlKey && !event.metaKey;
  128. const focusedElement = document.activeElement;
  129. if (isTabKey && focusedElement) {
  130. const container = event.currentTarget;
  131. const [first, last] = $d3863c46a17e8a28$var$getTabbableEdges(container);
  132. const hasTabbableElementsInside = first && last; // we can only wrap focus if we have tabbable edges
  133. if (!hasTabbableElementsInside) {
  134. if (focusedElement === container) event.preventDefault();
  135. } else {
  136. if (!event.shiftKey && focusedElement === last) {
  137. event.preventDefault();
  138. if (loop) $d3863c46a17e8a28$var$focus(first, {
  139. select: true
  140. });
  141. } else if (event.shiftKey && focusedElement === first) {
  142. event.preventDefault();
  143. if (loop) $d3863c46a17e8a28$var$focus(last, {
  144. select: true
  145. });
  146. }
  147. }
  148. }
  149. }, [
  150. loop,
  151. trapped,
  152. focusScope.paused
  153. ]);
  154. return /*#__PURE__*/ $45QHv$createElement($45QHv$Primitive.div, $45QHv$babelruntimehelpersesmextends({
  155. tabIndex: -1
  156. }, scopeProps, {
  157. ref: composedRefs,
  158. onKeyDown: handleKeyDown
  159. }));
  160. });
  161. /*#__PURE__*/ Object.assign($d3863c46a17e8a28$export$20e40289641fbbb6, {
  162. displayName: $d3863c46a17e8a28$var$FOCUS_SCOPE_NAME
  163. });
  164. /* -------------------------------------------------------------------------------------------------
  165. * Utils
  166. * -----------------------------------------------------------------------------------------------*/ /**
  167. * Attempts focusing the first element in a list of candidates.
  168. * Stops when focus has actually moved.
  169. */ function $d3863c46a17e8a28$var$focusFirst(candidates, { select: select = false } = {}) {
  170. const previouslyFocusedElement = document.activeElement;
  171. for (const candidate of candidates){
  172. $d3863c46a17e8a28$var$focus(candidate, {
  173. select: select
  174. });
  175. if (document.activeElement !== previouslyFocusedElement) return;
  176. }
  177. }
  178. /**
  179. * Returns the first and last tabbable elements inside a container.
  180. */ function $d3863c46a17e8a28$var$getTabbableEdges(container) {
  181. const candidates = $d3863c46a17e8a28$var$getTabbableCandidates(container);
  182. const first = $d3863c46a17e8a28$var$findVisible(candidates, container);
  183. const last = $d3863c46a17e8a28$var$findVisible(candidates.reverse(), container);
  184. return [
  185. first,
  186. last
  187. ];
  188. }
  189. /**
  190. * Returns a list of potential tabbable candidates.
  191. *
  192. * NOTE: This is only a close approximation. For example it doesn't take into account cases like when
  193. * elements are not visible. This cannot be worked out easily by just reading a property, but rather
  194. * necessitate runtime knowledge (computed styles, etc). We deal with these cases separately.
  195. *
  196. * See: https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker
  197. * Credit: https://github.com/discord/focus-layers/blob/master/src/util/wrapFocus.tsx#L1
  198. */ function $d3863c46a17e8a28$var$getTabbableCandidates(container) {
  199. const nodes = [];
  200. const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
  201. acceptNode: (node)=>{
  202. const isHiddenInput = node.tagName === 'INPUT' && node.type === 'hidden';
  203. if (node.disabled || node.hidden || isHiddenInput) return NodeFilter.FILTER_SKIP; // `.tabIndex` is not the same as the `tabindex` attribute. It works on the
  204. // runtime's understanding of tabbability, so this automatically accounts
  205. // for any kind of element that could be tabbed to.
  206. return node.tabIndex >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
  207. }
  208. });
  209. while(walker.nextNode())nodes.push(walker.currentNode); // we do not take into account the order of nodes with positive `tabIndex` as it
  210. // hinders accessibility to have tab order different from visual order.
  211. return nodes;
  212. }
  213. /**
  214. * Returns the first visible element in a list.
  215. * NOTE: Only checks visibility up to the `container`.
  216. */ function $d3863c46a17e8a28$var$findVisible(elements, container) {
  217. for (const element of elements){
  218. // we stop checking if it's hidden at the `container` level (excluding)
  219. if (!$d3863c46a17e8a28$var$isHidden(element, {
  220. upTo: container
  221. })) return element;
  222. }
  223. }
  224. function $d3863c46a17e8a28$var$isHidden(node, { upTo: upTo }) {
  225. if (getComputedStyle(node).visibility === 'hidden') return true;
  226. while(node){
  227. // we stop at `upTo` (excluding it)
  228. if (upTo !== undefined && node === upTo) return false;
  229. if (getComputedStyle(node).display === 'none') return true;
  230. node = node.parentElement;
  231. }
  232. return false;
  233. }
  234. function $d3863c46a17e8a28$var$isSelectableInput(element) {
  235. return element instanceof HTMLInputElement && 'select' in element;
  236. }
  237. function $d3863c46a17e8a28$var$focus(element, { select: select = false } = {}) {
  238. // only focus if that element is focusable
  239. if (element && element.focus) {
  240. const previouslyFocusedElement = document.activeElement; // NOTE: we prevent scrolling on focus, to minimize jarring transitions for users
  241. element.focus({
  242. preventScroll: true
  243. }); // only select if its not the same element, it supports selection and we need to select
  244. if (element !== previouslyFocusedElement && $d3863c46a17e8a28$var$isSelectableInput(element) && select) element.select();
  245. }
  246. }
  247. /* -------------------------------------------------------------------------------------------------
  248. * FocusScope stack
  249. * -----------------------------------------------------------------------------------------------*/ const $d3863c46a17e8a28$var$focusScopesStack = $d3863c46a17e8a28$var$createFocusScopesStack();
  250. function $d3863c46a17e8a28$var$createFocusScopesStack() {
  251. /** A stack of focus scopes, with the active one at the top */ let stack = [];
  252. return {
  253. add (focusScope) {
  254. // pause the currently active focus scope (at the top of the stack)
  255. const activeFocusScope = stack[0];
  256. if (focusScope !== activeFocusScope) activeFocusScope === null || activeFocusScope === void 0 || activeFocusScope.pause();
  257. // remove in case it already exists (because we'll re-add it at the top of the stack)
  258. stack = $d3863c46a17e8a28$var$arrayRemove(stack, focusScope);
  259. stack.unshift(focusScope);
  260. },
  261. remove (focusScope) {
  262. var _stack$;
  263. stack = $d3863c46a17e8a28$var$arrayRemove(stack, focusScope);
  264. (_stack$ = stack[0]) === null || _stack$ === void 0 || _stack$.resume();
  265. }
  266. };
  267. }
  268. function $d3863c46a17e8a28$var$arrayRemove(array, item) {
  269. const updatedArray = [
  270. ...array
  271. ];
  272. const index = updatedArray.indexOf(item);
  273. if (index !== -1) updatedArray.splice(index, 1);
  274. return updatedArray;
  275. }
  276. function $d3863c46a17e8a28$var$removeLinks(items) {
  277. return items.filter((item)=>item.tagName !== 'A'
  278. );
  279. }
  280. const $d3863c46a17e8a28$export$be92b6f5f03c0fe9 = $d3863c46a17e8a28$export$20e40289641fbbb6;
  281. export {$d3863c46a17e8a28$export$20e40289641fbbb6 as FocusScope, $d3863c46a17e8a28$export$be92b6f5f03c0fe9 as Root};
  282. //# sourceMappingURL=index.mjs.map