summaryrefslogtreecommitdiffhomepage
path: root/app
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2017-12-19 10:31:24 +0100
committerAndrej Mihajlov <and@mullvad.net>2017-12-19 10:31:24 +0100
commit383a48638198accef99e987cd3297283ed25398b (patch)
tree4e1f1c3e0b6421d36d2c6ab159f9adc1b4e33d5d /app
parentf5d2f70b62f435f85f1f3bc1ded25e101d145442 (diff)
parentd2a472b0aee0822277c1b9ae3b6bba18beb51697 (diff)
downloadmullvadvpn-383a48638198accef99e987cd3297283ed25398b.tar.xz
mullvadvpn-383a48638198accef99e987cd3297283ed25398b.zip
Merge branch 'add-accordion'
Diffstat (limited to 'app')
-rw-r--r--app/components/Accordion.js146
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