diff --git a/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx b/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx index 8366cd44ccd..0724dd0b1ce 100644 --- a/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx +++ b/web/pgadmin/static/js/SchemaView/DataGridView/grid.jsx @@ -122,12 +122,33 @@ export default function DataGridView({ ) ).includes(true); + // Virtualising a small grid buys nothing (there's no offscreen window to + // skip rendering) but still pays for measureElement's per-row + // getBoundingClientRect on every mount/remeasure. That remeasure is + // exactly what fires when a dialog tab holding the grid is hidden via + // `display: none` and then shown again, since the scroll viewport + // momentarily measures 0 and the virtualizer's ResizeObserver treats + // that as a real resize. Below the threshold we skip virtualisation + // entirely and render every row in normal document flow, so showing a + // hidden tab is a pure CSS toggle again. + // + // The threshold scales with visible column count rather than being a + // flat row count, since render cost tracks total cells (rows * cols), + // not rows alone: formula and bounds from VIBVEL47's PR #10146. + const visibleColCount = table.getVisibleLeafColumns().length; + const virtualiseThreshold = viewHelperProps.virtualiseThreshold ?? + (visibleColCount > 0 + ? Math.min(400, Math.max(25, Math.round(700 / visibleColCount))) + : 100); + const shouldVirtualise = rows.length > virtualiseThreshold; + const virtualizer = useVirtualizer({ count: rows.length, getScrollElement: () => tableEleRef.current, estimateSize: () => 50, measureElement: - typeof window !== 'undefined' && + shouldVirtualise && + typeof window !== 'undefined' && navigator.userAgent.indexOf('Firefox') === -1 ? element => element?.getBoundingClientRect().height : undefined, @@ -152,22 +173,29 @@ export default function DataGridView({ ref={tableEleRef} table={table} data-test="data-grid-view" tableClassName='DataGridView-table'> - + { - virtualizer.getVirtualItems().map((virtualRow) => { + ( + shouldVirtualise + ? virtualizer.getVirtualItems() + : rows.map((_row, index) => ({index, start: 0})) + ).map((virtualRow) => { const row = rows[virtualRow.index]; return ( virtualizer.measureElement(node)} - style={{ - // This should always be a `style` as it changes on - // scroll. - transform: `translateY(${virtualRow.start}px)`, - }} + ref={shouldVirtualise ? node => virtualizer.measureElement(node) : undefined} + className={shouldVirtualise ? undefined : 'pgrt-row--static'} + style={ + shouldVirtualise ? { + // This should always be a `style` as it changes + // on scroll. + transform: `translateY(${virtualRow.start}px)`, + } : undefined + } > ({ position: 'absolute', width: '100%', + // Opted out of the virtualizer's absolute positioning for grids + // small enough that virtualisation isn't used. Keeps the row in + // normal document flow so a hidden/shown dialog tab is a pure CSS + // toggle instead of triggering a virtualizer remeasure. + '&.pgrt-row--static': { + position: 'static', + }, + '& .pgrt-row-content': { display: 'flex', minHeight: 0, diff --git a/web/regression/javascript/SchemaView/SchemaDialogView.spec.js b/web/regression/javascript/SchemaView/SchemaDialogView.spec.js index 9c4c944138e..19d0f0be89a 100644 --- a/web/regression/javascript/SchemaView/SchemaDialogView.spec.js +++ b/web/regression/javascript/SchemaView/SchemaDialogView.spec.js @@ -172,6 +172,44 @@ describe('SchemaView', ()=>{ await user.type(ctrl.container.querySelectorAll('[name="field5"]')[1], 'rval51'); expect(ctrl.container.querySelector('[data-test="notifier-message"]')).toHaveTextContent('Field5 in FieldColl must be unique.'); }); + + it('does not virtualise a small grid, rendering rows in static flow', async ()=>{ + await simulateValidData(); + + const dataRows = ctrl.container.querySelectorAll('[data-test="data-table-row"]'); + expect(dataRows.length).toBe(2); + + // Every row should be fully mounted and opted out of the + // virtualizer's absolute positioning, so a hidden dialog tab is a + // pure CSS toggle rather than something the virtualizer has to + // remeasure when the tab is shown again. + const pgrtRows = ctrl.container.querySelectorAll('.pgrt-row'); + expect(pgrtRows.length).toBe(2); + pgrtRows.forEach((rowEl)=>{ + expect(rowEl.classList.contains('pgrt-row--static')).toBe(true); + expect(rowEl.style.transform).toBe(''); + }); + }); + + it('virtualises a large grid, mounting only a window of rows', async ()=>{ + // The default threshold scales with visible column count (capped at + // 400), so this needs to comfortably clear that cap regardless of + // how many columns FieldColl renders. + const manyRows = Array.from({length: 450}, (_, i)=>( + {field3: i, field4: 'field4val', field5: `field5val${i}`} + )); + + await ctrlMount({ + getInitData: ()=>Promise.resolve({fieldcoll: manyRows}), + }); + + const pgrtRows = ctrl.container.querySelectorAll('.pgrt-row'); + expect(pgrtRows.length).toBeGreaterThan(0); + expect(pgrtRows.length).toBeLessThan(manyRows.length); + pgrtRows.forEach((rowEl)=>{ + expect(rowEl.classList.contains('pgrt-row--static')).toBe(false); + }); + }); }); describe('SQL tab', ()=>{