| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- "use client";
- import clsx from "clsx";
- import { useState, useLayoutEffect, useRef } from "react";
- import Portal from "@/components/Portal";
- /**
- * 为了弹窗里的表单校验开发整个组件:
- * 当弹窗关闭,校验弹窗里的表单时要保证弹窗里的表单元素还存在文档流中,所以这个组件采用display block/none 来控制弹窗的显示和隐藏
- */
- export default function CommonModal({
- isOpen,
- contentClassName,
- clickOutsideClose,
- onClose,
- header,
- body,
- footer
- }: {
- isOpen: boolean;
- contentClassName?: string;
- clickOutsideClose?: boolean;
- onClose: (e: boolean) => void;
- header: React.ReactNode;
- body: React.ReactNode;
- footer: React.ReactNode;
- }) {
- // show - 显示
- // outing - 隐藏动画中
- // hidden - 隐藏
- const [rootVisible, setRootVisible] = useState<'show' | 'outing' | 'hidden'>('hidden');
-
- const cssInit = useRef(false);
- const rootRef = useRef<HTMLDivElement>(null);
- const footerRef = useRef<HTMLDivElement>(null);
- const headerRef = useRef<HTMLDivElement>(null);
-
- const [rootPddingBottom, setRootPddingBottom] = useState(0);
- const [rootPddingTop, setRootPddingTop] = useState(0);
- const baseClassNname = "absolute box-border w-full bg-white transition-transform duration-300 ease-out";
- let applyClassName = "bottom-0 left-0 h-17/20";
- if(contentClassName) {
- applyClassName = contentClassName;
- }
- useLayoutEffect(() => {
- const original = document.documentElement.style.overflow;
- if(isOpen) {
- document.documentElement.style.overflow = "hidden";
- } else {
- document.documentElement.style.overflow = "";
- }
- if(isOpen) {
- setRootVisible('show');
- } else {
- if(cssInit.current) {
- setRootVisible('outing');
- }
-
- }
- return () => { document.documentElement.style.overflow = original; };
- },[isOpen]);
- // 当弹窗完全显示后计算 footer 高度
- useLayoutEffect(() => {
- if (rootVisible === 'show' && !cssInit.current && footerRef.current && headerRef.current) {
- const footerHeight = footerRef.current.getBoundingClientRect().height;
- const headerHeight = headerRef.current.getBoundingClientRect().height;
- setRootPddingBottom(footerHeight);
- setRootPddingTop(headerHeight);
- cssInit.current = true;
- }
- }, [rootVisible]);
- const rootClick = (e: React.MouseEvent) => {
- e.stopPropagation();
- if(clickOutsideClose) {
- onClose(false);
- }
- }
- // 如果是关闭,动画结束之后才隐藏
- const handlerAnimationEnd = (e: React.AnimationEvent<HTMLDivElement>) => {
- if(e.animationName === "fade-out") {
- setRootVisible('hidden');
- }
-
- }
- // 初始化时是隐藏状态
- // fade-in的时候display block + fade-in
- // fade-out的时候需要先加fade-out 类名,动画效果结束之后才能display none
- return (
- <Portal>
- <div
- ref={rootRef}
- className={clsx("w-full h-full backdrop-blur-md backdrop-saturate-150 bg-black/30 fixed inset-0 z-50 transition-all ease-in-out",{
- "block fade-in": rootVisible === "show",
- "hidden": rootVisible === "hidden",
- "fade-out": rootVisible === "outing"
- })}
- onAnimationEnd={handlerAnimationEnd}
- onClick={rootClick}
- >
- <div
- className={clsx(baseClassNname,applyClassName,
- {
- "slide-bottom-in": rootVisible === "show",
- "slide-bottom-out": rootVisible === "outing",
- }
- )}
- style={{
- paddingTop: rootPddingTop,
- paddingBottom: rootPddingBottom
- }}
- >
- <div ref={headerRef} className="w-full box-border absolute left-0 top-0">
- {header}
- </div>
-
- <div className="w-full box-border h-full overflow-y-auto relative">
-
- {body}
-
- </div>
- <div ref={footerRef} className="w-full box-border absolute bottom-0 left-0 border-t border-gray-200">
- {footer}
- </div>
- </div>
-
- </div>
- </Portal>
- );
- }
|