summaryrefslogtreecommitdiffhomepage
path: root/gui/packages/components/src
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2018-07-18 15:07:37 +0200
committerAndrej Mihajlov <and@mullvad.net>2018-08-15 17:39:38 +0200
commit71592249b2dd669b6f24f37bfb7b0f4e43b74998 (patch)
treea6097dc7e5d94d06e99c65fdfe160e824395f50c /gui/packages/components/src
parente84e87f4ce5a8c242f756566cdc6fb59a45f7bea (diff)
downloadmullvadvpn-71592249b2dd669b6f24f37bfb7b0f4e43b74998.tar.xz
mullvadvpn-71592249b2dd669b6f24f37bfb7b0f4e43b74998.zip
Add workspaces
Diffstat (limited to 'gui/packages/components/src')
-rw-r--r--gui/packages/components/src/Accordion.js141
-rw-r--r--gui/packages/components/src/index.js3
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';