diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2018-07-18 15:07:37 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2018-08-15 17:39:38 +0200 |
| commit | 71592249b2dd669b6f24f37bfb7b0f4e43b74998 (patch) | |
| tree | a6097dc7e5d94d06e99c65fdfe160e824395f50c /gui/packages/components/src | |
| parent | e84e87f4ce5a8c242f756566cdc6fb59a45f7bea (diff) | |
| download | mullvadvpn-71592249b2dd669b6f24f37bfb7b0f4e43b74998.tar.xz mullvadvpn-71592249b2dd669b6f24f37bfb7b0f4e43b74998.zip | |
Add workspaces
Diffstat (limited to 'gui/packages/components/src')
| -rw-r--r-- | gui/packages/components/src/Accordion.js | 141 | ||||
| -rw-r--r-- | gui/packages/components/src/index.js | 3 |
2 files changed, 144 insertions, 0 deletions
diff --git a/gui/packages/components/src/Accordion.js b/gui/packages/components/src/Accordion.js new file mode 100644 index 0000000000..6be18b00b1 --- /dev/null +++ b/gui/packages/components/src/Accordion.js @@ -0,0 +1,141 @@ +// @flow + +import * as React from 'react'; +import { Component, View, Styles, Animated, UserInterface } from 'reactxp'; + +type Props = { + height: number | 'auto', + animationDuration?: number, + children?: React.Node, +}; + +type State = { + animatedValue: ?Animated.Value, +}; + +const containerOverflowStyle = Styles.createViewStyle({ overflow: 'hidden' }); + +export default class Accordion extends Component<Props, State> { + static defaultProps = { + height: 'auto', + animationDuration: 350, + }; + + state: State = { + animatedValue: null, + animation: null, + }; + + _containerView: ?React.Node; + _contentHeight = 0; + _animation = (null: ?Animated.CompositeAnimation); + + constructor(props: Props) { + super(props); + + // set the initial height if it's known + if (typeof props.height === 'number') { + this.state = { + animatedValue: Animated.createValue(props.height), + }; + } + } + + componentWillUnmount() { + if (this._animation) { + this._animation.stop(); + } + } + + shouldComponentUpdate(nextProps: Props, nextState: State) { + return ( + nextState.animatedValue !== this.state.animatedValue || + nextProps.height !== this.props.height || + nextProps.children !== this.props.children + ); + } + + componentDidUpdate(prevProps: Props, _prevState: State) { + if (prevProps.height !== this.props.height) { + this._animateHeightChanges(); + } + } + + render() { + const { + style: style, + height: _height, + children, + animationDuration: _animationDuration, + ...otherProps + } = this.props; + const containerStyles = [style]; + + if (this.state.animatedValue !== null) { + const animatedStyle = Styles.createAnimatedViewStyle({ + height: this.state.animatedValue, + }); + + containerStyles.push(containerOverflowStyle, animatedStyle); + } + + return ( + <Animated.View + {...otherProps} + style={containerStyles} + ref={(node) => (this._containerView = node)}> + <View onLayout={this._contentLayoutDidChange}>{children}</View> + </Animated.View> + ); + } + + async _animateHeightChanges() { + const containerView = this._containerView; + if (!containerView) { + return; + } + + if (this._animation) { + this._animation.stop(); + this._animation = null; + } + + try { + const layout = await UserInterface.measureLayoutRelativeToWindow(containerView); + const fromValue = this.state.animatedValue || Animated.createValue(layout.height); + const toValue = this.props.height === 'auto' ? this._contentHeight : this.props.height; + + // calculate the animation duration based on travel distance + const multiplier = Math.abs(toValue - layout.height) / Math.max(1, this._contentHeight); + const duration = Math.ceil(this.props.animationDuration * multiplier); + + const animation = Animated.timing(fromValue, { + toValue: toValue, + easing: Animated.Easing.InOut(), + duration: duration, + useNativeDriver: true, + }); + + this._animation = animation; + this.setState({ animatedValue: fromValue }, () => { + animation.start(this._onAnimationEnd); + }); + } catch (error) { + // TODO: log error + } + } + + _onAnimationEnd = ({ finished }) => { + if (finished) { + this._animation = null; + + // reset height after transition to let element layout naturally + // if animation finished without interruption + if (this.props.height === 'auto') { + this.setState({ animatedValue: null }); + } + } + }; + + _contentLayoutDidChange = ({ height }) => (this._contentHeight = height); +} diff --git a/gui/packages/components/src/index.js b/gui/packages/components/src/index.js new file mode 100644 index 0000000000..10cc634888 --- /dev/null +++ b/gui/packages/components/src/index.js @@ -0,0 +1,3 @@ +// @flow + +export { default as Accordion } from './Accordion'; |
