diff --git a/src/components/views/elements/LazyRenderList.js b/src/components/views/elements/LazyRenderList.js index d7d2a0ab99..0fc0ef6733 100644 --- a/src/components/views/elements/LazyRenderList.js +++ b/src/components/views/elements/LazyRenderList.js @@ -15,9 +15,7 @@ limitations under the License. */ import React from "react"; - -const OVERFLOW_ITEMS = 20; -const OVERFLOW_MARGIN = 5; +import PropTypes from 'prop-types'; class ItemRange { constructor(topCount, renderCount, bottomCount) { @@ -27,11 +25,22 @@ class ItemRange { } contains(range) { + // don't contain empty ranges + // as it will prevent clearing the list + // once it is scrolled far enough out of view + if (!range.renderCount && this.renderCount) { + return false; + } return range.topCount >= this.topCount && (range.topCount + range.renderCount) <= (this.topCount + this.renderCount); } expand(amount) { + // don't expand ranges that won't render anything + if (this.renderCount === 0) { + return this; + } + const topGrow = Math.min(amount, this.topCount); const bottomGrow = Math.min(amount, this.bottomCount); return new ItemRange( @@ -40,52 +49,80 @@ class ItemRange { this.bottomCount - bottomGrow, ); } + + totalSize() { + return this.topCount + this.renderCount + this.bottomCount; + } } export default class LazyRenderList extends React.Component { - constructor(props) { - super(props); - const renderRange = LazyRenderList.getVisibleRangeFromProps(props).expand(OVERFLOW_ITEMS); - this.state = {renderRange}; + static getDerivedStateFromProps(props, state) { + const range = LazyRenderList.getVisibleRangeFromProps(props); + const intersectRange = range.expand(props.overflowMargin); + const renderRange = range.expand(props.overflowItems); + const listHasChangedSize = !!state && renderRange.totalSize() !== state.renderRange.totalSize(); + // only update render Range if the list has shrunk/grown and we need to adjust padding OR + // if the new range + overflowMargin isn't contained by the old anymore + if (listHasChangedSize || !state || !state.renderRange.contains(intersectRange)) { + return {renderRange}; + } + return null; } static getVisibleRangeFromProps(props) { const {items, itemHeight, scrollTop, height} = props; const length = items ? items.length : 0; - const topCount = Math.max(0, Math.floor(scrollTop / itemHeight)); + const topCount = Math.min(Math.max(0, Math.floor(scrollTop / itemHeight)), length); const itemsAfterTop = length - topCount; - const renderCount = Math.min(Math.ceil(height / itemHeight), itemsAfterTop); + const visibleItems = height !== 0 ? Math.ceil(height / itemHeight) : 0; + const renderCount = Math.min(visibleItems, itemsAfterTop); const bottomCount = itemsAfterTop - renderCount; return new ItemRange(topCount, renderCount, bottomCount); } - componentWillReceiveProps(props) { - const state = this.state; - const range = LazyRenderList.getVisibleRangeFromProps(props); - const intersectRange = range.expand(OVERFLOW_MARGIN); - - const prevSize = this.props.items ? this.props.items.length : 0; - const listHasChangedSize = props.items.length !== prevSize; - // only update renderRange if the list has shrunk/grown and we need to adjust padding or - // if the new range isn't contained by the old anymore - if (listHasChangedSize || !state.renderRange || !state.renderRange.contains(intersectRange)) { - this.setState({renderRange: range.expand(OVERFLOW_ITEMS)}); - } - } - render() { const {itemHeight, items, renderItem} = this.props; - const {renderRange} = this.state; - const paddingTop = renderRange.topCount * itemHeight; - const paddingBottom = renderRange.bottomCount * itemHeight; + const {topCount, renderCount, bottomCount} = renderRange; + + const paddingTop = topCount * itemHeight; + const paddingBottom = bottomCount * itemHeight; const renderedItems = (items || []).slice( - renderRange.topCount, - renderRange.topCount + renderRange.renderCount, + topCount, + topCount + renderCount, ); - return (