diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2017-12-19 10:31:24 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2017-12-19 10:31:24 +0100 |
| commit | 383a48638198accef99e987cd3297283ed25398b (patch) | |
| tree | 4e1f1c3e0b6421d36d2c6ab159f9adc1b4e33d5d /app | |
| parent | f5d2f70b62f435f85f1f3bc1ded25e101d145442 (diff) | |
| parent | d2a472b0aee0822277c1b9ae3b6bba18beb51697 (diff) | |
| download | mullvadvpn-383a48638198accef99e987cd3297283ed25398b.tar.xz mullvadvpn-383a48638198accef99e987cd3297283ed25398b.zip | |
Merge branch 'add-accordion'
Diffstat (limited to 'app')
| -rw-r--r-- | app/components/Accordion.js | 146 |
1 files changed, 146 insertions, 0 deletions
diff --git a/app/components/Accordion.js b/app/components/Accordion.js new file mode 100644 index 0000000000..456a95fe05 --- /dev/null +++ b/app/components/Accordion.js @@ -0,0 +1,146 @@ +// @flow + +import React, { Component } from 'react'; + +export type AccordionProps = { + height?: number | string, + transitionStyle?: string, + children?: Array<React.Element<*>> | React.Element<*> // see https://github.com/facebook/flow/issues/1964 +}; + +export type AccordionState = { + computedHeight: ?number | ?string, +}; + +export default class Accordion extends Component { + props: AccordionProps; + static defaultProps: $Shape<AccordionProps> = { + height: 'auto', + transitionStyle: 'height 0.25s ease-in-out' + }; + + state: AccordionState = { + computedHeight: null, + }; + + _containerElement: ?HTMLElement; + _contentElement: ?HTMLElement; + + componentDidMount() { + const containerElement = this._containerElement; + if(!containerElement) { + throw new Error('containerElement cannot be null'); + } + + // update initial state + if(this.props.height !== Accordion.defaultProps.height) { + this._updateHeight(); + } + + containerElement.addEventListener('transitionend', this._onTransitionEnd); + } + + componentWillUnmount() { + const containerElement = this._containerElement; + if(!containerElement) { + throw new Error('containerElement cannot be null'); + } + containerElement.removeEventListener('transitionend', this._onTransitionEnd); + } + + componentDidUpdate(prevProps: AccordionProps, _prevState: AccordionState) { + if(prevProps.height !== this.props.height) { + (async () => { + const { transitionStyle } = this.props; + + // make sure to warm up CSS transition before updating height + // do not warm up transitions if they are not expected to run + if(transitionStyle && transitionStyle.toLowerCase() !== 'none') { + await this._warmupTransition(); + this._updateHeight(); + } else { + this._updateHeight(); + this._onTransitionEnd(); + } + + })(); + } + } + + render() { + const { height: _height, children, transitionStyle, ...otherProps } = this.props; + let style = { + transition: transitionStyle, + }; + + if(typeof(this.state.computedHeight) === 'number') { + style = { + ...style, + overflow: 'hidden', + height: this.state.computedHeight.toString() + 'px', + }; + } + + return ( + <div { ...otherProps } style={ style } ref={ this._onContainerRef }> + <div ref={ this._onContentRef }> + { children } + </div> + </div> + ); + } + + // Sets initial height and delays transition until next runloop + // to make sure CSS transitions properly kick in. + // This method resolves immediately if the height is already set. + _warmupTransition() { + const contentElement = this._contentElement; + if(!contentElement) { + throw new Error('contentElement cannot be null'); + } + return new Promise((resolve, _) => { + // CSS transition always needs the initial height + // to perform the animation + if(this.state.computedHeight === null) { + this.setState({ + computedHeight: contentElement.clientHeight + }, () => { + // important to skip a run loop + // for CSS transition to kick in + setTimeout(resolve, 0); + }); + } else { + resolve(); + } + }); + } + + _updateHeight() { + const contentElement = this._contentElement; + if(!contentElement) { + throw new Error('contentElement cannot be null'); + } + this.setState({ + computedHeight: this.props.height === 'auto' ? + contentElement.clientHeight : + this.props.height + }); + } + + _onTransitionEnd = () => { + // reset height after transition to let element layout naturally + if(this.props.height === 'auto') { + this.setState({ + computedHeight: null, + }); + } + } + + _onContainerRef = (element) => { + this._containerElement = element; + } + + _onContentRef = (element) => { + this._contentElement = element; + } +}
\ No newline at end of file |
