Dialog

Categoryprimitive
ArchitectureReact Client Component
Dependencies

Usage

Dialog is a primitive component, therefore it won't be used as an standalone component, you can checkout some of it use cases in the following components:

Install Instructions

Copy and paste the code into that file

import {
  type Dispatch,
  type SetStateAction,
  type MouseEventHandler,
  type ComponentProps,
  type SyntheticEvent,
  type EventHandler,
  useEffect,
  useRef,
  forwardRef,
  useCallback,
  useState,
} from "react";
import { createPortal } from "react-dom";

// CVA
import { cva } from "class-variance-authority";

// TW Merge
import { twMerge } from "tailwind-merge";

export interface DialogProps
  extends Omit<ComponentProps<"dialog">, "onFocusCapture" | "onBlur"> {
  state: boolean; // Defines the current state of the dialog element
  setState?: Dispatch<SetStateAction<boolean>> | undefined; // Sets the current state of the dialog element
  onExit?: () => void; // Will get called after the end of dialog animation
  className?: string; // Will be appended to the default dialog classes
  transitionDuration?: number; // Duration of the animation
  animateOpacity?: boolean; // Weather if opacity should be animated
  animateTransform?: boolean; // Weather if transition should be animated
  closable?: boolean; // Weather if the dialog can be closed be other means
}

const dialogStyles = cva(
  [
    "not-prose",
    "max-h-none",
    "max-w-none",
    "origin-center",
    "flex-col",
    "content-center",
    "items-center",
    "justify-center",
    "fill-neutral-900",
    "text-neutral-900",
    "duration-[var(--transition-duration)]",
    "backdrop:bg-neutral-200/0",
    "backdrop:opacity-0",
    "backdrop:backdrop-blur-lg",
    "backdrop:md:backdrop-blur-md",
    "backdrop:transition-all",
    "backdrop:bg-neutral-950/25",
    "backdrop:dark:bg-neutral-950/50",
    "backdrop:transition-all",
    "data-[display=false]:pointer-events-none",
    "data-[display=true]:opacity-100",
    "data-[display=true]:backdrop:opacity-100",
    "dark:fill-neutral-100",
    "dark:text-neutral-100",
  ],
  {
    variants: {
      transitions: {
        all: ["transition-all", "opacity-0"],
        opacityOnly: ["transition-opacity", "opacity-0"],
        transformOnly: ["transition-transform", "opacity-100"],
      },
    },
    defaultVariants: {
      transitions: "all",
    },
  }
);

const getVariant = (animateOpacity: boolean, animateTransform: boolean) => {
  if (animateOpacity && animateTransform) return "all";
  if (animateOpacity) return "opacityOnly";
  if (animateTransform) return "transformOnly";
  return "all";
};

/** Calculates the width of the scrollbar */
const calcScrollBarWidth = () => {
  if (typeof document === "undefined") return 0;

  const innerWidth = document.body.getBoundingClientRect().width;
  const windowWidth = window.innerWidth;

  return windowWidth - innerWidth;
};

const preventEvent: EventHandler<any> = (e) => e.preventDefault();

/**
 * Base Dialog element, mainly for internal usage.
 */
export const Dialog = forwardRef<HTMLDialogElement, DialogProps>(
  (
    {
      state,
      setState,
      transitionDuration = 300,
      className = "",
      animateOpacity = true,
      animateTransform = true,
      closable = true,
      onExit: onExitCB,
      children,
      ...props
    },
    ref?
  ) => {
    const hasModifiedBodyRef = useRef(false);
    const dialogRef = useRef<HTMLDialogElement>(null);
    const [isBrowser, setIsBrowser] = useState(false);

    useEffect(() => {
      if (typeof window !== "undefined") setIsBrowser(true);
    }, []);

    useEffect(() => {
      const el =
        typeof ref === "undefined" || ref === null
          ? dialogRef.current
          : (ref as { current: HTMLDialogElement }).current;
      const timeouts: number[] = [];
      const isTheOnlyModal = document.body.getAttribute("data-modal-open");

      const scrollbarWidth = calcScrollBarWidth();

      if (el === null) return;
      el.style.setProperty("--transition-duration", `${transitionDuration}ms`);

      if (state === true) {
        el.showModal();

        // Only modifying the body incase this is the only open modal
        if (!isTheOnlyModal) {
          document.documentElement.style.setProperty(
            "padding-right",
            `${scrollbarWidth}px`
          );
          document.documentElement.style.setProperty("overflow", "hidden");

          // Notifying that there is a open modal
          document.documentElement.setAttribute("data-modal-open", "true");
          hasModifiedBodyRef.current = true;

          (el.firstElementChild as HTMLElement | null)?.focus();
        }

        el.style.display = "flex";

        requestAnimationFrame(() => el.setAttribute("data-display", "true"));
      } else {
        el.setAttribute("data-display", "false");
        timeouts.push(
          setTimeout(() => {
            requestAnimationFrame(() => {
              el.close();
              el.style.display = "none";

              // Incase we have modified the body, we need to change it back to default
              if (hasModifiedBodyRef.current) {
                document.documentElement.style.removeProperty("padding-right");
                document.documentElement.style.removeProperty("overflow");

                // Cleanup
                document.documentElement.removeAttribute("data-modal-open");
                hasModifiedBodyRef.current = false;
              }

              onExitCB && onExitCB();
            });
          }, transitionDuration) as unknown as number
        );
      }

      return () => timeouts.forEach((t) => clearTimeout(t));
    }, [state, transitionDuration, ref, onExitCB]);

    /**
     * Closes the dialog incase it is closable and user clicked outside of the dialog contents
     */
    const onClick: MouseEventHandler<HTMLDialogElement> = useCallback(
      (e) => {
        if (!closable) return;

        const el =
          typeof ref === "undefined" || ref === null
            ? dialogRef.current
            : (ref as { current: HTMLDialogElement }).current;

        if (el === null) return;

        const { top, bottom, left, right } = el.getBoundingClientRect();
        const { clientX, clientY } = e;

        if (
          clientX < left ||
          clientX > right ||
          clientY < top ||
          clientY > bottom
        ) {
          setState && setState(false);
        }
      },
      [closable, ref, setState]
    );

    /** Closes the modal in an elegant way */
    const onExit = useCallback(
      (e: SyntheticEvent<HTMLDialogElement, Event>) => {
        e.preventDefault();

        const el =
          typeof ref === "undefined" || ref === null
            ? dialogRef.current
            : (ref as { current: HTMLDialogElement }).current;

        const eventOnTheSameElement = (e.target as HTMLDivElement).isSameNode(
          el
        );

        // Only closing the element in case all the following criterias are met
        // eventOnTheSameElement -> the close/cancel event happened on the current element
        // closable -> the prop 'closable' is set to true
        // setState -> the prop 'setState' is not undefined
        if (eventOnTheSameElement && closable && setState) {
          setState(false);
        }
      },
      [closable, setState, ref]
    );

    return (
      <>
        {isBrowser ? (
          createPortal(
            <dialog
              ref={typeof ref === "undefined" || ref === null ? dialogRef : ref}
              className={twMerge(
                dialogStyles({
                  transitions: getVariant(animateOpacity, animateTransform),
                }),
                className
              )}
              data-dialog-open={state}
              data-draggable={true}
              onClick={onClick}
              onClose={onExit}
              onCancel={onExit}
              onTouchMove={preventEvent}
              {...props}
            >
              {children}
            </dialog>,
            document.body
          )
        ) : (
          <></>
        )}
      </>
    );
  }
);

Dialog.displayName = "Dialog";

Loading...