index.tsx 7.9 KB

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