Skip to content

Commit 1fa3dd1

Browse files
authored
Fix Firefox scroll position may be locked occasionally (#12)
* Fix Firefox scroll position locking * Remove console.log * Clean up * Update PR number * Typo
1 parent 60cfde3 commit 1fa3dd1

File tree

2 files changed

+29
-8
lines changed

2 files changed

+29
-8
lines changed

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Changed
9+
- `Composer`: fix #13, user scrolling in Firefox may have the scroll position locked occasionally, in PR [#12](https://github.com/compulim/react-scroll-to-bottom/issues/12)
810

911
## [1.3.0] - 2019-01-21
1012
### Changed
1113
- Playground: bumped to `[email protected]`, `[email protected]`, and `[email protected]`
12-
- Update algorithm, instead of using `componentDidUpdate`, we now use `setInterval` to check if the panel is sticky or not, this help to track content update that happen outside of React lifecycle, for example, `HTMLImageElement.onload` event
13-
- `scrollTo()` now accepts `"100%"` instead of `"bottom"`
14+
- `*`: Update algorithm, instead of using `componentDidUpdate`, we now use `setInterval` to check if the panel is sticky or not, this help to track content update that happen outside of React lifecycle, for example, `HTMLImageElement.onload` event
15+
- `Composer`: `scrollTo()` now accepts `"100%"` instead of `"bottom"`
1416

1517
### Removed
1618
- Removed `threshold` props because the algorithm is now more robust

packages/component/src/ScrollToBottom/Composer.js

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import InternalContext from './InternalContext';
99
import SpineTo from '../SpineTo';
1010
import StateContext from './StateContext';
1111

12-
const MIN_CHECK_INTERVAL = 17;
12+
const MIN_CHECK_INTERVAL = 17; // 1 frame
13+
const SCROLL_DECISION_DURATION = 34; // 2 frames
1314

1415
function setImmediateInterval(fn, ms) {
1516
fn();
@@ -85,15 +86,33 @@ export default class Composer extends React.Component {
8586
enableWorker() {
8687
clearInterval(this._stickyCheckTimeout);
8788

89+
let stickyButNotAtEndSince = false;
90+
8891
this._stickyCheckTimeout = setImmediateInterval(
8992
() => {
9093
const { state } = this;
9194
const { stateContext: { sticky }, target } = state;
9295

93-
if (sticky && target) {
94-
const { atEnd } = computeViewState(state);
95-
96-
!atEnd && state.functionContext.scrollToEnd();
96+
if (
97+
sticky
98+
&& target
99+
&& !computeViewState(state).atEnd
100+
) {
101+
if (!stickyButNotAtEndSince) {
102+
stickyButNotAtEndSince = Date.now();
103+
} else if (Date.now() - stickyButNotAtEndSince > SCROLL_DECISION_DURATION) {
104+
// Quirks: In Firefox, after user scroll down, Firefox do two things:
105+
// 1. Set to a new "scrollTop"
106+
// 2. Fire "scroll" event
107+
// For what we observed, #1 is fired about 20ms before #2. There is a chance that this stickyCheckTimeout is being scheduled between 1 and 2.
108+
// That means, if we just look at #1 to decide if we should scroll, we will always scroll, in oppose to the user's intention.
109+
// Repro: Open Firefox, set checkInterval to a lower number, and try to scroll by dragging the scroll handler. It will jump back.
110+
111+
state.functionContext.scrollToEnd();
112+
stickyButNotAtEndSince = false;
113+
}
114+
} else {
115+
stickyButNotAtEndSince = false;
97116
}
98117
},
99118
Math.max(MIN_CHECK_INTERVAL, this.props.checkInterval) || MIN_CHECK_INTERVAL
@@ -207,7 +226,7 @@ export default class Composer extends React.Component {
207226
}
208227

209228
Composer.defaultProps = {
210-
checkInterval: 150,
229+
checkInterval: 100,
211230
debounce: 17
212231
};
213232

0 commit comments

Comments
 (0)