diff --git a/README.md b/README.md index 2e20401578..4a80556fa8 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,7 @@ interface Renderers { renderCheckbox?: Maybe<(props: RenderCheckboxProps) => ReactNode>; renderRow?: Maybe<(key: Key, props: RenderRowProps) => ReactNode>; renderSortStatus?: Maybe<(props: RenderSortStatusProps) => ReactNode>; + renderCell?: Maybe<(key: Key, props: CellRendererProps) => ReactNode>; noRowsFallback?: Maybe; } ``` diff --git a/src/Cell.tsx b/src/Cell.tsx index e90e308617..67a27d00c6 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import { forwardRef, memo, type RefAttributes } from 'react'; import { css } from '@linaria/core'; import { useRovingTabIndex } from './hooks'; @@ -25,31 +25,37 @@ const cellDraggedOver = css` const cellDraggedOverClassname = `rdg-cell-dragged-over ${cellDraggedOver}`; -function Cell({ - column, - colSpan, - isCellSelected, - isCopied, - isDraggedOver, - row, - rowIdx, - onClick, - onDoubleClick, - onContextMenu, - onRowChange, - selectCell, - ...props -}: CellRendererProps) { +function Cell( + { + column, + colSpan, + isCellSelected, + isCopied, + isDraggedOver, + row, + rowIdx, + className, + onClick, + onDoubleClick, + onContextMenu, + onRowChange, + selectCell, + style, + ...props + }: CellRendererProps, + ref: React.Ref +) { const { tabIndex, childTabIndex, onFocus } = useRovingTabIndex(isCellSelected); const { cellClass } = column; - const className = getCellClassname( + className = getCellClassname( column, { [cellCopiedClassname]: isCopied, [cellDraggedOverClassname]: isDraggedOver }, - typeof cellClass === 'function' ? cellClass(row) : cellClass + typeof cellClass === 'function' ? cellClass(row) : cellClass, + className ); const isEditable = isCellEditableUtil(column, row); @@ -95,9 +101,13 @@ function Cell({ aria-colspan={colSpan} aria-selected={isCellSelected} aria-readonly={!isEditable || undefined} + ref={ref} tabIndex={tabIndex} className={className} - style={getCellStyle(column, colSpan)} + style={{ + ...getCellStyle(column, colSpan), + ...style + }} onClick={handleClick} onDoubleClick={handleDoubleClick} onContextMenu={handleContextMenu} @@ -116,4 +126,12 @@ function Cell({ ); } -export default memo(Cell) as (props: CellRendererProps) => React.JSX.Element; +const CellComponent = memo(forwardRef(Cell)) as ( + props: CellRendererProps & RefAttributes +) => React.JSX.Element; + +export default CellComponent; + +export function defaultRenderCell(key: React.Key, props: CellRendererProps) { + return ; +} diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index a16597e6fc..c5c002345d 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -52,6 +52,7 @@ import type { SelectRowEvent, SortColumn } from './types'; +import { defaultRenderCell } from './Cell'; import { renderCheckbox as defaultRenderCheckbox } from './cellRenderers'; import { DataGridDefaultRenderersProvider, @@ -273,6 +274,7 @@ function DataGrid( const headerRowHeight = rawHeaderRowHeight ?? (typeof rowHeight === 'number' ? rowHeight : 35); const summaryRowHeight = rawSummaryRowHeight ?? (typeof rowHeight === 'number' ? rowHeight : 35); const renderRow = renderers?.renderRow ?? defaultRenderers?.renderRow ?? defaultRenderRow; + const renderCell = renderers?.renderCell ?? defaultRenderers?.renderCell ?? defaultRenderCell; const renderSortStatus = renderers?.renderSortStatus ?? defaultRenderers?.renderSortStatus ?? defaultRenderSortStatus; const renderCheckbox = @@ -364,9 +366,10 @@ function DataGrid( const defaultGridComponents = useMemo( () => ({ renderCheckbox, - renderSortStatus + renderSortStatus, + renderCell }), - [renderCheckbox, renderSortStatus] + [renderCheckbox, renderSortStatus, renderCell] ); const headerSelectionValue = useMemo((): HeaderRowSelectionContextValue => { diff --git a/src/Row.tsx b/src/Row.tsx index 1e2c795001..9b811a3397 100644 --- a/src/Row.tsx +++ b/src/Row.tsx @@ -4,7 +4,7 @@ import clsx from 'clsx'; import { RowSelectionProvider, useLatestFunc, type RowSelectionContextValue } from './hooks'; import { getColSpan, getRowStyle } from './utils'; import type { CalculatedColumn, RenderRowProps } from './types'; -import Cell from './Cell'; +import { useDefaultRenderers } from './DataGridDefaultRenderersProvider'; import { rowClassname, rowSelectedClassname } from './style/row'; function Row( @@ -33,6 +33,8 @@ function Row( }: RenderRowProps, ref: React.Ref ) { + const renderCell = useDefaultRenderers()!.renderCell!; + const handleRowChange = useLatestFunc((column: CalculatedColumn, newRow: R) => { onRowChange(column, rowIdx, newRow); }); @@ -68,21 +70,20 @@ function Row( cells.push(selectedCellEditor); } else { cells.push( - + renderCell(column.key, { + column, + colSpan, + row, + rowIdx, + isCopied: copiedCellIdx === idx, + isDraggedOver: draggedOverCellIdx === idx, + isCellSelected, + onClick: onCellClick, + onDoubleClick: onCellDoubleClick, + onContextMenu: onCellContextMenu, + onRowChange: handleRowChange, + selectCell + }) ); } } diff --git a/src/index.ts b/src/index.ts index 49a6bc9511..d76283ea28 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ export { default, type DataGridProps, type DataGridHandle } from './DataGrid'; export { default as TreeDataGrid, type TreeDataGridProps } from './TreeDataGrid'; export { DataGridDefaultRenderersProvider } from './DataGridDefaultRenderersProvider'; export { default as Row } from './Row'; +export { default as Cell } from './Cell'; export * from './Columns'; export * from './cellRenderers'; export { default as textEditor } from './editors/textEditor'; diff --git a/src/types.ts b/src/types.ts index 38281c6075..8eff5abe2b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -145,7 +145,7 @@ export interface CellRendererProps extends Pick, 'row' | 'rowIdx' | 'selectCell'>, Omit< React.HTMLAttributes, - 'style' | 'children' | 'onClick' | 'onDoubleClick' | 'onContextMenu' + 'children' | 'onClick' | 'onDoubleClick' | 'onContextMenu' > { column: CalculatedColumn; colSpan: number | undefined; @@ -312,6 +312,7 @@ export interface Renderers { renderCheckbox?: Maybe<(props: RenderCheckboxProps) => ReactNode>; renderRow?: Maybe<(key: Key, props: RenderRowProps) => ReactNode>; renderSortStatus?: Maybe<(props: RenderSortStatusProps) => ReactNode>; + renderCell?: Maybe<(key: Key, props: CellRendererProps) => ReactNode>; noRowsFallback?: Maybe; } diff --git a/test/browser/renderers.test.tsx b/test/browser/renderers.test.tsx index ae520bf128..071cd5aac4 100644 --- a/test/browser/renderers.test.tsx +++ b/test/browser/renderers.test.tsx @@ -7,11 +7,19 @@ import DataGrid, { renderSortIcon, SelectColumn } from '../../src'; -import type { Column, DataGridProps, RenderSortStatusProps, SortColumn } from '../../src'; -import { getHeaderCells, getRows, setup } from './utils'; +import type { + CellRendererProps, + Column, + DataGridProps, + RenderSortStatusProps, + SortColumn +} from '../../src'; +import { getCells, getHeaderCells, getRows, setup } from './utils'; interface Row { id: number; + col1: string; + col2: string; } const noRows: readonly Row[] = []; @@ -30,6 +38,22 @@ const columns: readonly Column[] = [ } ]; +function globalCellRenderer(key: React.Key, props: CellRendererProps) { + return ( +
+ {props.row[props.column.key as keyof Row]} +
+ ); +} + +function localCellRenderer(key: React.Key) { + return ( +
+ local +
+ ); +} + function NoRowsFallback() { return
Local no rows fallback
; } @@ -76,7 +100,8 @@ function setupProvider(props: DataGridProps, renderCheckbox: globalRenderCheckbox, - renderSortStatus: globalSortStatus + renderSortStatus: globalSortStatus, + renderCell: globalCellRenderer }} > @@ -106,21 +131,29 @@ test('fallback defined using both provider and renderers with no rows', () => { }); test('fallback defined using renderers prop with a row', () => { - setup({ columns, rows: [{ id: 1 }], renderers: { noRowsFallback: } }); + setup({ + columns, + rows: [{ id: 1, col1: 'col 1 value', col2: 'col 2 value' }], + renderers: { noRowsFallback: } + }); expect(getRows()).toHaveLength(1); expect(screen.queryByText('Local no rows fallback')).not.toBeInTheDocument(); }); test('fallback defined using provider with a row', () => { - setupProvider({ columns, rows: [{ id: 1 }] }); + setupProvider({ columns, rows: [{ id: 1, col1: 'col 1 value', col2: 'col 2 value' }] }); expect(getRows()).toHaveLength(1); expect(screen.queryByText('Global no rows fallback')).not.toBeInTheDocument(); }); test('fallback defined using both provider and renderers with a row', () => { - setupProvider({ columns, rows: [{ id: 1 }], renderers: { noRowsFallback: } }); + setupProvider({ + columns, + rows: [{ id: 1, col1: 'col 1 value', col2: 'col 2 value' }], + renderers: { noRowsFallback: } + }); expect(getRows()).toHaveLength(1); expect(screen.queryByText('Global no rows fallback')).not.toBeInTheDocument(); @@ -180,3 +213,24 @@ test('sortPriority defined using both providers and renderers', async () => { expect(screen.queryByTestId('global-sort-priority')).not.toBeInTheDocument(); }); + +test('renderCell defined using provider', () => { + setupProvider({ columns, rows: [{ id: 1, col1: 'col 1 value', col2: 'col 2 value' }] }); + + const [, cell1, cell2] = getCells(); + expect(cell1).toHaveTextContent('col 1 value'); + expect(cell2).toHaveTextContent('col 2 value'); +}); + +test('renderCell defined using both providers and renderers', () => { + setupProvider({ + columns, + rows: [{ id: 1, col1: 'col 1 value', col2: 'col 2 value' }], + renderers: { renderCell: localCellRenderer } + }); + + const [selectCell, cell1, cell2] = getCells(); + expect(selectCell).toHaveTextContent('local'); + expect(cell1).toHaveTextContent('local'); + expect(cell2).toHaveTextContent('local'); +});