├── .gitignore ├── .npmignore ├── .prettierrc ├── HISTORY.md ├── LICENSE ├── README.md ├── babel.config.js ├── example ├── babel.config.js ├── index.html ├── package.json ├── src │ ├── NavBar.js │ ├── Screens │ │ ├── Docs │ │ │ ├── AnimatedRoute │ │ │ │ ├── Code.js │ │ │ │ ├── Demo.js │ │ │ │ ├── Overview.js │ │ │ │ ├── example.js │ │ │ │ └── index.js │ │ │ ├── AnimatedSwitch │ │ │ │ ├── Code.js │ │ │ │ ├── Demo.js │ │ │ │ ├── Overview.js │ │ │ │ ├── Props.js │ │ │ │ ├── example.css │ │ │ │ ├── example.js │ │ │ │ └── index.js │ │ │ ├── GettingStarted.js │ │ │ ├── index.js │ │ │ └── shared │ │ │ │ ├── Browser │ │ │ │ ├── Input.js │ │ │ │ ├── TopBar.js │ │ │ │ ├── Viewport.js │ │ │ │ ├── Wrapper.js │ │ │ │ └── index.js │ │ │ │ ├── ComponentPage.js │ │ │ │ ├── Footer.js │ │ │ │ ├── Page.js │ │ │ │ └── PageNav.js │ │ ├── Home.js │ │ └── Screen.js │ ├── globals.css │ ├── grayscale-prism-theme.css │ └── index.js └── webpack.config.js ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swp 3 | 4 | example/bundle.* 5 | 6 | lib 7 | es 8 | 9 | node_modules 10 | 11 | package-lock.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.* 2 | 3 | /example/ 4 | 5 | /webpack.* 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "jsxBracketSameLine": false, 5 | "printWidth": 80, 6 | "requirePragma": false, 7 | "semi": true, 8 | "singleQuote": true, 9 | "tabWidth": 2, 10 | "tabs": false, 11 | "trailingComma": "all" 12 | } 13 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ### 0.0.6 (08/31/2016) 2 | * Added `react` to webpack `externals` 3 | 4 | ### 0.0.5 (04/08/2016) 5 | * Added `runOnMount` to `RouteTransition` 6 | * Added react to devDependencies 7 | 8 | ### 0.0.4 (02/11/2016) 9 | * Initial release 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Richard Maisano 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Router Transition 2 | 3 | Painless transitions for React Router, powered by React Motion. [Example site](http://maisano.github.io/react-router-transition/). 4 | 5 | ### Requirements 6 | 7 | To use the latest version of this package (`2.x`), you'll need to use a version 8 | of React compatible with hooks, as well as version `5.x` of `react-router-dom`. 9 | 10 | ### Installation 11 | 12 | `npm install --save react-router-transition react-router-dom` 13 | 14 | ### Example Usage 15 | ```jsx 16 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 17 | import { AnimatedSwitch } from 'react-router-transition'; 18 | 19 | export default () => ( 20 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | ) 33 | ``` 34 | 35 | ```css 36 | .switch-wrapper { 37 | position: relative; 38 | } 39 | 40 | .switch-wrapper > div { 41 | position: absolute; 42 | } 43 | ``` 44 | 45 | ### Docs 46 | 47 | - [AnimatedSwitch](http://maisano.github.io/react-router-transition/animated-switch) 48 | - [AnimatedRoute](http://maisano.github.io/react-router-transition/animated-route) 49 | 50 | ### Limitations 51 | 52 | This library has some obvious limitations, the most marked of which are: 53 | 54 | - no staggering or sequencing of animations 55 | - no durations or timing functions 56 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-react', 5 | { 6 | useBuiltIns: true, 7 | }, 8 | ], 9 | [ 10 | '@babel/preset-env', 11 | { 12 | modules: false, 13 | }, 14 | ], 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const glamor = require('glamor/babel'); 2 | 3 | module.exports = { 4 | presets: [ 5 | [ 6 | '@babel/preset-react', 7 | { 8 | useBuiltIns: true, 9 | }, 10 | ], 11 | [ 12 | '@babel/preset-env', 13 | { 14 | modules: false, 15 | }, 16 | ], 17 | ], 18 | plugins: [ 19 | '@babel/plugin-proposal-class-properties', 20 | [ 21 | '@babel/plugin-transform-react-jsx', 22 | { 23 | pragma: 'Glamor.createElement', 24 | }, 25 | ], 26 | // Glamor doesn't ship a Babel 7 compliant plugin. 27 | () => glamor, 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Router Transition 6 | 7 | 8 | 9 | 10 |
11 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router-transition-example", 3 | "version": "1.0.0", 4 | "description": "An example showcasing some react-router-transition usage", 5 | "scripts": { 6 | "build": "NODE_ENV=production webpack -p", 7 | "start": "webpack-dev-server --history-api-fallback" 8 | }, 9 | "author": "maisano", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@babel/core": "^7.6.2", 13 | "@babel/plugin-proposal-class-properties": "^7.5.5", 14 | "@babel/preset-env": "^7.6.2", 15 | "@babel/preset-react": "^7.0.0", 16 | "babel-loader": "^8.0.6", 17 | "classnames": "^2.2.6", 18 | "css-loader": "^3.2.0", 19 | "glamor": "^2.20.40", 20 | "prismjs": "^1.17.1", 21 | "prismjs-loader": "0.0.4", 22 | "style-loader": "^1.0.0", 23 | "webpack": "^4.41.0", 24 | "webpack-cli": "^3.3.9", 25 | "webpack-dev-server": "^3.8.2" 26 | }, 27 | "dependencies": {} 28 | } 29 | -------------------------------------------------------------------------------- /example/src/NavBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, NavLink } from 'react-router-dom'; 3 | import { css } from 'glamor'; 4 | 5 | const rule = css` 6 | display: flex; 7 | padding-top: 2rem; 8 | margin-top: -2rem; 9 | height: 5rem; 10 | line-height: 5rem; 11 | background-color: #202020; 12 | 13 | & a { 14 | color: #eee; 15 | height: 5rem; 16 | line-height: 5rem; 17 | padding: 0 2rem; 18 | flex-grow: 1; 19 | text-align: center; 20 | } 21 | 22 | & a:hover, & a.active { 23 | background-color: #2a2a2a; 24 | color: #fff; 25 | } 26 | 27 | @media(max-width: 600px) { 28 | justify-content: space-around; 29 | overflow: hidden; 30 | 31 | & a { 32 | padding: 0; 33 | font-size: 1.2rem; 34 | } 35 | 36 | & a.github { 37 | display: none; 38 | } 39 | } 40 | `; 41 | 42 | const NavBar = () => ( 43 | 55 | ); 56 | 57 | export default NavBar; 58 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/AnimatedRoute/Code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Footer from '../shared/Footer'; 4 | 5 | import AnimatedRouteJS from '!!prismjs-loader?lang=jsx!./example'; 6 | 7 | const ExampleCode = () => ( 8 |
9 |

Example Code

10 |
11 |

AnimatedRouteDemo.js

12 |
16 |     
17 |
19 | ); 20 | 21 | export default ExampleCode; 22 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/AnimatedRoute/Demo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { css } from 'glamor'; 4 | 5 | import { AnimatedRoute } from 'react-router-transition'; 6 | 7 | import Browser from '../shared/Browser'; 8 | import Footer from '../shared/Footer'; 9 | 10 | const bodyRule = css` 11 | display: flex; 12 | height: 100%; 13 | width: 100%; 14 | justify-content: center; 15 | align-items: center; 16 | `; 17 | 18 | const wrapperRule = css` 19 | position: absolute; 20 | top: 0; 21 | bottom: 0; 22 | `; 23 | 24 | const routeRule = css` 25 | position: relative; 26 | height: 100%; 27 | width: 200px; 28 | 29 | & > div { 30 | position: absolute; 31 | width: 100%; 32 | height: 100%; 33 | } 34 | `; 35 | 36 | const sidebarRule = css` 37 | height: 100%; 38 | padding: 2rem; 39 | box-sizing: border-box; 40 | background-color: #fafafa; 41 | `; 42 | 43 | const sidebarTransition = { 44 | atEnter: { 45 | offset: -100, 46 | }, 47 | atLeave: { 48 | offset: -100, 49 | }, 50 | atActive: { 51 | offset: 0, 52 | }, 53 | }; 54 | 55 | function mapStyles(styles) { 56 | return { 57 | transform: `translateX(${styles.offset}%)`, 58 | }; 59 | } 60 | 61 | const Sidebar = () => ( 62 |
63 |
64 |

Hi, hello, I'm a sidebar.

65 |

I'm not entirely certain that this component is useful, but it's kinda fun.

66 |

Close me.

67 |
68 |
69 | ); 70 | 71 | const AnimatedRouteDemo = () => ( 72 |
73 |

Demo

74 | 75 |
76 |

Open sidebar

77 |
78 |
79 | 86 |
87 |
88 |
90 | ); 91 | 92 | export default AnimatedRouteDemo; 93 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/AnimatedRoute/Overview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from 'glamor'; 3 | 4 | import Footer from '../shared/Footer'; 5 | 6 | const Element = ({ name }) => ( 7 | <{name} /> 8 | ); 9 | 10 | const Overview = () => ( 11 |
12 |

Overview

13 |

A , but with mounting & unmounting transitions.

14 |

Interruptible

15 |

Transitions on an are interruptible. This means that if an animation is currently in motion and its match is toggled, it will redirect itself to its proper state mid-transition.

16 |

Nestable

17 |

You can put instances inside one another! You can even make them recursively nest and match–it's very likely not useful, but it sure is interesting.

18 |
20 | ); 21 | 22 | export default Overview; 23 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/AnimatedRoute/example.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { AnimatedRoute } from 'react-router-transition'; 4 | 5 | import Wrapper from './Wrapper'; 6 | import Sidebar from './Sidebar'; 7 | 8 | export default () => ( 9 | 10 | Show sidebar 11 | ({ 18 | transform: `translateX(${styles.offset}%)`, 19 | })} 20 | /> 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/AnimatedRoute/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ComponentPage from '../shared/ComponentPage'; 4 | 5 | import Overview from './Overview'; 6 | import Props from '../AnimatedSwitch/Props'; // lolol 7 | import Demo from './Demo'; 8 | import Code from './Code'; 9 | 10 | const sections = [{ 11 | title: 'Overview', 12 | path: '/animated-route', 13 | exact: true, 14 | component: Overview, 15 | }, { 16 | title: 'Props', 17 | path: '/animated-route/props', 18 | component: Props, 19 | }, { 20 | title: 'Demo', 21 | path: '/animated-route/demo', 22 | component: Demo, 23 | }, { 24 | title: 'Code', 25 | path: '/animated-route/code', 26 | component: Code, 27 | }]; 28 | 29 | export default () => ( 30 | 34 | ); 35 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/AnimatedSwitch/Code.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Footer from '../shared/Footer'; 4 | 5 | import AnimatedSwitchJS from '!!prismjs-loader?lang=jsx!./example'; 6 | import AnimatedSwitchCSS from '!!prismjs-loader?lang=css!./example.css'; 7 | 8 | const ExampleCode = () => ( 9 |
10 |

