Files
cozsweet-frontend-nextjs/src/app/_components/core/bottom-sheet.tsx
T
admin 6e51cb7d16 Refactor: Remove original Dart references from comments across multiple files
- Updated comments in various components, schemas, and repositories to remove references to original Dart files.
- Ensured consistency in documentation while maintaining the context of the code.
2026-06-29 11:31:21 +08:00

76 lines
1.9 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";
/**
* 通用底部弹层(Bottom Sheet
*
*
*
* 设计目标:
* - portal 渲染到 body
* - 固定底部、最大宽度 500px、顶部圆角 `--radius-bottom-sheet`
* - ESC 关闭 / 点击 scrim 关闭 / 打开时锁定 body 滚动
* - 简单 slide-up 动画(与原 Dart 行为接近)
* - 调用方只关心内容布局
*/
import { type ReactNode, useEffect } from "react";
import { createPortal } from "react-dom";
import styles from "./bottom-sheet.module.css";
export interface BottomSheetProps {
open: boolean;
onClose: () => void;
children: ReactNode;
/** 蒙层透明度(0-1)。默认 0.45。 */
scrimOpacity?: number;
/** 禁用点击外部 / ESC 关闭。 */
persistent?: boolean;
/** ARIA 标签。 */
ariaLabel?: string;
}
export function BottomSheet({
open,
onClose,
children,
scrimOpacity = 0.45,
persistent = false,
ariaLabel,
}: BottomSheetProps) {
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}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>,
document.body,
);
}