Skip to content

Commit 0989abd

Browse files
authored
[refactor] create CellConfigurationProvider (#468)
* [refactor] create CellConfigurationProvider * style + doc * fix docstring about stringify, and use StringifyFunction type * fix bug: if renderCellContent exists and returns undefined, use that value
1 parent 08a65fd commit 0989abd

9 files changed

Lines changed: 178 additions & 128 deletions

File tree

src/components/Cell.tsx

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
1-
import type { KeyboardEvent, MouseEvent, ReactNode } from 'react'
1+
import type { KeyboardEvent, MouseEvent } from 'react'
22
import { useCallback, useContext, useEffect, useMemo, useRef } from 'react'
33

4+
import { CellCallbacksContext, RenderCellContentContext, StringifyContext } from '../contexts/CellConfigurationContext.js'
45
import { ColumnWidthsContext } from '../contexts/ColumnWidthsContext.js'
5-
import type { ResolvedValue } from '../helpers/dataframe/index.js'
66
import { useCellFocus } from '../hooks/useCellFocus.js'
77
import { useOnCopy } from '../hooks/useOnCopyToClipboard.js'
88

9-
export interface CellContentProps {
10-
stringify: (value: unknown) => string | undefined
11-
cell?: ResolvedValue
12-
col: number
13-
row?: number // the row index in the original data, undefined if the value has not been fetched yet
14-
}
15-
169
interface Props {
1710
/** aria column index */
1811
ariaColIndex: number
@@ -22,30 +15,23 @@ interface Props {
2215
columnIndex: number
2316
/** index in the visible columns array (used for styling/widths) */
2417
visibleColumnIndex: number
25-
/** function to stringify the cell value, used for default rendering and for copy to clipboard */
26-
stringify: (value: unknown) => string | undefined
2718
/** cell value, undefined if the value has not been fetched yet, or if the value is actually undefined. Use hasResolved to distinguish these cases. */
2819
cellValue?: unknown
2920
/** whether the cell value has been resolved */
3021
hasResolved?: boolean
3122
/** class name */
3223
className?: string
33-
/** double click callback */
34-
onDoubleClickCell?: (event: MouseEvent, col: number, row: number) => void
35-
/** mouse down callback */
36-
onMouseDownCell?: (event: MouseEvent, col: number, row: number) => void
37-
/** key down callback, for accessibility, it should be passed if onDoubleClickCell is passed. It can handle more than that action though. */
38-
onKeyDownCell?: (event: KeyboardEvent, col: number, row: number) => void
3924
/** the row index in the original data, undefined if the value has not been fetched yet */
4025
rowNumber?: number
41-
/** custom cell content component, if not provided, the default stringified value will be used */
42-
renderCellContent?: (props: CellContentProps) => ReactNode
4326
}
4427

4528
/**
4629
* Render a table cell <td> with title and optional custom rendering
4730
*/
48-
export default function Cell({ cellValue, hasResolved, onDoubleClickCell, onMouseDownCell, onKeyDownCell, stringify, columnIndex, visibleColumnIndex, className, ariaColIndex, ariaRowIndex, rowNumber, renderCellContent }: Props) {
31+
export default function Cell({ cellValue, hasResolved, columnIndex, visibleColumnIndex, className, ariaColIndex, ariaRowIndex, rowNumber }: Props) {
32+
const { onDoubleClickCell, onMouseDownCell, onKeyDownCell } = useContext(CellCallbacksContext)
33+
const stringify = useContext(StringifyContext)
34+
const renderCellContent = useContext(RenderCellContentContext)
4935
const { tabIndex, navigateToCell, focusIfNeeded } = useCellFocus({ ariaColIndex, ariaRowIndex })
5036

5137
const cell = useMemo(() => {
@@ -79,7 +65,9 @@ export default function Cell({ cellValue, hasResolved, onDoubleClickCell, onMous
7965
}
8066
}, [str])
8167
const content = useMemo(() => {
82-
return renderCellContent?.({ cell, stringify, col: columnIndex, row: rowNumber }) ?? str
68+
return renderCellContent === undefined
69+
? str
70+
: renderCellContent({ cell, stringify, col: columnIndex, row: rowNumber })
8371
}, [cell, stringify, columnIndex, rowNumber, renderCellContent, str])
8472

8573
const handleMouseDown = useCallback((event: MouseEvent) => {

src/components/HighTable.tsx

Lines changed: 47 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { type ReactNode } from 'react'
22

33
import { columnWidthsSuffix } from '../helpers/constants.js'
44
import styles from '../HighTable.module.css'
5+
import { CellConfigurationProvider } from '../providers/CellConfigurationProvider.js'
56
import { CellNavigationProvider } from '../providers/CellNavigationProvider.js'
67
import { ColumnParametersProvider } from '../providers/ColumnParametersProvider.js'
78
import { ColumnsVisibilityProvider } from '../providers/ColumnsVisibilityProvider.js'
@@ -31,7 +32,7 @@ export default function HighTable({ data, ...props }: HighTableProps) {
3132
)
3233
}
3334

34-
type StateProps = Pick<HighTableProps, 'columnConfiguration' | 'cacheKey' | 'cellPosition' | 'columnsVisibility' | 'focus' | 'numRowsPerPage' | 'orderBy' | 'overscan' | 'padding' | 'selection' | 'onCellPositionChange' | 'onColumnsVisibilityChange' | 'onError' | 'onOrderByChange' | 'onSelectionChange'>
35+
type StateProps = Pick<HighTableProps, 'columnConfiguration' | 'cacheKey' | 'cellPosition' | 'columnsVisibility' | 'focus' | 'numRowsPerPage' | 'orderBy' | 'overscan' | 'padding' | 'selection' | 'onCellPositionChange' | 'onColumnsVisibilityChange' | 'onDoubleClickCell' | 'onError' | 'onKeyDownCell' | 'onMouseDownCell' | 'onOrderByChange' | 'onSelectionChange' | 'renderCellContent' | 'stringify'>
3536
& { children: ReactNode }
3637

3738
function State({
@@ -48,79 +49,81 @@ function State({
4849
selection,
4950
onCellPositionChange,
5051
onColumnsVisibilityChange,
52+
onDoubleClickCell,
5153
onError,
54+
onKeyDownCell,
55+
onMouseDownCell,
5256
onOrderByChange,
5357
onSelectionChange,
58+
renderCellContent,
59+
stringify,
5460
}: StateProps) {
5561
return (
5662
/* The state is handled with contexts, even if it creates a "Providers hell". No need for state library for now. */
5763
<ViewportSizeProvider>
5864
<TableCornerSizeProvider>
59-
<ColumnParametersProvider columnConfiguration={columnConfiguration}>
60-
<ColumnWidthsProvider
65+
<CellConfigurationProvider
66+
onDoubleClickCell={onDoubleClickCell}
67+
onKeyDownCell={onKeyDownCell}
68+
onMouseDownCell={onMouseDownCell}
69+
renderCellContent={renderCellContent}
70+
stringify={stringify}
71+
>
72+
<ColumnParametersProvider columnConfiguration={columnConfiguration}>
73+
<ColumnWidthsProvider
6174
// Recreate a context if a new cacheKey is provided.
62-
key={cacheKey}
63-
// TODO(SL): pass cacheKey, memoize
64-
localStorageKey={cacheKey ? `${cacheKey}${columnWidthsSuffix}` : undefined}
65-
>
66-
<ColumnsVisibilityProvider
67-
columnsVisibility={columnsVisibility}
68-
onColumnsVisibilityChange={onColumnsVisibilityChange}
75+
key={cacheKey}
76+
// TODO(SL): pass cacheKey, memoize
77+
localStorageKey={cacheKey ? `${cacheKey}${columnWidthsSuffix}` : undefined}
6978
>
70-
<OrderByProvider
71-
orderBy={orderBy}
72-
onOrderByChange={onOrderByChange}
79+
<ColumnsVisibilityProvider
80+
columnsVisibility={columnsVisibility}
81+
onColumnsVisibilityChange={onColumnsVisibilityChange}
7382
>
74-
<SelectionProvider
75-
selection={selection}
76-
onError={onError}
77-
onSelectionChange={onSelectionChange}
83+
<OrderByProvider
84+
orderBy={orderBy}
85+
onOrderByChange={onOrderByChange}
7886
>
79-
<CellNavigationProvider
80-
cellPosition={cellPosition}
81-
focus={focus}
82-
numRowsPerPage={numRowsPerPage}
83-
onCellPositionChange={onCellPositionChange}
87+
<SelectionProvider
88+
selection={selection}
89+
onError={onError}
90+
onSelectionChange={onSelectionChange}
8491
>
85-
<ScrollProvider padding={padding} onError={onError} overscan={overscan}>
86-
{children}
87-
</ScrollProvider>
88-
</CellNavigationProvider>
89-
</SelectionProvider>
90-
</OrderByProvider>
91-
</ColumnsVisibilityProvider>
92-
</ColumnWidthsProvider>
93-
</ColumnParametersProvider>
92+
<CellNavigationProvider
93+
cellPosition={cellPosition}
94+
focus={focus}
95+
numRowsPerPage={numRowsPerPage}
96+
onCellPositionChange={onCellPositionChange}
97+
>
98+
<ScrollProvider padding={padding} onError={onError} overscan={overscan}>
99+
{children}
100+
</ScrollProvider>
101+
</CellNavigationProvider>
102+
</SelectionProvider>
103+
</OrderByProvider>
104+
</ColumnsVisibilityProvider>
105+
</ColumnWidthsProvider>
106+
</ColumnParametersProvider>
107+
</CellConfigurationProvider>
94108
</TableCornerSizeProvider>
95109
</ViewportSizeProvider>
96110
)
97111
}
98112

99-
type DOMProps = Pick<HighTableProps, 'className' | 'maxRowNumber' | 'styled' | 'onDoubleClickCell' | 'onKeyDownCell' | 'onMouseDownCell' | 'renderCellContent' | 'stringify'>
113+
type DOMProps = Pick<HighTableProps, 'className' | 'maxRowNumber' | 'styled'>
100114

101115
function DOM({
102116
className = '',
103117
maxRowNumber,
104118
styled = true,
105-
onDoubleClickCell,
106-
onKeyDownCell,
107-
onMouseDownCell,
108-
renderCellContent,
109-
stringify,
110119
}: DOMProps) {
111120
return (
112121
<Wrapper styled={styled} maxRowNumber={maxRowNumber} className={className}>
113122
<div className={styles.topBorder} role="presentation" />
114123

115124
<Scroller>
116125
<Slice>
117-
<Table
118-
onDoubleClickCell={onDoubleClickCell}
119-
onKeyDownCell={onKeyDownCell}
120-
onMouseDownCell={onMouseDownCell}
121-
renderCellContent={renderCellContent}
122-
stringify={stringify}
123-
/>
126+
<Table />
124127
</Slice>
125128
</Scroller>
126129

src/components/Table.tsx

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,13 @@ import { OrderByContext } from '../contexts/OrderByContext.js'
88
import { RenderedRowsContext } from '../contexts/ScrollContext.js'
99
import { SelectionContext } from '../contexts/SelectionContext.js'
1010
import { ariaOffset } from '../helpers/constants.js'
11-
import type { HighTableProps } from '../types.js'
12-
import { stringify as stringifyDefault } from '../utils/stringify.js'
1311
import Cell from './Cell.js'
1412
import Row from './Row.js'
1513
import RowHeader from './RowHeader.js'
1614
import TableCorner from './TableCorner.js'
1715
import TableHeader from './TableHeader.js'
1816

19-
type TableProps = Pick<HighTableProps, 'onDoubleClickCell' | 'onKeyDownCell' | 'onMouseDownCell' | 'renderCellContent' | 'stringify'>
20-
21-
export default function Table({
22-
onDoubleClickCell,
23-
onKeyDownCell,
24-
onMouseDownCell,
25-
renderCellContent,
26-
stringify = stringifyDefault,
27-
}: TableProps) {
17+
export default function Table() {
2818
const { moveCell } = useContext(CellNavigationContext)
2919
const orderBy = useContext(OrderByContext)
3020
const { selectable, toggleAllRows, pendingSelectionGesture, onTableKeyDown: onSelectionTableKeyDown, allRowsSelected, isRowSelected, toggleRowNumber, toggleRangeToRowNumber } = useContext(SelectionContext)
@@ -214,10 +204,6 @@ export default function Table({
214204
return (
215205
<Cell
216206
key={columnIndex}
217-
onDoubleClickCell={onDoubleClickCell}
218-
onMouseDownCell={onMouseDownCell}
219-
onKeyDownCell={onKeyDownCell}
220-
stringify={stringify}
221207
columnIndex={columnIndex}
222208
visibleColumnIndex={visibleColumnIndex}
223209
className={className}
@@ -226,7 +212,6 @@ export default function Table({
226212
cellValue={cell?.value}
227213
hasResolved={cell !== undefined}
228214
rowNumber={rowNumber}
229-
renderCellContent={renderCellContent}
230215
/>
231216
)
232217
})}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { KeyboardEvent, MouseEvent, ReactNode } from 'react'
2+
import { createContext } from 'react'
3+
4+
import type { CellContentProps, StringifyFunction } from '../types.js'
5+
import { stringify } from '../utils/stringify.js'
6+
7+
/** function to stringify the cell value, used for default rendering and for copy to clipboard */
8+
export const StringifyContext = createContext<StringifyFunction>(stringify)
9+
10+
/** custom cell content component, if not provided, the default stringified value will be used */
11+
export const RenderCellContentContext = createContext<((props: CellContentProps) => ReactNode) | undefined>(undefined)
12+
13+
/** callbacks for cell interactions (click, double click and key down), used for accessibility and custom interactions */
14+
export const CellCallbacksContext = createContext<{
15+
/** double click callback */
16+
onDoubleClickCell?: (event: MouseEvent, col: number, row: number) => void
17+
/** mouse down callback */
18+
onMouseDownCell?: (event: MouseEvent, col: number, row: number) => void
19+
/** key down callback, for accessibility, it should be passed if onDoubleClickCell is passed. It can handle more than that action though. */
20+
onKeyDownCell?: (event: KeyboardEvent, col: number, row: number) => void
21+
}>({})

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import HighTable from './components/HighTable.js'
2-
export { type CellContentProps } from './components/Cell.js'
32
export type { ColumnConfig, ColumnConfiguration, CustomMenuGroup, CustomMenuItem } from './helpers/columnConfiguration.js'
43
export type { Cells, DataFrame, DataFrameEvents, ResolvedValue } from './helpers/dataframe/index.js'
54
export { arrayDataFrame, checkSignal, createGetRowNumber, sortableDataFrame, validateColumn, validateFetchParams, validateGetCellParams, validateGetRowNumberParams, validateOrderBy, validateRow } from './helpers/dataframe/index.js'
65
export type { Selection } from './helpers/selection.js'
76
export type { Direction, OrderBy } from './helpers/sort.js'
87
export type { CustomEventTarget, TypedCustomEvent } from './helpers/typedEventTarget.js'
98
export { createEventTarget } from './helpers/typedEventTarget.js'
9+
export type { CellContentProps } from './types.js'
1010
export { stringify } from './utils/stringify.js'
1111
export { HighTable }
1212
export default HighTable
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { type ReactNode, useMemo } from 'react'
2+
3+
import { CellCallbacksContext, RenderCellContentContext, StringifyContext } from '../contexts/CellConfigurationContext.js'
4+
import type { HighTableProps } from '../types.js'
5+
import { stringify as defaultStringify } from '../utils/stringify.js'
6+
7+
type Props = Pick<HighTableProps, 'onDoubleClickCell' | 'onKeyDownCell' | 'onMouseDownCell' | 'renderCellContent' | 'stringify'> & {
8+
/** Child components */
9+
children: ReactNode
10+
}
11+
12+
/**
13+
* Provide cell configuration contexts.
14+
*/
15+
export function CellConfigurationProvider({ children, onDoubleClickCell, onKeyDownCell, onMouseDownCell, renderCellContent, stringify }: Props) {
16+
const cellCallbacks = useMemo(() => ({
17+
onDoubleClickCell,
18+
onKeyDownCell,
19+
onMouseDownCell,
20+
}), [onDoubleClickCell, onKeyDownCell, onMouseDownCell])
21+
22+
// Multiple contexts, to avoid unnecessary re-renders of the components consuming the API when only the data changes, and vice-versa. See https://react.dev/reference/react/useContext#caveats for more details.
23+
return (
24+
<CellCallbacksContext.Provider value={cellCallbacks}>
25+
<RenderCellContentContext.Provider value={renderCellContent}>
26+
<StringifyContext.Provider value={stringify ?? defaultStringify}>
27+
{children}
28+
</StringifyContext.Provider>
29+
</RenderCellContentContext.Provider>
30+
</CellCallbacksContext.Provider>
31+
)
32+
}

src/types.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import type { KeyboardEvent, MouseEvent, ReactNode } from 'react'
22

3-
import type { CellContentProps } from './components/Cell.js'
43
import type { ColumnConfiguration } from './helpers/columnConfiguration.js'
5-
import type { DataFrame } from './helpers/dataframe/index.js'
4+
import type { DataFrame, ResolvedValue } from './helpers/dataframe/index.js'
65
import type { Selection } from './helpers/selection.js'
76
import type { OrderBy } from './helpers/sort.js'
87
import type { ColumnsVisibility } from './providers/ColumnsVisibilityProvider.js'
@@ -25,6 +24,15 @@ export interface CellPosition {
2524
rowIndex: number
2625
}
2726

27+
export type StringifyFunction = (value: unknown) => string | undefined
28+
29+
export interface CellContentProps {
30+
stringify: StringifyFunction
31+
cell?: ResolvedValue
32+
col: number
33+
row?: number // the row index in the original data, undefined if the value has not been fetched yet
34+
}
35+
2836
// TODO(SL): update selection, onSelectionChange docstrings to reflect the reality
2937

3038
export interface HighTableProps {
@@ -181,8 +189,10 @@ export interface HighTableProps {
181189
/**
182190
* Optional function to stringify cell values for rendering, copy/paste and export.
183191
*
192+
* If not provided, the default stringification will be used.
193+
*
184194
* @param value The cell value
185-
* @returns The stringified value, or undefined to use the default stringification
195+
* @returns The stringified value, or undefined if the value cannot be stringified
186196
*/
187-
stringify?: (value: unknown) => string | undefined
197+
stringify?: StringifyFunction
188198
}

stories/HighTable.stories.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
22
import type { MouseEvent, ReactNode } from 'react'
33
import { useState } from 'react'
44

5-
import type { CellContentProps } from '../src/components/Cell.js'
65
import HighTable from '../src/components/HighTable.js'
76
import { checkSignal, createGetRowNumber, validateFetchParams, validateGetCellParams } from '../src/helpers/dataframe/helpers.js'
87
import type { DataFrame, DataFrameEvents } from '../src/helpers/dataframe/index.js'
@@ -13,7 +12,7 @@ import type { Selection } from '../src/helpers/selection.js'
1312
import type { OrderBy } from '../src/helpers/sort.js'
1413
import { createEventTarget } from '../src/helpers/typedEventTarget.js'
1514
import type { ColumnsVisibility } from '../src/providers/ColumnsVisibilityProvider.js'
16-
import type { CellPosition } from '../src/types.js'
15+
import type { CellContentProps, CellPosition } from '../src/types.js'
1716

1817
function random(seed: number) {
1918
const x = Math.sin(seed) * 10000

0 commit comments

Comments
 (0)