Example Code

11 |
12 |

AnimatedSwitchDemo.js

13 |
17 |     
18 |
19 |

AnimatedSwitchDemo.css

20 |
24 |     
25 |
26 |
27 | ); 28 | 29 | export default ExampleCode; 30 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/AnimatedSwitch/Demo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink, Route } from 'react-router-dom'; 3 | import { css } from 'glamor'; 4 | 5 | import { AnimatedSwitch, spring } from 'react-router-transition'; 6 | 7 | import Browser from '../shared/Browser'; 8 | import Footer from '../shared/Footer'; 9 | 10 | const rule = css` 11 | position: relative; 12 | height: calc("100% - 40px"); 13 | 14 | & > div { 15 | position: absolute; 16 | width: 100%; 17 | height: 100%; 18 | } 19 | `; 20 | 21 | const navRule = css` 22 | display: flex; 23 | height: 4rem; 24 | line-height: 4rem; 25 | justify-content: center; 26 | background-color: #fafafa; 27 | 28 | & a { 29 | flex-grow: 1; 30 | text-align: center; 31 | color: #303030; 32 | } 33 | 34 | & a:first-child { 35 | border-right: 1px solid #fff; 36 | } 37 | `; 38 | 39 | const activeRule = css` 40 | background-color: #f5f5f5; 41 | `; 42 | 43 | const demoPageRule = css` 44 | height: 100%; 45 | width: 100%; 46 | display: flex; 47 | justify-content: center; 48 | align-items: center; 49 | font-size: 400px; 50 | `; 51 | 52 | function bounce(val) { 53 | return spring(val, { 54 | stiffness: 330, 55 | damping: 22, 56 | }); 57 | } 58 | 59 | const demoConfig = { 60 | atEnter: { 61 | opacity: 0, 62 | scale: 1.2, 63 | }, 64 | atLeave: { 65 | opacity: bounce(0), 66 | scale: bounce(0.8), 67 | }, 68 | atActive: { 69 | opacity: bounce(1), 70 | scale: bounce(1), 71 | }, 72 | }; 73 | 74 | function mapStyles(styles) { 75 | return { 76 | opacity: styles.opacity, 77 | transform: `scale(${styles.scale})`, 78 | }; 79 | } 80 | 81 | const DemoPage = ({ children }) => ( 82 |
83 | {children} 84 |
85 | ); 86 | 87 | const A = () => ( 88 | A 89 | ); 90 | 91 | const B = () => ( 92 | B 93 | ); 94 | 95 | const C = () => ( 96 | C 97 | ); 98 | 99 | const Demo = () => ( 100 |
101 |

Demo

102 | 103 | 108 | 113 | 114 | 115 | 116 | 117 | 118 |
119 |
120 | ); 121 | 122 | export default Demo; 123 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/AnimatedSwitch/Overview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from 'glamor'; 3 | 4 | import Footer from '../shared/Footer'; 5 | 6 | const Overview = () => ( 7 |
8 |

Overview

9 |

A <Switch />, but with transitions when the child route changes. Allows for animating sibling routes with a mounting and unmounting transition.

10 |

Uninterruptible

11 |

Transitions within an Animated Switch are not interruptible. This is by design, as we will mount each newly matched child route as a separate component with a unique key.

12 |

Nestable

13 |

Just like <Switch />, this component is meant to be used anywhere you are rendering exclusive matches. This means nesting transitions is simple, providing your layout affords usage.

14 |
15 |
16 | ); 17 | 18 | export default Overview; 19 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/AnimatedSwitch/Props.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from 'glamor'; 3 | 4 | import Footer from '../shared/Footer'; 5 | 6 | const propTypesRule = css` 7 | font-size: 1.6rem; 8 | border-bottom: 1px solid #eee; 9 | padding-bottom: 1rem; 10 | margin: 3rem 0 1rem; 11 | `; 12 | 13 | const Props = () => ( 14 |
15 |

Props

16 |
17 | atEnter, atLeave, atActive 18 |
19 |

Objects with numerical values, expressing the interpolatable states a component will have when mounting, unmounting and mounted, respectively. Note that spring objects are valid only for atActive and atLeave.

20 |
didLeave
21 |

An optional function that will be called when animation is done.

22 |
mapStyles
23 |

An optional function for transforming values that don't map 1:1 with styles (e.g. translateX or other values of the transform style property).

24 |
runOnMount
25 |

A boolean flag to signal whether or not to apply a transition to the child component while mounting the parent.

26 |
wrapperComponent
27 |

The element type ('div', 'span', etc.) to wrap the transitioning routes with. Use false to transition child components themselves, though this requires consuming a style prop that gets injected into your component.

28 |
className
29 |

A class name to apply to the root node of the transition.

30 |
31 |
32 | ); 33 | 34 | export default Props; 35 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/AnimatedSwitch/example.css: -------------------------------------------------------------------------------- 1 | .route-wrapper { 2 | position: relative; 3 | } 4 | 5 | .route-wrapper > div { 6 | position: absolute; 7 | } 8 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/AnimatedSwitch/example.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | import { spring, AnimatedSwitch } from 'react-router-transition'; 4 | 5 | import A from './A'; 6 | import B from './B'; 7 | import C from './C'; 8 | 9 | // we need to map the `scale` prop we define below 10 | // to the transform style property 11 | function mapStyles(styles) { 12 | return { 13 | opacity: styles.opacity, 14 | transform: `scale(${styles.scale})`, 15 | }; 16 | } 17 | 18 | // wrap the `spring` helper to use a bouncy config 19 | function bounce(val) { 20 | return spring(val, { 21 | stiffness: 330, 22 | damping: 22, 23 | }); 24 | } 25 | 26 | // child matches will... 27 | const bounceTransition = { 28 | // start in a transparent, upscaled state 29 | atEnter: { 30 | opacity: 0, 31 | scale: 1.2, 32 | }, 33 | // leave in a transparent, downscaled state 34 | atLeave: { 35 | opacity: bounce(0), 36 | scale: bounce(0.8), 37 | }, 38 | // and rest at an opaque, normally-scaled state 39 | atActive: { 40 | opacity: bounce(1), 41 | scale: bounce(1), 42 | }, 43 | }; 44 | 45 | export default () => ( 46 | 53 | 54 | 55 | 56 | 57 | ); 58 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/AnimatedSwitch/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ComponentPage from '../shared/ComponentPage'; 4 | 5 | import Overview from './Overview'; 6 | import Props from './Props'; 7 | import Demo from './Demo'; 8 | import Code from './Code'; 9 | 10 | const sections = [{ 11 | title: 'Overview', 12 | path: '/animated-switch', 13 | exact: true, 14 | component: Overview, 15 | }, { 16 | title: 'Props', 17 | path: '/animated-switch/props', 18 | component: Props, 19 | }, { 20 | title: 'Demo', 21 | path: '/animated-switch/demo', 22 | component: Demo, 23 | }, { 24 | title: 'Code', 25 | path: '/animated-switch/code', 26 | component: Code, 27 | }]; 28 | 29 | export default () => ( 30 | 34 | ); 35 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/GettingStarted.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import Page from './shared/Page'; 5 | import Footer from './shared/Footer'; 6 | 7 | const Element = ({ name }) => ( 8 | <{name} /> 9 | ); 10 | 11 | export default () => ( 12 | 13 |
14 |

