/* Copyright 2023, 2024 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ import { type FC, type ReactNode, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { Root as DialogRoot, Portal as DialogPortal, Overlay as DialogOverlay, Content as DialogContent, Title as DialogTitle, Close as DialogClose, } from "@radix-ui/react-dialog"; import { Drawer } from "vaul"; import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; import { CloseIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import classNames from "classnames"; import { Heading, Glass } from "@vector-im/compound-web"; import styles from "./Modal.module.css"; import overlayStyles from "./Overlay.module.css"; import { useMediaQuery } from "./useMediaQuery"; export interface Props { title: string; /** * Hide the modal header. Used for smaller popups where the context is readily apparent. * A title should still be specified for users using assistive technology. */ hideHeader?: boolean; children: ReactNode; className?: string; /** * Class name to be used when in drawer mode (touchscreen). */ classNameDrawer?: string; /** * Class name to be used when in modal mode (desktop). */ classNameModal?: string; /** * The controlled open state of the modal. */ // An option to leave the open state uncontrolled is intentionally not // provided, since modals are always opened due to external triggers, and it // is the author's belief that controlled components lead to more obvious code. open: boolean; /** * Callback for when the user dismisses the modal. If undefined, the modal * will be non-dismissable. */ onDismiss?: () => void; /** * Whether the modal content has tabs. */ // TODO: Better tabs support tabbed?: boolean; } /** * A modal, taking the form of a drawer / bottom sheet on touchscreen devices, * and a dialog box on desktop. */ export const Modal: FC = ({ title, hideHeader, children, className, classNameDrawer, classNameModal, open, onDismiss, tabbed, ...rest }) => { const { t } = useTranslation(); // Empirically, Chrome on Android can end up not matching (hover: none), but // still matching (pointer: coarse) :/ const touchscreen = useMediaQuery("(hover: none) or (pointer: coarse)"); const onOpenChange = useCallback( (open: boolean) => { if (!open) onDismiss?.(); }, [onDismiss], ); if (touchscreen) { return (
{title}
{children}
); } else { const titleNode = ( {title} ); const header = (
{titleNode} {onDismiss !== undefined && ( )}
); return (
{!hideHeader ? header : null} {hideHeader ? ( {titleNode} ) : null}
{children}
); } };