Skip to content

Instantly share code, notes, and snippets.

@ro-savage
Created June 15, 2016 23:16
Show Gist options
  • Select an option

  • Save ro-savage/2ee466eba354b114fb7f33ebddc2c8d1 to your computer and use it in GitHub Desktop.

Select an option

Save ro-savage/2ee466eba354b114fb7f33ebddc2c8d1 to your computer and use it in GitHub Desktop.
Updated react-router-scroll
import React, { PropTypes } from 'react'
import { Provider } from 'react-redux'
import { Router, applyRouterMiddleware } from 'react-router'
import useScroll from '../helpers/ScrollBehaviourContainer'
import getRoutes from '../routes'
const Root = (props) => {
return (
<Provider store={props.store}>
<Router
history={props.history}
routes={getRoutes()}
render={applyRouterMiddleware(useScroll(undefined, 'mainContent'))}
/>
</Provider>
)
}
Root.propTypes = {
history: PropTypes.object.isRequired,
store: PropTypes.object.isRequired,
}
export default Root
/* eslint-disable */
import off from 'dom-helpers/events/off';
import on from 'dom-helpers/events/on';
import scrollLeft from 'dom-helpers/query/scrollLeft';
import scrollTop from 'dom-helpers/query/scrollTop';
import requestAnimationFrame from 'dom-helpers/util/requestAnimationFrame';
import { PUSH } from 'history/lib/Actions';
import { readState, saveState } from 'history/lib/DOMStateStorage';
// FIXME: Stop using this gross hack. This won't collide with any actual
// history location keys, but it's dirty to sneakily use the same storage here.
const KEY_PREFIX = 's/';
// Try at most this many times to scroll, to avoid getting stuck.
const MAX_SCROLL_ATTEMPTS = 2;
export default class ScrollBehavior {
constructor(history, getCurrentLocation, elTargetId) {
this._history = history;
this._getCurrentLocation = getCurrentLocation;
this._elTargetId = elTargetId || false
// This helps avoid some jankiness in fighting against the browser's
// default scroll behavior on `POP` transitions.
/* istanbul ignore if: not supported by any browsers on Travis */
if ('scrollRestoration' in window.history) {
this._oldScrollRestoration = window.history.scrollRestoration;
window.history.scrollRestoration = 'manual';
} else {
this._oldScrollRestoration = null;
}
this._savePositionHandle = null;
this._checkScrollHandle = null;
this._scrollTarget = null;
this._numScrollAttempts = 0;
// We have to listen to each scroll update rather than to just location
// updates, because some browsers will update scroll position before
// emitting the location change.
on(window, 'scroll', this._onScroll);
this._unlistenBefore = history.listenBefore(() => {
if (this._savePositionHandle !== null) {
requestAnimationFrame.cancel(this._savePositionHandle);
this._savePositionHandle = null;
}
});
}
stop() {
/* istanbul ignore if: not supported by any browsers on Travis */
if (this._oldScrollRestoration) {
window.history.scrollRestoration = this._oldScrollRestoration;
}
off(window, 'scroll', this._onScroll);
this._cancelCheckScroll();
this._unlistenBefore();
}
updateScroll(scrollPosition) {
// Whatever we were doing before isn't relevant any more.
this._cancelCheckScroll();
if (scrollPosition && !Array.isArray(scrollPosition)) {
this._scrollTarget = this._getDefaultScrollTarget();
} else {
this._scrollTarget = scrollPosition;
}
// Check the scroll position to see if we even need to scroll.
this._onScroll();
if (!this._scrollTarget) {
return;
}
this._numScrollAttempts = 0;
this._checkScrollPosition();
}
readPosition(location) {
return readState(this._getKey(location));
}
_onScroll = () => {
// It's possible that this scroll operation was triggered by what will be a
// `POP` transition. Instead of updating the saved location immediately, we
// have to enqueue the update, then potentially cancel it if we observe a
// location update.
if (this._savePositionHandle === null) {
this._savePositionHandle = requestAnimationFrame(this._savePosition);
}
console.log('this._scrollTarget', this._scrollTarget)
if (this._scrollTarget) {
const [xTarget, yTarget] = this._scrollTarget;
let x = false;
let y = false;
// Check if we are comparing to a element or window
if (this._elTargetId) {
const el = document.getElementById(this._elTargetId)
x = el.scrollLeft
y = el.scrollTop
} else {
x = scrollLeft(window);
y = scrollTop(window);
}
if (x === xTarget && y === yTarget) {
this._scrollTarget = null;
this._cancelCheckScroll();
}
}
};
_savePosition = () => {
this._savePositionHandle = null;
// We have to directly update `DOMStateStorage`, because actually updating
// the location could cause e.g. React Router to re-render the entire page,
// which would lead to observably bad scroll performance.
saveState(
this._getKey(this._getCurrentLocation()),
[scrollLeft(window), scrollTop(window)]
);
};
_getKey(location) {
// Use fallback key when actual key is unavailable.
const key = location.key || this._history.createPath(location);
return `${KEY_PREFIX}${key}`;
}
_cancelCheckScroll() {
if (this._checkScrollHandle !== null) {
requestAnimationFrame.cancel(this._checkScrollHandle);
this._checkScrollHandle = null;
}
}
_getDefaultScrollTarget() {
const location = this._getCurrentLocation();
if (location.action === PUSH) {
return [0, 0];
}
return this.readPosition(location) || [0, 0];
}
_checkScrollPosition = () => {
this._checkScrollHandle = null;
// We can only get here if scrollTarget is set. Every code path that unsets
// scroll target also cancels the handle to avoid calling this handler.
// Still, check anyway just in case.
/* istanbul ignore if: paranoid guard */
if (!this._scrollTarget) {
return;
}
const [x, y] = this._scrollTarget;
// Check if we are scrolling a target or window
if (this._elTargetId) {
const el = document.getElementById(this._elTargetId)
el.scrollLeft = x
el.scrollTop = y
} else {
window.scrollTo(x, y);
}
++this._numScrollAttempts;
/* istanbul ignore if: paranoid guard */
if (this._numScrollAttempts >= MAX_SCROLL_ATTEMPTS) {
this._scrollTarget = null;
return;
}
this._checkScrollHandle = requestAnimationFrame(this._checkScrollPosition);
};
}
/* eslint-disable */
import React from 'react';
import ScrollBehavior from './scroll-behavior';
class ScrollBehaviorContainer extends React.Component {
static propTypes = {
shouldUpdateScroll: React.PropTypes.func,
routerProps: React.PropTypes.object.isRequired,
children: React.PropTypes.node.isRequired,
elTarget: React.PropTypes.string
};
componentDidMount() {
const { routerProps, elTargetId } = this.props;
this.scrollBehavior = new ScrollBehavior(
routerProps.router,
() => this.props.routerProps.location,
elTargetId // pass a target
);
this.onUpdate(null, routerProps);
}
componentDidUpdate(prevProps) {
const { routerProps } = this.props;
const prevRouterProps = prevProps.routerProps;
if (routerProps.location === prevRouterProps.location) {
return;
}
this.onUpdate(prevRouterProps, routerProps);
}
componentWillUnmount() {
this.scrollBehavior.stop();
}
onUpdate(prevRouterProps, routerProps) {
const { shouldUpdateScroll } = this.props;
let scrollPosition;
if (!shouldUpdateScroll) {
scrollPosition = true;
} else {
scrollPosition = shouldUpdateScroll.call(
this.scrollBehavior, prevRouterProps, routerProps
);
}
this.scrollBehavior.updateScroll(scrollPosition);
}
render() {
return this.props.children;
}
}
export default function useScroll(shouldUpdateScroll, elTargetId) {
return {
renderRouterContext: (child, props) => (
<ScrollBehaviorContainer
shouldUpdateScroll={shouldUpdateScroll}
routerProps={props}
elTargetId={elTargetId}
>
{child}
</ScrollBehaviorContainer>
),
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment