Skip to content
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
64 changes: 64 additions & 0 deletions __tests__/scrolling-threshold.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="/react-scroll-to-bottom.development.js"></script>
<script src="/test-harness.js"></script>
<script src="/assets/page-object-model.js"></script>
</head>
<body>
<div id="app"></div>
</body>
<script type="text/babel" data-presets="react">
'use strict';

run(async function () {
await new Promise(resolve =>
ReactDOM.render(
<ReactScrollToBottom.default
className="react-scroll-to-bottom"
followButtonClassName="follow"
scrollViewClassName="scrollable"
autoScrollThreshold={50}
>
{pageObjects.paragraphs.map(paragraph => (
<p key={paragraph}>{paragraph}</p>
))}
</ReactScrollToBottom.default>,
document.getElementById('app'),
resolve
)
);

await pageObjects.scrollStabilizedAtBottom();

expect(document.getElementsByClassName('follow')[0]).toBeFalsy();

// Scroll up by 40px, which is within the 50px threshold
// Note: mouseWheel(-40) might not translate exactly to 40px pixel scroll depending on implementation
// But typically it should be enough to test "small scroll" vs "large scroll".
// Let's try to set scrollTop directly if possible, but pageObjects is safer.
// Assuming mouseWheel units are pixels or close to it.

// Let's use a explicit scroll to be safe if pageObjects allows,
// otherwise use mouseWheel and hope it maps linearly or use step-by-step.

// Checking pageObjects.scrollStabilized usage in other files...

await pageObjects.mouseWheel(-40);
await pageObjects.scrollStabilized();

// Should still be sticky, so NO follow button
expect(document.getElementsByClassName('follow')[0]).toBeFalsy();

// Scroll up another 20px, total 60px (> 50px)
await pageObjects.mouseWheel(-20);
await pageObjects.scrollStabilized();

// Should show follow button now
expect(document.getElementsByClassName('follow')[0]).toBeTruthy();
});
</script>
</html>
3 changes: 3 additions & 0 deletions __tests__/scrolling-threshold.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/** @jest-environment ./packages/test-harness/JestEnvironment */

test('autoScrollThreshold should keep stickiness when within threshold', () => runHTML('scrolling-threshold.html'));
9 changes: 7 additions & 2 deletions packages/component/src/BasicScrollToBottom.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';

import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';

import AutoHideFollowButton from './ScrollToBottom/AutoHideFollowButton';
import Composer from './ScrollToBottom/Composer';
Expand Down Expand Up @@ -48,9 +49,11 @@ const BasicScrollToBottom = ({
nonce,
scroller,
scrollViewClassName,
styleOptions
styleOptions,
autoScrollThreshold
}) => (
<Composer
autoScrollThreshold={autoScrollThreshold}
checkInterval={checkInterval}
debounce={debounce}
debug={debug}
Expand All @@ -71,6 +74,7 @@ const BasicScrollToBottom = ({
);

BasicScrollToBottom.defaultProps = {
autoScrollThreshold: 1,
checkInterval: undefined,
children: undefined,
className: undefined,
Expand All @@ -86,6 +90,7 @@ BasicScrollToBottom.defaultProps = {
};

BasicScrollToBottom.propTypes = {
autoScrollThreshold: PropTypes.number,
checkInterval: PropTypes.number,
children: PropTypes.any,
className: PropTypes.string,
Expand Down
29 changes: 22 additions & 7 deletions packages/component/src/ScrollToBottom/Composer.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const DEFAULT_SCROLLER = () => Infinity;
const MIN_CHECK_INTERVAL = 17; // 1 frame
const MODE_BOTTOM = 'bottom';
const MODE_TOP = 'top';
const NEAR_END_THRESHOLD = 1;

const SCROLL_DECISION_DURATION = 34; // 2 frames

function setImmediateInterval(fn, ms) {
Expand All @@ -26,9 +26,9 @@ function setImmediateInterval(fn, ms) {
return setInterval(fn, ms);
}

function computeViewState({ mode, target: { offsetHeight, scrollHeight, scrollTop } }) {
const atBottom = scrollHeight - scrollTop - offsetHeight < NEAR_END_THRESHOLD;
const atTop = scrollTop < NEAR_END_THRESHOLD;
function computeViewState({ mode, autoScrollThreshold, target: { offsetHeight, scrollHeight, scrollTop } }) {
const atBottom = scrollHeight - scrollTop - offsetHeight < autoScrollThreshold;
const atTop = scrollTop < autoScrollThreshold;

const atEnd = mode === MODE_TOP ? atTop : atBottom;
const atStart = mode !== MODE_TOP ? atTop : atBottom;
Expand All @@ -46,6 +46,7 @@ function isEnd(animateTo, mode) {
}

const Composer = ({
autoScrollThreshold,
checkInterval,
children,
debounce,
Expand Down Expand Up @@ -325,7 +326,7 @@ const Composer = ({
return;
}

const { atBottom, atEnd, atStart, atTop } = computeViewState({ mode, target });
const { atBottom, atEnd, atStart, atTop } = computeViewState({ mode, autoScrollThreshold, target });

setAtBottom(atBottom);
setAtEnd(atEnd);
Expand Down Expand Up @@ -416,6 +417,7 @@ const Composer = ({
},
[
animateToRef,
autoScrollThreshold,
debug,
ignoreScrollEventBeforeRef,
mode,
Expand All @@ -442,7 +444,7 @@ const Composer = ({
const animating = animateToRef.current !== null;

if (stickyRef.current) {
if (!computeViewState({ mode, target }).atEnd) {
if (!computeViewState({ mode, autoScrollThreshold, target }).atEnd) {
if (!stickyButNotAtEndSince) {
stickyButNotAtEndSince = Date.now();
} else if (Date.now() - stickyButNotAtEndSince > SCROLL_DECISION_DURATION) {
Expand Down Expand Up @@ -495,7 +497,18 @@ const Composer = ({

return () => clearInterval(timeout);
}
}, [animateToRef, checkInterval, debug, mode, scrollToSticky, setSticky, stickyRef, target, targetRef]);
}, [
animateToRef,
autoScrollThreshold,
checkInterval,
debug,
mode,
scrollToSticky,
setSticky,
stickyRef,
target,
targetRef
]);

const emotion = useEmotion(nonce, styleOptions?.stylesRoot);
const styleToClassName = useCallback(style => emotion.css(style) + '', [emotion]);
Expand Down Expand Up @@ -610,6 +623,7 @@ const Composer = ({
};

Composer.defaultProps = {
autoScrollThreshold: 1,
checkInterval: 100,
children: undefined,
debounce: 17,
Expand All @@ -622,6 +636,7 @@ Composer.defaultProps = {
};

Composer.propTypes = {
autoScrollThreshold: PropTypes.number,
checkInterval: PropTypes.number,
children: PropTypes.any,
debounce: PropTypes.number,
Expand Down