Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
27 changes: 20 additions & 7 deletions src/DataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import {
useRef,
useLayoutEffect,
useImperativeHandle,
useCallback
useCallback,
useMemo
} from 'react';
import type { RefAttributes } from 'react';
import clsx from 'clsx';

import { rootClassname, viewportDraggingClassname, focusSinkClassname } from './style';
import { useGridDimensions, useViewportColumns, useViewportRows, useLatestFunc } from './hooks';
import HeaderRow from './HeaderRow';
import ParentHeaderRow from './ParentHeaderRow';
import FilterRow from './FilterRow';
import Row from './Row';
import GroupRowRenderer from './GroupRow';
Expand Down Expand Up @@ -39,7 +41,8 @@ import type {
FillEvent,
PasteEvent,
CellNavigationMode,
SortDirection
SortDirection,
ParentColumn
} from './types';

interface SelectCellState extends Position {
Expand Down Expand Up @@ -82,7 +85,7 @@ export interface DataGridProps<R, SR = unknown> extends SharedDivProps {
* Grid and data Props
*/
/** An array of objects representing each column on the grid */
columns: readonly Column<R, SR>[];
columns: readonly (Column<R, SR> | ParentColumn<R, SR>)[];
Copy link
Collaborator

Choose a reason for hiding this comment

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

Thank you for the POC.

I can foresee a few issues with this approach.

  1. It cannot handle more that 2 header rows
  2. It relies on unique key to match the parent and the child rows. What happens when a key does not match or column does not have a parent key.

I think a better api would be use an implicit relationship between header rows so something like

interface ParentColumn {
   name: '',
   children: Array<Column | ParentColumn>;
}

interface Column {
}

This way we can specify any level of nesting

Copy link
Collaborator

Choose a reason for hiding this comment

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

I have started a PR based on the above idea. Let me know if the api handles your use case.

/** A function called for each rendered row that should return a plain key/value pair object */
rows: readonly R[];
/**
Expand Down Expand Up @@ -246,13 +249,16 @@ function DataGrid<R, SR>({
* computed values
*/
const [gridRef, gridWidth, gridHeight] = useGridDimensions();
const headerRowsCount = enableFilterRow ? 2 : 1;
const enableParentHeader = useMemo(() => rawColumns.filter(c => 'children' in c).length > 0, [rawColumns]);
const parentRowCount = enableParentHeader ? 1 : 0;
const filterRowCount = enableFilterRow ? 1 : 0;
const headerRowsCount = parentRowCount + filterRowCount + 1;
const summaryRowsCount = summaryRows?.length ?? 0;
const totalHeaderHeight = headerRowHeight + (enableFilterRow ? headerFiltersHeight : 0);
const totalHeaderHeight = headerRowHeight * (parentRowCount + 1) + (enableFilterRow ? headerFiltersHeight : 0);
const clientHeight = gridHeight - totalHeaderHeight - summaryRowsCount * rowHeight;
const isSelectable = selectedRows !== undefined && onSelectedRowsChange !== undefined;

const { columns, viewportColumns, layoutCssVars, columnMetrics, totalColumnWidth, lastFrozenColumnIndex, totalFrozenColumnWidth, groupBy } = useViewportColumns({
const { parentColumns, columns, viewportColumns, layoutCssVars, columnMetrics, totalColumnWidth, lastFrozenColumnIndex, totalFrozenColumnWidth, groupBy } = useViewportColumns({
rawColumns,
columnWidths,
scrollLeft,
Expand Down Expand Up @@ -821,7 +827,7 @@ function DataGrid<R, SR>({

function getViewportRows() {
const rowElements = [];
let startRowIndex = 0;
let startRowIndex = enableParentHeader ? 1 : 0;
for (let rowIdx = rowOverscanStartIdx; rowIdx <= rowOverscanEndIdx; rowIdx++) {
const row = rows[rowIdx];
const top = rowIdx * rowHeight + totalHeaderHeight;
Expand Down Expand Up @@ -920,6 +926,11 @@ function DataGrid<R, SR>({
ref={gridRef}
onScroll={handleScroll}
>
{enableParentHeader && (
<ParentHeaderRow<R, SR>
columns={parentColumns}
/>
)}
<HeaderRow<R, SR>
rowKeyGetter={rowKeyGetter}
rows={rawRows}
Expand All @@ -930,6 +941,8 @@ function DataGrid<R, SR>({
sortColumn={sortColumn}
sortDirection={sortDirection}
onSort={onSort}
rowIndex={enableParentHeader ? 2 : undefined}
topOffset={parentRowCount * headerRowHeight}
/>
{enableFilterRow && (
<FilterRow<R, SR>
Expand Down
11 changes: 9 additions & 2 deletions src/HeaderRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export interface HeaderRowProps<R, SR> extends SharedDataGridProps<R, SR> {
columns: readonly CalculatedColumn<R, SR>[];
allRowsSelected: boolean;
onColumnResize: (column: CalculatedColumn<R, SR>, width: number) => void;
rowIndex?: number;
topOffset?: number;
}

function HeaderRow<R, SR>({
Expand All @@ -30,7 +32,9 @@ function HeaderRow<R, SR>({
onColumnResize,
sortColumn,
sortDirection,
onSort
onSort,
rowIndex = 1,
topOffset = 0
}: HeaderRowProps<R, SR>) {
const handleAllRowsSelectionChange = useCallback((checked: boolean) => {
if (!onSelectedRowsChange) return;
Expand All @@ -50,8 +54,11 @@ function HeaderRow<R, SR>({
return (
<div
role="row"
aria-rowindex={1} // aria-rowindex is 1 based
aria-rowindex={rowIndex} // aria-rowindex is 1 based
className={headerRowClassname}
style={{
top: topOffset
}}
>
{columns.map(column => {
return (
Expand Down
35 changes: 35 additions & 0 deletions src/ParentHeaderCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { CalculatedParentColumn } from './types';
import { getParentCellClassname, getParentCellStyle } from './utils';

export interface HeaderCellProps<R, SR> {
column: CalculatedParentColumn<R, SR>;
}

export default function ParentHeaderCell<R, SR>({
column
}: HeaderCellProps<R, SR>) {
function getCell() {
if (column.parentHeaderRenderer) {
return (
<column.parentHeaderRenderer
column={column}
/>
);
}

return column.name;
}

const className = getParentCellClassname(column, column.parentHeaderCellClass);

return (
<div
role="columnheader"
aria-colindex={column.idx + 1}
className={className}
style={getParentCellStyle(column)}
>
{getCell()}
</div>
);
}
32 changes: 32 additions & 0 deletions src/ParentHeaderRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { memo } from 'react';

import type { CalculatedParentColumn } from './types';
import { headerRowClassname } from './style';
import ParentHeaderCell from './ParentHeaderCell';

export interface ParentHeaderRowProps<R, SR> {
columns: readonly CalculatedParentColumn<R, SR>[];
}

function ParentHeaderRow<R, SR>({
columns
}: ParentHeaderRowProps<R, SR>) {
return (
<div
role="row"
aria-rowindex={1} // aria-rowindex is 1 based
className={headerRowClassname}
>
{columns.map(column => {
return (
<ParentHeaderCell<R, SR>
key={column.key}
column={column}
/>
);
})}
</div>
);
}

export default memo(ParentHeaderRow) as <R, SR>(props: ParentHeaderRowProps<R, SR>) => JSX.Element;
81 changes: 74 additions & 7 deletions src/hooks/useViewportColumns.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useMemo } from 'react';

import type { CalculatedColumn, Column, ColumnMetric } from '../types';
import type { CalculatedColumn, CalculatedParentColumn, Column, ColumnMetric, ParentColumn } from '../types';
import type { DataGridProps } from '../DataGrid';
import { ValueFormatter, ToggleGroupFormatter } from '../formatters';
import { SELECT_COLUMN_KEY } from '../Columns';

interface ViewportColumnsArgs<R, SR> extends Pick<DataGridProps<R, SR>, 'defaultColumnOptions'> {
rawColumns: readonly Column<R, SR>[];
rawColumns: readonly (Column<R, SR> | ParentColumn<R, SR>)[];
rawGroupBy?: readonly string[];
viewportWidth: number;
scrollLeft: number;
Expand All @@ -26,15 +26,19 @@ export function useViewportColumns<R, SR>({
const defaultSortable = defaultColumnOptions?.sortable ?? false;
const defaultResizable = defaultColumnOptions?.resizable ?? false;

const { columns, lastFrozenColumnIndex, groupBy } = useMemo(() => {
const { parentColumns, columns, lastFrozenColumnIndex, groupBy } = useMemo(() => {
// Filter rawGroupBy and ignore keys that do not match the columns prop
const groupBy: string[] = [];
let lastFrozenColumnIndex = -1;
let lastFrozenParentColumnIndex = -1;

const columns = rawColumns.map(rawColumn => {
const columns: CalculatedColumn<R, SR>[] = [];
const parentColumns: CalculatedParentColumn<R, SR>[] = [];

function getColumn(rawColumn: Column<R, SR>, parentFrozen = false) {
const rowGroup = rawGroupBy?.includes(rawColumn.key) ?? false;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const frozen = rowGroup || rawColumn.frozen || false;
const frozen = rowGroup || rawColumn.frozen || parentFrozen || false;

const column: CalculatedColumn<R, SR> = {
...rawColumn,
Expand All @@ -44,7 +48,8 @@ export function useViewportColumns<R, SR>({
rowGroup,
sortable: rawColumn.sortable ?? defaultSortable,
resizable: rawColumn.resizable ?? defaultResizable,
formatter: rawColumn.formatter ?? defaultFormatter
formatter: rawColumn.formatter ?? defaultFormatter,
width: rawColumn.width ?? rawColumn.maxWidth
};

if (rowGroup) {
Expand All @@ -56,6 +61,34 @@ export function useViewportColumns<R, SR>({
}

return column;
}

function getParentColumn(rawColumn: ParentColumn<R, SR>) {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const frozen = rawColumn.frozen || false;

const column: CalculatedParentColumn<R, SR> = {
...rawColumn,
idx: 0,
frozen,
isLastFrozenColumn: false,
span: rawColumn.children.length
};

if (frozen) {
lastFrozenParentColumnIndex++;
}

return column;
}

rawColumns.forEach((rawColumn) => {
if ('children' in rawColumn) {
parentColumns.push(getParentColumn(rawColumn));
columns.push(...rawColumn.children.map(col => getColumn(col, rawColumn.frozen)));
} else {
columns.push(getColumn(rawColumn));
}
});

columns.sort(({ key: aKey, frozen: frozenA }, { key: bKey, frozen: frozenB }) => {
Expand Down Expand Up @@ -83,6 +116,31 @@ export function useViewportColumns<R, SR>({
return 0;
});

parentColumns.sort(({ key: aKey, frozen: frozenA }, { key: bKey, frozen: frozenB }) => {
// Sort select column first:
if (aKey === SELECT_COLUMN_KEY) return -1;
if (bKey === SELECT_COLUMN_KEY) return 1;

// // Sort grouped columns second, following the groupBy order:
// if (rawGroupBy?.includes(aKey)) {
// if (rawGroupBy.includes(bKey)) {
// return rawGroupBy.indexOf(aKey) - rawGroupBy.indexOf(bKey);
// }
// return -1;
// }
// if (rawGroupBy?.includes(bKey)) return 1;

// Sort frozen columns third:
if (frozenA) {
if (frozenB) return 0;
return -1;
}
if (frozenB) return 1;

// Sort other columns last:
return 0;
});

columns.forEach((column, idx) => {
column.idx = idx;

Expand All @@ -91,11 +149,20 @@ export function useViewportColumns<R, SR>({
}
});

parentColumns.forEach((column, idx) => {
column.idx = idx;
});

if (lastFrozenColumnIndex !== -1) {
columns[lastFrozenColumnIndex].isLastFrozenColumn = true;
}

if (lastFrozenParentColumnIndex !== -1) {
parentColumns[lastFrozenParentColumnIndex].isLastFrozenColumn = true;
}

return {
parentColumns,
columns,
lastFrozenColumnIndex,
groupBy
Expand Down Expand Up @@ -213,7 +280,7 @@ export function useViewportColumns<R, SR>({
return viewportColumns;
}, [colOverscanEndIdx, colOverscanStartIdx, columns]);

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

function getSpecifiedWidth<R, SR>(
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export { default as TextEditor } from './editors/TextEditor';
export { default as SortableHeaderCell } from './headerCells/SortableHeaderCell';
export type {
Column,
ParentColumn,
CalculatedColumn,
FormatterProps,
SummaryFormatterProps,
Expand Down
25 changes: 25 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@ import type { ReactElement } from 'react';

export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

export interface ParentColumn<TRow, TSummaryRow = unknown> {
/** The name of the column. By default it will be displayed in the header cell */
name: string | ReactElement;
/** A unique key to distinguish each column */
key: string;
/** Determines whether a column and all of its children are frozen or not */
frozen?: boolean;
/** Header renderer for each header cell */
parentHeaderRenderer?: React.ComponentType<ParentHeaderRendererProps<TRow, TSummaryRow>>;
/** Child columns grouped underneath the parent */
children: Omit<Column<TRow, TSummaryRow>, 'frozen'>[];
parentHeaderCellClass?: string;
}

export interface Column<TRow, TSummaryRow = unknown> {
/** The name of the column. By default it will be displayed in the header cell */
name: string | ReactElement;
Expand Down Expand Up @@ -55,6 +69,13 @@ export interface Column<TRow, TSummaryRow = unknown> {
filterRenderer?: React.ComponentType<FilterRendererProps<TRow, any, TSummaryRow>>;
}

export interface CalculatedParentColumn<TRow, TSummaryRow = unknown> extends ParentColumn<TRow, TSummaryRow> {
idx: number;
frozen: boolean;
isLastFrozenColumn: boolean;
span: number;
}

export interface CalculatedColumn<TRow, TSummaryRow = unknown> extends Column<TRow, TSummaryRow> {
idx: number;
resizable: boolean;
Expand Down Expand Up @@ -125,6 +146,10 @@ export interface HeaderRendererProps<TRow, TSummaryRow = unknown> {
onAllRowsSelectionChange: (checked: boolean) => void;
}

export interface ParentHeaderRendererProps<TRow, TSummaryRow = unknown> {
column: CalculatedParentColumn<TRow, TSummaryRow>;
}

interface SelectedCellPropsBase {
idx: number;
onKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void;
Expand Down
Loading