Spaces:
Running
Running
| "use client"; | |
| // src/focus-scope.tsx | |
| import * as React from "react"; | |
| import { useComposedRefs } from "@radix-ui/react-compose-refs"; | |
| import { Primitive } from "@radix-ui/react-primitive"; | |
| import { useCallbackRef } from "@radix-ui/react-use-callback-ref"; | |
| import { jsx } from "react/jsx-runtime"; | |
| var AUTOFOCUS_ON_MOUNT = "focusScope.autoFocusOnMount"; | |
| var AUTOFOCUS_ON_UNMOUNT = "focusScope.autoFocusOnUnmount"; | |
| var EVENT_OPTIONS = { bubbles: false, cancelable: true }; | |
| var FOCUS_SCOPE_NAME = "FocusScope"; | |
| var FocusScope = React.forwardRef((props, forwardedRef) => { | |
| const { | |
| loop = false, | |
| trapped = false, | |
| onMountAutoFocus: onMountAutoFocusProp, | |
| onUnmountAutoFocus: onUnmountAutoFocusProp, | |
| ...scopeProps | |
| } = props; | |
| const [container, setContainer] = React.useState(null); | |
| const onMountAutoFocus = useCallbackRef(onMountAutoFocusProp); | |
| const onUnmountAutoFocus = useCallbackRef(onUnmountAutoFocusProp); | |
| const lastFocusedElementRef = React.useRef(null); | |
| const composedRefs = useComposedRefs(forwardedRef, (node) => setContainer(node)); | |
| const focusScope = React.useRef({ | |
| paused: false, | |
| pause() { | |
| this.paused = true; | |
| }, | |
| resume() { | |
| this.paused = false; | |
| } | |
| }).current; | |
| React.useEffect(() => { | |
| if (trapped) { | |
| let handleFocusIn2 = function(event) { | |
| if (focusScope.paused || !container) return; | |
| const target = event.target; | |
| if (container.contains(target)) { | |
| lastFocusedElementRef.current = target; | |
| } else { | |
| focus(lastFocusedElementRef.current, { select: true }); | |
| } | |
| }, handleFocusOut2 = function(event) { | |
| if (focusScope.paused || !container) return; | |
| const relatedTarget = event.relatedTarget; | |
| if (relatedTarget === null) return; | |
| if (!container.contains(relatedTarget)) { | |
| focus(lastFocusedElementRef.current, { select: true }); | |
| } | |
| }, handleMutations2 = function(mutations) { | |
| const focusedElement = document.activeElement; | |
| if (focusedElement !== document.body) return; | |
| for (const mutation of mutations) { | |
| if (mutation.removedNodes.length > 0) focus(container); | |
| } | |
| }; | |
| var handleFocusIn = handleFocusIn2, handleFocusOut = handleFocusOut2, handleMutations = handleMutations2; | |
| document.addEventListener("focusin", handleFocusIn2); | |
| document.addEventListener("focusout", handleFocusOut2); | |
| const mutationObserver = new MutationObserver(handleMutations2); | |
| if (container) mutationObserver.observe(container, { childList: true, subtree: true }); | |
| return () => { | |
| document.removeEventListener("focusin", handleFocusIn2); | |
| document.removeEventListener("focusout", handleFocusOut2); | |
| mutationObserver.disconnect(); | |
| }; | |
| } | |
| }, [trapped, container, focusScope.paused]); | |
| React.useEffect(() => { | |
| if (container) { | |
| focusScopesStack.add(focusScope); | |
| const previouslyFocusedElement = document.activeElement; | |
| const hasFocusedCandidate = container.contains(previouslyFocusedElement); | |
| if (!hasFocusedCandidate) { | |
| const mountEvent = new CustomEvent(AUTOFOCUS_ON_MOUNT, EVENT_OPTIONS); | |
| container.addEventListener(AUTOFOCUS_ON_MOUNT, onMountAutoFocus); | |
| container.dispatchEvent(mountEvent); | |
| if (!mountEvent.defaultPrevented) { | |
| focusFirst(removeLinks(getTabbableCandidates(container)), { select: true }); | |
| if (document.activeElement === previouslyFocusedElement) { | |
| focus(container); | |
| } | |
| } | |
| } | |
| return () => { | |
| container.removeEventListener(AUTOFOCUS_ON_MOUNT, onMountAutoFocus); | |
| setTimeout(() => { | |
| const unmountEvent = new CustomEvent(AUTOFOCUS_ON_UNMOUNT, EVENT_OPTIONS); | |
| container.addEventListener(AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus); | |
| container.dispatchEvent(unmountEvent); | |
| if (!unmountEvent.defaultPrevented) { | |
| focus(previouslyFocusedElement ?? document.body, { select: true }); | |
| } | |
| container.removeEventListener(AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus); | |
| focusScopesStack.remove(focusScope); | |
| }, 0); | |
| }; | |
| } | |
| }, [container, onMountAutoFocus, onUnmountAutoFocus, focusScope]); | |
| const handleKeyDown = React.useCallback( | |
| (event) => { | |
| if (!loop && !trapped) return; | |
| if (focusScope.paused) return; | |
| const isTabKey = event.key === "Tab" && !event.altKey && !event.ctrlKey && !event.metaKey; | |
| const focusedElement = document.activeElement; | |
| if (isTabKey && focusedElement) { | |
| const container2 = event.currentTarget; | |
| const [first, last] = getTabbableEdges(container2); | |
| const hasTabbableElementsInside = first && last; | |
| if (!hasTabbableElementsInside) { | |
| if (focusedElement === container2) event.preventDefault(); | |
| } else { | |
| if (!event.shiftKey && focusedElement === last) { | |
| event.preventDefault(); | |
| if (loop) focus(first, { select: true }); | |
| } else if (event.shiftKey && focusedElement === first) { | |
| event.preventDefault(); | |
| if (loop) focus(last, { select: true }); | |
| } | |
| } | |
| } | |
| }, | |
| [loop, trapped, focusScope.paused] | |
| ); | |
| return /* @__PURE__ */ jsx(Primitive.div, { tabIndex: -1, ...scopeProps, ref: composedRefs, onKeyDown: handleKeyDown }); | |
| }); | |
| FocusScope.displayName = FOCUS_SCOPE_NAME; | |
| function focusFirst(candidates, { select = false } = {}) { | |
| const previouslyFocusedElement = document.activeElement; | |
| for (const candidate of candidates) { | |
| focus(candidate, { select }); | |
| if (document.activeElement !== previouslyFocusedElement) return; | |
| } | |
| } | |
| function getTabbableEdges(container) { | |
| const candidates = getTabbableCandidates(container); | |
| const first = findVisible(candidates, container); | |
| const last = findVisible(candidates.reverse(), container); | |
| return [first, last]; | |
| } | |
| function getTabbableCandidates(container) { | |
| const nodes = []; | |
| const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, { | |
| acceptNode: (node) => { | |
| const isHiddenInput = node.tagName === "INPUT" && node.type === "hidden"; | |
| if (node.disabled || node.hidden || isHiddenInput) return NodeFilter.FILTER_SKIP; | |
| return node.tabIndex >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; | |
| } | |
| }); | |
| while (walker.nextNode()) nodes.push(walker.currentNode); | |
| return nodes; | |
| } | |
| function findVisible(elements, container) { | |
| for (const element of elements) { | |
| if (!isHidden(element, { upTo: container })) return element; | |
| } | |
| } | |
| function isHidden(node, { upTo }) { | |
| if (getComputedStyle(node).visibility === "hidden") return true; | |
| while (node) { | |
| if (upTo !== void 0 && node === upTo) return false; | |
| if (getComputedStyle(node).display === "none") return true; | |
| node = node.parentElement; | |
| } | |
| return false; | |
| } | |
| function isSelectableInput(element) { | |
| return element instanceof HTMLInputElement && "select" in element; | |
| } | |
| function focus(element, { select = false } = {}) { | |
| if (element && element.focus) { | |
| const previouslyFocusedElement = document.activeElement; | |
| element.focus({ preventScroll: true }); | |
| if (element !== previouslyFocusedElement && isSelectableInput(element) && select) | |
| element.select(); | |
| } | |
| } | |
| var focusScopesStack = createFocusScopesStack(); | |
| function createFocusScopesStack() { | |
| let stack = []; | |
| return { | |
| add(focusScope) { | |
| const activeFocusScope = stack[0]; | |
| if (focusScope !== activeFocusScope) { | |
| activeFocusScope?.pause(); | |
| } | |
| stack = arrayRemove(stack, focusScope); | |
| stack.unshift(focusScope); | |
| }, | |
| remove(focusScope) { | |
| stack = arrayRemove(stack, focusScope); | |
| stack[0]?.resume(); | |
| } | |
| }; | |
| } | |
| function arrayRemove(array, item) { | |
| const updatedArray = [...array]; | |
| const index = updatedArray.indexOf(item); | |
| if (index !== -1) { | |
| updatedArray.splice(index, 1); | |
| } | |
| return updatedArray; | |
| } | |
| function removeLinks(items) { | |
| return items.filter((item) => item.tagName !== "A"); | |
| } | |
| var Root = FocusScope; | |
| export { | |
| FocusScope, | |
| Root | |
| }; | |
| //# sourceMappingURL=index.mjs.map | |