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

feat: allow grouped table #792

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"workbench.colorCustomizations": {}
}
43 changes: 43 additions & 0 deletions examples/group-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { useMemo } from 'react'
import { GroupedTableVirtuoso } from '../src/'

export default function App() {
const groupCounts = useMemo(() => {
return Array(1000).fill(10) as number[]
}, [])

return (
<GroupedTableVirtuoso
groupCounts={groupCounts}
style={{ height: 700 }}
fixedHeaderContent={() => {
return (
<tr style={{ background: 'white', textAlign: 'left' }}>
<th key={1} style={{ width: '140px' }}>
Item index
</th>
<th key={2} style={{ width: '140px' }}>
Greetings
</th>
</tr>
)
}}
itemContent={(index) => {
return (
<>
<td style={{ height: 21 }}>{index}</td>
<td style={{ height: 21 }}>Hello</td>
</>
)
}}
groupContent={(index) => {
return (
<>
<td style={{ height: 21, background: 'white' }}>Group {index}</td>
<td style={{ height: 21, background: 'white' }} />
</>
)
}}
/>
)
}
56 changes: 56 additions & 0 deletions site/docs/grouped-table.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
id: grouped-table
title: Grouped Table
sidebar_label: Grouped Table
slug: /grouped-table/
---

The example below shows a simple table grouping mode.

```jsx live
import { GroupedTableVirtuoso } from 'react-virtuoso'
import { useMemo, useRef } from 'react'

export default function App() {
const ref = useRef()

const groupCounts = useMemo(() => {
return Array(1000).fill(10)
}, [])

return (
<GroupedTableVirtuoso
groupCounts={groupCounts}
style={{ height: 400 }}
fixedHeaderContent={() => {
return (
<tr style={{ background: 'white', textAlign: 'left' }}>
<th key={1} style={{ width: '140px' }}>
Item index
</th>
<th key={2} style={{ width: '140px' }}>
Greetings
</th>
</tr>
)
}}
itemContent={(index) => {
return (
<>
<td style={{ height: 21 }}>{index}</td>
<td style={{ height: 21 }}>Hello</td>
</>
)
}}
groupContent={(index) => {
return (
<>
<td style={{ height: 21, background: 'white' }}>Group {index}</td>
<td style={{ height: 21, background: 'white' }} />
</>
)
}}
/>
)
}
```
13 changes: 10 additions & 3 deletions site/docs/table-virtuoso-api-reference.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
---
id: table-virtuoso-api-reference
title: Table Virtuoso API Reference
sidebar_label: Table Virtuoso
sidebar_label: Table Virtuoso
slug: /table-virtuoso-api-reference/
---

import Props from './api/interfaces/_component_interfaces_tablevirtuoso_.tablevirtuosoprops.md'
import GroupProps from './api/interfaces/_component_interfaces_tablevirtuoso_.groupedtablevirtuosoprops.md'
import VirtuosoProps from './api/interfaces/_component_interfaces_virtuoso_.virtuosoprops.md'
import Methods from './api/interfaces/_component_interfaces_virtuoso_.virtuosohandle.md'

All properties are optional - by default, the component will render empty.
All properties are optional - by default, the component will render empty.

If you are using TypeScript and want to use correctly typed component `ref`, you can use the `VirtuosoHandle`.

