UNPKG

@ariakit/react-core

Version:

Ariakit React core

275 lines (272 loc) 8.48 kB
"use client"; import { PortalContext } from "./AOQQTIBO.js"; import { FocusTrap } from "./S2F2XXEH.js"; import { createElement, createHook, forwardRef } from "./VOQWLFSQ.js"; import { useMergeRefs, useSafeLayoutEffect, useWrapElement } from "./5GGHRIN3.js"; import { setRef } from "./SK3NAZA3.js"; import { __objRest, __spreadProps, __spreadValues } from "./3YLGPPWQ.js"; // src/portal/portal.tsx import { getDocument } from "@ariakit/core/utils/dom"; import { isFocusEventOutside } from "@ariakit/core/utils/events"; import { disableFocusIn, getNextTabbable, getPreviousTabbable, restoreFocusIn } from "@ariakit/core/utils/focus"; import { useContext, useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { Fragment, jsx, jsxs } from "react/jsx-runtime"; var TagName = "div"; function getRootElement(element) { return getDocument(element).body; } function getPortalElement(element, portalElement) { if (!portalElement) { return getDocument(element).createElement("div"); } if (typeof portalElement === "function") { return portalElement(element); } return portalElement; } function getRandomId(prefix = "id") { return `${prefix ? `${prefix}-` : ""}${Math.random().toString(36).slice(2, 8)}`; } function queueFocus(element) { queueMicrotask(() => { element == null ? void 0 : element.focus(); }); } var usePortal = createHook(function usePortal2(_a) { var _b = _a, { preserveTabOrder, preserveTabOrderAnchor, portalElement, portalRef, portal = true } = _b, props = __objRest(_b, [ "preserveTabOrder", "preserveTabOrderAnchor", "portalElement", "portalRef", "portal" ]); const ref = useRef(null); const refProp = useMergeRefs(ref, props.ref); const context = useContext(PortalContext); const [portalNode, setPortalNode] = useState(null); const [anchorPortalNode, setAnchorPortalNode] = useState( null ); const outerBeforeRef = useRef(null); const innerBeforeRef = useRef(null); const innerAfterRef = useRef(null); const outerAfterRef = useRef(null); useSafeLayoutEffect(() => { const element = ref.current; if (!element || !portal) { setPortalNode(null); return; } const portalEl = getPortalElement(element, portalElement); if (!portalEl) { setPortalNode(null); return; } const isPortalInDocument = portalEl.isConnected; if (!isPortalInDocument) { const rootElement = context || getRootElement(element); rootElement.appendChild(portalEl); } if (!portalEl.id) { portalEl.id = element.id ? `portal/${element.id}` : getRandomId(); } setPortalNode(portalEl); setRef(portalRef, portalEl); if (isPortalInDocument) return; return () => { portalEl.remove(); setRef(portalRef, null); }; }, [portal, portalElement, context, portalRef]); useSafeLayoutEffect(() => { if (!portal) return; if (!preserveTabOrder) return; if (!preserveTabOrderAnchor) return; const doc = getDocument(preserveTabOrderAnchor); const element = doc.createElement("span"); element.style.position = "fixed"; preserveTabOrderAnchor.insertAdjacentElement("afterend", element); setAnchorPortalNode(element); return () => { element.remove(); setAnchorPortalNode(null); }; }, [portal, preserveTabOrder, preserveTabOrderAnchor]); useEffect(() => { if (!portalNode) return; if (!preserveTabOrder) return; let raf = 0; const onFocus = (event) => { if (!isFocusEventOutside(event)) return; const focusing = event.type === "focusin"; cancelAnimationFrame(raf); if (focusing) { return restoreFocusIn(portalNode); } raf = requestAnimationFrame(() => { disableFocusIn(portalNode, true); }); }; portalNode.addEventListener("focusin", onFocus, true); portalNode.addEventListener("focusout", onFocus, true); return () => { cancelAnimationFrame(raf); portalNode.removeEventListener("focusin", onFocus, true); portalNode.removeEventListener("focusout", onFocus, true); }; }, [portalNode, preserveTabOrder]); props = useWrapElement( props, (element) => { element = // While the portal node is not in the DOM, we need to pass the // current context to the portal context, otherwise it's going to // reset to the body element on nested portals. /* @__PURE__ */ jsx(PortalContext.Provider, { value: portalNode || context, children: element }); if (!portal) return element; if (!portalNode) { return /* @__PURE__ */ jsx( "span", { ref: refProp, id: props.id, style: { position: "fixed" }, hidden: true } ); } element = /* @__PURE__ */ jsxs(Fragment, { children: [ preserveTabOrder && portalNode && /* @__PURE__ */ jsx( FocusTrap, { ref: innerBeforeRef, "data-focus-trap": props.id, className: "__focus-trap-inner-before", onFocus: (event) => { if (isFocusEventOutside(event, portalNode)) { queueFocus(getNextTabbable()); } else { queueFocus(outerBeforeRef.current); } } } ), element, preserveTabOrder && portalNode && /* @__PURE__ */ jsx( FocusTrap, { ref: innerAfterRef, "data-focus-trap": props.id, className: "__focus-trap-inner-after", onFocus: (event) => { if (isFocusEventOutside(event, portalNode)) { queueFocus(getPreviousTabbable()); } else { queueFocus(outerAfterRef.current); } } } ) ] }); if (portalNode) { element = createPortal(element, portalNode); } let preserveTabOrderElement = /* @__PURE__ */ jsxs(Fragment, { children: [ preserveTabOrder && portalNode && /* @__PURE__ */ jsx( FocusTrap, { ref: outerBeforeRef, "data-focus-trap": props.id, className: "__focus-trap-outer-before", onFocus: (event) => { const fromOuter = event.relatedTarget === outerAfterRef.current; if (!fromOuter && isFocusEventOutside(event, portalNode)) { queueFocus(innerBeforeRef.current); } else { queueFocus(getPreviousTabbable()); } } } ), preserveTabOrder && // We're using position: fixed here so that the browser doesn't // add margin to the element when setting gap on a parent element. /* @__PURE__ */ jsx("span", { "aria-owns": portalNode == null ? void 0 : portalNode.id, style: { position: "fixed" } }), preserveTabOrder && portalNode && /* @__PURE__ */ jsx( FocusTrap, { ref: outerAfterRef, "data-focus-trap": props.id, className: "__focus-trap-outer-after", onFocus: (event) => { if (isFocusEventOutside(event, portalNode)) { queueFocus(innerAfterRef.current); } else { const nextTabbable = getNextTabbable(); if (nextTabbable === innerBeforeRef.current) { requestAnimationFrame(() => { var _a2; return (_a2 = getNextTabbable()) == null ? void 0 : _a2.focus(); }); return; } queueFocus(nextTabbable); } } } ) ] }); if (anchorPortalNode && preserveTabOrder) { preserveTabOrderElement = createPortal( preserveTabOrderElement, anchorPortalNode ); } return /* @__PURE__ */ jsxs(Fragment, { children: [ preserveTabOrderElement, element ] }); }, [portalNode, context, portal, props.id, preserveTabOrder, anchorPortalNode] ); props = __spreadProps(__spreadValues({}, props), { ref: refProp }); return props; }); var Portal = forwardRef(function Portal2(props) { const htmlProps = usePortal(props); return createElement(TagName, htmlProps); }); export { usePortal, Portal };