index.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  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>) => {
  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 ref={setHeight} style={{ overflow: 'hidden' }} id={id}>{children}</div>
  82. </div>
  83. );
  84. };
  85. const didLeave = () => {
  86. setLeft(true);
  87. !shouldKeepDOM() && setMaxHeight(collapseHeight);
  88. };
  89. const renderContent = () => {
  90. if (left && !shouldKeepDOM()) {
  91. return null;
  92. }
  93. const mergedMotion = getMotionObjFromProps({
  94. didEnter: resetHeight,
  95. didLeave,
  96. motion,
  97. });
  98. return (
  99. <Transition
  100. state={isOpen ? 'enter' : 'leave'}
  101. immediate={transitionImmediate}
  102. from={{ maxHeight: 0 }}
  103. enter={{ maxHeight: { val: maxHeight, easing: ease, duration } }}
  104. leave={{ maxHeight: { val: collapseHeight, easing: ease, duration } }}
  105. {...mergedMotion}
  106. >
  107. {(transitionStyle: Record<string, any>) =>
  108. renderChildren(motion ? transitionStyle : null)
  109. }
  110. </Transition>
  111. );
  112. };
  113. return renderContent();
  114. };
  115. Collapsible.propType = {
  116. motion: PropTypes.oneOfType([PropTypes.bool, PropTypes.func, PropTypes.object]),
  117. children: PropTypes.node,
  118. isOpen: PropTypes.bool,
  119. duration: PropTypes.number,
  120. keepDOM: PropTypes.bool,
  121. collapseHeight: PropTypes.number,
  122. style: PropTypes.object,
  123. className: PropTypes.string,
  124. reCalcKey: PropTypes.oneOfType([
  125. PropTypes.string,
  126. PropTypes.number
  127. ]),
  128. };
  129. Collapsible.defaultProps = {
  130. isOpen: false,
  131. duration: 250,
  132. motion: true,
  133. keepDOM: false,
  134. collapseHeight: 0
  135. };
  136. export default Collapsible;