collapse.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. /* eslint-disable arrow-body-style */
  2. import React, { useRef, useState, useEffect, useCallback } from 'react';
  3. import { Transition } from '@douyinfe/semi-animation-react';
  4. import PropTypes from 'prop-types';
  5. import { noop } from 'lodash';
  6. import { cssClasses } from '@douyinfe/semi-foundation/collapsible/constants';
  7. import getMotionObjFromProps from '@douyinfe/semi-foundation/utils/getMotionObjFromProps';
  8. export interface CollapseProps {
  9. [x: string]: any;
  10. motion?: boolean;
  11. children?: React.ReactNode[];
  12. duration?: number;
  13. onMotionEnd?: () => void;
  14. motionType?: string;
  15. }
  16. export interface TransitionStyle {
  17. [x: string]: any;
  18. maxHeight?: number;
  19. }
  20. const ease = 'cubicBezier(.25,.1,.25,1)';
  21. const Collapse = (props: CollapseProps) => {
  22. const {
  23. motion,
  24. children,
  25. duration,
  26. onMotionEnd,
  27. motionType,
  28. } = props;
  29. const ref = useRef(null);
  30. const [maxHeight, setMaxHeight] = useState(0);
  31. // cache last state
  32. const [open, setOpen] = useState(true);
  33. const [left, setLeft] = useState(false);
  34. const [immediateAttr, setImmediateAttr] = useState(false);
  35. useEffect(() => {
  36. if (motionType === 'enter') {
  37. !open && setOpen(true);
  38. left && setLeft(false);
  39. } else if (motionType === 'leave') {
  40. !open && setOpen(true);
  41. !immediateAttr && setImmediateAttr(true);
  42. left && setLeft(false);
  43. }
  44. }, [motionType]);
  45. const setHeight = useCallback(node => {
  46. const currHeight = node && node.scrollHeight;
  47. if (currHeight && maxHeight !== currHeight) {
  48. setMaxHeight(currHeight);
  49. }
  50. }, [left]);
  51. const resetHeight = () => {
  52. ref.current.style.maxHeight = 'none';
  53. };
  54. const formatStyle = (style: TransitionStyle) => {
  55. // eslint-disable-next-line @typescript-eslint/no-shadow
  56. const { maxHeight } = style;
  57. return { maxHeight };
  58. };
  59. const renderChildren = (transitionStyle: TransitionStyle) => {
  60. const transition =
  61. transitionStyle && typeof transitionStyle === 'object' ? formatStyle(transitionStyle) : {};
  62. const style = {
  63. overflow: 'hidden',
  64. maxHeight: open ? 'none' : 0,
  65. ...transition,
  66. };
  67. return (
  68. <div style={style} className={`${cssClasses.PREFIX}-wrapper`} ref={ref}>
  69. <div ref={setHeight}>{children}</div>
  70. </div>
  71. );
  72. };
  73. const didLeave = () => {
  74. setLeft(true);
  75. setMaxHeight(0);
  76. motionType === 'leave' && onMotionEnd();
  77. };
  78. const onImmediateEnter = () => {
  79. open && setOpen(false);
  80. setImmediateAttr(false);
  81. };
  82. const didEnter = () => {
  83. resetHeight();
  84. immediateAttr && onImmediateEnter();
  85. motionType === 'enter' && onMotionEnd();
  86. };
  87. const renderContent = () => {
  88. if (left) {
  89. return null;
  90. }
  91. const mergeMotion = getMotionObjFromProps({
  92. didEnter,
  93. didLeave,
  94. motion,
  95. });
  96. return motion ? (
  97. <Transition
  98. state={open ? 'enter' : 'leave'}
  99. immediate={immediateAttr}
  100. from={{ maxHeight: 0 }}
  101. enter={{ maxHeight: { val: maxHeight, easing: ease, duration } }}
  102. leave={{ maxHeight: { val: 0, easing: ease, duration } }}
  103. {...mergeMotion}
  104. >
  105. {(transitionStyle: TransitionStyle) => renderChildren(transitionStyle)}
  106. </Transition>
  107. ) : (
  108. renderChildren(null)
  109. );
  110. };
  111. return renderContent();
  112. };
  113. Collapse.propType = {
  114. motion: PropTypes.oneOfType([PropTypes.bool, PropTypes.func, PropTypes.object]),
  115. children: PropTypes.node,
  116. duration: PropTypes.number,
  117. onMotionEnd: PropTypes.func,
  118. };
  119. Collapse.defaultProps = {
  120. duration: 250,
  121. motion: true,
  122. onMotionEnd: noop,
  123. };
  124. export default Collapse;