diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 536fdc43e1..cf7da46374 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -76,7 +76,7 @@ import { viewportDraggingClassname, frozenColumnShadowTopClassname } from './style/core'; -import SummaryRow from './SummaryRow'; +import { defaultRenderSummaryRow } from './SummaryRow'; export type DefaultColumnOptions = Pick< Column, @@ -294,6 +294,8 @@ export function DataGrid(props: DataGridPr const headerRowHeight = rawHeaderRowHeight ?? (typeof rowHeight === 'number' ? rowHeight : 35); const summaryRowHeight = rawSummaryRowHeight ?? (typeof rowHeight === 'number' ? rowHeight : 35); const renderRow = renderers?.renderRow ?? defaultRenderers?.renderRow ?? defaultRenderRow; + const renderSummaryRow = + renderers?.renderSummaryRow ?? defaultRenderers?.renderSummaryRow ?? defaultRenderSummaryRow; const renderCell = renderers?.renderCell ?? defaultRenderers?.renderCell ?? defaultRenderCell; const renderSortStatus = renderers?.renderSortStatus ?? defaultRenderers?.renderSortStatus ?? defaultRenderSortStatus; @@ -1081,8 +1083,7 @@ export function DataGrid(props: DataGridPr draggedOverCellIdx: getDraggedOverCellIdx(rowIdx), onRowChange: handleFormatterRowChangeLatest, setActivePosition: setPositionLatest, - activeCellEditor: getCellEditor(rowIdx), - isTreeGrid + activeCellEditor: getCellEditor(rowIdx) }); }) .toArray(); @@ -1181,22 +1182,18 @@ export function DataGrid(props: DataGridPr const isSummaryRowActive = activePosition.rowIdx === summaryRowIdx; const top = headerRowsHeight + summaryRowHeight * rowIdx; - return ( - - ); + return renderSummaryRow(rowIdx, { + 'aria-rowindex': gridRowStart, + rowIdx: summaryRowIdx, + gridRowStart, + row, + top, + bottom: undefined, + iterateOverViewportColumnsForRow, + activeCellIdx: isSummaryRowActive ? activePosition.idx : undefined, + isTop: true, + setActivePosition: setPositionLatest + }); })} {getViewportRows()} @@ -1214,22 +1211,18 @@ export function DataGrid(props: DataGridPr ? summaryRowHeight * (bottomSummaryRowsCount - 1 - rowIdx) : undefined; - return ( - - ); + return renderSummaryRow(rowIdx, { + 'aria-rowindex': ariaRowCount - bottomSummaryRowsCount + rowIdx + 1, + rowIdx: summaryRowIdx, + gridRowStart, + row, + top, + bottom, + iterateOverViewportColumnsForRow, + activeCellIdx: isSummaryRowActive ? activePosition.idx : undefined, + isTop: false, + setActivePosition: setPositionLatest + }); })} )} diff --git a/src/GroupRow.tsx b/src/GroupRow.tsx index 6c02310b47..77f449fa3b 100644 --- a/src/GroupRow.tsx +++ b/src/GroupRow.tsx @@ -7,7 +7,7 @@ import type { BaseRenderRowProps, GroupRow, Omit } from './types'; import { SELECT_COLUMN_KEY } from './Columns'; import GroupCell from './GroupCell'; import { cell, cellFrozen } from './style/cell'; -import { rowClassname, rowActiveClassname } from './style/row'; +import { rowClassname } from './style/row'; const groupRow = css` @layer rdg.GroupedRow { @@ -46,8 +46,6 @@ function GroupedRow({ toggleGroup, ...props }: GroupRowRendererProps) { - const isPositionOnRow = activeCellIdx === -1; - let idx = row.level; function handleSelectGroup() { @@ -67,12 +65,10 @@ function GroupedRow({ aria-setsize={row.setSize} aria-posinset={row.posInSet + 1} // aria-posinset is 1-based aria-expanded={row.isExpanded} - tabIndex={isPositionOnRow ? 0 : -1} className={classnames( rowClassname, groupRowClassname, `rdg-row-${rowIdx % 2 === 0 ? 'even' : 'odd'}`, - isPositionOnRow && rowActiveClassname, className )} onMouseDown={handleSelectGroup} diff --git a/src/HeaderRow.tsx b/src/HeaderRow.tsx index 8c0f140706..e7f63a6b45 100644 --- a/src/HeaderRow.tsx +++ b/src/HeaderRow.tsx @@ -13,7 +13,6 @@ import type { import type { DataGridProps } from './DataGrid'; import HeaderCell from './HeaderCell'; import { cell, cellFrozen } from './style/cell'; -import { rowActiveClassname } from './style/row'; type SharedDataGridProps = Pick< DataGridProps, @@ -67,7 +66,6 @@ function HeaderRow({ direction }: HeaderRowProps) { const [draggedColumnKey, setDraggedColumnKey] = useState(); - const isPositionOnRow = activeCellIdx === -1; const cells = iterateOverViewportColumnsForRow(activeCellIdx, { type: 'HEADER' }) .map(([column, isCellActive, colSpan], index) => ( @@ -95,11 +93,7 @@ function HeaderRow({
{cells}
diff --git a/src/Row.tsx b/src/Row.tsx index 2638890fa2..f8a75511bb 100644 --- a/src/Row.tsx +++ b/src/Row.tsx @@ -4,7 +4,7 @@ import { RowSelectionContext, type RowSelectionContextValue } from './hooks'; import { classnames } from './utils'; import type { RenderRowProps } from './types'; import { useDefaultRenderers } from './DataGridDefaultRenderersContext'; -import { rowClassname, rowActiveClassname } from './style/row'; +import { rowClassname } from './style/row'; function Row({ className, @@ -17,7 +17,6 @@ function Row({ row, iterateOverViewportColumnsForRow, activeCellEditor, - isTreeGrid, onCellMouseDown, onCellClick, onCellDoubleClick, @@ -30,12 +29,9 @@ function Row({ }: RenderRowProps) { const renderCell = useDefaultRenderers()!.renderCell!; - const isPositionOnRow = activeCellIdx === -1; - className = classnames( rowClassname, `rdg-row-${rowIdx % 2 === 0 ? 'even' : 'odd'}`, - isPositionOnRow && rowActiveClassname, rowClass?.(row, rowIdx), className ); @@ -72,7 +68,6 @@ function Row({
= Pick< - RenderRowProps, - | 'iterateOverViewportColumnsForRow' - | 'rowIdx' - | 'gridRowStart' - | 'setActivePosition' - | 'activeCellIdx' - | 'isTreeGrid' ->; - -interface SummaryRowProps extends SharedRenderRowProps { - 'aria-rowindex': number; - row: SR; - top: number | undefined; - bottom: number | undefined; - isTop: boolean; -} - const summaryRow = css` @layer rdg.SummaryRow { position: sticky; @@ -39,6 +16,8 @@ const summaryRow = css` const summaryRowClassname = `rdg-summary-row ${summaryRow}`; function SummaryRow({ + tabIndex, + className, rowIdx, gridRowStart, row, @@ -48,11 +27,8 @@ function SummaryRow({ top, bottom, isTop, - isTreeGrid, 'aria-rowindex': ariaRowIndex -}: SummaryRowProps) { - const isPositionOnRow = activeCellIdx === -1; - +}: RenderSummaryRowProps) { const cells = iterateOverViewportColumnsForRow(activeCellIdx, { type: 'SUMMARY', row }) .map(([column, isCellActive, colSpan]) => ( @@ -71,13 +47,13 @@ function SummaryRow({
({ ); } -export default memo(SummaryRow) as (props: SummaryRowProps) => React.JSX.Element; +const SummaryRowComponent = memo(SummaryRow) as ( + props: RenderSummaryRowProps +) => React.JSX.Element; + +export default SummaryRowComponent; + +export function defaultRenderSummaryRow( + key: React.Key, + props: RenderSummaryRowProps +) { + return ; +} diff --git a/src/TreeDataGrid.tsx b/src/TreeDataGrid.tsx index 37c366b4fa..0ddf82e56c 100644 --- a/src/TreeDataGrid.tsx +++ b/src/TreeDataGrid.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo } from 'react'; import type { Key } from 'react'; import { useLatestFunc } from './hooks'; -import { assertIsValidKeyGetter, getLeftRightKey } from './utils'; +import { assertIsValidKeyGetter, classnames, getLeftRightKey } from './utils'; import type { CellClipboardEvent, CellCopyArgs, @@ -14,6 +14,7 @@ import type { Maybe, Omit, RenderRowProps, + RenderSummaryRowProps, RowHeightArgs, RowsChangeData } from './types'; @@ -24,6 +25,8 @@ import type { DataGridProps } from './DataGrid'; import { useDefaultRenderers } from './DataGridDefaultRenderersContext'; import GroupedRow from './GroupRow'; import { defaultRenderRow } from './Row'; +import { rowFocusable, rowActiveClassname } from './style/row'; +import SummaryRowComponent from './SummaryRow'; export interface TreeDataGridProps extends Omit< DataGridProps, @@ -379,11 +382,15 @@ export function TreeDataGrid({ onRowChange, draggedOverCellIdx, activeCellEditor, + className, isRowSelectionDisabled, - isTreeGrid, ...rowProps }: RenderRowProps ) { + const isPositionOnRow = rowProps.activeCellIdx === -1; + const tabIndex = isPositionOnRow ? 0 : -1; + className = classnames(className, rowFocusable, isPositionOnRow && rowActiveClassname); + if (isGroupRow(row)) { const { startRowIndex } = row; return ( @@ -391,6 +398,8 @@ export function TreeDataGrid({ key={key} {...rowProps} aria-rowindex={headerAndTopSummaryRowsCount + startRowIndex + 1} + className={className} + tabIndex={tabIndex} row={row} groupBy={groupBy} toggleGroup={toggleGroupLatest} @@ -411,6 +420,8 @@ export function TreeDataGrid({ 'aria-rowindex': ariaRowIndex, row, rowClass, + className, + tabIndex, onCellMouseDown, onCellClick, onCellDoubleClick, @@ -418,8 +429,7 @@ export function TreeDataGrid({ onRowChange, draggedOverCellIdx, activeCellEditor, - isRowSelectionDisabled, - isTreeGrid + isRowSelectionDisabled }); } @@ -442,7 +452,8 @@ export function TreeDataGrid({ onCellPaste={rawOnCellPaste ? handleCellPaste : undefined} renderers={{ ...renderers, - renderRow + renderRow, + renderSummaryRow }} /> ); @@ -452,6 +463,25 @@ function defaultGroupIdGetter(groupKey: string, parentId: string | undefined) { return parentId !== undefined ? `${parentId}__${groupKey}` : groupKey; } +function renderSummaryRow( + key: React.Key, + { activeCellIdx, className, ...props }: RenderSummaryRowProps +) { + const isPositionOnRow = activeCellIdx === -1; + const tabIndex = isPositionOnRow ? 0 : -1; + className = classnames(className, rowFocusable, isPositionOnRow && rowActiveClassname); + + return ( + + ); +} + function isReadonlyArray(arr: unknown): arr is readonly unknown[] { return Array.isArray(arr); } diff --git a/src/index.ts b/src/index.ts index d965cdf85f..ac7b114aed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ export { TreeDataGrid, type TreeDataGridProps } from './TreeDataGrid'; export { DataGridDefaultRenderersContext } from './DataGridDefaultRenderersContext'; export { default as Row } from './Row'; export { default as Cell } from './Cell'; +export { default as SummaryRow } from './SummaryRow'; export * from './Columns'; export * from './cellRenderers'; export { renderTextEditor } from './editors/renderTextEditor'; @@ -47,6 +48,7 @@ export type { RenderSortPriorityProps, RenderSortStatusProps, RenderSummaryCellProps, + RenderSummaryRowProps, RowHeightArgs, RowsChangeData, SelectHeaderRowEvent, diff --git a/src/style/row.ts b/src/style/row.ts index a7feeb7743..0d7d2d929b 100644 --- a/src/style/row.ts +++ b/src/style/row.ts @@ -13,6 +13,18 @@ export const row = css` background-color: var(--rdg-row-hover-background-color); } + &[aria-selected='true'] { + background-color: var(--rdg-row-selected-background-color); + + &:hover { + background-color: var(--rdg-row-selected-hover-background-color); + } + } + } +`; + +export const rowFocusable = css` + @layer rdg.Row { &:focus { outline: none; } @@ -36,14 +48,6 @@ export const row = css` border-inline-start: var(--rdg-selection-width) solid var(--rdg-selection-color); } } - - &[aria-selected='true'] { - background-color: var(--rdg-row-selected-background-color); - - &:hover { - background-color: var(--rdg-row-selected-hover-background-color); - } - } } `; diff --git a/src/types.ts b/src/types.ts index a981e4d915..6195c83005 100644 --- a/src/types.ts +++ b/src/types.ts @@ -281,7 +281,23 @@ export interface RenderRowProps extends BaseRenderR activeCellEditor: ReactElement> | undefined; onRowChange: (column: CalculatedColumn, rowIdx: number, newRow: TRow) => void; rowClass: Maybe<(row: TRow, rowIdx: number) => Maybe>; - isTreeGrid: boolean; +} + +export interface RenderSummaryRowProps extends Pick< + BaseRenderRowProps, + | 'className' + | 'tabIndex' + | 'iterateOverViewportColumnsForRow' + | 'rowIdx' + | 'gridRowStart' + | 'setActivePosition' + | 'activeCellIdx' +> { + 'aria-rowindex': number; + row: TSummaryRow; + top: number | undefined; + bottom: number | undefined; + isTop: boolean; } export interface RowsChangeData { @@ -364,6 +380,9 @@ export interface Renderers { renderCell?: Maybe<(key: Key, props: CellRendererProps) => ReactNode>; renderCheckbox?: Maybe<(props: RenderCheckboxProps) => ReactNode>; renderRow?: Maybe<(key: Key, props: RenderRowProps) => ReactNode>; + renderSummaryRow?: Maybe< + (key: Key, props: RenderSummaryRowProps) => ReactNode + >; renderSortStatus?: Maybe<(props: RenderSortStatusProps) => ReactNode>; noRowsFallback?: Maybe; } diff --git a/test/browser/renderers.test.tsx b/test/browser/renderers.test.tsx index 54059ee126..3e0d4369bc 100644 --- a/test/browser/renderers.test.tsx +++ b/test/browser/renderers.test.tsx @@ -6,6 +6,7 @@ import { DataGrid, DataGridDefaultRenderersContext, Row as DefaultRow, + SummaryRow as DefaultSummaryRow, renderSortIcon, SelectColumn } from '../../src'; @@ -14,6 +15,7 @@ import type { Column, DataGridProps, RenderRowProps, + RenderSummaryRowProps, RenderSortStatusProps, SortColumn } from '../../src'; @@ -24,15 +26,17 @@ interface Row { col1: string; col2: string; } +type SummaryRow = undefined; const noRows: readonly Row[] = []; -const columns: readonly Column[] = [ +const columns: readonly Column[] = [ SelectColumn, { key: 'col1', name: 'Column1', - sortable: true + sortable: true, + renderSummaryCell: () => 'summary' }, { key: 'col2', @@ -41,22 +45,30 @@ const columns: readonly Column[] = [ } ]; -function renderGlobalCell(key: React.Key, props: CellRendererProps) { +function renderGlobalCell(key: React.Key, props: CellRendererProps) { return ; } -function renderLocalCell(key: React.Key, props: CellRendererProps) { +function renderLocalCell(key: React.Key, props: CellRendererProps) { return ; } -function renderGlobalRow(key: React.Key, props: RenderRowProps) { +function renderGlobalRow(key: React.Key, props: RenderRowProps) { return ; } -function renderLocalRow(key: React.Key, props: RenderRowProps) { +function renderLocalRow(key: React.Key, props: RenderRowProps) { return ; } +function renderGlobalSummaryRow(key: React.Key, props: RenderSummaryRowProps) { + return ; +} + +function renderLocalSummaryRow(key: React.Key, props: RenderSummaryRowProps) { + return ; +} + function NoRowsFallback() { return
Local no rows fallback
; } @@ -97,7 +109,7 @@ function TestGrid(props: DataGridProps) { return ; } -function setupContext(props: DataGridProps) { +function setupContext(props: DataGridProps) { return page.render( (props: DataGridProps renderCheckbox: renderGlobalCheckbox, renderSortStatus: renderGlobalSortStatus, renderCell: renderGlobalCell, - renderRow: renderGlobalRow + renderRow: renderGlobalRow, + renderSummaryRow: renderGlobalSummaryRow }} > @@ -114,28 +127,36 @@ function setupContext(props: DataGridProps } test('fallback defined using renderers prop with no rows', async () => { - await setup({ columns, rows: noRows, renderers: { noRowsFallback: } }); + await setup({ + columns, + rows: noRows, + renderers: { noRowsFallback: } + }); await testRowCount(0); await expect.element(page.getByText('Local no rows fallback')).toBeInTheDocument(); }); test('fallback defined using context with no rows', async () => { - await setupContext({ columns, rows: noRows }); + await setupContext({ columns, rows: noRows }); await testRowCount(0); await expect.element(page.getByText('Global no rows fallback')).toBeInTheDocument(); }); test('fallback defined using both context and renderers with no rows', async () => { - await setupContext({ columns, rows: noRows, renderers: { noRowsFallback: } }); + await setupContext({ + columns, + rows: noRows, + renderers: { noRowsFallback: } + }); await testRowCount(0); await expect.element(page.getByText('Local no rows fallback')).toBeInTheDocument(); }); test('fallback defined using renderers prop with a row', async () => { - await setup({ + await setup({ columns, rows: [{ id: 1, col1: 'value 1', col2: 'value 2' }], renderers: { noRowsFallback: } @@ -146,14 +167,17 @@ test('fallback defined using renderers prop with a row', async () => { }); test('fallback defined using context with a row', async () => { - await setupContext({ columns, rows: [{ id: 1, col1: 'value 1', col2: 'value 2' }] }); + await setupContext({ + columns, + rows: [{ id: 1, col1: 'value 1', col2: 'value 2' }] + }); await testRowCount(1); await expect.element(page.getByText('Global no rows fallback')).not.toBeInTheDocument(); }); test('fallback defined using both context and renderers with a row', async () => { - await setupContext({ + await setupContext({ columns, rows: [{ id: 1, col1: 'value 1', col2: 'value 2' }], renderers: { noRowsFallback: } @@ -165,21 +189,29 @@ test('fallback defined using both context and renderers with a row', async () => }); test('checkbox defined using renderers prop', async () => { - await setup({ columns, rows: noRows, renderers: { renderCheckbox: renderLocalCheckbox } }); + await setup({ + columns, + rows: noRows, + renderers: { renderCheckbox: renderLocalCheckbox } + }); await testRowCount(0); await expect.element(page.getByText('Local checkbox')).toBeInTheDocument(); }); test('checkbox defined using context', async () => { - await setupContext({ columns, rows: noRows }); + await setupContext({ columns, rows: noRows }); await testRowCount(0); await expect.element(page.getByText('Global checkbox')).toBeInTheDocument(); }); test('checkbox defined using both context and renderers', async () => { - await setupContext({ columns, rows: noRows, renderers: { renderCheckbox: renderLocalCheckbox } }); + await setupContext({ + columns, + rows: noRows, + renderers: { renderCheckbox: renderLocalCheckbox } + }); await testRowCount(0); await expect.element(page.getByText('Local checkbox')).toBeInTheDocument(); @@ -187,7 +219,7 @@ test('checkbox defined using both context and renderers', async () => { }); test('sortPriority defined using both contexts', async () => { - await setupContext({ columns, rows: noRows }); + await setupContext({ columns, rows: noRows }); const column1 = page.getHeaderCell({ name: 'Column1' }); const column2 = page.getHeaderCell({ name: 'Column2' }); @@ -204,7 +236,7 @@ test('sortPriority defined using both contexts', async () => { }); test('sortPriority defined using both contexts and renderers', async () => { - await setupContext({ + await setupContext({ columns, rows: noRows, renderers: { renderSortStatus: renderLocalSortStatus } @@ -225,7 +257,10 @@ test('sortPriority defined using both contexts and renderers', async () => { }); test('renderCell defined using context', async () => { - await setupContext({ columns, rows: [{ id: 1, col1: 'value 1', col2: 'value 2' }] }); + await setupContext({ + columns, + rows: [{ id: 1, col1: 'value 1', col2: 'value 2' }] + }); const cell1 = page.getCell({ name: 'value 1' }); const cell2 = page.getCell({ name: 'value 2' }); @@ -239,7 +274,7 @@ test('renderCell defined using context', async () => { }); test('renderCell defined using both contexts and renderers', async () => { - await setupContext({ + await setupContext({ columns, rows: [{ id: 1, col1: 'value 1', col2: 'value 2' }], renderers: { renderCell: renderLocalCell } @@ -257,7 +292,10 @@ test('renderCell defined using both contexts and renderers', async () => { }); test('renderRow defined using context', async () => { - await setupContext({ columns, rows: [{ id: 1, col1: 'value 1', col2: 'value 2' }] }); + await setupContext({ + columns, + rows: [{ id: 1, col1: 'value 1', col2: 'value 2' }] + }); const row = getRowWithCell(page.getCell({ name: 'value 1' })); await expect.element(row).toHaveClass('global'); @@ -265,7 +303,7 @@ test('renderRow defined using context', async () => { }); test('renderRow defined using both contexts and renderers', async () => { - await setupContext({ + await setupContext({ columns, rows: [{ id: 1, col1: 'value 1', col2: 'value 2' }], renderers: { renderRow: renderLocalRow } @@ -275,3 +313,28 @@ test('renderRow defined using both contexts and renderers', async () => { await expect.element(row).toHaveClass('local'); await expect.element(row).not.toHaveClass('global'); }); + +test('renderSummaryRow defined using context', async () => { + await setupContext({ + columns, + rows: [{ id: 10, col1: 'value 10', col2: 'value 20' }], + topSummaryRows: [undefined] + }); + + const row = getRowWithCell(page.getCell({ name: 'summary' })); + await expect.element(row).toHaveClass('global'); + await expect.element(row).not.toHaveClass('local'); +}); + +test('renderSummaryRow defined using both contexts and renderers', async () => { + await setupContext({ + columns, + rows: [{ id: 10, col1: 'value 10', col2: 'value 20' }], + bottomSummaryRows: [undefined], + renderers: { renderSummaryRow: renderLocalSummaryRow } + }); + + const row = getRowWithCell(page.getCell({ name: 'summary' })); + await expect.element(row).toHaveClass('local'); + await expect.element(row).not.toHaveClass('global'); +});