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 | |
| parent | e84e87f4ce5a8c242f756566cdc6fb59a45f7bea (diff) | |
| download | mullvadvpn-71592249b2dd669b6f24f37bfb7b0f4e43b74998.tar.xz mullvadvpn-71592249b2dd669b6f24f37bfb7b0f4e43b74998.zip | |
Add workspaces
Diffstat (limited to 'gui/packages/components')
| -rw-r--r-- | gui/packages/components/.babelrc | 12 | ||||
| -rw-r--r-- | gui/packages/components/.eslintrc | 5 | ||||
| -rw-r--r-- | gui/packages/components/package.json | 40 | ||||
| -rw-r--r-- | gui/packages/components/src/Accordion.js | 141 | ||||
| -rw-r--r-- | gui/packages/components/src/index.js | 3 | ||||
| -rw-r--r-- | gui/packages/components/test/.eslintrc | 12 | ||||
| -rw-r--r-- | gui/packages/components/test/setup.js | 33 | ||||
| -rw-r--r-- | gui/packages/components/test/test-stub.spec.js | 3 |
8 files changed, 249 insertions, 0 deletions
diff --git a/gui/packages/components/.babelrc b/gui/packages/components/.babelrc new file mode 100644 index 0000000000..3c788e0ea7 --- /dev/null +++ b/gui/packages/components/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + ["env", { + "targets": { "node": "8.9" }, + "useBuiltIns": true + }], "react" + ], + "plugins": [ + "transform-class-properties", + "transform-object-rest-spread" + ] +} diff --git a/gui/packages/components/.eslintrc b/gui/packages/components/.eslintrc new file mode 100644 index 0000000000..e5a34aec6a --- /dev/null +++ b/gui/packages/components/.eslintrc @@ -0,0 +1,5 @@ +{ + "env": { + "browser": true + } +} diff --git a/gui/packages/components/package.json b/gui/packages/components/package.json new file mode 100644 index 0000000000..65e66d71a2 --- /dev/null +++ b/gui/packages/components/package.json @@ -0,0 +1,40 @@ +{ + "name": "@mullvad/components", + "version": "0.1.0", + "main": "build/index.js", + "license": "GPL-3.0", + "private": true, + "scripts": { + "postinstall": "yarn run build", + "test": "mocha -R spec --require babel-core/register --require \"test/setup.js\" \"test/**/*.spec.js\"", + "build": "run-s private:build:clean private:build:compile", + "private:build:clean": "rimraf build", + "private:build:compile": "babel src/ --copy-files --out-dir build" + }, + "devDependencies": { + "babel-cli": "^6.26.0", + "babel-core": "^6.26.3", + "babel-plugin-transform-class-properties": "^6.24.1", + "babel-plugin-transform-object-rest-spread": "^6.26.0", + "babel-preset-env": "^1.7.0", + "babel-preset-react": "^6.24.1", + "chai": "^4.1.2", + "enzyme": "^3.3.0", + "enzyme-adapter-react-16": "^1.1.1", + "jsdom": "^11.12.0", + "mocha": "^5.2.0", + "npm-run-all": "^4.1.3", + "react": "^16.0.0", + "react-dom": "^16.0.0", + "reactxp": "^1.3.3", + "rimraf": "^2.6.2" + }, + "dependencies": { + "babel-runtime": "^6.26.0" + }, + "peerDependencies": { + "react": "^16.0.0", + "react-dom": "^16.0.0", + "reactxp": "^1.3.2" + } +} 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'; diff --git a/gui/packages/components/test/.eslintrc b/gui/packages/components/test/.eslintrc new file mode 100644 index 0000000000..fc2ab4d496 --- /dev/null +++ b/gui/packages/components/test/.eslintrc @@ -0,0 +1,12 @@ +{ + "rules": { + "no-unused-expressions" : "off", + "react/no-render-return-value": "off" + }, + "env": { + "mocha": true + }, + "globals": { + "expect": true + } +} diff --git a/gui/packages/components/test/setup.js b/gui/packages/components/test/setup.js new file mode 100644 index 0000000000..fefae9ebec --- /dev/null +++ b/gui/packages/components/test/setup.js @@ -0,0 +1,33 @@ +const { JSDOM } = require('jsdom'); +const Enzyme = require('enzyme'); +const Adapter = require('enzyme-adapter-react-16'); +const chai = require('chai'); + +Enzyme.configure({ + adapter: new Adapter(), +}); + +const jsdom = new JSDOM('<!doctype html><html><body></body></html>'); +const { window } = jsdom; + +function copyProps(src, target) { + const props = Object.getOwnPropertyNames(src) + .filter((prop) => typeof target[prop] === 'undefined') + .reduce( + (result, prop) => ({ + ...result, + [prop]: Object.getOwnPropertyDescriptor(src, prop), + }), + {}, + ); + Object.defineProperties(target, props); +} + +global.window = window; +global.document = window.document; +global.navigator = { + userAgent: 'node.js', +}; +copyProps(window, global); + +global.expect = chai.expect; diff --git a/gui/packages/components/test/test-stub.spec.js b/gui/packages/components/test/test-stub.spec.js new file mode 100644 index 0000000000..3b70b327e9 --- /dev/null +++ b/gui/packages/components/test/test-stub.spec.js @@ -0,0 +1,3 @@ +// @flow + +describe('No tests', () => {}); |
