Skip to content

checking for an unreachable scroll position, without trying to scroll… #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,12 @@ reads and writes scroll positions from/to session storage, saves positions befor
of nested components like WindowScroller and ElementScroller, and performs delayed scrolling to hash links anywhere
within the document. It has the following properties:

| Name | Type | Required | Description |
|------|------|----------|-------------|
| history | object | yes | A [history](https://github.com/ReactTraining/history) object, as returned by `createBrowserHistory` or `createMemoryHistory`. |
| sessionKey | string | no | The key under which session state is stored. Defaults to `ScrollManager`. |
| timeout | number | no | The maximum number of milliseconds to wait for rendering to complete. Defaults to 3000. |
| Name | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| history | object | yes | | A [history](https://github.com/ReactTraining/history) object, as returned by `createBrowserHistory` or `createMemoryHistory`. |
| sessionKey | string | no | `ScrollManager` | The key under which session state is stored. |
| timeout | number | no | `3000` | The maximum number of milliseconds to wait for rendering to complete. |
| blockSizeTolerance | number | no | `0` | The maximum number of pixels which will be considered as a successful scroll to saved position.<br><b>Example</b>: page height is 500px, last saved position is 503px, `blockSizeTolerance` is `10`. Lib will scroll to `500px` and considered as a successful without waiting for the page to become `>503px`.<br>This is important when your page has effects (for example, animations, hovers) that slightly change the page size and this needs to be compensated. |

### WindowScroller

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-scroll-manager",
"version": "1.0.3",
"version": "1.1.0",
"description": "Scroll position manager for React applications",
"main": "lib/index.js",
"files": [
Expand Down
50 changes: 35 additions & 15 deletions src/ScrollManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const debug = require('debug')('ScrollManager');
const ManagerContext = React.createContext();

const defaultTimeout = 3000;
// const defaultSkippingSaveDeferredScroll = true;
const defaultBlockSizeTolerance = 0;

export class ScrollManager extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -136,15 +138,23 @@ export class ScrollManager extends React.Component {

_savePosition(scrollKey, position) {
debug('save', this._locationKey, scrollKey, position);
if (!(scrollKey in this._deferredNodes)) {
let loc = this._positions[this._locationKey];
if (!loc) {
loc = this._positions[this._locationKey] = {};
}
loc[scrollKey] = position;
} else {
debug(`Skipping save due to deferred scroll of ${scrollKey}`);
// const { skippingSaveDeferredScroll = defaultSkippingSaveDeferredScroll } = this.props;

// if (!(!(scrollKey in this._deferredNodes) && skippingSaveDeferredScroll)) {
if (scrollKey in this._deferredNodes) {
this._cancelDeferred(scrollKey);
}
let loc = this._positions[this._locationKey];
if (!loc) {
loc = this._positions[this._locationKey] = {};
}
loc[scrollKey] = position;
///////////////////////////////////////////////////////////////////////////////
// No need this because since now there are no false positives on save calls //
///////////////////////////////////////////////////////////////////////////////
// } else {
// debug(`Skipping save due to deferred scroll of ${scrollKey}`);
// }
}

_loadPosition(scrollKey) {
Expand All @@ -155,14 +165,19 @@ export class ScrollManager extends React.Component {
_restoreNode(scrollKey) {
const position = this._loadPosition(scrollKey);
const { scrollLeft = 0, scrollTop = 0 } = position || {};
const { blockSizeTolerance = defaultBlockSizeTolerance } = this.props;
debug('restore', this._locationKey, scrollKey, scrollLeft, scrollTop);

this._cancelDeferred(scrollKey);
const node = this._scrollableNodes[scrollKey];
const attemptScroll = () => {
node.scrollLeft = scrollLeft;
node.scrollTop = scrollTop;
return node.scrollLeft === scrollLeft && node.scrollTop === scrollTop;
const availableScrollX = node.scrollWidth - node.clientWidth + blockSizeTolerance;
const availableScrollY = node.scrollHeight - node.clientHeight + blockSizeTolerance;
if (availableScrollX >= scrollX && availableScrollY >= scrollY) {
node.scrollLeft = scrollLeft;
node.scrollTop = scrollTop;
}
return node.scrollLeft + blockSizeTolerance >= scrollLeft && node.scrollTop + blockSizeTolerance >= scrollTop;
};
if (!attemptScroll()) {
const failedScroll = () => {
Expand All @@ -186,17 +201,20 @@ export class ScrollManager extends React.Component {
const scrollKey = 'window';
const position = this._loadPosition(scrollKey);
const { scrollX = 0, scrollY = 0 } = position || {};
const { blockSizeTolerance = defaultBlockSizeTolerance } = this.props;
debug('restore', this._locationKey, scrollKey, scrollX, scrollY);

this._cancelDeferred(scrollKey);
const attemptScroll = () => {
window.scrollTo(scrollX, scrollY);
return window.pageXOffset === scrollX && window.pageYOffset === scrollY;
const availableScrollX = document.documentElement.scrollWidth - document.documentElement.clientWidth + blockSizeTolerance;
const availableScrollY = document.documentElement.scrollHeight - document.documentElement.clientHeight + blockSizeTolerance;
if (availableScrollX >= scrollX && availableScrollY >= scrollY) window.scrollTo(scrollX, scrollY);
return window.pageXOffset + blockSizeTolerance >= scrollX && window.pageYOffset + blockSizeTolerance >= scrollY;
};
if (!attemptScroll()) {
const failedScroll = () => {
debug(`Could not scroll ${scrollKey} to (${scrollX}, ${scrollY})` +
`; scroll size is (${document.body.scrollWidth}, ${document.body.scrollHeight})`);
`; scroll size is (${document.documentElement.scrollWidth}, ${document.documentElement.scrollHeight})`);
};

const { timeout = defaultTimeout } = this.props;
Expand Down Expand Up @@ -231,7 +249,9 @@ ScrollManager.propTypes = {
history: PropTypes.object.isRequired,
sessionKey: PropTypes.string,
timeout: PropTypes.number,
children: PropTypes.node
children: PropTypes.node,
// skippingSaveDeferredScroll: PropTypes.bool,
blockSizeTolerance: PropTypes.number
};

export function withManager(Component) {
Expand Down
2 changes: 2 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export interface ScrollManagerProps {
history: History;
sessionKey?: string;
timeout?: number;
// skippingSaveDeferredScroll?: boolean;
blockSizeTolerance?: number;
}

export class ScrollManager extends React.Component<ScrollManagerProps> { }
Expand Down
7 changes: 7 additions & 0 deletions test/ScrollManager.stored.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ window.sessionStorage.setItem('scroll', JSON.stringify({
locationKey
}));

beforeEach(() => {
jest.spyOn(document.documentElement, 'scrollHeight', 'get').mockImplementation(() => 500);
jest.spyOn(document.documentElement, 'clientHeight', 'get').mockImplementation(() => 250);
jest.spyOn(document.documentElement, 'scrollWidth', 'get').mockImplementation(() => 500);
jest.spyOn(document.documentElement, 'clientWidth', 'get').mockImplementation(() => 250);
});

test('Stored positioning', () => {
const history = createHistory();
const tree = renderer.create(
Expand Down
16 changes: 16 additions & 0 deletions test/mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,21 @@ export const mockElement = Object.create(null, {
enumerable: true,
get: getScrollTop,
set: setScrollTop
},
scrollWidth: {
enumerable: true,
get: jest.fn(() => 500)
},
clientWidth: {
enumerable: true,
get: jest.fn(() => 250)
},
scrollHeight: {
enumerable: true,
get: jest.fn(() => 500)
},
clientHeight: {
enumerable: true,
get: jest.fn(() => 250)
}
});