diff --git a/README.md b/README.md index 7ad7807480..1a19a8f4ae 100644 --- a/README.md +++ b/README.md @@ -881,7 +881,7 @@ function MyGrid() { Function to generate unique IDs for group rows. If not provided, a default implementation is used that concatenates parent and group keys with `__`. -###### `rowHeight?: Maybe) => number)>` +###### `rowHeight?: Maybe) => number)>` **Note:** Unlike `DataGrid`, the `rowHeight` function receives [`RowHeightArgs`](#rowheightargstrow) which includes a `type` property to distinguish between regular rows and group rows: diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 5c750a00fc..1f38fe0669 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -1,10 +1,13 @@ -import { useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react'; import type { Key, KeyboardEvent } from 'react'; +import { useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react'; import { flushSync } from 'react-dom'; import { + type ActivePosition, HeaderRowSelectionChangeContext, HeaderRowSelectionContext, + type HeaderRowSelectionContextValue, + type PartialPosition, RowSelectionChangeContext, useActivePosition, useCalculatedColumns, @@ -14,10 +17,7 @@ import { useScrollState, useScrollToPosition, useViewportColumns, - useViewportRows, - type ActivePosition, - type HeaderRowSelectionContextValue, - type PartialPosition + useViewportRows } from './hooks'; import { assertIsValidKeyGetter, @@ -45,7 +45,6 @@ import type { CellMouseEventHandler, CellNavigationMode, CellPasteArgs, - PositionChangeArgs, Column, ColumnOrColumnGroup, ColumnWidths, @@ -53,11 +52,12 @@ import type { FillEvent, Maybe, Position, + PositionChangeArgs, Renderers, RowsChangeData, - SetActivePositionOptions, SelectHeaderRowEvent, SelectRowEvent, + SetActivePositionOptions, SortColumn } from './types'; import { defaultRenderCell } from './Cell'; @@ -138,7 +138,7 @@ export interface DataGridProps extends Sha * Height of each row in pixels * @default 35 */ - rowHeight?: Maybe) => number)>; + rowHeight?: Maybe) => number)>; /** * Height of the header row in pixels * @default 35 @@ -303,9 +303,13 @@ export function DataGrid(props: DataGridPr const renderCheckbox = renderers?.renderCheckbox ?? defaultRenderers?.renderCheckbox ?? defaultRenderCheckbox; const noRowsFallback = renderers?.noRowsFallback ?? defaultRenderers?.noRowsFallback; - const enableVirtualization = rawEnableVirtualization ?? true; + const enableVirtualization = rawEnableVirtualization ?? typeof rawRowHeight !== 'string'; const direction = rawDirection ?? 'ltr'; + if (enableVirtualization && typeof rowHeight === 'string') { + throw new Error('`rowHeight` cannot be a string when `enableVirtualization` is true.'); + } + /** * ref */ @@ -413,7 +417,9 @@ export function DataGrid(props: DataGridPr maxRowIdx, setDraggedOverRowIdx }); - const { setScrollToPosition, scrollToPositionElement } = useScrollToPosition({ gridRef }); + const { setScrollToPosition, scrollToPositionElement } = useScrollToPosition({ + gridRef + }); const defaultGridComponents = useMemo( () => ({ @@ -457,10 +463,16 @@ export function DataGrid(props: DataGridPr findRowIdx } = useViewportRows({ rows, - rowHeight, clientHeight, scrollTop, - enableVirtualization + enableVirtualization, + ...(typeof rowHeight === 'string' + ? { + rowHeight, + gridRef, + gridHeight + } + : { rowHeight }) }); const { @@ -690,7 +702,11 @@ export function DataGrid(props: DataGridPr if (isSelectable && shiftKey && key === ' ') { assertIsValidKeyGetter(rowKeyGetter); const rowKey = rowKeyGetter(row); - selectRow({ row, checked: !selectedRows.has(rowKey), isShiftClick: false }); + selectRow({ + row, + checked: !selectedRows.has(rowKey), + isShiftClick: false + }); // prevent scrolling event.preventDefault(); return; @@ -776,7 +792,11 @@ export function DataGrid(props: DataGridPr const indexes: number[] = []; for (let i = startRowIdx; i < endRowIdx; i++) { if (isCellEditable({ rowIdx: i, idx })) { - const updatedRow = onFill!({ columnKey: column.key, sourceRow, targetRow: rows[i] }); + const updatedRow = onFill!({ + columnKey: column.key, + sourceRow, + targetRow: rows[i] + }); if (updatedRow !== rows[i]) { updatedRows[i] = updatedRow; indexes.push(i); @@ -1048,7 +1068,11 @@ export function DataGrid(props: DataGridPr }); function closeEditor(shouldFocus: boolean) { - const newPosition: ActivePosition = { idx: activePosition.idx, rowIdx, mode: 'ACTIVE' }; + const newPosition: ActivePosition = { + idx: activePosition.idx, + rowIdx, + mode: 'ACTIVE' + }; setActivePosition(newPosition); if (shouldFocus) { setPositionToFocus(newPosition); diff --git a/src/hooks/useViewportRows.ts b/src/hooks/useViewportRows.ts index 13b8502302..2f4ecfe399 100644 --- a/src/hooks/useViewportRows.ts +++ b/src/hooks/useViewportRows.ts @@ -1,22 +1,37 @@ -import { useMemo } from 'react'; +import { useMemo, type RefObject } from 'react'; import { floor, max, min } from '../utils'; -interface ViewportRowsArgs { +interface ViewportRowsBaseArgs { rows: readonly R[]; - rowHeight: number | ((row: R) => number); clientHeight: number; scrollTop: number; enableVirtualization: boolean; + gridHeight?: number; +} + +interface ViewportRowsArgsStringHeight { + rowHeight: string; + gridRef: RefObject; + gridHeight: number; +} + +interface ViewportRowsArgsRegularHeight { + rowHeight: number | ((row: R) => number); } +type ViewportRowsArgs = ViewportRowsBaseArgs & + (ViewportRowsArgsStringHeight | ViewportRowsArgsRegularHeight); + export function useViewportRows({ rows, rowHeight, clientHeight, scrollTop, - enableVirtualization + enableVirtualization, + ...rest }: ViewportRowsArgs) { + const { gridRef, gridHeight } = rest as Partial; const { totalRowHeight, gridTemplateRows, getRowTop, getRowHeight, findRowIdx } = useMemo(() => { if (typeof rowHeight === 'number') { return { @@ -28,6 +43,68 @@ export function useViewportRows({ }; } + if (typeof rowHeight === 'string') { + if (!gridHeight) { + throw new Error( + 'props.gridHeight is required when rowHeight is a string. This is needed to calculate the total height of the rows.' + ); + } + + const getRowElementFirstCell = (element: Element, rowIdx: number): Element | null => { + const nth = element.querySelector('.rdg-header-row') ? rowIdx + 2 : rowIdx + 1; + return element.querySelector(`[role="row"][aria-rowindex="${nth}"] > [role="gridcell"]`); + }; + + const getRowYTop = (element: Element, rowIdx: number) => { + const cell = getRowElementFirstCell(element, rowIdx); + if (!cell) return -1; + return cell.getBoundingClientRect().top + element.scrollTop; + }; + + return { + totalRowHeight: gridHeight, + gridTemplateRows: ` repeat(${rows.length}, ${rowHeight})`, + getRowTop(rowIdx: number) { + const element = gridRef?.current; + if (!element) return -1; + const cell = getRowElementFirstCell(element, rowIdx); + if (!cell) return -1; + return cell.getBoundingClientRect().top + element.scrollTop; + }, + getRowHeight(rowIdx: number) { + const element = gridRef?.current; + if (!element) return -1; + const cell = getRowElementFirstCell(element, rowIdx); + if (!cell) return -1; + return cell.clientHeight; + }, + findRowIdx(offset: number) { + const element = gridRef?.current; + if (!element) return -1; + let start = 0; + let end = rows.length - 1; + + while (start <= end) { + const middle = start + floor((end - start) / 2); + const currentScrollTop = getRowYTop(element, middle); + const prevScrollTop = getRowYTop(element, middle - 1); + + if (currentScrollTop >= offset && prevScrollTop < offset) return middle; + + if (currentScrollTop < offset) { + start = middle + 1; + } else if (currentScrollTop > offset) { + end = middle - 1; + } + + if (start > end) return end; + } + + return -1; + } + }; + } + // Calcule the height of all the rows upfront. This can cause performance issues // and we can consider using a similar approach as react-window // https://github.com/bvaughn/react-window/blob/b0a470cc264e9100afcaa1b78ed59d88f7914ad4/src/VariableSizeList.js#L68 @@ -102,15 +179,21 @@ export function useViewportRows({ return 0; } }; - }, [rowHeight, rows]); + }, [rowHeight, rows, gridRef, gridHeight]); let rowOverscanStartIdx = 0; let rowOverscanEndIdx = rows.length - 1; if (enableVirtualization) { const overscanThreshold = 4; + // `findRowIdx` only reads `gridRef.current` in the string-rowHeight branch, + // which is unreachable here because `enableVirtualization` is forced off when + // `rowHeight` is a string (see DataGrid.tsx). + /* eslint-disable react-hooks/refs */ const rowVisibleStartIdx = findRowIdx(scrollTop); const rowVisibleEndIdx = findRowIdx(scrollTop + clientHeight); + /* eslint-enable react-hooks/refs */ + rowOverscanStartIdx = max(0, rowVisibleStartIdx - overscanThreshold); rowOverscanEndIdx = min(rows.length - 1, rowVisibleEndIdx + overscanThreshold); } diff --git a/test/browser/rowHeight.test.ts b/test/browser/rowHeight.test.ts deleted file mode 100644 index 5ed9647509..0000000000 --- a/test/browser/rowHeight.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { page, userEvent } from 'vitest/browser'; - -import type { Column, DataGridProps } from '../../src'; -import { safeTab, setup, testRowCount } from './utils'; - -const grid = page.getGrid(); - -type Row = number; - -function setupGrid(rowHeight: DataGridProps['rowHeight']) { - const columns: Column[] = []; - const rows: readonly Row[] = Array.from({ length: 50 }, (_, i) => i); - - for (let i = 0; i < 5; i++) { - const key = String(i); - columns.push({ - key, - name: key, - width: 80 - }); - } - return setup({ columns, rows, rowHeight }); -} - -async function expectGridRows(rowHeightFn: (row: number) => number, expected: string) { - await setupGrid(rowHeightFn); - - expect(grid.element().style.gridTemplateRows).toBe(expected); -} - -test('rowHeight is number', async () => { - await setupGrid(40); - - await expect.element(grid).toHaveStyle({ - gridTemplateRows: - '40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px' - }); - await testRowCount(30); - await safeTab(); - await expect.element(grid).toHaveProperty('scrollTop', 0); - await userEvent.keyboard('{Control>}{end}'); - const gridEl = grid.element(); - expect(gridEl.scrollTop + gridEl.clientHeight).toBe(gridEl.scrollHeight); -}); - -test('rowHeight is function', async () => { - await setupGrid((row) => [40, 60, 80][row % 3]); - - await expect.element(grid).toHaveStyle({ - gridTemplateRows: - '35px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px' - }); - await testRowCount(22); - - await safeTab(); - await expect.element(grid).toHaveProperty('scrollTop', 0); - await userEvent.keyboard('{Control>}{end}'); - const gridEl = grid.element(); - expect(gridEl.scrollTop + gridEl.clientHeight).toBe(gridEl.scrollHeight); -}); - -test('rowHeight with repeat pattern - multiple identical heights', async () => { - await expectGridRows(() => 40, 'repeat(1, 35px) repeat(50, 40px)'); -}); - -test('rowHeight with mixed heights - one unique in middle', async () => { - await expectGridRows( - (row) => (row === 25 ? 40 : 50), - 'repeat(1, 35px) repeat(25, 50px) 40px repeat(24, 50px)' - ); -}); - -test('rowHeight with unique heights', async () => { - await expectGridRows( - (row) => row + 1, - 'repeat(1, 35px) 1px 2px 3px 4px 5px 6px 7px 8px 9px 10px 11px 12px 13px 14px 15px 16px 17px 18px 19px 20px 21px 22px 23px 24px 25px 26px 27px 28px 29px 30px 31px 32px 33px 34px 35px 36px 37px 38px 39px 40px 41px 42px 43px 44px 45px 46px 47px 48px 49px 50px' - ); -}); - -test('rowHeight with unique first and unique last heights', async () => { - await expectGridRows((row) => { - if (row === 0) { - return 10; - } - - if (row === 49) { - return 20; - } - - return 50; - }, 'repeat(1, 35px) 10px repeat(48, 50px) 20px'); -}); - -test('rowHeight with unique last height', async () => { - await expectGridRows((row) => { - return row === 49 ? 50 : 20; - }, 'repeat(1, 35px) repeat(49, 20px) 50px'); -}); - -test('rowHeight with unique first height', async () => { - await expectGridRows((row) => { - return row === 0 ? 45 : 50; - }, 'repeat(1, 35px) 45px repeat(49, 50px)'); -}); diff --git a/test/browser/rowHeight.test.tsx b/test/browser/rowHeight.test.tsx new file mode 100644 index 0000000000..88da865a37 --- /dev/null +++ b/test/browser/rowHeight.test.tsx @@ -0,0 +1,220 @@ +import { page, userEvent } from 'vitest/browser'; + +import type { Column, DataGridProps } from '../../src'; +import { safeTab, setup, testRowCount } from './utils'; + +const grid = page.getGrid(); + +type Row = number; + +function setupGrid(rowHeight: DataGridProps['rowHeight']) { + const columns: Column[] = []; + const rows: readonly Row[] = Array.from({ length: 50 }, (_, i) => i); + + for (let i = 0; i < 5; i++) { + const key = String(i); + columns.push({ + key, + name: key, + width: 80 + }); + } + return setup({ columns, rows, rowHeight }); +} + +async function expectGridRows(rowHeightFn: (row: number) => number, expected: string) { + await setupGrid(rowHeightFn); + + expect(grid.element().style.gridTemplateRows).toBe(expected); +} + +// Data rows are rendered with `aria-rowindex` starting at 2 (the header row is 1), +// so the zero-based row index of the active cell is `aria-rowindex - 2`. +function getActiveDataRowIdx() { + const cell = page.getActiveCell().element(); + const row = cell.closest('[role="row"]')!; + return Number(row.getAttribute('aria-rowindex')) - 2; +} + +test('rowHeight is number', async () => { + await setupGrid(40); + + await expect.element(grid).toHaveStyle({ + gridTemplateRows: + '40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px 40px' + }); + await testRowCount(30); + await safeTab(); + await expect.element(grid).toHaveProperty('scrollTop', 0); + await userEvent.keyboard('{Control>}{end}'); + const gridEl = grid.element(); + expect(gridEl.scrollTop + gridEl.clientHeight).toBe(gridEl.scrollHeight); +}); + +test('rowHeight is function', async () => { + await setupGrid((row) => [40, 60, 80][row % 3]); + + await expect.element(grid).toHaveStyle({ + gridTemplateRows: + '35px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px 80px 40px 60px' + }); + await testRowCount(22); + + await safeTab(); + await expect.element(grid).toHaveProperty('scrollTop', 0); + await userEvent.keyboard('{Control>}{end}'); + const gridEl = grid.element(); + expect(gridEl.scrollTop + gridEl.clientHeight).toBe(gridEl.scrollHeight); +}); + +test('rowHeight with repeat pattern - multiple identical heights', async () => { + await expectGridRows(() => 40, 'repeat(1, 35px) repeat(50, 40px)'); +}); + +test('rowHeight with mixed heights - one unique in middle', async () => { + await expectGridRows( + (row) => (row === 25 ? 40 : 50), + 'repeat(1, 35px) repeat(25, 50px) 40px repeat(24, 50px)' + ); +}); + +test('rowHeight with unique heights', async () => { + await expectGridRows( + (row) => row + 1, + 'repeat(1, 35px) 1px 2px 3px 4px 5px 6px 7px 8px 9px 10px 11px 12px 13px 14px 15px 16px 17px 18px 19px 20px 21px 22px 23px 24px 25px 26px 27px 28px 29px 30px 31px 32px 33px 34px 35px 36px 37px 38px 39px 40px 41px 42px 43px 44px 45px 46px 47px 48px 49px 50px' + ); +}); + +test('rowHeight with unique first and unique last heights', async () => { + await expectGridRows((row) => { + if (row === 0) { + return 10; + } + + if (row === 49) { + return 20; + } + + return 50; + }, 'repeat(1, 35px) 10px repeat(48, 50px) 20px'); +}); + +test('rowHeight with unique last height', async () => { + await expectGridRows((row) => { + return row === 49 ? 50 : 20; + }, 'repeat(1, 35px) repeat(49, 20px) 50px'); +}); + +test('rowHeight with unique first height', async () => { + await expectGridRows((row) => { + return row === 0 ? 45 : 50; + }, 'repeat(1, 35px) 45px repeat(49, 50px)'); +}); + +test('rowHeight is "auto" sets gridTemplateRows to repeat(N, auto)', async () => { + await setupGrid('auto'); + + expect(grid.element().style.gridTemplateRows).toBe('repeat(1, 35px) repeat(50, auto)'); +}); + +test('rowHeight as a string auto-disables virtualization and renders all rows', async () => { + await setupGrid('auto'); + + // virtualization is off by default when `rowHeight` is a string, + // so every row is rendered regardless of viewport size + await testRowCount(50); +}); + +test('rowHeight accepts arbitrary CSS track values', async () => { + await setupGrid('min-content'); + + expect(grid.element().style.gridTemplateRows).toBe('repeat(1, 35px) repeat(50, min-content)'); + await testRowCount(50); +}); + +test('rowHeight is "auto" sizes rows to fit their content', async () => { + const columns: Column<{ id: number; content: string }>[] = [ + { key: 'id', name: 'ID', width: 80 }, + { + key: 'content', + name: 'Content', + width: 200, + renderCell: ({ row }) =>
{row.content}
+ } + ]; + const rows = [ + { id: 0, content: 'short' }, + { id: 1, content: 'line one\nline two\nline three\nline four' } + ]; + + await setup({ columns, rows, rowHeight: 'auto' }); + + const row0 = page.getRow().nth(0).element() as HTMLElement; + const row1 = page.getRow().nth(1).element() as HTMLElement; + + // multi-line cell must render taller than the single-line cell + expect(row1.clientHeight).toBeGreaterThan(row0.clientHeight); +}); + +test('rowHeight string + explicit enableVirtualization=true throws', async () => { + // Suppress React's error logging for this expected render error + // eslint-disable-next-line no-console + vi.mocked(console.error).mockImplementation(() => {}); + + await expect( + setup({ + columns: [{ key: 'id', name: 'ID' }], + rows: [{ id: 0 }], + rowHeight: 'auto', + enableVirtualization: true + }) + ).rejects.toThrow('`rowHeight` cannot be a string when `enableVirtualization` is true.'); + + // eslint-disable-next-line no-console + vi.mocked(console.error).mockClear(); +}); + +test('PageDown/PageUp navigate through string (auto) height rows', async () => { + const columns: Column<{ id: number; content: string }>[] = [ + { key: 'id', name: 'ID', width: 80 }, + { + key: 'content', + name: 'Content', + width: 200, + renderCell: ({ row }) =>
{row.content}
+ } + ]; + // Multi-line content makes each auto-sized row tall, and the fixed grid height + // forces a scrollable viewport so paging moves through several rows at a time. + // This exercises the string-height getRowTop/getRowHeight/findRowIdx DOM paths, + // which measure the rendered cells rather than computing offsets from a number. + const rows = Array.from({ length: 50 }, (_, i) => ({ + id: i, + content: `row ${i}\nline two\nline three` + })); + + await setup({ columns, rows, rowHeight: 'auto', style: { blockSize: 300 } }); + + // tab into the grid (lands on the header row) then move onto the first data row + await safeTab(); + await userEvent.keyboard('{arrowdown}'); + expect(getActiveDataRowIdx()).toBe(0); + + // PageDown moves the active cell forward through the measured rows + await userEvent.keyboard('{PageDown}'); + const afterPageDown = getActiveDataRowIdx(); + expect(afterPageDown).toBeGreaterThan(0); + + // PageUp moves it back up + await userEvent.keyboard('{PageUp}'); + const afterPageUp = getActiveDataRowIdx(); + expect(afterPageUp).toBeLessThan(afterPageDown); + + // and PageDown again moves forward from there + await userEvent.keyboard('{PageDown}'); + expect(getActiveDataRowIdx()).toBeGreaterThan(afterPageUp); + + // Ctrl+End reaches the last row with string heights + await userEvent.keyboard('{Control>}{end}{/Control}'); + expect(getActiveDataRowIdx()).toBe(rows.length - 1); +}); diff --git a/website/Nav.tsx b/website/Nav.tsx index 42731a3144..5ad31e64ff 100644 --- a/website/Nav.tsx +++ b/website/Nav.tsx @@ -91,6 +91,7 @@ export default function Nav({ direction, onDirectionChange }: Props) { Columns Reordering Context Menu Customizable Renderers + Dynamic Height Cells Row Grouping Header Filters Infinite Scrolling diff --git a/website/routeTree.gen.ts b/website/routeTree.gen.ts index d6d76ed36b..13ba61382a 100644 --- a/website/routeTree.gen.ts +++ b/website/routeTree.gen.ts @@ -19,6 +19,7 @@ import { Route as ColumnsReorderingRouteImport } from './routes/ColumnsReorderin import { Route as CommonFeaturesRouteImport } from './routes/CommonFeatures' import { Route as ContextMenuRouteImport } from './routes/ContextMenu' import { Route as CustomizableRenderersRouteImport } from './routes/CustomizableRenderers' +import { Route as DynamicHeightCellsRouteImport } from './routes/DynamicHeightCells' import { Route as HeaderFiltersRouteImport } from './routes/HeaderFilters' import { Route as InfiniteScrollingRouteImport } from './routes/InfiniteScrolling' import { Route as MasterDetailRouteImport } from './routes/MasterDetail' @@ -81,6 +82,11 @@ const CustomizableRenderersRoute = CustomizableRenderersRouteImport.update({ path: '/CustomizableRenderers', getParentRoute: () => rootRouteImport, } as any) +const DynamicHeightCellsRoute = DynamicHeightCellsRouteImport.update({ + id: '/DynamicHeightCells', + path: '/DynamicHeightCells', + getParentRoute: () => rootRouteImport, +} as any) const HeaderFiltersRoute = HeaderFiltersRouteImport.update({ id: '/HeaderFilters', path: '/HeaderFilters', @@ -148,6 +154,7 @@ export interface FileRoutesByFullPath { '/CommonFeatures': typeof CommonFeaturesRoute '/ContextMenu': typeof ContextMenuRoute '/CustomizableRenderers': typeof CustomizableRenderersRoute + '/DynamicHeightCells': typeof DynamicHeightCellsRoute '/HeaderFilters': typeof HeaderFiltersRoute '/InfiniteScrolling': typeof InfiniteScrollingRoute '/MasterDetail': typeof MasterDetailRoute @@ -171,6 +178,7 @@ export interface FileRoutesByTo { '/CommonFeatures': typeof CommonFeaturesRoute '/ContextMenu': typeof ContextMenuRoute '/CustomizableRenderers': typeof CustomizableRenderersRoute + '/DynamicHeightCells': typeof DynamicHeightCellsRoute '/HeaderFilters': typeof HeaderFiltersRoute '/InfiniteScrolling': typeof InfiniteScrollingRoute '/MasterDetail': typeof MasterDetailRoute @@ -195,6 +203,7 @@ export interface FileRoutesById { '/CommonFeatures': typeof CommonFeaturesRoute '/ContextMenu': typeof ContextMenuRoute '/CustomizableRenderers': typeof CustomizableRenderersRoute + '/DynamicHeightCells': typeof DynamicHeightCellsRoute '/HeaderFilters': typeof HeaderFiltersRoute '/InfiniteScrolling': typeof InfiniteScrollingRoute '/MasterDetail': typeof MasterDetailRoute @@ -220,6 +229,7 @@ export interface FileRouteTypes { | '/CommonFeatures' | '/ContextMenu' | '/CustomizableRenderers' + | '/DynamicHeightCells' | '/HeaderFilters' | '/InfiniteScrolling' | '/MasterDetail' @@ -243,6 +253,7 @@ export interface FileRouteTypes { | '/CommonFeatures' | '/ContextMenu' | '/CustomizableRenderers' + | '/DynamicHeightCells' | '/HeaderFilters' | '/InfiniteScrolling' | '/MasterDetail' @@ -266,6 +277,7 @@ export interface FileRouteTypes { | '/CommonFeatures' | '/ContextMenu' | '/CustomizableRenderers' + | '/DynamicHeightCells' | '/HeaderFilters' | '/InfiniteScrolling' | '/MasterDetail' @@ -290,6 +302,7 @@ export interface RootRouteChildren { CommonFeaturesRoute: typeof CommonFeaturesRoute ContextMenuRoute: typeof ContextMenuRoute CustomizableRenderersRoute: typeof CustomizableRenderersRoute + DynamicHeightCellsRoute: typeof DynamicHeightCellsRoute HeaderFiltersRoute: typeof HeaderFiltersRoute InfiniteScrollingRoute: typeof InfiniteScrollingRoute MasterDetailRoute: typeof MasterDetailRoute @@ -375,6 +388,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof CustomizableRenderersRouteImport parentRoute: typeof rootRouteImport } + '/DynamicHeightCells': { + id: '/DynamicHeightCells' + path: '/DynamicHeightCells' + fullPath: '/DynamicHeightCells' + preLoaderRoute: typeof DynamicHeightCellsRouteImport + parentRoute: typeof rootRouteImport + } '/HeaderFilters': { id: '/HeaderFilters' path: '/HeaderFilters' @@ -466,6 +486,7 @@ const rootRouteChildren: RootRouteChildren = { CommonFeaturesRoute: CommonFeaturesRoute, ContextMenuRoute: ContextMenuRoute, CustomizableRenderersRoute: CustomizableRenderersRoute, + DynamicHeightCellsRoute: DynamicHeightCellsRoute, HeaderFiltersRoute: HeaderFiltersRoute, InfiniteScrollingRoute: InfiniteScrollingRoute, MasterDetailRoute: MasterDetailRoute, diff --git a/website/routes/DynamicHeightCells.tsx b/website/routes/DynamicHeightCells.tsx new file mode 100644 index 0000000000..6fd5618fbd --- /dev/null +++ b/website/routes/DynamicHeightCells.tsx @@ -0,0 +1,121 @@ +import { useState, type JSX } from 'react'; +import { createFileRoute } from '@tanstack/react-router'; +import { css } from 'ecij'; + +import { DataGrid, type Column } from '../../src'; +import { useDirection } from '../directionContext'; + +export const Route = createFileRoute('/DynamicHeightCells')({ + component: DynamicHeightCells +}); + +interface Row { + id: number; + task: string; + complete: number; + priority: string; + issueType: string; + startDate: string; + completeDate: string; + dynamicContent: JSX.Element; +} + +const columns: Column[] = [ + { + key: 'id', + name: 'ID', + width: 80 + }, + { + key: 'task', + name: 'Title' + }, + { + key: 'priority', + name: 'Priority' + }, + { + key: 'issueType', + name: 'Issue Type' + }, + { + key: 'complete', + name: '% Complete' + }, + { + key: 'startDate', + name: 'Start Date' + }, + { + key: 'completeDate', + name: 'Expected Complete', + width: 200 + }, + { + key: 'dynamicContent', + name: 'Dynamic HTML Content', + width: 200 + } +]; + +function getRandomDate(start: Date, end: Date) { + return new Date( + start.getTime() + Math.random() * (end.getTime() - start.getTime()) + ).toLocaleDateString(); +} + +function createRows(): Row[] { + const rows = []; + for (let i = 1; i < 500; i++) { + rows.push({ + id: i, + task: `Task ${i}`, + complete: Math.min(100, Math.round(Math.random() * 110)), + priority: ['Critical', 'High', 'Medium', 'Low'][Math.floor(Math.random() * 3 + 1)], + issueType: ['Bug', 'Improvement', 'Epic', 'Story'][Math.floor(Math.random() * 3 + 1)], + startDate: getRandomDate(new Date(2015, 3, 1), new Date()), + completeDate: getRandomDate(new Date(), new Date(2016, 0, 1)), + dynamicContent: (() => { + const arr = []; + for (let i = 0; i < Math.ceil(Math.random() * 6); i++) { + arr.push(`Dynamic content ${i + 1}`); + } + return ( +
    + {arr.map((item, idx) => ( +
  • {item}
  • + ))} +
+ ); + })() + }); + } + + return rows; +} + +const rootClassname = css` + display: flex; + flex-direction: column; + block-size: 100%; + gap: 10px; + + > .rdg { + flex: 1; + } + + .rdg-cell { + padding-block: 0.75em; + } +`; + +function DynamicHeightCells() { + const direction = useDirection(); + const [rows] = useState(createRows); + + return ( +
+ +
+ ); +}