Skip to content
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

[v4] Add Table.VirtualBody #267

Merged
merged 3 commits into from
Jul 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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