summaryrefslogtreecommitdiffhomepage
path: root/gui/packages/components
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
parente84e87f4ce5a8c242f756566cdc6fb59a45f7bea (diff)
downloadmullvadvpn-71592249b2dd669b6f24f37bfb7b0f4e43b74998.tar.xz
mullvadvpn-71592249b2dd669b6f24f37bfb7b0f4e43b74998.zip
Add workspaces
Diffstat (limited to 'gui/packages/components')
-rw-r--r--gui/packages/components/.babelrc12
-rw-r--r--gui/packages/components/.eslintrc5
-rw-r--r--gui/packages/components/package.json40
-rw-r--r--gui/packages/components/src/Accordion.js141
-rw-r--r--gui/packages/components/src/index.js3
-rw-r--r--gui/packages/components/test/.eslintrc12
-rw-r--r--gui/packages/components/test/setup.js33
-rw-r--r--gui/packages/components/test/test-stub.spec.js3
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', () => {});