Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use grid layout for rows, split column metrics from compute columns #2272

Merged
merged 7 commits into from
Jan 13, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions src/Cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<R, SR>({
Expand Down Expand Up @@ -72,10 +72,7 @@ function Cell<R, SR>({
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)}
Expand Down
15 changes: 8 additions & 7 deletions src/DataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import GroupRowRenderer from './GroupRow';
import SummaryRow from './SummaryRow';
import {
assertIsValidKeyGetter,
getColumnScrollPosition,
onEditorNavigation,
getNextSelectedCellPosition,
isSelectedCellEditable,
Expand Down Expand Up @@ -248,7 +247,7 @@ function DataGrid<R, SR>({
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,
Expand Down Expand Up @@ -637,12 +636,13 @@ function DataGrid<R, SR>({

if (typeof idx === 'number' && idx > lastFrozenColumnIndex) {
const { clientWidth } = current;
const { left, width } = columns[idx];
const { left, width } = columnMetrics.get(columns[idx])!;
const isCellAtLeftBoundary = left < scrollLeft + width + 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;
}
}

Expand Down Expand Up @@ -884,7 +884,8 @@ function DataGrid<R, SR>({
'--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}
Expand Down
6 changes: 2 additions & 4 deletions src/EditCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<R, SR> = Pick<CellRendererProps<R, SR>,
Expand Down Expand Up @@ -69,10 +70,7 @@ export default function EditCell<R, SR>({
aria-selected
ref={cellRef}
className={className}
style={{
width: column.width,
left: column.left
}}
style={getCellStyle(column)}
{...props}
>
{getCellContent()}
Expand Down
8 changes: 2 additions & 6 deletions src/FilterRow.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -32,21 +33,16 @@ function FilterRow<R, SR>({
>
{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 (
<div
key={key}
style={style}
className={className}
style={getCellStyle(column)}
>
{column.filterRenderer && (
<column.filterRenderer
Expand Down
4 changes: 2 additions & 2 deletions src/GroupCell.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { memo } from 'react';
import clsx from 'clsx';

import { getCellStyle } from './utils';
import type { CalculatedColumn } from './types';
import type { GroupRowRendererProps } from './GroupRow';

Expand Down Expand Up @@ -56,8 +57,7 @@ function GroupCell<R, SR>({
'rdg-cell-selected': isCellSelected
})}
style={{
width: column.width,
left: column.left,
...getCellStyle(column),
cursor: isLevelMatching ? 'pointer' : 'default'
}}
onClick={isLevelMatching ? toggleGroup : undefined}
Expand Down
7 changes: 2 additions & 5 deletions src/HeaderCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -109,18 +110,14 @@ export default function HeaderCell<R, SR>({
'rdg-cell-frozen': column.frozen,
'rdg-cell-frozen-last': column.isLastFrozenColumn
});
const style: React.CSSProperties = {
width: column.width,
left: column.left
};

return (
<div
role="columnheader"
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()}
Expand Down
5 changes: 3 additions & 2 deletions src/SummaryCell.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { memo } from 'react';
import clsx from 'clsx';

import { getCellStyle } from './utils';
import type { CellRendererProps } from './types';

type SharedCellRendererProps<R, SR> = Pick<CellRendererProps<R, SR>, 'column'>;
Expand All @@ -13,7 +14,7 @@ function SummaryCell<R, SR>({
column,
row
}: SummaryCellProps<R, SR>) {
const { summaryFormatter: SummaryFormatter, width, left, summaryCellClass } = column;
const { summaryFormatter: SummaryFormatter, summaryCellClass } = column;
const className = clsx(
'rdg-cell',
{
Expand All @@ -28,7 +29,7 @@ function SummaryCell<R, SR>({
role="gridcell"
aria-colindex={column.idx + 1}
className={className}
style={{ width, left }}
style={getCellStyle(column)}
>
{SummaryFormatter && <SummaryFormatter column={column} row={row} />}
</div>
Expand Down
149 changes: 88 additions & 61 deletions src/hooks/useViewportColumns.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -26,31 +26,25 @@ export function useViewportColumns<R, SR>({
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<R, SR> & { 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 => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only create 1 columns array now in this useMemo from 2 arrays before.
We also create the column object once, no more intermediary column object.

const isGroup = rawGroupBy?.includes(rawColumn.key);

const column: CalculatedColumn<R, SR> = {
...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++;
Expand Down Expand Up @@ -84,51 +78,84 @@ export function useViewportColumns<R, SR>({
return 0;
});

const unallocatedWidth = viewportWidth - allocatedWidths;
columns.forEach((column, idx) => {
column.idx = idx;

if (idx === lastFrozenColumnIndex) {
column.isLastFrozenColumn = true;
}

if (column.rowGroup) {
groupBy.push(column.key);
column.groupFormatter ??= ToggleGroupFormatter;
Copy link
Contributor Author

@nstepien nstepien Jan 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Browsers already support this syntax, and babel transpiles it. 👍
https://caniuse.com/mdn-javascript_operators_logical_nullish_assignment
Bonus point: it doesn't re-assign the value if it already exists.
https://v8.dev/blog/v8-release-85#logical-assignment-operators

}
});

return {
columns,
lastFrozenColumnIndex,
groupBy
};
}, [rawColumns, defaultFormatter, defaultResizable, defaultSortable, rawGroupBy]);

const { layoutCssVars, totalColumnWidth, totalFrozenColumnWidth, columnMetrics } = useMemo(() => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We now compute column metrics in a separate useMemo, and store it in a separate Map.
That way we can, for example, resize columns without re-creating the column objects, and reducing re-renders.

const columnMetrics = new Map<CalculatedColumn<R, SR>, 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 });
}
}

const unallocatedWidth = viewportWidth - allocatedWidth;
const unallocatedColumnWidth = Math.max(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to use Math.max if we are using clampColumnWidth later?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't, I've removed it now, we also don't need to floor it, now columns will always fill all the available space when possible.
For a before/after comparison, open the columns reordering story, and resize the browser, you'll see that with Math.floor() there can be some space between the last column and the scrollbar.

Math.floor(unallocatedWidth / unassignedColumnsCount),
minColumnWidth
);

// Filter rawGroupBy and ignore keys that do not match the columns prop
const groupBy: string[] = [];
const calculatedColumns: CalculatedColumn<R, SR>[] = 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 (newColumn.rowGroup) {
groupBy.push(column.key);
newColumn.groupFormatter = column.groupFormatter ?? ToggleGroupFormatter;
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 });
}

totalWidth += width;
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<string, string> = {
'--template-columns': templateColumns
Copy link
Contributor Author

@nstepien nstepien Jan 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is used for the grid-template-columns css property in all rows now.
That way we set the template on the root, and we don't have to update all rows/cells (in js).

};
}, [columnWidths, defaultFormatter, defaultResizable, defaultSortable, minColumnWidth, rawColumns, rawGroupBy, viewportWidth]);

for (let i = 0; i <= lastFrozenColumnIndex; i++) {
const column = columns[i];
layoutCssVars[`--sticky-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
Expand All @@ -146,7 +173,7 @@ export function useViewportColumns<R, SR>({
// 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) {
Expand All @@ -158,7 +185,7 @@ export function useViewportColumns<R, SR>({
// 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) {
Expand All @@ -171,7 +198,7 @@ export function useViewportColumns<R, SR>({
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<R, SR>[] => {
const viewportColumns: CalculatedColumn<R, SR>[] = [];
Expand All @@ -185,7 +212,7 @@ export function useViewportColumns<R, SR>({
return viewportColumns;
}, [colOverscanEndIdx, colOverscanStartIdx, columns]);

return { columns, viewportColumns, totalColumnWidth, lastFrozenColumnIndex, totalFrozenColumnWidth, groupBy };
return { columns, viewportColumns, layoutCssVars, columnMetrics, totalColumnWidth, lastFrozenColumnIndex, totalFrozenColumnWidth, groupBy };
}

function getSpecifiedWidth<R, SR>(
Expand Down
Loading