Skip to content

Commit 3ea0392

Browse files
authored
Add scroller prop to programmatically pause scrolling and initialScrollBehavior (#73)
* Add scroller * Fix ESLint * Add test plan * Update test plan * Fix emptying container * Fix losing stickiness in auto behavior * Add initialScrollBehavior * Add scroller and initialScrollBehavior * Typo * Add debug prop * Performance and debug prop * Fix ESLint * Add useObserveScrollPosition section * Typo * Remove version number * Add PR number * Add combined StateContext * Typo
1 parent e49ce60 commit 3ea0392

22 files changed

+902
-253
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Added `scroller` prop for limiting scroll distance when `mode` is set to `bottom`, in PR [#73](https://github.com/compulim/react-scroll-to-bottom/pull/73)
13+
- Added `initialScrollBehavior` prop for first scroll behavior. When set to `"auto"` (discrete scrolling), it will jump to end on initialization. in PR [#73](https://github.com/compulim/react-scroll-to-bottom/pull/73)
14+
- Added `debug` prop for dumping debug log to console, in PR [#73](https://github.com/compulim/react-scroll-to-bottom/pull/73)
15+
- Improved performance by separating `StateContext` into 2 tiers, in PR [#73](https://github.com/compulim/react-scroll-to-bottom/pull/73)
16+
17+
### Fixed
18+
19+
- Emptying container should regain stickiness, in PR [#73](https://github.com/compulim/react-scroll-to-bottom/pull/73)
20+
1021
## [4.0.0] - 2020-09-01
1122

1223
### Added

README.md

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,18 @@ export default props => (
6262
6363
## Props
6464

65-
| Name | Type | Default | Description |
66-
| ----------------------- | -------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------ |
67-
| `checkInterval` | `number` | 150 | Recurring interval of stickiness check, in milliseconds (minimum is 17 ms) |
68-
| `className` | `string` | | Set the class name for the root element |
69-
| `debounce` | `number` | `17` | Set the debounce for tracking the `onScroll` event |
70-
| `followButtonClassName` | `string` | | Set the class name for the follow button |
71-
| `mode` | `string` | `"bottom"` | Set it to `"bottom"` for scroll-to-bottom, `"top"` for scroll-to-top |
72-
| `nonce` | `string` | | Set the nonce for [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) |
73-
| `scrollViewClassName` | `string` | | Set the class name for the container element that house all `props.children` |
65+
| Name | Type | Default | Description |
66+
| ----------------------- | ---------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------ |
67+
| `checkInterval` | `number` | 150 | Recurring interval of stickiness check, in milliseconds (minimum is 17 ms) |
68+
| `className` | `string` | | Set the class name for the root element |
69+
| `debounce` | `number` | `17` | Set the debounce for tracking the `onScroll` event |
70+
| `debug` | `bool` | false | Show debug information in console |
71+
| `followButtonClassName` | `string` | | Set the class name for the follow button |
72+
| `initialScrollBehavior` | `string` | `smooth` | Set the initial scroll behavior, either `"auto"` (discrete scrolling) or `"smooth"` |
73+
| `mode` | `string` | `"bottom"` | Set it to `"bottom"` for scroll-to-bottom, `"top"` for scroll-to-top |
74+
| `nonce` | `string` | | Set the nonce for [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) |
75+
| `scroller` | `function` | `() => Infinity` | A function to determine how far should scroll when scroll is needed |
76+
| `scrollViewClassName` | `string` | | Set the class name for the container element that house all `props.children` |
7477

7578
## Hooks
7679

@@ -369,6 +372,60 @@ export default () => (
369372
);
370373
```
371374

375+
## Observing scroll position
376+
377+
You can use `useObserveScrollPosition` to listen to scroll change.
378+
379+
```js
380+
// This is the content rendered inside the scrollable container
381+
const ScrollContent = () => {
382+
const observer = useCallback(({ scrollTop }) => {
383+
console.log(scrollTop);
384+
}, []);
385+
386+
useObserveScrollPosition(observer);
387+
388+
return <div>Hello, World!</div>;
389+
};
390+
```
391+
392+
> If you want to turn off the hook, in the render call, pass a falsy value, e.g. `useObserveScrollPosition(false)`.
393+
394+
Please note that the observer will called very frequently, it is recommended:
395+
396+
- Only observe the scroll position when needed
397+
- Don't put too much logic inside the callback function
398+
- If logic is needed, consider deferring handling using `setTimeout` or similar functions
399+
- Make sure the callback function passed on each render call is memoized appropriately, e.g. `useCallback`
400+
401+
For best practices on handling `scroll` event, please read [this article](https://developer.mozilla.org/en-US/docs/Web/API/Document/scroll_event).
402+
403+
## Programmatically pausing scroll
404+
405+
> This only works when `mode` prop is set to `bottom` (default).
406+
407+
You can pass a function to the `scroller` prop to customize how far the scrollable should animate/scroll (in pixel) when its content changed. The signature of the scroller function is:
408+
409+
```js
410+
scroller({ maxValue, minValue, offsetHeight, scrollHeight, scrollTop }) => number;
411+
```
412+
413+
| Argument | Type | Description |
414+
| -------------- | -------- | ------------------------------------------------------------------------------------------ |
415+
| `maxValue` | `number` | Maximum distance (in pixel) to scroll |
416+
| `minValue` | `number` | Minimum distance (in pixel) to scroll, see notes below |
417+
| `offsetHeight` | `number` | View height of the scrollable container |
418+
| `scrollHeight` | `number` | Total height of the content in the container, must be equal or greater than `offsetHeight` |
419+
| `scrollTop` | `number` | Current scroll position (in pixel) |
420+
421+
Note: the `scroller` function will get called when the scrollable is sticky and the content size change. If the scrollable is not sticky, the function will not be called as animation is not needed.
422+
423+
When the scrollable is animating, if there are new contents added to the scrollable, the `scroller` function will get called again with `minValue` set to the current position. The `minValue` means how far the animation has already scrolled.
424+
425+
By default, the `scroller` function will returns `Infinity`. When new content is added, it will scroll all the way to the bottom.
426+
427+
You can return a different value (in number) to indicates how far you want to scroll when the content has changed. If you return `0`, the scrollable will stop scrolling for any new content. Returning any values less than `maxValue` will make the scrollable to lose its stickiness after animation. After the scrollable lose its stickiness, the `scroller` function will not be called again for any future content change, until the scrollable regains its stickiness.
428+
372429
# Security
373430

374431
We support nonce-based [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy). To enable, the following directive is required:

TESTS.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Manual tests
2+
3+
## Quirks
4+
5+
These are tests for regressions.
6+
7+
Assumptions:
8+
9+
- The container size is `500px`
10+
- Each element size is default at `100px` (unless specified)
11+
- The container contains 10 elements and is sticky
12+
13+
### Add elements quickly
14+
15+
> Press and hold <kbd>1</kbd> in playground for a few seconds.
16+
17+
- [ ] Add 20+ elements very quickly
18+
- [ ] Test it again on Firefox
19+
20+
Expect:
21+
22+
- It should not lose stickiness
23+
- During elements add, it should not lose stickiness for a split second
24+
- In playground, it should not turn pink at any moments
25+
26+
### Scroller
27+
28+
- [ ] Set a scroller of `100px`
29+
- [ ] Add 1 element of `50px`
30+
- [ ] Add another element of `200px` very quickly after the previous one
31+
- Preferably, use `requestAnimationFrame`
32+
33+
Expect:
34+
35+
- It should stop at 100px
36+
37+
### Resizing container
38+
39+
> Press <kbd>4</kbd> <kbd>1</kbd> <kbd>5</kbd> <kbd>1</kbd> <kbd>1</kbd> in the playground.
40+
41+
- [ ] Change the container size to `200px`
42+
- [ ] Add an element
43+
- [ ] Change the container size back to `500px`
44+
- [ ] Add 2 elements
45+
46+
Expect:
47+
48+
- It should not lose stickiness during the whole test
49+
50+
### Focusing to an interactive element
51+
52+
- [ ] Add 10 elements
53+
- [ ] Scroll to top (losing stickiness)
54+
- [ ] Add a `<button>` to the very bottom
55+
- [ ] Press <kbd>TAB</kbd> to focus on the button
56+
57+
Expect:
58+
59+
- Browser will scroll down to show the button
60+
- It should become sticky as it is on the bottom of the screen now
61+
62+
### Scroll to pause scrolling
63+
64+
- [ ] Set a scroller of `0px`
65+
- [ ] Add an element
66+
67+
Expect:
68+
69+
- It should lose stickiness after adding an element
70+
71+
### Emptying the container
72+
73+
- [ ] Scroll up and lose stickiness
74+
- [ ] Clear the container
75+
- [ ] Add 10 elements
76+
77+
Expect:
78+
79+
- It should retain stickiness after the container is emptied
80+
81+
### Scroll to bottom while at bottom
82+
83+
- [ ] Show command bar
84+
- [ ] Scroll to bottom, it should gain stickiness
85+
- [ ] Uncheck "Smooth" checkbox
86+
- [ ] Click the "Scroll to end" button multiple times
87+
88+
Expect:
89+
90+
- It should not lose stickiness after clicking on the "Scroll to end" button multiple times

packages/component/src/BasicScrollToBottom.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,24 @@ const BasicScrollToBottom = ({
4141
children,
4242
className,
4343
debounce,
44+
debug,
4445
followButtonClassName,
46+
initialScrollBehavior,
4547
mode,
4648
nonce,
49+
scroller,
4750
scrollViewClassName
4851
}) => {
4952
return (
50-
<Composer checkInterval={checkInterval} debounce={debounce} mode={mode} nonce={nonce}>
53+
<Composer
54+
checkInterval={checkInterval}
55+
debounce={debounce}
56+
debug={debug}
57+
initialScrollBehavior={initialScrollBehavior}
58+
mode={mode}
59+
nonce={nonce}
60+
scroller={scroller}
61+
>
5162
<BasicScrollToBottomCore
5263
className={className}
5364
followButtonClassName={followButtonClassName}
@@ -64,7 +75,9 @@ BasicScrollToBottom.defaultProps = {
6475
children: undefined,
6576
className: undefined,
6677
debounce: undefined,
78+
debug: false,
6779
followButtonClassName: undefined,
80+
initialScrollBehavior: 'smooth',
6881
mode: undefined,
6982
nonce: undefined,
7083
scrollViewClassName: undefined
@@ -75,7 +88,9 @@ BasicScrollToBottom.propTypes = {
7588
children: PropTypes.any,
7689
className: PropTypes.string,
7790
debounce: PropTypes.number,
91+
debug: PropTypes.bool,
7892
followButtonClassName: PropTypes.string,
93+
initialScrollBehavior: PropTypes.oneOf(['auto', 'smooth']),
7994
mode: PropTypes.oneOf(['bottom', 'top']),
8095
nonce: PropTypes.string,
8196
scrollViewClassName: PropTypes.string

0 commit comments

Comments
 (0)