summaryrefslogtreecommitdiffhomepage
path: root/app
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@codeispoetry.ru>2017-05-23 13:31:07 +0100
committerAndrej Mihajlov <and@codeispoetry.ru>2017-06-05 13:18:52 +0300
commitc166b83b56f3a9f98be7ac9fe3674a652e58f7d8 (patch)
tree1e110a4f988343aaf58598a78244184b67e7f0e3 /app
parentfdb86c8ab71493cd10d176505653bb411a6921e2 (diff)
downloadmullvadvpn-c166b83b56f3a9f98be7ac9fe3674a652e58f7d8.tar.xz
mullvadvpn-c166b83b56f3a9f98be7ac9fe3674a652e58f7d8.zip
Add transitions
Diffstat (limited to 'app')
-rw-r--r--app/assets/css/style.css2
-rw-r--r--app/assets/css/transitions.css91
-rw-r--r--app/components/HeaderBar.css17
-rw-r--r--app/components/Settings.css2
-rw-r--r--app/components/WindowChrome.css6
-rw-r--r--app/components/WindowChrome.js26
-rw-r--r--app/lib/transition-rule.js74
-rw-r--r--app/routes.js33
-rw-r--r--app/transitions.js107
9 files changed, 334 insertions, 24 deletions
diff --git a/app/assets/css/style.css b/app/assets/css/style.css
index 4755c3c85f..95e601a060 100644
--- a/app/assets/css/style.css
+++ b/app/assets/css/style.css
@@ -3,8 +3,10 @@
@import 'fonts.css';
@import 'global.css';
@import 'buttons.css';
+@import 'transitions.css';
/* app */
+@import '../../components/WindowChrome.css';
@import '../../components/CustomScrollbars.css';
@import '../../components/Login.css';
@import '../../components/Connect.css';
diff --git a/app/assets/css/transitions.css b/app/assets/css/transitions.css
new file mode 100644
index 0000000000..239be5b63b
--- /dev/null
+++ b/app/assets/css/transitions.css
@@ -0,0 +1,91 @@
+/**
+ * CSS rules for transitions using React-router and CSSTransitionGroup
+ */
+
+div[class*="-transition-leave"], div[class*="-transition-enter"] {
+ /* keep animated .layout divs pinned to viewport boundaries */
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100vw;
+
+ /* disable UI interaction during transitions */
+ pointer-events: none;
+}
+
+.transition-container {
+ position: relative;
+ width: 100vw;
+ height: 100vh;
+ overflow: hidden;
+}
+
+/* New view slides bottom top */
+
+.slide-up-transition-leave { z-index: 0; }
+.slide-up-transition-enter {
+ transform: translateY(100vh);
+ z-index: 1;
+}
+
+.slide-up-transition-enter.slide-up-transition-enter-active {
+ transform: translateY(0);
+ transition: transform 450ms ease;
+}
+
+/* New view slides top bottom */
+
+.slide-down-transition-enter { z-index: 0; }
+.slide-down-transition-leave {
+ transform: translateY(0);
+ z-index: 1;
+}
+
+.slide-down-transition-leave.slide-down-transition-leave-active {
+ transform: translateY(100vh);
+ transition: transform 450ms ease;
+}
+
+/* New view slids right to left */
+
+.push-transition-leave {
+ transform: translateX(0vw);
+ z-index: 0;
+}
+
+.push-transition-enter {
+ transform: translateX(100vw);
+ z-index: 1;
+}
+
+.push-transition-leave.push-transition-leave-active {
+ transform: translateX(-50vw);
+ transition: transform 450ms ease;
+}
+
+.push-transition-enter.push-transition-enter-active {
+ transform: translateX(0);
+ transition: transform 450ms ease;
+}
+
+/* New view slides left to right */
+
+.pop-transition-enter {
+ transform: translateX(-50vw);
+ z-index: 0;
+}
+
+.pop-transition-leave {
+ transform: translateX(0);
+ z-index: 1;
+}
+
+.pop-transition-enter.pop-transition-enter-active {
+ transform: translateX(0vw);
+ transition: transform 450ms ease;
+}
+
+.pop-transition-leave.pop-transition-leave-active {
+ transform: translateX(100vw);
+ transition: transform 450ms ease;
+} \ No newline at end of file
diff --git a/app/components/HeaderBar.css b/app/components/HeaderBar.css
index d4505a79fd..9fcc65047b 100644
--- a/app/components/HeaderBar.css
+++ b/app/components/HeaderBar.css
@@ -4,12 +4,9 @@
transition: 0.5s background-color ease-in-out;
}
-/* macOS app runs as menubar app so create arrow and add extra padding */
+/* macOS app runs as menubar app so add extra padding */
.headerbar--darwin {
padding-top: 24px;
- -webkit-mask:
- url(../assets/images/app-triangle.svg) 50% 0% no-repeat,
- url(../assets/images/app-header-backdrop.svg) no-repeat;
}
.headerbar--hidden {
@@ -20,26 +17,14 @@
background-color: #192E45;
}
-.headerbar--style-defaultDark:before {
- background-image: url(../assets/images/app-triangle-default-dark.svg);
-}
-
.headerbar--style-error {
background-color: #D0021B;
}
-.headerbar--style-error:before {
- background-image: url(../assets/images/app-triangle-error.svg);
-}
-
.headerbar--style-success {
background-color: #44AD4D;
}
-.headerbar--style-success:before {
- background-image: url(../assets/images/app-triangle-success.svg);
-}
-
.headerbar__container {
display: flex;
flex-direction: row;
diff --git a/app/components/Settings.css b/app/components/Settings.css
index aff3cf55be..4bd2de5929 100644
--- a/app/components/Settings.css
+++ b/app/components/Settings.css
@@ -116,4 +116,4 @@
padding: 24px;
}
-.settings__footer .button + .button { margin-top: 16px; }
+.settings__footer .button + .button { margin-top: 16px; } \ No newline at end of file
diff --git a/app/components/WindowChrome.css b/app/components/WindowChrome.css
new file mode 100644
index 0000000000..8f3fcdb52c
--- /dev/null
+++ b/app/components/WindowChrome.css
@@ -0,0 +1,6 @@
+/* macOS app runs as menubar; create an app chrome with arrow using mask */
+.window-chrome--darwin {
+ -webkit-mask:
+ url(../assets/images/app-triangle.svg) 50% 0% no-repeat,
+ url(../assets/images/app-header-backdrop.svg) no-repeat;
+} \ No newline at end of file
diff --git a/app/components/WindowChrome.js b/app/components/WindowChrome.js
new file mode 100644
index 0000000000..e1df040d6c
--- /dev/null
+++ b/app/components/WindowChrome.js
@@ -0,0 +1,26 @@
+import React, { Component, PropTypes } from 'react';
+
+/**
+ * A component used to chip out arrow in the app header using CSS mask
+ *
+ * @export
+ * @class WindowChrome
+ * @extends {Component}
+ */
+export default class WindowChrome extends Component {
+ static propTypes = {
+ children: PropTypes.oneOfType([
+ PropTypes.arrayOf(PropTypes.element),
+ PropTypes.element,
+ ])
+ };
+
+ render() {
+ const chromeClass = ['window-chrome', 'window-chrome--' + process.platform];
+ return (
+ <div className={ chromeClass.join(' ') }>
+ { this.props.children }
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/app/lib/transition-rule.js b/app/lib/transition-rule.js
new file mode 100644
index 0000000000..f0c7020870
--- /dev/null
+++ b/app/lib/transition-rule.js
@@ -0,0 +1,74 @@
+// @flow
+
+export type TransitionDescriptor = {
+ name: string,
+ duration: number
+};
+
+export type TransitionFork = {
+ forward: TransitionDescriptor,
+ backward: TransitionDescriptor
+};
+
+/**
+ * Transition rule
+ *
+ * @class TransitionRule
+ */
+export default class TransitionRule {
+
+ from: ?string;
+ to: string;
+ fork: TransitionFork;
+ dir: 'forward' | 'backward' = 'forward';
+
+ /**
+ * Creates an instance of TransitionRule.
+ * @param {string} from - source route to match against, pass null for any.
+ * @param {string} to - destination route to match against
+ * @param {TransitionFork} fork - transition
+ *
+ * @memberof TransitionRule
+ */
+ constructor(from: ?string, to: string, fork: TransitionFork) {
+ this.from = from;
+ this.to = to;
+ this.fork = fork;
+ }
+
+ /**
+ * Attempts to match the transition between routes A -> B and B -> A
+ *
+ * @param {string} [fromRoute] source route, pass null for any
+ * @param {string} toRoute
+ * @returns {boolean} true if matches, otherwise false
+ *
+ * @memberof TransitionRule
+ */
+ match(fromRoute: ?string, toRoute: string): boolean {
+ if((!this.from || this.from === fromRoute) && this.to === toRoute) {
+ this.dir = 'forward';
+ return true;
+ }
+
+ if((!this.from || this.from === toRoute) && this.to === fromRoute) {
+ this.dir = 'backward';
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns transition descriptor.
+ * Make sure you run match() before to obtain the direction
+ * of transition before calling this method
+ *
+ * @returns {TransitionDescriptor} transitionDescriptor
+ *
+ * @memberof TransitionRule
+ */
+ transitionDescriptor(): TransitionDescriptor {
+ return this.fork[this.dir];
+ }
+} \ No newline at end of file
diff --git a/app/routes.js b/app/routes.js
index bcee8ca5e1..a85e5efbda 100644
--- a/app/routes.js
+++ b/app/routes.js
@@ -1,11 +1,14 @@
import React from 'react';
import { Switch, Route, Redirect } from 'react-router';
+import { CSSTransitionGroup } from 'react-transition-group';
+import Chrome from './components/Chrome';
import LoginPage from './containers/LoginPage';
import ConnectPage from './containers/ConnectPage';
import SettingsPage from './containers/SettingsPage';
import AccountPage from './containers/AccountPage';
import SelectLocationPage from './containers/SelectLocationPage';
import { LoginState } from './enums';
+import { getTransitionProps } from './transitions';
/**
* Create routes
@@ -84,13 +87,29 @@ export default function makeRoutes(getState, componentProps) {
);
};
+ // store previous route
+ let previousRoute;
+
return (
- <Switch>
- <LoginRoute exact path="/" component={ LoginPage } />
- <PrivateRoute exact path="/connect" component={ ConnectPage } />
- <PublicRoute exact path="/settings" component={ SettingsPage } />
- <PrivateRoute path="/settings/account" component={ AccountPage } />
- <PrivateRoute path="/select-location" component={ SelectLocationPage } />
- </Switch>
+ <Route render={({location}) => {
+ const toRoute = location.pathname;
+ const fromRoute = previousRoute;
+ const transitionProps = getTransitionProps(fromRoute, toRoute);
+ previousRoute = toRoute;
+
+ return (
+ <Chrome>
+ <CSSTransitionGroup component="div" className="transition-container" { ...transitionProps }>
+ <Switch key={ location.key } location={ location }>
+ <LoginRoute exact path="/" component={ LoginPage } />
+ <PrivateRoute exact path="/connect" component={ ConnectPage } />
+ <PublicRoute exact path="/settings" component={ SettingsPage } />
+ <PrivateRoute path="/settings/account" component={ AccountPage } />
+ <PrivateRoute path="/select-location" component={ SelectLocationPage } />
+ </Switch>
+ </CSSTransitionGroup>
+ </Chrome>
+ );
+ }} />
);
}
diff --git a/app/transitions.js b/app/transitions.js
new file mode 100644
index 0000000000..20d18a0ad8
--- /dev/null
+++ b/app/transitions.js
@@ -0,0 +1,107 @@
+// @flow
+
+import TransitionRule from './lib/transition-rule';
+import type { TransitionFork, TransitionDescriptor } from './lib/transition-rule';
+
+export type CSSTransitionGroupProps = {
+ transitionName: string,
+ transitionEnterTimeout: number,
+ transitionLeaveTimeout: number,
+ transitionEnter: boolean,
+ transitionLeave: boolean,
+ transitionAppear?: boolean,
+ transitionAppearTimeout?: number
+};
+
+type TransitionMap = {
+ [name: string]: TransitionFork
+};
+
+/**
+ * Calculate CSSTransitionGroup props.
+ *
+ * @param {string} [fromRoute] - source route
+ * @param {string} toRoute - target route
+ */
+export const getTransitionProps = (fromRoute: ?string, toRoute: string): CSSTransitionGroupProps => {
+ // ignore initial transition and transition between the same routes
+ if(!fromRoute || fromRoute === toRoute) {
+ return noTransitionProps();
+ }
+
+ const match = transitionRules.find(e => e.match(fromRoute, toRoute));
+ if(match) {
+ return toCSSTransitionGroupProps(match.transitionDescriptor());
+ }
+
+ return noTransitionProps();
+};
+
+/**
+ * Integrate TransitionDescriptor into CSSTransitionGroupProps
+ * @param {TransitionDescriptor} descriptor
+ */
+const toCSSTransitionGroupProps = (descriptor: TransitionDescriptor): CSSTransitionGroupProps => {
+ const {name, duration} = descriptor;
+ return {
+ transitionName: name,
+ transitionEnterTimeout: duration,
+ transitionLeaveTimeout: duration,
+ transitionEnter: true,
+ transitionLeave: true
+ };
+};
+
+/**
+ * Returns default props with animations disabled
+ */
+const noTransitionProps = (): CSSTransitionGroupProps => ({
+ transitionName: '',
+ transitionEnterTimeout: 0,
+ transitionLeaveTimeout: 0,
+ transitionEnter: false,
+ transitionLeave: false
+});
+
+/**
+ * Transition descriptors
+ */
+const transitions: TransitionMap = {
+ slide: {
+ forward: {
+ name: 'slide-up-transition',
+ duration: 450
+ },
+ backward: {
+ name: 'slide-down-transition',
+ duration: 450
+ }
+ },
+ push: {
+ forward: {
+ name: 'push-transition',
+ duration: 450
+ },
+ backward: {
+ name: 'pop-transition',
+ duration: 450
+ }
+ }
+};
+
+/**
+ * Shortcut to create TransitionRule
+ */
+const r = (from: ?string, to: string, fork: TransitionFork): TransitionRule => {
+ return new TransitionRule(from, to, fork);
+};
+
+/**
+ * Transition rules
+ * (null) is used to indicate any route.
+ */
+const transitionRules = [
+ r('/settings', '/settings/account', transitions.push),
+ r(null, '/settings', transitions.slide),
+ r(null, '/select-location', transitions.slide)
+]; \ No newline at end of file