From 181a1e68e447a2e4e690cfa3d2ccb508b8c55157 Mon Sep 17 00:00:00 2001 From: Aleksey Potsetsuev Date: Mon, 17 Apr 2023 14:48:14 +0300 Subject: [PATCH 1/7] Feat cell renderer --- src/Cell.tsx | 34 ++++++++++++++++++++++++++++------ src/DataGrid.tsx | 7 +++++-- src/Row.tsx | 41 +++++++++++++++++++++-------------------- src/index.ts | 1 + src/types.ts | 1 + 5 files changed, 56 insertions(+), 28 deletions(-) diff --git a/src/Cell.tsx b/src/Cell.tsx index 698ce52b72..8379e4d36a 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import { RefAttributes, forwardRef, memo } from 'react'; import { css } from '@linaria/core'; import { useRovingCellRef } from './hooks'; @@ -35,23 +35,37 @@ function Cell({ rowIdx, dragHandle, skipCellFocusRef, + className, onClick, onDoubleClick, onContextMenu, onRowChange, selectCell, ...props -}: CellRendererProps) { +}: CellRendererProps, +refComponent: React.Ref) { const { ref, tabIndex, onFocus } = useRovingCellRef(isCellSelected, skipCellFocusRef); + function setRef(element: HTMLDivElement | null) { + ref?.(element); + + if (typeof refComponent === 'function') { + refComponent(element); + } else if (typeof refComponent === 'object' && refComponent !== null) { + //@ts-expect-error ref mutation + refComponent.current = element; + } + } + 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 = isCellEditable(column, row); @@ -97,7 +111,7 @@ function Cell({ aria-selected={isCellSelected} aria-colspan={colSpan} aria-readonly={!isEditable || undefined} - ref={ref} + ref={setRef} tabIndex={tabIndex} className={className} style={getCellStyle(column, colSpan)} @@ -123,4 +137,12 @@ function Cell({ ); } -export default memo(Cell) as (props: CellRendererProps) => JSX.Element; +const CellComponent = memo(forwardRef(Cell)) as ( + props: CellRendererProps & RefAttributes +) => JSX.Element; + +export default CellComponent; + +export function defaultCellRenderer(key: React.Key, props: CellRendererProps) { + return ; +} diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 90624b3328..a1563c3361 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -58,6 +58,7 @@ import EditCell from './EditCell'; import GroupRowRenderer from './GroupRow'; import HeaderRow from './HeaderRow'; import { defaultRowRenderer } from './Row'; +import { defaultCellRenderer } from './Cell'; import SummaryRow from './SummaryRow'; import { checkboxFormatter as defaultCheckboxFormatter } from './formatters'; import { default as defaultSortStatus } from './sortStatus'; @@ -251,6 +252,7 @@ function DataGrid( const headerRowHeight = rawHeaderRowHeight ?? (typeof rowHeight === 'number' ? rowHeight : 35); const summaryRowHeight = rawSummaryRowHeight ?? (typeof rowHeight === 'number' ? rowHeight : 35); const rowRenderer = renderers?.rowRenderer ?? defaultRenderers?.rowRenderer ?? defaultRowRenderer; + const cellRenderer = renderers?.cellRenderer ?? defaultRenderers?.cellRenderer ?? defaultCellRenderer; const sortStatus = renderers?.sortStatus ?? defaultRenderers?.sortStatus ?? defaultSortStatus; const checkboxFormatter = renderers?.checkboxFormatter ?? defaultRenderers?.checkboxFormatter ?? defaultCheckboxFormatter; @@ -300,9 +302,10 @@ function DataGrid( const defaultGridComponents = useMemo( () => ({ sortStatus, - checkboxFormatter + checkboxFormatter, + cellRenderer }), - [sortStatus, checkboxFormatter] + [sortStatus, checkboxFormatter, cellRenderer] ); const allRowsSelected = useMemo((): boolean => { diff --git a/src/Row.tsx b/src/Row.tsx index f6163dfe98..21e5608fd3 100644 --- a/src/Row.tsx +++ b/src/Row.tsx @@ -4,8 +4,9 @@ import clsx from 'clsx'; import { RowSelectionProvider, useLatestFunc } from './hooks'; import { getColSpan, getRowStyle } from './utils'; import type { CalculatedColumn, RowRendererProps } from './types'; -import Cell from './Cell'; +import { defaultCellRenderer } from './Cell'; import { rowClassname, rowSelectedClassname } from './style'; +import { useDefaultRenderers } from './DataGridDefaultRenderersProvider'; function Row( { @@ -35,6 +36,9 @@ function Row( }: RowRendererProps, ref: React.Ref ) { + const defaultComponents = useDefaultRenderers(); + const cellRenderer = defaultComponents?.cellRenderer ?? defaultCellRenderer; + const handleRowChange = useLatestFunc((column: CalculatedColumn, newRow: R) => { onRowChange(column, rowIdx, newRow); }); @@ -69,25 +73,22 @@ function Row( if (isCellSelected && selectedCellEditor) { cells.push(selectedCellEditor); } else { - cells.push( - - ); + cells.push(cellRenderer(column.key, { + column, + colSpan, + row, + rowIdx, + isCopied: copiedCellIdx === idx, + isDraggedOver: draggedOverCellIdx === idx, + isCellSelected: isCellSelected, + dragHandle: isCellSelected ? selectedCellDragHandle : undefined, + onClick: onCellClick, + onDoubleClick: onCellDoubleClick, + onContextMenu: onCellContextMenu, + onRowChange: handleRowChange, + selectCell: selectCell, + skipCellFocusRef: skipCellFocusRef + })); } } diff --git a/src/index.ts b/src/index.ts index b5eced9327..05816ed578 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export { default, type DataGridProps, type DataGridHandle } from './DataGrid'; export { DataGridDefaultRenderersProvider } from './DataGridDefaultRenderersProvider'; export { default as Row } from './Row'; +export { default as Cell } from './Cell'; export * from './Columns'; export * from './formatters'; export { default as textEditor } from './editors/textEditor'; diff --git a/src/types.ts b/src/types.ts index 79780150e1..9abd837f00 100644 --- a/src/types.ts +++ b/src/types.ts @@ -268,6 +268,7 @@ export interface Renderers { (props: CheckboxFormatterProps, ref: RefObject) => ReactNode >; rowRenderer?: Maybe<(key: Key, props: RowRendererProps) => ReactNode>; + cellRenderer?: Maybe<(key: Key, props: CellRendererProps) => ReactNode>; noRowsFallback?: Maybe; } From dcad4dabc674c092785fbeac12387f03a786da46 Mon Sep 17 00:00:00 2001 From: Aleksey Potsetsuev Date: Tue, 27 Jun 2023 12:36:50 +0300 Subject: [PATCH 2/7] chore: rename cellRenderer -> renderCell --- src/Cell.tsx | 4 ++-- src/DataGrid.tsx | 13 ++++++------- src/Row.tsx | 4 ++-- src/types.ts | 2 +- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/Cell.tsx b/src/Cell.tsx index 3092878346..00ae94f10e 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -1,4 +1,4 @@ -import { RefAttributes, forwardRef, memo } from 'react'; +import { forwardRef, memo, RefAttributes } from 'react'; import { css } from '@linaria/core'; import { useRovingTabIndex } from './hooks'; @@ -129,6 +129,6 @@ const CellComponent = memo(forwardRef(Cell)) as ( export default CellComponent; -export function defaultCellRenderer(key: React.Key, props: CellRendererProps) { +export function defaultRenderCell(key: React.Key, props: CellRendererProps) { return ; } diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 132e5115b3..3ae5702c5b 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -48,7 +48,8 @@ import type { SelectRowEvent, SortColumn } from './types'; -import { defaultCellRenderer } from './Cell'; +import { defaultRenderCell } from './Cell'; +import { renderCheckbox as defaultRenderCheckbox } from './cellRenderers'; import { DataGridDefaultRenderersProvider, useDefaultRenderers @@ -59,8 +60,6 @@ import HeaderRow from './HeaderRow'; import { defaultRenderRow } from './Row'; import type { PartialPosition } from './ScrollToCell'; import ScrollToCell from './ScrollToCell'; -import SummaryRow from './SummaryRow'; -import { renderCheckbox as defaultRenderCheckbox } from './cellRenderers'; import { default as defaultRenderSortStatus } from './sortStatus'; import { focusSinkClassname, @@ -69,6 +68,7 @@ import { viewportDraggingClassname } from './style/core'; import { rowSelected, rowSelectedWithFrozenCell } from './style/row'; +import SummaryRow from './SummaryRow'; export interface SelectCellState extends Position { readonly mode: 'SELECT'; @@ -252,8 +252,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 cellRenderer = - renderers?.cellRenderer ?? defaultRenderers?.cellRenderer ?? defaultCellRenderer; + const renderCell = renderers?.renderCell ?? defaultRenderers?.renderCell ?? defaultRenderCell; const renderSortStatus = renderers?.renderSortStatus ?? defaultRenderers?.renderSortStatus ?? defaultRenderSortStatus; const renderCheckbox = @@ -314,9 +313,9 @@ function DataGrid( () => ({ renderCheckbox, renderSortStatus, - cellRenderer + renderCell }), - [renderCheckbox, renderSortStatus, cellRenderer] + [renderCheckbox, renderSortStatus, renderCell] ); const allRowsSelected = useMemo((): boolean => { diff --git a/src/Row.tsx b/src/Row.tsx index 7e05fbd3f3..5ebcca32b7 100644 --- a/src/Row.tsx +++ b/src/Row.tsx @@ -4,7 +4,7 @@ import clsx from 'clsx'; import { RowSelectionProvider, useLatestFunc } from './hooks'; import { getColSpan, getRowStyle } from './utils'; import type { CalculatedColumn, RenderRowProps } from './types'; -import { defaultCellRenderer } from './Cell'; +import { defaultRenderCell } from './Cell'; import { useDefaultRenderers } from './DataGridDefaultRenderersProvider'; import { rowClassname, rowSelectedClassname } from './style/row'; @@ -36,7 +36,7 @@ function Row( ref: React.Ref ) { const defaultComponents = useDefaultRenderers(); - const cellRenderer = defaultComponents?.cellRenderer ?? defaultCellRenderer; + const cellRenderer = defaultComponents?.renderCell ?? defaultRenderCell; const handleRowChange = useLatestFunc((column: CalculatedColumn, newRow: R) => { onRowChange(column, rowIdx, newRow); diff --git a/src/types.ts b/src/types.ts index 928ce53db7..ea535131f1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -277,7 +277,7 @@ export interface Renderers { renderCheckbox?: Maybe<(props: RenderCheckboxProps) => ReactNode>; renderRow?: Maybe<(key: Key, props: RenderRowProps) => ReactNode>; renderSortStatus?: Maybe<(props: RenderSortStatusProps) => ReactNode>; - cellRenderer?: Maybe<(key: Key, props: CellRendererProps) => ReactNode>; + renderCell?: Maybe<(key: Key, props: CellRendererProps) => ReactNode>; noRowsFallback?: Maybe; } From 333d30f30f733b9ed2618dfd0fee5d01a4d637a4 Mon Sep 17 00:00:00 2001 From: Aleksey Potsetsuev Date: Mon, 11 Sep 2023 13:08:26 +0200 Subject: [PATCH 3/7] chore: codestyle --- src/Cell.tsx | 2 +- src/Row.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Cell.tsx b/src/Cell.tsx index b0996afc77..62dc8bf8ea 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -1,4 +1,4 @@ -import { forwardRef, memo, RefAttributes } from 'react'; +import { forwardRef, memo, type RefAttributes } from 'react'; import { css } from '@linaria/core'; import { useRovingTabIndex } from './hooks'; diff --git a/src/Row.tsx b/src/Row.tsx index fbb41e9f70..ac0d24bc09 100644 --- a/src/Row.tsx +++ b/src/Row.tsx @@ -79,12 +79,12 @@ function Row( rowIdx, isCopied: copiedCellIdx === idx, isDraggedOver: draggedOverCellIdx === idx, - isCellSelected: isCellSelected, + isCellSelected, onClick: onCellClick, onDoubleClick: onCellDoubleClick, onContextMenu: onCellContextMenu, onRowChange: handleRowChange, - selectCell: selectCell + selectCell }) ); } From 3a7fec4bbbf7b01b435b44c2234546d1694be417 Mon Sep 17 00:00:00 2001 From: Aleksey Potsetsuev Date: Tue, 12 Sep 2023 17:37:54 +0200 Subject: [PATCH 4/7] chore: add renderCell tests and readme --- README.md | 1 + test/renderers.test.tsx | 52 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index abc2e50cd6..d618cdc3b0 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,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/test/renderers.test.tsx b/test/renderers.test.tsx index 7226cd578b..53bab7ca7c 100644 --- a/test/renderers.test.tsx +++ b/test/renderers.test.tsx @@ -3,11 +3,19 @@ import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import DataGrid, { DataGridDefaultRenderersProvider, renderSortIcon, SelectColumn } from '../src'; -import type { Column, DataGridProps, RenderSortStatusProps, SortColumn } from '../src'; -import { getHeaderCells, getRows, render, setup } from './utils'; +import type { + CellRendererProps, + Column, + DataGridProps, + RenderSortStatusProps, + SortColumn +} from '../src'; +import { getCells, getHeaderCells, getRows, render, setup } from './utils'; interface Row { id: number; + col1?: string; + col2?: string; } const columns: readonly Column[] = [ @@ -24,6 +32,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
; } @@ -70,7 +94,8 @@ function setupProvider(props: DataGridProps, renderCheckbox: globalRenderCheckbox, - renderSortStatus: globalSortStatus + renderSortStatus: globalSortStatus, + renderCell: globalCellRenderer }} > @@ -174,3 +199,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 ans 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'); +}); From 434132917064dc64d0c57ef3a56adcb6563509df Mon Sep 17 00:00:00 2001 From: Aleksey Potsetsuev Date: Tue, 12 Sep 2023 17:42:04 +0200 Subject: [PATCH 5/7] chore: pr review fixes --- src/Cell.tsx | 4 ++-- src/Row.tsx | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Cell.tsx b/src/Cell.tsx index 62dc8bf8ea..f2e5dba82e 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -42,7 +42,7 @@ function Cell( selectCell, ...props }: CellRendererProps, - refComponent: React.Ref + ref: React.Ref ) { const { tabIndex, childTabIndex, onFocus } = useRovingTabIndex(isCellSelected); @@ -100,7 +100,7 @@ function Cell( aria-colspan={colSpan} aria-selected={isCellSelected} aria-readonly={!isEditable || undefined} - ref={refComponent} + ref={ref} tabIndex={tabIndex} className={className} style={getCellStyle(column, colSpan)} diff --git a/src/Row.tsx b/src/Row.tsx index ac0d24bc09..f1f7a81992 100644 --- a/src/Row.tsx +++ b/src/Row.tsx @@ -4,7 +4,6 @@ import clsx from 'clsx'; import { RowSelectionProvider, useLatestFunc } from './hooks'; import { getColSpan, getRowStyle } from './utils'; import type { CalculatedColumn, RenderRowProps } from './types'; -import { defaultRenderCell } from './Cell'; import { useDefaultRenderers } from './DataGridDefaultRenderersProvider'; import { rowClassname, rowSelectedClassname } from './style/row'; @@ -34,8 +33,7 @@ function Row( }: RenderRowProps, ref: React.Ref ) { - const defaultComponents = useDefaultRenderers(); - const cellRenderer = defaultComponents?.renderCell ?? defaultRenderCell; + const renderCell = useDefaultRenderers()!.renderCell!; const handleRowChange = useLatestFunc((column: CalculatedColumn, newRow: R) => { onRowChange(column, rowIdx, newRow); @@ -72,7 +70,7 @@ function Row( cells.push(selectedCellEditor); } else { cells.push( - cellRenderer(column.key, { + renderCell(column.key, { column, colSpan, row, From 6c826552bdbb5cdb2881c12f3cc87ddc437ff239 Mon Sep 17 00:00:00 2001 From: Aleksey Potsetsuev Date: Sun, 17 Sep 2023 23:53:51 +0200 Subject: [PATCH 6/7] fix typo --- test/renderers.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/renderers.test.tsx b/test/renderers.test.tsx index 53bab7ca7c..26795216a0 100644 --- a/test/renderers.test.tsx +++ b/test/renderers.test.tsx @@ -208,7 +208,7 @@ test('renderCell defined using provider', () => { expect(cell2).toHaveTextContent('col 2 value'); }); -test('renderCell defined using both providers ans renderers', () => { +test('renderCell defined using both providers and renderers', () => { setupProvider({ columns, rows: [{ id: 1, col1: 'col 1 value', col2: 'col 2 value' }], From 025ab95f1f622faa3c9f18c2adc647c6b381613e Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Mon, 7 Oct 2024 19:26:43 +0200 Subject: [PATCH 7/7] Refactor setupProvider function signature in renderers.test.tsx --- test/browser/renderers.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/browser/renderers.test.tsx b/test/browser/renderers.test.tsx index 391756c4dc..5ec9b8be18 100644 --- a/test/browser/renderers.test.tsx +++ b/test/browser/renderers.test.tsx @@ -94,7 +94,7 @@ function TestGrid(props: DataGridProps) { return ; } -function setupProvider(props: DataGridProps) { +function setupProvider(props: DataGridProps) { return render(