// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause import * as DialogPrimitive from "@radix-ui/react-dialog" import cx from "classnames" import React, { Component, ComponentProps, FormEvent } from "react" import X from "src/assets/icons/x.svg?react" import Button from "src/ui/button" import PortalContainerContext from "src/ui/portal-container-context" import { isObject } from "src/utils/util" type ButtonProp = boolean | string | Partial> /** * ControlledDialogProps are common props required for dialog components with * controlled state. Since Dialog components frequently expose these props to * their callers, we've consolidated them here for easy access. */ export type ControlledDialogProps = { /** * open is a boolean that controls whether the dialog is open or not. */ open: boolean /** * onOpenChange is a callback that is called when the open state of the dialog * changes. */ onOpenChange: (open: boolean) => void } type PointerDownOutsideEvent = CustomEvent<{ originalEvent: PointerEvent }> type Props = { className?: string /** * title is the title of the dialog, shown at the top. */ title: string /** * titleSuffixDecoration is added to the title, but is not part of the ARIA label for * the dialog. This is useful for adding a badge or other non-semantic * information to the title. */ titleSuffixDecoration?: React.ReactNode /** * trigger is an element to use as a trigger for a dialog. Using trigger is * preferrable to using `open` for managing state, as it allows for better * focus management for screen readers. */ trigger?: React.ReactNode /** * children is the content of the dialog. */ children: React.ReactNode /** * defaultOpen is the default state of the dialog. This is meant to be used for * uncontrolled dialogs, and should not be combined with `open` or * `onOpenChange`. */ defaultOpen?: boolean /** * restoreFocus determines whether the dialog returns focus to the trigger * element or not after closing. */ restoreFocus?: boolean onPointerDownOutside?: (e: PointerDownOutsideEvent) => void } & Partial const dialogOverlay = "fixed overflow-y-auto inset-0 py-8 z-10 bg-gray-900 bg-opacity-[0.07]" const dialogWindow = cx( "bg-white rounded-lg relative max-w-lg min-w-[19rem] w-[97%] shadow-dialog", "p-4 md:p-6 my-8 mx-auto", // We use `transform-gpu` here to force the browser to put the dialog on its // own layer. This helps fix some weird artifacting bugs in Safari caused by // box-shadows. See: https://github.com/tailscale/corp/issues/12270 "transform-gpu" ) /** * Dialog provides a modal dialog, for prompting a user for input or confirmation * before proceeding. */ export default function Dialog(props: Props) { const { open, className, defaultOpen, onOpenChange, trigger, title, titleSuffixDecoration, children, restoreFocus = true, onPointerDownOutside, } = props return ( {trigger && ( {trigger} )} {(portalContainer) => ( e.preventDefault() : undefined } onPointerDownOutside={onPointerDownOutside} >
{title} {titleSuffixDecoration}
{children}
)}
) } /** * Dialog.Form is a standard way of providing form-based interactions in a * Dialog component. Prefer it to custom form implementations. See each props * documentation for details. * * * * */ Dialog.Form = DialogForm type FormProps = { /** * destructive declares whether the submit button should be styled as a danger * button or not. Prefer `destructive` over passing a props object to * `submitButton`, since objects cause unnecessary re-renders unless they are * moved outside the render function. */ destructive?: boolean /** * children is the content of the dialog form. */ children?: React.ReactNode /** * disabled determines whether the submit button should be disabled. The * cancel button cannot be disabled via this prop. */ disabled?: boolean /** * loading determines whether the submit button should display a loading state * and the cancel button should be disabled. */ loading?: boolean /** * cancelButton determines how the cancel button looks. You can pass `true`, * which adds a default button, pass a string which changes the button label, * or pass an object, which is a set of props to pass to a `Button` component. * Any unspecified props will fall back to default values. * * * * */ cancelButton?: ButtonProp /** * submitButton determines how the submit button looks. You can pass `true`, * which adds a default button, pass a string which changes the button label, * or pass an object, which is a set of props to pass to a `Button` component. * Any unspecified props will fall back to default values. * * * * * */ submitButton?: ButtonProp /** * onSubmit is the callback to use when the form is submitted. Using `onSubmit` * is preferrable to a `onClick` handler on `submitButton`, which doesn't get * triggered on keyboard events. */ onSubmit?: () => void /** * autoFocus makes it easy to focus a particular action button without * overriding the button props. */ autoFocus?: "submit" | "cancel" } function DialogForm(props: FormProps) { const { children, disabled = false, destructive = false, loading = false, autoFocus = "submit", cancelButton, submitButton, onSubmit, } = props const hasFooter = Boolean(cancelButton || submitButton) const handleSubmit = (e: FormEvent) => { e.preventDefault() onSubmit?.() } const cancelAutoFocus = Boolean( cancelButton && !loading && autoFocus === "cancel" ) const submitAutoFocus = Boolean( submitButton && !loading && !disabled && autoFocus === "submit" ) const submitIntent = destructive ? "danger" : "primary" let cancelButtonEl = null if (cancelButton) { cancelButtonEl = cancelButton === true ? (