From 2b775bf700301402222395e117e74f7116772a21 Mon Sep 17 00:00:00 2001 From: malo Date: Mon, 21 Nov 2022 09:34:39 -0500 Subject: [PATCH 1/3] feat: allow grouped table --- .vscode/settings.json | 3 + examples/group-table.tsx | 108 ++++++++++++++++++++++ src/TableVirtuoso.tsx | 55 +++++++++-- src/component-interfaces/TableVirtuoso.ts | 10 ++ src/interfaces.ts | 5 + 5 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 examples/group-table.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..6649301f6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "workbench.colorCustomizations": {} +} \ No newline at end of file diff --git a/examples/group-table.tsx b/examples/group-table.tsx new file mode 100644 index 000000000..aced96af4 --- /dev/null +++ b/examples/group-table.tsx @@ -0,0 +1,108 @@ +import * as React from 'react' +import { TableVirtuoso, TableVirtuosoHandle } from '../src/' + +export default function App() { + const ref = React.useRef(null) + return ( + <> + { + return ( + + + Empty + + + ) + }, + }} + groupCounts={Array.from({ length: 100 }).fill(10) as number[]} + style={{ height: 700 }} + fixedHeaderContent={() => { + return ( + + + TH 1 + + + TH meh + + + ) + }} + fixedFooterContent={() => { + return ( + + + Footer TH 1 + + + Footer TH meh + + + ) + }} + itemContent={(index) => { + return ( + <> + {index}Cell 1 + Cell 2 + + ) + }} + groupContent={(index) => { + return ( + <> + Group {index} + Meh + + ) + }} + /> + + + + + +

Buttons should align 900 correctly

