import classNames from "classnames" ; import * as React from "react" ; import { polyfill } from "react-lifecycles-compat" ; import { AbstractPureComponent2 , Classes } from "../../common" ; import { DISPLAYNAME_PREFIX , Props } from "../../common/props" ; export type CollapseProps = ICollapseProps ; deprecated export interface ICollapseProps extends Props { default component ? : React . ElementType ; default isOpen ? : boolean ; default keepChildrenMounted ? : boolean ; default transitionDuration ? : number ; } export interface ICollapseState { /** The state the element is currently in. */ animationState : AnimationStates ; /** The height that should be used for the content animations. This is a CSS value, not just a number. */ height : string | undefined ; /**
     * The most recent non-zero height (once a height has been measured upon first open; it is undefined until then)
     */ heightWhenOpen : number | undefined ; } /**
 * `Collapse` can be in one of six states, enumerated here.
 * When changing the `isOpen` prop, the following happens to the states:
 * isOpen={true}  : CLOSED -> OPEN_START -> OPENING -> OPEN
 * isOpen={false} : OPEN -> CLOSING_START -> CLOSING -> CLOSED
 */ export enum AnimationStates { /**
     * The body is re-rendered, height is set to the measured body height and
     * the body Y is set to 0.
     */ OPEN_START , /**
     * Animation begins, height is set to auto. This is all animated, and on
     * complete, the state changes to OPEN.
     */ OPENING , /**
     * The collapse height is set to auto, and the body Y is set to 0 (so the
     * element can be seen as normal).
     */ OPEN , /**
     * Height has been changed from auto to the measured height of the body to
     * prepare for the closing animation in CLOSING.
     */ CLOSING_START , /**
     * Height is set to 0 and the body Y is at -height. Both of these properties
     * are transformed, and then after the animation is complete, the state
     * changes to CLOSED.
     */ CLOSING , /**
     * The contents of the collapse is not rendered, the collapse height is 0,
     * and the body Y is at -height (so that the bottom of the body is at Y=0).
     */ CLOSED } @ polyfill export class Collapse extends AbstractPureComponent2 < CollapseProps , ICollapseState > { public static displayName = `${ DISPLAYNAME_PREFIX }.Collapse` ; public static defaultProps : CollapseProps = { component : "div" , isOpen : false , keepChildrenMounted : false , transitionDuration : 200 , } ; public static getDerivedStateFromProps ( props , state ) { const { isOpen } = props ; const { animationState } = state ; if ( isOpen ) { switch ( animationState ) { case AnimationStates . OPEN : break ; case AnimationStates . OPENING : break ; default : return { animationState : AnimationStates . OPEN_START } ; } } else { switch ( animationState ) { case AnimationStates . CLOSED : break ; case AnimationStates . CLOSING : break ; default : return { animationState : AnimationStates . CLOSING_START , height : `${ state . heightWhenOpen }px` , } ; } } return null ; } public state : ICollapseState = { animationState : this . props . isOpen ? AnimationStates . OPEN : AnimationStates . CLOSED , height : undefined , heightWhenOpen : undefined , } ; private contents : HTMLElement | null = null ; public render ( ) { const isContentVisible = this . state . animationState !== AnimationStates . CLOSED ; const shouldRenderChildren = isContentVisible || this . props . keepChildrenMounted ; const displayWithTransform = isContentVisible && this . state . animationState !== AnimationStates . CLOSING ; const isAutoHeight = this . state . height === "auto" ; const containerStyle = { height : isContentVisible ? this . state . height : undefined , overflowY : isAutoHeight ? "visible" : undefined , transition : isAutoHeight ? "none" : undefined , } ; const contentsStyle = { transform : displayWithTransform ? "translateY(0)" : `translateY(-${ this . state . heightWhenOpen }px)` , transition : isAutoHeight ? "none" : undefined , } ; return React . createElement ( this . props . component ! , { className : classNames ( Classes . COLLAPSE , this . props . className ) , style : containerStyle , } , < div className = { Classes . COLLAPSE_BODY } ref = { this . contentsRefHandler } style = { contentsStyle } aria - hidden = { ! isContentVisible && this . props . keepChildrenMounted } > { shouldRenderChildren ? this . props . children : null } < /div>); } public componentDidMount ( ) { this . forceUpdate ( ) ; if ( this . props . isOpen ) { this . setState ( { animationState : AnimationStates . OPEN , height : "auto" } ) ; } else { this . setState ( { animationState : AnimationStates . CLOSED , height : "0px" } ) ; } } public componentDidUpdate ( ) { if ( this . contents == null ) { return ; } const { transitionDuration } = this . props ; const { animationState } = this . state ; if ( animationState === AnimationStates . OPEN_START ) { const { clientHeight } = this . contents ; this . setState ( { animationState : AnimationStates . OPENING , height : `${ clientHeight }px` , heightWhenOpen : clientHeight , } ) ; this . setTimeout ( ( ) => this . onDelayedStateChange ( ) , transitionDuration ) ; } else if ( animationState === AnimationStates . CLOSING_START ) { const { clientHeight } = this . contents ; this . setTimeout ( ( ) => this . setState ( { animationState : AnimationStates . CLOSING , height : "0px" , heightWhenOpen : clientHeight , } ) ) ; this . setTimeout ( ( ) => this . onDelayedStateChange ( ) , transitionDuration ) ; } } private contentsRefHandler = ( el ) => { this . contents = el ; if ( this . contents != null ) { const height = this . contents . clientHeight ; this . setState ( { animationState : this . props . isOpen ? AnimationStates . OPEN : AnimationStates . CLOSED , height : height === 0 ? undefined : `${ height }px` , heightWhenOpen : height === 0 ? undefined : height , } ) ; } } ; private onDelayedStateChange ( ) { switch ( this . state . animationState ) { case AnimationStates . OPENING : this . setState ( { animationState : AnimationStates . OPEN , height : "auto" } ) ; break ; case AnimationStates . CLOSING : this . setState ( { animationState : AnimationStates . CLOSED } ) ; break ; default : break ; } } }