|
@@ -0,0 +1,134 @@
|
|
|
|
|
+"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>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|