Getting started

15 |

Installation

16 |

npm install --save react-router-transition

17 |

What's included

18 |
    19 |
  • 20 | 21 | 22 | 23 |
  • 24 |
  • 25 | 26 | 27 | 28 |
  • 29 |
  • 30 | 31 |
  • 32 |
  • 33 | spring 34 |
  • 35 |
36 |

Design

37 |

React Router Transition tries to be as simple as possible. While some configuration is required, the library exposes two components that act as thin layers on top of components from React Router DOM. Where these are not flexible enough, the package also exports a lower-level wrapper around React Motion's .

38 |

The package also exports React Motion's spring helper function for specifying the spring configuration for the animation. To learn more about this helper, check the React Motion README.

39 |

Limitations

40 |

This library has some obvious limitations, the most marked of which are:

41 |
    42 |
  • no staggering or sequencing of animations
  • 43 |
  • no durations or timing functions
  • 44 |
45 |

If you're looking for one of the above features, I would recommend using Animated and Transition Group.

46 |
47 |
48 |
49 | ); 50 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | import { css } from 'glamor'; 4 | 5 | import { AnimatedSwitch, spring } from 'react-router-transition'; 6 | 7 | import Screen from '../Screen'; 8 | 9 | import GettingStartedPage from './GettingStarted'; 10 | import AnimatedSwitchPage from './AnimatedSwitch'; 11 | import AnimatedRoutePage from './AnimatedRoute'; 12 | 13 | const rule = css` 14 | padding-top: 5rem; 15 | box-sizing: border-box; 16 | background-color: #fff; 17 | color: #333; 18 | `; 19 | 20 | const switchRule = css` 21 | position: relative; 22 | height: calc("100vh - 50px"); 23 | width: 100vw; 24 | background-color: #fff; 25 | 26 | & > div { 27 | position: absolute; 28 | width: 100%; 29 | height: 100%; 30 | overflow-y: auto; 31 | -webkit-overflow-scrolling: touch; 32 | } 33 | `; 34 | 35 | function mapStyles(styles) { 36 | return { 37 | opacity: styles.opacity, 38 | transform: `translateX(${styles.offset}px)`, 39 | }; 40 | } 41 | 42 | function glide(val) { 43 | return spring(val, { 44 | stiffness: 174, 45 | damping: 19, 46 | }); 47 | } 48 | 49 | const pageTransitions = { 50 | atEnter: { 51 | offset: 200, 52 | opacity: 0, 53 | }, 54 | atLeave: { 55 | offset: glide(-100), 56 | opacity: glide(0), 57 | }, 58 | atActive: { 59 | offset: glide(0), 60 | opacity: glide(1), 61 | }, 62 | }; 63 | 64 | export default () => ( 65 | 66 |
67 | 72 | 73 | 74 | 75 | 76 |
77 |
78 | ); 79 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/shared/Browser/Input.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { css } from 'glamor'; 4 | 5 | const inputRule = css` 6 | flex-grow: 1; 7 | margin-left: 8px; 8 | padding: 5px 7px; 9 | border-radius: 3px; 10 | border: 1px solid transparent; 11 | font-size: 12px; 12 | line-height: 1.5em; 13 | 14 | &:focus { 15 | outline: none; 16 | border-color: rgb(89, 163, 240); 17 | } 18 | `; 19 | 20 | class LocationInput extends Component { 21 | static propTypes = { 22 | pathname: PropTypes.string.isRequired, 23 | push: PropTypes.func.isRequired, 24 | }; 25 | 26 | state = { 27 | value: this.props.pathname, 28 | }; 29 | 30 | componentWillReceiveProps(nextProps) { 31 | if (this.props.pathname !== nextProps.pathname) { 32 | this.setState({ 33 | value: nextProps.pathname, 34 | }); 35 | } 36 | } 37 | 38 | selectText = () => { 39 | this.node.setSelectionRange(1, this.node.value.length); 40 | } 41 | 42 | handleChange = (e) => { 43 | this.setState({ 44 | value: e.target.value, 45 | }); 46 | } 47 | 48 | handleKeyDown = (e) => { 49 | if (e.key === 'Escape') { 50 | this.setState({ 51 | value: this.props.pathname, 52 | }, this.selectText); 53 | } 54 | } 55 | 56 | handleKeyPress = (e) => { 57 | if (e.key === 'Enter') { 58 | this.props.push(e.target.value); 59 | } 60 | } 61 | 62 | handleBlur = () => { 63 | this.setState({ 64 | value: this.props.pathname, 65 | }); 66 | } 67 | 68 | render() { 69 | return ( 70 | { this.node = n; }} 80 | /> 81 | ); 82 | } 83 | } 84 | 85 | export default LocationInput; 86 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/shared/Browser/TopBar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { css } from 'glamor'; 4 | 5 | import { useHistory, useLocation } from 'react-router-dom'; 6 | 7 | import LocationInput from './Input'; 8 | 9 | const topBarRule = css` 10 | display: flex; 11 | padding: 7px; 12 | border-bottom: 2px solid rgb(236, 236, 236); 13 | background-color: rgb(246, 246, 246); 14 | `; 15 | 16 | const buttonRule = css` 17 | border: none; 18 | background: none; 19 | padding: 3px 7px; 20 | font-size: 1.6rem; 21 | margin: 0 2px; 22 | border-radius: 3px; 23 | color: rgb(111, 111, 111); 24 | transition: background-color 150ms ease-in-out; 25 | 26 | &:focus { 27 | outline: none; 28 | } 29 | 30 | &:hover { 31 | background-color: rgb(231, 231, 231); 32 | } 33 | 34 | &[disabled] { 35 | color: rgb(219, 219, 219); 36 | } 37 | 38 | &[disabled]:hover { 39 | background-color: transparent; 40 | } 41 | `; 42 | 43 | const TopBar = (props) => { 44 | const history = useHistory(); 45 | const location = useLocation(); 46 | 47 | return ( 48 |
49 | 56 | 63 | 67 |
68 | ) 69 | }; 70 | 71 | export default TopBar; 72 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/shared/Browser/Viewport.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from 'glamor'; 3 | 4 | const viewportRule = css` 5 | overflow: hidden; 6 | height: 480px; 7 | position: relative; 8 | `; 9 | 10 | const viewportBodyRule = css` 11 | height: 100%; 12 | overflow: auto; 13 | `; 14 | 15 | const Viewport = ({ children }) => ( 16 |
17 |
18 | {children} 19 |
20 |
21 | ); 22 | 23 | export default Viewport; 24 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/shared/Browser/Wrapper.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from 'glamor'; 3 | 4 | const wrapperRule = css` 5 | max-width: 100%; 6 | border: 2px solid rgb(236, 236, 236); 7 | box-sizing: border-box; 8 | border-radius: 4px; 9 | margin: 0 auto 3rem; 10 | overflow: hidden; 11 | `; 12 | 13 | const Wrapper = ({ children }) => ( 14 |
15 | {children} 16 |
17 | ); 18 | 19 | export default Wrapper; 20 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/shared/Browser/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { MemoryRouter as Router } from 'react-router-dom'; 3 | 4 | import BrowserWrapper from './Wrapper'; 5 | import BrowserTopBar from './TopBar'; 6 | import BrowserViewport from './Viewport'; 7 | 8 | const Browser = ({ children, defaultPathname }) => ( 9 | 10 | 11 | 12 | 13 | {children} 14 | 15 | 16 | 17 | ); 18 | 19 | Browser.defaultProps = { 20 | defaultPathname: '/', 21 | }; 22 | 23 | export default Browser; 24 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/shared/ComponentPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | import { css } from 'glamor'; 4 | 5 | import Page from './Page'; 6 | import PageNav from './PageNav'; 7 | 8 | import { AnimatedSwitch, spring } from 'react-router-transition'; 9 | 10 | const switchRule = css` 11 | position: relative; 12 | 13 | & > div { 14 | position: absolute; 15 | width: 100%; 16 | padding-bottom: 3rem; 17 | } 18 | `; 19 | 20 | function zoom(val) { 21 | return spring(val, { 22 | stiffness: 135, 23 | damping: 15, 24 | }); 25 | } 26 | 27 | const switchConfig = { 28 | atEnter: { 29 | opacity: 0, 30 | offset: -50, 31 | }, 32 | atLeave: { 33 | opacity: 0, 34 | offset: zoom(50), 35 | }, 36 | atActive: { 37 | opacity: 1, 38 | offset: zoom(0), 39 | }, 40 | }; 41 | 42 | function mapStyles(styles) { 43 | return { 44 | opacity: styles.opacity, 45 | transform: `translateY(${styles.offset}px)`, 46 | }; 47 | } 48 | 49 | const ComponentPage = ({ sections, title }) => ( 50 | 51 |
52 |

{title}

53 | 54 |
55 | 60 | {sections.map(({ title, ...rest }) => ( 61 | 62 | ))} 63 | 64 |
65 | ); 66 | 67 | export default ComponentPage; 68 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/shared/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from 'glamor'; 3 | 4 | const rule = css` 5 | border-top: 1px solid #f1f1f1; 6 | margin-top: 2rem; 7 | padding-top: 2rem; 8 | color: #909090; 9 | font-size: 1rem; 10 | 11 | & a { 12 | color: #505050; 13 | } 14 | 15 | & a:hover { 16 | background-color: #eee; 17 | } 18 | `; 19 | 20 | const Footer = () => ( 21 | 22 | ); 23 | 24 | export default Footer; 25 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/shared/Page.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from 'glamor'; 3 | 4 | const rule = css` 5 | padding: 5rem 3rem; 6 | 7 | @media(max-width: 600px) { 8 | padding: 2rem 3rem 3rem; 9 | } 10 | 11 | & > * { 12 | width: 730px; 13 | margin: 0 auto; 14 | max-width: 100%; 15 | } 16 | 17 | & pre[class*="language-"] { 18 | padding: 2rem; 19 | box-sizing: border-box; 20 | } 21 | 22 | & a { 23 | color: rgb(40, 100, 220); 24 | } 25 | 26 | & a:hover, 27 | & a:visited { 28 | color: rgb(30, 80, 180); 29 | } 30 | `; 31 | 32 | const Page = ({ children }) => ( 33 |
34 | {children} 35 |
36 | ); 37 | 38 | export default Page; 39 | -------------------------------------------------------------------------------- /example/src/Screens/Docs/shared/PageNav.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | import { css } from 'glamor'; 4 | 5 | const rule = css` 6 | padding-bottom: 3rem; 7 | `; 8 | 9 | const linkRule = css` 10 | background-color: white; 11 | margin: 0 1rem 0 0; 12 | padding: 1rem; 13 | text-transform: uppercase; 14 | font-size: 1.2rem; 15 | border-radius: 2px; 16 | transition: background-color 100ms ease-in-out; 17 | 18 | &:hover { 19 | background-color: #f7f7f7; 20 | } 21 | 22 | &.active { 23 | background-color: #f1f1f1; 24 | } 25 | `; 26 | 27 | const PageNav = ({ sections }) => ( 28 |
29 | {sections.map(({ exact, title, path }) => ( 30 | 36 | {title} 37 | 38 | ))} 39 |
40 | ); 41 | 42 | export default PageNav; 43 | -------------------------------------------------------------------------------- /example/src/Screens/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { css } from 'glamor'; 4 | 5 | import Screen from './Screen'; 6 | 7 | const rule = css` 8 | padding: 0 5rem; 9 | width: 960px; 10 | max-width: 100%; 11 | box-sizing: border-box; 12 | margin: 0 auto; 13 | 14 | @media(max-width: 600px) { 15 | padding: 0 3rem 3rem; 16 | } 17 | `; 18 | 19 | const titleRule = css` 20 | font-size: 1.8rem; 21 | font-weight: 300; 22 | margin-top: 10vh; 23 | color: #fff; 24 | 25 | @media(max-width: 600px) { 26 | margin-top: 6rem; 27 | } 28 | `; 29 | 30 | const bodyRule = css` 31 | margin-top: 50vh; 32 | 33 | & span { 34 | font-size: 3.8rem; 35 | } 36 | 37 | @media(max-width: 600px) { 38 | margin-top: 14rem; 39 | } 40 | 41 | @media(min-width: 800px) { 42 | & span { 43 | display: block; 44 | } 45 | } 46 | `; 47 | 48 | const buttonsRule = css` 49 | margin-top: 10vh; 50 | `; 51 | 52 | const buttonRule = css` 53 | display: inline-block; 54 | font-size: 1.6rem; 55 | padding: 1rem 2.5rem; 56 | margin: 0.5rem 1.2rem 0.5rem 0; 57 | background-color: #202020; 58 | border: 1px solid #eee; 59 | border-radius: 1px; 60 | color: #fff; 61 | 62 | &:visited { 63 | color: #fff; 64 | } 65 | 66 | &:hover { 67 | color: #202020; 68 | border-color: #202020; 69 | background-color: #fff; 70 | } 71 | `; 72 | 73 | const primaryButtonRule = css` 74 | color: #202020; 75 | border-color: #202020; 76 | background-color: #eee 77 | 78 | &:visited { 79 | color: #202020; 80 | } 81 | `; 82 | 83 | export default () => ( 84 | 85 |
86 |

React Router Transition

87 |
88 | Painless transitions built for react-router, 89 | powered by react-motion 90 |
91 |
92 | 93 | Demos & Docs 94 | 95 | 99 | GitHub 100 | 101 |
102 |
103 |
104 | ); 105 | -------------------------------------------------------------------------------- /example/src/Screens/Screen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from 'glamor'; 3 | 4 | const rule = css` 5 | height: 100vh; 6 | width: 100vw; 7 | overflow-y: auto; 8 | `; 9 | 10 | const Screen = ({ children }) => ( 11 |
12 | {children} 13 |
14 | ); 15 | 16 | export default Screen; 17 | -------------------------------------------------------------------------------- /example/src/globals.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | html, body { 7 | height: 100%; 8 | width: 100%; 9 | } 10 | 11 | html { 12 | font-size: 62.5%; 13 | } 14 | 15 | body { 16 | font-family: 'Roboto', sans-serif; 17 | font-size: 14px; 18 | font-size: 1.4rem; 19 | letter-spacing: 0.033rem; 20 | background-color: #202020; 21 | color: #eee; 22 | } 23 | 24 | h1 { 25 | font-size: 3rem; 26 | margin: 2.3rem 0; 27 | color: #505050; 28 | } 29 | 30 | h2 { 31 | font-size: 2.2rem; 32 | margin: 2rem 0; 33 | color: #303030; 34 | } 35 | 36 | h3 { 37 | margin: 1.5rem 0; 38 | } 39 | 40 | a { 41 | text-decoration: none; 42 | } 43 | 44 | p { 45 | font-size: 1.6rem; 46 | line-height: 1.7em; 47 | margin-bottom: 1.6rem; 48 | } 49 | 50 | ul { 51 | margin-bottom: 1.6rem; 52 | } 53 | 54 | li { 55 | font-size: 1.6rem; 56 | line-height: 1.7em; 57 | } 58 | 59 | code { 60 | background-color: #f1f1f1; 61 | border-radius: 3px; 62 | padding: 0.3rem 0.7rem; 63 | vertical-align: top; 64 | } 65 | 66 | pre { 67 | font-size: 1.3rem; 68 | } 69 | -------------------------------------------------------------------------------- /example/src/grayscale-prism-theme.css: -------------------------------------------------------------------------------- 1 | /** 2 | * A theme based on @simurai's duotone-dark-sea-syntax 3 | */ 4 | code[class*="language-"], 5 | pre[class*="language-"] { 6 | color: #888; 7 | background: #fafafa; 8 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 9 | text-align: left; 10 | white-space: pre; 11 | word-spacing: normal; 12 | word-break: normal; 13 | word-wrap: normal; 14 | line-height: 1.5; 15 | 16 | -moz-tab-size: 2; 17 | -o-tab-size: 2; 18 | tab-size: 2; 19 | 20 | -webkit-hyphens: none; 21 | -moz-hyphens: none; 22 | -ms-hyphens: none; 23 | hyphens: none; 24 | } 25 | 26 | /* Code blocks */ 27 | pre[class*="language-"] { 28 | margin: .5em 0; 29 | overflow: auto; 30 | } 31 | 32 | :not(pre) > code[class*="language-"], 33 | pre[class*="language-"] { 34 | } 35 | 36 | /* Inline code */ 37 | :not(pre) > code[class*="language-"] { 38 | padding: .1em; 39 | border-radius: .3em; 40 | white-space: normal; 41 | } 42 | 43 | .token.comment, 44 | .token.block-comment, 45 | .token.prolog, 46 | .token.doctype, 47 | .token.cdata { 48 | color: #bbb; 49 | } 50 | 51 | .token.punctuation { 52 | color: #bbb; 53 | } 54 | 55 | .token.tag, 56 | .token.namespace, 57 | .token.deleted { 58 | color: #555; 59 | } 60 | 61 | .token.attr-name { 62 | color: #777; 63 | } 64 | 65 | .token.function-name { 66 | color: #888; 67 | } 68 | 69 | .token.boolean, 70 | .token.number, 71 | .token.function { 72 | color: #666; 73 | } 74 | 75 | .token.property, 76 | .token.class-name, 77 | .token.constant, 78 | .token.symbol { 79 | color: #555; 80 | } 81 | 82 | .token.selector, 83 | .token.important, 84 | .token.atrule, 85 | .token.keyword, 86 | .token.builtin { 87 | color: #666; 88 | } 89 | 90 | .token.string, 91 | .token.char, 92 | .token.attr-value, 93 | .token.regex, 94 | .token.variable { 95 | color: #555; 96 | } 97 | 98 | .token.operator, 99 | .token.entity, 100 | .token.url { 101 | color: #777; 102 | } 103 | 104 | .token.important, 105 | .token.bold { 106 | font-weight: bold; 107 | } 108 | .token.italic { 109 | font-style: italic; 110 | } 111 | 112 | .token.entity { 113 | cursor: help; 114 | } 115 | 116 | .token.inserted { 117 | color: #ccc; 118 | } 119 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter as Router, Route } from 'react-router-dom'; 4 | import { css } from 'glamor'; 5 | 6 | import NavBar from './NavBar'; 7 | 8 | import LandingScreen from './Screens/Home'; 9 | import DocumentationScreen from './Screens/Docs'; 10 | 11 | import { AnimatedSwitch, AnimatedRoute, spring } from 'react-router-transition'; 12 | 13 | import './globals.css'; 14 | import './grayscale-prism-theme.css'; 15 | 16 | // silly workaround for gh-pages 17 | const basename = process.env.NODE_ENV === 'production' 18 | ? '/react-router-transition' 19 | : null; 20 | 21 | const switchRule = css` 22 | position: relative; 23 | 24 | & > div { 25 | position: absolute; 26 | } 27 | `; 28 | 29 | const routeRule = css` 30 | position: relative; 31 | 32 | & > div { 33 | position: absolute; 34 | width: 100%; 35 | } 36 | `; 37 | 38 | function glide(val) { 39 | return spring(val, { 40 | stiffness: 174, 41 | damping: 24, 42 | }); 43 | } 44 | 45 | function slide(val) { 46 | return spring(val, { 47 | stiffness: 125, 48 | damping: 16, 49 | }); 50 | } 51 | 52 | const pageTransitions = { 53 | atEnter: { 54 | offset: 100, 55 | }, 56 | atLeave: { 57 | offset: glide(-100), 58 | }, 59 | atActive: { 60 | offset: glide(0), 61 | }, 62 | }; 63 | 64 | const topBarTransitions = { 65 | atEnter: { 66 | offset: -100, 67 | }, 68 | atLeave: { 69 | offset: slide(-150), 70 | }, 71 | atActive: { 72 | offset: slide(0), 73 | }, 74 | }; 75 | 76 | const App = () => ( 77 | 78 | ( 79 |
80 | ({ 85 | transform: `translateX(${styles.offset}%)`, 86 | })} 87 | > 88 | 89 | 90 | 91 | ({ 97 | transform: `translateY(${styles.offset}%)`, 98 | })} 99 | /> 100 |
101 | )} /> 102 |
103 | ); 104 | 105 | ReactDOM.render( 106 | , 107 | document.getElementById('root') 108 | ); 109 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | 6 | const src = path.join(__dirname, 'src'); 7 | const parentDir = path.join(__dirname, '../'); 8 | 9 | module.exports = { 10 | devtool: 'sourcemap', 11 | 12 | entry: path.join(src, 'index.js'), 13 | 14 | output: { 15 | path: __dirname, 16 | filename: 'bundle.js', 17 | }, 18 | 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.js$/, 23 | loader: 'babel-loader', 24 | include: [src, parentDir], 25 | }, 26 | { 27 | test: /\.css$/, 28 | use: ['style-loader', 'css-loader'], 29 | }, 30 | ], 31 | }, 32 | 33 | resolve: { 34 | modules: ['node_modules'], 35 | 36 | alias: { 37 | 'react-router-transition': path.resolve(parentDir, 'index.js'), 38 | }, 39 | }, 40 | 41 | resolveLoader: { 42 | modules: ['node_modules'], 43 | }, 44 | 45 | plugins: [ 46 | new webpack.DefinePlugin({ 47 | 'process.env.NODE_ENV': JSON.stringify( 48 | process.env.NODE_ENV || 'development', 49 | ), 50 | }), 51 | 52 | new webpack.ProvidePlugin({ 53 | Glamor: require.resolve('glamor/react'), 54 | }), 55 | ], 56 | }; 57 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React, { 2 | cloneElement, 3 | createElement, 4 | useEffect, 5 | useRef, 6 | useState, 7 | } from 'react'; 8 | 9 | import PropTypes from 'prop-types'; 10 | 11 | import { Route, Switch, matchPath, useLocation } from 'react-router-dom'; 12 | 13 | import TransitionMotion from 'react-motion/lib/TransitionMotion'; 14 | import spring from 'react-motion/lib/spring'; 15 | 16 | // Helpers 17 | 18 | function ensureSpring(styles) { 19 | const obj = {}; 20 | 21 | for (var key in styles) { 22 | const value = styles[key]; 23 | if (typeof value === 'number') { 24 | obj[key] = spring(value); 25 | } else { 26 | obj[key] = value; 27 | } 28 | } 29 | 30 | return obj; 31 | } 32 | 33 | function identity(v) { 34 | return v; 35 | } 36 | 37 | function noop() {} 38 | 39 | // Components 40 | 41 | function RouteTransition({ 42 | children, 43 | className, 44 | atEnter, 45 | atActive, 46 | atLeave, 47 | wrapperComponent = 'div', 48 | didLeave = noop, 49 | mapStyles = identity, 50 | runOnMount = false, 51 | }) { 52 | const defaultStyles = 53 | runOnMount === false 54 | ? null 55 | : children == undefined 56 | ? [] 57 | : [ 58 | { 59 | key: children.key, 60 | data: children, 61 | style: atEnter, 62 | }, 63 | ]; 64 | 65 | const styles = 66 | children == undefined 67 | ? [] 68 | : [ 69 | { 70 | key: children.key, 71 | data: children, 72 | style: ensureSpring(atActive), 73 | }, 74 | ]; 75 | 76 | return ( 77 | atEnter} 81 | willLeave={() => ensureSpring(atLeave)} 82 | didLeave={didLeave} 83 | > 84 | {(interpolatedStyles) => ( 85 |
86 | {interpolatedStyles.map((config) => { 87 | const props = { 88 | style: mapStyles(config.style), 89 | key: config.key, 90 | }; 91 | 92 | return wrapperComponent !== false 93 | ? createElement(wrapperComponent, props, config.data) 94 | : cloneElement(config.data, props); 95 | })} 96 |
97 | )} 98 |
99 | ); 100 | } 101 | 102 | RouteTransition.propTypes = { 103 | className: PropTypes.string, 104 | wrapperComponent: PropTypes.oneOfType([ 105 | PropTypes.bool, 106 | PropTypes.element, 107 | PropTypes.string, 108 | PropTypes.func, 109 | ]), 110 | atEnter: PropTypes.object.isRequired, 111 | atActive: PropTypes.object.isRequired, 112 | atLeave: PropTypes.object.isRequired, 113 | didLeave: PropTypes.func, 114 | mapStyles: PropTypes.func, 115 | runOnMount: PropTypes.bool, 116 | }; 117 | 118 | // AnimatedRoute 119 | 120 | // The key-getter for RouteTransition. It's either on or off. 121 | function getKey({ pathname }, path, exact) { 122 | return matchPath(pathname, { exact, path }) ? 'match' : 'no-match'; 123 | } 124 | 125 | function AnimatedRoute({ 126 | render, 127 | component, 128 | path, 129 | exact, 130 | strict, 131 | sensitive, 132 | children, 133 | ...routeTransitionProps 134 | }) { 135 | const location = useLocation(); 136 | 137 | return ( 138 | 139 | 150 | 151 | ); 152 | } 153 | 154 | // AnimatedSwitch 155 | 156 | const NO_MATCH = { 157 | key: 'no-match', 158 | }; 159 | 160 | // Not every location object has a `key` property (e.g. HashHistory). 161 | function getLocationKey(location) { 162 | return typeof location.key === 'string' ? location.key : ''; 163 | } 164 | 165 | // Some superfluous work, but something we need to do in order 166 | // to persist matches/allow for nesting/etc. 167 | function getMatchedRoute(children, { pathname }) { 168 | const childrenArray = React.Children.toArray(children); 169 | 170 | for (let i = 0; i < childrenArray.length; i++) { 171 | const child = childrenArray[i]; 172 | const matches = matchPath(pathname, { 173 | exact: child.props.exact, 174 | path: child.props.path, 175 | }); 176 | 177 | if (matches) { 178 | return child; 179 | } 180 | } 181 | 182 | return NO_MATCH; 183 | } 184 | 185 | let counter = 0; 186 | 187 | function AnimatedSwitch({ children, ...routeTransitionProps }) { 188 | const location = useLocation(); 189 | const match = useRef(null); 190 | const key = useRef(null); 191 | 192 | const nextMatch = getMatchedRoute(children, location); 193 | 194 | if (match.current === null) { 195 | // Persist a reference to the most recent match 196 | match.current = nextMatch; 197 | key.current = getLocationKey(location); 198 | 199 | } else if (match.current.key !== nextMatch.key) { 200 | // Update the key given to Switch anytime the matched route changes 201 | match.current = nextMatch; 202 | key.current = getLocationKey(location) + ++counter; 203 | } 204 | 205 | return ( 206 | 207 | 208 | {children} 209 | 210 | 211 | ); 212 | } 213 | 214 | export { ensureSpring, spring, RouteTransition, AnimatedRoute, AnimatedSwitch }; 215 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-router-transition", 3 | "version": "2.1.0", 4 | "description": "A thin layer over react-motion for animating routes in react-router.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "babel index.js --out-dir lib", 8 | "clean": "rm -rf lib", 9 | "gh-pages": "git b -D gh-pages && git co -b gh-pages && (cd example && npm run build) && cp example/{index.html,bundle.js} ./ && cp index.html 404.html && git add . && git commit -m 'gh-pages' && git push origin gh-pages --force", 10 | "prepublishOnly": "npm run clean && npm run build", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/maisano/react-router-transition.git" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "react-router", 20 | "react-motion", 21 | "animation", 22 | "transition" 23 | ], 24 | "author": "maisano", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/maisano/react-router-transition/issues" 28 | }, 29 | "homepage": "https://github.com/maisano/react-router-transition#readme", 30 | "peerDependencies": { 31 | "react": "^16.8.0", 32 | "react-dom": "^16.8.0", 33 | "react-router-dom": "^5.0.0" 34 | }, 35 | "dependencies": { 36 | "prop-types": "^15.7.2", 37 | "react-motion": "^0.5.2" 38 | }, 39 | "devDependencies": { 40 | "@babel/cli": "^7.6.2", 41 | "@babel/core": "^7.6.2", 42 | "@babel/preset-env": "^7.6.2", 43 | "@babel/preset-react": "^7.0.0", 44 | "prettier": "^1.18.2", 45 | "react": "^16.10.2", 46 | "react-dom": "^16.10.2", 47 | "react-router-dom": "^5.1.2" 48 | } 49 | } 50 | --------------------------------------------------------------------------------