UNPKG

@crossed/primitive

Version:

A universal & performant styling library for React Native, Next.js & React

288 lines (287 loc) 9.79 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var FocusScope_exports = {}; __export(FocusScope_exports, { FocusScope: () => FocusScope, useFocusScope: () => useFocusScope }); module.exports = __toCommonJS(FocusScope_exports); var import_jsx_runtime = require("react/jsx-runtime"); var React = __toESM(require("react")); var import_core = require("@crossed/core"); var import_useEvent = require("../useEvent"); const AUTOFOCUS_ON_MOUNT = "focusScope.autoFocusOnMount"; const AUTOFOCUS_ON_UNMOUNT = "focusScope.autoFocusOnUnmount"; const EVENT_OPTIONS = { bubbles: false, cancelable: true }; const FocusScope = React.forwardRef( function FocusScope2(props, forwardedRef) { const childProps = useFocusScope(props, forwardedRef); if (typeof props.children === "function") { return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: props.children(childProps) }); } return React.cloneElement( React.Children.only(props.children), childProps ); } ); function useFocusScope(props, forwardedRef) { const { loop = false, enabled = true, trapped = false, onMountAutoFocus: onMountAutoFocusProp, onUnmountAutoFocus: onUnmountAutoFocusProp, forceUnmount, children: _children, ...scopeProps } = props; const [container, setContainer] = React.useState(null); const onMountAutoFocus = (0, import_useEvent.useEvent)(onMountAutoFocusProp); const onUnmountAutoFocus = (0, import_useEvent.useEvent)(onUnmountAutoFocusProp); const lastFocusedElementRef = React.useRef(null); const composedRefs = (0, import_core.useComposedRefs)( forwardedRef, (node) => setContainer(node) ); const focusScope = React.useRef({ paused: false, pause() { this.paused = true; }, resume() { this.paused = false; } }).current; React.useEffect(() => { if (!enabled) return; if (!trapped) return; function handleFocusIn(event) { if (focusScope.paused || !container) return; const target = event.target; if (container.contains(target)) { lastFocusedElementRef.current = target; } else { focus(lastFocusedElementRef.current, { select: true }); } } function handleFocusOut(event) { if (focusScope.paused || !container) return; if (!container.contains(event.relatedTarget)) { focus(lastFocusedElementRef.current, { select: true }); } } document.addEventListener("focusin", handleFocusIn); document.addEventListener("focusout", handleFocusOut); return () => { document.removeEventListener("focusin", handleFocusIn); document.removeEventListener("focusout", handleFocusOut); }; }, [trapped, forceUnmount, container, focusScope.paused]); React.useEffect(() => { if (!enabled) return; if (!container) return; if (forceUnmount) return; 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); 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); }; }, [ enabled, container, forceUnmount, onMountAutoFocus, onUnmountAutoFocus, focusScope ]); const handleKeyDown = React.useCallback( (event) => { if (!trapped) return; if (!loop) 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 { tabIndex: -1, ...scopeProps, ref: composedRefs, onKeyDown: handleKeyDown }; } 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; } return; } 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 } = {}) { setTimeout(() => { if (element == null ? void 0 : element.focus) { const previouslyFocusedElement = document.activeElement; element.focus({ preventScroll: true }); if (element !== previouslyFocusedElement && isSelectableInput(element) && select) element.select(); } }); } const focusScopesStack = createFocusScopesStack(); function createFocusScopesStack() { let stack = []; return { add(focusScope) { const activeFocusScope = stack[0]; if (focusScope !== activeFocusScope) { activeFocusScope == null ? void 0 : activeFocusScope.pause(); } stack = arrayRemove(stack, focusScope); stack.unshift(focusScope); }, remove(focusScope) { var _a; stack = arrayRemove(stack, focusScope); (_a = stack[0]) == null ? void 0 : _a.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"); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { FocusScope, useFocusScope }); //# sourceMappingURL=FocusScope.js.map