diff options
| -rw-r--r-- | app/assets/css/style.css | 2 | ||||
| -rw-r--r-- | app/assets/css/transitions.css | 91 | ||||
| -rw-r--r-- | app/components/HeaderBar.css | 17 | ||||
| -rw-r--r-- | app/components/Settings.css | 2 | ||||
| -rw-r--r-- | app/components/WindowChrome.css | 6 | ||||
| -rw-r--r-- | app/components/WindowChrome.js | 26 | ||||
| -rw-r--r-- | app/lib/transition-rule.js | 74 | ||||
| -rw-r--r-- | app/routes.js | 33 | ||||
| -rw-r--r-- | app/transitions.js | 107 | ||||
| -rw-r--r-- | package.json | 1 |
10 files changed, 335 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 diff --git a/package.json b/package.json index f5429c29c2..5df0203bfe 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "react-redux": "^5.0.2", "react-router": "^4.1.1", "react-router-redux": "5.0.0-alpha.6", + "react-transition-group": "^1.1.3", "redux": "^3.0.0", "redux-actions": "^2.0.1", "redux-localstorage": "^0.4.1", |
