diff --git a/.gitignore b/.gitignore index 27ce11eb8..1df06a941 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store -node_modules -npm-debug.log build coverage +dist +node_modules +npm-debug.log diff --git a/.npmignore b/.npmignore index 330439e46..1b201fe21 100644 --- a/.npmignore +++ b/.npmignore @@ -1,4 +1,5 @@ build +codemods coverage docs source diff --git a/CHANGELOG.md b/CHANGELOG.md index a052f5fd8..7e95fca27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ Changelog ------------ +# 7.0.0 +Version 7 changes are described in detail on the [Version 7 Roadmap wiki page](https://github.com/bvaughn/react-virtualized/wiki/Version-7-Roadmap). +Upgrade instructions and [jscodeshift](https://github.com/facebook/jscodeshift) mods can also be found there. + ##### 6.3.2 Fixed edge-case bug in `Collection` where initial `scrollLeft` and `scrollTop` would not correctly adjust inner offsets. Thanks @edulan for the contribution! diff --git a/README.md b/README.md index d60dd9fea..d4d92afbf 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ There are also a couple of how-to guides: * [Displaying items in reverse order](docs/reverseList.md) * [Using AutoSizer](docs/usingAutoSizer.md) * [Creating an infinite-loading list](docs/creatingAnInfiniteLoadingList.md) +* [Displaying a reverse list](docs/reverseList.md) Examples --------------- @@ -71,6 +72,7 @@ Here are some online demos of each component: * [ArrowKeyStepper](https://bvaughn.github.io/react-virtualized/?component=ArrowKeyStepper) * [AutoSizer](https://bvaughn.github.io/react-virtualized/?component=AutoSizer) +* [CellMeasurer](https://bvaughn.github.io/react-virtualized/?component=CellMeasurer) * [Collection](https://bvaughn.github.io/react-virtualized/?component=Collection) * [ColumnSizer](https://bvaughn.github.io/react-virtualized/?component=ColumnSizer) * [FlexTable](https://bvaughn.github.io/react-virtualized/?component=FlexTable) @@ -82,6 +84,8 @@ Here are some online demos of each component: And here are some "recipe" type demos: * [Collapsable tree view](https://rawgit.com/bvaughn/react-virtualized/master/playground/tree.html) * [Full-page grid (spreadsheet)](https://rawgit.com/bvaughn/react-virtualized/master/playground/grid.html) +* [Dyanmic cell measuring](https://rawgit.com/bvaughn/react-virtualized/master/playground/chat.html) +* [Cell hover effects](https://rawgit.com/bvaughn/react-virtualized/master/playground/hover.html) Contributions ------------ diff --git a/codemods/6-to-7/rename-properties.js b/codemods/6-to-7/rename-properties.js new file mode 100644 index 000000000..0852d15ac --- /dev/null +++ b/codemods/6-to-7/rename-properties.js @@ -0,0 +1,63 @@ +'use strict' + +// Renames react-virtualized version 6.x properties to be version-7 compatible +module.exports = function transformer (file, api) { + const jscodeshift = api.jscodeshift + + let source = file.source + + // Rename variable references + for (var property in propertyRenameMap) { + source = jscodeshift(source) + .findVariableDeclarators(property) + .renameTo(propertyRenameMap[property]) + .toSource() + } + + // Rename JSX attributes + source = jscodeshift(source) + .find(jscodeshift.JSXAttribute) + .filter(shouldAttributeBeRenamed) + .replaceWith(renameReactVirtualizedAttribute) + .toSource() + + return source +} + +const reactVirtualizedElementNames = [ + 'ArrowKeyStepper', + 'AutoSizer', + 'Collection', + 'ColumnSizer', + 'FlexTable', + 'Grid', + 'ScrollSync', + 'VirtualScroll' +] + +// @param path jscodeshift.JSXAttribute +const attributeBelongsToReactVirtualizedElement = path => reactVirtualizedElementNames.includes(path.parent.value.name.name) + +// See https://github.com/bvaughn/react-virtualized/wiki/Version-7-Roadmap#clean-up-property-names +const propertyRenameMap = { + cellClassName: 'className', + columnsCount: 'columnCount', + overscanColumnsCount: 'overscanColumnCount', + overscanRowsCount: 'overscanRowCount', + renderCell: 'cellRenderer', + renderCellRanges: 'cellRangeRenderer', + rowsCount: 'rowCount' +} + +// @param path jscodeshift.JSXAttribute +const shouldAttributeBeRenamed = path => attributeBelongsToReactVirtualizedElement(path) && isAttributeInPropertyRenameMap(path) + +// @param path jscodeshift.JSXAttribute +const isAttributeInPropertyRenameMap = path => propertyRenameMap.hasOwnProperty(path.value.name.name) + +// @param path jscodeshift.JSXAttribute +const renameReactVirtualizedAttribute = path => { + path.value.name.name = propertyRenameMap[path.value.name.name] || path.value.name.name + + return path.node +} diff --git a/docs/ArrowKeyStepper.md b/docs/ArrowKeyStepper.md index e88f15cc9..0c5572b59 100644 --- a/docs/ArrowKeyStepper.md +++ b/docs/ArrowKeyStepper.md @@ -10,18 +10,17 @@ The appearance of this wrapper element can be customized using the `className` p ### Prop Types | Property | Type | Required? | Description | |:---|:---|:---:|:---| -| children | Function | ✓ | Function respondible for rendering children. This function should implement the following signature: `({ onKeyDown, onSectionRendered, scrollToColumn, scrollToRow }) => PropTypes.element` | +| children | Function | ✓ | Function respondible for rendering children. This function should implement the following signature: `({ onSectionRendered: Function, scrollToColumn: number, scrollToRow: number }) => PropTypes.element` | | className | String | | CSS class name to attach to the wrapper `
`. | -| columnsCount | Number | ✓ | Number of columns in grid; for `FlexTable` and `VirtualScroll` this property should always be `1`. | -| rowsCount | Number | ✓ | Number of rows in grid. | +| columnCount | Number | ✓ | Number of columns in grid; for `FlexTable` and `VirtualScroll` this property should always be `1`. | +| rowCount | Number | ✓ | Number of rows in grid. | ### Children function The child function is passed the following named parameters: | Parameter | Type | Description | -|:---|:---|:---:| -| onKeyDown | Function | Key-down event handler to be attached to the DOM hierarchy. | +|:---|:---|:---| | onSectionRendered | Function | Pass-through callback to be attached to child component; informs the key-stepper which range of cells are currently visible. | | scrollToColumn | Number | Specifies which column in the child component should be visible | | scrollToRow | Number | Specifies which row in the child component should be visible | @@ -38,20 +37,18 @@ import 'react-virtualized/styles.css'; // only needs to be imported once ReactDOM.render( - {({ onKeyDown, onSectionRendered, scrollToColumn, scrollToRow }) => ( -
- -
+ {({ onSectionRendered, scrollToColumn, scrollToRow }) => ( + )}
, document.getElementById('example') diff --git a/docs/AutoSizer.md b/docs/AutoSizer.md index e86c7ccc4..1078b5ae9 100644 --- a/docs/AutoSizer.md +++ b/docs/AutoSizer.md @@ -6,10 +6,10 @@ High-order component that automatically adjusts the width and height of a single ### Prop Types | Property | Type | Required? | Description | |:---|:---|:---:|:---| -| children | Function | ✓ | Function respondible for rendering children. This function should implement the following signature: `({ height, width }) => PropTypes.element` | -| disableHeight | Boolean | | If true the child's `height` property will not be managed | -| disableWidth | Boolean | | If true the child's `width` property will not be managed | -| onResize | Function | Callback to be invoked on-resize; it is passed the following named parameters: `({ height, width })` | +| children | Function | ✓ | Function respondible for rendering children. This function should implement the following signature: `({ height: number, width: number }) => PropTypes.element` | +| disableHeight | Boolean | | Fixed `height`; if specified, the child's `height` property will not be managed | +| disableWidth | Boolean | | Fixed `width`; if specified, the child's `width` property will not be managed | +| onResize | Function | | Callback to be invoked on-resize; it is passed the following named parameters: `({ height: number, width: number })`. | ### Examples @@ -36,7 +36,7 @@ ReactDOM.render( list[index] // Could also be a DOM element diff --git a/docs/CellMeasurer.md b/docs/CellMeasurer.md new file mode 100644 index 000000000..e6a58c599 --- /dev/null +++ b/docs/CellMeasurer.md @@ -0,0 +1,65 @@ +CellMeasurer +--------------- + +High-order component for automatically measuring a cell's contents by rendering it in a way that is not visible to the user. +Specify a fixed width or height constraint if you only want to measure one dimension. + +**Warning**: This HOC is fairly experimental and may change in future releases. +At this time it is only intended for use with a `Grid` (not `VirtualScroll` or `FlexTable` as their item rendering and cell measuring signatures are different). +Also note that in order to measure a column's width for a `Grid`, that column's content must be rendered for all rows in order to determine the maximum width. +For this reason it may not be a good idea to use this HOC for `Grid`s containing a large number of both columns _and_ cells. + +### Prop Types +| Property | Type | Required? | Description | +|:---|:---|:---:|:---| +| cellRenderer | Function | ✓ | Renders a cell given its indices. `({ columnIndex: number, rowIndex: number }): PropTypes.node` | +| children | Function | ✓ | Function respondible for rendering a virtualized component; `({ getColumnWidth: Function, getRowHeight: Function, resetMeasurements: Function }) => PropTypes.element` | +| columnCount | number | ✓ | Number of columns in the `Grid`; in order to measure a row's height, all of that row's columns must be rendered. | +| container | | | A Node, Component instance, or function that returns either. If this property is not specified the document body will be used. | +| height | number | | Fixed height; specify this property to measure cell-width only. | +| rowCount | number | ✓ | Number of rows in the `Grid`; in order to measure a column's width, all of that column's rows must be rendered. | +| width | number | | Fixed width; specify this property to measure cell-height only. | + +### Children function + +The child function is passed the following named parameters: + +| Parameter | Type | Description | +|:---|:---|:---| +| getColumnWidth | Function | Callback to set as the `columnWidth` property of a `Grid` | +| getRowHeight | Function | Callback to set as the `rowHeight` property of a `Grid` | +| resetMeasurements | Function | Use this function to clear cached measurements in `CellRenderer`; its size will be remeasured the next time it is requested. | + +### Examples + +This example shows a `Grid` with fixed column widths and dynamic row heights. +For more examples check out the component [demo page](https://bvaughn.github.io/react-virtualized/?component=CellMeasurer). + +```javascript +import React from 'react'; +import ReactDOM from 'react-dom'; +import { CellMeasurer, Grid } from 'react-virtualized'; +import 'react-virtualized/styles.css'; // only needs to be imported once + +ReactDOM.render( + + {({ getColumnWidth }) => ( + + )} + , + document.getElementById('example') +); +``` diff --git a/docs/Collection.md b/docs/Collection.md index bf2b6bb25..c4656f706 100644 --- a/docs/Collection.md +++ b/docs/Collection.md @@ -4,22 +4,25 @@ Collection Renders scattered or non-linear data. Unlike `Grid`, which renders checkerboard data, `Collection` can render arbitrarily positioned- even overlapping- data. +**Note** that this component's measuring and layout phase is more expensive than `Grid` since it can not assume a correlation between a cell's index and position. For this reason it will take signifnicantly longer to initialize than the more linear/checkerboard components. + ### Prop Types | Property | Type | Required? | Description | |:---|:---|:---:|:---| | className | String | | Optional custom CSS class name to attach to root Collection element. | | cellCount | Number | ✓ | Number of cells in collection. | +| cellGroupRenderer | Function | ✓ | Responsible for rendering a group of cells given their indices.: `({ cellSizeAndPositionGetter:Function, indices: Array, cellRenderer: Function }): Array` | +| cellRenderer | Function | ✓ | Responsible for rendering a cell given an row and column index: `({ index: number, isScrolling: boolean }): PropTypes.element` | +| cellSizeAndPositionGetter | Function | ✓ | Callback responsible for returning size and offset/position information for a given cell (index): `({ index: number }): { height: number, width: number, x: number, y: number }` | | height | Number | ✓ | Height of Collection; this property determines the number of visible (vs virtualized) rows. | | noContentRenderer | Function | | Optional renderer to be rendered inside the grid when `cellCount` is 0: `(): PropTypes.node` | -| onSectionRendered | Function | | Callback invoked with information about the section of the Collection that was just rendered: `(indices: Array): void` | -| onScroll | Function | | Callback invoked whenever the scroll offset changes within the inner scrollable region: `({ clientHeight, clientWidth, scrollHeight, scrollLeft, scrollTop, scrollWidth }): void` | -| cellRenderer | Function | ✓ | Responsible for rendering a cell given an row and column index: `(index: number): PropTypes.node` | -| cellGroupRenderer | Function | ✓ | Responsible for rendering a group of cells given their indices.: `({ cellSizeAndPositionGetter:Function, indices: Array, cellRenderer: Function }): Array` | -| cellSizeAndPositionGetter | Function | ✓ | Callback responsible for returning size and offset/position information for a given cell (index): `(index): { height: number, width: number, x: number, y: number }` | +| onSectionRendered | Function | | Callback invoked with information about the section of the Collection that was just rendered: `({ indices: Array }): void` | +| onScroll | Function | | Callback invoked whenever the scroll offset changes within the inner scrollable region: `({ clientHeight: number, clientWidth: number, scrollHeight: number, scrollLeft: number, scrollTop: number, scrollWidth: number }): void` | | scrollLeft | Number | | Horizontal offset | | scrollToCell | Number | | Cell index to ensure visible (by scrolling if necessary) | | scrollTop | Number | | Vertical offset | -| sectionSize | Number | | Optionally override the size of the sections a Collection's cells are split into. | +| sectionSize | Number | | Optionally override the size of the sections a Collection's cells are split into. This is an advanced option and should only be used for performance tuning purposes. | +| style | Object | | Optional custom inline style to attach to root Collection element. | | width | Number | ✓ | Width of Collection; this property determines the number of visible (vs virtualized) columns. | ### Public Methods @@ -29,7 +32,7 @@ Unlike `Grid`, which renders checkerboard data, `Collection` can render arbitrar Recomputes cell sizes and positions. This function should be called if cell sizes or positions have changed but nothing else has. -Since Collection only receives `cellCount` it has no way of detecting when the underlying data changes. +Since Collection only receives `cellCount` (and not the underlying List or Array) it has no way of detecting when the underlying data changes. ### Class names @@ -62,9 +65,16 @@ const list = [ ReactDOM.render( list[index].name} - cellSizeAndPositionGetter={(index) => list[index]} - columnsCount={list.length} + cellRenderer={({ index }) => list[index].name} + cellSizeAndPositionGetter={({ index }) => { + const datum = list[index] + return { + height: datum.height, + width: datum.width, + x: datum.x, + y: datum.y + } + }} height={300} width={300} />, diff --git a/docs/ColumnSizer.md b/docs/ColumnSizer.md index f604bda90..9ed0e6220 100644 --- a/docs/ColumnSizer.md +++ b/docs/ColumnSizer.md @@ -6,7 +6,7 @@ High-order component that auto-calculates column-widths for `Grid` cells. ### Prop Types | Property | Type | Required? | Description | |:---|:---|:---:|:---| -| children | Function | ✓ | Function respondible for rendering a virtualized Grid. This function should implement the following signature: `({ adjustedWidth, getColumnWidth, registerChild }) => PropTypes.element` | +| children | Function | ✓ | Function respondible for rendering a virtualized Grid. This function should implement the following signature: `({ adjustedWidth: number, getColumnWidth: Function, registerChild: Function }) => PropTypes.element` | | columnMaxWidth | Number | | Optional maximum allowed column width | | columnMinWidth | Number | | Optional minimum allowed column width | | width | Number | ✓ | Width of Grid or `FlexTable` child | @@ -16,14 +16,14 @@ High-order component that auto-calculates column-widths for `Grid` cells. The child function is passed the following named parameters: | Parameter | Type | Description | -|:---|:---|:---:| +|:---|:---|:---| +| adjustedWidth | Number | This number reflects the lesser of the overall `Grid` width or the width of all columns. Use this to make your `Grid` shrink to fit sparse content. | | getColumnWidth | Function | This function should be passed to the `Grid`'s `columnWidth` property. | | registerChild | Function | This function should be set as the child's `ref` property. It enables a set of rows to be refreshed once their data has finished loading. | -| adjustedWidth | Number | This number reflects the lesser of the overall `Grid` width or the width of all columns. Use this to make your `Grid` shrink to fit sparse content. | ### Examples -This example displays a `Grid` that shrinks to fit sparse content (using the `adjustedWidth` parameter). +This example displays a `Grid` that shrinks to fit sparse content (using the `adjustedWidth` parameter). An interactive demo of this component can be seen [here](https://bvaughn.github.io/react-virtualized/?component=ColumnSizer). ```javascript import React from 'react'; @@ -38,18 +38,18 @@ ReactDOM.render( {({ adjustedWidth, getColumnWidth, registerChild }) => ( )} diff --git a/docs/FlexColumn.md b/docs/FlexColumn.md index 4a833cd19..134d2f16c 100644 --- a/docs/FlexColumn.md +++ b/docs/FlexColumn.md @@ -1,16 +1,16 @@ FlexColumn --------------- -Describes the header and cell contents of a table column +Describes the header and cell contents of a table column. #### Prop Types | Property | Type | Required? | Description | |:---|:---|:---:|:---| -| cellClassName | String | | CSS class to apply to cell | | cellDataGetter | Function | | Callback responsible for returning a cell's data, given its `dataKey`. [Learn more](#celldatagetter) | | cellRenderer | Function | | Callback responsible for rendering a cell's contents. [Learn more](#cellrenderer) | -| columnData | Object | | Additional data passed to this column's `cellDataGetter` | -| dataKey | any | ✓ | Uniquely identifies the row-data attribute correspnding to this cell | +| className | String | | CSS class to apply to rendered cell container | +| columnData | Object | | Additional data passed to this column's `cellDataGetter`. Use this object to relay action-creators or relational data. | +| dataKey | any | ✓ | Uniquely identifies the row-data attribute correspnding to this cell (eg this might be "name" in an array of user objects). | | disableSort | Boolean | | If sort is enabled for the table at large, disable it for this column | | flexGrow | Number | | Flex grow style; defaults to 0 | | flexShrink | Number | | Flex shrink style; defaults to 1 | @@ -19,6 +19,7 @@ Describes the header and cell contents of a table column | label | String | | Header label for this column | | maxWidth | Number | | Maximum width of column; this property will only be used if :flexGrow is greater than 0 | | minWidth | Number | | Minimum width of column | +| style | Object | | Optional inline style to apply to rendered cell container | | width | Number | ✓ | Flex basis (width) for this column; This value can grow or shrink based on `flexGrow` and `flexShrink` properties | #### cellDataGetter @@ -27,10 +28,10 @@ Callback responsible for returning a cell's data, given its `dataKey`. It should implement the following signature: ```javascript -function (dataKey: string, rowData: any, columnData: any): any +function ({ columnData: any, dataKey: string, rowData: any }): any ``` -A default `cellDataGetter` is provided that simply returns the attribute as a String. +A [default `cellDataGetter`](https://github.com/bvaughn/react-virtualized/blob/master/source/FlexTable/defaultCellDataGetter.js) is provided that simply returns the attribute as a String. This function expects to operate on either a vanilla Object or a Map-like object with a get method. You should override this default method if your data is calculated or requires any custom processing. @@ -40,10 +41,10 @@ Callback responsible for rendering a cell's contents. It should implement the following signature: ```javascript -function (cellData: any, cellDataKey: string, rowData: any, rowIndex: number, columnData: any): element +function ({ cellData: any, columnData: any, dataKey: string, isScrolling: boolean, rowData: any, rowIndex: number }): node ``` -A default `cellRenderer` is provided that displays an attribute as a simple string +A [default `cellRenderer`](https://github.com/bvaughn/react-virtualized/blob/master/source/FlexTable/defaultCellRenderer.js) is provided that displays an attribute as a simple string You should override this default method if your data is some other type of object or requires custom formatting. #### headerRenderer @@ -55,5 +56,5 @@ It should implement the following signature: function ({ columnData: any, dataKey: string, disableSort: boolean, label: string, sortBy: string, sortDirection: SortDirection }): element ``` -A default `headerRenderer` is provided that displays the column `label` along with a sort indicator if the column is sort-enabled and active. +A [default `headerRenderer`](https://github.com/bvaughn/react-virtualized/blob/master/source/FlexTable/defaultHeaderRenderer.js) is provided that displays the column `label` along with a sort indicator if the column is sort-enabled and active. You should override this default method if you want to customize the appearance of table columns. diff --git a/docs/FlexTable.md b/docs/FlexTable.md index 9e8e5969a..f5bc73a53 100644 --- a/docs/FlexTable.md +++ b/docs/FlexTable.md @@ -7,27 +7,30 @@ This component expects explicit width, height, and padding parameters. ### Prop Types | Property | Type | Required? | Description | |:---|:---|:---:|:---| -| children | [FlexColumn](FlexColumn.md) | ✓ | One or more FlexColumns describing the data displayed in this row | -| className | String | | CSS class name | -| disableHeader | Boolean | | Disable rendering the header at all | +| children | [FlexColumn](FlexColumn.md) | ✓ | One or more FlexColumns describing the data displayed in this table | +| className | String | | Optional custom CSS class name to attach to root `FlexTable` element. | +| disableHeader | Boolean | | Do not render the table header (only the rows) | | headerClassName | String | | CSS class to apply to all column headers | | headerHeight | Number | ✓ | Fixed height of header row | +| headerStyle | Object | | Optional custom inline style to attach to table header columns. | | height | Number | ✓ | Fixed/available height for out DOM element | -| noRowsRenderer | | Function | Callback used to render placeholder content when :rowsCount is 0 | +| noRowsRenderer | | Function | Callback used to render placeholder content when :rowCount is 0 | | onHeaderClick | | Function | Callback invoked when a user clicks on a table header. `(dataKey: string, columnData: any): void` | -| onRowClick | | Function | Callback invoked when a user clicks on a table row. `(rowIndex: number): void` | -| onRowsRendered | | Function | Callback invoked with information about the slice of rows that were just rendered: `({ overscanStartIndex, overscanStopIndex, startIndex, stopIndex }): void` | -| overscanRowsCount | | Number | Number of rows to render above/below the visible bounds of the list. This can help reduce flickering during scrolling on certain browers/devices. | -| onScroll | Function | | Callback invoked whenever the scroll offset changes within the inner scrollable region: `({ clientHeight, scrollHeight, scrollTop }): void` | -| rowClassName | String or Function | | CSS class to apply to all table rows (including the header row). This value may be either a static string or a function with the signature `(rowIndex: number): string`. Note that for the header row an index of `-1` is provided. | -| rowGetter | Function | ✓ | Callback responsible for returning a data row given an index. `(index: int): any` | -| rowHeight | Number or Function | ✓ | Either a fixed row height (number) or a function that returns the height of a row given its index: `(index: number): number` | -| rowsCount | Number | ✓ | Number of rows in table. | +| onRowClick | | Function | Callback invoked when a user clicks on a table row. `({ index: number }): void` | +| onRowsRendered | | Function | Callback invoked with information about the slice of rows that were just rendered: `({ overscanStartIndex: number, overscanStopIndex: number, startIndex: number, stopIndex: number }): void` | +| overscanRowCount | | Number | Number of rows to render above/below the visible bounds of the list. This can help reduce flickering during scrolling on certain browers/devices. | +| onScroll | Function | | Callback invoked whenever the scroll offset changes within the inner scrollable region: `({ clientHeight: number, scrollHeight: number: number, scrollTop: number }): void` | +| rowClassName | String or Function | | CSS class to apply to all table rows (including the header row). This value may be either a static string or a function with the signature `({ index: number }): string`. Note that for the header row an index of `-1` is provided. | +| rowCount | Number | ✓ | Number of rows in table. | +| rowGetter | Function | ✓ | Callback responsible for returning a data row given an index. `({ index: int }): any` | +| rowHeight | Number or Function | ✓ | Either a fixed row height (number) or a function that returns the height of a row given its index: `({ index: number }): number` | +| rowStyle | Object | | Optional custom inline style to attach to table rows. | | scrollToIndex | Number | | Row index to ensure visible (by forcefully scrolling if necessary) | | scrollTop | Number | | Vertical offset | -| sort | Function | | Sort function to be called if a sortable header is clicked. `(dataKey: string, sortDirection: SortDirection): void` | +| sort | Function | | Sort function to be called if a sortable header is clicked. `({ sortBy: string, sortDirection: SortDirection }): void` | | sortBy | String | | Data is currently sorted by this `dataKey` (if it is sorted at all) | | sortDirection | [SortDirection](SortDirection.md) | | Data is currently sorted in this direction (if it is sorted at all) | +| style | Object | | Optional custom inline style to attach to root `FlexTable` element. | | width | Number | ✓ | Width of the table | ### Public Methods @@ -76,7 +79,7 @@ ReactDOM.render( height={300} headerHeight={20} rowHeight={30} - rowsCount={list.length} + rowCount={list.length} rowGetter={index => list[index]} > `. [Learn more](#cellRangeRenderer) | +| cellRenderer | Function | ✓ | Responsible for rendering a cell given an row and column index: `({ columnIndex: number, isScrolling: boolean, rowIndex: number }): PropTypes.node` | +| className | String | | Optional custom CSS class name to attach to root `Grid` element. | +| columnCount | Number | ✓ | Number of columns in grid. | +| columnWidth | Number or Function | ✓ | Either a fixed column width (number) or a function that returns the width of a column given its index: `({ index: number }): number` | | height | Number | ✓ | Height of Grid; this property determines the number of visible (vs virtualized) rows. | -| noContentRenderer | Function | | Optional renderer to be rendered inside the grid when either `rowsCount` or `columnsCount` is 0: `(): PropTypes.node` | -| onSectionRendered | Function | | Callback invoked with information about the section of the Grid that was just rendered: `({ columnOverscanStartIndex, columnOverscanStopIndex, columnStartIndex, columnStopIndex, rowOverscanStartIndex, rowOverscanStopIndex, rowStartIndex, rowStopIndex }): void` | -| onScroll | Function | | Callback invoked whenever the scroll offset changes within the inner scrollable region: `({ clientHeight, clientWidth, scrollHeight, scrollLeft, scrollTop, scrollWidth }): void` | -| overscanColumnsCount | Number | | Number of columns to render before/after the visible slice of the grid. This can help reduce flickering during scrolling on certain browers/devices. | -| overscanRowsCount | Number | | Number of rows to render above/below the visible slice of the grid. This can help reduce flickering during scrolling on certain browers/devices. | -| renderCell | Function | ✓ | Responsible for rendering a cell given an row and column index: `({ columnIndex: number, rowIndex: number }): PropTypes.node` | -| renderCellRanges | Function | ✓ | Responsible for rendering a group of cells given their index ranges.: `({ columnMetadata:Array, columnStartIndex: number, columnStopIndex: number, renderCell: Function, rowMetadata:Array, rowStartIndex: number, rowStopIndex: number }): Array` | -| rowsCount | Number | ✓ | Number of rows in grid. | -| rowHeight | Number or Function | ✓ | Either a fixed row height (number) or a function that returns the height of a row given its index: `(index: number): number` | +| noContentRenderer | Function | | Optional renderer to be rendered inside the grid when either `rowCount` or `columnCount` is empty: `(): PropTypes.node` | +| onSectionRendered | Function | | Callback invoked with information about the section of the Grid that was just rendered: `({ columnOverscanStartIndex: number, columnOverscanStopIndex: number, columnStartIndex: number, columnStopIndex: number, rowOverscanStartIndex: number, rowOverscanStopIndex: number, rowStartIndex: number, rowStopIndex: number }): void` | +| onScroll | Function | | Callback invoked whenever the scroll offset changes within the inner scrollable region: `({ clientHeight: number, clientWidth: number, scrollHeight: number, scrollLeft: number, scrollTop: number, scrollWidth: number }): void` | +| overscanColumnCount | Number | | Number of columns to render before/after the visible slice of the grid. This can help reduce flickering during scrolling on certain browers/devices. | +| overscanRowCount | Number | | Number of rows to render above/below the visible slice of the grid. This can help reduce flickering during scrolling on certain browers/devices. | +| rowCount | Number | ✓ | Number of rows in grid. | +| rowHeight | Number or Function | ✓ | Either a fixed row height (number) or a function that returns the height of a row given its index: `({ index: number }): number` | | scrollLeft | Number | | Horizontal offset | | scrollToColumn | Number | | Column index to ensure visible (by forcefully scrolling if necessary) | | scrollToRow | Number | | Row index to ensure visible (by forcefully scrolling if necessary) | | scrollTop | Number | | Vertical offset | +| style | Object | | Optional custom inline style to attach to root `Grid` element. | | width | Number | ✓ | Width of Grid; this property determines the number of visible (vs virtualized) columns. | ### Public Methods @@ -33,7 +34,7 @@ Only a small number of cells are rendered based on the horizontal and vertical s Recomputes row heights and column widths. This function should be called if dynamic column or row sizes have changed but nothing else has. -Since Grid only receives `columnsCount` and `rowsCount` it has no way of detecting when the underlying data changes. +Since Grid only receives `columnCount` and `rowCount` it has no way of detecting when the underlying data changes. ### Class names @@ -45,6 +46,48 @@ The Grid component supports the following static class names | Grid__innerScrollContainer | Inner scrollable area | | Grid__cell | Individual cell | +### cellRangeRenderer + +This is an advanced property. +It is useful for situations where the `Grid` requires additional, overlayed UI (such as a Gantt chart or a calendar application). +Many use cases can be solved more easily using the `onScroll` callback or the `ScrollSync` HOC. +If you do choose to implement your own range renderer though, consider starting by forking the [`defaultCellRangeRenderer`](https://github.com/bvaughn/react-virtualized/blob/master/source/Grid/defaultCellRangeRenderer.js) function. + +The general shape of your range renderer function should look something like the following: + +```js +function cellRangeRenderer ({ + cellCache, + cellRenderer, + columnSizeAndPositionManager, + columnStartIndex, + columnStopIndex, + isScrolling, + rowSizeAndPositionManager, + rowStartIndex, + rowStopIndex +}) { + const renderedCells = [] + + for (let rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) { + // This contains :offset (top) and :size (height) information for the cell + let rowDatum = rowSizeAndPositionManager.getSizeAndPositionOfCell(rowIndex) + + for (let columnIndex = columnStartIndex; columnIndex <= columnStopIndex; columnIndex++) { + // This contains :offset (left) and :size (width) information for the cell + let columnDatum = columnSizeAndPositionManager.getSizeAndPositionOfCell(columnIndex) + + // Now render your cell and additional UI as you see fit. + // Be sure to provide unique :key attributes. + // Add all rendered children to the :renderedCells Array. + } + } + + return renderedCells +} +} +``` + ### Examples Below is a very basic `Grid` example. The grid displays an array of objects with fixed row and column sizes. (Dynamic sizes are also supported but this example is intended to be basic.) [See here](../source/Grid/Grid.example.js) for a more full-featured example with dynamic cell sizes and more. @@ -68,9 +111,9 @@ ReactDOM.render( height={300} columnWidth={100} rowHeight={30} - columnsCount={list[0].length} - rowsCount={list.length} - renderCell={({ columnIndex, rowIndex }) => list[rowIndex][columnIndex]} + columnCount={list[0].length} + rowCount={list.length} + cellRenderer={({ columnIndex, isScrolling, rowIndex }) => list[rowIndex][columnIndex]} />, document.getElementById('example') ); diff --git a/docs/InfiniteLoader.md b/docs/InfiniteLoader.md index 6d021bbac..547eb5dc5 100644 --- a/docs/InfiniteLoader.md +++ b/docs/InfiniteLoader.md @@ -3,28 +3,33 @@ InfiniteLoader High-order component that manages just-in-time fetching of data as a user scrolls up or down in a list. +Note that this component is inteded to assist with row-loading. +As such it is best suited for use with `FlexTable` and `VirtualScroll` (although it can also be used with `Grid`). +This HOC is not compatible with the `Collection` component. + ### Prop Types | Property | Type | Required? | Description | |:---|:---|:---:|:---| -| children | Function | ✓ | Function responsible for rendering a virtualized component. This function should implement the following signature: `({ onRowsRendered, registerChild }) => PropTypes.element` | -| isRowLoaded | Function | ✓ | Function responsible for tracking the loaded state of each row. It should implement the following signature: `(index: number): boolean` | -| loadMoreRows | Function | ✓ | Callback to be invoked when more rows must be loaded. It should implement the following signature: `({ startIndex, stopIndex }): Promise`. The returned Promise should be resolved once row data has finished loading. It will be used to determine when to refresh the list with the newly-loaded data. This callback may be called multiple times in reaction to a single scroll event. | +| children | Function | ✓ | Function responsible for rendering a virtualized component. This function should implement the following signature: `({ onRowsRendered: Function, registerChild: Function }) => PropTypes.element` | +| isRowLoaded | Function | ✓ | Function responsible for tracking the loaded state of each row. It should implement the following signature: `({ index: number }): boolean` | +| loadMoreRows | Function | ✓ | Callback to be invoked when more rows must be loaded. It should implement the following signature: `({ startIndex: number, stopIndex: number }): Promise`. The returned Promise should be resolved once row data has finished loading. It will be used to determine when to refresh the list with the newly-loaded data. This callback may be called multiple times in reaction to a single scroll event. | | minimumBatchSize | Number | | Minimum number of rows to be loaded at a time. This property can be used to batch requests to reduce HTTP requests. Defaults to `10`. | -| rowsCount | Number | ✓ | Number of rows in list; can be arbitrary high number if actual number is unknown. | -| threshold | Number | | Threshold at which to pre-fetch data. A threshold X means that data will start loading when a user scrolls within X rows. This value defaults to 15. | +| rowCount | Number | ✓ | Number of rows in list; can be arbitrary high number if actual number is unknown. | +| threshold | Number | | Threshold at which to pre-fetch data. A threshold X means that data will start loading when a user scrolls within X rows. Defaults to `15`. | ### Children function The child function is passed the following named parameters: | Parameter | Type | Description | -|:---|:---|:---:| +|:---|:---|:---| | onRowsRendered | Function | This function should be passed as the child's `onRowsRendered` property. It informs the loader when the user is scrolling. | | registerChild | Function | This function should be set as the child's `ref` property. It enables a set of rows to be refreshed once their data has finished loading. | ### Examples This example uses `InfiniteLoader` to prefetch rows in a `VirtualScroll` list as a user scrolls. +An interactive demo can be seen [here](https://bvaughn.github.io/react-virtualized/?component=InfiniteLoader). ```js import React from 'react'; @@ -34,7 +39,7 @@ import 'react-virtualized/styles.css'; // only needs to be imported once const list = []; -function isRowLoaded (index) { +function isRowLoaded ({ index }) { return !!list[index]; } @@ -50,7 +55,7 @@ ReactDOM.render( {({ onRowsRendered, registerChild }) => ( list[index] // Could also be a DOM element diff --git a/docs/README.md b/docs/README.md index ffbe14e4f..c75b12147 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,8 +11,10 @@ Documentation * [VirtualScroll](VirtualScroll.md) ### High-Order Components + * [ArrowKeyStepper](ArrowKeyStepper.md) * [AutoSizer](AutoSizer.md) +* [CellMeasurer](CellMeasurer.md) * [ColumnSizer](ColumnSizer.md) * [InfiniteLoader](InfiniteLoader.md) * [ScrollSync](ScrollSync.md) @@ -23,3 +25,4 @@ Documentation * [Displaying items in reverse order](reverseList.md) * [Using AutoSizer](usingAutoSizer.md) * [Creating an infinite-loading list](creatingAnInfiniteLoadingList.md) +* [Displaying a reverse list](reverseList.md) diff --git a/docs/ScrollSync.md b/docs/ScrollSync.md index 349575ccf..ebc923b02 100644 --- a/docs/ScrollSync.md +++ b/docs/ScrollSync.md @@ -6,7 +6,7 @@ High order component that simplifies the process of synchronizing scrolling betw ### Prop Types | Property | Type | Required? | Description | |:---|:---|:---:|:---| -| children | Function | ✓ | Function responsible for rendering 2 or more virtualized components. This function should implement the following signature: `({ onScroll, scrollLeft, scrollTop }) => PropTypes.element` | +| children | Function | ✓ | Function responsible for rendering 2 or more virtualized components. [See below](#children-function) for details about this function's signature. | ### Children function diff --git a/docs/VirtualScroll.md b/docs/VirtualScroll.md index ef6a142b9..be1467cd7 100644 --- a/docs/VirtualScroll.md +++ b/docs/VirtualScroll.md @@ -6,17 +6,18 @@ This component renders a virtualized list of elements with either fixed or dynam ### Prop Types | Property | Type | Required? | Description | |:---|:---|:---:|:---| -| className | String | | CSS class name | +| className | String | | Optional custom CSS class name to attach to root `VirtualScroll` element. | | height | Number | ✓ | Height constraint for list (determines how many actual rows are rendered) | -| noRowsRenderer | Function | | Callback used to render placeholder content when `rowsCount` is 0 | -| onRowsRendered | Function | | Callback invoked with information about the slice of rows that were just rendered: `({ overscanStartIndex, overscanStopIndex, startIndex, stopIndex }): void` | -| onScroll | Function | | Callback invoked whenever the scroll offset changes within the inner scrollable region: `({ clientHeight, scrollHeight, scrollTop }): void` | -| overscanRowsCount | Number | | Number of rows to render above/below the visible bounds of the list. This can help reduce flickering during scrolling on certain browers/devices. | -| rowHeight | Number or Function | ✓ | Either a fixed row height (number) or a function that returns the height of a row given its index: `(index: number): number` | -| rowRenderer | Function | ✓ | Responsbile for rendering a row given an index. Signature should look like `(index: number): React.PropTypes.node` | -| rowsCount | Number | ✓ | Number of rows in list. | +| noRowsRenderer | Function | | Callback used to render placeholder content when `rowCount` is 0 | +| onRowsRendered | Function | | Callback invoked with information about the slice of rows that were just rendered: `({ overscanStartIndex: number, overscanStopIndex: number, startIndex: number, stopIndex: number }): void` | +| onScroll | Function | | Callback invoked whenever the scroll offset changes within the inner scrollable region: `({ clientHeight: number, scrollHeight: number, scrollTop: number }): void` | +| overscanRowCount | Number | | Number of rows to render above/below the visible bounds of the list. This can help reduce flickering during scrolling on certain browers/devices. | +| rowHeight | Number or Function | ✓ | Either a fixed row height (number) or a function that returns the height of a row given its index: `({ index: number }): number` | +| rowRenderer | Function | ✓ | Responsbile for rendering a row given an index. Signature should look like `({ index: number, isScrolling: boolean }): React.PropTypes.node` | +| rowCount | Number | ✓ | Number of rows in list. | | scrollToIndex | Number | | Row index to ensure visible (by forcefully scrolling if necessary) | -| scrollTop | Number | | Vertical offset | +| scrollTop | Number | | Forced vertical scroll offset; can be used to synchronize scrolling between components | +| style | Object | | Optional custom inline style to attach to root `VirtualScroll` element. | | width | Number | ✓ | Width of the list | ### Public Methods @@ -61,10 +62,10 @@ ReactDOM.render( list[index] // Could also be a DOM element + ({ index, isScrolling }) => list[index] // Could also be a DOM element } />, document.getElementById('example') diff --git a/docs/creatingAnInfiniteLoadingList.md b/docs/creatingAnInfiniteLoadingList.md index 23864a9d2..6820cc1a2 100644 --- a/docs/creatingAnInfiniteLoadingList.md +++ b/docs/creatingAnInfiniteLoadingList.md @@ -17,7 +17,7 @@ function MyComponent ({ loadNextPage }) { // If there are more items to be loaded then add an extra row to hold a loading indicator. - const rowsCount = hasNextPage + const rowCount = hasNextPage ? list.size + 1 : list.size @@ -43,7 +43,7 @@ function MyComponent ({ {({ onRowsRendered, registerChild }) => ( @@ -61,3 +61,5 @@ export default class Example extends Component { } } ``` + +You can see a demo of this [here](https://s3.amazonaws.com/brianvaughn/react-virtualized/reverse-list/index.html). diff --git a/package.json b/package.json index 27b3237ea..b94eec7b0 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "React components for efficiently rendering large, scrollable lists and tabular data", "author": "Brian Vaughn ", "user": "bvaughn", - "version": "6.3.2", + "version": "7.0.0", "homepage": "https://github.com/bvaughn/react-virtualized", "main": "dist/commonjs/index.js", "jsnext:main": "dist/es/index.js", diff --git a/playground/chat.html b/playground/chat.html new file mode 100644 index 000000000..f28f845b2 --- /dev/null +++ b/playground/chat.html @@ -0,0 +1,45 @@ + + + + + + foo + + + + + + + +
+ + + diff --git a/playground/chat.js b/playground/chat.js new file mode 100644 index 000000000..d11e525d4 --- /dev/null +++ b/playground/chat.js @@ -0,0 +1,124 @@ +function rowRenderer (params) { + var datum = chatHistory[params.index] + + return React.createElement( + 'div', + { + className: 'item' + }, + React.createElement( + 'strong', + null, + datum.name + ), + ':', + datum.text + ) +} + +function cellRenderer (params) { + return rowRenderer({ + index: params.rowIndex + }) +} + +var cellMeasurer +var virtualScroll +var mostRecentWidth + +var App = React.createClass({ + render: function () { + return React.createElement( + 'div', + { + className: 'container' + }, + React.createElement( + ReactVirtualized.AutoSizer, + {}, + function (autoSizerParams) { + if (mostRecentWidth && mostRecentWidth !== autoSizerParams.width) { + cellMeasurer.resetMeasurements() + virtualScroll.recomputeRowHeights() + } + + mostRecentWidth = autoSizerParams.width + + return React.createElement( + ReactVirtualized.CellMeasurer, + { + cellRenderer: cellRenderer, + columnCount: 1, + ref: function (ref) { + cellMeasurer = ref + }, + rowCount: chatHistory.length, + width: autoSizerParams.width + }, + function (cellMeasurerParams) { + return React.createElement( + ReactVirtualized.VirtualScroll, + { + className: 'chat', + height: autoSizerParams.height, + ref: function (ref) { + virtualScroll = ref + }, + rowCount: chatHistory.length, + rowHeight: cellMeasurerParams.getRowHeight, + rowRenderer: rowRenderer, + width: autoSizerParams.width + } + ) + } + ) + } + ) + ) + } +}) + +var NAMES = ['Peter Brimer', 'Tera Gaona', 'Kandy Liston', 'Lonna Wrede', 'Kristie Yard', 'Raul Host', 'Yukiko Binger', 'Velvet Natera', 'Donette Ponton', 'Loraine Grim', 'Shyla Mable', 'Marhta Sing', 'Alene Munden', 'Holley Pagel', 'Randell Tolman', 'Wilfred Juneau', 'Naida Madson', 'Marine Amison', 'Glinda Palazzo', 'Lupe Island', 'Cordelia Trotta', 'Samara Berrier', 'Era Stepp', 'Malka Spradlin', 'Edward Haner', 'Clemencia Feather', 'Loretta Rasnake', 'Dana Hasbrouck', 'Sanda Nery', 'Soo Reiling', 'Apolonia Volk', 'Liliana Cacho', 'Angel Couchman', 'Yvonne Adam', 'Jonas Curci', 'Tran Cesar', 'Buddy Panos', 'Rosita Ells', 'Rosalind Tavares', 'Renae Keehn', 'Deandrea Bester', 'Kelvin Lemmon', 'Guadalupe Mccullar', 'Zelma Mayers', 'Laurel Stcyr', 'Edyth Everette', 'Marylin Shevlin', 'Hsiu Blackwelder', 'Mark Ferguson', 'Winford Noggle', 'Shizuko Gilchrist', 'Roslyn Cress', 'Nilsa Lesniak', 'Agustin Grant', 'Earlie Jester', 'Libby Daigle', 'Shanna Maloy', 'Brendan Wilken', 'Windy Knittel', 'Alice Curren', 'Eden Lumsden', 'Klara Morfin', 'Sherryl Noack', 'Gala Munsey', 'Stephani Frew', 'Twana Anthony', 'Mauro Matlock', 'Claudie Meisner', 'Adrienne Petrarca', 'Pearlene Shurtleff', 'Rachelle Piro', 'Louis Cocco', 'Susann Mcsweeney', 'Mandi Kempker', 'Ola Moller', 'Leif Mcgahan', 'Tisha Wurster', 'Hector Pinkett', 'Benita Jemison', 'Kaley Findley', 'Jim Torkelson', 'Freda Okafor', 'Rafaela Markert', 'Stasia Carwile', 'Evia Kahler', 'Rocky Almon', 'Sonja Beals', 'Dee Fomby', 'Damon Eatman', 'Alma Grieve', 'Linsey Bollig', 'Stefan Cloninger', 'Giovanna Blind', 'Myrtis Remy', 'Marguerita Dostal', 'Junior Baranowski', 'Allene Seto', 'Margery Caves', 'Nelly Moudy', 'Felix Sailer'] +var SENTENCES = [ + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + 'Phasellus vulputate odio commodo tortor sodales, et vehicula ipsum viverra.', + 'Cras tincidunt nisi in urna molestie varius.', + 'Curabitur ac enim dictum arcu varius fermentum vel sodales dui.', + 'Ut tristique augue at congue molestie.', + 'Cras eget enim nec odio feugiat tristique eu quis ante.', + 'Phasellus eget enim vitae nunc luctus sodales a eu erat.', + 'Nulla bibendum quam id velit blandit dictum.', + 'Donec dignissim mi ac libero feugiat, vitae lacinia odio viverra.', + 'Praesent vel lectus venenatis, elementum mauris vitae, ullamcorper nulla.', + 'Quisque sollicitudin nulla nec tellus feugiat hendrerit.', + 'Vestibulum a eros accumsan, lacinia eros non, pretium diam.', + 'Donec ornare felis et dui hendrerit, eget bibendum nibh interdum.', + 'Donec nec diam vel tellus egestas lobortis.', + 'Sed ornare nisl sit amet dolor pellentesque, eu fermentum leo interdum.', + 'Sed eget mauris condimentum, molestie justo eu, feugiat felis.', + 'Sed luctus justo vitae nibh bibendum blandit.', + 'Nulla ac eros vestibulum, mollis ante eu, rutrum nulla.', + 'Sed cursus magna ut vehicula rutrum.' +] + +var chatHistory = [] + +for (var i = 0; i < 1000; i++) { + var name = NAMES[Math.floor(Math.random() * NAMES.length)] + var sentences = Math.ceil(Math.random() * 5) + var texts = [] + + for (var x = 0; x < sentences; x++) { + texts.push(SENTENCES[Math.floor(Math.random() * SENTENCES.length)]) + } + + chatHistory.push({ + name, + text: texts.join(' ') + }) +} + +ReactDOM.render( + React.createElement(App), + document.querySelector('#mount') +) diff --git a/playground/grid.html b/playground/grid.html index 44ae279bf..f99a9a523 100644 --- a/playground/grid.html +++ b/playground/grid.html @@ -36,8 +36,8 @@ width: 100%; } - - + +
diff --git a/playground/grid.js b/playground/grid.js index fb5ada1a5..c89de950e 100644 --- a/playground/grid.js +++ b/playground/grid.js @@ -1,7 +1,7 @@ var REACT_VIRTUALIZED_BANNER = 'https://cloud.githubusercontent.com/assets/29597/11737732/0ca1e55e-9f91-11e5-97f3-098f2f8ed866.png' -function getColumnWidth (columnIndex) { - switch (columnIndex % 3) { +function getColumnWidth (params) { + switch (params.index % 3) { case 0: return 65 case 1: @@ -11,7 +11,7 @@ function getColumnWidth (columnIndex) { } } -function renderCell (params) { +function cellRenderer (params) { var key = `c:${params.columnIndex}, r:${params.rowIndex}` switch (params.columnIndex % 3) { case 0: @@ -41,13 +41,13 @@ var App = React.createClass({ return React.createElement( ReactVirtualized.Grid, { - columnsCount: 1000, + columnCount: 1000, columnWidth: getColumnWidth, height: params.height, - overscanRowsCount: 0, - renderCell: renderCell, + overscanRowCount: 0, + cellRenderer: cellRenderer, rowHeight: 30, - rowsCount: 1000, + rowCount: 1000, width: params.width } ) diff --git a/playground/hover.html b/playground/hover.html index 86db1bb68..82161015c 100644 --- a/playground/hover.html +++ b/playground/hover.html @@ -30,8 +30,8 @@ background-color: rgba(0, 0, 0, .1); } - - + + diff --git a/playground/hover.js b/playground/hover.js index 55188e7d1..430e6e8ff 100644 --- a/playground/hover.js +++ b/playground/hover.js @@ -4,7 +4,7 @@ var App = React.createClass({ }, render: function() { - var renderCell = this._renderCell + var cellRenderer = this._cellRenderer return React.createElement( ReactVirtualized.AutoSizer, @@ -15,14 +15,14 @@ var App = React.createClass({ return React.createElement( ReactVirtualized.Grid, { - columnsCount: 1000, + columnCount: 1000, columnWidth: 100, height: params.height, - overscanRowsCount: 0, + overscanRowCount: 0, ref: 'Grid', - renderCell: renderCell, + cellRenderer: cellRenderer, rowHeight: 30, - rowsCount: 1000, + rowCount: 1000, width: params.width } ) @@ -30,7 +30,7 @@ var App = React.createClass({ ) }, - _renderCell (params) { + _cellRenderer (params) { var columnIndex = params.columnIndex var rowIndex = params.rowIndex var key = `c:${columnIndex}, r:${rowIndex}` diff --git a/playground/tree.html b/playground/tree.html index 4bf3fd397..f6d0300f8 100644 --- a/playground/tree.html +++ b/playground/tree.html @@ -30,8 +30,8 @@ user-select: none; } - - + + diff --git a/playground/tree.js b/playground/tree.js index 9b74d9849..a048e5ce9 100644 --- a/playground/tree.js +++ b/playground/tree.js @@ -7,6 +7,7 @@ function renderItem (item, keyPrefix) { event.stopPropagation() item.expanded = !item.expanded VirtualScroll.recomputeRowHeights() + VirtualScroll.forceUpdate() } var props = { key: keyPrefix } @@ -56,12 +57,12 @@ function setRef (ref) { VirtualScroll = ref } -function renderCell (index) { - return renderItem(data[index], index) +function cellRenderer (params) { + return renderItem(data[params.index], params.index) } -function rowHeight (index) { - return getExpandedItemCount(data[index]) * ROW_HEIGHT +function rowHeight (params) { + return getExpandedItemCount(data[params.index]) * ROW_HEIGHT } var App = React.createClass({ @@ -74,11 +75,11 @@ var App = React.createClass({ ReactVirtualized.VirtualScroll, { height: params.height, - overscanRowsCount: 10, + overscanRowCount: 10, ref: setRef, rowHeight: rowHeight, - rowRenderer: renderCell, - rowsCount: data.length, + rowRenderer: cellRenderer, + rowCount: data.length, width: params.width } ) diff --git a/source/ArrowKeyStepper/ArrowKeyStepper.example.css b/source/ArrowKeyStepper/ArrowKeyStepper.example.css index ee3d156bf..b1015f732 100644 --- a/source/ArrowKeyStepper/ArrowKeyStepper.example.css +++ b/source/ArrowKeyStepper/ArrowKeyStepper.example.css @@ -7,6 +7,7 @@ display: flex; align-items: center; justify-content: center; + text-align: center; border-right: 1px solid #e0e0e0; border-bottom: 1px solid #e0e0e0; } diff --git a/source/ArrowKeyStepper/ArrowKeyStepper.example.js b/source/ArrowKeyStepper/ArrowKeyStepper.example.js index 1c82b6530..3591a34e9 100644 --- a/source/ArrowKeyStepper/ArrowKeyStepper.example.js +++ b/source/ArrowKeyStepper/ArrowKeyStepper.example.js @@ -19,7 +19,7 @@ export default class ArrowKeyStepperExample extends Component { this._getColumnWidth = this._getColumnWidth.bind(this) this._getRowHeight = this._getRowHeight.bind(this) - this._renderCell = this._renderCell.bind(this) + this._cellRenderer = this._cellRenderer.bind(this) } render () { @@ -43,8 +43,8 @@ export default class ArrowKeyStepperExample extends Component { {({ onSectionRendered, scrollToColumn, scrollToRow }) => (
@@ -57,12 +57,12 @@ export default class ArrowKeyStepperExample extends Component { this._renderCell({ columnIndex, rowIndex, scrollToColumn, scrollToRow }) } + cellRenderer={({ columnIndex, rowIndex }) => this._cellRenderer({ columnIndex, rowIndex, scrollToColumn, scrollToRow }) } rowHeight={this._getRowHeight} - rowsCount={100} + rowCount={100} scrollToColumn={scrollToColumn} scrollToRow={scrollToRow} width={width} @@ -80,15 +80,15 @@ export default class ArrowKeyStepperExample extends Component { return shallowCompare(this, nextProps, nextState) } - _getColumnWidth (index) { + _getColumnWidth ({ index }) { return (1 + (index % 3)) * 60 } - _getRowHeight (index) { + _getRowHeight ({ index }) { return (1 + (index % 3)) * 30 } - _renderCell ({ columnIndex, rowIndex, scrollToColumn, scrollToRow }) { + _cellRenderer ({ columnIndex, rowIndex, scrollToColumn, scrollToRow }) { const className = cn(styles.Cell, { [styles.FocusedCell]: columnIndex === scrollToColumn && rowIndex === scrollToRow }) diff --git a/source/ArrowKeyStepper/ArrowKeyStepper.js b/source/ArrowKeyStepper/ArrowKeyStepper.js index b2cf0a887..5d7a800fd 100644 --- a/source/ArrowKeyStepper/ArrowKeyStepper.js +++ b/source/ArrowKeyStepper/ArrowKeyStepper.js @@ -9,8 +9,8 @@ export default class ArrowKeyStepper extends Component { static propTypes = { children: PropTypes.func.isRequired, className: PropTypes.string, - columnsCount: PropTypes.number.isRequired, - rowsCount: PropTypes.number.isRequired + columnCount: PropTypes.number.isRequired, + rowCount: PropTypes.number.isRequired } constructor (props, context) { @@ -53,7 +53,7 @@ export default class ArrowKeyStepper extends Component { } _onKeyDown (event) { - const { columnsCount, rowsCount } = this.props + const { columnCount, rowCount } = this.props // The above cases all prevent default event event behavior. // This is to keep the grid from scrolling after the snap-to update. @@ -61,7 +61,7 @@ export default class ArrowKeyStepper extends Component { case 'ArrowDown': event.preventDefault() this.setState({ - scrollToRow: Math.min(this._rowStopIndex + 1, rowsCount - 1) + scrollToRow: Math.min(this._rowStopIndex + 1, rowCount - 1) }) break case 'ArrowLeft': @@ -73,7 +73,7 @@ export default class ArrowKeyStepper extends Component { case 'ArrowRight': event.preventDefault() this.setState({ - scrollToColumn: Math.min(this._columnStopIndex + 1, columnsCount - 1) + scrollToColumn: Math.min(this._columnStopIndex + 1, columnCount - 1) }) break case 'ArrowUp': diff --git a/source/ArrowKeyStepper/ArrowKeyStepper.test.js b/source/ArrowKeyStepper/ArrowKeyStepper.test.js index 6225f063c..6482f6859 100644 --- a/source/ArrowKeyStepper/ArrowKeyStepper.test.js +++ b/source/ArrowKeyStepper/ArrowKeyStepper.test.js @@ -17,16 +17,16 @@ function ChildComponent ({ scrollToColumn, scrollToRow }) { describe('ArrowKeyStepper', () => { function renderHelper ({ className, - columnsCount = 10, - rowsCount = 10 + columnCount = 10, + rowCount = 10 } = {}) { let onSectionRenderedCallback const node = findDOMNode(render( {({ onSectionRendered, scrollToColumn, scrollToRow }) => { onSectionRenderedCallback = onSectionRendered @@ -71,8 +71,8 @@ describe('ArrowKeyStepper', () => { it('should not scroll past the row and column boundaries provided', () => { const { node } = renderHelper({ - columnsCount: 2, - rowsCount: 2 + columnCount: 2, + rowCount: 2 }) Simulate.keyDown(node, {key: 'ArrowDown'}) Simulate.keyDown(node, {key: 'ArrowDown'}) diff --git a/source/AutoSizer/AutoSizer.example.js b/source/AutoSizer/AutoSizer.example.js index b32d705b0..9e7628f62 100644 --- a/source/AutoSizer/AutoSizer.example.js +++ b/source/AutoSizer/AutoSizer.example.js @@ -66,7 +66,7 @@ export default class AutoSizerExample extends Component { + + + + This component renders content for a given column or row in order to determine the widest or tallest cell. + It can be used to just-in-time measure dynamic content (eg. messages in a chat interface). + + + + {({ width }) => ( +
+

Fixed height, dynamic width

+ this._columnWidthMeasurerRef = ref} + rowCount={ROW_COUNT} + > + {({ getColumnWidth }) => ( + + )} + + +

Fixed width, dynamic height

+ + {({ getRowHeight }) => ( + + )} + +
+ )} +
+ + ) + } + + shouldComponentUpdate (nextProps, nextState) { + return shallowCompare(this, nextProps, nextState) + } + + _cellRenderer ({ columnIndex, rowIndex }) { + const datum = this._getDatum(rowIndex) + const rowClass = this._getRowClassName(rowIndex) + const classNames = cn(rowClass, styles.cell, { + [styles.centeredCell]: columnIndex > 2 + }) + + let content + + switch (columnIndex % 3) { + case 0: + content = datum.color + break + case 1: + content = datum.name + break + case 2: + content = datum.random + break + } + + return ( +
+ {content} +
+ ) + } + + _getDatum (index) { + const { list } = this.props + + return list.get(index % list.size) + } + + _getRowClassName (row) { + return row % 2 === 0 ? styles.evenRow : styles.oddRow + } +} diff --git a/source/CellMeasurer/CellMeasurer.js b/source/CellMeasurer/CellMeasurer.js new file mode 100644 index 000000000..786dfb7f9 --- /dev/null +++ b/source/CellMeasurer/CellMeasurer.js @@ -0,0 +1,223 @@ +/** @flow */ +import React, { Component, PropTypes } from 'react' +import ReactDOM from 'react-dom' +import ReactDOMServer from 'react-dom/server' + +/** + * Measures a Grid cell's contents by rendering them in a way that is not visible to the user. + * Either a fixed width or height may be provided if it is desirable to measure only in one direction. + */ +export default class CellMeasurer extends Component { + + static propTypes = { + /** + * Renders a cell given its indices. + * Should implement the following interface: ({ columnIndex: number, rowIndex: number }): PropTypes.node + */ + cellRenderer: PropTypes.func.isRequired, + + /** + * Function respondible for rendering a virtualized component. + * This function should implement the following signature: + * ({ getColumnWidth, getRowHeight, resetMeasurements }) => PropTypes.element + */ + children: PropTypes.func.isRequired, + + /** + * Number of columns in grid. + */ + columnCount: PropTypes.number.isRequired, + + /** + * A Node, Component instance, or function that returns either. + * If this property is not specified the document body will be used. + */ + container: React.PropTypes.oneOfType([ + React.PropTypes.func, + React.PropTypes.node + ]), + + /** + * Assign a fixed :height in order to measure dynamic text :width only. + */ + height: PropTypes.number, + + /** + * Number of rows in grid. + */ + rowCount: PropTypes.number.isRequired, + + /** + * Assign a fixed :width in order to measure dynamic text :height only. + */ + width: PropTypes.number + }; + + constructor (props, state) { + super(props, state) + + this._cachedColumnWidths = {} + this._cachedRowHeights = {} + + this.getColumnWidth = this.getColumnWidth.bind(this) + this.getRowHeight = this.getRowHeight.bind(this) + this.resetMeasurements = this.resetMeasurements.bind(this) + } + + getColumnWidth ({ index }) { + if (this._cachedColumnWidths[index]) { + return this._cachedColumnWidths[index] + } + + const { rowCount } = this.props + + let maxWidth = 0 + + for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) { + let { width } = this._measureCell({ + clientWidth: true, + columnIndex: index, + rowIndex + }) + + maxWidth = Math.max(maxWidth, width) + } + + this._cachedColumnWidths[index] = maxWidth + + return maxWidth + } + + getRowHeight ({ index }) { + if (this._cachedRowHeights[index]) { + return this._cachedRowHeights[index] + } + + const { columnCount } = this.props + + let maxHeight = 0 + + for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) { + let { height } = this._measureCell({ + clientHeight: true, + columnIndex, + rowIndex: index + }) + + maxHeight = Math.max(maxHeight, height) + } + + this._cachedRowHeights[index] = maxHeight + + return maxHeight + } + + resetMeasurements () { + this._cachedColumnWidths = {} + this._cachedRowHeights = {} + } + + componentDidMount () { + this._renderAndMount() + } + + componentWillReceiveProps (nextProps) { + this._updateDivDimensions(nextProps) + } + + componentWillUnmount () { + this._unmountContainer() + } + + render () { + const { children } = this.props + + return children({ + getColumnWidth: this.getColumnWidth, + getRowHeight: this.getRowHeight, + resetMeasurements: this.resetMeasurements + }) + } + + _getContainerNode (props) { + const { container } = props + + if (container) { + return ReactDOM.findDOMNode( + typeof container === 'function' + ? container() + : container + ) + } else { + const node = ReactDOM.findDOMNode(this) + + return node.ownerDocument.body + } + } + + _measureCell ({ + clientHeight = false, + clientWidth = true, + columnIndex, + rowIndex + }) { + const { cellRenderer } = this.props + + const rendered = cellRenderer({ + columnIndex, + rowIndex + }) + + this._div.innerHTML = ReactDOMServer.renderToString(rendered) + + return { + height: clientHeight && this._div.clientHeight, + width: clientWidth && this._div.clientWidth + } + } + + _renderAndMount () { + if (!this._div) { + this._div = document.createElement('div') + this._div.style.display = 'inline-block' + this._div.style.position = 'absolute' + this._div.style.visibility = 'hidden' + this._div.style.zIndex = -1 + + this._updateDivDimensions(this.props) + + this._containerNode = this._getContainerNode(this.props) + this._containerNode.appendChild(this._div) + } + } + + _unmountContainer () { + if (this._div) { + this._containerNode.removeChild(this._div) + + this._div = null + } + + this._containerNode = null + } + + _updateDivDimensions (props) { + const { height, width } = props + + if ( + height && + height !== this._divHeight + ) { + this._divHeight = height + this._div.style.height = `${height}px` + } + + if ( + width && + width !== this._divWidth + ) { + this._divWidth = width + this._div.style.width = `${width}px` + } + } +} diff --git a/source/CellMeasurer/CellMeasurer.test.js b/source/CellMeasurer/CellMeasurer.test.js new file mode 100644 index 000000000..b118796ed --- /dev/null +++ b/source/CellMeasurer/CellMeasurer.test.js @@ -0,0 +1,125 @@ +import React from 'react' +import { findDOMNode } from 'react-dom' +import { render } from '../TestUtils' +import CellMeasurer from './CellMeasurer' + +const HEIGHTS = [75, 50, 125, 100, 150] +const WIDTHS = [125, 50, 200, 175, 100] + +function createCellRenderer () { + const cellRendererParams = [] + const cellRenderer = (params) => { + cellRendererParams.push(params) + return ( +
+ cell +
+ ) + } + + return { + cellRenderer, + cellRendererParams + } +} + +function renderHelper ({ + cellRenderer, + columnCount = 1, + columnWidth, + rowCount = 1, + rowHeight +} = {}) { + const params = {} + findDOMNode(render( +
+ + {({ getColumnWidth, getRowHeight }) => { + params.getColumnWidth = getColumnWidth + params.getRowHeight = getRowHeight + + return
foo
+ }} +
+
+ )) + + return params +} + +describe('CellMeasurer', () => { + it('should calculate the height of a single-column row', () => { + const { + cellRenderer, + cellRendererParams + } = createCellRenderer() + const params = renderHelper({ + cellRenderer, + columnWidth: 100 + }) + expect(cellRendererParams).toEqual([]) + expect(params.getRowHeight({ index: 0 })).toEqual(75) + expect(cellRendererParams).toEqual([{ columnIndex: 0, rowIndex: 0 }]) + expect(params.getColumnWidth({ index: 0 })).toEqual(100) + + // For some reason this explicit unmount is necessary. + // Without it, Jasmine's :afterEach doesn't pick up and unmount the component correctly. + render.unmount() + }) + + it('should calculate the width of a single-row column', () => { + const { + cellRenderer, + cellRendererParams + } = createCellRenderer() + const params = renderHelper({ + cellRenderer, + rowHeight: 50 + }) + expect(cellRendererParams).toEqual([]) + expect(params.getColumnWidth({ index: 0 })).toEqual(125) + expect(cellRendererParams).toEqual([{ columnIndex: 0, rowIndex: 0 }]) + expect(params.getRowHeight({ index: 0 })).toEqual(50) + }) + + it('should calculate the height of a multi-column row based on the tallest column-cell', () => { + const { + cellRenderer, + cellRendererParams + } = createCellRenderer() + const params = renderHelper({ + cellRenderer, + columnCount: 5, + columnWidth: 100 + }) + expect(cellRendererParams.length).toEqual(0) + expect(params.getRowHeight({ index: 0 })).toEqual(150) + expect(cellRendererParams.length).toEqual(5) + expect(params.getColumnWidth({ index: 0 })).toEqual(100) + }) + + it('should calculate the width of a multi-row column based on the widest row-cell', () => { + const { + cellRenderer, + cellRendererParams + } = createCellRenderer() + const params = renderHelper({ + cellRenderer, + rowCount: 5, + rowHeight: 50 + }) + expect(cellRendererParams.length).toEqual(0) + expect(params.getColumnWidth({ index: 0 })).toEqual(200) + expect(cellRendererParams.length).toEqual(5) + expect(params.getRowHeight({ index: 0 })).toEqual(50) + }) +}) diff --git a/source/CellMeasurer/index.js b/source/CellMeasurer/index.js new file mode 100644 index 000000000..406c5bd44 --- /dev/null +++ b/source/CellMeasurer/index.js @@ -0,0 +1,2 @@ +export default from './CellMeasurer' +export CellMeasurer from './CellMeasurer' diff --git a/source/Collection/Collection.example.js b/source/Collection/Collection.example.js index 2787a5081..ef5836760 100644 --- a/source/Collection/Collection.example.js +++ b/source/Collection/Collection.example.js @@ -25,7 +25,8 @@ export default class CollectionExample extends Component { cellCount: props.list.size, columnCount: this._getColumnCount(props.list.size), height: 300, - scrollToCell: undefined + scrollToCell: undefined, + showScrollingPlaceholder: false } this._columnYMap = [] @@ -39,7 +40,7 @@ export default class CollectionExample extends Component { } render () { - const { cellCount, height, scrollToCell } = this.state + const { cellCount, height, scrollToCell, showScrollingPlaceholder } = this.state return ( @@ -54,6 +55,19 @@ export default class CollectionExample extends Component { Unlike Grid, which renders checkerboard data, Collection can render arbitrarily positioned- even overlapping- data. + + + + - {index} + {showScrollingPlaceholder && isScrolling ? '...' : index}
) } - _cellSizeAndPositionGetter (index) { + _cellSizeAndPositionGetter ({ index }) { const { list } = this.props const { columnCount } = this.state diff --git a/source/Collection/Collection.js b/source/Collection/Collection.js index 5bebf82fa..efe1be079 100644 --- a/source/Collection/Collection.js +++ b/source/Collection/Collection.js @@ -31,13 +31,13 @@ export default class Collection extends Component { /** * Responsible for rendering a cell given an row and column index. - * Should implement the following interface: (index: number): PropTypes.node + * Should implement the following interface: ({ index: number }): PropTypes.element */ cellRenderer: PropTypes.func.isRequired, /** * Callback responsible for returning size and offset/position information for a given cell (index). - * (index): { height: number, width: number, x: number, y: number } + * ({ index: number }): { height: number, width: number, x: number, y: number } */ cellSizeAndPositionGetter: PropTypes.func.isRequired, @@ -149,7 +149,7 @@ export default class Collection extends Component { } } - renderCells ({ + cellRenderers ({ height, isScrolling, width, @@ -168,8 +168,9 @@ export default class Collection extends Component { return cellGroupRenderer({ cellRenderer, - cellSizeAndPositionGetter: (index) => this._sectionManager.getCellMetadata(index), - indices: this._lastRenderedCellIndices + cellSizeAndPositionGetter: ({ index }) => this._sectionManager.getCellMetadata({ index }), + indices: this._lastRenderedCellIndices, + isScrolling }) } } @@ -177,12 +178,16 @@ export default class Collection extends Component { function defaultCellGroupRenderer ({ cellRenderer, cellSizeAndPositionGetter, - indices + indices, + isScrolling }) { return indices .map((index) => { - const cellMetadata = cellSizeAndPositionGetter(index) - const renderedCell = cellRenderer(index) + const cellMetadata = cellSizeAndPositionGetter({ index }) + const renderedCell = cellRenderer({ + index, + isScrolling + }) if (renderedCell == null || renderedCell === false) { return null diff --git a/source/Collection/Collection.test.js b/source/Collection/Collection.test.js index 2085f88d5..99e24f862 100644 --- a/source/Collection/Collection.test.js +++ b/source/Collection/Collection.test.js @@ -10,6 +10,14 @@ import Collection from './Collection' import { CELLS, SECTION_SIZE } from './TestData' describe('Collection', () => { + function defaultCellRenderer ({ index }) { + return ( +
+ cell:{index} +
+ ) + } + function getMarkup ({ className, cellCount = CELLS.length, @@ -24,17 +32,10 @@ describe('Collection', () => { scrollLeft, scrollToCell, scrollTop, + style, width = SECTION_SIZE * 2 } = {}) { - function defaultCellRenderer (index) { - return ( -
- cell:{index} -
- ) - } - - function defaultCellSizeAndPositionGetter (index) { + function defaultCellSizeAndPositionGetter ({ index }) { index = index % cellCount return CELLS[index] @@ -55,11 +56,18 @@ describe('Collection', () => { scrollLeft={scrollLeft} scrollToCell={scrollToCell} scrollTop={scrollTop} + style={style} width={width} /> ) } + function simulateScroll ({ collection, scrollLeft, scrollTop }) { + const target = { scrollLeft, scrollTop } + collection.refs.CollectionView.refs.scrollingContainer = target // HACK to work around _onScroll target check + Simulate.scroll(findDOMNode(collection), { target }) + } + function compareArrays (array1, array2) { expect(array1.length).toEqual(array2.length) @@ -81,7 +89,7 @@ describe('Collection', () => { // Small performance tweak added in 5.5.6 it('should not render/parent cells that are null or false', () => { - function cellRenderer (index) { + function cellRenderer ({ index }) { if (index > 2) { return null } else { @@ -199,7 +207,7 @@ describe('Collection', () => { it('should call :onSectionRendered if at least one cell is rendered', () => { let indices render(getMarkup({ - onSectionRendered: params => indices = params + onSectionRendered: params => indices = params.indices })) compareArrays(indices, [0, 1, 2, 3]) }) @@ -208,7 +216,7 @@ describe('Collection', () => { let numCalls = 0 let indices const onSectionRendered = params => { - indices = params + indices = params.indices numCalls++ } render(getMarkup({ onSectionRendered })) @@ -223,7 +231,7 @@ describe('Collection', () => { let numCalls = 0 let indices const onSectionRendered = params => { - indices = params + indices = params.indices numCalls++ } render(getMarkup({ onSectionRendered })) @@ -258,7 +266,7 @@ describe('Collection', () => { it('should render correctly when an initial :scrollLeft and :scrollTop properties are specified', () => { let indices render(getMarkup({ - onSectionRendered: params => indices = params, + onSectionRendered: params => indices = params.indices, scrollLeft: 2, scrollTop: 2 })) @@ -268,11 +276,11 @@ describe('Collection', () => { it('should render correctly when :scrollLeft and :scrollTop properties are updated', () => { let indices render(getMarkup({ - onSectionRendered: params => indices = params + onSectionRendered: params => indices = params.indices })) compareArrays(indices, [0, 1, 2, 3]) render(getMarkup({ - onSectionRendered: params => indices = params, + onSectionRendered: params => indices = params.indices, scrollLeft: 2, scrollTop: 2 })) @@ -290,15 +298,15 @@ describe('Collection', () => { const rendered = findDOMNode(render(getMarkup({ className: 'foo' }))) expect(rendered.className).toContain('foo') }) + + it('should use a custom :style if specified', () => { + const style = { backgroundColor: 'red' } + const rendered = findDOMNode(render(getMarkup({ style }))) + expect(rendered.style.backgroundColor).toEqual('red') + }) }) describe('onScroll', () => { - function helper ({ collection, scrollLeft, scrollTop }) { - const target = { scrollLeft, scrollTop } - collection.refs.CollectionView.refs.scrollingContainer = target // HACK to work around _onScroll target check - Simulate.scroll(findDOMNode(collection), { target }) - } - it('should trigger callback when component is mounted', () => { const onScrollCalls = [] render(getMarkup({ @@ -321,7 +329,7 @@ describe('Collection', () => { const collection = render(getMarkup({ onScroll: params => onScrollCalls.push(params) })) - helper({ + simulateScroll({ collection, scrollLeft: 1, scrollTop: 0 @@ -342,7 +350,7 @@ describe('Collection', () => { const collection = render(getMarkup({ onScroll: params => onScrollCalls.push(params) })) - helper({ + simulateScroll({ collection, scrollLeft: 0, scrollTop: 2 @@ -363,7 +371,7 @@ describe('Collection', () => { it('should use a custom :cellGroupRenderer if specified', () => { let cellGroupRendererCalled = 0 let cellGroupRendererParams - const cellRenderer = (index) => index + const cellRenderer = ({ index }) => index findDOMNode(render(getMarkup({ cellRenderer, cellGroupRenderer: (params) => { @@ -381,4 +389,19 @@ describe('Collection', () => { compareArrays(cellGroupRendererParams.indices, [0, 1, 2, 3]) }) }) + + it('should pass the cellRenderer an :isScrolling flag when scrolling is in progress', () => { + const cellRendererCalls = [] + function cellRenderer ({ index, isScrolling }) { + cellRendererCalls.push(isScrolling) + return defaultCellRenderer({ index }) + } + const collection = render(getMarkup({ + cellRenderer + })) + expect(cellRendererCalls[0]).toEqual(false) + cellRendererCalls.splice(0) + simulateScroll({ collection, scrollTop: 100 }) + expect(cellRendererCalls[0]).toEqual(true) + }) }) diff --git a/source/Collection/CollectionView.js b/source/Collection/CollectionView.js index 6c835c64f..99039df51 100644 --- a/source/Collection/CollectionView.js +++ b/source/Collection/CollectionView.js @@ -52,7 +52,7 @@ export default class CollectionView extends Component { height: PropTypes.number.isRequired, /** - * Optional renderer to be used in place of rows when either :rowsCount or :cellCount is 0. + * Optional renderer to be used in place of rows when either :rowCount or :cellCount is 0. */ noContentRenderer: PropTypes.func.isRequired, @@ -65,7 +65,7 @@ export default class CollectionView extends Component { /** * Callback invoked with information about the section of the Collection that was just rendered. - * This callback is passed an array of the most recently rendered section indices. + * This callback is passed a named :indices parameter which is an Array of the most recently rendered section indices. */ onSectionRendered: PropTypes.func.isRequired, @@ -84,6 +84,11 @@ export default class CollectionView extends Component { */ scrollTop: PropTypes.number, + /** + * Optional custom inline style to attach to root Collection element. + */ + style: PropTypes.object, + /** * Width of Collection; this property determines the number of visible (vs virtualized) columns. */ @@ -94,7 +99,8 @@ export default class CollectionView extends Component { 'aria-label': 'grid', noContentRenderer: () => null, onScroll: () => null, - onSectionRendered: () => null + onSectionRendered: () => null, + style: {} }; constructor (props, context) { @@ -263,6 +269,7 @@ export default class CollectionView extends Component { className, height, noContentRenderer, + style, width } = this.props @@ -273,7 +280,7 @@ export default class CollectionView extends Component { } = this.state const childrenToDisplay = height > 0 && width > 0 - ? cellLayoutManager.renderCells({ + ? cellLayoutManager.cellRenderers({ height, isScrolling, width, @@ -286,19 +293,20 @@ export default class CollectionView extends Component { width: totalWidth } = cellLayoutManager.getTotalSize() - const gridStyle = { - height: height, - width: width + const collectionStyle = { + ...style, + height, + width } // Force browser to hide scrollbars when we know they aren't necessary. // Otherwise once scrollbars appear they may not disappear again. // For more info see issue #116 if (totalHeight <= height) { - gridStyle.overflowY = 'hidden' + collectionStyle.overflowY = 'hidden' } if (totalWidth <= width) { - gridStyle.overflowX = 'hidden' + collectionStyle.overflowX = 'hidden' } return ( @@ -308,7 +316,7 @@ export default class CollectionView extends Component { className={cn('Collection', className)} onScroll={this._onScroll} role='grid' - style={gridStyle} + style={collectionStyle} tabIndex={0} > {childrenToDisplay.length > 0 && @@ -361,7 +369,9 @@ export default class CollectionView extends Component { this._onSectionRenderedMemoizer({ callback: onSectionRendered, - indices: cellLayoutManager.getLastRenderedIndices() + indices: { + indices: cellLayoutManager.getLastRenderedIndices() + } }) } diff --git a/source/Collection/Section.js b/source/Collection/Section.js index 7357332c5..046fe2bb2 100644 --- a/source/Collection/Section.js +++ b/source/Collection/Section.js @@ -1,5 +1,5 @@ /** @rlow */ -import type { SizeAndPositionInfo } from './types' +import type { Index, SizeAndPositionInfo } from './types' /** * A section of the Window. @@ -24,7 +24,9 @@ export default class Section { } /** Add a cell to this section. */ - addCellIndex (index: number) { + addCellIndex ({ + index + }: Index) { if (!this._indexMap[index]) { this._indexMap[index] = true this._indices.push(index) diff --git a/source/Collection/Section.test.js b/source/Collection/Section.test.js index 3b19fb3a5..ee1224ff6 100644 --- a/source/Collection/Section.test.js +++ b/source/Collection/Section.test.js @@ -18,19 +18,19 @@ describe('Section', () => { it('should add a new cell index', () => { const section = helper() expect(section.getCellIndices()).toEqual([]) - section.addCellIndex(0) + section.addCellIndex({ index: 0 }) expect(section.getCellIndices()).toEqual([0]) - section.addCellIndex(1) + section.addCellIndex({ index: 1 }) expect(section.getCellIndices()).toEqual([0, 1]) }) it('should not add a duplicate cell index', () => { const section = helper() - section.addCellIndex(0) - section.addCellIndex(1) - section.addCellIndex(0) - section.addCellIndex(1) - section.addCellIndex(2) + section.addCellIndex({ index: 0 }) + section.addCellIndex({ index: 1 }) + section.addCellIndex({ index: 0 }) + section.addCellIndex({ index: 1 }) + section.addCellIndex({ index: 2 }) expect(section.getCellIndices()).toEqual([0, 1, 2]) }) diff --git a/source/Collection/SectionManager.js b/source/Collection/SectionManager.js index 20a410ed4..41099277a 100644 --- a/source/Collection/SectionManager.js +++ b/source/Collection/SectionManager.js @@ -4,7 +4,7 @@ * @flow */ import Section from './Section' -import type { SizeAndPositionInfo } from './types' +import type { Index, SizeAndPositionInfo } from './types' const SECTION_SIZE = 100 @@ -50,7 +50,9 @@ export default class SectionManager { } /** Get size and position information for the cell specified. */ - getCellMetadata (index: number): SizeAndPositionInfo { + getCellMetadata ({ + index + }: Index): SizeAndPositionInfo { return this._cellMetadata[index] } @@ -108,6 +110,6 @@ export default class SectionManager { this._cellMetadata[index] = cellMetadatum this.getSections(cellMetadatum) - .forEach((section) => section.addCellIndex(index)) + .forEach((section) => section.addCellIndex({ index })) } } diff --git a/source/Collection/types.js b/source/Collection/types.js index f4b91bb6c..65a5ac35a 100644 --- a/source/Collection/types.js +++ b/source/Collection/types.js @@ -1,5 +1,9 @@ /** @flow */ +export type Index = { + index: number +}; + export type PositionInfo = { x: number, y: number diff --git a/source/Collection/utils/calculateSizeAndPositionData.js b/source/Collection/utils/calculateSizeAndPositionData.js index e036d2fed..60ad611e1 100644 --- a/source/Collection/utils/calculateSizeAndPositionData.js +++ b/source/Collection/utils/calculateSizeAndPositionData.js @@ -11,7 +11,7 @@ export default function calculateSizeAndPositionData ({ let width = 0 for (let index = 0; index < cellCount; index++) { - const cellMetadatum = cellSizeAndPositionGetter(index) + const cellMetadatum = cellSizeAndPositionGetter({ index }) if ( cellMetadatum.height == null || isNaN(cellMetadatum.height) || diff --git a/source/Collection/utils/calculateSizeAndPositionData.test.js b/source/Collection/utils/calculateSizeAndPositionData.test.js index f64f81efa..ff32561da 100644 --- a/source/Collection/utils/calculateSizeAndPositionData.test.js +++ b/source/Collection/utils/calculateSizeAndPositionData.test.js @@ -3,7 +3,7 @@ import calculateSizeAndPositionData from './calculateSizeAndPositionData' describe('calculateSizeAndPositionData', () => { it('should query for size and position of each cell', () => { const cellSizeAndPositionGetterCalls = [] - function cellSizeAndPositionGetter (index) { + function cellSizeAndPositionGetter ({ index }) { cellSizeAndPositionGetterCalls.push(index) return { x: index * 50, @@ -24,7 +24,7 @@ describe('calculateSizeAndPositionData', () => { expect(() => ( calculateSizeAndPositionData({ cellCount: 3, - cellSizeAndPositionGetter: (index) => {} + cellSizeAndPositionGetter: ({ index }) => {} }) )).toThrow() }) diff --git a/source/ColumnSizer/ColumnSizer.example.js b/source/ColumnSizer/ColumnSizer.example.js index b0a9cf8a6..44f3b5b43 100644 --- a/source/ColumnSizer/ColumnSizer.example.js +++ b/source/ColumnSizer/ColumnSizer.example.js @@ -22,14 +22,14 @@ export default class ColumnSizerExample extends Component { this.state = { columnMaxWidth: 100, columnMinWidth: 75, - columnsCount: 10 + columnCount: 10 } this._noColumnMaxWidthChange = this._noColumnMaxWidthChange.bind(this) this._noColumnMinWidthChange = this._noColumnMinWidthChange.bind(this) - this._onColumnsCountChange = this._onColumnsCountChange.bind(this) + this._onColumnCountChange = this._onColumnCountChange.bind(this) this._noContentRenderer = this._noContentRenderer.bind(this) - this._renderCell = this._renderCell.bind(this) + this._cellRenderer = this._cellRenderer.bind(this) } render () { @@ -38,7 +38,7 @@ export default class ColumnSizerExample extends Component { const { columnMaxWidth, columnMinWidth, - columnsCount + columnCount } = this.state return ( @@ -56,9 +56,9 @@ export default class ColumnSizerExample extends Component { @@ -95,12 +95,12 @@ export default class ColumnSizerExample extends Component { @@ -141,8 +141,8 @@ export default class ColumnSizerExample extends Component { this.setState({ columnMinWidth }) } - _onColumnsCountChange (event) { - this.setState({ columnsCount: parseInt(event.target.value, 10) || 0 }) + _onColumnCountChange (event) { + this.setState({ columnCount: parseInt(event.target.value, 10) || 0 }) } _noContentRenderer () { @@ -153,7 +153,7 @@ export default class ColumnSizerExample extends Component { ) } - _renderCell ({ columnIndex, rowIndex }) { + _cellRenderer ({ columnIndex, rowIndex }) { const className = columnIndex === 0 ? styles.firstCell : styles.cell diff --git a/source/ColumnSizer/ColumnSizer.js b/source/ColumnSizer/ColumnSizer.js index 2c62c97dd..78e19e261 100644 --- a/source/ColumnSizer/ColumnSizer.js +++ b/source/ColumnSizer/ColumnSizer.js @@ -26,7 +26,7 @@ export default class ColumnSizer extends Component { columnMinWidth: PropTypes.number, /** Number of columns in Grid or FlexTable child */ - columnsCount: PropTypes.number.isRequired, + columnCount: PropTypes.number.isRequired, /** Width of Grid or FlexTable child */ width: PropTypes.number.isRequired @@ -42,14 +42,14 @@ export default class ColumnSizer extends Component { const { columnMaxWidth, columnMinWidth, - columnsCount, + columnCount, width } = this.props if ( columnMaxWidth !== prevProps.columnMaxWidth || columnMinWidth !== prevProps.columnMinWidth || - columnsCount !== prevProps.columnsCount || + columnCount !== prevProps.columnCount || width !== prevProps.width ) { if (this._registeredChild) { @@ -63,7 +63,7 @@ export default class ColumnSizer extends Component { children, columnMaxWidth, columnMinWidth, - columnsCount, + columnCount, width } = this.props @@ -73,12 +73,12 @@ export default class ColumnSizer extends Component { ? Math.min(columnMaxWidth, width) : width - let columnWidth = width / columnsCount + let columnWidth = width / columnCount columnWidth = Math.max(safeColumnMinWidth, columnWidth) columnWidth = Math.min(safeColumnMaxWidth, columnWidth) columnWidth = Math.floor(columnWidth) - let adjustedWidth = Math.min(width, columnWidth * columnsCount) + let adjustedWidth = Math.min(width, columnWidth * columnCount) return children({ adjustedWidth, diff --git a/source/ColumnSizer/ColumnSizer.test.js b/source/ColumnSizer/ColumnSizer.test.js index 427498cb2..7cef1609d 100644 --- a/source/ColumnSizer/ColumnSizer.test.js +++ b/source/ColumnSizer/ColumnSizer.test.js @@ -8,10 +8,10 @@ describe('ColumnSizer', () => { function getMarkup ({ columnMinWidth = undefined, columnMaxWidth = undefined, - columnsCount = 10, + columnCount = 10, width = 200 } = {}) { - function renderCell ({ columnIndex, rowIndex }) { + function cellRenderer ({ columnIndex, rowIndex }) { return (
{`row:${rowIndex}, column:${columnIndex}`} @@ -23,19 +23,19 @@ describe('ColumnSizer', () => { {({ adjustedWidth, getColumnWidth, registerChild }) => (
@@ -87,8 +87,8 @@ describe('ColumnSizer', () => { helper({ width: 300 }, 'columnWidth:30') }) - it('should recompute metadata sizes if :columnsCount changes', () => { - helper({ columnsCount: 2 }, 'columnWidth:100') + it('should recompute metadata sizes if :columnCount changes', () => { + helper({ columnCount: 2 }, 'columnWidth:100') }) }) @@ -112,7 +112,7 @@ describe('ColumnSizer', () => { {({ adjustedWidth, getColumnWidth, registerChild }) => ( diff --git a/source/FlexTable/FlexColumn.js b/source/FlexTable/FlexColumn.js index d5abe1704..20a120d2e 100644 --- a/source/FlexTable/FlexColumn.js +++ b/source/FlexTable/FlexColumn.js @@ -1,75 +1,8 @@ /** @flow */ -import React, { Component, PropTypes } from 'react' -import SortIndicator from './SortIndicator' - -/** - * Default cell renderer that displays an attribute as a simple string - * You should override the column's cellRenderer if your data is some other type of object. - */ -export function defaultCellRenderer ( - cellData: any, - cellDataKey: string, - rowData: any, - rowIndex: number, - columnData: any -): string { - if (cellData === null || cellData === undefined) { - return '' - } else { - return String(cellData) - } -} - -/** - * Default accessor for returning a cell value for a given attribute. - * This function expects to operate on either a vanilla Object or an Immutable Map. - * You should override the column's cellDataGetter if your data is some other type of object. - */ -export function defaultCellDataGetter ( - dataKey: string, - rowData: any, - columnData: any -) { - if (rowData.get instanceof Function) { - return rowData.get(dataKey) - } else { - return rowData[dataKey] - } -} - -/** - * Default table header renderer. - */ -export function defaultHeaderRenderer ({ - columnData, - dataKey, - disableSort, - label, - sortBy, - sortDirection -}) { - const showSortIndicator = sortBy === dataKey - const children = [ -
- {label} -
- ] - - if (showSortIndicator) { - children.push( - - ) - } - - return children -} +import { Component, PropTypes } from 'react' +import defaultHeaderRenderer from './defaultHeaderRenderer' +import defaultCellRenderer from './defaultCellRenderer' +import defaultCellDataGetter from './defaultCellDataGetter' /** * Describes the header and cell contents of a table column. @@ -79,6 +12,7 @@ export default class Column extends Component { static defaultProps = { cellDataGetter: defaultCellDataGetter, cellRenderer: defaultCellRenderer, + cellStyle: {}, flexGrow: 0, flexShrink: 1, headerRenderer: defaultHeaderRenderer @@ -88,21 +22,21 @@ export default class Column extends Component { /** Optional aria-label value to set on the column header */ 'aria-label': PropTypes.string, - /** Optional CSS class to apply to cell */ - cellClassName: PropTypes.string, - /** * Callback responsible for returning a cell's data, given its :dataKey - * (dataKey: string, rowData: any): any + * ({ columnData: any, dataKey: string, rowData: any }): any */ cellDataGetter: PropTypes.func, /** * Callback responsible for rendering a cell's contents. - * (cellData: any, cellDataKey: string, rowData: any, rowIndex: number, columnData: any): element + * ({ cellData: any, columnData: any, dataKey: string, rowData: any, rowIndex: number }): node */ cellRenderer: PropTypes.func, + /** Optional CSS class to apply to cell */ + className: PropTypes.string, + /** Optional additional data passed to this column's :cellDataGetter */ columnData: PropTypes.object, @@ -136,6 +70,9 @@ export default class Column extends Component { /** Minimum width of column. */ minWidth: PropTypes.number, + /** Optional inline style to apply to cell */ + style: PropTypes.object, + /** Flex basis (width) for this column; This value can grow or shrink based on :flexGrow and :flexShrink properties. */ width: PropTypes.number.isRequired } diff --git a/source/FlexTable/FlexColumn.test.js b/source/FlexTable/FlexColumn.test.js index 8c7e958db..61df6127f 100644 --- a/source/FlexTable/FlexColumn.test.js +++ b/source/FlexTable/FlexColumn.test.js @@ -1,32 +1,62 @@ import Immutable from 'immutable' -import { defaultCellDataGetter, defaultCellRenderer } from './FlexColumn' +import defaultCellDataGetter from './defaultCellDataGetter' +import defaultCellRenderer from './defaultCellRenderer' describe('Column', () => { - const map = Immutable.Map({ + const rowData = Immutable.Map({ foo: 'Foo', bar: 1 }) describe('defaultCellDataGetter', () => { it('should return a value for specified attributes', () => { - expect(defaultCellDataGetter('foo', map)).toEqual('Foo') - expect(defaultCellDataGetter('bar', map)).toEqual(1) + expect(defaultCellDataGetter({ + dataKey: 'foo', + rowData + })).toEqual('Foo') + expect(defaultCellDataGetter({ + dataKey: 'bar', + rowData + })).toEqual(1) }) it('should return undefined for missing attributes', () => { - expect(defaultCellDataGetter('baz', map)).toEqual(undefined) + expect(defaultCellDataGetter({ + dataKey: 'baz', + rowData + })).toEqual(undefined) }) }) describe('defaultCellRenderer', () => { it('should render a value for specified attributes', () => { - expect(defaultCellRenderer('Foo', 'foo', map, 0)).toEqual('Foo') - expect(defaultCellRenderer(1, 'bar', map, 0)).toEqual('1') + expect(defaultCellRenderer({ + cellData: 'Foo', + dataKey: 'foo', + rowData, + rowIndex: 0 + })).toEqual('Foo') + expect(defaultCellRenderer({ + cellData: 1, + dataKey: 'bar', + rowData, + rowIndex: 0 + })).toEqual('1') }) it('should render empty string for null or missing attributes', () => { - expect(defaultCellRenderer(null, 'baz', map, 0)).toEqual('') - expect(defaultCellRenderer(undefined, 'baz', map, 0)).toEqual('') + expect(defaultCellRenderer({ + cellData: null, + dataKey: 'baz', + rowData, + rowIndex: 0 + })).toEqual('') + expect(defaultCellRenderer({ + cellData: undefined, + dataKey: 'baz', + rowData, + rowIndex: 0 + })).toEqual('') }) }) }) diff --git a/source/FlexTable/FlexTable.example.js b/source/FlexTable/FlexTable.example.js index 0affcb85e..5596b7079 100644 --- a/source/FlexTable/FlexTable.example.js +++ b/source/FlexTable/FlexTable.example.js @@ -20,12 +20,13 @@ export default class FlexTableExample extends Component { super(props, context) this.state = { + disableHeader: false, headerHeight: 30, height: 270, hideIndexRow: false, - overscanRowsCount: 0, + overscanRowCount: 0, rowHeight: 40, - rowsCount: 1000, + rowCount: 1000, scrollToIndex: undefined, sortBy: 'index', sortDirection: SortDirection.ASC, @@ -35,19 +36,20 @@ export default class FlexTableExample extends Component { this._getRowHeight = this._getRowHeight.bind(this) this._headerRenderer = this._headerRenderer.bind(this) this._noRowsRenderer = this._noRowsRenderer.bind(this) - this._onRowsCountChange = this._onRowsCountChange.bind(this) + this._onRowCountChange = this._onRowCountChange.bind(this) this._onScrollToRowChange = this._onScrollToRowChange.bind(this) this._sort = this._sort.bind(this) } render () { const { + disableHeader, headerHeight, height, hideIndexRow, - overscanRowsCount, + overscanRowCount, rowHeight, - rowsCount, + rowCount, scrollToIndex, sortBy, sortDirection, @@ -65,7 +67,7 @@ export default class FlexTableExample extends Component { ) : list - const rowGetter = index => this._getDatum(sortedList, index) + const rowGetter = ({ index }) => this._getDatum(sortedList, index) return ( @@ -86,9 +88,9 @@ export default class FlexTableExample extends Component {