From 309ea68f8d4a8dea0f32a89cdd5e7e33aa81a37f Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Fri, 6 Feb 2026 13:31:48 +0000 Subject: [PATCH 1/3] Add --- src/DataGrid.tsx | 248 +++++++++++++++++---------------- src/hooks/useGridDimensions.ts | 41 ++++-- 2 files changed, 158 insertions(+), 131 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index b8532c13fd..ab098ff5d8 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -1,4 +1,5 @@ import { + Activity, useCallback, useImperativeHandle, useLayoutEffect, @@ -353,7 +354,8 @@ export function DataGrid(props: DataGridPr [columnWidths] ); - const [gridRef, gridWidth, gridHeight, horizontalScrollbarHeight] = useGridDimensions(); + const [gridRef, activityMode, gridWidth, gridHeight, horizontalScrollbarHeight] = + useGridDimensions(); const { columns, colSpanColumns, @@ -1229,134 +1231,136 @@ export function DataGrid(props: DataGridPr data-testid={testId} data-cy={dataCy} > - - - - {Array.from({ length: groupedColumnHeaderRowsCount }, (_, index) => ( - + + + + {Array.from({ length: groupedColumnHeaderRowsCount }, (_, index) => ( + + ))} + - ))} - - - - {rows.length === 0 && noRowsFallback ? ( - noRowsFallback - ) : ( - <> - {topSummaryRows?.map((row, rowIdx) => { - const gridRowStart = headerRowsCount + 1 + rowIdx; - const summaryRowIdx = mainHeaderRowIdx + 1 + rowIdx; - const isSummaryRowSelected = selectedPosition.rowIdx === summaryRowIdx; - const top = headerRowsHeight + summaryRowHeight * rowIdx; - - return ( - - ); - })} - - {getViewportRows()} - - {bottomSummaryRows?.map((row, rowIdx) => { - const gridRowStart = headerAndTopSummaryRowsCount + rows.length + rowIdx + 1; - const summaryRowIdx = rows.length + rowIdx; - const isSummaryRowSelected = selectedPosition.rowIdx === summaryRowIdx; - const top = - clientHeight > totalRowHeight - ? gridHeight - summaryRowHeight * (bottomSummaryRows.length - rowIdx) - : undefined; - const bottom = - top === undefined - ? summaryRowHeight * (bottomSummaryRows.length - 1 - rowIdx) - : undefined; - - return ( - - ); + + + {rows.length === 0 && noRowsFallback ? ( + noRowsFallback + ) : ( + <> + {topSummaryRows?.map((row, rowIdx) => { + const gridRowStart = headerRowsCount + 1 + rowIdx; + const summaryRowIdx = mainHeaderRowIdx + 1 + rowIdx; + const isSummaryRowSelected = selectedPosition.rowIdx === summaryRowIdx; + const top = headerRowsHeight + summaryRowHeight * rowIdx; + + return ( + + ); + })} + + {getViewportRows()} + + {bottomSummaryRows?.map((row, rowIdx) => { + const gridRowStart = headerAndTopSummaryRowsCount + rows.length + rowIdx + 1; + const summaryRowIdx = rows.length + rowIdx; + const isSummaryRowSelected = selectedPosition.rowIdx === summaryRowIdx; + const top = + clientHeight > totalRowHeight + ? gridHeight - summaryRowHeight * (bottomSummaryRows.length - rowIdx) + : undefined; + const bottom = + top === undefined + ? summaryRowHeight * (bottomSummaryRows.length - 1 - rowIdx) + : undefined; + + return ( + + ); + })} + + )} + + + {getDragHandle()} + + {/* render empty cells that span only 1 column so we can safely measure column widths, regardless of colSpan */} + {renderMeasuringCells(viewportColumns)} + + {/* extra div is needed for row navigation in a treegrid */} + {isTreeGrid && ( +
+ style={{ + gridRowStart: selectedPosition.rowIdx + headerAndTopSummaryRowsCount + 1 + }} + /> )} - - - {getDragHandle()} - - {/* render empty cells that span only 1 column so we can safely measure column widths, regardless of colSpan */} - {renderMeasuringCells(viewportColumns)} - - {/* extra div is needed for row navigation in a treegrid */} - {isTreeGrid && ( -
- )} - {scrollToPosition !== null && ( - - )} + {scrollToPosition !== null && ( + + )} +
); } diff --git a/src/hooks/useGridDimensions.ts b/src/hooks/useGridDimensions.ts index ddd2ac67e3..d5ddab910b 100644 --- a/src/hooks/useGridDimensions.ts +++ b/src/hooks/useGridDimensions.ts @@ -1,4 +1,4 @@ -import { useLayoutEffect, useRef, useState } from 'react'; +import { useLayoutEffect, useRef, useState, type ActivityProps } from 'react'; import { flushSync } from 'react-dom'; export function useGridDimensions() { @@ -6,16 +6,22 @@ export function useGridDimensions() { const [inlineSize, setInlineSize] = useState(1); const [blockSize, setBlockSize] = useState(1); const [horizontalScrollbarHeight, setHorizontalScrollbarHeight] = useState(0); + const [isMeasured, setIsMeasured] = useState(false); + const [isVisible, setIsVisible] = useState(true); + const [isDocumentVisible, setIsDocumentVisible] = useState(false); + const activityMode: ActivityProps['mode'] = + isMeasured && isVisible && isDocumentVisible ? 'visible' : 'hidden'; useLayoutEffect(() => { - const { ResizeObserver } = window; + const { ResizeObserver, IntersectionObserver } = window; - // don't break in Node.js (SSR), jsdom, and browsers that don't support ResizeObserver + // don't break in environments like JSDOM that do not support observers // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (ResizeObserver == null) return; + if (ResizeObserver == null || IntersectionObserver == null) return; - const { clientWidth, clientHeight, offsetWidth, offsetHeight } = gridRef.current!; - const { width, height } = gridRef.current!.getBoundingClientRect(); + const grid = gridRef.current!; + const { clientWidth, clientHeight, offsetWidth, offsetHeight } = grid; + const { width, height } = grid.getBoundingClientRect(); const initialHorizontalScrollbarHeight = offsetHeight - clientHeight; const initialWidth = width - offsetWidth + clientWidth; const initialHeight = height - initialHorizontalScrollbarHeight; @@ -23,10 +29,11 @@ export function useGridDimensions() { setInlineSize(initialWidth); setBlockSize(initialHeight); setHorizontalScrollbarHeight(initialHorizontalScrollbarHeight); + setIsMeasured(true); const resizeObserver = new ResizeObserver((entries) => { const size = entries[0].contentBoxSize[0]; - const { clientHeight, offsetHeight } = gridRef.current!; + const { clientHeight, offsetHeight } = grid; // we use flushSync here to avoid flashing scrollbars flushSync(() => { @@ -35,12 +42,28 @@ export function useGridDimensions() { setHorizontalScrollbarHeight(offsetHeight - clientHeight); }); }); - resizeObserver.observe(gridRef.current!); + + const intersectionObserver = new IntersectionObserver((entries) => { + flushSync(() => { + setIsVisible(entries[0].isIntersecting); + }); + }); + + function onVisibilityChange() { + setIsDocumentVisible(!document.hidden); + } + + resizeObserver.observe(grid); + intersectionObserver.observe(grid); + document.addEventListener('visibilitychange', onVisibilityChange); + onVisibilityChange(); return () => { resizeObserver.disconnect(); + intersectionObserver.disconnect(); + document.removeEventListener('visibilitychange', onVisibilityChange); }; }, []); - return [gridRef, inlineSize, blockSize, horizontalScrollbarHeight] as const; + return [gridRef, activityMode, inlineSize, blockSize, horizontalScrollbarHeight] as const; } From c862990132a2e45cc00f22a4a7688d852ccd410c Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Fri, 6 Feb 2026 13:34:57 +0000 Subject: [PATCH 2/3] NonNullable --- src/hooks/useGridDimensions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useGridDimensions.ts b/src/hooks/useGridDimensions.ts index d5ddab910b..f8390ab97f 100644 --- a/src/hooks/useGridDimensions.ts +++ b/src/hooks/useGridDimensions.ts @@ -9,7 +9,7 @@ export function useGridDimensions() { const [isMeasured, setIsMeasured] = useState(false); const [isVisible, setIsVisible] = useState(true); const [isDocumentVisible, setIsDocumentVisible] = useState(false); - const activityMode: ActivityProps['mode'] = + const activityMode: NonNullable = isMeasured && isVisible && isDocumentVisible ? 'visible' : 'hidden'; useLayoutEffect(() => { From ff55b553bb83fdf3f0f23b0c5e12f250b4cbc97e Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Fri, 6 Feb 2026 13:38:46 +0000 Subject: [PATCH 3/3] increase actionTimeout --- vite.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index d4652ff0fa..e81d6ee58c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -69,7 +69,7 @@ function getInstances(): BrowserInstanceOption[] { { browser: 'chromium', provider: playwright({ - actionTimeout: 1000, + actionTimeout: 2000, contextOptions: { viewport }, @@ -81,7 +81,7 @@ function getInstances(): BrowserInstanceOption[] { { browser: 'firefox', provider: playwright({ - actionTimeout: 1000, + actionTimeout: 2000, contextOptions: { viewport }