Skip to content

Commit d0df2f1

Browse files
authored
Differentiate Chrome-crafted scroll events from user-initiated events (#24)
* Move to React hooks and add resize buttons * Use useEffect instead of useMemo * Fix add paragraph * Fix losing stickiness with Chrome-crafted scroll events
1 parent e58495d commit d0df2f1

File tree

8 files changed

+8077
-12273
lines changed

8 files changed

+8077
-12273
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
66

77
## [Unreleased]
88
### Changed
9-
- `*`: Bump `babel-jest@24.8.0`, `lerna@3.15.0`, and `jest@24.8.0`, in PR [#22](https://github.com/compulim/react-scroll-to-bottom/pull/22)
9+
- `*`: bumped to `babel-jest@24.8.0`, `lerna@3.15.0`, and `jest@24.8.0`, in PR [#22](https://github.com/compulim/react-scroll-to-bottom/pull/22)
10+
### Fixed
11+
- `Composer`: fix [#22](https://github.com/compulim/react-scroll-to-bottom/issue/22), synthetic `scroll` events crafted by Chrome should not cause stickiness to lose, in PR [#23](https://github.com/compulim/react-scroll-to-bottom/issue/23)
1012

1113
## [1.3.1] - 2019-02-13
1214
### Changed

packages/component/src/ScrollToBottom/Composer.js

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ export default class Composer extends React.Component {
6060
scrollToTop: () => this.state.functionContext.scrollTo(0)
6161
},
6262
internalContext: {
63+
offsetHeight: 0,
64+
scrollHeight: 0,
6365
setTarget: target => this.setState(() => ({ target }))
6466
},
6567
scrollTop: props.mode === 'top' ? 0 : '100%',
@@ -151,29 +153,51 @@ export default class Composer extends React.Component {
151153
const { target } = state;
152154

153155
if (target) {
154-
const { scrollTop, stateContext } = state;
156+
const { internalContext, scrollTop, stateContext } = state;
155157
const { atBottom, atEnd, atStart, atTop } = computeViewState(state);
158+
let nextInternalContext = internalContext;
156159
let nextStateContext = stateContext;
157160

158161
nextStateContext = updateIn(nextStateContext, ['atBottom'], () => atBottom);
159162
nextStateContext = updateIn(nextStateContext, ['atEnd'], () => atEnd);
160163
nextStateContext = updateIn(nextStateContext, ['atStart'], () => atStart);
161164
nextStateContext = updateIn(nextStateContext, ['atTop'], () => atTop);
162165

166+
// Chrome will emit "synthetic" scroll event if the container is resized or an element is added
167+
// We need to ignore these "synthetic" events
168+
// Repro: In playground, press 4-1-5-1-1 (small, add one, normal, add one, add one)
169+
// Nomatter how fast or slow the sequence is being presssed, it should still stick to the bottom
170+
const { offsetHeight, scrollHeight } = target;
171+
const resized = offsetHeight !== internalContext.offsetHeight;
172+
const elementChanged = scrollHeight !== internalContext.scrollHeight;
173+
174+
if (resized) {
175+
nextInternalContext = updateIn(nextInternalContext, ['offsetHeight'], () => offsetHeight);
176+
}
177+
178+
if (elementChanged) {
179+
nextInternalContext = updateIn(nextInternalContext, ['scrollHeight'], () => scrollHeight);
180+
}
181+
163182
// Sticky means:
164183
// - If it is scrolled programatically, we are still in sticky mode
165184
// - If it is scrolled by the user, then sticky means if we are at the end
166-
nextStateContext = updateIn(nextStateContext, ['sticky'], () => stateContext.animating ? true : atEnd);
185+
186+
// Only update stickiness if the scroll event is not due to synthetic scroll done by Chrome
187+
if (!resized && !elementChanged) {
188+
nextStateContext = updateIn(nextStateContext, ['sticky'], () => stateContext.animating ? true : atEnd);
189+
}
167190

168191
// If no scrollTop is set (not in programmatic scrolling mode), we should set "animating" to false
169192
// "animating" is used to calculate the "sticky" property
170193
if (scrollTop === null) {
171194
nextStateContext = updateIn(nextStateContext, ['animating'], () => false);
172195
}
173196

174-
if (stateContext !== nextStateContext) {
175-
return { stateContext: nextStateContext };
176-
}
197+
return {
198+
...internalContext === nextInternalContext ? {} : { internalContext: nextInternalContext },
199+
...stateContext === nextStateContext ? {} : { stateContext: nextStateContext }
200+
};
177201
}
178202
}, () => {
179203
this.state.stateContext.sticky && this.enableWorker();

packages/component/src/ScrollToBottom/FunctionContext.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const context = React.createContext({
44
scrollTo: () => 0,
55
scrollToBottom: () => 0,
66
scrollToEnd: () => 0,
7+
scrollToStart: () => 0,
78
scrollToTop: () => 0
89
});
910

packages/component/src/ScrollToBottom/InternalContext.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import React from 'react';
22

33
const context = React.createContext({
4-
_handleUpdate: () => 0,
5-
_setTarget: () => 0
4+
offsetHeight: 0,
5+
scrollHeight: 0,
6+
setTarget: () => 0
67
});
78

89
context.displayName = 'ScrollToBottomInternalContext';

packages/component/src/ScrollToBottom/StateContext.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import React from 'react';
22

33
const context = React.createContext({
4+
animating: false,
45
atBottom: true,
56
atEnd: true,
67
atTop: true,
7-
mode: 'bottom'
8+
mode: 'bottom',
9+
sticky: true
810
});
911

1012
context.displayName = 'ScrollToBottomStateContext';

0 commit comments

Comments
 (0)