react-virtualized
Advanced tools
Comparing version 2.5.0 to 2.6.0
Changelog | ||
------------ | ||
#### 2.6.0 | ||
`VirtualScroll` and `FlexTable` now support dynamic row heights by accepting a function as the `rowHeight` property. | ||
#### 2.5.0 | ||
@@ -5,0 +8,0 @@ Added `AutoSizer` component for wrapping `FlexTable` or `VirtualScroll` and growing to fill the parent container. This should hopefully simplify usage of these components. |
@@ -7,3 +7,3 @@ FlexTable | ||
#### Prop Types | ||
### Prop Types | ||
| Property | Type | Required? | Description | | ||
@@ -23,3 +23,3 @@ |:---|:---|:---:|:---| | ||
| rowGetter | Function | ✓ | Callback responsible for returning a data row given an index. `(index: int): any` | | ||
| rowHeight | | ✓ | Fixed height of table row | | ||
| 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. | | ||
@@ -31,1 +31,12 @@ | sort | Function | | Sort function to be called if a sortable header is clicked. `(dataKey: string, sortDirection: SortDirection): void` | | ||
| verticalPadding | Number | | Vertical padding of outer DOM element | | ||
### Public Methods | ||
##### recomputeRowHeights | ||
Recompute row heights and offsets. | ||
VirtualScroll has no way of knowing when its underlying list data has changed since it only receives a `rowHeight` property. If the `rowHeight` is a number it can compare before and after values but if it is a function that comparison is error prone. In the event that a dynamic `rowHeight` function is in use and the row heights have changed this function should be manually called by the "smart" container parent. | ||
##### scrollToRow | ||
Scroll the list to ensure the row at the specified index is visible. This method exists so that a user can forcefully scroll to the same row twice. (The `scrollToIndex` property would not change in that case and so it would not be picked up by VirtualScroll.) |
@@ -6,3 +6,3 @@ VirtualScroll | ||
#### Prop Types | ||
### Prop Types | ||
| Property | Type | Required? | Description | | ||
@@ -12,7 +12,18 @@ |:---|:---|:---:|:---| | ||
| 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 | | ||
| 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: `({ startIndex, stopIndex }): void` | | ||
| rowHeight | Number | ✓ | Fixed row height; the number of rows displayed is calculated by dividing height by rowHeight | | ||
| 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. Rendered rows must have a unique `key` attribute. | | ||
| rowsCount | Number | ✓ | Number of rows in list. | | ||
| scrollToIndex | Number | | Row index to ensure visible (by forcefully scrolling if necessary) | | ||
### Public Methods | ||
##### recomputeRowHeights | ||
Recompute row heights and offsets. | ||
VirtualScroll has no way of knowing when its underlying list data has changed since it only receives a `rowHeight` property. If the `rowHeight` is a number it can compare before and after values but if it is a function that comparison is error prone. In the event that a dynamic `rowHeight` function is in use and the row heights have changed this function should be manually called by the "smart" container parent. | ||
##### scrollToRow | ||
Scroll the list to ensure the row at the specified index is visible. This method exists so that a user can forcefully scroll to the same row twice. (The `scrollToIndex` property would not change in that case and so it would not be picked up by VirtualScroll.) |
@@ -6,3 +6,3 @@ { | ||
"user": "bvaughn", | ||
"version": "2.5.0", | ||
"version": "2.6.0", | ||
"scripts": { | ||
@@ -63,2 +63,4 @@ "build": "npm run build:demo && npm run build:dist", | ||
"expect", | ||
"fdescribe", | ||
"fit", | ||
"it", | ||
@@ -65,0 +67,0 @@ "jasmine" |
import React, { PropTypes } from 'react' | ||
import cn from 'classnames' | ||
import styles from './LabeledInput.css' | ||
export function LabeledInput ({ label, name, onChange, placeholder, value }) { | ||
export function LabeledInput ({ | ||
disabled, | ||
label, | ||
name, | ||
onChange, | ||
placeholder, | ||
value | ||
}) { | ||
const labelClassName = cn(styles.Label, { | ||
[styles.LabelDisabled]: disabled | ||
}) | ||
return ( | ||
<div className={styles.LabeledInput}> | ||
<label className={styles.Label}> | ||
<label className={labelClassName}> | ||
{label} | ||
@@ -16,2 +28,3 @@ </label> | ||
value={value} | ||
disabled={disabled} | ||
/> | ||
@@ -22,2 +35,3 @@ </div> | ||
LabeledInput.propTypes = { | ||
disabled: PropTypes.bool, | ||
label: PropTypes.string.isRequired, | ||
@@ -24,0 +38,0 @@ name: PropTypes.string.isRequired, |
@@ -28,2 +28,4 @@ /** @flow */ | ||
export default class FlexTable extends Component { | ||
static shouldComponentUpdate = shouldPureComponentUpdate | ||
static defaultProps = { | ||
@@ -109,9 +111,11 @@ disableHeader: false, | ||
/** | ||
* Scroll the table to ensure the specified index is visible. | ||
* | ||
* @private | ||
* Why was this functionality implemented as a method instead of a property? | ||
* Short answer: A user of this component may want to scroll to the same row twice. | ||
* In this case the scroll-to-row property would not change and so it would not be picked up by the component. | ||
* See VirtualScroll#recomputeRowHeights | ||
*/ | ||
recomputeRowHeights () { | ||
this.refs.VirtualScroll.recomputeRowHeights() | ||
} | ||
/** | ||
* See VirtualScroll#scrollToRow | ||
*/ | ||
scrollToRow (scrollToIndex) { | ||
@@ -121,10 +125,2 @@ this.refs.VirtualScroll.scrollToRow(scrollToIndex) | ||
getRenderedHeaderRow () { | ||
const { children, disableHeader } = this.props | ||
const items = disableHeader ? [] : children | ||
return React.Children.map(items, (column, columnIndex) => | ||
this._createHeader(column, columnIndex) | ||
) | ||
} | ||
render () { | ||
@@ -168,3 +164,3 @@ const { | ||
> | ||
{this.getRenderedHeaderRow()} | ||
{this._getRenderedHeaderRow()} | ||
</div> | ||
@@ -313,4 +309,11 @@ )} | ||
} | ||
_getRenderedHeaderRow () { | ||
const { children, disableHeader } = this.props | ||
const items = disableHeader ? [] : children | ||
return React.Children.map(items, (column, columnIndex) => | ||
this._createHeader(column, columnIndex) | ||
) | ||
} | ||
} | ||
FlexTable.prototype.shouldComponentUpdate = shouldPureComponentUpdate | ||
@@ -317,0 +320,0 @@ /** |
@@ -123,6 +123,5 @@ import React from 'react' | ||
// 100px height should fit 1 header (20px) and 9 rows (10px each) - | ||
// 8 to fill the remaining space and 1 to account for partial scrolling | ||
// 100px height should fit 1 header (20px) and 8 rows (10px each) - | ||
expect(findAll(tableDOMNode, '.headerRow').length).toEqual(1) | ||
expect(findAll(tableDOMNode, '.row').length).toEqual(9) | ||
expect(findAll(tableDOMNode, '.row').length).toEqual(8) | ||
}) | ||
@@ -151,3 +150,3 @@ | ||
const rows = findAll(tableDOMNode, '.row') | ||
expect(rows.length).toEqual(3) | ||
expect(rows.length).toEqual(2) | ||
@@ -356,3 +355,3 @@ for (let index = 0; index < rows.length; index++) { | ||
expect(startIndex).toEqual(0) | ||
expect(stopIndex).toEqual(8) | ||
expect(stopIndex).toEqual(7) | ||
}) | ||
@@ -359,0 +358,0 @@ |
@@ -20,3 +20,4 @@ /** | ||
color: BADGE_COLORS[i % BADGE_COLORS.length], | ||
name: NAMES[i % NAMES.length] | ||
name: NAMES[i % NAMES.length], | ||
height: ROW_HEIGHTS[Math.floor(Math.random() * ROW_HEIGHTS.length)] | ||
}) | ||
@@ -28,2 +29,3 @@ } | ||
scrollToIndex: undefined, | ||
useDynamicRowHeight: false, | ||
virtualScrollHeight: 300, | ||
@@ -33,2 +35,3 @@ virtualScrollRowHeight: 60 | ||
this._getRowHeight = this._getRowHeight.bind(this) | ||
this._noRowsRenderer = this._noRowsRenderer.bind(this) | ||
@@ -38,6 +41,13 @@ this._onRowsCountChange = this._onRowsCountChange.bind(this) | ||
this._rowRenderer = this._rowRenderer.bind(this) | ||
this._updateUseDynamicRowHeight = this._updateUseDynamicRowHeight.bind(this) | ||
} | ||
render () { | ||
const { rowsCount, scrollToIndex, virtualScrollHeight, virtualScrollRowHeight } = this.state | ||
const { | ||
rowsCount, | ||
scrollToIndex, | ||
useDynamicRowHeight, | ||
virtualScrollHeight, | ||
virtualScrollRowHeight | ||
} = this.state | ||
@@ -57,2 +67,14 @@ return ( | ||
<ContentBoxParagraph> | ||
<label className={styles.checkboxLabel}> | ||
<input | ||
className={styles.checkbox} | ||
type='checkbox' | ||
value={useDynamicRowHeight} | ||
onChange={event => this._updateUseDynamicRowHeight(event.target.checked)} | ||
/> | ||
Use dynamic row heights? | ||
</label> | ||
</ContentBoxParagraph> | ||
<InputRow> | ||
@@ -79,2 +101,3 @@ <LabeledInput | ||
<LabeledInput | ||
disabled={useDynamicRowHeight} | ||
label='Row height' | ||
@@ -88,2 +111,3 @@ name='virtualScrollRowHeight' | ||
<VirtualScroll | ||
ref='VirtualScroll' | ||
className={styles.VirtualScroll} | ||
@@ -94,3 +118,3 @@ width={310} | ||
rowsCount={rowsCount} | ||
rowHeight={virtualScrollRowHeight} | ||
rowHeight={useDynamicRowHeight ? this._getRowHeight : virtualScrollRowHeight} | ||
rowRenderer={this._rowRenderer} | ||
@@ -103,2 +127,6 @@ scrollToIndex={scrollToIndex} | ||
_getRowHeight (index) { | ||
return this._list[index].height | ||
} | ||
_noRowsRenderer () { | ||
@@ -131,9 +159,22 @@ return ( | ||
_rowRenderer (index) { | ||
const { virtualScrollRowHeight } = this.state | ||
const rowStyle = { | ||
height: virtualScrollRowHeight | ||
const { useDynamicRowHeight, virtualScrollRowHeight } = this.state | ||
const datum = this._list[index] | ||
const height = useDynamicRowHeight | ||
? datum.height | ||
: virtualScrollRowHeight | ||
let additionalContent | ||
if (useDynamicRowHeight) { | ||
switch (datum.height) { | ||
case 75: | ||
additionalContent = <div>It is medium-sized.</div> | ||
break | ||
case 100: | ||
additionalContent = <div>It is large-sized.<br/>It has a 3rd row.</div> | ||
break | ||
} | ||
} | ||
const data = this._list[index] | ||
return ( | ||
@@ -143,3 +184,3 @@ <div | ||
className={styles.row} | ||
style={rowStyle} | ||
style={{ height }} | ||
> | ||
@@ -149,10 +190,10 @@ <div | ||
style={{ | ||
backgroundColor: data.color | ||
backgroundColor: datum.color | ||
}} | ||
> | ||
{data.name.charAt(0)} | ||
{datum.name.charAt(0)} | ||
</div> | ||
<div> | ||
<div className={styles.name}> | ||
{data.name} | ||
{datum.name} | ||
</div> | ||
@@ -162,6 +203,19 @@ <div className={styles.index}> | ||
</div> | ||
{additionalContent} | ||
</div> | ||
{useDynamicRowHeight && | ||
<span className={styles.height}> | ||
{datum.height}px | ||
</span> | ||
} | ||
</div> | ||
) | ||
} | ||
_updateUseDynamicRowHeight (value) { | ||
this.setState({ | ||
rowsCount: 100, | ||
useDynamicRowHeight: value | ||
}) | ||
} | ||
} | ||
@@ -171,1 +225,2 @@ | ||
const 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'] | ||
const ROW_HEIGHTS = [50, 75, 100] |
@@ -6,2 +6,7 @@ /** @flow */ | ||
import raf from 'raf' | ||
import { | ||
getUpdatedOffsetForIndex, | ||
getVisibleRowIndices, | ||
initCellMetadata | ||
} from '../utils' | ||
import styles from './VirtualScroll.css' | ||
@@ -23,2 +28,4 @@ | ||
export default class VirtualScroll extends Component { | ||
static shouldComponentUpdate = shouldPureComponentUpdate | ||
static propTypes = { | ||
@@ -36,4 +43,4 @@ /** Optional CSS class name */ | ||
onRowsRendered: PropTypes.func, | ||
/** Fixed row height; the number of rows displayed is calculated by dividing height by rowHeight */ | ||
rowHeight: PropTypes.number.isRequired, | ||
/** Either a fixed row height (number) or a function that returns the height of a row given its index. */ | ||
rowHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.func]).isRequired, | ||
/** Responsbile for rendering a row given an index */ | ||
@@ -56,2 +63,3 @@ rowRenderer: PropTypes.func.isRequired, | ||
this.state = { | ||
computeCellMetadataOnNextUpdate: false, | ||
isScrolling: false, | ||
@@ -67,2 +75,13 @@ scrollTop: 0 | ||
/** | ||
* Forced recompute of row heights. | ||
* This function should be called if dynamic row heights have changed but nothing else has. | ||
* Since VirtualScroll receives a :rowsCount it has no way of knowing if the underlying list data has changed. | ||
*/ | ||
recomputeRowHeights () { | ||
this.setState({ | ||
computeCellMetadataOnNextUpdate: true | ||
}) | ||
} | ||
/** | ||
* Scroll the list to ensure the row at the specified index is visible. | ||
@@ -88,14 +107,2 @@ * This method exists so that a user can forcefully scroll to the same row twice. | ||
componentWillUnmount () { | ||
if (this._disablePointerEventsTimeoutId) { | ||
clearTimeout(this._disablePointerEventsTimeoutId) | ||
} | ||
if (this._scrollTopId) { | ||
clearImmediate(this._scrollTopId) | ||
} | ||
if (this._setNextStateAnimationFrameId) { | ||
raf.cancel(this._setNextStateAnimationFrameId) | ||
} | ||
} | ||
componentDidUpdate (prevProps, prevState) { | ||
@@ -105,4 +112,2 @@ const { height, rowsCount, rowHeight, scrollToIndex } = this.props | ||
const previousRowsCount = prevProps.rowsCount | ||
// Make sure any changes to :scrollTop (from :scrollToIndex) get applied | ||
@@ -114,3 +119,10 @@ if (scrollTop >= 0 && scrollTop !== prevState.scrollTop) { | ||
const hasScrollToIndex = scrollToIndex >= 0 && scrollToIndex < rowsCount | ||
const sizeHasChanged = height !== prevProps.height || rowHeight !== prevProps.rowHeight | ||
const sizeHasChanged = ( | ||
height !== prevProps.height || | ||
!prevProps.rowHeight || | ||
( | ||
rowHeight instanceof Number && | ||
rowHeight !== prevProps.rowHeight | ||
) | ||
) | ||
@@ -124,9 +136,8 @@ // If we have a new scroll target OR if height/row-height has changed, | ||
// Make sure we aren't scrolled too far past the current content. | ||
} else if (!hasScrollToIndex && (height < prevProps.height || rowsCount < previousRowsCount)) { | ||
const calculatedScrollTop = VirtualScroll._calculateScrollTopForIndex({ | ||
height, | ||
rowHeight, | ||
rowsCount, | ||
scrollTop, | ||
scrollToIndex: rowsCount - 1 | ||
} else if (!hasScrollToIndex && (height < prevProps.height || rowsCount < prevProps.rowsCount)) { | ||
const calculatedScrollTop = getUpdatedOffsetForIndex({ | ||
cellMetadata: this._cellMetadata, | ||
containerSize: height, | ||
currentOffset: scrollTop, | ||
targetIndex: rowsCount - 1 | ||
}) | ||
@@ -141,8 +152,51 @@ | ||
componentWillUpdate (prevProps, prevState) { | ||
const { rowsCount } = this.props | ||
componentWillMount () { | ||
this._computeCellMetadata(this.props) | ||
} | ||
if (rowsCount === 0) { | ||
componentWillUnmount () { | ||
if (this._disablePointerEventsTimeoutId) { | ||
clearTimeout(this._disablePointerEventsTimeoutId) | ||
} | ||
if (this._scrollTopId) { | ||
clearImmediate(this._scrollTopId) | ||
} | ||
if (this._setNextStateAnimationFrameId) { | ||
raf.cancel(this._setNextStateAnimationFrameId) | ||
} | ||
} | ||
componentWillUpdate (nextProps, nextState) { | ||
if ( | ||
nextProps.rowsCount === 0 && | ||
nextState.scrollTop !== 0 | ||
) { | ||
this.setState({ scrollTop: 0 }) | ||
} | ||
// Don't compare rowHeight if it's a function because inline functions would cause infinite loops. | ||
// In that event users should use recomputeRowHeights() to inform of changes. | ||
if ( | ||
nextState.computeCellMetadataOnNextUpdate || | ||
this.props.rowsCount !== nextProps.rowsCount || | ||
( | ||
( | ||
typeof this.props.rowHeight === 'number' || | ||
typeof nextProps.rowHeight === 'number' | ||
) && | ||
this.props.rowHeight !== nextProps.rowHeight | ||
) | ||
) { | ||
this._computeCellMetadata(nextProps) | ||
this.setState({ | ||
computeCellMetadataOnNextUpdate: false | ||
}) | ||
// Updated cell metadata may have hidden the previous scrolled-to item. | ||
// In this case we should also update the scrollTop to ensure it stays visible. | ||
if (this.props.scrollToIndex === nextProps.scrollToIndex) { | ||
this._updateScrollTopForScrollToIndex() | ||
} | ||
} | ||
} | ||
@@ -157,3 +211,2 @@ | ||
rowsCount, | ||
rowHeight, | ||
rowRenderer | ||
@@ -167,8 +220,2 @@ } = this.props | ||
const totalRowsHeight = rowsCount * rowHeight | ||
// Shift the visible rows down so that they remain visible while scrolling. | ||
// This mimicks scrolling behavior within a non-virtualized list. | ||
const paddingTop = scrollTop - (scrollTop % rowHeight) | ||
let childrenToDisplay = [] | ||
@@ -179,18 +226,30 @@ | ||
const { | ||
rowIndexStart, | ||
rowIndexStop | ||
} = VirtualScroll._getStartAndStopIndexForScrollTop({ | ||
height, | ||
rowHeight, | ||
rowsCount, | ||
scrollTop | ||
start, | ||
stop | ||
} = getVisibleRowIndices({ | ||
cellCount: rowsCount, | ||
cellMetadata: this._cellMetadata, | ||
containerSize: height, | ||
currentOffset: scrollTop | ||
}) | ||
for (let i = rowIndexStart; i <= rowIndexStop; i++) { | ||
childrenToDisplay.push(rowRenderer(i)) | ||
for (let i = start; i <= stop; i++) { | ||
let datum = this._cellMetadata[i] | ||
let child = React.cloneElement( | ||
rowRenderer(i), { | ||
style: { | ||
position: 'absolute', | ||
top: datum.offset, | ||
width: '100%', | ||
height: this._getRowHeight(i) | ||
} | ||
} | ||
) | ||
childrenToDisplay.push(child) | ||
} | ||
onRowsRendered({ | ||
startIndex: rowIndexStart, | ||
stopIndex: rowIndexStop | ||
startIndex: start, | ||
stopIndex: stop | ||
}) | ||
@@ -215,5 +274,4 @@ } | ||
style={{ | ||
height: totalRowsHeight, | ||
maxHeight: totalRowsHeight, | ||
paddingTop: paddingTop, | ||
height: this._getTotalRowsHeight(), | ||
maxHeight: this._getTotalRowsHeight(), | ||
pointerEvents: isScrolling ? 'none' : 'auto' | ||
@@ -232,49 +290,27 @@ }} | ||
/** | ||
* Scroll the table to ensure the specified index is visible. | ||
* | ||
* @private | ||
* Why was this functionality implemented as a method instead of a property? | ||
* Short answer: A user of this component may want to scroll to the same row twice. | ||
* In this case the scroll-to-row property would not change and so it would not be picked up by the component. | ||
*/ | ||
static _calculateScrollTopForIndex ({ rowsCount, height, rowHeight, scrollTop, scrollToIndex }) { | ||
scrollToIndex = Math.max(0, Math.min(rowsCount - 1, scrollToIndex)) | ||
_computeCellMetadata (props) { | ||
const { rowHeight, rowsCount } = props | ||
const maxScrollTop = scrollToIndex * rowHeight | ||
const minScrollTop = maxScrollTop - height + rowHeight | ||
const newScrollTop = Math.max(minScrollTop, Math.min(maxScrollTop, scrollTop)) | ||
return newScrollTop | ||
this._cellMetadata = initCellMetadata({ | ||
cellCount: rowsCount, | ||
size: rowHeight | ||
}) | ||
} | ||
/** | ||
* Calculates the maximum number of visible rows based on the row-height and the number of rows in the table. | ||
*/ | ||
static _getMaxVisibleRows ({ height, rowHeight, rowsCount }) { | ||
const minNumRowsToFillSpace = Math.ceil(height / rowHeight) | ||
_getRowHeight (index) { | ||
const { rowHeight } = this.props | ||
// Add one to account for partially-clipped rows on the top and bottom | ||
const maxNumRowsToFillSpace = minNumRowsToFillSpace + 1 | ||
return Math.min(rowsCount, maxNumRowsToFillSpace) | ||
return rowHeight instanceof Function | ||
? rowHeight(index) | ||
: rowHeight | ||
} | ||
/** | ||
* Calculates the start and end index for visible rows based on a scroll offset. | ||
* Handles edge-cases to ensure that the table never scrolls past the available rows. | ||
*/ | ||
static _getStartAndStopIndexForScrollTop ({ height, rowHeight, rowsCount, scrollTop }) { | ||
const maxVisibleRows = VirtualScroll._getMaxVisibleRows({ height, rowHeight, rowsCount }) | ||
const totalRowsHeight = rowHeight * rowsCount | ||
const safeScrollTop = Math.max(0, Math.min(totalRowsHeight - height, scrollTop)) | ||
_getTotalRowsHeight () { | ||
if (this._cellMetadata.length === 0) { | ||
return 0 | ||
} | ||
let scrollPercentage = safeScrollTop / totalRowsHeight | ||
let rowIndexStart = Math.floor(scrollPercentage * rowsCount) | ||
let rowIndexStop = Math.min(rowsCount, rowIndexStart + maxVisibleRows) - 1 | ||
const datum = this._cellMetadata[this._cellMetadata.length - 1] | ||
return { | ||
rowIndexStart, | ||
rowIndexStop | ||
} | ||
return datum.offset + datum.size | ||
} | ||
@@ -329,12 +365,11 @@ | ||
const { height, rowsCount, rowHeight } = this.props | ||
const { height } = this.props | ||
const { scrollTop } = this.state | ||
if (scrollToIndex >= 0) { | ||
const calculatedScrollTop = VirtualScroll._calculateScrollTopForIndex({ | ||
height, | ||
rowHeight, | ||
rowsCount, | ||
scrollTop, | ||
scrollToIndex | ||
const calculatedScrollTop = getUpdatedOffsetForIndex({ | ||
cellMetadata: this._cellMetadata, | ||
containerSize: height, | ||
currentOffset: scrollTop, | ||
targetIndex: scrollToIndex | ||
}) | ||
@@ -349,5 +384,7 @@ | ||
_onKeyPress (event) { | ||
const { rowHeight } = this.props | ||
const { height, rowsCount } = this.props | ||
const { scrollTop } = this.state | ||
let start, datum, newScrollTop | ||
switch (event.key) { | ||
@@ -357,7 +394,17 @@ case 'ArrowDown': | ||
const { height, rowsCount } = this.props | ||
const totalRowsHeight = rowsCount * rowHeight | ||
const newScrollTop = Math.min(totalRowsHeight - height, scrollTop + rowHeight) | ||
start = getVisibleRowIndices({ | ||
cellCount: rowsCount, | ||
cellMetadata: this._cellMetadata, | ||
containerSize: height, | ||
currentOffset: scrollTop | ||
}).start | ||
datum = this._cellMetadata[start] | ||
newScrollTop = Math.min( | ||
this._getTotalRowsHeight() - height, | ||
scrollTop + datum.size | ||
) | ||
this.setState({ scrollTop: newScrollTop }) | ||
this.setState({ | ||
scrollTop: newScrollTop | ||
}) | ||
break | ||
@@ -367,5 +414,10 @@ case 'ArrowUp': | ||
this.setState({ | ||
scrollTop: Math.max(0, scrollTop - rowHeight) | ||
}) | ||
start = getVisibleRowIndices({ | ||
cellCount: rowsCount, | ||
cellMetadata: this._cellMetadata, | ||
containerSize: height, | ||
currentOffset: scrollTop | ||
}).start | ||
this.scrollToRow(Math.max(0, start - 1)) | ||
break | ||
@@ -387,4 +439,4 @@ } | ||
// We can avoid that by doing some simple bounds checking to ensure that scrollTop never exceeds the total height. | ||
const { height, rowsCount, rowHeight } = this.props | ||
const totalRowsHeight = rowsCount * rowHeight | ||
const { height } = this.props | ||
const totalRowsHeight = this._getTotalRowsHeight() | ||
const scrollTop = Math.min(totalRowsHeight - height, event.target.scrollTop) | ||
@@ -429,2 +481,1 @@ | ||
} | ||
VirtualScroll.prototype.shouldComponentUpdate = shouldPureComponentUpdate |
@@ -76,7 +76,7 @@ import React from 'react' | ||
describe('number of rendered children', () => { | ||
it('should render enough children to fill the view +1 for partial visibility at top and bottom', () => { | ||
it('should render enough children to fill the view', () => { | ||
const list = renderList() | ||
const listDOMNode = findDOMNode(list) | ||
expect(listDOMNode.querySelectorAll('.listItem').length).toEqual(11) | ||
expect(listDOMNode.querySelectorAll('.listItem').length).toEqual(10) | ||
}) | ||
@@ -116,110 +116,2 @@ | ||
/** Tests fine-grained control of scrolling from position A to B */ | ||
describe('scrollToIndex / _calculateScrollTopForIndex', () => { | ||
function calculateScrollTopForIndex (scrollToIndex, scrollTop = 0) { | ||
return VirtualScroll._calculateScrollTopForIndex({ | ||
height: 100, | ||
rowHeight: 10, | ||
rowsCount: list.size, | ||
scrollToIndex, | ||
scrollTop | ||
}) | ||
} | ||
it('should scroll to the top', () => { | ||
expect(calculateScrollTopForIndex(0)).toEqual(0) | ||
}) | ||
it('should scroll down to the middle', () => { | ||
expect(calculateScrollTopForIndex(49)).toEqual(400) | ||
}) | ||
it('should scroll up to the middle', () => { | ||
expect(calculateScrollTopForIndex(49, 800)).toEqual(490) | ||
}) | ||
it('should not scroll if an item is already visible', () => { | ||
expect(calculateScrollTopForIndex(49, 470)).toEqual(470) | ||
}) | ||
it('should scroll to the bottom', () => { | ||
expect(calculateScrollTopForIndex(99)).toEqual(900) | ||
}) | ||
it('should not scroll past the top', () => { | ||
expect(calculateScrollTopForIndex(-5)).toEqual(0) | ||
}) | ||
it('should not scroll past the bottom', () => { | ||
expect(calculateScrollTopForIndex(105)).toEqual(900) | ||
}) | ||
}) | ||
describe('_getMaxVisibleRows', () => { | ||
function getMaxVisibleRows (rowsCount) { | ||
return VirtualScroll._getMaxVisibleRows({ | ||
height: 100, | ||
rowHeight: 20, | ||
rowsCount | ||
}) | ||
} | ||
it('should handle no rows', () => { | ||
expect(getMaxVisibleRows(0)).toEqual(0) | ||
}) | ||
it('should handle when there are fewer rows than available height', () => { | ||
expect(getMaxVisibleRows(2)).toEqual(2) | ||
}) | ||
it('should handle when the rows exactly fit', () => { | ||
expect(getMaxVisibleRows(5)).toEqual(5) | ||
}) | ||
it('should handle when there are more rows than the available height', () => { | ||
expect(getMaxVisibleRows(100)).toEqual(6) // Exact fit +1 extra for overlap | ||
}) | ||
}) | ||
describe('_getStartAndStopIndexForScrollTop', () => { | ||
function getStartAndStopIndexForScrollTop (scrollTop) { | ||
return VirtualScroll._getStartAndStopIndexForScrollTop({ | ||
height: 100, | ||
rowHeight: 20, | ||
rowsCount: 100, | ||
scrollTop | ||
}) | ||
} | ||
it('should handle unscrolled', () => { | ||
const { rowIndexStart, rowIndexStop } = getStartAndStopIndexForScrollTop(0) | ||
expect(rowIndexStart).toEqual(0) | ||
expect(rowIndexStop).toEqual(5) | ||
}) | ||
it('should handle scrolled to the middle', () => { | ||
const { rowIndexStart, rowIndexStop } = getStartAndStopIndexForScrollTop(1000) | ||
expect(rowIndexStart).toEqual(50) | ||
expect(rowIndexStop).toEqual(55) | ||
}) | ||
it('should handle scrolled to the end', () => { | ||
const { rowIndexStart, rowIndexStop } = getStartAndStopIndexForScrollTop(1920) | ||
expect(rowIndexStart).toEqual(95) | ||
expect(rowIndexStop).toEqual(99) | ||
}) | ||
it('should handle scrolled past the end', () => { | ||
const { rowIndexStart, rowIndexStop } = getStartAndStopIndexForScrollTop(3000) | ||
expect(rowIndexStart).toEqual(95) | ||
expect(rowIndexStop).toEqual(99) | ||
}) | ||
it('should handle scrolled past the beginning', () => { | ||
const { rowIndexStart, rowIndexStop } = getStartAndStopIndexForScrollTop(-200) | ||
expect(rowIndexStart).toEqual(0) | ||
expect(rowIndexStop).toEqual(5) | ||
}) | ||
}) | ||
describe('property updates', () => { | ||
@@ -281,3 +173,3 @@ it('should update :scrollToIndex position when :rowHeight changes', () => { | ||
expect(startIndex).toEqual(0) | ||
expect(stopIndex).toEqual(10) | ||
expect(stopIndex).toEqual(9) | ||
}) | ||
@@ -284,0 +176,0 @@ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
305604
54
5034