diff --git a/src/Cell.tsx b/src/Cell.tsx index 2862826266..6950af4e11 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -2,7 +2,7 @@ import { forwardRef, memo, useRef } from 'react'; import clsx from 'clsx'; import type { CellRendererProps } from './types'; -import { wrapEvent } from './utils'; +import { getCellStyle, wrapEvent } from './utils'; import { useCombinedRefs } from './hooks'; function Cell({ @@ -72,10 +72,7 @@ function Cell({ aria-selected={isCellSelected} ref={useCombinedRefs(cellRef, ref)} className={className} - style={{ - width: column.width, - left: column.left - }} + style={getCellStyle(column)} onClick={wrapEvent(handleClick, onClick)} onDoubleClick={wrapEvent(handleDoubleClick, onDoubleClick)} onContextMenu={wrapEvent(handleContextMenu, onContextMenu)} diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 9e0dcb7839..d59bccafc8 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -16,7 +16,6 @@ import GroupRowRenderer from './GroupRow'; import SummaryRow from './SummaryRow'; import { assertIsValidKeyGetter, - getColumnScrollPosition, onEditorNavigation, getNextSelectedCellPosition, isSelectedCellEditable, @@ -248,7 +247,7 @@ function DataGrid({ const clientHeight = gridHeight - totalHeaderHeight - summaryRowsCount * rowHeight; const isSelectable = selectedRows !== undefined && onSelectedRowsChange !== undefined; - const { columns, viewportColumns, totalColumnWidth, lastFrozenColumnIndex, totalFrozenColumnWidth, groupBy } = useViewportColumns({ + const { columns, viewportColumns, layoutCssVars, columnMetrics, totalColumnWidth, lastFrozenColumnIndex, totalFrozenColumnWidth, groupBy } = useViewportColumns({ rawColumns, columnWidths, scrollLeft, @@ -637,12 +636,13 @@ function DataGrid({ if (typeof idx === 'number' && idx > lastFrozenColumnIndex) { const { clientWidth } = current; - const { left, width } = columns[idx]; - const isCellAtLeftBoundary = left < scrollLeft + width + totalFrozenColumnWidth; + const { left, width } = columnMetrics.get(columns[idx])!; + const isCellAtLeftBoundary = left < scrollLeft + totalFrozenColumnWidth; const isCellAtRightBoundary = left + width > clientWidth + scrollLeft; - if (isCellAtLeftBoundary || isCellAtRightBoundary) { - const newScrollLeft = getColumnScrollPosition(columns, idx, scrollLeft, clientWidth); - current.scrollLeft = scrollLeft + newScrollLeft; + if (isCellAtLeftBoundary) { + current.scrollLeft = left - totalFrozenColumnWidth; + } else if (isCellAtRightBoundary) { + current.scrollLeft = left + width - clientWidth; } } @@ -884,7 +884,8 @@ function DataGrid({ '--header-row-height': `${headerRowHeight}px`, '--filter-row-height': `${headerFiltersHeight}px`, '--row-width': `${totalColumnWidth}px`, - '--row-height': `${rowHeight}px` + '--row-height': `${rowHeight}px`, + ...layoutCssVars } as unknown as React.CSSProperties} ref={gridRef} onScroll={handleScroll} diff --git a/src/EditCell.tsx b/src/EditCell.tsx index fafb96a747..46eb63ad00 100644 --- a/src/EditCell.tsx +++ b/src/EditCell.tsx @@ -2,6 +2,7 @@ import { useState, useCallback } from 'react'; import clsx from 'clsx'; import EditorContainer from './editors/EditorContainer'; +import { getCellStyle } from './utils'; import type { CellRendererProps, SharedEditorProps, Omit } from './types'; type SharedCellRendererProps = Pick, @@ -69,10 +70,7 @@ export default function EditCell({ aria-selected ref={cellRef} className={className} - style={{ - width: column.width, - left: column.left - }} + style={getCellStyle(column)} {...props} > {getCellContent()} diff --git a/src/FilterRow.tsx b/src/FilterRow.tsx index 411361a12c..b5a7886f9c 100644 --- a/src/FilterRow.tsx +++ b/src/FilterRow.tsx @@ -1,6 +1,7 @@ import { memo } from 'react'; import clsx from 'clsx'; +import { getCellStyle } from './utils'; import type { CalculatedColumn, Filters } from './types'; import type { DataGridProps } from './DataGrid'; @@ -32,21 +33,16 @@ function FilterRow({ > {columns.map(column => { const { key } = column; - const className = clsx('rdg-cell', { 'rdg-cell-frozen': column.frozen, 'rdg-cell-frozen-last': column.isLastFrozenColumn }); - const style: React.CSSProperties = { - width: column.width, - left: column.left - }; return (
{column.filterRenderer && ( ({ 'rdg-cell-selected': isCellSelected })} style={{ - width: column.width, - left: column.left, + ...getCellStyle(column), cursor: isLevelMatching ? 'pointer' : 'default' }} onClick={isLevelMatching ? toggleGroup : undefined} diff --git a/src/HeaderCell.tsx b/src/HeaderCell.tsx index 9857eb0774..880d6c6a02 100644 --- a/src/HeaderCell.tsx +++ b/src/HeaderCell.tsx @@ -3,6 +3,7 @@ import clsx from 'clsx'; import type { CalculatedColumn } from './types'; import type { HeaderRowProps } from './HeaderRow'; import SortableHeaderCell from './headerCells/SortableHeaderCell'; +import { getCellStyle } from './utils'; import type { SortDirection } from './enums'; function getAriaSort(sortDirection?: SortDirection) { @@ -109,10 +110,6 @@ export default function HeaderCell({ 'rdg-cell-frozen': column.frozen, 'rdg-cell-frozen-last': column.isLastFrozenColumn }); - const style: React.CSSProperties = { - width: column.width, - left: column.left - }; return (
({ aria-colindex={column.idx + 1} aria-sort={sortColumn === column.key ? getAriaSort(sortDirection) : undefined} className={className} - style={style} + style={getCellStyle(column)} onPointerDown={column.resizable ? onPointerDown : undefined} > {getCell()} diff --git a/src/SummaryCell.tsx b/src/SummaryCell.tsx index fa34e6ee42..b66735acf5 100644 --- a/src/SummaryCell.tsx +++ b/src/SummaryCell.tsx @@ -1,6 +1,7 @@ import { memo } from 'react'; import clsx from 'clsx'; +import { getCellStyle } from './utils'; import type { CellRendererProps } from './types'; type SharedCellRendererProps = Pick, 'column'>; @@ -13,7 +14,7 @@ function SummaryCell({ column, row }: SummaryCellProps) { - const { summaryFormatter: SummaryFormatter, width, left, summaryCellClass } = column; + const { summaryFormatter: SummaryFormatter, summaryCellClass } = column; const className = clsx( 'rdg-cell', { @@ -28,7 +29,7 @@ function SummaryCell({ role="gridcell" aria-colindex={column.idx + 1} className={className} - style={{ width, left }} + style={getCellStyle(column)} > {SummaryFormatter && }
diff --git a/src/hooks/useGridDimensions.ts b/src/hooks/useGridDimensions.ts index 5c22fe4495..945739bb15 100644 --- a/src/hooks/useGridDimensions.ts +++ b/src/hooks/useGridDimensions.ts @@ -24,10 +24,12 @@ export function useGridDimensions(): [React.RefObject, number, n // don't break in jest/jsdom and browsers that don't support ResizeObserver if (ResizeObserver == null) return; - const resizeObserver = new ResizeObserver(entries => { - const { width, height } = entries[0].contentRect; - setGridWidth(width); - setGridHeight(height); + const resizeObserver = new ResizeObserver(() => { + // Get dimensions without scrollbars. + // The dimensions given by the callback entries in Firefox do not substract the scrollbar sizes. + const { clientWidth, clientHeight } = gridRef.current!; + setGridWidth(clientWidth); + setGridHeight(clientHeight); }); resizeObserver.observe(gridRef.current!); diff --git a/src/hooks/useViewportColumns.ts b/src/hooks/useViewportColumns.ts index 9d3be59cdb..b546b018f8 100644 --- a/src/hooks/useViewportColumns.ts +++ b/src/hooks/useViewportColumns.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react'; -import type { CalculatedColumn, Column } from '../types'; +import type { CalculatedColumn, Column, ColumnMetric } from '../types'; import type { DataGridProps } from '../DataGrid'; import { ValueFormatter, ToggleGroupFormatter } from '../formatters'; import { SELECT_COLUMN_KEY } from '../Columns'; @@ -26,31 +26,25 @@ export function useViewportColumns({ const defaultSortable = defaultColumnOptions?.sortable ?? false; const defaultResizable = defaultColumnOptions?.resizable ?? false; - const { columns, lastFrozenColumnIndex, totalColumnWidth, totalFrozenColumnWidth, groupBy } = useMemo(() => { - let left = 0; - let totalWidth = 0; - let allocatedWidths = 0; - let unassignedColumnsCount = 0; + const { columns, lastFrozenColumnIndex, groupBy } = useMemo(() => { + // Filter rawGroupBy and ignore keys that do not match the columns prop + const groupBy: string[] = []; let lastFrozenColumnIndex = -1; - type IntermediateColumn = Column & { width: number | undefined; rowGroup?: boolean }; - let totalFrozenColumnWidth = 0; - - const columns = rawColumns.map(metricsColumn => { - let width = getSpecifiedWidth(metricsColumn, columnWidths, viewportWidth); - - if (width === undefined) { - unassignedColumnsCount++; - } else { - width = clampColumnWidth(width, metricsColumn, minColumnWidth); - allocatedWidths += width; - } - - const column: IntermediateColumn = { ...metricsColumn, width }; - if (rawGroupBy?.includes(column.key)) { - column.frozen = true; - column.rowGroup = true; - } + const columns = rawColumns.map(rawColumn => { + const isGroup = rawGroupBy?.includes(rawColumn.key); + + const column: CalculatedColumn = { + ...rawColumn, + idx: 0, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + frozen: isGroup || rawColumn.frozen || false, + isLastFrozenColumn: false, + rowGroup: isGroup, + sortable: rawColumn.sortable ?? defaultSortable, + resizable: rawColumn.resizable ?? defaultResizable, + formatter: rawColumn.formatter ?? defaultFormatter + }; if (column.frozen) { lastFrozenColumnIndex++; @@ -84,51 +78,81 @@ export function useViewportColumns({ return 0; }); - const unallocatedWidth = viewportWidth - allocatedWidths; - const unallocatedColumnWidth = Math.max( - Math.floor(unallocatedWidth / unassignedColumnsCount), - minColumnWidth - ); + columns.forEach((column, idx) => { + column.idx = idx; - // Filter rawGroupBy and ignore keys that do not match the columns prop - const groupBy: string[] = []; - const calculatedColumns: CalculatedColumn[] = columns.map((column, idx) => { - // Every column should have a valid width as this stage - const width = column.width ?? clampColumnWidth(unallocatedColumnWidth, column, minColumnWidth); - const newColumn = { - ...column, - idx, - width, - left, - sortable: column.sortable ?? defaultSortable, - resizable: column.resizable ?? defaultResizable, - formatter: column.formatter ?? defaultFormatter - }; + if (idx === lastFrozenColumnIndex) { + column.isLastFrozenColumn = true; + } - if (newColumn.rowGroup) { + if (column.rowGroup) { groupBy.push(column.key); - newColumn.groupFormatter = column.groupFormatter ?? ToggleGroupFormatter; + column.groupFormatter ??= ToggleGroupFormatter; + } + }); + + return { + columns, + lastFrozenColumnIndex, + groupBy + }; + }, [rawColumns, defaultFormatter, defaultResizable, defaultSortable, rawGroupBy]); + + const { layoutCssVars, totalColumnWidth, totalFrozenColumnWidth, columnMetrics } = useMemo(() => { + const columnMetrics = new Map, ColumnMetric>(); + let left = 0; + let totalColumnWidth = 0; + let totalFrozenColumnWidth = 0; + let templateColumns = ''; + let allocatedWidth = 0; + let unassignedColumnsCount = 0; + + for (const column of columns) { + let width = getSpecifiedWidth(column, columnWidths, viewportWidth); + + if (width === undefined) { + unassignedColumnsCount++; + } else { + width = clampColumnWidth(width, column, minColumnWidth); + allocatedWidth += width; + columnMetrics.set(column, { width, left: 0 }); } + } - totalWidth += width; + const unallocatedWidth = viewportWidth - allocatedWidth; + const unallocatedColumnWidth = unallocatedWidth / unassignedColumnsCount; + + for (const column of columns) { + let width; + if (columnMetrics.has(column)) { + const columnMetric = columnMetrics.get(column)!; + columnMetric.left = left; + ({ width } = columnMetric); + } else { + width = clampColumnWidth(unallocatedColumnWidth, column, minColumnWidth); + columnMetrics.set(column, { width, left }); + } + totalColumnWidth += width; left += width; - return newColumn; - }); + templateColumns += `${width}px `; + } if (lastFrozenColumnIndex !== -1) { - const lastFrozenColumn = calculatedColumns[lastFrozenColumnIndex]; - lastFrozenColumn.isLastFrozenColumn = true; - totalFrozenColumnWidth = lastFrozenColumn.left + lastFrozenColumn.width; + const columnMetric = columnMetrics.get(columns[lastFrozenColumnIndex])!; + totalFrozenColumnWidth = columnMetric.left + columnMetric.width; } - return { - columns: calculatedColumns, - lastFrozenColumnIndex, - totalFrozenColumnWidth, - totalColumnWidth: totalWidth, - groupBy + const layoutCssVars: Record = { + '--template-columns': templateColumns }; - }, [columnWidths, defaultFormatter, defaultResizable, defaultSortable, minColumnWidth, rawColumns, rawGroupBy, viewportWidth]); + + for (let i = 0; i <= lastFrozenColumnIndex; i++) { + const column = columns[i]; + layoutCssVars[`--frozen-left-${column.key}`] = `${columnMetrics.get(column)!.left}px`; + } + + return { layoutCssVars, totalColumnWidth, totalFrozenColumnWidth, columnMetrics }; + }, [columnWidths, columns, viewportWidth, minColumnWidth, lastFrozenColumnIndex]); const [colOverscanStartIdx, colOverscanEndIdx] = useMemo((): [number, number] => { // get the viewport's left side and right side positions for non-frozen columns @@ -146,7 +170,7 @@ export function useViewportColumns({ // get the first visible non-frozen column index let colVisibleStartIdx = firstUnfrozenColumnIdx; while (colVisibleStartIdx < lastColIdx) { - const { left, width } = columns[colVisibleStartIdx]; + const { left, width } = columnMetrics.get(columns[colVisibleStartIdx])!; // if the right side of the columnn is beyond the left side of the available viewport, // then it is the first column that's at least partially visible if (left + width > viewportLeft) { @@ -158,7 +182,7 @@ export function useViewportColumns({ // get the last visible non-frozen column index let colVisibleEndIdx = colVisibleStartIdx; while (colVisibleEndIdx < lastColIdx) { - const { left, width } = columns[colVisibleEndIdx]; + const { left, width } = columnMetrics.get(columns[colVisibleEndIdx])!; // if the right side of the column is beyond or equal to the right side of the available viewport, // then it the last column that's at least partially visible, as the previous column's right side is not beyond the viewport. if (left + width >= viewportRight) { @@ -171,7 +195,7 @@ export function useViewportColumns({ const colOverscanEndIdx = Math.min(lastColIdx, colVisibleEndIdx + 1); return [colOverscanStartIdx, colOverscanEndIdx]; - }, [columns, lastFrozenColumnIndex, scrollLeft, totalFrozenColumnWidth, viewportWidth]); + }, [columns, columnMetrics, lastFrozenColumnIndex, scrollLeft, totalFrozenColumnWidth, viewportWidth]); const viewportColumns = useMemo((): readonly CalculatedColumn[] => { const viewportColumns: CalculatedColumn[] = []; @@ -185,7 +209,7 @@ export function useViewportColumns({ return viewportColumns; }, [colOverscanEndIdx, colOverscanStartIdx, columns]); - return { columns, viewportColumns, totalColumnWidth, lastFrozenColumnIndex, totalFrozenColumnWidth, groupBy }; + return { columns, viewportColumns, layoutCssVars, columnMetrics, totalColumnWidth, lastFrozenColumnIndex, totalFrozenColumnWidth, groupBy }; } function getSpecifiedWidth( diff --git a/src/types.ts b/src/types.ts index 923b4baf25..b675e9e746 100644 --- a/src/types.ts +++ b/src/types.ts @@ -58,15 +58,19 @@ export interface Column { export interface CalculatedColumn extends Column { idx: number; - width: number; - left: number; resizable: boolean; sortable: boolean; - isLastFrozenColumn?: boolean; + frozen: boolean; + isLastFrozenColumn: boolean; rowGroup?: boolean; formatter: React.ComponentType>; } +export interface ColumnMetric { + width: number; + left: number; +} + export interface Position { idx: number; rowIdx: number; diff --git a/src/utils/columnUtils.ts b/src/utils/columnUtils.ts deleted file mode 100644 index ed71e7006b..0000000000 --- a/src/utils/columnUtils.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { CalculatedColumn } from '../types'; - -export function getColumnScrollPosition(columns: readonly CalculatedColumn[], idx: number, currentScrollLeft: number, currentClientWidth: number): number { - let left = 0; - let frozen = 0; - - for (let i = 0; i < idx; i++) { - const column = columns[i]; - if (column) { - if (column.width) { - left += column.width; - } - if (column.frozen) { - frozen += column.width; - } - } - } - - const selectedColumn = columns[idx]; - if (selectedColumn) { - const scrollLeft = left - frozen - currentScrollLeft; - const scrollRight = left + selectedColumn.width - currentScrollLeft; - - if (scrollLeft < 0) { - return scrollLeft; - } - if (scrollRight > currentClientWidth) { - return scrollRight - currentClientWidth; - } - } - - return 0; -} - -/** - * By default, the following navigation keys are enabled while an editor is open, under specific conditions: - * - Tab: - * - The editor must be an , a