Skip to content

Add support for scrollToTransition prop #28

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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ render(
| scrollOffset | Number | | Can be used to control the scroll offset; Also useful for setting an initial scroll offset |
| scrollToIndex | Number | | Item index to scroll to (by forcefully scrolling if necessary) |
| scrollToAlignment | String | | Used in combination with `scrollToIndex`, this prop controls the alignment of the scrolled to item. One of: `'start'`, `'center'`, `'end'` or `'auto'`. Use `'start'` to always align items to the top of the container and `'end'` to align them bottom. Use `'center`' to align them in the middle of the container. `'auto'` scrolls the least amount possible to ensure that the specified `scrollToIndex` item is fully visible. |
| scrollToTransition | String | | Used in combination with `scrollToIndex`, if passed this prop adds a transition when `scrollToIndex` changes. Use the same syntax as the CSS `transition` property. Do not specify a property. Example transitions: `200ms ease`, `1s cubic-bezier(0.4, 0.0, 0.2, 1)` |
| overscanCount | Number | | Number of extra buffer items to render above/below the visible items. Tweaking this can help reduce scroll flickering on certain browsers/devices. |
| estimatedItemSize | Number | | Used to estimate the total size of the list before all of its items have actually been measured. The estimated total height is progressively adjusted as items are rendered. |
| onItemsRendered | Function | | Callback invoked with information about the slice of rows/columns that were just rendered. It has the following signature: `({startIndex: number, stopIndex: number})`. |
Expand Down
5 changes: 5 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,8 @@ export const positionProp = {
[DIRECTION_VERTICAL]: 'top',
[DIRECTION_HORIZONTAL]: 'left',
};

export const transformProp = {
[DIRECTION_VERTICAL]: 'translateY',
[DIRECTION_HORIZONTAL]: 'translateX',
};
80 changes: 74 additions & 6 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
positionProp,
scrollProp,
sizeProp,
transformProp
} from './constants';

const STYLE_WRAPPER: React.CSSProperties = {
Expand Down Expand Up @@ -70,6 +71,7 @@ export interface Props {
scrollToIndex?: number,
scrollToAlignment?: ALIGNMENT,
scrollDirection?: DIRECTION,
scrollToTransition?: string,
style?: any,
width?: number | string,
onItemsRendered?({startIndex, stopIndex}: RenderedRows): void,
Expand Down Expand Up @@ -100,6 +102,7 @@ export default class VirtualList extends React.PureComponent<Props, State> {
scrollOffset: PropTypes.number,
scrollToIndex: PropTypes.number,
scrollToAlignment: PropTypes.oneOf([ALIGN_AUTO, ALIGN_START, ALIGN_CENTER, ALIGN_END]),
scrollToTransition: PropTypes.string,
scrollDirection: PropTypes.oneOf([DIRECTION_HORIZONTAL, DIRECTION_VERTICAL]).isRequired,
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
};
Expand All @@ -120,16 +123,17 @@ export default class VirtualList extends React.PureComponent<Props, State> {
};

private rootNode: HTMLElement;
private innerNode: HTMLElement;

private styleCache: StyleCache = {};

componentDidMount() {
const {scrollOffset, scrollToIndex} = this.props;

if (scrollOffset != null) {
this.scrollTo(scrollOffset);
this.scrollTo(scrollOffset, true);
} else if (scrollToIndex != null) {
this.scrollTo(this.getOffsetForIndex(scrollToIndex));
this.scrollTo(this.getOffsetForIndex(scrollToIndex), true);
}
}

Expand Down Expand Up @@ -218,9 +222,59 @@ export default class VirtualList extends React.PureComponent<Props, State> {
return this.rootNode[scrollProp[scrollDirection]];
}

scrollTo(value: number) {
scrollTo(value: number, skipTransition: boolean = false) {
const {scrollDirection = DIRECTION_VERTICAL} = this.props;

// We use the FLIP technique to animate the scroll change.
// See https://aerotwist.com/blog/flip-your-animations/ for more info.

// Get the element's rect which will be used to determine how far the list
// has scrolled once the scroll position has been set
const preScrollRect = this.innerNode.getBoundingClientRect();

// Scroll to the right position
this.rootNode[scrollProp[scrollDirection]] = value;

// Return early and perform no animation if forced, or no transition has
// been passed
if (
skipTransition ||
this.props.scrollToTransition === undefined ||
this.innerNode.style.transition !== ''
) {
return;
}

// The rect of the element after being scrolled lets us calculate the
// distance it has travelled
const postScrollRect = this.innerNode.getBoundingClientRect();

const delta = preScrollRect[positionProp[scrollDirection]] - postScrollRect[positionProp[scrollDirection]];

// Set `translateX` or `translateY` (depending on the scroll direction) in
// order to move the element back to the original position before scrolling
this.innerNode.style.transform = `${transformProp[scrollDirection]}(${delta}px)`;

// Wait for the next frame, then add a transition to the element and move it
// back to its current position. This makes the browser animate the
// transform as if the element moved from its location pre-scroll to its
// final location.
requestAnimationFrame(() => {
this.innerNode.style.transition = this.props.scrollToTransition || '';
this.innerNode.style.transitionProperty = 'transform';

this.innerNode.style.transform = '';
});

// We listen to the end of the transition in order to perform some cleanup
const reset = () => {
this.innerNode.style.transition = '';
this.innerNode.style.transitionProperty = '';

this.innerNode.removeEventListener('transitionend', reset);
}

this.innerNode.addEventListener('transitionend', reset);
}

getOffsetForIndex(index: number, scrollToAlignment = this.props.scrollToAlignment, itemCount: number = this.props.itemCount): number {
Expand Down Expand Up @@ -278,6 +332,7 @@ export default class VirtualList extends React.PureComponent<Props, State> {
onItemsRendered,
onScroll,
scrollDirection = DIRECTION_VERTICAL,
scrollToTransition,
scrollOffset,
scrollToIndex,
scrollToAlignment,
Expand Down Expand Up @@ -310,15 +365,28 @@ export default class VirtualList extends React.PureComponent<Props, State> {
}

return (
<div ref={this.getRef} {...props} onScroll={this.handleScroll} style={{...STYLE_WRAPPER, ...style, height, width}}>
<div style={{...STYLE_INNER, [sizeProp[scrollDirection]]: this.sizeAndPositionManager.getTotalSize()}}>
<div ref={this.getRootNodeRef} {...props} onScroll={this.handleScroll} style={{...STYLE_WRAPPER, ...style, height, width}}>
<div
ref={this.getInnerNodeRef}
style={{
...STYLE_INNER,
willChange: scrollToTransition !== undefined ? 'transform' : null,
[sizeProp[
scrollDirection
]]: this.sizeAndPositionManager.getTotalSize()
}}
>
{items}
</div>
</div>
);
}

private getRef = (node: HTMLDivElement): void => {
private getRootNodeRef = (node: HTMLDivElement): void => {
this.rootNode = node;
}

private getInnerNodeRef = (node: HTMLDivElement): void => {
this.innerNode = node;
}
}