Skip to content

Commit

Permalink
[v4] Add Table.VirtualBody (#267)
Browse files Browse the repository at this point in the history
* wip virtual body

* add Table.VirtualBody
  • Loading branch information
jeroenransijn authored Jul 30, 2018
1 parent fab005d commit 5099629
Show file tree
Hide file tree
Showing 8 changed files with 349 additions and 20 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"eslint-config-xo-react": "^0.14.0",
"eslint-plugin-react": "^7.5.1",
"execa": "^0.8.0",
"faker": "^4.1.0",
"file-loader": "^1.1.5",
"fs-extra": "^4.0.3",
"husky": "^0.14.3",
Expand Down
8 changes: 3 additions & 5 deletions src/table/src/Table.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { PureComponent } from 'react'
import { Pane } from '../../layers'
import TableBody from './TableBody'
import TableVirtualBody from './TableVirtualBody'
import TableCell from './TableCell'
import TableHead from './TableHead'
import TableHeaderCell from './TableHeaderCell'
Expand All @@ -11,6 +12,7 @@ import SearchTableHeaderCell from './SearchTableHeaderCell'

export default class Table extends PureComponent {
static Body = TableBody
static VirtualBody = TableVirtualBody
static Head = TableHead
static HeaderCell = TableHeaderCell
static TextHeaderCell = TextTableHeaderCell
Expand All @@ -28,10 +30,6 @@ export default class Table extends PureComponent {

render() {
const { children, ...props } = this.props
return (
<Pane border {...props}>
{children}
</Pane>
)
return <Pane {...props}>{children}</Pane>
}
}
1 change: 1 addition & 0 deletions src/table/src/TableHead.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export default class TableHead extends PureComponent {
return (
<Pane
display="flex"
flexShrink={0}
paddingRight={scrollbarWidth}
borderBottom="default"
background="tint2"
Expand Down
259 changes: 259 additions & 0 deletions src/table/src/TableVirtualBody.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import VirtualList from 'react-tiny-virtual-list'
import debounce from 'lodash.debounce'
import { Pane } from '../../layers'

export default class TableVirtualBody extends PureComponent {
static propTypes = {
/**
* Composes the Pane component as the base.
*/
...Pane.propTypes,

/**
* Children needs to be an array of a single node.
*/
children: PropTypes.arrayOf(PropTypes.node),

/**
* Default height of each row.
* 48 is the default height of a TableRow.
*/
defaultHeight: PropTypes.number,

/**
* When true, support `height="auto"` on children being rendered.
* This is somewhat of an expirmental feature.
*/
allowAutoHeight: PropTypes.bool,

/**
* When passed, this is used as the `estimatedItemSize` in react-tiny-virtual-list.
* Only when `allowAutoHeight` and`useAverageAutoHeightEstimation` are false.
*/
estimatedItemSize: PropTypes.number,

/**
* When allowAutoHeight is true and this prop is true, the estimated height
* will be computed based on the average height of auto height rows.
*/
useAverageAutoHeightEstimation: PropTypes.bool
}

static defaultProps = {
defaultHeight: 48,
allowAutoHeight: false,
useAverageAutoHeightEstimation: true
}

state = {
isIntegerHeight: false,
calculatedHeight: 0
}

static getDerivedStateFromProps(props, state) {
if (props.height !== state.calculatedHeight) {
return {
isIntegerHeight: Number.isInteger(props.height)
}
}

// Return null to indicate no change to state.
return null
}

constructor(props) {
super(props)

this.initializeHelpers()

// Add a onResize.
this.onResize = debounce(this.onResize, 200)
}

componentDidMount() {
// Call this to initialize and set
this.updateOnResize()
window.addEventListener('resize', this.onResize, false)
}

componentWillUnmount() {
window.removeEventListener('resize', this.onResize)
}

initializeHelpers = () => {
this.autoHeights = []
this.autoHeightRefs = []
this.averageAutoHeight = this.props.defaultHeight
}

/**
* This function will process all items that have height="auto" set.
* It will loop through all refs and get calculate the height.
*/
processAutoHeights = () => {
let isUpdated = false

// This will determine the averageAutoHeight.
let total = 0
let totalAmount = 0

// Loop through all of the refs that have height="auto".
this.autoHeightRefs.forEach((ref, index) => {
// If the height is already calculated, skip it,
// but calculate the height for the total.
if (this.autoHeights[index]) {
total += this.autoHeights[index]
totalAmount += 1
return
}

// Make sure the ref has a child
if (
ref &&
ref.childNodes &&
ref.childNodes[0] &&
Number.isInteger(ref.childNodes[0].offsetHeight)
) {
const height = ref.childNodes[0].offsetHeight

// Add to the total to calculate the averageAutoHeight.
total += height
totalAmount += 1

// Cache the height.
this.autoHeights[index] = height

// Set the update flag to true.
isUpdated = true
}
})

// Save the average height.
this.averageAutoHeight = total / totalAmount

// There are some new heights detected that had previously not been calculated.
// Call forceUpdate to make sure the virtual list renders again.
if (isUpdated) this.forceUpdate()
}

onRef = ref => {
this.paneRef = ref
}

onVirtualHelperRef = (index, ref) => {
this.autoHeightRefs[index] = ref

requestAnimationFrame(() => {
this.processAutoHeights()
})
}

onResize = () => {
this.updateOnResize()
}

updateOnResize = () => {
this.initializeHelpers()

// Simply return when we now the height of the pane is fixed.
if (this.state.isIntegerHeight) return

// Return if we are in a weird edge case in which the ref is no longer valid.
if (!this.paneRef) return

// Save the calculated height which is needed for the VirtualList.
this.setState({
calculatedHeight: this.paneRef.offsetHeight
})
}

render() {
const {
children = [],
height: paneHeight,
defaultHeight,
allowAutoHeight,
estimatedItemSize,
useAverageAutoHeightEstimation,
...props
} = this.props

// VirtualList needs a fixed height.
const { calculatedHeight } = this.state

return (
<Pane
innerRef={this.onRef}
height={paneHeight}
flex="1"
overflow="hidden"
{...props}
>
<VirtualList
height={calculatedHeight}
width="100%"
estimatedItemSize={
allowAutoHeight && useAverageAutoHeightEstimation
? this.averageAutoHeight
: estimatedItemSize || null
}
itemSize={index => {
const { height } = children[index].props

// When the height is number simply, simply return it.
if (Number.isInteger(height)) {
return height
}

// When allowAutoHeight is set and the height is set to "auto"...
if (allowAutoHeight && children[index].props.height === 'auto') {
// ... and the height is calculated, return the calculated height.
if (this.autoHeights[index]) return this.autoHeights[index]

// ... if the height is not yet calculated, return the averge
if (useAverageAutoHeightEstimation) return this.averageAutoHeight
}

// Return the default height.
return defaultHeight
}}
overscanCount={5}
itemCount={React.Children.count(children)}
renderItem={({ index, style }) => {
// When allowing height="auto" for rows, and a auto height item is
// rendered for the first time...
if (
allowAutoHeight &&
children[index].props.height === 'auto' &&
// ... and only when the height is not already been calculated.
!this.autoHeights[index]
) {
// ... render the item in a helper div, the ref is used to calculate
// the height of its children.
return (
<div
ref={ref => this.onVirtualHelperRef(index, ref)}
data-virtual-index={index}
style={{
opacity: 0,
...style
}}
>
{children[index]}
</div>
)
}

// When allowAutoHeight is false, or when the height is known.
// Simply render the item.
return React.cloneElement(children[index], {
style
})
}}
/>
</Pane>
)
}
}
20 changes: 5 additions & 15 deletions src/table/stories/AdvancedTable.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react'
import { filter } from 'fuzzaldrin-plus'
import VirtualList from 'react-tiny-virtual-list'
import { Table } from '../../table'
import { Popover } from '../../popover'
import { Position } from '../../positioner'
Expand Down Expand Up @@ -218,9 +217,9 @@ export default class AdvancedTable extends React.Component {
)
}

renderRow = ({ profile, style }) => {
renderRow = ({ profile }) => {
return (
<Table.Row key={profile.id} style={style}>
<Table.Row key={profile.id}>
<Table.Cell display="flex" alignItems="center">
<Avatar name={profile.name} flexShrink={0} />
<Text marginLeft={8} size={300} fontWeight={500}>
Expand Down Expand Up @@ -254,18 +253,9 @@ export default class AdvancedTable extends React.Component {
{this.renderLTVTableHeaderCell()}
<Table.HeaderCell width={48} flex="none" />
</Table.Head>
<Table.Body height={640}>
<VirtualList
height={640}
width="100%"
itemSize={48}
overscanCount={3}
itemCount={items.length}
renderItem={({ index, style }) => {
return this.renderRow({ profile: items[index], style })
}}
/>
</Table.Body>
<Table.VirtualBody height={640}>
{items.map(item => this.renderRow({ profile: item }))}
</Table.VirtualBody>
</Table>
)
}
Expand Down
Loading

0 comments on commit 5099629

Please sign in to comment.