Files
cozsweet-frontend-nextjs/src/app/_components/core/dialog.tsx
T

80 lines
2.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
/**
* 通用 Dialog 基础组件(headless + 简单样式)
*
* 原始 Dart: 散落在各处的 `showDialog` + `Dialog` widgetquota_dialog, pwa_install_dialog,
* username_dialog, pronouns_dialog, subscription_dialog, environment_dialog, auth_other_options_dialog,
* facebook_login_dialog, external_browser_dialog)。
*
* 设计目标:~70 行覆盖所有 9 种 dialog 的通用逻辑(portal, ESC, scroll lock, click-outside),
* 调用方只关心内容布局。
*
* 注意:项目不引入 Radix/shadcn,纯依赖 React + CSS Modules。
*/
import { type ReactNode, useEffect } from "react";
import { createPortal } from "react-dom";
import styles from "./dialog.module.css";
export interface DialogProps {
open: boolean;
onClose: () => void;
children: ReactNode;
/** 内容最大宽度(px)。默认 360。 */
maxWidth?: number;
/** 蒙层透明度(0-1)。默认 0.5。 */
scrimOpacity?: number;
/** 禁用点击外部 / ESC 关闭。 */
persistent?: boolean;
/** ARIA 标签。 */
ariaLabel?: string;
}
export function Dialog({
open,
onClose,
children,
maxWidth = 360,
scrimOpacity = 0.5,
persistent = false,
ariaLabel,
}: DialogProps) {
useEffect(() => {
if (!open || typeof document === "undefined") return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape" && !persistent) onClose();
};
document.addEventListener("keydown", onKey);
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKey);
document.body.style.overflow = prevOverflow;
};
}, [open, onClose, persistent]);
if (!open || typeof document === "undefined") return null;
return createPortal(
<div
className={styles.scrim}
style={{ background: `rgba(0, 0, 0, ${scrimOpacity})` }}
onClick={(e) => {
if (!persistent && e.target === e.currentTarget) onClose();
}}
>
<div
role="dialog"
aria-modal="true"
aria-label={ariaLabel}
className={styles.panel}
style={{ maxWidth }}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>,
document.body,
);
}