diff --git a/src/DynamicSizeList.js b/src/DynamicSizeList.js index 76be920..6ec34f0 100644 --- a/src/DynamicSizeList.js +++ b/src/DynamicSizeList.js @@ -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, 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; - } - } - ); - } - } } }; diff --git a/src/createListComponent.js b/src/createListComponent.js index 8dea628..bf6d3aa 100644 --- a/src/createListComponent.js +++ b/src/createListComponent.js @@ -55,11 +55,10 @@ export type Props = {| 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, 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, 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 );