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
This commit is contained in:
Ivan Starkov authored and GitHub committed 2024-05-08 22:50:57 +03:00
1 parent ce1f9026cd
commit e806bcbf5c
1 file changed
+122 -2
@@ -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<ComponentPropsWithoutRef<typeof DialogPrimitive.Root>, "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 <DialogPrimitive.Root {...props} />;
});
@@ -49,7 +120,56 @@ export const DialogOverlay = forwardRef<
);
});
export const DialogContent = DialogPrimitive.Content;
export const DialogContent = forwardRef<
HTMLDivElement,
ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>((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 (
<DialogPrimitive.Content
ref={ref}
{...props}
onCloseAutoFocus={(event) => {
if (preventAutoFocusOnClose.current) {
event.preventDefault();
}
}}
/>
);
});
export const DialogClose = DialogPrimitive.Close;
type Tag = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";