index.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import React from 'react';
  2. import type {
  3. CollapsibleAdapter,
  4. CollapsibleFoundationProps,
  5. CollapsibleFoundationState
  6. } from "@douyinfe/semi-foundation/collapsible/foundation";
  7. import CollapsibleFoundation from "@douyinfe/semi-foundation/collapsible/foundation";
  8. import BaseComponent from "../_base/baseComponent";
  9. import PropTypes from "prop-types";
  10. import cls from "classnames";
  11. import { cssClasses } from "@douyinfe/semi-foundation/collapsible/constants";
  12. import { isEqual, omit, pick } from "lodash";
  13. import "@douyinfe/semi-foundation/collapsible/collapsible.scss";
  14. import { getDefaultPropsFromGlobalConfig } from "../_utils";
  15. export interface CollapsibleProps extends CollapsibleFoundationProps {
  16. motion?: boolean;
  17. children?: React.ReactNode;
  18. isOpen?: boolean;
  19. duration?: number;
  20. keepDOM?: boolean;
  21. lazyRender?: boolean;
  22. className?: string;
  23. style?: React.CSSProperties;
  24. collapseHeight?: number;
  25. reCalcKey?: number | string;
  26. id?: string;
  27. onMotionEnd?: () => void
  28. }
  29. interface CollapsibleState extends CollapsibleFoundationState {
  30. domInRenderTree: boolean;
  31. domHeight: number;
  32. visible: boolean;
  33. isTransitioning: boolean;
  34. cacheIsOpen: boolean
  35. }
  36. class Collapsible extends BaseComponent<CollapsibleProps, CollapsibleState> {
  37. static __SemiComponentName__ = "Collapsible";
  38. static defaultProps = getDefaultPropsFromGlobalConfig(Collapsible.__SemiComponentName__, {
  39. isOpen: false,
  40. duration: 250,
  41. motion: true,
  42. keepDOM: false,
  43. lazyRender: false,
  44. collapseHeight: 0,
  45. fade: false
  46. })
  47. public foundation: CollapsibleFoundation;
  48. private domRef = React.createRef<HTMLDivElement>();
  49. private resizeObserver: ResizeObserver | null;
  50. private hasBeenRendered: boolean = false;
  51. constructor(props: CollapsibleProps) {
  52. super(props);
  53. this.state = {
  54. domInRenderTree: false,
  55. domHeight: 0,
  56. visible: this.props.isOpen,
  57. isTransitioning: false,
  58. cacheIsOpen: this.props.isOpen,
  59. };
  60. this.foundation = new CollapsibleFoundation(this.adapter);
  61. }
  62. get adapter(): CollapsibleAdapter<CollapsibleProps, CollapsibleState> {
  63. return {
  64. ...super.adapter,
  65. setDOMInRenderTree: (domInRenderTree) => {
  66. if (this.state.domInRenderTree !== domInRenderTree) {
  67. this.setState({ domInRenderTree });
  68. }
  69. },
  70. setDOMHeight: (domHeight) => {
  71. if (this.state.domHeight !== domHeight) {
  72. this.setState({ domHeight });
  73. }
  74. },
  75. setVisible: (visible) => {
  76. if (this.state.visible !== visible) {
  77. this.setState({ visible });
  78. }
  79. },
  80. setIsTransitioning: (isTransitioning) => {
  81. if (this.state.isTransitioning !== isTransitioning) {
  82. this.setState({ isTransitioning });
  83. }
  84. }
  85. };
  86. }
  87. static getEntryInfo = (entry: ResizeObserverEntry) => {
  88. //judge whether parent or self display none
  89. let inRenderTree: boolean;
  90. if (entry.borderBoxSize) {
  91. inRenderTree = !(entry.borderBoxSize[0].blockSize === 0 && entry.borderBoxSize[0].inlineSize === 0);
  92. } else {
  93. inRenderTree = !(entry.contentRect.height === 0 && entry.contentRect.width === 0);
  94. }
  95. let height = 0;
  96. if (entry.borderBoxSize) {
  97. height = Math.ceil(entry.borderBoxSize[0].blockSize);
  98. } else {
  99. const target = entry.target as HTMLElement;
  100. height = target.clientHeight;
  101. }
  102. return {
  103. isShown: inRenderTree, height
  104. };
  105. }
  106. componentDidMount() {
  107. super.componentDidMount();
  108. this.resizeObserver = new ResizeObserver(this.handleResize);
  109. this.resizeObserver.observe(this.domRef.current);
  110. const domInRenderTree = this.isChildrenInRenderTree();
  111. this.foundation.updateDOMInRenderTree(domInRenderTree);
  112. if (domInRenderTree) {
  113. this.foundation.updateDOMHeight(this.domRef.current.scrollHeight);
  114. }
  115. }
  116. static getDerivedStateFromProps(props: CollapsibleProps, prevState: CollapsibleState) {
  117. const newState: Partial<CollapsibleState> = {};
  118. const isOpenChanged = props.isOpen !== prevState.cacheIsOpen;
  119. if (isOpenChanged) {
  120. if (props.isOpen || !props.motion) {
  121. newState.visible = props.isOpen;
  122. }
  123. }
  124. if (props.motion && isOpenChanged) {
  125. newState.isTransitioning = true;
  126. }
  127. newState.cacheIsOpen = props.isOpen;
  128. return newState;
  129. }
  130. componentDidUpdate(prevProps: Readonly<CollapsibleProps>, prevState: Readonly<CollapsibleState>, snapshot?: any) {
  131. const changedPropKeys = Object.keys(pick(this.props, ['reCalcKey'])).filter(key => !isEqual(this.props[key], prevProps[key]));
  132. const changedStateKeys = Object.keys(pick(this.state, ['domInRenderTree'])).filter(key => !isEqual(this.state[key], prevState[key]));
  133. if (changedPropKeys.includes("reCalcKey")) {
  134. this.foundation.updateDOMHeight(this.domRef.current.scrollHeight);
  135. }
  136. if (changedStateKeys.includes("domInRenderTree") && this.state.domInRenderTree) {
  137. this.foundation.updateDOMHeight(this.domRef.current.scrollHeight);
  138. }
  139. }
  140. componentWillUnmount() {
  141. super.componentWillUnmount();
  142. this.resizeObserver.disconnect();
  143. }
  144. handleResize = (entryList: ResizeObserverEntry[]) => {
  145. const entry = entryList[0];
  146. if (entry) {
  147. const entryInfo = Collapsible.getEntryInfo(entry);
  148. this.foundation.updateDOMHeight(entryInfo.height);
  149. this.foundation.updateDOMInRenderTree(entryInfo.isShown);
  150. }
  151. }
  152. isChildrenInRenderTree = () => {
  153. if (this.domRef.current) {
  154. return this.domRef.current.offsetHeight > 0;
  155. }
  156. return false;
  157. }
  158. render() {
  159. const wrapperStyle: React.CSSProperties = {
  160. overflow: 'hidden',
  161. height: this.props.isOpen ? this.state.domHeight : this.props.collapseHeight,
  162. opacity: (this.props.isOpen || !this.props.fade || this.props.collapseHeight !== 0) ? 1 : 0,
  163. transitionDuration: `${this.props.motion && this.state.isTransitioning ? this.props.duration : 0}ms`,
  164. ...this.props.style
  165. };
  166. const wrapperCls = cls(`${cssClasses.PREFIX}-wrapper`, {
  167. [`${cssClasses.PREFIX}-transition`]: this.props.motion && this.state.isTransitioning
  168. }, this.props.className);
  169. const shouldRender = (this.props.keepDOM &&
  170. (this.props.lazyRender ? this.hasBeenRendered : true)) ||
  171. this.props.collapseHeight !== 0 || this.state.visible || this.props.isOpen;
  172. if (shouldRender && !this.hasBeenRendered) {
  173. this.hasBeenRendered = true;
  174. }
  175. return (
  176. <div
  177. className={wrapperCls}
  178. style={wrapperStyle}
  179. onTransitionEnd={() => {
  180. if (!this.props.isOpen) {
  181. this.foundation.updateVisible(false);
  182. }
  183. this.foundation.updateIsTransitioning(false);
  184. this.props.onMotionEnd?.();
  185. }}
  186. {...this.getDataAttr(this.props)}
  187. >
  188. <div
  189. x-semi-prop="children"
  190. ref={this.domRef}
  191. style={{ overflow: 'hidden' }}
  192. id={this.props.id}
  193. >
  194. {
  195. shouldRender && this.props.children
  196. }
  197. </div>
  198. </div>
  199. );
  200. }
  201. }
  202. Collapsible.propTypes = {
  203. motion: PropTypes.bool,
  204. children: PropTypes.node,
  205. isOpen: PropTypes.bool,
  206. duration: PropTypes.number,
  207. keepDOM: PropTypes.bool,
  208. collapseHeight: PropTypes.number,
  209. style: PropTypes.object,
  210. className: PropTypes.string,
  211. reCalcKey: PropTypes.oneOfType([
  212. PropTypes.string,
  213. PropTypes.number
  214. ]),
  215. };
  216. export default Collapsible;