@1771technologies/lytenyte-pro
Version:
Blazingly fast headless React data grid with 100s of features.
222 lines (221 loc) • 9.39 kB
JavaScript
import { jsx as _jsx } from "react/jsx-runtime";
import { forwardRef, useEffect, useRef, useState } from "react";
import { useDialogRoot } from "./context.js";
import { arrow, autoUpdate, computePosition, flip, hide, inline, offset, shift, } from "../../external/floating-ui.js";
import { transformOrigin } from "./transform-origin.js";
import { FocusTrap } from "@1771technologies/lytenyte-shared";
import { getActiveElement, getTabbables, SCROLL_LOCKER } from "@1771technologies/lytenyte-shared";
import { useCombinedRefs } from "@1771technologies/lytenyte-core/internal";
import { useTransitioned } from "../../../hooks/use-transitioned-open.js";
function DialogContainerBase(props, ref) {
const { open, onOpenChange, onOpenChangeComplete, titleId, descriptionId, focusCanReturn, focusCanTrap, focusFallback, focusInitial, focusPreventScroll, focusReturn, focusTrap, lockScroll, lightDismiss, modal = true, anchor, alignOffset, inline: inlineV, placement, shiftPadding, sideOffset, hide: shouldHide, arrow: arrowEl, } = useDialogRoot();
const [dialog, setDialog] = useState(null);
const [t, shouldMount] = useTransitioned(open, dialog, onOpenChangeComplete);
const locked = useRef(false);
useEffect(() => {
if (!open || !anchor)
return;
const anchorEl = typeof anchor === "string" ? document.querySelector(anchor) : anchor;
if (!anchorEl || !dialog)
return;
const middleware = [offset({ alignmentAxis: alignOffset, mainAxis: sideOffset })];
if (inlineV)
middleware.push(inline({ padding: shiftPadding }));
const flipMw = flip({
crossAxis: "alignment",
fallbackAxisSideDirection: "end",
});
const shiftMw = shift({
padding: shiftPadding,
mainAxis: true,
});
const shiftMwX = shift({
padding: shiftPadding,
crossAxis: true,
});
if (placement.includes("-"))
middleware.push(flipMw, shiftMw, shiftMwX);
else
middleware.push(shiftMw, flipMw, shiftMwX);
middleware.push(transformOrigin({ arrowHeight: 0, arrowWidth: 0 }));
if (shouldHide) {
middleware.push(hide());
}
if (arrowEl)
middleware.push(arrow({ element: arrowEl, padding: 0 }));
const clean = autoUpdate(anchorEl, dialog, async () => {
const pos = await computePosition(anchorEl, dialog, {
strategy: "fixed",
placement: placement,
middleware,
});
const hidden = pos.middlewareData.hide?.referenceHidden;
if (hidden && shouldHide) {
dialog.style.visibility = "hidden";
}
else {
dialog.style.visibility = "visible";
}
const x = pos.middlewareData.transformOrigin.x;
const y = pos.middlewareData.transformOrigin.y;
const anchorBB = anchorEl.getBoundingClientRect();
Object.assign(dialog.style, {
top: `${pos.y}px`,
left: `${pos.x}px`,
transformOrigin: `${x} ${y}`,
});
dialog.style.setProperty("--ln-anchor-width", `${anchorBB.width}px`);
dialog.style.setProperty("--ln-anchor-height", `${anchorBB.height}px`);
if (arrowEl) {
const { x, y } = pos.middlewareData.arrow ?? {};
const top = y;
const left = x;
arrowEl.setAttribute("data-ln-placement", pos.placement.split("-").at(0) ?? "");
Object.assign(arrowEl.style, {
left: left != null ? `${left}px` : "",
top: top != null ? `${top}px` : "",
});
}
});
return () => clean();
}, [
alignOffset,
anchor,
arrowEl,
dialog,
inlineV,
open,
placement,
shiftPadding,
shouldHide,
shouldMount,
sideOffset,
]);
const lockTimeout = useRef(null);
useEffect(() => {
if (lockScroll == false)
return;
if (lockTimeout.current)
clearTimeout(lockTimeout.current);
if (shouldMount && !locked.current) {
SCROLL_LOCKER.acquire(null);
locked.current = true;
}
else if (!shouldMount && locked.current) {
SCROLL_LOCKER.release();
locked.current = false;
}
return () => {
lockTimeout.current = setTimeout(() => {
if (locked.current)
SCROLL_LOCKER.release();
locked.current = false;
lockTimeout.current = null;
}, 20);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldMount]);
const trapRef = useRef(null);
useEffect(() => {
if (!dialog)
return;
const controller = new AbortController();
if (shouldMount) {
dialog.addEventListener("keydown", (ev) => {
if (ev.key === "Escape") {
// Check if the dialog contains another open dialog
const otherDialog = dialog.querySelector('[data-ln-light-dismiss="true"]');
if (otherDialog)
return;
ev.stopPropagation();
if (lightDismiss != false)
onOpenChange(false);
ev.preventDefault();
}
}, { signal: controller.signal });
}
if (shouldMount && lightDismiss != false) {
document.addEventListener("click", (ev) => {
const bb = dialog.getBoundingClientRect();
if (ev.button !== 0)
return;
if (ev.target != dialog && dialog.contains(ev.target))
return;
// Check if the dialog contains another open dialog
const otherDialog = dialog.querySelector('[data-ln-light-dismiss="true"]');
if (otherDialog)
return;
if (ev.clientX < bb.left ||
ev.clientX > bb.right ||
ev.clientY < bb.top ||
ev.clientY > bb.bottom) {
ev.stopPropagation();
ev.stopImmediatePropagation();
if (typeof lightDismiss === "function") {
const res = lightDismiss(ev.target);
if (!res)
return;
}
setTimeout(() => onOpenChange(false));
}
}, { capture: true, signal: controller.signal });
}
const options = {
preventScroll: focusPreventScroll,
checkCanReturnFocus: focusCanReturn,
checkCanFocusTrap: focusCanTrap,
fallbackFocus: focusFallback,
initialFocus: focusInitial,
setReturnFocus: focusReturn ?? getActiveElement(document),
};
Object.keys(options).forEach((c) => {
if (options[c] === undefined)
delete options[c];
});
let obsRef = null;
if (!options.checkCanFocusTrap) {
const checkCanFocusTrap = (dialog) => {
const tabbables = getTabbables(dialog[0]);
if (tabbables.length)
return Promise.resolve();
return new Promise((res) => {
obsRef = new MutationObserver(() => {
if (getTabbables(dialog[0])) {
obsRef?.disconnect();
res();
}
});
obsRef.observe(dialog[0], { childList: true, subtree: true });
});
};
options.checkCanFocusTrap = checkCanFocusTrap;
}
const trap = new FocusTrap(dialog, options);
trapRef.current = trap;
if (shouldMount) {
if (modal != false)
dialog.showModal();
else
dialog.showPopover();
if (focusTrap != false)
trap.activate();
}
return () => {
controller.abort();
trap.deactivate();
obsRef?.disconnect();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dialog, shouldMount]);
// Deactivate and close the dialog if the component will no longer be mounted.
if (trapRef.current && !shouldMount) {
trapRef.current.deactivate();
trapRef.current = null;
}
const combined = useCombinedRefs(ref, setDialog);
const Element = (modal != false ? "dialog" : "div");
if (!shouldMount)
return null;
return (_jsx(Element, { ...props, popover: !modal ? "manual" : undefined, "aria-describedby": descriptionId, "aria-labelledby": titleId, "data-ln-light-dismiss": lightDismiss != false, "data-ln-transition": t, "data-ln-dialog": !props["data-ln-popover"] && !props["data-ln-menu-popover"] ? true : undefined, ref: combined, children: props.children }));
}
export const DialogContainer = forwardRef(DialogContainerBase);