diff --git a/.flowconfig b/.flowconfig index 20ccf3cfe..fdb388a4e 100644 --- a/.flowconfig +++ b/.flowconfig @@ -1,10 +1,8 @@ [ignore] -.*/node_modules/.* .*/ArrowKeyStepper/.* .*/AutoSizer/.* .*/Collection/.* .*/ColumnSizer/.* -.*/Grid/.* .*/InfiniteLoader/.* .*/List/.* .*/Masonry/.* diff --git a/source/Grid/Grid.example.js b/source/Grid/Grid.example.js index 80d743262..693e84a68 100644 --- a/source/Grid/Grid.example.js +++ b/source/Grid/Grid.example.js @@ -1,4 +1,3 @@ -/** @flow */ import Immutable from "immutable"; import PropTypes from "prop-types"; import React, { PureComponent } from "react"; diff --git a/source/Grid/Grid.js b/source/Grid/Grid.js index 35575fc69..bde19a110 100644 --- a/source/Grid/Grid.js +++ b/source/Grid/Grid.js @@ -1,6 +1,8 @@ /** @flow */ + +import type { CellSizeGetter, Alignment } from "./types.js"; + import React, { PureComponent } from "react"; -import PropTypes from "prop-types"; import cn from "classnames"; import calculateSizeAndPositionDataAndUpdateScrollOffset from "./utils/calculateSizeAndPositionDataAndUpdateScrollOffset"; import ScalingCellSizeAndPositionManager from "./utils/ScalingCellSizeAndPositionManager"; @@ -28,227 +30,251 @@ const SCROLL_POSITION_CHANGE_REASONS = { REQUESTED: "requested" }; +type ScrollPosition = { scrollTop: number, scrollLeft: number }; + +type CellPosition = { columnIndex: number, rowIndex: number }; + +type CellRenderer = (props: CellPosition) => React.Element<*>; + +type Props = { + "aria-label": string, + "aria-readonly": boolean, + + /** + * Set the width of the inner scrollable container to 'auto'. + * This is useful for single-column Grids to ensure that the column doesn't extend below a vertical scrollbar. + */ + autoContainerWidth: boolean, + + /** + * Removes fixed height from the scrollingContainer so that the total height of rows can stretch the window. + * Intended for use with WindowScroller + */ + autoHeight: boolean, + + /** + * Removes fixed width from the scrollingContainer so that the total width of rows can stretch the window. + * Intended for use with WindowScroller + */ + autoWidth: boolean, + + /** + * Responsible for rendering a cell given an row and column index. + */ + cellRenderer: CellRenderer, + + /** + * Responsible for rendering a group of cells given their index ranges. + */ + cellRangeRenderer: (params: { + cellCache: Object, + cellRenderer: CellRenderer, + columnSizeAndPositionManager: ScalingCellSizeAndPositionManager, + columnStartIndex: number, + columnStopIndex: number, + isScrolling: boolean, + rowSizeAndPositionManager: ScalingCellSizeAndPositionManager, + rowStartIndex: number, + rowStopIndex: number, + scrollLeft: number, + scrollTop: number + }) => React.Element<*>[], + /** + * Optional custom CSS class name to attach to root Grid element. + */ + className?: string, + + /** + * Number of columns in grid. + */ + columnCount: number, + + /** + * Either a fixed column width (number) or a function that returns the width of a column given its index. + */ + columnWidth: CellSizeGetter | number, + + /** + * ARIA role for the cell-container. + */ + containerRole: string, + + /** Optional inline style applied to inner cell-container */ + containerStyle?: Object, + + /** + * If CellMeasurer is used to measure this Grid's children, this should be a pointer to its CellMeasurerCache. + * A shared CellMeasurerCache reference enables Grid and CellMeasurer to share measurement data. + */ + deferredMeasurementCache?: Object, + + /** + * Used to estimate the total width of a Grid before all of its columns have actually been measured. + * The estimated total width is adjusted as columns are rendered. + */ + estimatedColumnSize: number, + + /** + * Used to estimate the total height of a Grid before all of its rows have actually been measured. + * The estimated total height is adjusted as rows are rendered. + */ + estimatedRowSize: number, + + /** + * Exposed for testing purposes only. + */ + getScrollbarSize: () => number, + + /** + * Height of Grid; this property determines the number of visible (vs virtualized) rows. + */ + height: number, + + /** + * Optional custom id to attach to root Grid element. + */ + id?: string, + + /** + * Override internal is-scrolling state tracking. + * This property is primarily intended for use with the WindowScroller component. + */ + isScrolling?: boolean, + + /** + * Optional renderer to be used in place of rows when either :rowCount or :columnCount is 0. + */ + noContentRenderer: () => React.Element<*> | null, + + /** + * Callback invoked whenever the scroll offset changes within the inner scrollable region. + * This callback can be used to sync scrolling between lists, tables, or grids. + */ + onScroll: (params: { + clientHeight: number, + clientWidth: number, + scrollHeight: number, + scrollLeft: number, + scrollTop: number, + scrollWidth: number + }) => void, + + /** + * Called whenever a horizontal or vertical scrollbar is added or removed. + * This prop is not intended for end-user use; + * It is used by MultiGrid to support fixed-row/fixed-column scroll syncing. + */ + onScrollbarPresenceChange: (params: { + horizontal: boolean, + vertical: boolean, + size: number + }) => void, + + /** + * Callback invoked with information about the section of the Grid that was just rendered. + */ + onSectionRendered: (params: { + columnStartIndex: number, + columnStopIndex: number, + rowStartIndex: number, + rowStopIndex: number + }) => void, + + /** + * Number of columns to render before/after the visible section of the grid. + * These columns can help for smoother scrolling on touch devices or browsers that send scroll events infrequently. + */ + overscanColumnCount: number, + + /** + * Calculates the number of cells to overscan before and after a specified range. + * This function ensures that overscanning doesn't exceed the available cells. + */ + overscanIndicesGetter: (params: { + cellCount: number, + overscanCellsCount: number, + scrollDirection: number, + startIndex: number, + stopIndex: number + }) => { + overscanStartIndex: number, + overscanStopIndex: number + }, + + /** + * Number of rows to render above/below the visible section of the grid. + * These rows can help for smoother scrolling on touch devices or browsers that send scroll events infrequently. + */ + overscanRowCount: number, + + /** + * ARIA role for the grid element. + */ + role: string, + + /** + * Either a fixed row height (number) or a function that returns the height of a row given its index. + * Should implement the following interface: ({ index: number }): number + */ + rowHeight: CellSizeGetter | number, + + /** + * Number of rows in grid. + */ + rowCount: number, + + /** Wait this amount of time after the last scroll event before resetting Grid `pointer-events`. */ + scrollingResetTimeInterval: number, + + /** Horizontal offset. */ + scrollLeft: number, + + /** + * Controls scroll-to-cell behavior of the Grid. + * The default ("auto") scrolls the least amount possible to ensure that the specified cell is fully visible. + * Use "start" to align cells to the top/left of the Grid and "end" to align bottom/right. + */ + scrollToAlignment: Alignment, + + /** + * Column index to ensure visible (by forcefully scrolling if necessary) + */ + scrollToColumn: number, + + /** Vertical offset. */ + scrollTop: number, + + /** + * Row index to ensure visible (by forcefully scrolling if necessary) + */ + scrollToRow: number, + + /** Optional inline style */ + style: Object, + + /** Tab index for focus */ + tabIndex: number, + + /** + * Width of Grid; this property determines the number of visible (vs virtualized) columns. + */ + width: number +}; + +type State = { + isScrolling: boolean, + scrollDirectionHorizontal: -1 | 1, + scrollDirectionVertical: -1 | 1, + scrollLeft: number, + scrollTop: number, + scrollPositionChangeReason?: "observed" | "requested" +}; + /** * Renders tabular data with virtualization along the vertical and horizontal axes. * Row heights and column widths must be known ahead of time and specified as properties. */ export default class Grid extends PureComponent { - static propTypes = { - "aria-label": PropTypes.string, - "aria-readonly": PropTypes.bool, - - /** - * Set the width of the inner scrollable container to 'auto'. - * This is useful for single-column Grids to ensure that the column doesn't extend below a vertical scrollbar. - */ - autoContainerWidth: PropTypes.bool, - - /** - * Removes fixed height from the scrollingContainer so that the total height of rows can stretch the window. - * Intended for use with WindowScroller - */ - autoHeight: PropTypes.bool, - - /** - * Removes fixed width from the scrollingContainer so that the total width of rows can stretch the window. - * Intended for use with WindowScroller - */ - autoWidth: PropTypes.bool, - - /** - * Responsible for rendering a cell given an row and column index. - * Should implement the following interface: ({ columnIndex: number, rowIndex: number }): PropTypes.node - */ - cellRenderer: PropTypes.func.isRequired, - - /** - * Responsible for rendering a group of cells given their index ranges. - * Should implement the following interface: ({ - * cellCache: Map, - * cellRenderer: Function, - * columnSizeAndPositionManager: CellSizeAndPositionManager, - * columnStartIndex: number, - * columnStopIndex: number, - * isScrolling: boolean, - * rowSizeAndPositionManager: CellSizeAndPositionManager, - * rowStartIndex: number, - * rowStopIndex: number, - * scrollLeft: number, - * scrollTop: number - * }): Array - */ - cellRangeRenderer: PropTypes.func.isRequired, - - /** - * Optional custom CSS class name to attach to root Grid element. - */ - className: PropTypes.string, - - /** - * Number of columns in grid. - */ - columnCount: PropTypes.number.isRequired, - - /** - * Either a fixed column width (number) or a function that returns the width of a column given its index. - * Should implement the following interface: (index: number): number - */ - columnWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.func]) - .isRequired, - - /** - * ARIA role for the cell-container. - */ - containerRole: PropTypes.string, - - /** Optional inline style applied to inner cell-container */ - containerStyle: PropTypes.object, - - /** - * If CellMeasurer is used to measure this Grid's children, this should be a pointer to its CellMeasurerCache. - * A shared CellMeasurerCache reference enables Grid and CellMeasurer to share measurement data. - */ - deferredMeasurementCache: PropTypes.object, - - /** - * Used to estimate the total width of a Grid before all of its columns have actually been measured. - * The estimated total width is adjusted as columns are rendered. - */ - estimatedColumnSize: PropTypes.number.isRequired, - - /** - * Used to estimate the total height of a Grid before all of its rows have actually been measured. - * The estimated total height is adjusted as rows are rendered. - */ - estimatedRowSize: PropTypes.number.isRequired, - - /** - * Exposed for testing purposes only. - */ - getScrollbarSize: PropTypes.func.isRequired, - - /** - * Height of Grid; this property determines the number of visible (vs virtualized) rows. - */ - height: PropTypes.number.isRequired, - - /** - * Optional custom id to attach to root Grid element. - */ - id: PropTypes.string, - - /** - * Override internal is-scrolling state tracking. - * This property is primarily intended for use with the WindowScroller component. - */ - isScrolling: PropTypes.bool, - - /** - * Optional renderer to be used in place of rows when either :rowCount or :columnCount is 0. - */ - noContentRenderer: PropTypes.func.isRequired, - - /** - * Callback invoked whenever the scroll offset changes within the inner scrollable region. - * This callback can be used to sync scrolling between lists, tables, or grids. - * ({ clientHeight, clientWidth, scrollHeight, scrollLeft, scrollTop, scrollWidth }): void - */ - onScroll: PropTypes.func.isRequired, - - /** - * Called whenever a horizontal or vertical scrollbar is added or removed. - * This prop is not intended for end-user use; - * It is used by MultiGrid to support fixed-row/fixed-column scroll syncing. - */ - onScrollbarPresenceChange: PropTypes.func.isRequired, - - /** - * Callback invoked with information about the section of the Grid that was just rendered. - * ({ columnStartIndex, columnStopIndex, rowStartIndex, rowStopIndex }): void - */ - onSectionRendered: PropTypes.func.isRequired, - - /** - * Number of columns to render before/after the visible section of the grid. - * These columns can help for smoother scrolling on touch devices or browsers that send scroll events infrequently. - */ - overscanColumnCount: PropTypes.number.isRequired, - - /** - * Calculates the number of cells to overscan before and after a specified range. - * This function ensures that overscanning doesn't exceed the available cells. - * Should implement the following interface: ({ - * cellCount: number, - * overscanCellsCount: number, - * scrollDirection: number, - * startIndex: number, - * stopIndex: number - * }): {overscanStartIndex: number, overscanStopIndex: number} - */ - overscanIndicesGetter: PropTypes.func.isRequired, - - /** - * Number of rows to render above/below the visible section of the grid. - * These rows can help for smoother scrolling on touch devices or browsers that send scroll events infrequently. - */ - overscanRowCount: PropTypes.number.isRequired, - - /** - * ARIA role for the grid element. - */ - role: PropTypes.string, - - /** - * Either a fixed row height (number) or a function that returns the height of a row given its index. - * Should implement the following interface: ({ index: number }): number - */ - rowHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.func]) - .isRequired, - - /** - * Number of rows in grid. - */ - rowCount: PropTypes.number.isRequired, - - /** Wait this amount of time after the last scroll event before resetting Grid `pointer-events`. */ - scrollingResetTimeInterval: PropTypes.number, - - /** Horizontal offset. */ - scrollLeft: PropTypes.number, - - /** - * Controls scroll-to-cell behavior of the Grid. - * The default ("auto") scrolls the least amount possible to ensure that the specified cell is fully visible. - * Use "start" to align cells to the top/left of the Grid and "end" to align bottom/right. - */ - scrollToAlignment: PropTypes.oneOf(["auto", "end", "start", "center"]) - .isRequired, - - /** - * Column index to ensure visible (by forcefully scrolling if necessary) - */ - scrollToColumn: PropTypes.number.isRequired, - - /** Vertical offset. */ - scrollTop: PropTypes.number, - - /** - * Row index to ensure visible (by forcefully scrolling if necessary) - */ - scrollToRow: PropTypes.number.isRequired, - - /** Optional inline style */ - style: PropTypes.object, - - /** Tab index for focus */ - tabIndex: PropTypes.number, - - /** - * Width of Grid; this property determines the number of visible (vs virtualized) columns. - */ - width: PropTypes.number.isRequired - }; - static defaultProps = { "aria-label": "grid", "aria-readonly": true, @@ -273,62 +299,81 @@ export default class Grid extends PureComponent { tabIndex: 0 }; - constructor(props, context) { - super(props, context); + props: Props; - this.state = { - isScrolling: false, - scrollDirectionHorizontal: SCROLL_DIRECTION_FORWARD, - scrollDirectionVertical: SCROLL_DIRECTION_FORWARD, - scrollLeft: 0, - scrollTop: 0 - }; + state: State = { + isScrolling: false, + scrollDirectionHorizontal: SCROLL_DIRECTION_FORWARD, + scrollDirectionVertical: SCROLL_DIRECTION_FORWARD, + scrollLeft: 0, + scrollTop: 0 + }; - // Invokes onSectionRendered callback only when start/stop row or column indices change - this._onGridRenderedMemoizer = createCallbackMemoizer(); - this._onScrollMemoizer = createCallbackMemoizer(false); + // Invokes onSectionRendered callback only when start/stop row or column indices change + _onGridRenderedMemoizer = createCallbackMemoizer(); + _onScrollMemoizer = createCallbackMemoizer(false); - // Bind functions to instance so they don't lose context when passed around - this._debounceScrollEndedCallback = this._debounceScrollEndedCallback.bind( - this - ); - this._invokeOnGridRenderedHelper = this._invokeOnGridRenderedHelper.bind( - this - ); - this._onScroll = this._onScroll.bind(this); - this._setScrollingContainerRef = this._setScrollingContainerRef.bind(this); + _deferredInvalidateColumnIndex = null; + _deferredInvalidateRowIndex = null; + _recomputeScrollLeftFlag = false; + _recomputeScrollTopFlag = false; - this._columnWidthGetter = this._wrapSizeGetter(props.columnWidth); - this._rowHeightGetter = this._wrapSizeGetter(props.rowHeight); + _horizontalScrollBarSize = 0; + _verticalScrollBarSize = 0; + _scrollbarPresenceChanged = false; + + _columnWidthGetter: CellSizeGetter; + _rowHeightGetter: CellSizeGetter; - this._deferredInvalidateColumnIndex = null; - this._deferredInvalidateRowIndex = null; - this._recomputeScrollLeftFlag = false; - this._recomputeScrollTopFlag = false; + _columnSizeAndPositionManager: ScalingCellSizeAndPositionManager; + _rowSizeAndPositionManager: ScalingCellSizeAndPositionManager; - this._horizontalScrollBarSize = 0; - this._verticalScrollBarSize = 0; - this._scrollbarPresenceChanged = false; + // See defaultCellRangeRenderer() for more information on the usage of these caches + _cellCache = {}; + _styleCache = {}; + + _scrollbarSize: number; + _scrollbarSizeMeasured = false; + _scrollingContainer: Element; + + _childrenToDisplay: React.Element<*>[]; + + _renderedColumnStartIndex = 0; + _renderedColumnStopIndex = 0; + _renderedRowStartIndex = 0; + _renderedRowStopIndex = 0; + + _columnStartIndex: number; + _columnStopIndex: number; + _rowStartIndex: number; + _rowStopIndex: number; + + _disablePointerEventsTimeoutId: ?number; + + constructor(props: Props) { + super(props); + + this._columnWidthGetter = this._wrapSizeGetter(props.columnWidth); + this._rowHeightGetter = this._wrapSizeGetter(props.rowHeight); const deferredMeasurementCache = props.deferredMeasurementCache; - const deferredMode = typeof deferredMeasurementCache !== "undefined"; this._columnSizeAndPositionManager = new ScalingCellSizeAndPositionManager({ - batchAllCells: deferredMode && !deferredMeasurementCache.hasFixedHeight(), + batchAllCells: + deferredMeasurementCache !== undefined && + !deferredMeasurementCache.hasFixedHeight(), cellCount: props.columnCount, cellSizeGetter: params => this._columnWidthGetter(params), estimatedCellSize: this._getEstimatedColumnSize(props) }); this._rowSizeAndPositionManager = new ScalingCellSizeAndPositionManager({ - batchAllCells: deferredMode && !deferredMeasurementCache.hasFixedWidth(), + batchAllCells: + deferredMeasurementCache !== undefined && + !deferredMeasurementCache.hasFixedWidth(), cellCount: props.rowCount, cellSizeGetter: params => this._rowHeightGetter(params), estimatedCellSize: this._getEstimatedRowSize(props) }); - - // See defaultCellRangeRenderer() for more information on the usage of these caches - this._cellCache = {}; - this._styleCache = {}; } /** @@ -339,6 +384,10 @@ export default class Grid extends PureComponent { alignment = this.props.scrollToAlignment, columnIndex = this.props.scrollToColumn, rowIndex = this.props.scrollToRow + }: { + alignment: Alignment, + columnIndex: number, + rowIndex: number } = {} ) { const offsetProps = { @@ -361,7 +410,7 @@ export default class Grid extends PureComponent { handleScrollEvent({ scrollLeft: scrollLeftParam = 0, scrollTop: scrollTopParam = 0 - }) { + }: ScrollPosition) { // On iOS, we can arrive at negative offsets by swiping past the start. // To prevent flicker here, we make playing in the negative offset zone cause nothing to happen. if (scrollTopParam < 0) { @@ -412,7 +461,7 @@ export default class Grid extends PureComponent { : SCROLL_DIRECTION_BACKWARD : this.state.scrollDirectionVertical; - const newState = { + const newState: Object = { isScrolling: true, scrollDirectionHorizontal, scrollDirectionVertical, @@ -445,7 +494,7 @@ export default class Grid extends PureComponent { * This method is intended for advanced use-cases like CellMeasurer. */ // @TODO (bvaughn) Add automated test coverage for this. - invalidateCellSizeAfterRender({ columnIndex, rowIndex }) { + invalidateCellSizeAfterRender({ columnIndex, rowIndex }: CellPosition) { this._deferredInvalidateColumnIndex = typeof this._deferredInvalidateColumnIndex === "number" ? Math.min(this._deferredInvalidateColumnIndex, columnIndex) @@ -475,7 +524,7 @@ export default class Grid extends PureComponent { * This function should be called if dynamic column or row sizes have changed but nothing else has. * Since Grid only receives :columnCount and :rowCount it has no way of detecting when the underlying data changes. */ - recomputeGridSize({ columnIndex = 0, rowIndex = 0 } = {}) { + recomputeGridSize({ columnIndex = 0, rowIndex = 0 }: CellPosition = {}) { const { scrollToColumn, scrollToRow } = this.props; this._columnSizeAndPositionManager.resetCell(columnIndex); @@ -499,7 +548,7 @@ export default class Grid extends PureComponent { /** * Ensure column and row are visible. */ - scrollToCell({ columnIndex, rowIndex }) { + scrollToCell({ columnIndex, rowIndex }: CellPosition) { const { columnCount } = this.props; const props = this.props; @@ -523,7 +572,7 @@ export default class Grid extends PureComponent { * Scroll to the specified offset(s). * Useful for animating position changes. */ - scrollToPosition({ scrollLeft, scrollTop } = {}) { + scrollToPosition({ scrollLeft, scrollTop }: ScrollPosition = {}) { this._setScrollPosition({ scrollLeft, scrollTop }); } @@ -583,7 +632,7 @@ export default class Grid extends PureComponent { * This method updates scrollLeft/scrollTop in state for the following conditions: * 1) New scroll-to-cell props have been set */ - componentDidUpdate(prevProps, prevState) { + componentDidUpdate(prevProps: Props, prevState: State) { const { autoHeight, autoWidth, @@ -654,7 +703,10 @@ export default class Grid extends PureComponent { updateScrollIndexHelper({ cellSizeAndPositionManager: this._columnSizeAndPositionManager, previousCellsCount: prevProps.columnCount, - previousCellSize: prevProps.columnWidth, + previousCellSize: + typeof prevProps.columnWidth === "number" + ? prevProps.columnWidth + : null, previousScrollToAlignment: prevProps.scrollToAlignment, previousScrollToIndex: prevProps.scrollToColumn, previousSize: prevProps.width, @@ -675,7 +727,8 @@ export default class Grid extends PureComponent { updateScrollIndexHelper({ cellSizeAndPositionManager: this._rowSizeAndPositionManager, previousCellsCount: prevProps.rowCount, - previousCellSize: prevProps.rowHeight, + previousCellSize: + typeof prevProps.rowHeight === "number" ? prevProps.rowHeight : null, previousScrollToAlignment: prevProps.scrollToAlignment, previousScrollToIndex: prevProps.scrollToRow, previousSize: prevProps.height, @@ -740,7 +793,7 @@ export default class Grid extends PureComponent { * 2) New scroll props overriding the current state * 3) Cells-count or cells-size has changed, making previous scroll offsets invalid */ - componentWillReceiveProps(nextProps) { + componentWillReceiveProps(nextProps: Props) { const { scrollLeft, scrollTop } = this.state; if ( @@ -808,12 +861,18 @@ export default class Grid extends PureComponent { // Update scroll offsets if the size or number of cells have changed, invalidating the previous value calculateSizeAndPositionDataAndUpdateScrollOffset({ cellCount: columnCount, - cellSize: this.props.columnWidth, + cellSize: + typeof this.props.columnWidth === "number" + ? this.props.columnWidth + : null, computeMetadataCallback: () => this._columnSizeAndPositionManager.resetCell(0), computeMetadataCallbackProps: nextProps, nextCellsCount: nextProps.columnCount, - nextCellSize: nextProps.columnWidth, + nextCellSize: + typeof nextProps.columnWidth === "number" + ? nextProps.columnWidth + : null, nextScrollToIndex: nextProps.scrollToColumn, scrollToIndex: this.props.scrollToColumn, updateScrollOffsetForScrollToIndex: () => @@ -821,12 +880,14 @@ export default class Grid extends PureComponent { }); calculateSizeAndPositionDataAndUpdateScrollOffset({ cellCount: rowCount, - cellSize: this.props.rowHeight, + cellSize: + typeof this.props.rowHeight === "number" ? this.props.rowHeight : null, computeMetadataCallback: () => this._rowSizeAndPositionManager.resetCell(0), computeMetadataCallbackProps: nextProps, nextCellsCount: nextProps.rowCount, - nextCellSize: nextProps.rowHeight, + nextCellSize: + typeof nextProps.rowHeight === "number" ? nextProps.rowHeight : null, nextScrollToIndex: nextProps.scrollToRow, scrollToIndex: this.props.scrollToRow, updateScrollOffsetForScrollToIndex: () => @@ -834,7 +895,7 @@ export default class Grid extends PureComponent { }); } - componentWillUpdate(nextProps, nextState) { + componentWillUpdate(nextProps: Props, nextState: State) { this._calculateChildrenToRender(nextProps, nextState); } @@ -857,7 +918,7 @@ export default class Grid extends PureComponent { const isScrolling = this._isScrolling(); - const gridStyle = { + const gridStyle: Object = { boxSizing: "border-box", direction: "ltr", height: autoHeight ? "auto" : height, @@ -941,7 +1002,10 @@ export default class Grid extends PureComponent { /* ---------------------------- Helper methods ---------------------------- */ - _calculateChildrenToRender(props = this.props, state = this.state) { + _calculateChildrenToRender( + props: Props = this.props, + state: State = this.state + ) { const { cellRenderer, cellRangeRenderer, @@ -1005,8 +1069,14 @@ export default class Grid extends PureComponent { cellCount: columnCount, overscanCellsCount: overscanColumnCount, scrollDirection: scrollDirectionHorizontal, - startIndex: this._renderedColumnStartIndex, - stopIndex: this._renderedColumnStopIndex + startIndex: + typeof this._renderedColumnStartIndex === "number" + ? this._renderedColumnStartIndex + : 0, + stopIndex: + typeof this._renderedColumnStopIndex === "number" + ? this._renderedColumnStopIndex + : -1 }); const overscanRowIndices = overscanIndicesGetter({ @@ -1014,8 +1084,15 @@ export default class Grid extends PureComponent { cellCount: rowCount, overscanCellsCount: overscanRowCount, scrollDirection: scrollDirectionVertical, - startIndex: this._renderedRowStartIndex, - stopIndex: this._renderedRowStopIndex + startIndex: + typeof this._renderedRowStartIndex === "number" + ? this._renderedRowStartIndex + : 0, + stopIndex: + typeof this._renderedRowStopIndex === "number" + ? this._renderedRowStopIndex + : -1 + // stopIndex: this._renderedRowStopIndex }); // Store for _invokeOnGridRenderedHelper() @@ -1065,18 +1142,18 @@ export default class Grid extends PureComponent { ); } - _debounceScrollEndedCallback() { + _debounceScrollEndedCallback = () => { this._disablePointerEventsTimeoutId = null; this._resetStyleCache(); - } + }; - _getEstimatedColumnSize(props) { + _getEstimatedColumnSize(props: Props) { return typeof props.columnWidth === "number" ? props.columnWidth : props.estimatedColumnSize; } - _getEstimatedRowSize(props) { + _getEstimatedRowSize(props: Props) { return typeof props.rowHeight === "number" ? props.rowHeight : props.estimatedRowSize; @@ -1087,7 +1164,10 @@ export default class Grid extends PureComponent { * This will occur the first time one or more previously unmeasured cells are rendered. */ _handleInvalidatedGridSize() { - if (typeof this._deferredInvalidateColumnIndex === "number") { + if ( + typeof this._deferredInvalidateColumnIndex === "number" && + typeof this._deferredInvalidateRowIndex === "number" + ) { const columnIndex = this._deferredInvalidateColumnIndex; const rowIndex = this._deferredInvalidateRowIndex; @@ -1098,7 +1178,7 @@ export default class Grid extends PureComponent { } } - _invokeOnGridRenderedHelper() { + _invokeOnGridRenderedHelper = () => { const { onSectionRendered } = this.props; this._onGridRenderedMemoizer({ @@ -1114,13 +1194,18 @@ export default class Grid extends PureComponent { rowStopIndex: this._renderedRowStopIndex } }); - } + }; _invokeOnScrollMemoizer({ scrollLeft, scrollTop, totalColumnsWidth, totalRowsHeight + }: { + scrollLeft: number, + scrollTop: number, + totalColumnsWidth: number, + totalRowsHeight: number }) { this._onScrollMemoizer({ callback: ({ scrollLeft, scrollTop }) => { @@ -1142,12 +1227,12 @@ export default class Grid extends PureComponent { }); } - _isScrolling(props = this.props, state = this.state) { + _isScrolling(props: Props = this.props, state: State = this.state): boolean { // If isScrolling is defined in props, use it to override the value in state // This is a performance optimization for WindowScroller + Grid return Object.hasOwnProperty.call(props, "isScrolling") - ? props.isScrolling - : state.isScrolling; + ? Boolean(props.isScrolling) + : Boolean(state.isScrolling); } _maybeCallOnScrollbarPresenceChange() { @@ -1164,12 +1249,12 @@ export default class Grid extends PureComponent { } } - _setScrollingContainerRef(ref) { + _setScrollingContainerRef = (ref: Element) => { this._scrollingContainer = ref; - } + }; - _setScrollPosition({ scrollLeft, scrollTop }) { - const newState = { + _setScrollPosition({ scrollLeft, scrollTop }: ScrollPosition) { + const newState: Object = { scrollPositionChangeReason: SCROLL_POSITION_CHANGE_REASONS.REQUESTED }; @@ -1197,15 +1282,14 @@ export default class Grid extends PureComponent { } } - _wrapPropertyGetter(value) { - return typeof value === "function" ? value : () => value; + _wrapSizeGetter(value: CellSizeGetter | number): CellSizeGetter { + return typeof value === "function" ? value : () => (value: any); } - _wrapSizeGetter(size) { - return this._wrapPropertyGetter(size); - } - - _getCalculatedScrollLeft(props = this.props, state = this.state) { + _getCalculatedScrollLeft( + props: Props = this.props, + state: State = this.state + ) { const { columnCount, height, @@ -1232,18 +1316,29 @@ export default class Grid extends PureComponent { } } - _updateScrollLeftForScrollToColumn(props = this.props, state = this.state) { + _updateScrollLeftForScrollToColumn( + props: Props = this.props, + state: State = this.state + ) { const { scrollLeft } = state; const calculatedScrollLeft = this._getCalculatedScrollLeft(props, state); - if (calculatedScrollLeft >= 0 && scrollLeft !== calculatedScrollLeft) { + if ( + typeof calculatedScrollLeft === "number" && + calculatedScrollLeft >= 0 && + scrollLeft !== calculatedScrollLeft + ) { this._setScrollPosition({ - scrollLeft: calculatedScrollLeft + scrollLeft: calculatedScrollLeft, + scrollTop: -1 }); } } - _getCalculatedScrollTop(props = this.props, state = this.state) { + _getCalculatedScrollTop( + props: Props = this.props, + state: State = this.state + ) { const { height, rowCount, scrollToAlignment, scrollToRow, width } = props; const { scrollTop } = state; @@ -1292,25 +1387,31 @@ export default class Grid extends PureComponent { }); } - _updateScrollTopForScrollToRow(props = this.props, state = this.state) { + _updateScrollTopForScrollToRow( + props: Props = this.props, + state: State = this.state + ) { const { scrollTop } = state; const calculatedScrollTop = this._getCalculatedScrollTop(props, state); - if (calculatedScrollTop >= 0 && scrollTop !== calculatedScrollTop) { + if ( + typeof calculatedScrollTop === "number" && + calculatedScrollTop >= 0 && + scrollTop !== calculatedScrollTop + ) { this._setScrollPosition({ + scrollLeft: -1, scrollTop: calculatedScrollTop }); } } - _onScroll(event) { + _onScroll = (event: Event) => { // In certain edge-cases React dispatches an onScroll event with an invalid target.scrollLeft / target.scrollTop. // This invalid event can be detected by comparing event.target to this component's scrollable DOM element. // See issue #404 for more information. - if (event.target !== this._scrollingContainer) { - return; + if (event.target === this._scrollingContainer) { + this.handleScrollEvent((event.target: any)); } - - this.handleScrollEvent(event.target); - } + }; } diff --git a/source/Grid/accessibilityOverscanIndicesGetter.js b/source/Grid/accessibilityOverscanIndicesGetter.js index 50204253a..00dfdb90f 100644 --- a/source/Grid/accessibilityOverscanIndicesGetter.js +++ b/source/Grid/accessibilityOverscanIndicesGetter.js @@ -1,47 +1,62 @@ +// @flow + export const SCROLL_DIRECTION_BACKWARD = -1; export const SCROLL_DIRECTION_FORWARD = 1; export const SCROLL_DIRECTION_HORIZONTAL = "horizontal"; export const SCROLL_DIRECTION_VERTICAL = "vertical"; + /** * Calculates the number of cells to overscan before and after a specified range. * This function ensures that overscanning doesn't exceed the available cells. - * - * @param direction One of SCROLL_DIRECTION_HORIZONTAL or SCROLL_DIRECTION_VERTICAL - * @param cellCount Number of rows or columns in the current axis - * @param scrollDirection One of SCROLL_DIRECTION_BACKWARD or SCROLL_DIRECTION_FORWARD - * @param overscanCellsCount Maximum number of cells to over-render in either direction - * @param startIndex Begin of range of visible cells - * @param stopIndex End of range of visible cells */ + +type Params = { + // One of SCROLL_DIRECTION_HORIZONTAL or SCROLL_DIRECTION_VERTICAL + direction: "horizontal" | "vertical", + + // Number of rows or columns in the current axis + cellCount: number, + + // One of SCROLL_DIRECTION_BACKWARD or SCROLL_DIRECTION_FORWARD + scrollDirection: -1 | 1, + + // Maximum number of cells to over-render in either direction + overscanCellsCount: number, + + // Begin of range of visible cells + startIndex: number, + + // End of range of visible cells + stopIndex: number +}; + +type Returns = { + overscanStartIndex: number, + overscanStopIndex: number +}; + export default function defaultOverscanIndicesGetter({ cellCount, overscanCellsCount, scrollDirection, startIndex, stopIndex -}) { - let overscanStartIndex; - let overscanStopIndex; - +}: Params): Returns { // Make sure we render at least 1 cell extra before and after (except near boundaries) // This is necessary in order to support keyboard navigation (TAB/SHIFT+TAB) in some cases // For more info see issues #625 overscanCellsCount = Math.max(1, overscanCellsCount); - switch (scrollDirection) { - case SCROLL_DIRECTION_FORWARD: - overscanStartIndex = startIndex - 1; - overscanStopIndex = stopIndex + overscanCellsCount; - break; - case SCROLL_DIRECTION_BACKWARD: - overscanStartIndex = startIndex - overscanCellsCount; - overscanStopIndex = stopIndex + 1; - break; + if (scrollDirection === SCROLL_DIRECTION_FORWARD) { + return { + overscanStartIndex: Math.max(0, startIndex - 1), + overscanStopIndex: Math.min(cellCount - 1, stopIndex + overscanCellsCount) + }; + } else { + return { + overscanStartIndex: Math.max(0, startIndex - overscanCellsCount), + overscanStopIndex: Math.min(cellCount - 1, stopIndex + 1) + }; } - - return { - overscanStartIndex: Math.max(0, overscanStartIndex), - overscanStopIndex: Math.min(cellCount - 1, overscanStopIndex) - }; } diff --git a/source/Grid/defaultCellRangeRenderer.js b/source/Grid/defaultCellRangeRenderer.js index 1bf86b60b..7f79227f6 100644 --- a/source/Grid/defaultCellRangeRenderer.js +++ b/source/Grid/defaultCellRangeRenderer.js @@ -1,8 +1,33 @@ /** @flow */ + +import CellSizeAndPositionManager from "./utils/CellSizeAndPositionManager.js"; + /** * Default implementation of cellRangeRenderer used by Grid. * This renderer supports cell-caching while the user is scrolling. */ + +type DefaultCellRangeRendererParams = { + cellCache: Object, + cellRenderer: Function, + columnSizeAndPositionManager: CellSizeAndPositionManager, + columnStartIndex: number, + columnStopIndex: number, + deferredMeasurementCache: Object, + horizontalOffsetAdjustment: number, + isScrolling: boolean, + parent: any, + rowSizeAndPositionManager: CellSizeAndPositionManager, + rowStartIndex: number, + rowStopIndex: number, + scrollLeft: number, + scrollTop: number, + styleCache: Object, + verticalOffsetAdjustment: number, + visibleColumnIndices: Object, + visibleRowIndices: Object +}; + export default function defaultCellRangeRenderer({ cellCache, cellRenderer, @@ -163,22 +188,3 @@ function warnAboutMissingStyle(parent, renderedCell) { } } } - -type DefaultCellRangeRendererParams = { - cellCache: Object, - cellRenderer: Function, - columnSizeAndPositionManager: Object, - columnStartIndex: number, - columnStopIndex: number, - horizontalOffsetAdjustment: number, - isScrolling: boolean, - rowSizeAndPositionManager: Object, - rowStartIndex: number, - rowStopIndex: number, - scrollLeft: number, - scrollTop: number, - styleCache: Object, - verticalOffsetAdjustment: number, - visibleColumnIndices: Object, - visibleRowIndices: Object -}; diff --git a/source/Grid/defaultOverscanIndicesGetter.js b/source/Grid/defaultOverscanIndicesGetter.js index b1d79030c..b5cf63104 100644 --- a/source/Grid/defaultOverscanIndicesGetter.js +++ b/source/Grid/defaultOverscanIndicesGetter.js @@ -1,8 +1,11 @@ +// @flow + export const SCROLL_DIRECTION_BACKWARD = -1; export const SCROLL_DIRECTION_FORWARD = 1; export const SCROLL_DIRECTION_HORIZONTAL = "horizontal"; export const SCROLL_DIRECTION_VERTICAL = "vertical"; + /** * Calculates the number of cells to overscan before and after a specified range. * This function ensures that overscanning doesn't exceed the available cells. @@ -14,29 +17,36 @@ export const SCROLL_DIRECTION_VERTICAL = "vertical"; * @param startIndex Begin of range of visible cells * @param stopIndex End of range of visible cells */ + +type Params = { + cellCount: number, + scrollDirection: -1 | 1, + overscanCellsCount: number, + startIndex: number, + stopIndex: number +}; + +type Returns = { + overscanStartIndex: number, + overscanStopIndex: number +}; + export default function defaultOverscanIndicesGetter({ cellCount, overscanCellsCount, scrollDirection, startIndex, stopIndex -}) { - let overscanStartIndex; - let overscanStopIndex; - - switch (scrollDirection) { - case SCROLL_DIRECTION_FORWARD: - overscanStartIndex = startIndex; - overscanStopIndex = stopIndex + overscanCellsCount; - break; - case SCROLL_DIRECTION_BACKWARD: - overscanStartIndex = startIndex - overscanCellsCount; - overscanStopIndex = stopIndex; - break; +}: Params): Returns { + if (scrollDirection === SCROLL_DIRECTION_FORWARD) { + return { + overscanStartIndex: Math.max(0, startIndex), + overscanStopIndex: Math.min(cellCount - 1, stopIndex + overscanCellsCount) + }; + } else { + return { + overscanStartIndex: Math.max(0, startIndex - overscanCellsCount), + overscanStopIndex: Math.min(cellCount - 1, stopIndex) + }; } - - return { - overscanStartIndex: Math.max(0, overscanStartIndex), - overscanStopIndex: Math.min(cellCount - 1, overscanStopIndex) - }; } diff --git a/source/Grid/index.js b/source/Grid/index.js index c73b71287..26995b19b 100644 --- a/source/Grid/index.js +++ b/source/Grid/index.js @@ -1,6 +1,13 @@ /** @flow */ -export default from "./Grid"; -export Grid from "./Grid"; -export accessibilityOverscanIndicesGetter from "./accessibilityOverscanIndicesGetter"; -export defaultCellRangeRenderer from "./defaultCellRangeRenderer"; -export defaultOverscanIndicesGetter from "./defaultOverscanIndicesGetter"; + +export { default } from "./Grid"; +export { default as Grid } from "./Grid"; +export { + default as accessibilityOverscanIndicesGetter +} from "./accessibilityOverscanIndicesGetter"; +export { + default as defaultCellRangeRenderer +} from "./defaultCellRangeRenderer"; +export { + default as defaultOverscanIndicesGetter +} from "./defaultOverscanIndicesGetter"; diff --git a/source/Grid/types.js b/source/Grid/types.js new file mode 100644 index 000000000..040e64d91 --- /dev/null +++ b/source/Grid/types.js @@ -0,0 +1,10 @@ +// @flow + +export type Alignment = "auto" | "end" | "start" | "center"; + +export type VisibleCellRange = { + start?: number, + stop?: number +}; + +export type CellSizeGetter = (params: { index: number }) => number; diff --git a/source/Grid/utils/CellSizeAndPositionManager.jest.js b/source/Grid/utils/CellSizeAndPositionManager.jest.js index c3c37a250..8b7c4f0c5 100644 --- a/source/Grid/utils/CellSizeAndPositionManager.jest.js +++ b/source/Grid/utils/CellSizeAndPositionManager.jest.js @@ -1,4 +1,3 @@ -/** @flow */ import CellSizeAndPositionManager from "./CellSizeAndPositionManager"; describe("CellSizeAndPositionManager", () => { diff --git a/source/Grid/utils/CellSizeAndPositionManager.js b/source/Grid/utils/CellSizeAndPositionManager.js index eb0f64697..fb4e37a64 100644 --- a/source/Grid/utils/CellSizeAndPositionManager.js +++ b/source/Grid/utils/CellSizeAndPositionManager.js @@ -1,36 +1,73 @@ /** @flow */ +import type { CellSizeGetter, Alignment, VisibleCellRange } from "../types.js"; + +type CellSizeAndPositionManagerParams = { + batchAllCells: boolean, + cellCount: number, + cellSizeGetter: CellSizeGetter, + estimatedCellSize: number +}; + +type ConfigureParams = { + cellCount: number, + estimatedCellSize: number +}; + +type GetUpdatedOffsetForIndex = { + align: Alignment, + containerSize: number, + currentOffset: number, + targetIndex: number +}; + +type GetVisibleCellRangeParams = { + containerSize: number, + offset: number +}; + +type SizeAndPositionData = { + offset: number, + size: number +}; + /** * Just-in-time calculates and caches size and position information for a collection of cells. */ + export default class CellSizeAndPositionManager { + // Cache of size and position data for cells, mapped by cell index. + // Note that invalid values may exist in this map so only rely on cells up to this._lastMeasuredIndex + _cellSizeAndPositionData = {}; + + // Measurements for cells up to this index can be trusted; cells afterward should be estimated. + _lastMeasuredIndex = -1; + + // Used in deferred mode to track which cells have been queued for measurement. + _lastBatchedIndex = -1; + + _batchAllCells: boolean; + _cellCount: number; + _cellSizeGetter: CellSizeGetter; + _estimatedCellSize: number; + constructor({ batchAllCells = false, cellCount, cellSizeGetter, estimatedCellSize - }: CellSizeAndPositionManagerConstructorParams) { + }: CellSizeAndPositionManagerParams) { this._batchAllCells = batchAllCells; this._cellSizeGetter = cellSizeGetter; this._cellCount = cellCount; this._estimatedCellSize = estimatedCellSize; - - // Cache of size and position data for cells, mapped by cell index. - // Note that invalid values may exist in this map so only rely on cells up to this._lastMeasuredIndex - this._cellSizeAndPositionData = {}; - - // Measurements for cells up to this index can be trusted; cells afterward should be estimated. - this._lastMeasuredIndex = -1; - - // Used in deferred mode to track which cells have been queued for measurement. - this._lastBatchedIndex = -1; } - areOffsetsAdjusted(): boolean { + areOffsetsAdjusted() { return false; } - configure({ cellCount, estimatedCellSize }: ConfigureParams): void { + configure({ cellCount, estimatedCellSize }: ConfigureParams) { this._cellCount = cellCount; this._estimatedCellSize = estimatedCellSize; } @@ -47,7 +84,7 @@ export default class CellSizeAndPositionManager { return this._lastMeasuredIndex; } - getOffsetAdjustment(): number { + getOffsetAdjustment() { return 0; } @@ -138,7 +175,7 @@ export default class CellSizeAndPositionManager { containerSize, currentOffset, targetIndex - }) { + }: GetUpdatedOffsetForIndex): number { if (containerSize <= 0) { return 0; } @@ -216,13 +253,10 @@ export default class CellSizeAndPositionManager { this._lastMeasuredIndex = Math.min(this._lastMeasuredIndex, index - 1); } - _binarySearch({ high, low, offset }): number { - let middle; - let currentOffset; - + _binarySearch(high: number, low: number, offset: number): number { while (low <= high) { - middle = low + Math.floor((high - low) / 2); - currentOffset = this.getSizeAndPositionOfCell(middle).offset; + const middle = low + Math.floor((high - low) / 2); + const currentOffset = this.getSizeAndPositionOfCell(middle).offset; if (currentOffset === offset) { return middle; @@ -235,10 +269,12 @@ export default class CellSizeAndPositionManager { if (low > 0) { return low - 1; + } else { + return 0; } } - _exponentialSearch({ index, offset }): number { + _exponentialSearch(index: number, offset: number): number { let interval = 1; while ( @@ -249,11 +285,11 @@ export default class CellSizeAndPositionManager { interval *= 2; } - return this._binarySearch({ - high: Math.min(index, this._cellCount - 1), - low: Math.floor(index / 2), + return this._binarySearch( + Math.min(index, this._cellCount - 1), + Math.floor(index / 2), offset - }); + ); } /** @@ -276,46 +312,12 @@ export default class CellSizeAndPositionManager { if (lastMeasuredCellSizeAndPosition.offset >= offset) { // If we've already measured cells within this range just use a binary search as it's faster. - return this._binarySearch({ - high: lastMeasuredIndex, - low: 0, - offset - }); + return this._binarySearch(lastMeasuredIndex, 0, offset); } else { // If we haven't yet measured this high, fallback to an exponential search with an inner binary search. // The exponential search avoids pre-computing sizes for the full set of cells as a binary search would. // The overall complexity for this approach is O(log n). - return this._exponentialSearch({ - index: lastMeasuredIndex, - offset - }); + return this._exponentialSearch(lastMeasuredIndex, offset); } } } - -type CellSizeAndPositionManagerConstructorParams = { - batchAllCells?: boolean, - cellCount: number, - cellSizeGetter: Function, - estimatedCellSize: number -}; - -type ConfigureParams = { - cellCount: number, - estimatedCellSize: number -}; - -type GetVisibleCellRangeParams = { - containerSize: number, - offset: number -}; - -type SizeAndPositionData = { - offset: number, - size: number -}; - -type VisibleCellRange = { - start: ?number, - stop: ?number -}; diff --git a/source/Grid/utils/ScalingCellSizeAndPositionManager.jest.js b/source/Grid/utils/ScalingCellSizeAndPositionManager.jest.js index 93b238a13..e5e34fd1f 100644 --- a/source/Grid/utils/ScalingCellSizeAndPositionManager.jest.js +++ b/source/Grid/utils/ScalingCellSizeAndPositionManager.jest.js @@ -1,4 +1,3 @@ -/** @flow */ import ScalingCellSizeAndPositionManager from "./ScalingCellSizeAndPositionManager"; describe("ScalingCellSizeAndPositionManager", () => { diff --git a/source/Grid/utils/ScalingCellSizeAndPositionManager.js b/source/Grid/utils/ScalingCellSizeAndPositionManager.js index cf1fc62ce..1b6e6a181 100644 --- a/source/Grid/utils/ScalingCellSizeAndPositionManager.js +++ b/source/Grid/utils/ScalingCellSizeAndPositionManager.js @@ -1,6 +1,14 @@ /** @flow */ + +import type { CellSizeGetter, Alignment, VisibleCellRange } from "../types.js"; + import CellSizeAndPositionManager from "./CellSizeAndPositionManager"; +type ContainerSizeAndOffset = { + containerSize: number, + offset: number +}; + /** * Browsers have scroll offset limitations (eg Chrome stops scrolling at ~33.5M pixels where as Edge tops out at ~1.5M pixels). * After a certain position, the browser won't allow the user to scroll further (even via JavaScript scroll offset adjustments). @@ -8,11 +16,22 @@ import CellSizeAndPositionManager from "./CellSizeAndPositionManager"; */ export const DEFAULT_MAX_SCROLL_SIZE = 1500000; +type Params = { + maxScrollSize?: number, + batchAllCells: boolean, + cellCount: number, + cellSizeGetter: CellSizeGetter, + estimatedCellSize: number +}; + /** * Extends CellSizeAndPositionManager and adds scaling behavior for lists that are too large to fit within a browser's native limits. */ export default class ScalingCellSizeAndPositionManager { - constructor({ maxScrollSize = DEFAULT_MAX_SCROLL_SIZE, ...params }) { + _cellSizeAndPositionManager: CellSizeAndPositionManager; + _maxScrollSize: number; + + constructor({ maxScrollSize = DEFAULT_MAX_SCROLL_SIZE, ...params }: Params) { // Favor composition over inheritance to simplify IE10 support this._cellSizeAndPositionManager = new CellSizeAndPositionManager(params); this._maxScrollSize = maxScrollSize; @@ -24,7 +43,7 @@ export default class ScalingCellSizeAndPositionManager { ); } - configure(params): void { + configure(params: { cellCount: number, estimatedCellSize: number }) { this._cellSizeAndPositionManager.configure(params); } @@ -80,8 +99,12 @@ export default class ScalingCellSizeAndPositionManager { align = "auto", containerSize, currentOffset, // safe - targetIndex, - totalSize + targetIndex + }: { + align: Alignment, + containerSize: number, + currentOffset: number, + targetIndex: number }) { currentOffset = this._safeOffsetToOffset({ containerSize, @@ -92,8 +115,7 @@ export default class ScalingCellSizeAndPositionManager { align, containerSize, currentOffset, - targetIndex, - totalSize + targetIndex }); return this._offsetToSafeOffset({ @@ -126,6 +148,10 @@ export default class ScalingCellSizeAndPositionManager { containerSize, offset, // safe totalSize + }: { + containerSize: number, + offset: number, + totalSize: number }) { return totalSize <= containerSize ? 0 @@ -172,13 +198,3 @@ export default class ScalingCellSizeAndPositionManager { } } } - -type ContainerSizeAndOffset = { - containerSize: number, - offset: number -}; - -type VisibleCellRange = { - start: ?number, - stop: ?number -}; diff --git a/source/Grid/utils/calculateSizeAndPositionDataAndUpdateScrollOffset.js b/source/Grid/utils/calculateSizeAndPositionDataAndUpdateScrollOffset.js index 9610602dd..f855efe7d 100644 --- a/source/Grid/utils/calculateSizeAndPositionDataAndUpdateScrollOffset.js +++ b/source/Grid/utils/calculateSizeAndPositionDataAndUpdateScrollOffset.js @@ -1,16 +1,38 @@ +// @flow + /** * Helper method that determines when to recalculate row or column metadata. - * - * @param cellCount Number of rows or columns in the current axis - * @param cellsSize Width or height of cells for the current axis - * @param computeMetadataCallback Method to invoke if cell metadata should be recalculated - * @param computeMetadataCallbackProps Parameters to pass to :computeMetadataCallback - * @param nextCellsCount Newly updated number of rows or columns in the current axis - * @param nextCellsSize Newly updated width or height of cells for the current axis - * @param nextScrollToIndex Newly updated scroll-to-index - * @param scrollToIndex Scroll-to-index - * @param updateScrollOffsetForScrollToIndex Callback to invoke if the scroll position should be recalculated */ + +type Params = { + // Number of rows or columns in the current axis + cellCount: number, + + // Width or height of cells for the current axis + cellSize: ?number, + + // Method to invoke if cell metadata should be recalculated + computeMetadataCallback: (props: T) => void, + + // Parameters to pass to :computeMetadataCallback + computeMetadataCallbackProps: T, + + // Newly updated number of rows or columns in the current axis + nextCellsCount: number, + + // Newly updated width or height of cells for the current axis + nextCellSize: ?number, + + // Newly updated scroll-to-index + nextScrollToIndex: number, + + // Scroll-to-index + scrollToIndex: number, + + // Callback to invoke if the scroll position should be recalculated + updateScrollOffsetForScrollToIndex: () => void +}; + export default function calculateSizeAndPositionDataAndUpdateScrollOffset({ cellCount, cellSize, @@ -21,7 +43,7 @@ export default function calculateSizeAndPositionDataAndUpdateScrollOffset({ nextScrollToIndex, scrollToIndex, updateScrollOffsetForScrollToIndex -}) { +}: Params<*>) { // Don't compare cell sizes if they are functions because inline functions would cause infinite loops. // In that event users should use the manual recompute methods to inform of changes. if ( diff --git a/source/Grid/utils/updateScrollIndexHelper.js b/source/Grid/utils/updateScrollIndexHelper.js index d3f42cfbe..b0c7cb8e5 100644 --- a/source/Grid/utils/updateScrollIndexHelper.js +++ b/source/Grid/utils/updateScrollIndexHelper.js @@ -1,18 +1,52 @@ +// @flow + +import type { Alignment } from "../types.js"; + +import ScalingCellSizeAndPositionManager from "./ScalingCellSizeAndPositionManager.js"; + /** * Helper function that determines when to update scroll offsets to ensure that a scroll-to-index remains visible. * This function also ensures that the scroll ofset isn't past the last column/row of cells. - * - * @param cellsSize Width or height of cells for the current axis - * @param cellSizeAndPositionManager Manages size and position metadata of cells - * @param previousCellsCount Previous number of rows or columns - * @param previousCellsSize Previous width or height of cells - * @param previousScrollToIndex Previous scroll-to-index - * @param previousSize Previous width or height of the virtualized container - * @param scrollOffset Current scrollLeft or scrollTop - * @param scrollToIndex Scroll-to-index - * @param size Width or height of the virtualized container - * @param updateScrollIndexCallback Callback to invoke with an scroll-to-index value */ + +type Params = { + // Width or height of cells for the current axis + cellSize?: number, + + // Manages size and position metadata of cells + cellSizeAndPositionManager: ScalingCellSizeAndPositionManager, + + // Previous number of rows or columns + previousCellsCount: number, + + // Previous width or height of cells + previousCellSize: ?number, + + previousScrollToAlignment: Alignment, + + // Previous scroll-to-index + previousScrollToIndex: number, + + // Previous width or height of the virtualized container + previousSize: number, + + // Current scrollLeft or scrollTop + scrollOffset: number, + + scrollToAlignment: Alignment, + + // Scroll-to-index + scrollToIndex: number, + + // Width or height of the virtualized container + size: number, + + sizeJustIncreasedFromZero: boolean, + + // Callback to invoke with an scroll-to-index value + updateScrollIndexCallback: (index: number) => void +}; + export default function updateScrollIndexHelper({ cellSize, cellSizeAndPositionManager, @@ -27,7 +61,7 @@ export default function updateScrollIndexHelper({ size, sizeJustIncreasedFromZero, updateScrollIndexCallback -}) { +}: Params) { const cellCount = cellSizeAndPositionManager.getCellCount(); const hasScrollToIndex = scrollToIndex >= 0 && scrollToIndex < cellCount; const sizeHasChanged =