index.js 15 KB

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