CommonModal.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. "use client";
  2. import clsx from "clsx";
  3. import { useState, useLayoutEffect, useRef } from "react";
  4. import Portal from "@/components/Portal";
  5. /**
  6. * 为了弹窗里的表单校验开发整个组件:
  7. * 当弹窗关闭,校验弹窗里的表单时要保证弹窗里的表单元素还存在文档流中,所以这个组件采用display block/none 来控制弹窗的显示和隐藏
  8. */
  9. export default function CommonModal({
  10. isOpen,
  11. contentClassName,
  12. clickOutsideClose,
  13. onClose,
  14. header,
  15. body,
  16. footer
  17. }: {
  18. isOpen: boolean;
  19. contentClassName?: string;
  20. clickOutsideClose?: boolean;
  21. onClose: (e: boolean) => void;
  22. header: React.ReactNode;
  23. body: React.ReactNode;
  24. footer: React.ReactNode;
  25. }) {
  26. // show - 显示
  27. // outing - 隐藏动画中
  28. // hidden - 隐藏
  29. const [rootVisible, setRootVisible] = useState<'show' | 'outing' | 'hidden'>('hidden');
  30. const cssInit = useRef(false);
  31. const rootRef = useRef<HTMLDivElement>(null);
  32. const footerRef = useRef<HTMLDivElement>(null);
  33. const headerRef = useRef<HTMLDivElement>(null);
  34. const [rootPddingBottom, setRootPddingBottom] = useState(0);
  35. const [rootPddingTop, setRootPddingTop] = useState(0);
  36. const baseClassNname = "absolute box-border w-full bg-white transition-transform duration-300 ease-out";
  37. let applyClassName = "bottom-0 left-0 h-17/20";
  38. if(contentClassName) {
  39. applyClassName = contentClassName;
  40. }
  41. useLayoutEffect(() => {
  42. const original = document.documentElement.style.overflow;
  43. if(isOpen) {
  44. document.documentElement.style.overflow = "hidden";
  45. } else {
  46. document.documentElement.style.overflow = "";
  47. }
  48. if(isOpen) {
  49. setRootVisible('show');
  50. } else {
  51. if(cssInit.current) {
  52. setRootVisible('outing');
  53. }
  54. }
  55. return () => { document.documentElement.style.overflow = original; };
  56. },[isOpen]);
  57. // 当弹窗完全显示后计算 footer 高度
  58. useLayoutEffect(() => {
  59. if (rootVisible === 'show' && !cssInit.current && footerRef.current && headerRef.current) {
  60. const footerHeight = footerRef.current.getBoundingClientRect().height;
  61. const headerHeight = headerRef.current.getBoundingClientRect().height;
  62. setRootPddingBottom(footerHeight);
  63. setRootPddingTop(headerHeight);
  64. cssInit.current = true;
  65. }
  66. }, [rootVisible]);
  67. const rootClick = (e: React.MouseEvent) => {
  68. e.stopPropagation();
  69. if(clickOutsideClose) {
  70. onClose(false);
  71. }
  72. }
  73. // 如果是关闭,动画结束之后才隐藏
  74. const handlerAnimationEnd = (e: React.AnimationEvent<HTMLDivElement>) => {
  75. if(e.animationName === "fade-out") {
  76. setRootVisible('hidden');
  77. }
  78. }
  79. // 初始化时是隐藏状态
  80. // fade-in的时候display block + fade-in
  81. // fade-out的时候需要先加fade-out 类名,动画效果结束之后才能display none
  82. return (
  83. <Portal>
  84. <div
  85. ref={rootRef}
  86. 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",{
  87. "block fade-in": rootVisible === "show",
  88. "hidden": rootVisible === "hidden",
  89. "fade-out": rootVisible === "outing"
  90. })}
  91. onAnimationEnd={handlerAnimationEnd}
  92. onClick={rootClick}
  93. >
  94. <div
  95. className={clsx(baseClassNname,applyClassName,
  96. {
  97. "slide-bottom-in": rootVisible === "show",
  98. "slide-bottom-out": rootVisible === "outing",
  99. }
  100. )}
  101. style={{
  102. paddingTop: rootPddingTop,
  103. paddingBottom: rootPddingBottom
  104. }}
  105. >
  106. <div ref={headerRef} className="w-full box-border absolute left-0 top-0">
  107. {header}
  108. </div>
  109. <div className="w-full box-border h-full overflow-y-auto relative">
  110. {body}
  111. </div>
  112. <div ref={footerRef} className="w-full box-border absolute bottom-0 left-0 border-t border-gray-200">
  113. {footer}
  114. </div>
  115. </div>
  116. </div>
  117. </Portal>
  118. );
  119. }