Just partially finished my battle with plasmic’s Dialog (based on radix-ui).
My requirements:
- I want dialog overlay to be scrollable, but only when the dialog content exceeds the viewport.
- I want dialog content to be centered to the screen.
- Clicking on the scrollbar shouldn’t close the dialog.
Currently:
- You can do this by yourself in Plasmic but then requirement no 2. is impossible.
- To do 1+2 you have to apply CSS to the overlay as in https://www.radix-ui.com/primitives/docs/components/dialog#scrollable-overlay . But because
display: grid
can’t be set at the overlay class name level, I had to do a dirty workaround by adding a global css to the project:
[data-state].pl__fixed.pl__inset-0.pl__z-50 {
display: grid;
place-items: center;
overflow: auto;
}
- This one is still impossible. I think one needs to set
onPointerDownOutside
prop on the radix’s element and prevent the dialog from being closed when user points down on the scrollbar. It might be Radix’s bug ( https://github.com/radix-ui/primitives/issues/1280 ).
Problem is, I can’t set onPointerDownOutside
without modifying plasmic’s code. Could we export it as a prop?
This will also allow for preventing the overlay click from closing the dialog in general. (So to close the dialog, user needs to explicitly click on the close button.)
FYI just managed to solve 3. by passing the given onPointerDownOutside
to the radix’s Dialog.Content
:
/**
* Based on <https://github.com/tailwindlabs/headlessui/pull/1333/files#diff-d095a5f3fa3ad7f5ff99576cb61e5d75a979a6b7d5557f8a092f5d5c8c0c34deR49>
*/
function preventEventIfScrollbarClick(event: PointerDownOutsideEvent) {
const target = event.target as HTMLElement;
const viewport = target.ownerDocument.documentElement;
// Ignore if the target doesn't exist in the DOM anymore
if (!viewport.contains(target)) return;
const originalEvent = event.detail.originalEvent;
const scrollbarWidth = 20;
if (
originalEvent.clientX > viewport.clientWidth - scrollbarWidth ||
originalEvent.clientX < scrollbarWidth ||
originalEvent.clientY > viewport.clientHeight - scrollbarWidth ||
originalEvent.clientY < scrollbarWidth
) {
event.preventDefault();
}
}
but I had to rewrite Dialog by scratch for now for myself here’s the code if anybody needs:
import * as Dialog from "@radix-ui/react-dialog";
import { ReactNode } from "react";
import styles from "./MyDialog.module.css";
export interface MyDialogProps {
open?: boolean;
defaultOpen?: boolean;
overlayClass: string;
themeResetClass: string;
children: ReactNode;
onOpenChange?(open: boolean): void;
onPointerDownOutside?: (event: PointerDownOutsideEvent) => void;
}
type PointerDownOutsideEvent = CustomEvent<{
originalEvent: PointerEvent;
}>;
export function MyDialog({
open,
defaultOpen,
overlayClass,
themeResetClass,
children,
onOpenChange,
onPointerDownOutside,
}: MyDialogProps) {
return (
<Dialog.Root
open={open}
modal
defaultOpen={defaultOpen}
onOpenChange={onOpenChange}
>
<Dialog.Trigger />
<Dialog.Portal>
<Dialog.Overlay
className={[themeResetClass, styles.overlay, overlayClass].join(" ")}
>
<Dialog.Content
onPointerDownOutside={(event) => {
preventEventIfScrollbarClick(event);
onPointerDownOutside?.(event);
}}
>
{children}
</Dialog.Content>
</Dialog.Overlay>
</Dialog.Portal>
</Dialog.Root>
);
}
/**
* Based on <https://github.com/tailwindlabs/headlessui/pull/1333/files#diff-d095a5f3fa3ad7f5ff99576cb61e5d75a979a6b7d5557f8a092f5d5c8c0c34deR49>
*/
function preventEventIfScrollbarClick(event: PointerDownOutsideEvent) {
const target = event.target as HTMLElement;
const viewport = target.ownerDocument.documentElement;
// Ignore if the target doesn't exist in the DOM anymore
if (!viewport.contains(target)) return;
const originalEvent = event.detail.originalEvent;
const scrollbarWidth = 20;
if (
originalEvent.clientX > viewport.clientWidth - scrollbarWidth ||
originalEvent.clientX < scrollbarWidth ||
originalEvent.clientY > viewport.clientHeight - scrollbarWidth ||
originalEvent.clientY < scrollbarWidth
) {
event.preventDefault();
}
}
export function MyDialogTitle({
className,
children,
}: {
className: string;
children: ReactNode;
}) {
return <Dialog.Title className={className}>{children}</Dialog.Title>;
}
export function MyDialogDescription({
className,
children,
}: {
className: string;
children: ReactNode;
}) {
return (
<Dialog.Description className={className}>{children}</Dialog.Description>
);
}
export function MyDialogClose({
className,
children,
}: {
className: string;
children: ReactNode;
}) {
return <Dialog.Title className={className}>{children}</Dialog.Title>;
}
// styles.css
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: grid;
place-items: center;
overflow-y: auto;
}