├── .babelrc
├── .eslintrc
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── karma.conf.js
├── package.json
├── src
├── ScrollBehaviorContext.js
├── ScrollContainer.js
├── StateStorage.js
├── index.js
└── useScroll.js
├── test
├── .eslintrc
├── ScrollContainer.test.js
├── components.js
├── histories.js
├── index.js
├── routes.js
├── run.js
└── useScroll.test.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {
4 | "loose": true
5 | }],
6 | "stage-1",
7 | "react"
8 | ],
9 | "plugins": ["dev-expression", "add-module-exports"]
10 | }
11 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "4catalyzer-react",
3 | "env": {
4 | "browser": true
5 | },
6 | "globals": {
7 | "__DEV__": false
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
18 | .grunt
19 |
20 | # node-waf configuration
21 | .lock-wscript
22 |
23 | # Compiled binary addons (http://nodejs.org/api/addons.html)
24 | build/Release
25 |
26 | # Dependency directory
27 | node_modules
28 |
29 | # Optional npm cache directory
30 | .npm
31 |
32 | # Optional REPL history
33 | .node_repl_history
34 |
35 | # Transpiled code
36 | /lib
37 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - node
4 |
5 | jobs:
6 | include:
7 | - addons:
8 | chrome: stable
9 | env: BROWSER=ChromeCi
10 | - addons:
11 | firefox: latest
12 | env: BROWSER=Firefox
13 |
14 | services:
15 | - xvfb
16 |
17 | cache:
18 | yarn: true
19 | npm: true
20 |
21 | branches:
22 | only:
23 | - master
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Jimmy Jia
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-scroll [![Travis][build-badge]][build] [![npm][npm-badge]][npm]
2 |
3 | [React Router](https://github.com/reactjs/react-router) scroll management.
4 |
5 | react-router-scroll is a React Router middleware that adds scroll management using [scroll-behavior](https://github.com/taion/scroll-behavior). By default, the middleware adds browser-style scroll behavior, but you can customize it to scroll however you want on route transitions.
6 |
7 | **This library does not currently support React Router v4, because React Router v4 has no concept of router middlewares. See ongoing discussion in [#52](https://github.com/taion/react-router-scroll/issues/52). For an interim solution for just scrolling to top on navigation, see the React Router [documentation on scroll restoration](https://reacttraining.com/react-router/web/guides/scroll-restoration).**
8 |
9 | ## Usage
10 |
11 | ```js
12 | import { applyRouterMiddleware, browserHistory, Router } from 'react-router';
13 | import { useScroll } from 'react-router-scroll';
14 |
15 | /* ... */
16 |
17 | ReactDOM.render(
18 | ,
23 | container
24 | );
25 | ```
26 |
27 | ## Guide
28 |
29 | ### Installation
30 |
31 | ```shell
32 | $ npm i -S react react-dom react-router
33 | $ npm i -S react-router-scroll
34 | ```
35 |
36 | ### Basic usage
37 |
38 | Apply the `useScroll` router middleware using `applyRouterMiddleware`, as in the example above.
39 |
40 | ### Custom scroll behavior
41 |
42 | You can provide a custom `shouldUpdateScroll` callback as an argument to `useScroll`. This callback is called with the previous and the current router props.
43 |
44 | The callback can return:
45 |
46 | - a falsy value to suppress updating the scroll position
47 | - a position array of `x` and `y`, such as `[0, 100]`, to scroll to that position
48 | - a string with the `id` or `name` of an element, to scroll to that element
49 | - a truthy value to emulate the browser default scroll behavior
50 |
51 | ```js
52 | useScroll((prevRouterProps, { location }) => (
53 | !prevRouterProps || location.pathname !== prevRouterProps.location.pathname
54 | ));
55 |
56 | useScroll((prevRouterProps, { routes }) => {
57 | if (routes.some(route => route.ignoreScrollBehavior)) {
58 | return false;
59 | }
60 |
61 | if (routes.some(route => route.scrollToTop)) {
62 | return [0, 0];
63 | }
64 |
65 | return true;
66 | });
67 | ```
68 |
69 | You can customize `useScroll` even further by providing a configuration object with a `createScrollBehavior` callback that creates the scroll behavior object. This allows using a custom subclass of `ScrollBehavior` from scroll-behavior with custom logic. When using a configuration object, you can specify the `shouldUpdateScroll` callback as above under the `shouldUpdateScroll` key.
70 |
71 | ```js
72 | useScroll({
73 | createScrollBehavior: (config) => new MyScrollBehavior(config),
74 | shouldUpdateScroll,
75 | });
76 | ```
77 |
78 | ### Scrolling elements other than `window`
79 |
80 | Use `` in components rendered by a router with the `useScroll` middleware to manage the scroll behavior of elements other than `window`. Each `` must be given a unique `scrollKey`, and can be given an optional `shouldUpdateScroll` callback that behaves as above.
81 |
82 | ```js
83 | import { ScrollContainer } from 'react-router-scroll';
84 |
85 | function Page() {
86 | /* ... */
87 |
88 | return (
89 |
93 |
94 |
95 | );
96 | }
97 | ```
98 |
99 | `` does not support on-the-fly changes to `scrollKey` or to the DOM node for its child.
100 |
101 | ### Notes
102 |
103 | #### Minimizing bundle size
104 |
105 | If you are not using ``, you can reduce your bundle size by importing the `useScroll` module directly.
106 |
107 | ```js
108 | import useScroll from 'react-router-scroll/lib/useScroll';
109 | ```
110 |
111 | #### Server rendering
112 |
113 | Do not apply the `useScroll` middleware when rendering on a server. You may use `` in server-rendered components; it will do nothing when rendering on a server.
114 |
115 | [build-badge]: https://img.shields.io/travis/taion/react-router-scroll/master.svg
116 | [build]: https://travis-ci.org/taion/react-router-scroll
117 |
118 | [npm-badge]: https://img.shields.io/npm/v/react-router-scroll.svg
119 | [npm]: https://www.npmjs.org/package/react-router-scroll
120 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack'); // eslint-disable-line import/no-extraneous-dependencies
2 |
3 | module.exports = (config) => {
4 | const { env } = process;
5 |
6 | config.set({
7 | frameworks: ['mocha', 'sinon-chai'],
8 |
9 | files: ['test/index.js'],
10 |
11 | preprocessors: {
12 | 'test/index.js': ['webpack', 'sourcemap'],
13 | },
14 |
15 | webpack: {
16 | module: {
17 | rules: [
18 | { test: /\.js$/, exclude: /node_modules/, use: 'babel-loader' },
19 | ],
20 | },
21 | plugins: [
22 | new webpack.DefinePlugin({
23 | 'process.env.NODE_ENV': JSON.stringify('test'),
24 | __DEV__: true,
25 | }),
26 | ],
27 | devtool: 'cheap-module-inline-source-map',
28 | },
29 |
30 | webpackMiddleware: {
31 | noInfo: true,
32 | },
33 |
34 | reporters: ['mocha'],
35 |
36 | mochaReporter: {
37 | output: 'autowatch',
38 | },
39 |
40 | customLaunchers: {
41 | ChromeCi: {
42 | base: 'Chrome',
43 | flags: ['--no-sandbox'],
44 | },
45 | },
46 |
47 | browsers: env.BROWSER ? env.BROWSER.split(',') : ['Chrome', 'Firefox'],
48 | });
49 | };
50 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-router-scroll",
3 | "version": "0.4.4",
4 | "description": "React Router scroll management",
5 | "files": [
6 | "lib"
7 | ],
8 | "main": "lib/index.js",
9 | "scripts": {
10 | "build": "rimraf lib && babel src -d lib",
11 | "lint": "eslint src test *.js",
12 | "prepublish": "npm run build",
13 | "tdd": "cross-env NODE_ENV=test karma start",
14 | "test": "npm run lint && npm run testonly",
15 | "testonly": "npm run tdd -- --single-run"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/taion/react-router-scroll.git"
20 | },
21 | "keywords": [
22 | "react",
23 | "react router",
24 | "scroll"
25 | ],
26 | "author": "Jimmy Jia",
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/taion/react-router-scroll/issues"
30 | },
31 | "homepage": "https://github.com/taion/react-router-scroll#readme",
32 | "dependencies": {
33 | "prop-types": "^15.6.0",
34 | "scroll-behavior": "^0.9.5",
35 | "warning": "^3.0.0"
36 | },
37 | "peerDependencies": {
38 | "history": "^2.0.0 || ^3.0.0",
39 | "react": "^0.14.9 || >=15.3.0",
40 | "react-dom": "^0.14.9 || >=15.3.0",
41 | "react-router": "^2.3.0 || ^3.0.0"
42 | },
43 | "devDependencies": {
44 | "babel-cli": "^6.26.0",
45 | "babel-core": "^6.26.3",
46 | "babel-eslint": "^7.2.3",
47 | "babel-loader": "^7.1.5",
48 | "babel-plugin-add-module-exports": "^1.0.2",
49 | "babel-plugin-dev-expression": "^0.2.2",
50 | "babel-polyfill": "^6.26.0",
51 | "babel-preset-env": "^1.7.0",
52 | "babel-preset-react": "^6.24.1",
53 | "babel-preset-stage-1": "^6.24.1",
54 | "chai": "^4.2.0",
55 | "create-react-class": "^15.6.3",
56 | "cross-env": "^5.2.1",
57 | "dirty-chai": "^2.0.1",
58 | "dom-helpers": "^3.4.0",
59 | "eslint": "^4.19.1",
60 | "eslint-config-4catalyzer-react": "^0.3.3",
61 | "eslint-plugin-import": "^2.22.0",
62 | "eslint-plugin-jsx-a11y": "^5.1.1",
63 | "eslint-plugin-react": "^7.20.5",
64 | "history": "^2.1.2",
65 | "karma": "^1.7.1",
66 | "karma-chrome-launcher": "^2.2.0",
67 | "karma-firefox-launcher": "^1.3.0",
68 | "karma-mocha": "^1.3.0",
69 | "karma-mocha-reporter": "^2.2.5",
70 | "karma-sinon-chai": "^1.3.4",
71 | "karma-sourcemap-loader": "^0.3.7",
72 | "karma-webpack": "^2.0.13",
73 | "mocha": "^4.1.0",
74 | "react": "^16.13.1",
75 | "react-dom": "^16.13.1",
76 | "react-router": "^2.8.1",
77 | "rimraf": "^2.7.1",
78 | "sinon": "^4.5.0",
79 | "sinon-chai": "^2.14.0",
80 | "webpack": "^3.12.0"
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/ScrollBehaviorContext.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 |
4 | import StateStorage from './StateStorage';
5 |
6 | const propTypes = {
7 | shouldUpdateScroll: PropTypes.func,
8 | createScrollBehavior: PropTypes.func.isRequired,
9 | routerProps: PropTypes.object.isRequired,
10 | children: PropTypes.element.isRequired,
11 | };
12 |
13 | const childContextTypes = {
14 | scrollBehavior: PropTypes.object.isRequired,
15 | };
16 |
17 | class ScrollBehaviorContext extends React.Component {
18 | constructor(props, context) {
19 | super(props, context);
20 |
21 | const { routerProps } = props;
22 | const { router } = routerProps;
23 |
24 | this.scrollBehavior = props.createScrollBehavior({
25 | addTransitionHook: router.listenBefore,
26 | stateStorage: new StateStorage(router),
27 | getCurrentLocation: () => this.props.routerProps.location,
28 | shouldUpdateScroll: this.shouldUpdateScroll,
29 | });
30 |
31 | this.scrollBehavior.updateScroll(null, routerProps);
32 | }
33 |
34 | getChildContext() {
35 | return {
36 | scrollBehavior: this,
37 | };
38 | }
39 |
40 | componentDidUpdate(prevProps) {
41 | const { routerProps } = this.props;
42 | const prevRouterProps = prevProps.routerProps;
43 |
44 | if (routerProps.location === prevRouterProps.location) {
45 | return;
46 | }
47 |
48 | this.scrollBehavior.updateScroll(prevRouterProps, routerProps);
49 | }
50 |
51 | componentWillUnmount() {
52 | this.scrollBehavior.stop();
53 | }
54 |
55 | shouldUpdateScroll = (prevRouterProps, routerProps) => {
56 | const { shouldUpdateScroll } = this.props;
57 | if (!shouldUpdateScroll) {
58 | return true;
59 | }
60 |
61 | // Hack to allow accessing scrollBehavior._stateStorage.
62 | return shouldUpdateScroll.call(
63 | this.scrollBehavior, prevRouterProps, routerProps,
64 | );
65 | };
66 |
67 | registerElement = (key, element, shouldUpdateScroll) => {
68 | this.scrollBehavior.registerElement(
69 | key, element, shouldUpdateScroll, this.props.routerProps,
70 | );
71 | };
72 |
73 | unregisterElement = (key) => {
74 | this.scrollBehavior.unregisterElement(key);
75 | };
76 |
77 | render() {
78 | return this.props.children;
79 | }
80 | }
81 |
82 | ScrollBehaviorContext.propTypes = propTypes;
83 | ScrollBehaviorContext.childContextTypes = childContextTypes;
84 |
85 | export default ScrollBehaviorContext;
86 |
--------------------------------------------------------------------------------
/src/ScrollContainer.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import warning from 'warning';
5 |
6 | const propTypes = {
7 | scrollKey: PropTypes.string.isRequired,
8 | shouldUpdateScroll: PropTypes.func,
9 | children: PropTypes.element.isRequired,
10 | };
11 |
12 | const contextTypes = {
13 | // This is necessary when rendering on the client. However, when rendering on
14 | // the server, this container will do nothing, and thus does not require the
15 | // scroll behavior context.
16 | scrollBehavior: PropTypes.object,
17 | };
18 |
19 | class ScrollContainer extends React.Component {
20 | constructor(props, context) {
21 | super(props, context);
22 |
23 | // We don't re-register if the scroll key changes, so make sure we
24 | // unregister with the initial scroll key just in case the user changes it.
25 | this.scrollKey = props.scrollKey;
26 | }
27 |
28 | componentDidMount() {
29 | this.context.scrollBehavior.registerElement(
30 | this.props.scrollKey,
31 | ReactDOM.findDOMNode(this), // eslint-disable-line react/no-find-dom-node
32 | this.shouldUpdateScroll,
33 | );
34 |
35 | // Only keep around the current DOM node in development, as this is only
36 | // for emitting the appropriate warning.
37 | if (__DEV__) {
38 | this.domNode = ReactDOM.findDOMNode(this); // eslint-disable-line react/no-find-dom-node
39 | }
40 | }
41 |
42 | componentWillReceiveProps(nextProps) {
43 | warning(
44 | nextProps.scrollKey === this.props.scrollKey,
45 | ' does not support changing scrollKey.',
46 | );
47 | }
48 |
49 | componentDidUpdate() {
50 | if (__DEV__) {
51 | const prevDomNode = this.domNode;
52 | this.domNode = ReactDOM.findDOMNode(this); // eslint-disable-line react/no-find-dom-node
53 |
54 | warning(
55 | this.domNode === prevDomNode,
56 | ' does not support changing DOM node.',
57 | );
58 | }
59 | }
60 |
61 | componentWillUnmount() {
62 | this.context.scrollBehavior.unregisterElement(this.scrollKey);
63 | }
64 |
65 | shouldUpdateScroll = (prevRouterProps, routerProps) => {
66 | const { shouldUpdateScroll } = this.props;
67 | if (!shouldUpdateScroll) {
68 | return true;
69 | }
70 |
71 | // Hack to allow accessing scrollBehavior._stateStorage.
72 | return shouldUpdateScroll.call(
73 | this.context.scrollBehavior.scrollBehavior,
74 | prevRouterProps,
75 | routerProps,
76 | );
77 | };
78 |
79 | render() {
80 | return this.props.children;
81 | }
82 | }
83 |
84 | ScrollContainer.propTypes = propTypes;
85 | ScrollContainer.contextTypes = contextTypes;
86 |
87 | export default ScrollContainer;
88 |
--------------------------------------------------------------------------------
/src/StateStorage.js:
--------------------------------------------------------------------------------
1 | import { readState, saveState } from 'history/lib/DOMStateStorage';
2 |
3 | const STATE_KEY_PREFIX = '@@scroll|';
4 |
5 | export default class StateStorage {
6 | constructor(router) {
7 | this.getFallbackLocationKey = router.createPath;
8 | }
9 |
10 | read(location, key) {
11 | return readState(this.getStateKey(location, key));
12 | }
13 |
14 | save(location, key, value) {
15 | saveState(this.getStateKey(location, key), value);
16 | }
17 |
18 | getStateKey(location, key) {
19 | const locationKey = location.key || this.getFallbackLocationKey(location);
20 | const stateKeyBase = `${STATE_KEY_PREFIX}${locationKey}`;
21 | return key == null ? stateKeyBase : `${stateKeyBase}|${key}`;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export ScrollContainer from './ScrollContainer';
2 | export useScroll from './useScroll';
3 |
--------------------------------------------------------------------------------
/src/useScroll.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ScrollBehavior from 'scroll-behavior';
3 |
4 | import ScrollBehaviorContext from './ScrollBehaviorContext';
5 |
6 | function defaultCreateScrollBehavior(config) {
7 | return new ScrollBehavior(config);
8 | }
9 |
10 | export default function useScroll(shouldUpdateScrollOrConfig) {
11 | let shouldUpdateScroll;
12 | let createScrollBehavior;
13 |
14 | if (
15 | !shouldUpdateScrollOrConfig ||
16 | typeof shouldUpdateScrollOrConfig === 'function'
17 | ) {
18 | shouldUpdateScroll = shouldUpdateScrollOrConfig;
19 | createScrollBehavior = defaultCreateScrollBehavior;
20 | } else {
21 | ({
22 | shouldUpdateScroll,
23 | createScrollBehavior = defaultCreateScrollBehavior,
24 | } = shouldUpdateScrollOrConfig);
25 | }
26 |
27 | return {
28 | renderRouterContext: (child, props) => (
29 |
34 | {child}
35 |
36 | ),
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | },
5 | "globals": {
6 | "expect": false
7 | },
8 | "rules": {
9 | "import/no-extraneous-dependencies": ["error", {
10 | "devDependencies": true
11 | }]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/test/ScrollContainer.test.js:
--------------------------------------------------------------------------------
1 | import scrollTop from 'dom-helpers/query/scrollTop';
2 | import createBrowserHistory from 'history/lib/createBrowserHistory';
3 | import createHashHistory from 'history/lib/createHashHistory';
4 | import React from 'react';
5 | import ReactDOM from 'react-dom';
6 | import { applyRouterMiddleware, Router, useRouterHistory } from 'react-router';
7 |
8 | import useScroll from '../src/useScroll';
9 | import ScrollContainer from '../src/ScrollContainer';
10 |
11 | import { ScrollableComponent } from './components';
12 | import { createHashHistoryWithoutKey } from './histories';
13 | import { createElementRoutes } from './routes';
14 | import run from './run';
15 |
16 | describe('', () => {
17 | let container;
18 |
19 | beforeEach(() => {
20 | window.history.replaceState(null, null, '/');
21 |
22 | container = document.createElement('div');
23 | document.body.appendChild(container);
24 | });
25 |
26 | afterEach(() => {
27 | ReactDOM.unmountComponentAtNode(container);
28 | document.body.removeChild(container);
29 | });
30 |
31 | // Create a new history every time to avoid old state.
32 | [
33 | createBrowserHistory,
34 | createHashHistory,
35 | createHashHistoryWithoutKey,
36 | ].forEach((createHistory) => {
37 | let history;
38 |
39 | beforeEach(() => {
40 | history = useRouterHistory(createHistory)();
41 | });
42 |
43 | describe(createHistory.name, () => {
44 | it('should have correct default behavior', (done) => {
45 | const Page = () => (
46 |
47 |
48 |
49 | );
50 |
51 | const steps = [
52 | () => {
53 | scrollTop(container.firstChild, 10000);
54 | history.push('/other');
55 | },
56 | () => {
57 | expect(scrollTop(container.firstChild)).to.equal(0);
58 | history.goBack();
59 | },
60 | () => {
61 | expect(scrollTop(container.firstChild)).to.equal(10000);
62 | done();
63 | },
64 | ];
65 |
66 | ReactDOM.render(
67 | false))}
71 | onUpdate={run(steps)}
72 | />,
73 | container,
74 | );
75 | });
76 |
77 | it('should have support custom behavior', (done) => {
78 | const Page = () => (
79 | [0, 5000]}
82 | >
83 |
84 |
85 | );
86 |
87 | const steps = [
88 | () => {
89 | scrollTop(container.firstChild, 10000);
90 | history.push('/other');
91 | },
92 | () => {
93 | expect(scrollTop(container.firstChild)).to.equal(5000);
94 | history.goBack();
95 | },
96 | () => {
97 | expect(scrollTop(container.firstChild)).to.equal(5000);
98 | done();
99 | },
100 | ];
101 |
102 | ReactDOM.render(
103 | false))}
107 | onUpdate={run(steps)}
108 | />,
109 | container,
110 | );
111 | });
112 | });
113 | });
114 | });
115 |
--------------------------------------------------------------------------------
/test/components.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export function ScrollableComponent() {
4 | return (
5 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/test/histories.js:
--------------------------------------------------------------------------------
1 | import createHashHistory from 'history/lib/createHashHistory';
2 |
3 | export function createHashHistoryWithoutKey() {
4 | // Avoid persistence of stored data from previous tests.
5 | window.sessionStorage.clear();
6 |
7 | return createHashHistory({ queryKey: false });
8 | }
9 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 |
3 | import createReactClass from 'create-react-class';
4 | import dirtyChai from 'dirty-chai';
5 | import PropTypes from 'prop-types';
6 | import React from 'react';
7 |
8 | global.chai.use(dirtyChai);
9 |
10 | // FIXME: Tests fail with React Router v3, but React Router v2 doesn't work
11 | // with React v16. This hacks around that incompatibility.
12 | React.createClass = createReactClass;
13 | React.PropTypes = PropTypes;
14 |
15 | const testsContext = require.context('.', true, /\.test\.js$/);
16 | testsContext.keys().forEach(testsContext);
17 |
--------------------------------------------------------------------------------
/test/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IndexRoute, Route } from 'react-router';
3 |
4 | function Page1() {
5 | return (
6 |
7 | );
8 | }
9 |
10 | function Page2() {
11 | return (
12 |
13 | );
14 | }
15 |
16 | export const syncRoutes = [
17 | ,
18 | ,
19 | ];
20 |
21 | function asyncOnEnter(nextState, cb) {
22 | setTimeout(cb, 100);
23 | }
24 |
25 | export const asyncRoutes = [
26 | ,
27 | ,
28 | ];
29 |
30 | export function createElementRoutes(Page) {
31 | return (
32 |
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/test/run.js:
--------------------------------------------------------------------------------
1 | export function delay(cb) {
2 | // Give throttled scroll listeners time to settle down.
3 | requestAnimationFrame(() => requestAnimationFrame(cb));
4 | }
5 |
6 | export default function run(steps) {
7 | let i = 0;
8 |
9 | return () => {
10 | if (i === steps.length) {
11 | return;
12 | }
13 |
14 | delay(steps[i++]);
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/test/useScroll.test.js:
--------------------------------------------------------------------------------
1 | import scrollLeft from 'dom-helpers/query/scrollLeft';
2 | import scrollTop from 'dom-helpers/query/scrollTop';
3 | import createBrowserHistory from 'history/lib/createBrowserHistory';
4 | import createHashHistory from 'history/lib/createHashHistory';
5 | import React from 'react';
6 | import ReactDOM from 'react-dom';
7 | import { applyRouterMiddleware, Router, useRouterHistory } from 'react-router';
8 | import ScrollBehavior from 'scroll-behavior';
9 |
10 | import StateStorage from '../src/StateStorage';
11 | import useScroll from '../src/useScroll';
12 |
13 | import { createHashHistoryWithoutKey } from './histories';
14 | import { asyncRoutes, syncRoutes } from './routes';
15 | import run, { delay } from './run';
16 |
17 | describe('useScroll', () => {
18 | let container;
19 |
20 | beforeEach(() => {
21 | window.history.replaceState(null, null, '/');
22 |
23 | container = document.createElement('div');
24 | document.body.appendChild(container);
25 | });
26 |
27 | afterEach(() => {
28 | ReactDOM.unmountComponentAtNode(container);
29 | document.body.removeChild(container);
30 | });
31 |
32 | // Create a new history every time to avoid old state.
33 | [
34 | createBrowserHistory,
35 | createHashHistory,
36 | createHashHistoryWithoutKey,
37 | ].forEach((createHistory) => {
38 | let history;
39 |
40 | beforeEach(() => {
41 | history = useRouterHistory(createHistory)();
42 | });
43 |
44 | describe(createHistory.name, () => {
45 | [
46 | ['syncRoutes', syncRoutes],
47 | ['asyncRoutes', asyncRoutes],
48 | ].forEach(([routesName, routes]) => {
49 | describe(routesName, () => {
50 | it('should have correct default behavior', (done) => {
51 | const steps = [
52 | () => {
53 | scrollTop(window, 15000);
54 | delay(() => history.push('/page2'));
55 | },
56 | () => {
57 | expect(scrollTop(window)).to.equal(0);
58 | history.goBack();
59 | },
60 | () => {
61 | expect(scrollTop(window)).to.equal(15000);
62 | done();
63 | },
64 | ];
65 |
66 | ReactDOM.render(
67 | ,
73 | container,
74 | );
75 | });
76 |
77 | it('should support custom behavior', (done) => {
78 | let prevPosition;
79 | let position;
80 |
81 | function shouldUpdateScroll(prevRouterState, routerState) {
82 | const stateStorage = new StateStorage(routerState.router);
83 |
84 | if (prevRouterState) {
85 | prevPosition = stateStorage.read(prevRouterState.location);
86 | }
87 |
88 | position = stateStorage.read(routerState.location);
89 |
90 | if (prevRouterState === null) {
91 | return [10, 20];
92 | }
93 |
94 | if (prevRouterState.routes[0].path === '/') {
95 | return false;
96 | }
97 |
98 | if (routerState.location.action === 'POP') {
99 | return true;
100 | }
101 |
102 | expect.fail();
103 | return false;
104 | }
105 |
106 | const steps = [
107 | () => {
108 | expect(scrollLeft(window)).to.equal(10);
109 | expect(scrollTop(window)).to.equal(20);
110 |
111 | scrollTop(window, 15000);
112 |
113 | delay(() => history.push('/page2'));
114 | },
115 | () => {
116 | expect(prevPosition).to.eql([10, 15000]);
117 | expect(position).to.not.exist();
118 |
119 | expect(scrollLeft(window)).to.not.equal(0);
120 | expect(scrollTop(window)).to.not.equal(0);
121 |
122 | scrollLeft(window, 0);
123 | scrollTop(window, 0);
124 |
125 | delay(() => history.goBack());
126 | },
127 | () => {
128 | expect(prevPosition).to.eql([0, 0]);
129 | expect(position).to.eql([10, 15000]);
130 |
131 | expect(scrollLeft(window)).to.equal(10);
132 | expect(scrollTop(window)).to.equal(15000);
133 |
134 | done();
135 | },
136 | ];
137 |
138 | ReactDOM.render(
139 | ,
145 | container,
146 | );
147 | });
148 |
149 | it('should support a custom scroll behavior factory', (done) => {
150 | class MyScrollBehavior extends ScrollBehavior {
151 | scrollToTarget() {
152 | window.scrollTo(0, 50);
153 | }
154 | }
155 |
156 | const steps = [
157 | () => {
158 | history.push('/page2');
159 | },
160 | () => {
161 | expect(scrollTop(window)).to.equal(50);
162 |
163 | done();
164 | },
165 | ];
166 |
167 | ReactDOM.render(
168 | new MyScrollBehavior(config),
173 | }))}
174 | onUpdate={run(steps)}
175 | />,
176 | container,
177 | );
178 | });
179 |
180 | it('should support fully custom behavior', (done) => {
181 | class MyScrollBehavior extends ScrollBehavior {
182 | scrollToTarget(element, target) {
183 | element.scrollTo(20, target[1] + 10);
184 | }
185 | }
186 |
187 | function shouldUpdateScroll() {
188 | return [0, 50];
189 | }
190 |
191 | const steps = [
192 | () => {
193 | history.push('/page2');
194 | },
195 | () => {
196 | expect(scrollLeft(window)).to.equal(20);
197 | expect(scrollTop(window)).to.equal(60);
198 |
199 | done();
200 | },
201 | ];
202 |
203 | ReactDOM.render(
204 | new MyScrollBehavior(config),
209 | shouldUpdateScroll,
210 | }))}
211 | onUpdate={run(steps)}
212 | />,
213 | container,
214 | );
215 | });
216 | });
217 | });
218 | });
219 | });
220 | });
221 |
--------------------------------------------------------------------------------