index.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  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 } 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. }
  21. const Collapsible = (props: CollapsibleProps) => {
  22. const {
  23. motion,
  24. children,
  25. isOpen,
  26. duration,
  27. keepDOM,
  28. collapseHeight,
  29. style,
  30. className,
  31. reCalcKey
  32. } = props;
  33. const ref = useRef(null);
  34. const [maxHeight, setMaxHeight] = useState(0);
  35. const [open, setOpen] = useState(props.isOpen);
  36. const [isFirst, setIsFirst] = useState(true);
  37. const [transitionImmediate, setTransitionImmediate] = useState(open && isFirst);
  38. const [left, setLeft] = useState(!props.isOpen);
  39. if (isOpen !== open) {
  40. setOpen(isOpen);
  41. if (isFirst) {
  42. setIsFirst(false);
  43. setTransitionImmediate(false);
  44. }
  45. isOpen && setLeft(!isOpen);
  46. }
  47. const setHeight = useCallback(node => {
  48. const currHeight = node && node.scrollHeight;
  49. if (currHeight && maxHeight !== currHeight) {
  50. setMaxHeight(currHeight);
  51. }
  52. }, [left, reCalcKey, maxHeight]);
  53. const resetHeight = () => {
  54. ref.current.style.maxHeight = 'none';
  55. };
  56. const formatStyle = ({ maxHeight: maxHeightInTransitionStyle }: any) => ({ maxHeight: maxHeightInTransitionStyle });
  57. const shouldKeepDOM = () => keepDOM || collapseHeight !== 0;
  58. const renderChildren = (transitionStyle: Record<string, any>) => {
  59. const transition =
  60. transitionStyle && typeof transitionStyle === 'object' ?
  61. formatStyle(transitionStyle) :
  62. {};
  63. const wrapperstyle = {
  64. overflow: 'hidden',
  65. maxHeight: (isOpen || !shouldKeepDOM() && !motion) ? 'none' : collapseHeight,
  66. ...style,
  67. ...transition,
  68. };
  69. const wrapperCls = cls(`${cssClasses.PREFIX}-wrapper`, className);
  70. return (
  71. <div style={wrapperstyle} className={wrapperCls} ref={ref}>
  72. <div ref={setHeight}>{children}</div>
  73. </div>
  74. );
  75. };
  76. const didLeave = () => {
  77. setLeft(true);
  78. !shouldKeepDOM() && setMaxHeight(collapseHeight);
  79. };
  80. const renderContent = () => {
  81. if (left && !shouldKeepDOM()) {
  82. return null;
  83. }
  84. const mergedMotion = getMotionObjFromProps({
  85. didEnter: resetHeight,
  86. didLeave,
  87. motion,
  88. });
  89. return (
  90. <Transition
  91. state={isOpen ? 'enter' : 'leave'}
  92. immediate={transitionImmediate}
  93. from={{ maxHeight: 0 }}
  94. enter={{ maxHeight: { val: maxHeight, easing: ease, duration } }}
  95. leave={{ maxHeight: { val: collapseHeight, easing: ease, duration } }}
  96. {...mergedMotion}
  97. >
  98. {(transitionStyle: Record<string, any>) =>
  99. renderChildren(motion ? transitionStyle : null)
  100. }
  101. </Transition>
  102. );
  103. };
  104. return renderContent();
  105. };
  106. Collapsible.propType = {
  107. motion: PropTypes.oneOfType([PropTypes.bool, PropTypes.func, PropTypes.object]),
  108. children: PropTypes.node,
  109. isOpen: PropTypes.bool,
  110. duration: PropTypes.number,
  111. keepDOM: PropTypes.bool,
  112. collapseHeight: PropTypes.number,
  113. style: PropTypes.object,
  114. className: PropTypes.string,
  115. reCalcKey: PropTypes.oneOfType([
  116. PropTypes.string,
  117. PropTypes.number
  118. ]),
  119. };
  120. Collapsible.defaultProps = {
  121. isOpen: false,
  122. duration: 250,
  123. motion: true,
  124. keepDOM: false,
  125. collapseHeight: 0
  126. };
  127. export default Collapsible;