index.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. // @ts-ignore currently no type definition for @douyinfe/semi-animation-react
  2. import { Transition } from '@douyinfe/semi-animation-react';
  3. import PropTypes from 'prop-types';
  4. import cls from 'classnames';
  5. import React, { useRef, useState, useCallback, useMemo } from 'react';
  6. import { cssClasses } from '@douyinfe/semi-foundation/collapsible/constants';
  7. import { Motion } from '../_base/base';
  8. import getMotionObjFromProps from '@douyinfe/semi-foundation/utils/getMotionObjFromProps';
  9. const ease = 'cubicBezier(.25,.1,.25,1)';
  10. export interface CollapsibleProps {
  11. motion?: Motion;
  12. children?: React.ReactNode;
  13. isOpen?: boolean;
  14. duration?: number;
  15. keepDOM?: boolean;
  16. className?: string;
  17. style?: React.CSSProperties;
  18. collapseHeight?: number;
  19. reCalcKey?: number | string;
  20. id?:string,
  21. }
  22. const Collapsible = (props: CollapsibleProps) => {
  23. const {
  24. motion,
  25. children,
  26. isOpen,
  27. duration,
  28. keepDOM,
  29. collapseHeight,
  30. style,
  31. className,
  32. reCalcKey,
  33. id
  34. } = props;
  35. const ref = useRef(null);
  36. const [maxHeight, setMaxHeight] = useState(0);
  37. const [open, setOpen] = useState(props.isOpen);
  38. const [isFirst, setIsFirst] = useState(true);
  39. const [transitionImmediate, setTransitionImmediate] = useState(open && isFirst);
  40. const [left, setLeft] = useState(!props.isOpen);
  41. if (isOpen !== open) {
  42. setOpen(isOpen);
  43. if (isFirst) {
  44. setIsFirst(false);
  45. setTransitionImmediate(false);
  46. }
  47. isOpen && setLeft(!isOpen);
  48. }
  49. const setHeight = useCallback(node => {
  50. const currHeight = node && node.scrollHeight;
  51. if (currHeight && maxHeight !== currHeight) {
  52. setMaxHeight(currHeight);
  53. }
  54. // eslint-disable-next-line react-hooks/exhaustive-deps
  55. }, [left, reCalcKey, maxHeight]);
  56. const resetHeight = () => {
  57. ref.current.style.maxHeight = 'none';
  58. };
  59. const formatStyle = ({ maxHeight: maxHeightInTransitionStyle }: any) => ({ maxHeight: maxHeightInTransitionStyle });
  60. const shouldKeepDOM = () => keepDOM || collapseHeight !== 0;
  61. const defaultMaxHeight = useMemo(() => {
  62. return isOpen || !shouldKeepDOM() && !motion ? 'none' : collapseHeight;
  63. }, [collapseHeight, motion, isOpen, shouldKeepDOM]);
  64. const renderChildren = (transitionStyle: Record<string, any> | null) => {
  65. const transition =
  66. transitionStyle && typeof transitionStyle === 'object' ?
  67. formatStyle(transitionStyle) :
  68. {};
  69. const wrapperstyle = {
  70. overflow: 'hidden',
  71. maxHeight: defaultMaxHeight,
  72. ...style,
  73. ...transition,
  74. };
  75. if (isFirst) {
  76. wrapperstyle.maxHeight = defaultMaxHeight;
  77. }
  78. const wrapperCls = cls(`${cssClasses.PREFIX}-wrapper`, className);
  79. return (
  80. <div style={wrapperstyle} className={wrapperCls} ref={ref}>
  81. <div
  82. ref={setHeight}
  83. style={{ overflow: 'hidden' }}
  84. id={id}
  85. x-semi-prop="children"
  86. >
  87. {children}
  88. </div>
  89. </div>
  90. );
  91. };
  92. const didLeave = () => {
  93. setLeft(true);
  94. !shouldKeepDOM() && setMaxHeight(collapseHeight);
  95. };
  96. const renderContent = () => {
  97. if (left && !shouldKeepDOM()) {
  98. return null;
  99. }
  100. const mergedMotion = getMotionObjFromProps({
  101. didEnter: resetHeight,
  102. didLeave,
  103. motion,
  104. });
  105. return (
  106. <Transition
  107. state={isOpen ? 'enter' : 'leave'}
  108. immediate={transitionImmediate}
  109. from={{ maxHeight: 0 }}
  110. enter={{ maxHeight: { val: maxHeight, easing: ease, duration } }}
  111. leave={{ maxHeight: { val: collapseHeight, easing: ease, duration } }}
  112. {...mergedMotion}
  113. >
  114. {(transitionStyle: Record<string, any>) =>
  115. renderChildren(motion ? transitionStyle : null)
  116. }
  117. </Transition>
  118. );
  119. };
  120. return renderContent();
  121. };
  122. Collapsible.propType = {
  123. motion: PropTypes.oneOfType([PropTypes.bool, PropTypes.func, PropTypes.object]),
  124. children: PropTypes.node,
  125. isOpen: PropTypes.bool,
  126. duration: PropTypes.number,
  127. keepDOM: PropTypes.bool,
  128. collapseHeight: PropTypes.number,
  129. style: PropTypes.object,
  130. className: PropTypes.string,
  131. reCalcKey: PropTypes.oneOfType([
  132. PropTypes.string,
  133. PropTypes.number
  134. ]),
  135. };
  136. Collapsible.defaultProps = {
  137. isOpen: false,
  138. duration: 250,
  139. motion: true,
  140. keepDOM: false,
  141. collapseHeight: 0
  142. };
  143. export default Collapsible;