diff --git a/packages/sdk-components-react-radix/src/dialog.tsx b/packages/sdk-components-react-radix/src/dialog.tsx index 0ec90e8369..9f89df8175 100644 --- a/packages/sdk-components-react-radix/src/dialog.tsx +++ b/packages/sdk-components-react-radix/src/dialog.tsx @@ -7,15 +7,86 @@ import { forwardRef, Children, type ComponentProps, + useEffect, + useRef, + useContext, } from "react"; import * as DialogPrimitive from "@radix-ui/react-dialog"; -import { getClosestInstance, type Hook } from "@webstudio-is/react-sdk"; +import { + ReactSdkContext, + getClosestInstance, + type Hook, +} from "@webstudio-is/react-sdk"; + +/** + * Naive heuristic to determine if a click event will cause navigate + */ +const willNavigate = (event: MouseEvent) => { + const { target } = event; + + if (target instanceof HTMLAnchorElement === false) { + return false; + } + + if (target.hasAttribute("href") === false) { + return false; + } + + if (target.href === "#") { + return false; + } + + if (target.hasAttribute("target") && target.target === "_blank") { + return false; + } + + if (event.ctrlKey || event.metaKey) { + return false; + } + + return true; +}; // wrap in forwardRef because Root is functional component without ref export const Dialog = forwardRef< HTMLDivElement, Omit, "defaultOpen"> >((props, _ref) => { + const { open, onOpenChange } = props; + const { renderer } = useContext(ReactSdkContext); + + /** + * Close the dialog when a navigable link within it is clicked. + */ + useEffect(() => { + if (renderer !== undefined) { + return; + } + + if (open === false) { + return; + } + + const handleClick = (event: MouseEvent) => { + const { target } = event; + + if (willNavigate(event) === false) { + return; + } + + if (target instanceof HTMLAnchorElement === false) { + return false; + } + + if (target.closest('[role="dialog"]')) { + onOpenChange?.(false); + } + }; + + window.addEventListener("click", handleClick); + return () => window.removeEventListener("click", handleClick); + }, [open, onOpenChange, renderer]); + return ; }); @@ -49,7 +120,56 @@ export const DialogOverlay = forwardRef< ); }); -export const DialogContent = DialogPrimitive.Content; +export const DialogContent = forwardRef< + HTMLDivElement, + ComponentPropsWithoutRef +>((props, ref) => { + const preventAutoFocusOnClose = useRef(false); + const { renderer } = useContext(ReactSdkContext); + + /** + * Prevent focusing on the trigger after a navigable link in a dialog is clicked and closes the dialog. + */ + useEffect(() => { + if (renderer !== undefined) { + return; + } + + preventAutoFocusOnClose.current = false; + + const handleClick = (event: MouseEvent) => { + const { target } = event; + + if (willNavigate(event) === false) { + return; + } + + if (target instanceof HTMLAnchorElement === false) { + return false; + } + + if (target.closest('[role="dialog"]')) { + preventAutoFocusOnClose.current = true; + } + }; + + window.addEventListener("click", handleClick); + return () => window.removeEventListener("click", handleClick); + }, [renderer]); + + return ( + { + if (preventAutoFocusOnClose.current) { + event.preventDefault(); + } + }} + /> + ); +}); + export const DialogClose = DialogPrimitive.Close; type Tag = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";