Replaced margin top/left hack with scrollBy()

This commit is contained in:
Brian Vaughn
2018-12-07 13:04:43 -08:00
parent 6bab06b526
commit fe88aebbee
2 changed files with 33 additions and 85 deletions
+24 -66
View File
@@ -5,7 +5,7 @@ import { createElement } from 'react';
import createListComponent, { defaultItemKey } from './createListComponent';
import ItemMeasurer from './ItemMeasurer';
import type { Props, ScrollToAlign, State } from './createListComponent';
import type { Props, ScrollToAlign } from './createListComponent';
const DEFAULT_ESTIMATED_ITEM_SIZE = 50;
@@ -277,7 +277,7 @@ const DynamicSizeList = createListComponent({
let sizeDeltaTotal = 0;
// This method is called after mount and update.
instance._commitHook = (prevProps?: Props<any>, prevState?: State) => {
instance._commitHook = () => {
if (hasNewMeasurements) {
hasNewMeasurements = false;
@@ -307,17 +307,10 @@ const DynamicSizeList = createListComponent({
) {
// TRICKY
// If item(s) have changed size since they were last displayed, content will appear to jump.
// To avoid this, we may need to make small adjustments as a user scrolls to preserve apparent position.
// The most intuitive way to do this would be to adjust the scroll offset directly,
// but changing scroll offsets interrupts smooth scrolling for some browser's (e.g. Firefox).
// Instead we temporarily offset item positions by adjusting top/left margin on the container.
// Then after a delay/debounce (once scrolling has stopped) we remove this and adjust the offset.
// If we get to close to offset 0 (or item 0) we'll need to force this update,
// but that seems acceptable since the scrolling animation would be interrupted in that event anyway.
// To avoid this, we need to make small adjustments as a user scrolls to preserve apparent position.
// This also ensures that the first item eventually aligns with scroll offset 0.
return {
scrollOffset: prevState.scrollOffset + sizeDeltaForStateUpdate,
scrollOffsetDelta:
prevState.scrollOffsetDelta + sizeDeltaForStateUpdate,
};
} else {
// There's no state to update,
@@ -330,66 +323,31 @@ const DynamicSizeList = createListComponent({
() => {
if (shouldForceUpdate) {
instance.forceUpdate();
} else {
const { scrollOffset } = instance.state;
const { direction } = instance.props;
// Adjusting scroll offset directly interrupts smooth scrolling for some browsers (e.g. Firefox).
// The relative scrollBy() method doesn't interrupt (or at least it won't as of Firefox v65).
// Other browsers (e.g. Chrome, Safari) seem to handle both adjustments equally well.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1502059
const element = ((instance._outerRef: any): HTMLDivElement);
// $FlowFixMe Property scrollBy is missing in HTMLDivElement
if (typeof element.scrollBy === 'function') {
element.scrollBy(
direction === 'horizontal' ? sizeDeltaForStateUpdate : 0,
direction === 'horizontal' ? 0 : sizeDeltaForStateUpdate
);
} else if (direction === 'horizontal') {
element.scrollLeft = scrollOffset;
} else {
element.scrollTop = scrollOffset;
}
}
sizeDeltaTotal -= sizeDeltaForStateUpdate;
}
);
} else if (prevState != null) {
if (
!instance.state.isScrolling &&
prevState.isScrolling &&
prevState.scrollOffsetDelta !== 0
) {
const { scrollOffset, scrollOffsetDelta } = instance.state;
// TRICKY
// Clean up any temporary offset adjustments (made above) once scrolling has stopped.
// Be sure to update the scrollOffset in state before doing this,
// or the "scroll" event it triggers will cause a new batch of items to be rendered.
instance.setState(
{
scrollOffset: scrollOffset + scrollOffsetDelta,
scrollOffsetDelta: 0,
},
() => {
const element = ((instance._outerRef: any): HTMLDivElement);
if (instance.props.direction === 'horizontal') {
element.scrollLeft = scrollOffset + scrollOffsetDelta;
} else {
element.scrollTop = scrollOffset + scrollOffsetDelta;
}
}
);
} else {
// eslint-disable-next-line no-unused-vars
const [_, __, startIndex] = instance._getRangeToRender();
const { scrollOffset, scrollOffsetDelta } = instance.state;
// TRICKY
// If we get to close to the start of the list (either offset 0 or the first item)
// we should synchronously adjustour offsets without waiting for the debounce.
// This is still an awkward scrolling UX but it's hopefully not a common case.
if (
scrollOffsetDelta > 0 &&
(startIndex === 0 || scrollOffset + scrollOffsetDelta <= 0)
) {
instance.setState(
{
scrollOffset: scrollOffset + scrollOffsetDelta,
scrollOffsetDelta: 0,
},
() => {
const element = ((instance._outerRef: any): HTMLDivElement);
if (instance.props.direction === 'horizontal') {
element.scrollLeft = scrollOffset + scrollOffsetDelta;
} else {
element.scrollTop = scrollOffset + scrollOffsetDelta;
}
}
);
}
}
}
};
+9 -19
View File
@@ -55,11 +55,10 @@ export type Props<T> = {|
width: number | string,
|};
export type State = {|
type State = {|
isScrolling: boolean,
scrollDirection: ScrollDirection,
scrollOffset: number,
scrollOffsetDelta: number,
scrollUpdateWasRequested: boolean,
|};
@@ -141,7 +140,6 @@ export default function createListComponent({
typeof this.props.initialScrollOffset === 'number'
? this.props.initialScrollOffset
: 0,
scrollOffsetDelta: 0,
scrollUpdateWasRequested: false,
};
@@ -202,7 +200,7 @@ export default function createListComponent({
this._commitHook();
}
componentDidUpdate(prevProps: Props<any>, prevState: State) {
componentDidUpdate() {
const { direction } = this.props;
const { scrollOffset, scrollUpdateWasRequested } = this.state;
@@ -216,7 +214,7 @@ export default function createListComponent({
}
this._callPropsCallbacks();
this._commitHook(prevProps, prevState);
this._commitHook();
}
componentWillUnmount() {
@@ -238,7 +236,7 @@ export default function createListComponent({
style,
width,
} = this.props;
const { isScrolling, scrollOffsetDelta } = this.state;
const { isScrolling } = this.state;
const onScroll =
direction === 'vertical'
@@ -264,6 +262,7 @@ export default function createListComponent({
height,
width,
overflow: 'auto',
position: 'relative',
WebkitOverflowScrolling: 'touch',
willChange: 'transform',
...style,
@@ -273,16 +272,7 @@ export default function createListComponent({
children: items,
ref: innerRef,
style: {
position: 'relative',
height: direction === 'horizontal' ? '100%' : estimatedTotalSize,
marginLeft:
direction === 'horizontal'
? `${-scrollOffsetDelta}px`
: undefined,
marginTop:
direction === 'horizontal'
? undefined
: `${-scrollOffsetDelta}px`,
pointerEvents: isScrolling ? 'none' : '',
width: direction === 'horizontal' ? estimatedTotalSize : '100%',
},
@@ -364,7 +354,7 @@ export default function createListComponent({
// This method is called after mount and update.
// List implementations can override this method to be notified.
_commitHook(prevProps?: Props<any>, prevState?: State) {}
_commitHook() {}
// This method is called before unmounting.
// List implementations can override this method to be notified.
@@ -424,7 +414,7 @@ export default function createListComponent({
_getRangeToRender(): [number, number, number, number] {
const { itemCount, overscanCount } = this.props;
const { scrollDirection, scrollOffset, scrollOffsetDelta } = this.state;
const { scrollDirection, scrollOffset } = this.state;
if (itemCount === 0) {
return [0, 0, 0, 0];
@@ -432,13 +422,13 @@ export default function createListComponent({
const startIndex = getStartIndexForOffset(
this.props,
scrollOffset + scrollOffsetDelta,
scrollOffset,
this._instanceProps
);
const stopIndex = getStopIndexForStartIndex(
this.props,
startIndex,
scrollOffset + scrollOffsetDelta,
scrollOffset,
this._instanceProps
);