diff --git a/.changeset/lucky-suits-decide.md b/.changeset/lucky-suits-decide.md new file mode 100644 index 00000000000..7d5121eb94b --- /dev/null +++ b/.changeset/lucky-suits-decide.md @@ -0,0 +1,5 @@ +--- +'@itwin/itwinui-react': patch +--- + +Fixed broken `subComponent` when enabling virtualization in `Table`. diff --git a/.changeset/short-planets-listen.md b/.changeset/short-planets-listen.md new file mode 100644 index 00000000000..f52ab2c35ab --- /dev/null +++ b/.changeset/short-planets-listen.md @@ -0,0 +1,5 @@ +--- +'@itwin/itwinui-react': patch +--- + +`Table`'s animation to show and hide its `subComponent` is now removed. diff --git a/packages/itwinui-react/src/core/Table/Table.test.tsx b/packages/itwinui-react/src/core/Table/Table.test.tsx index ef0c12c2f65..6b9e49882e0 100644 --- a/packages/itwinui-react/src/core/Table/Table.test.tsx +++ b/packages/itwinui-react/src/core/Table/Table.test.tsx @@ -2040,7 +2040,7 @@ it('should render sub-rows and handle expansions', async () => { expect(onExpand).toHaveBeenNthCalledWith(1, [data[0]], expect.any(Object)); expect(onExpand).toHaveBeenNthCalledWith( 2, - [data[0], data[0].subRows[1]], + [data[0], data[1]], expect.any(Object), ); expect(onExpand).toHaveBeenNthCalledWith( diff --git a/packages/itwinui-react/src/core/Table/Table.tsx b/packages/itwinui-react/src/core/Table/Table.tsx index f34e64b0ddd..8bbe3feb639 100644 --- a/packages/itwinui-react/src/core/Table/Table.tsx +++ b/packages/itwinui-react/src/core/Table/Table.tsx @@ -38,7 +38,6 @@ import { useMergedRefs, useLatestRef, useVirtualScroll, - WithCSSTransition, } from '../../utils/index.js'; import type { CommonProps } from '../../utils/index.js'; import { TableColumnsContext } from './utils.js'; @@ -65,7 +64,7 @@ import { onTableResizeStart, } from './actionHandlers/index.js'; import { SELECTION_CELL_ID } from './columns/index.js'; -import { Virtualizer, type VirtualItem } from '@tanstack/react-virtual'; +import type { VirtualItem, Virtualizer } from '@tanstack/react-virtual'; import { ColumnHeader } from './ColumnHeader.js'; import { TableExpandableContentMemoized } from './TableExpandableContentMemoized.js'; @@ -580,6 +579,20 @@ export const Table = < ); }, [data, getSubRows]); + const [rowHasParent] = React.useState(() => new WeakSet()); + + const getSubRowsWithSubComponents = React.useCallback( + (originalRow: T) => { + if (!rowHasParent.has(originalRow)) { + rowHasParent.add(originalRow); + return [originalRow]; + } + + return (originalRow.subRows as T[]) || []; + }, + [rowHasParent], + ); + const instance = useTable( { manualPagination: !paginatorRenderer, // Prevents from paginating rows in regular table without pagination @@ -592,7 +605,7 @@ export const Table = < filterTypes, selectSubRows, data, - getSubRows, + getSubRows: subComponent ? getSubRowsWithSubComponents : getSubRows, initialState: { pageSize, ...props.initialState }, columnResizeMode, }, @@ -836,8 +849,9 @@ export const Table = < ) => { const row = page[index]; prepareRow(row); - return ( - <> + + if (row.subRows.length > 0 || !subComponent) { + return ( - {subComponent && ( - - - {subComponent(row)} - - - )} - - ); + ); + } else { + return ( + + {subComponent(row)} + + ); + } }, [ page, prepareRow, rowProps, + onRowInViewportRef, + onBottomReachedRef, intersectionMargin, state, onRowClickHandler, @@ -888,8 +908,6 @@ export const Table = < enableVirtualization, tableRowRef, density, - onBottomReachedRef, - onRowInViewportRef, ], ); diff --git a/packages/itwinui-react/src/core/Table/TableExpandableContentMemoized.tsx b/packages/itwinui-react/src/core/Table/TableExpandableContentMemoized.tsx index dfb42e75015..ebe72a749db 100644 --- a/packages/itwinui-react/src/core/Table/TableExpandableContentMemoized.tsx +++ b/packages/itwinui-react/src/core/Table/TableExpandableContentMemoized.tsx @@ -6,23 +6,31 @@ import cx from 'classnames'; import * as React from 'react'; import { Box } from '../../utils/index.js'; import type { PolymorphicForwardRefComponent } from '../../utils/index.js'; +import type { VirtualItem } from '@tanstack/react-virtual'; type TableExpandableContentProps = { children: React.ReactNode; + virtualItem?: VirtualItem; isDisabled?: boolean; }; const TableExpandableContent = React.forwardRef((props, ref) => { - const { children, className, style, isDisabled, ...rest } = props; + const { children, className, style, isDisabled, virtualItem, ...rest } = + props; return ( diff --git a/testing/e2e/app/routes/Table/route.tsx b/testing/e2e/app/routes/Table/route.tsx index 92f1781d402..6678c491104 100644 --- a/testing/e2e/app/routes/Table/route.tsx +++ b/testing/e2e/app/routes/Table/route.tsx @@ -70,6 +70,7 @@ function EverythingElse() { const oneRow = searchParams.get('oneRow') === 'true'; const stateReducer = searchParams.get('stateReducer') === 'true'; const scrollRow = Number(searchParams.get('scrollRow')); + const hasSubComponent = searchParams.get('hasSubComponent') === 'true'; const virtualizedData = React.useMemo(() => { const size = oneRow ? 1 : 100000; @@ -162,6 +163,13 @@ function EverythingElse() { } : undefined } + subComponent={ + hasSubComponent + ? (row) => ( +
{`Expanded component, name: ${row.original.name}`}
+ ) + : undefined + } /> ); diff --git a/testing/e2e/app/routes/Table/spec.ts b/testing/e2e/app/routes/Table/spec.ts index 1c36083175e..5071b33f14c 100644 --- a/testing/e2e/app/routes/Table/spec.ts +++ b/testing/e2e/app/routes/Table/spec.ts @@ -510,6 +510,50 @@ test.describe('Virtual Scroll Tests', () => { const rows = page.getByRole('rowgroup').getByRole('row'); expect((await rows.all()).length).toBe(1); }); + + test('virtualized table should allow expandable contents', async ({ + page, + }) => { + await page.goto( + '/Table?virtualization=true&scroll=true&scrollRow=50&hasSubComponent=true', + { + waitUntil: 'networkidle', + }, + ); //Need to wait until the virtual rows are able to be rendered for the tests to work. + + const rows = page.getByRole('rowgroup').getByRole('row'); + await expect(rows.nth(1)).toContainText('Name50'); + await expect(rows.nth(4)).toContainText('Name53'); + + const row50ExpanderContent = page.getByText( + 'Expanded component, name: Name50', + ); + const row53ExpanderContent = page.getByText( + 'Expanded component, name: Name53', + ); + + const expanderButtonRow50Cell = rows + .nth(1) + .getByRole('cell') + .nth(0) + .getByRole('button'); + const expanderButtonRow53Cell = rows + .nth(4) + .getByRole('cell') + .nth(0) + .getByRole('button'); + + await expanderButtonRow50Cell.click(); + await expanderButtonRow53Cell.click(); + await expect(row50ExpanderContent).toBeInViewport(); + await expect(row53ExpanderContent).toBeInViewport(); + + // Collapse the expanded content + await expanderButtonRow50Cell.click(); + await expect(row50ExpanderContent).not.toBeInViewport(); + await expanderButtonRow53Cell.click(); + await expect(row53ExpanderContent).not.toBeInViewport(); + }); }); test.describe('Table filters', () => { diff --git a/testing/e2e/playwright.config.ts b/testing/e2e/playwright.config.ts index 0d7f70d5794..e5f7d988727 100644 --- a/testing/e2e/playwright.config.ts +++ b/testing/e2e/playwright.config.ts @@ -5,7 +5,6 @@ import { defineConfig, devices } from '@playwright/test'; */ export default defineConfig({ testMatch: 'app/**/*spec.ts', // Look for test files anywhere in the app - fullyParallel: true, // Run tests in files in parallel forbidOnly: !!process.env.CI, // Fail CI if you accidentally left test.only retries: process.env.CI ? 2 : 0, // Retry on CI only