Scrollable and centered Radix dialog with scrollbar issue in Plasmic

Just partially finished my battle with plasmic’s Dialog (based on radix-ui).

My requirements:

  1. I want dialog overlay to be scrollable, but only when the dialog content exceeds the viewport.
  2. I want dialog content to be centered to the screen.
  3. Clicking on the scrollbar shouldn’t close the dialog.
    Currently:
  4. You can do this by yourself in Plasmic but then requirement no 2. is impossible.
  5. 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;
}
  1. 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 :disappointed: 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;
}