+ + ) +} diff --git a/src/TableVirtuoso.tsx b/src/TableVirtuoso.tsx index 8a5dd768b..be3d02f3c 100644 --- a/src/TableVirtuoso.tsx +++ b/src/TableVirtuoso.tsx @@ -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' @@ -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>((index: number) => Item ${index}) const context = u.statefulStream(null) const fixedHeaderContent = u.statefulStream(null) const fixedFooterContent = u.statefulStream(null) + const groupContent = u.statefulStream((index: number) => Group {index}) const components = u.statefulStream({}) const computeItemKey = u.statefulStream>(identity) const scrollerRef = u.statefulStream<(ref: HTMLElement | Window | null) => void>(u.noop) @@ -37,6 +50,7 @@ const tableComponentPropsSystem = /*#__PURE__*/ u.system(() => { return { context, itemContent, + groupContent, fixedHeaderContent, fixedFooterContent, components, @@ -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'), @@ -70,7 +85,7 @@ const DefaultFillerRow = ({ height }: { height: number }) => ( ) -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') @@ -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, @@ -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') @@ -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 ? : null + const paddingTopEl = showTopList === false && paddingTop > 0 ? : null - const paddingBottomEl = paddingBottom > 0 ? : null + const paddingBottomEl = showTopList === false && paddingBottom > 0 ? : 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) @@ -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, { @@ -145,6 +178,7 @@ 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' }, }, @@ -152,6 +186,8 @@ const Items = /*#__PURE__*/ memo(function VirtuosoItems() { ) }) + if (showTopList) return createElement(Fragment, {}, [paddingTopEl, ...items, paddingBottomEl]) + return createElement( TableBodyComponent, { ref: callbackRef, 'data-test-id': 'virtuoso-item-list', ...contextPropIfNotDomElement(TableBodyComponent, context) }, @@ -215,6 +251,7 @@ const TableRoot: FC = /*#__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( @@ -225,7 +262,8 @@ const TableRoot: FC = /*#__PURE__*/ memo(function TableVirtuosoR ref: theadRef, ...contextPropIfNotDomElement(TheTHead, context), }, - fixedHeaderContent() + fixedHeaderContent(), + ...(showTopList ? [] : []) ) : null const theFoot = fixedFooterContent @@ -276,6 +314,7 @@ const { topItemCount: 'topItemCount', initialTopMostItemIndex: 'initialTopMostItemIndex', components: 'components', + groupContent: 'groupContent', groupCounts: 'groupCounts', atBottomThreshold: 'atBottomThreshold', atTopThreshold: 'atTopThreshold', diff --git a/src/component-interfaces/TableVirtuoso.ts b/src/component-interfaces/TableVirtuoso.ts index 6fda4079c..1ee54ce63 100644 --- a/src/component-interfaces/TableVirtuoso.ts +++ b/src/component-interfaces/TableVirtuoso.ts @@ -5,6 +5,7 @@ import type { FlatScrollIntoViewLocation, FollowOutput, ItemContent, + GroupContent, ListItem, ListRange, ScrollSeekConfiguration, @@ -14,6 +15,15 @@ import type { import type { VirtuosoProps } from './Virtuoso' export interface TableVirtuosoProps extends Omit, 'components' | 'headerFooterTag' | 'topItemCount'> { + /** + * 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 /** * Use the `components` property for advanced customization of the elements rendered by the table. */ diff --git a/src/interfaces.ts b/src/interfaces.ts index 9df4de2a2..c95af5aff 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -172,6 +172,11 @@ export interface TableComponents { */ TableBody?: ComponentType + /** + * Set to customize the group item wrapping element. Use only if you would like to render list from elements different than a `div`. + */ + Group?: ComponentType + /** * Set to render a custom UI when the list is empty. */ From d606eb85c05d68e64696315a91195c63f943bc2c Mon Sep 17 00:00:00 2001 From: malo Date: Mon, 21 Nov 2022 10:08:05 -0500 Subject: [PATCH 2/3] split GroupedTableVirtuoso component --- examples/group-table.tsx | 7 +++--- src/component-interfaces/TableVirtuoso.ts | 28 +++++++++++++++-------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/examples/group-table.tsx b/examples/group-table.tsx index aced96af4..9ef7cc4d8 100644 --- a/examples/group-table.tsx +++ b/examples/group-table.tsx @@ -1,13 +1,12 @@ import * as React from 'react' -import { TableVirtuoso, TableVirtuosoHandle } from '../src/' +import { GroupedTableVirtuoso, GroupedTableVirtuosoHandle } from '../src/' export default function App() { - const ref = React.useRef(null) + const ref = React.useRef(null) return ( <> - { return ( diff --git a/src/component-interfaces/TableVirtuoso.ts b/src/component-interfaces/TableVirtuoso.ts index 1ee54ce63..22d5c0bed 100644 --- a/src/component-interfaces/TableVirtuoso.ts +++ b/src/component-interfaces/TableVirtuoso.ts @@ -11,19 +11,11 @@ import type { ScrollSeekConfiguration, SizeFunction, TableComponents, + GroupItemContent, } from '../interfaces' import type { VirtuosoProps } from './Virtuoso' export interface TableVirtuosoProps extends Omit, 'components' | 'headerFooterTag' | 'topItemCount'> { - /** - * 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 /** * Use the `components` property for advanced customization of the elements rendered by the table. */ @@ -219,6 +211,24 @@ export interface TableVirtuosoProps extends Omit, 'com atBottomThreshold?: number } +export interface GroupedTableVirtuosoProps extends Omit, '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 +} + export interface TableVirtuosoHandle { scrollIntoView(location: number | FlatScrollIntoViewLocation): void scrollToIndex(location: number | FlatIndexLocationWithAlign): void From 8e595b4288f5b3354404ec7d35c625fe99593d40 Mon Sep 17 00:00:00 2001 From: malo Date: Tue, 10 Jan 2023 10:47:35 -0500 Subject: [PATCH 3/3] docs: add GroupedVirtuosoTable doc --- examples/group-table.tsx | 140 ++++++---------------- site/docs/grouped-table.md | 56 +++++++++ site/docs/table-virtuoso-api-reference.md | 13 +- site/sidebars.js | 2 +- site/yarn.lock | 17 ++- 5 files changed, 121 insertions(+), 107 deletions(-) create mode 100644 site/docs/grouped-table.md diff --git a/examples/group-table.tsx b/examples/group-table.tsx index 9ef7cc4d8..ca241c64e 100644 --- a/examples/group-table.tsx +++ b/examples/group-table.tsx @@ -1,107 +1,43 @@ -import * as React from 'react' -import { GroupedTableVirtuoso, GroupedTableVirtuosoHandle } from '../src/' +import React, { useMemo } from 'react' +import { GroupedTableVirtuoso } from '../src/' export default function App() { - const ref = React.useRef(null) - return ( - <> - { - return ( - - - Empty - - - ) - }, - }} - groupCounts={Array.from({ length: 100 }).fill(10) as number[]} - style={{ height: 700 }} - fixedHeaderContent={() => { - return ( - - - TH 1 - - - TH meh - - - ) - }} - fixedFooterContent={() => { - return ( - - - Footer TH 1 - - - Footer TH meh - - - ) - }} - itemContent={(index) => { - return ( - <> - {index}Cell 1 - Cell 2 - - ) - }} - groupContent={(index) => { - return ( - <> - Group {index} - Meh - - ) - }} - /> - - + const groupCounts = useMemo(() => { + return Array(1000).fill(10) as number[] + }, []) - - -

Buttons should align 900 correctly

- + return ( + { + return ( + + + Item index + + + Greetings + + + ) + }} + itemContent={(index) => { + return ( + <> + {index} + Hello + + ) + }} + groupContent={(index) => { + return ( + <> + Group {index} + + + ) + }} + /> ) } diff --git a/site/docs/grouped-table.md b/site/docs/grouped-table.md new file mode 100644 index 000000000..360ca4515 --- /dev/null +++ b/site/docs/grouped-table.md @@ -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 ( + { + return ( + + + Item index + + + Greetings + + + ) + }} + itemContent={(index) => { + return ( + <> + {index} + Hello + + ) + }} + groupContent={(index) => { + return ( + <> + Group {index} + + + ) + }} + /> + ) +} +``` diff --git a/site/docs/table-virtuoso-api-reference.md b/site/docs/table-virtuoso-api-reference.md index d7f8366ba..103c9358f 100644 --- a/site/docs/table-virtuoso-api-reference.md +++ b/site/docs/table-virtuoso-api-reference.md @@ -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`. @@ -21,12 +22,18 @@ const ref = useRef(null) ``` -## Table Virtuoso Properties +## TableVirtuoso Properties
+## GroupedTableVirtuoso Properties + +
+ +
+ ## Methods
diff --git a/site/sidebars.js b/site/sidebars.js index f29e601c3..9accdb122 100644 --- a/site/sidebars.js +++ b/site/sidebars.js @@ -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: [ diff --git a/site/yarn.lock b/site/yarn.lock index 6276dc33f..6905443e0 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -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" @@ -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"