Expand All @@ -21,12 +22,18 @@ const ref = useRef<VirtuosoHandle>(null)
<TableVirtuoso ref={ref} /*...*/ />
```

## Table Virtuoso Properties
## TableVirtuoso Properties

<div className="generated-api">
<Props />
</div>

## GroupedTableVirtuoso Properties

<div className="generated-api">
<GroupProps />
</div>

## Methods

<div className="generated-api">
Expand Down
2 changes: 1 addition & 1 deletion site/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ module.exports = {
'initial-index',
'range-change-callback',
],
'Grouped Mode': ['grouped-numbers', 'grouped-by-first-letter', 'grouped-with-load-on-demand', 'scroll-to-group'],
'Grouped Mode': ['grouped-numbers', 'grouped-by-first-letter', 'grouped-with-load-on-demand', 'scroll-to-group', 'grouped-table'],
Table: ['hello-table', 'table-fixed-headers', 'mui-table-virtual-scroll', 'table-fixed-columns', 'react-table-integration'],
Grid: ['grid-responsive-columns'],
Scenarios: [
Expand Down
17 changes: 16 additions & 1 deletion site/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2308,6 +2308,11 @@
resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.3.tgz#e7b5aebbac150f8b5fdd4a46e7f0bd8e65e19109"
integrity sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==

"@types/prop-types@*":
version "15.7.5"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==

"@types/prop-types@^15.7.4":
version "15.7.4"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
Expand Down Expand Up @@ -2340,10 +2345,20 @@
"@types/react" "*"

"@types/react@*":
version "0.0.0"
version "18.0.5"
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"

"@types/react@link:../node_modules/@types/react":
version "0.0.0"
uid ""

"@types/scheduler@*":
version "0.16.2"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==

"@types/source-list-map@*":
version "0.1.2"
Expand Down
55 changes: 47 additions & 8 deletions src/TableVirtuoso.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { systemToComponent } from './react-urx'
import * as u from './urx'
import { createElement, FC, PropsWithChildren, ReactElement, Ref, useContext, memo, useState, useEffect } from 'react'
import { createElement, FC, PropsWithChildren, ReactElement, Ref, useContext, memo, useState, useEffect, Fragment } from 'react'
import useChangedListContentsSizes from './hooks/useChangedChildSizes'
import { ComputeItemKey, ItemContent, FixedHeaderContent, FixedFooterContent, TableComponents, TableRootProps } from './interfaces'
import {
ComputeItemKey,
ItemContent,
FixedHeaderContent,
FixedFooterContent,
TableComponents,
TableRootProps,
GroupContent,
} from './interfaces'
import { positionStickyCssValue } from './utils/positionStickyCssValue'

import { listSystem } from './listSystem'
import { identity, buildScroller, buildWindowScroller, viewportStyle, contextPropIfNotDomElement } from './Virtuoso'
import useSize from './hooks/useSize'
Expand All @@ -11,11 +21,14 @@ import useWindowViewportRectRef from './hooks/useWindowViewportRect'
import { VirtuosoMockContext } from './utils/context'
import { TableVirtuosoHandle, TableVirtuosoProps } from './component-interfaces/TableVirtuoso'

const GROUP_STYLE = { position: positionStickyCssValue(), zIndex: 1, overflowAnchor: 'none' } as const

const tableComponentPropsSystem = /*#__PURE__*/ u.system(() => {
const itemContent = u.statefulStream<ItemContent<any, unknown>>((index: number) => <td>Item ${index}</td>)
const context = u.statefulStream<unknown>(null)
const fixedHeaderContent = u.statefulStream<FixedHeaderContent>(null)
const fixedFooterContent = u.statefulStream<FixedFooterContent>(null)
const groupContent = u.statefulStream<GroupContent>((index: number) => <td>Group {index}</td>)
const components = u.statefulStream<TableComponents>({})
const computeItemKey = u.statefulStream<ComputeItemKey<any, unknown>>(identity)
const scrollerRef = u.statefulStream<(ref: HTMLElement | Window | null) => void>(u.noop)
Expand All @@ -37,6 +50,7 @@ const tableComponentPropsSystem = /*#__PURE__*/ u.system(() => {
return {
context,
itemContent,
groupContent,
fixedHeaderContent,
fixedFooterContent,
components,
Expand All @@ -47,6 +61,7 @@ const tableComponentPropsSystem = /*#__PURE__*/ u.system(() => {
TableFooterComponent: distinctProp('TableFoot', 'tfoot'),
TableBodyComponent: distinctProp('TableBody', 'tbody'),
TableRowComponent: distinctProp('TableRow', 'tr'),
GroupComponent: distinctProp('Group', 'tr'),
ScrollerComponent: distinctProp('Scroller', 'div'),
EmptyPlaceholder: distinctProp('EmptyPlaceholder'),
ScrollSeekPlaceholder: distinctProp('ScrollSeekPlaceholder'),
Expand All @@ -70,7 +85,7 @@ const DefaultFillerRow = ({ height }: { height: number }) => (
</tr>
)

const Items = /*#__PURE__*/ memo(function VirtuosoItems() {
const Items = /*#__PURE__*/ memo(function VirtuosoItems({ showTopList = false }: { showTopList?: boolean }) {
const listState = useEmitterValue('listState')
const sizeRanges = usePublisher('sizeRanges')
const useWindowScroll = useEmitterValue('useWindowScroll')
Expand All @@ -83,6 +98,7 @@ const Items = /*#__PURE__*/ memo(function VirtuosoItems() {
const trackItemSizes = useEmitterValue('trackItemSizes')
const itemSize = useEmitterValue('itemSize')
const log = useEmitterValue('log')
const groupContent = useEmitterValue('groupContent')

const { callbackRef, ref } = useChangedListContentsSizes(
sizeRanges,
Expand All @@ -106,6 +122,7 @@ const Items = /*#__PURE__*/ memo(function VirtuosoItems() {
const FillerRow = useEmitterValue('FillerRow') || DefaultFillerRow
const TableBodyComponent = useEmitterValue('TableBodyComponent')!
const TableRowComponent = useEmitterValue('TableRowComponent')!
const GroupComponent = useEmitterValue('GroupComponent')!
const computeItemKey = useEmitterValue('computeItemKey')
const isSeeking = useEmitterValue('isSeeking')
const paddingTopAddition = useEmitterValue('paddingTopAddition')
Expand All @@ -117,14 +134,14 @@ const Items = /*#__PURE__*/ memo(function VirtuosoItems() {
return createElement(EmptyPlaceholder, contextPropIfNotDomElement(EmptyPlaceholder, context))
}

const paddingTop = listState.offsetTop + paddingTopAddition + deviation
const paddingTop = listState.offsetTop - listState.topListHeight + paddingTopAddition + deviation
const paddingBottom = listState.offsetBottom

const paddingTopEl = paddingTop > 0 ? <FillerRow height={paddingTop} key="padding-top" /> : null
const paddingTopEl = showTopList === false && paddingTop > 0 ? <FillerRow height={paddingTop} key="padding-top" /> : null

const paddingBottomEl = paddingBottom > 0 ? <FillerRow height={paddingBottom} key="padding-bottom" /> : null
const paddingBottomEl = showTopList === false && paddingBottom > 0 ? <FillerRow height={paddingBottom} key="padding-bottom" /> : null

const items = listState.items.map((item) => {
const items = (showTopList ? listState.topItems : listState.items).map((item) => {
const index = item.originalIndex!
const key = computeItemKey(index + firstItemIndex, item.data, context)

Expand All @@ -137,6 +154,22 @@ const Items = /*#__PURE__*/ memo(function VirtuosoItems() {
type: item.type || 'item',
})
}

if (item.type === 'group') {
return createElement(
GroupComponent,
{
...contextPropIfNotDomElement(GroupComponent, context),
key,
'data-index': index,
'data-known-size': item.size,
'data-item-index': item.index,
style: GROUP_STYLE,
},
groupContent(item.index)
)
}

return createElement(
TableRowComponent,
{
Expand All @@ -145,13 +178,16 @@ const Items = /*#__PURE__*/ memo(function VirtuosoItems() {
'data-index': index,
'data-known-size': item.size,
'data-item-index': item.index,
'data-item-group-index': item.groupIndex,
item: item.data,
style: { overflowAnchor: 'none' },
},
itemContent(item.index, item.data, context)
)
})

if (showTopList) return createElement(Fragment, {}, [paddingTopEl, ...items, paddingBottomEl])

return createElement(
TableBodyComponent,
{ ref: callbackRef, 'data-test-id': 'virtuoso-item-list', ...contextPropIfNotDomElement(TableBodyComponent, context) },
Expand Down Expand Up @@ -215,6 +251,7 @@ const TableRoot: FC<TableRootProps> = /*#__PURE__*/ memo(function TableVirtuosoR
const TheTable = useEmitterValue('TableComponent')
const TheTHead = useEmitterValue('TableHeadComponent')
const TheTFoot = useEmitterValue('TableFooterComponent')
const showTopList = useEmitterValue('topItemsIndexes').length > 0

const theHead = fixedHeaderContent
? createElement(
Expand All @@ -225,7 +262,8 @@ const TableRoot: FC<TableRootProps> = /*#__PURE__*/ memo(function TableVirtuosoR
ref: theadRef,
...contextPropIfNotDomElement(TheTHead, context),
},
fixedHeaderContent()
fixedHeaderContent(),
...(showTopList ? [<Items key="fixed-header-content" showTopList />] : [])
)
: null
const theFoot = fixedFooterContent
Expand Down Expand Up @@ -276,6 +314,7 @@ const {
topItemCount: 'topItemCount',
initialTopMostItemIndex: 'initialTopMostItemIndex',
components: 'components',
groupContent: 'groupContent',
groupCounts: 'groupCounts',
atBottomThreshold: 'atBottomThreshold',
atTopThreshold: 'atTopThreshold',
Expand Down
20 changes: 20 additions & 0 deletions src/component-interfaces/TableVirtuoso.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import type {
FlatScrollIntoViewLocation,
FollowOutput,
ItemContent,
GroupContent,
ListItem,
ListRange,
ScrollSeekConfiguration,
SizeFunction,
TableComponents,
GroupItemContent,
} from '../interfaces'
import type { VirtuosoProps } from './Virtuoso'

Expand Down Expand Up @@ -209,6 +211,24 @@ export interface TableVirtuosoProps<D, C> extends Omit<VirtuosoProps<D, C>, 'com
atBottomThreshold?: number
}

export interface GroupedTableVirtuosoProps<D, C> extends Omit<TableVirtuosoProps<D, C>, 'totalCount' | 'itemContent'> {
/**
* Specifies the amount of items in each group (and, actually, how many groups are there).
* For example, passing [20, 30] will display 2 groups with 20 and 30 items each.
*/
groupCounts?: number[]

/**
* Specifies how each each group header gets rendered. The callback receives the zero-based index of the group.
*/
groupContent?: GroupContent

/**
* Specifies how each each item gets rendered.
*/
itemContent?: GroupItemContent<D, C>
}

export interface TableVirtuosoHandle {
scrollIntoView(location: number | FlatScrollIntoViewLocation): void
scrollToIndex(location: number | FlatIndexLocationWithAlign): void
Expand Down