Replaced margin top/left hack with scrollBy()
This commit is contained in:
+24
-66
@@ -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;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user