-
Notifications
You must be signed in to change notification settings - Fork 291
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Beta fix stub cells #460
Beta fix stub cells #460
Changes from all commits
6bf3e9a
de2c67b
d9f3420
22de3eb
250ac37
e4c3d0a
0968b72
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
/** | ||
* Copyright Schrodinger, LLC | ||
*/ | ||
|
||
"use strict"; | ||
|
||
const FakeObjectDataListStore = require('./helpers/FakeObjectDataListStore'); | ||
const { ImageCell, LinkCell } = require('./helpers/cells'); | ||
const { Table, Column, Cell } = require('fixed-data-table-2'); | ||
const React = require('react'); | ||
|
||
class AutoScrollExample extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
|
||
this.state = { | ||
dataList: new FakeObjectDataListStore(1000000), | ||
scrollTop: 0, | ||
scrollLeft: 0, | ||
autoScrollEnabled: true, | ||
horizontalScrollDelta: 0, | ||
verticalScrollDelta: 0, | ||
}; | ||
|
||
this.columns = []; | ||
const cellRenderer = ({ columnKey, rowIndex }) => | ||
(<div className='autoScrollCell'> {rowIndex}, {columnKey} </div>); | ||
|
||
for (let i = 0; i < 100; i++) { | ||
this.columns[i] = ( | ||
<Column | ||
key={i} | ||
columnKey={i} | ||
header={<div> {i} </div>} | ||
cell={cellRenderer} | ||
width={100} | ||
allowCellsRecycling={true} | ||
/> | ||
) | ||
} | ||
|
||
this.onVerticalScroll = this.onVerticalScroll.bind(this); | ||
this.onHorizontalScroll = this.onHorizontalScroll.bind(this); | ||
this.toggleAutoScroll = this.toggleAutoScroll.bind(this); | ||
this.setHorizontalScrollDelta = this.setHorizontalScrollDelta.bind(this); | ||
this.setVerticalScrollDelta = this.setVerticalScrollDelta.bind(this); | ||
} | ||
|
||
componentDidMount() { | ||
setInterval(() => { | ||
if (!this.state.autoScrollEnabled) { | ||
return; | ||
} | ||
this.setState((prevState) => ({ | ||
scrollTop: prevState.scrollTop + (prevState.verticalScrollDelta || 0), | ||
scrollLeft: prevState.scrollLeft + (prevState.horizontalScrollDelta || 0), | ||
})); | ||
}, 16); | ||
} | ||
|
||
render() { | ||
return ( | ||
<div className='autoScrollContainer'> | ||
{this.renderControls()} | ||
{this.renderTable()} | ||
</div> | ||
); | ||
} | ||
|
||
renderControls() { | ||
return ( | ||
<div className='autoScrollControls'> | ||
<label> | ||
Auto Scroll Enabled | ||
<input type='checkbox' checked={this.state.autoScrollEnabled} onChange={this.toggleAutoScroll} /> | ||
</label> | ||
<label> | ||
Horizontal Scroll Delta | ||
<input type='number' value={this.state.horizontalScrollDelta} onChange={this.setHorizontalScrollDelta} /> | ||
</label> | ||
<label> | ||
Vertical Scroll Delta | ||
<input type='number' value={this.state.verticalScrollDelta} onChange={this.setVerticalScrollDelta} /> | ||
</label> | ||
</div> | ||
) | ||
} | ||
|
||
renderTable() { | ||
var { dataList, scrollLeft, scrollTop } = this.state; | ||
return ( | ||
<Table | ||
rowHeight={50} | ||
headerHeight={50} | ||
rowsCount={dataList.getSize()} | ||
width={1000} | ||
height={500} | ||
scrollLeft={scrollLeft} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. scrollLeft and scrollTop specified since we make use of controlled scrolling |
||
scrollTop={scrollTop} | ||
onVerticalScroll={this.onVerticalScroll} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these are needed since the user can also scroll the table directly outside of our "autoscroll" logic |
||
onHorizontalScroll={this.onHorizontalScroll} | ||
{...this.props} | ||
> | ||
<Column | ||
columnKey="avatar" | ||
cell={<ImageCell data={dataList} />} | ||
fixed={true} | ||
width={50} | ||
/> | ||
<Column | ||
columnKey="firstName" | ||
header={<Cell>First Name</Cell>} | ||
cell={<LinkCell data={dataList} />} | ||
fixed={true} | ||
width={100} | ||
/> | ||
{this.columns} | ||
</Table> | ||
); | ||
} | ||
|
||
onVerticalScroll(scrollTop) { | ||
this.setState({ scrollTop }); | ||
} | ||
|
||
onHorizontalScroll(scrollLeft) { | ||
this.setState({ scrollLeft }); | ||
} | ||
|
||
toggleAutoScroll() { | ||
this.setState((prevState) => ({ | ||
autoScrollEnabled: !prevState.autoScrollEnabled, | ||
})); | ||
} | ||
|
||
setHorizontalScrollDelta(event) { | ||
const { value } = event.target; | ||
if (isNaN(value)) { | ||
return; | ||
} | ||
this.setState({ | ||
horizontalScrollDelta: parseInt(value), | ||
}); | ||
} | ||
|
||
setVerticalScrollDelta(event) { | ||
const { value } = event.target; | ||
if (isNaN(value)) { | ||
return; | ||
} | ||
this.setState({ | ||
verticalScrollDelta: parseInt(value), | ||
}); | ||
} | ||
} | ||
|
||
module.exports = AutoScrollExample; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -326,3 +326,17 @@ | |
height: 50px; | ||
width: 50px; | ||
} | ||
|
||
.autoScrollContainer { | ||
.autoScrollControls { | ||
display: flex; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just space the controls evenly (both between and outside) |
||
justify-content: space-around; | ||
align-items: baseline; | ||
} | ||
.autoScrollCell { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. center text both horizontally and vertically |
||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
height: 100%; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -77,119 +77,96 @@ class FixedDataTableBufferedRows extends React.Component { | |
} | ||
|
||
render() /*object*/ { | ||
var props = this.props; | ||
var rowClassNameGetter = props.rowClassNameGetter || emptyFunction; | ||
var rowsToRender = this.props.rowsToRender || []; | ||
|
||
if (props.isScrolling) { | ||
// We are scrolling, so there's no need to display any rows which lie outside the viewport. | ||
// We still need to render them though, so as to not cause any mounts/unmounts. | ||
this._staticRowArray.forEach((row, i) => { | ||
const rowOutsideViewport = !this.isRowInsideViewport(row.props.index); | ||
if (rowOutsideViewport) { | ||
this._staticRowArray[i] = this.getStubRow(i, false, row.props.index); | ||
} | ||
}); | ||
let { offsetTop, scrollTop, isScrolling, rowsToRender } = this.props; | ||
const baseOffsetTop = offsetTop - scrollTop; | ||
rowsToRender = rowsToRender || []; | ||
|
||
if (isScrolling) { | ||
// allow static array to grow while scrolling | ||
this._staticRowArray.length = Math.max(this._staticRowArray.length, rowsToRender.length); | ||
} else { | ||
// when scrolling is done, static array can shrink to fit the buffer | ||
this._staticRowArray.length = rowsToRender.length; | ||
} | ||
|
||
var baseOffsetTop = props.offsetTop - props.scrollTop; | ||
|
||
for (let i = 0; i < rowsToRender.length; i++) { | ||
const rowIndex = rowsToRender[i]; | ||
|
||
// if the row doesn't exist in the buffer, assign a fake row to it. | ||
// this is so that we can get rid of unnecessary row mounts/unmounts | ||
// render each row from the buffer into the static row array | ||
for (let i = 0; i < this._staticRowArray.length; i++) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. refactored so that there's only a single loop over the rows |
||
let rowIndex = rowsToRender[i]; | ||
// if the row doesn't exist in the buffer set, then take the previous one | ||
if (rowIndex === undefined) { | ||
// if a previous row existed, let's just make use of that | ||
if (this._staticRowArray[i] === undefined) { | ||
this._staticRowArray[i] = this.getStubRow(i, true, -1); | ||
} | ||
continue; | ||
rowIndex = this._staticRowArray[i] && this._staticRowArray[i].props.index; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. here we take the previous rendered row to avoid unmounts if it is still undefined (aka, fake row), it will be rendered as null |
||
} | ||
|
||
const currentRowHeight = this.props.rowSettings.rowHeightGetter(rowIndex); | ||
const currentSubRowHeight = this.props.rowSettings.subRowHeightGetter(rowIndex); | ||
const rowOffsetTop = baseOffsetTop + props.rowOffsets[rowIndex]; | ||
const rowKey = props.rowKeyGetter ? props.rowKeyGetter(rowIndex) : i; | ||
const hasBottomBorder = (rowIndex === props.rowSettings.rowsCount - 1) && | ||
props.showLastRowBorder; | ||
const visible = this.isRowInsideViewport(rowIndex); | ||
|
||
this._staticRowArray[i] = | ||
<FixedDataTableRow | ||
key={rowKey} | ||
isScrolling={props.isScrolling} | ||
index={rowIndex} | ||
width={props.width} | ||
height={currentRowHeight} | ||
subRowHeight={currentSubRowHeight} | ||
rowExpanded={props.rowExpanded} | ||
scrollLeft={Math.round(props.scrollLeft)} | ||
offsetTop={Math.round(rowOffsetTop)} | ||
visible={visible} | ||
fixedColumns={props.fixedColumns} | ||
fixedRightColumns={props.fixedRightColumns} | ||
scrollableColumns={props.scrollableColumns} | ||
onClick={props.onRowClick} | ||
onContextMenu={props.onRowContextMenu} | ||
onDoubleClick={props.onRowDoubleClick} | ||
onMouseDown={props.onRowMouseDown} | ||
onMouseUp={props.onRowMouseUp} | ||
onMouseEnter={props.onRowMouseEnter} | ||
onMouseLeave={props.onRowMouseLeave} | ||
onTouchStart={props.onRowTouchStart} | ||
onTouchEnd={props.onRowTouchEnd} | ||
onTouchMove={props.onRowTouchMove} | ||
showScrollbarY={props.showScrollbarY} | ||
className={joinClasses( | ||
rowClassNameGetter(rowIndex), | ||
cx('public/fixedDataTable/bodyRow'), | ||
cx({ | ||
'fixedDataTableLayout/hasBottomBorder': hasBottomBorder, | ||
'public/fixedDataTable/hasBottomBorder': hasBottomBorder, | ||
}) | ||
)} | ||
/>; | ||
this._staticRowArray[i] = this.renderRow({ | ||
rowIndex, | ||
key: i, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. out of interest, why is this the key not set to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. because if the key is given as a function of rowIndex, rows will get mounted/unmounted whenever row indexes change. So we keep the keys constant, form 0 to no:rows present. FDT uses IntegerBufferSet to manage row index to key mappings, so that when you scroll and it leads to a new row getting visible in the viewport, it'll replace the old one that went outside the viewport. So basically if we scroll down by 1 row, for an initial viewport of 10 rows, then row 0 at key 0 might gets replaced by row 10, and the key should still be 0 to prevent react mounting/unmounting. |
||
baseOffsetTop, | ||
}); | ||
} | ||
|
||
return <div>{this._staticRowArray}</div>; | ||
return <div> {this._staticRowArray} </div>; | ||
} | ||
|
||
|
||
/** | ||
* Returns a stub row which won't be visible to the user. | ||
* This allows us to still render a row and React won't unmount it. | ||
* | ||
* @param {number} rowIndex | ||
* @param {number} key | ||
* @param {boolean} fake | ||
* @param {number} index | ||
* @param {number} baseOffsetTop | ||
* @return {!Object} | ||
*/ | ||
getStubRow(key, fake, index) /*object*/ { | ||
renderRow({ rowIndex, key, baseOffsetTop }) /*object*/ { | ||
const props = this.props; | ||
const rowClassNameGetter = props.rowClassNameGetter || emptyFunction; | ||
const fake = rowIndex === undefined; | ||
let rowProps = {}; | ||
|
||
// if row exists, then calculate row specific props | ||
if (!fake) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this was were we had trouble (actual issue) previously rows outside the viewport were seen as stub/fake, and hence given bad props |
||
rowProps.height = this.props.rowSettings.rowHeightGetter(rowIndex); | ||
rowProps.subRowHeight = this.props.rowSettings.subRowHeightGetter(rowIndex); | ||
rowProps.offsetTop = Math.round(baseOffsetTop + props.rowOffsets[rowIndex]); | ||
rowProps.key = props.rowKeyGetter ? props.rowKeyGetter(rowIndex) : key; | ||
|
||
const hasBottomBorder = (rowIndex === props.rowSettings.rowsCount - 1) && props.showLastRowBorder; | ||
rowProps.className = joinClasses( | ||
rowClassNameGetter(rowIndex), | ||
cx('public/fixedDataTable/bodyRow'), | ||
cx({ | ||
'fixedDataTableLayout/hasBottomBorder': hasBottomBorder, | ||
'public/fixedDataTable/hasBottomBorder': hasBottomBorder, | ||
}) | ||
); | ||
} | ||
|
||
const visible = inRange(rowIndex, this.props.firstViewportRowIndex, this.props.endViewportRowIndex); | ||
|
||
return ( | ||
<FixedDataTableRow | ||
key={key} | ||
index={rowIndex} | ||
isScrolling={props.isScrolling} | ||
index={index} | ||
width={props.width} | ||
height={0} | ||
offsetTop={0} | ||
rowExpanded={props.rowExpanded} | ||
scrollLeft={Math.round(props.scrollLeft)} | ||
visible={false} | ||
fake={fake} | ||
fixedColumns={props.fixedColumns} | ||
fixedRightColumns={props.fixedRightColumns} | ||
scrollableColumns={props.scrollableColumns} | ||
onClick={props.onRowClick} | ||
onContextMenu={props.onRowContextMenu} | ||
onDoubleClick={props.onRowDoubleClick} | ||
onMouseDown={props.onRowMouseDown} | ||
onMouseUp={props.onRowMouseUp} | ||
onMouseEnter={props.onRowMouseEnter} | ||
onMouseLeave={props.onRowMouseLeave} | ||
onTouchStart={props.onRowTouchStart} | ||
onTouchEnd={props.onRowTouchEnd} | ||
onTouchMove={props.onRowTouchMove} | ||
showScrollbarY={props.showScrollbarY} | ||
visible={visible} | ||
fake={fake} | ||
{...rowProps} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. apply the row specific props (will be an empty object for fake/stub rows) |
||
/> | ||
); | ||
} | ||
|
||
isRowInsideViewport(/*number*/rowIndex) { | ||
return inRange(rowIndex, this.props.firstViewportRowIndex, this.props.endViewportRowIndex); | ||
} | ||
}; | ||
|
||
module.exports = FixedDataTableBufferedRows; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
60fps