Skip to content

Add scroller prop to programmatically pause scrolling and initialScrollBehavior #73

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

Merged
merged 18 commits into from
Jan 4, 2021
Merged
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: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added

- 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)
- 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)
- Added `debug` prop for dumping debug log to console, in PR [#73](https://github.com/compulim/react-scroll-to-bottom/pull/73)
- Improved performance by separating `StateContext` into 2 tiers, in PR [#73](https://github.com/compulim/react-scroll-to-bottom/pull/73)

### Fixed

- Emptying container should regain stickiness, in PR [#73](https://github.com/compulim/react-scroll-to-bottom/pull/73)

## [4.0.0] - 2020-09-01

### Added
Expand Down
75 changes: 66 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,18 @@ export default props => (

## Props

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

## Hooks

Expand Down Expand Up @@ -369,6 +372,60 @@ export default () => (
);
```

## Observing scroll position

You can use `useObserveScrollPosition` to listen to scroll change.

```js
// This is the content rendered inside the scrollable container
const ScrollContent = () => {
const observer = useCallback(({ scrollTop }) => {
console.log(scrollTop);
}, []);

useObserveScrollPosition(observer);

return <div>Hello, World!</div>;
};
```

> If you want to turn off the hook, in the render call, pass a falsy value, e.g. `useObserveScrollPosition(false)`.

Please note that the observer will called very frequently, it is recommended:

- Only observe the scroll position when needed
- Don't put too much logic inside the callback function
- If logic is needed, consider deferring handling using `setTimeout` or similar functions
- Make sure the callback function passed on each render call is memoized appropriately, e.g. `useCallback`

For best practices on handling `scroll` event, please read [this article](https://developer.mozilla.org/en-US/docs/Web/API/Document/scroll_event).

## Programmatically pausing scroll

> This only works when `mode` prop is set to `bottom` (default).

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:

```js
scroller({ maxValue, minValue, offsetHeight, scrollHeight, scrollTop }) => number;
```

| Argument | Type | Description |
| -------------- | -------- | ------------------------------------------------------------------------------------------ |
| `maxValue` | `number` | Maximum distance (in pixel) to scroll |
| `minValue` | `number` | Minimum distance (in pixel) to scroll, see notes below |
| `offsetHeight` | `number` | View height of the scrollable container |
| `scrollHeight` | `number` | Total height of the content in the container, must be equal or greater than `offsetHeight` |
| `scrollTop` | `number` | Current scroll position (in pixel) |

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.

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.

By default, the `scroller` function will returns `Infinity`. When new content is added, it will scroll all the way to the bottom.

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.

# Security

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:
Expand Down
90 changes: 90 additions & 0 deletions TESTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Manual tests

## Quirks

These are tests for regressions.

Assumptions:

- The container size is `500px`
- Each element size is default at `100px` (unless specified)
- The container contains 10 elements and is sticky

### Add elements quickly

> Press and hold <kbd>1</kbd> in playground for a few seconds.

- [ ] Add 20+ elements very quickly
- [ ] Test it again on Firefox

Expect:

- It should not lose stickiness
- During elements add, it should not lose stickiness for a split second
- In playground, it should not turn pink at any moments

### Scroller

- [ ] Set a scroller of `100px`
- [ ] Add 1 element of `50px`
- [ ] Add another element of `200px` very quickly after the previous one
- Preferably, use `requestAnimationFrame`

Expect:

- It should stop at 100px

### Resizing container

> Press <kbd>4</kbd> <kbd>1</kbd> <kbd>5</kbd> <kbd>1</kbd> <kbd>1</kbd> in the playground.

- [ ] Change the container size to `200px`
- [ ] Add an element
- [ ] Change the container size back to `500px`
- [ ] Add 2 elements

Expect:

- It should not lose stickiness during the whole test

### Focusing to an interactive element

- [ ] Add 10 elements
- [ ] Scroll to top (losing stickiness)
- [ ] Add a `<button>` to the very bottom
- [ ] Press <kbd>TAB</kbd> to focus on the button

Expect:

- Browser will scroll down to show the button
- It should become sticky as it is on the bottom of the screen now

### Scroll to pause scrolling

- [ ] Set a scroller of `0px`
- [ ] Add an element

Expect:

- It should lose stickiness after adding an element

### Emptying the container

- [ ] Scroll up and lose stickiness
- [ ] Clear the container
- [ ] Add 10 elements

Expect:

- It should retain stickiness after the container is emptied

### Scroll to bottom while at bottom

- [ ] Show command bar
- [ ] Scroll to bottom, it should gain stickiness
- [ ] Uncheck "Smooth" checkbox
- [ ] Click the "Scroll to end" button multiple times

Expect:

- It should not lose stickiness after clicking on the "Scroll to end" button multiple times
17 changes: 16 additions & 1 deletion packages/component/src/BasicScrollToBottom.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,24 @@ const BasicScrollToBottom = ({
children,
className,
debounce,
debug,
followButtonClassName,
initialScrollBehavior,
mode,
nonce,
scroller,
scrollViewClassName
}) => {
return (
<Composer checkInterval={checkInterval} debounce={debounce} mode={mode} nonce={nonce}>
<Composer
checkInterval={checkInterval}
debounce={debounce}
debug={debug}
initialScrollBehavior={initialScrollBehavior}
mode={mode}
nonce={nonce}
scroller={scroller}
>
<BasicScrollToBottomCore
className={className}
followButtonClassName={followButtonClassName}
Expand All @@ -64,7 +75,9 @@ BasicScrollToBottom.defaultProps = {
children: undefined,
className: undefined,
debounce: undefined,
debug: false,
followButtonClassName: undefined,
initialScrollBehavior: 'smooth',
mode: undefined,
nonce: undefined,
scrollViewClassName: undefined
Expand All @@ -75,7 +88,9 @@ BasicScrollToBottom.propTypes = {
children: PropTypes.any,
className: PropTypes.string,
debounce: PropTypes.number,
debug: PropTypes.bool,
followButtonClassName: PropTypes.string,
initialScrollBehavior: PropTypes.oneOf(['auto', 'smooth']),
mode: PropTypes.oneOf(['bottom', 'top']),
nonce: PropTypes.string,
scrollViewClassName: PropTypes.string
Expand Down
Loading