From e806bcbf5c6e06edb2bb20dec24c836e95cb63cb Mon Sep 17 00:00:00 2001 From: Ivan Starkov Date: Wed, 8 May 2024 22:50:57 +0300 Subject: [PATCH] feat: Close the dialog when a navigable link within it is clicked (#3329) ## Description Close the dialog when a navigable link within it is clicked Fixes the issue when we have anchor links inside Sheet ## Steps for reproduction ### Project Example: https://href.staging.webstudio.is/builder/95821bd9-040a-4294-aa7f-edf26d7f1ebe ### Published: https://link-inside-sheet-saas-sk7pk.wstd.work/ Open sheet Click links, links should work ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 5de6) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env.example` and the `builder/env-check.js` if mandatory --- .../sdk-components-react-radix/src/dialog.tsx | 124 +++++++++++++++++- 1 file changed, 122 insertions(+), 2 deletions(-) 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";