6e51cb7d16
- 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.
76 lines
1.9 KiB
TypeScript
76 lines
1.9 KiB
TypeScript
"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,
|
||
);
|
||
}
|