import React, { useCallback, useEffect, useRef, useState } from "react"; import { useId } from "../../utils-external"; import { useControllableState, useEventCallback, useTransitionStatus, } from "../../utils/hooks"; import { DialogBody, DialogBodyProps } from "../body/DialogBody"; import { DialogCloseTrigger, DialogCloseTriggerProps, } from "../close-trigger/DialogCloseTrigger"; import { DialogDescription, DialogDescriptionProps, } from "../description/DialogDescription"; import { DialogFooter, DialogFooterProps } from "../footer/DialogFooter"; import { DialogHeader, DialogHeaderProps } from "../header/DialogHeader"; import { DialogPopup, DialogPopupProps } from "../popup/DialogPopup"; import { DialogTitle, DialogTitleProps } from "../title/DialogTitle"; import { DialogTrigger, DialogTriggerProps } from "../trigger/DialogTrigger"; import { DialogContextProvider, useDialogContext } from "./DialogRoot.context"; interface DialogProps { children: React.ReactNode; /** * Whether the dialog is currently open. */ open?: boolean; /** * Whether the dialog should be initially open. * * To render a controlled dialog, use the `open` prop instead. * @default false */ defaultOpen?: boolean; /** * Event handler called when the dialog is opened or closed. */ onOpenChange?: (nextOpen: boolean, event: Event) => void; /** * Event handler called after any animations complete when the dialog is opened or closed. */ onOpenChangeComplete?: (open: boolean) => void; /** * Updates sub-component padding + DialogTitle and DialogDescription font-size. * @default "medium" */ size?: "medium" | "small"; } interface DialogComponent extends React.FC { /** * @see 🏷️ {@link DialogTriggerProps} * @example * ```jsx * * * * * * ``` */ Trigger: typeof DialogTrigger; /** * @see 🏷️ {@link DialogCloseTriggerProps} * @example * ```jsx * * * * * * * * ``` */ CloseTrigger: typeof DialogCloseTrigger; /** * @see 🏷️ {@link DialogPopupProps} * @example * ```jsx * * * ... * * * ``` */ Popup: typeof DialogPopup; /** * @see 🏷️ {@link DialogHeaderProps} * @example * ```jsx * * * * Dialog title * * * * ``` */ Header: typeof DialogHeader; /** * @see 🏷️ {@link DialogTitleProps} * @example * ```jsx * * * * Dialog title * * * * ``` */ Title: typeof DialogTitle; /** * @see 🏷️ {@link DialogDescriptionProps} * @example * ```jsx * * * * Dialog title * Dialog description * * * * ``` */ Description: typeof DialogDescription; /** * @see 🏷️ {@link DialogBodyProps} * @example * ```jsx * * * * Dialog body content * * * * ``` */ Body: typeof DialogBody; /** * @see 🏷️ {@link DialogFooterProps} * @example * ```jsx * * * * * * * * * * ``` */ Footer: typeof DialogFooter; } /** * Dialog component for displaying modal content on top of an application. * @see [📝 Documentation](https://aksel.nav.no/komponenter/core/dialog) * @see 🏷️ {@link DialogProps} * @example * ```jsx * * * * * * * Dialog title * Dialog description * * * Dialog body content * * * * * * * * * ``` */ export const Dialog: DialogComponent = (props: DialogProps) => { const { children, defaultOpen = false, open: openParam, onOpenChange, onOpenChangeComplete, size = "medium", } = props; const [open, setOpenStateInternal] = useControllableState({ defaultValue: defaultOpen, value: openParam, }); const { mounted, setMounted, transitionStatus } = useTransitionStatus(open); const popupRef = useRef(null); const [triggerElement, setTriggerElement] = useState( null, ); const [popupElement, setPopupElement] = useState(null); const defaultId = useId(); const [titleId, setTitleId] = useState(); const [ownNestedOpenDialogs, setOwnNestedOpenDialogs] = useState(0); const nestedDialogOpened = useCallback((nestedCount: number) => { setOwnNestedOpenDialogs(nestedCount + 1); }, []); const nestedDialogClosed = useCallback(() => { setOwnNestedOpenDialogs(0); }, []); const parentContext = useDialogContext(false); /** * Notify parent dialog about nested dialogs opening/closing. * This allows us to better hide/obscure parent dialogs when nested dialogs are opened. * * This pattern is not good for deep nesting since the context updates will cause cascading renders * but should work fine for 1-2 levels of nesting which is the most common use case here. */ useEffect(() => { if (open && parentContext) { parentContext.nestedDialogOpened(ownNestedOpenDialogs); return () => parentContext.nestedDialogClosed(); } }, [open, parentContext, ownNestedOpenDialogs]); /** * Passing the original event to onOpenChange to allow preventing the state change */ const setOpen = useEventCallback( (nextOpen: boolean, originalEvent: Event) => { onOpenChange?.(nextOpen, originalEvent); if (originalEvent?.defaultPrevented) { return; } setOpenStateInternal(nextOpen); }, ); return ( {children} ); }; Dialog.Trigger = DialogTrigger; Dialog.CloseTrigger = DialogCloseTrigger; Dialog.Header = DialogHeader; Dialog.Title = DialogTitle; Dialog.Description = DialogDescription; Dialog.Body = DialogBody; Dialog.Footer = DialogFooter; Dialog.Popup = DialogPopup; export default Dialog; export { DialogTrigger, DialogCloseTrigger, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter, DialogPopup, }; export type { DialogProps, DialogTriggerProps, DialogCloseTriggerProps, DialogHeaderProps, DialogTitleProps, DialogDescriptionProps, DialogBodyProps, DialogFooterProps, DialogPopupProps, };