@lovett/ui 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/data-grid/data-grid.tsx +55 -0
- package/src/data-grid/sortable-parts.tsx +6 -0
- package/src/data-grid/sticky.ts +152 -0
- package/src/data-grid/table-body.tsx +168 -121
- package/src/data-grid/table-header.tsx +68 -19
- package/src/data-grid/table-summary-footer.tsx +60 -37
- package/src/data-grid/table-view.tsx +18 -0
- package/src/data-grid/types.ts +15 -0
package/package.json
CHANGED
|
@@ -44,6 +44,7 @@ import { DrawerPanel } from './drawer-panel'
|
|
|
44
44
|
import { DataGridTableView } from './table-view'
|
|
45
45
|
import { useGridColumns } from './use-grid-columns'
|
|
46
46
|
import { useGridEditing } from './use-grid-editing'
|
|
47
|
+
import { SCROLL_EDGES_AT_REST, type ScrollEdges } from './sticky'
|
|
47
48
|
import type {
|
|
48
49
|
DataGridDrawerPanelProps,
|
|
49
50
|
DataGridProps,
|
|
@@ -86,6 +87,8 @@ export function DataGrid<Row extends DataGridRowBase, ColumnId extends string>({
|
|
|
86
87
|
drawerModal = false,
|
|
87
88
|
drawerSize,
|
|
88
89
|
stickySummaryFooter = false,
|
|
90
|
+
stickyHeader = false,
|
|
91
|
+
fillWidth = false,
|
|
89
92
|
fillAvailableHeight = false,
|
|
90
93
|
tableContainerClassName,
|
|
91
94
|
onRowsChange,
|
|
@@ -96,6 +99,8 @@ export function DataGrid<Row extends DataGridRowBase, ColumnId extends string>({
|
|
|
96
99
|
const [drawerCell, setDrawerCell] =
|
|
97
100
|
useState<EditingCell<ColumnId> | null>(null)
|
|
98
101
|
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([])
|
|
102
|
+
const [scrollEdges, setScrollEdges] =
|
|
103
|
+
useState<ScrollEdges>(SCROLL_EDGES_AT_REST)
|
|
99
104
|
const reactInstanceId = useId()
|
|
100
105
|
const instanceId = useMemo(
|
|
101
106
|
() => reactInstanceId.replace(/:/g, ''),
|
|
@@ -317,6 +322,53 @@ export function DataGrid<Row extends DataGridRowBase, ColumnId extends string>({
|
|
|
317
322
|
onToolbarPropsChange?.(toolbarProps)
|
|
318
323
|
}, [onToolbarPropsChange, toolbarProps])
|
|
319
324
|
|
|
325
|
+
// Track which edges of the scroll viewport are at rest, so sticky/pinned
|
|
326
|
+
// cells can show a scroll-shadow only when content is scrolled past them.
|
|
327
|
+
const hasPinnedColumn = useMemo(
|
|
328
|
+
() => columns.some((column) => column.pin),
|
|
329
|
+
[columns],
|
|
330
|
+
)
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
if (!stickyHeader && !stickySummaryFooter && !hasPinnedColumn) return
|
|
333
|
+
const scroller = tableRef.current?.parentElement
|
|
334
|
+
if (!scroller) return
|
|
335
|
+
|
|
336
|
+
let frame = 0
|
|
337
|
+
const measure = () => {
|
|
338
|
+
frame = 0
|
|
339
|
+
const next: ScrollEdges = {
|
|
340
|
+
atTop: scroller.scrollTop <= 0,
|
|
341
|
+
atBottom:
|
|
342
|
+
scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - 1,
|
|
343
|
+
atLeft: scroller.scrollLeft <= 0,
|
|
344
|
+
atRight:
|
|
345
|
+
scroller.scrollLeft + scroller.clientWidth >= scroller.scrollWidth - 1,
|
|
346
|
+
}
|
|
347
|
+
setScrollEdges((prev) =>
|
|
348
|
+
prev.atTop === next.atTop &&
|
|
349
|
+
prev.atBottom === next.atBottom &&
|
|
350
|
+
prev.atLeft === next.atLeft &&
|
|
351
|
+
prev.atRight === next.atRight
|
|
352
|
+
? prev
|
|
353
|
+
: next,
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
const onScroll = () => {
|
|
357
|
+
if (!frame) frame = requestAnimationFrame(measure)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
measure()
|
|
361
|
+
scroller.addEventListener('scroll', onScroll, { passive: true })
|
|
362
|
+
const observer = new ResizeObserver(measure)
|
|
363
|
+
observer.observe(scroller)
|
|
364
|
+
|
|
365
|
+
return () => {
|
|
366
|
+
scroller.removeEventListener('scroll', onScroll)
|
|
367
|
+
observer.disconnect()
|
|
368
|
+
if (frame) cancelAnimationFrame(frame)
|
|
369
|
+
}
|
|
370
|
+
}, [stickyHeader, stickySummaryFooter, hasPinnedColumn, rows.length, columnWidths])
|
|
371
|
+
|
|
320
372
|
return (
|
|
321
373
|
<>
|
|
322
374
|
<div
|
|
@@ -361,6 +413,9 @@ export function DataGrid<Row extends DataGridRowBase, ColumnId extends string>({
|
|
|
361
413
|
showSummaries={showSummaries}
|
|
362
414
|
renderSummary={renderSummary}
|
|
363
415
|
stickySummaryFooter={stickySummaryFooter}
|
|
416
|
+
stickyHeader={stickyHeader}
|
|
417
|
+
fillWidth={fillWidth}
|
|
418
|
+
scrollEdges={scrollEdges}
|
|
364
419
|
tableContainerClassName={tableContainerClassName}
|
|
365
420
|
isEmptyValue={isEmptyValue}
|
|
366
421
|
onResizeStart={beginResize}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import type {
|
|
19
|
+
CSSProperties,
|
|
19
20
|
KeyboardEvent as ReactKeyboardEvent,
|
|
20
21
|
PointerEvent as ReactPointerEvent,
|
|
21
22
|
} from 'react'
|
|
@@ -43,6 +44,9 @@ type SortableHeaderCellProps<ColumnId extends string> = {
|
|
|
43
44
|
/** ADR-027 — header click handler. When omitted the header is non-
|
|
44
45
|
* interactive for sort (column reorder + resize still work). */
|
|
45
46
|
onSortChange: ((columnId: ColumnId) => void) | undefined
|
|
47
|
+
/** Extra style merged onto the <th> (sticky/pinned positioning, opaque
|
|
48
|
+
* background + scroll-shadow). Spread last so it wins over the base. */
|
|
49
|
+
cellStyle?: CSSProperties
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
type SortableColumnOptionItemProps<ColumnId extends string> = {
|
|
@@ -130,6 +134,7 @@ export function SortableHeaderCell<ColumnId extends string>({
|
|
|
130
134
|
onResize,
|
|
131
135
|
sortDir,
|
|
132
136
|
onSortChange,
|
|
137
|
+
cellStyle,
|
|
133
138
|
}: SortableHeaderCellProps<ColumnId>) {
|
|
134
139
|
// ADR-027 — column sortability: explicit `sortable: false` opts out;
|
|
135
140
|
// missing `onSortChange` also disables (no parent handler).
|
|
@@ -173,6 +178,7 @@ export function SortableHeaderCell<ColumnId extends string>({
|
|
|
173
178
|
borderRight: '1px solid rgb(var(--border))',
|
|
174
179
|
background: 'rgb(var(--surface-overlay-soft))',
|
|
175
180
|
color: 'rgb(var(--foreground))',
|
|
181
|
+
...cellStyle,
|
|
176
182
|
}}
|
|
177
183
|
>
|
|
178
184
|
<div className="flex h-full min-w-0 items-center justify-between gap-1 overflow-hidden">
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sticky / pinned-column helpers for DataGrid.
|
|
3
|
+
*
|
|
4
|
+
* Centralizes the styling for the grid's three sticky surfaces so they
|
|
5
|
+
* behave consistently:
|
|
6
|
+
* • sticky header (top) and sticky summary footer (bottom)
|
|
7
|
+
* • pinned columns (`column.pin: 'left' | 'right'`), with the built-in
|
|
8
|
+
* select/control column implicitly pinned left
|
|
9
|
+
* • a trailing flex "spacer" column (`fillWidth`) so the table fills
|
|
10
|
+
* 100% width while real columns keep their exact widths (resize stays
|
|
11
|
+
* independent — the spacer gives/takes the slack)
|
|
12
|
+
*
|
|
13
|
+
* Two correctness rules every sticky cell must follow:
|
|
14
|
+
* 1. it MUST be opaque, or scrolled content bleeds through it; and
|
|
15
|
+
* 2. it shows a soft "scroll shadow" on its inner edge ONLY while there
|
|
16
|
+
* is content scrolled past that edge — so there's visual separation
|
|
17
|
+
* while pinned, and it disappears flush when snapped back.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { CSSProperties } from 'react'
|
|
21
|
+
|
|
22
|
+
import type { DataGridColumn } from './types'
|
|
23
|
+
|
|
24
|
+
export const CONTROL_COLUMN_WIDTH = 40
|
|
25
|
+
|
|
26
|
+
/** Which edges of the scroll viewport are currently AT rest (not scrolled
|
|
27
|
+
* away from). A shadow on a given side shows when its edge is NOT `at*`. */
|
|
28
|
+
export type ScrollEdges = {
|
|
29
|
+
atTop: boolean
|
|
30
|
+
atBottom: boolean
|
|
31
|
+
atLeft: boolean
|
|
32
|
+
atRight: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Default = everything at rest → no shadows (used before first scroll). */
|
|
36
|
+
export const SCROLL_EDGES_AT_REST: ScrollEdges = {
|
|
37
|
+
atTop: true,
|
|
38
|
+
atBottom: true,
|
|
39
|
+
atLeft: true,
|
|
40
|
+
atRight: true,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const SHADOW_RIGHT = '6px 0 8px -6px rgba(2, 6, 23, 0.22)' // pinned-left edge
|
|
44
|
+
const SHADOW_LEFT = '-6px 0 8px -6px rgba(2, 6, 23, 0.22)' // pinned-right edge
|
|
45
|
+
const SHADOW_DOWN = '0 6px 8px -6px rgba(2, 6, 23, 0.22)' // sticky header
|
|
46
|
+
const SHADOW_UP = '0 -6px 8px -6px rgba(2, 6, 23, 0.22)' // sticky footer
|
|
47
|
+
|
|
48
|
+
export type PinInfo = { side: 'left' | 'right'; offset: number }
|
|
49
|
+
|
|
50
|
+
/** Map each pinned column → its side + cumulative sticky offset (px), so
|
|
51
|
+
* multiple pinned columns on the same side stack correctly. The control
|
|
52
|
+
* column occupies the first `CONTROL_COLUMN_WIDTH` on the left. */
|
|
53
|
+
export function computePinOffsets<ColumnId extends string>(
|
|
54
|
+
visibleColumns: DataGridColumn<ColumnId>[],
|
|
55
|
+
columnWidths: Record<ColumnId, number>,
|
|
56
|
+
): Map<ColumnId, PinInfo> {
|
|
57
|
+
const map = new Map<ColumnId, PinInfo>()
|
|
58
|
+
const widthOf = (c: DataGridColumn<ColumnId>) =>
|
|
59
|
+
columnWidths[c.id] ?? c.defaultWidth
|
|
60
|
+
|
|
61
|
+
let leftOffset = CONTROL_COLUMN_WIDTH
|
|
62
|
+
for (const c of visibleColumns) {
|
|
63
|
+
if (c.pin !== 'left') continue
|
|
64
|
+
map.set(c.id, { side: 'left', offset: leftOffset })
|
|
65
|
+
leftOffset += widthOf(c)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let rightOffset = 0
|
|
69
|
+
const rightPinned = visibleColumns.filter((c) => c.pin === 'right')
|
|
70
|
+
for (let i = rightPinned.length - 1; i >= 0; i -= 1) {
|
|
71
|
+
const c = rightPinned[i]
|
|
72
|
+
if (!c) continue
|
|
73
|
+
map.set(c.id, { side: 'right', offset: rightOffset })
|
|
74
|
+
rightOffset += widthOf(c)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return map
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Split a column list at the first right-pinned column, so renderers can
|
|
81
|
+
* drop the flex spacer immediately before the right-pinned group (or at
|
|
82
|
+
* the end when nothing is pinned right). */
|
|
83
|
+
export function splitAtRightPinned<ColumnId extends string>(
|
|
84
|
+
visibleColumns: DataGridColumn<ColumnId>[],
|
|
85
|
+
): {
|
|
86
|
+
leading: DataGridColumn<ColumnId>[]
|
|
87
|
+
rightPinned: DataGridColumn<ColumnId>[]
|
|
88
|
+
} {
|
|
89
|
+
const firstRight = visibleColumns.findIndex((c) => c.pin === 'right')
|
|
90
|
+
if (firstRight === -1) {
|
|
91
|
+
return { leading: visibleColumns, rightPinned: [] }
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
leading: visibleColumns.slice(0, firstRight),
|
|
95
|
+
rightPinned: visibleColumns.slice(firstRight),
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
type Role = 'header' | 'body' | 'footer'
|
|
100
|
+
|
|
101
|
+
/** Opaque background. Header composites its translucent tint over an
|
|
102
|
+
* opaque base so it still reads tinted but never lets content through. */
|
|
103
|
+
function opaqueBg(role: Role): CSSProperties {
|
|
104
|
+
if (role === 'header') {
|
|
105
|
+
return {
|
|
106
|
+
backgroundColor: 'rgb(var(--bg-card))',
|
|
107
|
+
backgroundImage:
|
|
108
|
+
'linear-gradient(rgb(var(--surface-overlay-soft)), rgb(var(--surface-overlay-soft)))',
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { backgroundColor: 'rgb(var(--bg-card))' }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Compute the inline style for a (possibly) sticky cell. Returns `{}` when
|
|
116
|
+
* the cell isn't sticky in any axis, so callers can spread it
|
|
117
|
+
* unconditionally.
|
|
118
|
+
*/
|
|
119
|
+
export function stickyCellStyle(opts: {
|
|
120
|
+
role: Role
|
|
121
|
+
pin?: PinInfo | null
|
|
122
|
+
edges: ScrollEdges
|
|
123
|
+
stickyHeader: boolean
|
|
124
|
+
stickyFooter: boolean
|
|
125
|
+
}): CSSProperties {
|
|
126
|
+
const { role, pin, edges, stickyHeader, stickyFooter } = opts
|
|
127
|
+
const verticalSticky =
|
|
128
|
+
(role === 'header' && stickyHeader) || (role === 'footer' && stickyFooter)
|
|
129
|
+
if (!verticalSticky && !pin) return {}
|
|
130
|
+
|
|
131
|
+
const style: CSSProperties = { position: 'sticky' }
|
|
132
|
+
|
|
133
|
+
if (role === 'header' && stickyHeader) style.top = 0
|
|
134
|
+
if (role === 'footer' && stickyFooter) style.bottom = 0
|
|
135
|
+
if (pin?.side === 'left') style.left = pin.offset
|
|
136
|
+
if (pin?.side === 'right') style.right = pin.offset
|
|
137
|
+
|
|
138
|
+
Object.assign(style, opaqueBg(role))
|
|
139
|
+
|
|
140
|
+
// Corners (vertical-sticky ∩ pinned) must sit above both axes.
|
|
141
|
+
const vertZ = role === 'header' ? 30 : role === 'footer' ? 20 : 0
|
|
142
|
+
style.zIndex = vertZ + (pin ? 10 : 0) || (pin ? 10 : undefined)
|
|
143
|
+
|
|
144
|
+
const shadows: string[] = []
|
|
145
|
+
if (role === 'header' && stickyHeader && !edges.atTop) shadows.push(SHADOW_DOWN)
|
|
146
|
+
if (role === 'footer' && stickyFooter && !edges.atBottom) shadows.push(SHADOW_UP)
|
|
147
|
+
if (pin?.side === 'left' && !edges.atLeft) shadows.push(SHADOW_RIGHT)
|
|
148
|
+
if (pin?.side === 'right' && !edges.atRight) shadows.push(SHADOW_LEFT)
|
|
149
|
+
if (shadows.length) style.boxShadow = shadows.join(', ')
|
|
150
|
+
|
|
151
|
+
return style
|
|
152
|
+
}
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* rather than the Input primitive — the input is intentionally
|
|
19
19
|
* compact (h-7) and edge-to-edge inside the cell, not a typical
|
|
20
20
|
* form input.
|
|
21
|
+
* • Pinned columns + fill-width spacer (workspace addition) via the
|
|
22
|
+
* shared sticky helper.
|
|
21
23
|
*/
|
|
22
24
|
|
|
23
25
|
import type {
|
|
@@ -33,6 +35,12 @@ import { Button } from '../button'
|
|
|
33
35
|
import { Checkbox } from '../checkbox'
|
|
34
36
|
import { TableBody, TableCell, TableRow } from './table-elements'
|
|
35
37
|
|
|
38
|
+
import {
|
|
39
|
+
splitAtRightPinned,
|
|
40
|
+
stickyCellStyle,
|
|
41
|
+
type PinInfo,
|
|
42
|
+
type ScrollEdges,
|
|
43
|
+
} from './sticky'
|
|
36
44
|
import type {
|
|
37
45
|
DataGridColumn,
|
|
38
46
|
DataGridRowBase,
|
|
@@ -68,6 +76,9 @@ type DataGridTableBodyProps<
|
|
|
68
76
|
onToggleRowSelection: (rowId: string, checked: boolean) => void
|
|
69
77
|
columnWidths: Record<ColumnId, number>
|
|
70
78
|
draggingColumnId: ColumnId | null
|
|
79
|
+
pinOffsets: Map<ColumnId, PinInfo>
|
|
80
|
+
scrollEdges: ScrollEdges
|
|
81
|
+
fillWidth: boolean
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
export function DataGridTableBody<
|
|
@@ -93,14 +104,161 @@ export function DataGridTableBody<
|
|
|
93
104
|
onToggleRowSelection,
|
|
94
105
|
columnWidths,
|
|
95
106
|
draggingColumnId,
|
|
107
|
+
pinOffsets,
|
|
108
|
+
scrollEdges,
|
|
109
|
+
fillWidth,
|
|
96
110
|
}: DataGridTableBodyProps<Row, ColumnId>) {
|
|
111
|
+
const { leading, rightPinned } = splitAtRightPinned(visibleColumns)
|
|
112
|
+
|
|
113
|
+
const controlStyle = stickyCellStyle({
|
|
114
|
+
role: 'body',
|
|
115
|
+
pin: { side: 'left', offset: 0 },
|
|
116
|
+
edges: scrollEdges,
|
|
117
|
+
stickyHeader: false,
|
|
118
|
+
stickyFooter: false,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const renderDataCell = (
|
|
122
|
+
row: Row,
|
|
123
|
+
rowIndex: number,
|
|
124
|
+
column: DataGridColumn<ColumnId>,
|
|
125
|
+
colIndex: number,
|
|
126
|
+
) => {
|
|
127
|
+
const isEditing =
|
|
128
|
+
editingCell?.rowId === row.id && editingCell.columnId === column.id
|
|
129
|
+
const editable = isEditableColumn(column.id)
|
|
130
|
+
const showDrawerAction = canOpenDrawer(column.id)
|
|
131
|
+
const isDraggingThisColumn = draggingColumnId === column.id
|
|
132
|
+
const sticky = stickyCellStyle({
|
|
133
|
+
role: 'body',
|
|
134
|
+
pin: pinOffsets.get(column.id) ?? null,
|
|
135
|
+
edges: scrollEdges,
|
|
136
|
+
stickyHeader: false,
|
|
137
|
+
stickyFooter: false,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<TableCell
|
|
142
|
+
key={`${row.id}-${column.id}`}
|
|
143
|
+
data-grid-cell="true"
|
|
144
|
+
data-row-index={rowIndex}
|
|
145
|
+
data-col-index={colIndex}
|
|
146
|
+
tabIndex={isEditing ? -1 : 0}
|
|
147
|
+
className={cn(
|
|
148
|
+
'group/cell relative h-10 overflow-hidden px-2 py-1 whitespace-nowrap outline-none',
|
|
149
|
+
editable && 'cursor-text',
|
|
150
|
+
)}
|
|
151
|
+
style={{
|
|
152
|
+
width: columnWidths[column.id],
|
|
153
|
+
minWidth: columnWidths[column.id],
|
|
154
|
+
borderRight: '1px solid rgb(var(--border))',
|
|
155
|
+
...sticky,
|
|
156
|
+
...(isDraggingThisColumn
|
|
157
|
+
? { background: 'rgb(var(--surface-overlay-soft))' }
|
|
158
|
+
: null),
|
|
159
|
+
}}
|
|
160
|
+
onDoubleClick={() => {
|
|
161
|
+
if (editable) startEditing(row, column.id)
|
|
162
|
+
}}
|
|
163
|
+
onKeyDown={(event) =>
|
|
164
|
+
onCellKeyDown(event, row, rowIndex, column, colIndex)
|
|
165
|
+
}
|
|
166
|
+
>
|
|
167
|
+
{isEditing ? (
|
|
168
|
+
<input
|
|
169
|
+
ref={inputRef}
|
|
170
|
+
value={draftValue}
|
|
171
|
+
onChange={(event) => setDraftValue(event.target.value)}
|
|
172
|
+
onBlur={commitEdit}
|
|
173
|
+
onKeyDown={(event) => {
|
|
174
|
+
if (event.key === 'Enter') {
|
|
175
|
+
event.preventDefault()
|
|
176
|
+
commitEdit()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (event.key === 'Escape') {
|
|
180
|
+
event.preventDefault()
|
|
181
|
+
cancelEdit()
|
|
182
|
+
}
|
|
183
|
+
}}
|
|
184
|
+
className="h-7 w-full px-2 text-xs outline-none rounded-[var(--radius-sm)]"
|
|
185
|
+
style={{
|
|
186
|
+
background: 'rgb(var(--bg-input))',
|
|
187
|
+
border: '1px solid rgb(var(--border-strong))',
|
|
188
|
+
color: 'rgb(var(--foreground))',
|
|
189
|
+
}}
|
|
190
|
+
/>
|
|
191
|
+
) : (
|
|
192
|
+
<>
|
|
193
|
+
<span
|
|
194
|
+
aria-hidden
|
|
195
|
+
className="pointer-events-none absolute inset-0 transition-colors duration-150"
|
|
196
|
+
style={{ border: '1px solid transparent' }}
|
|
197
|
+
/>
|
|
198
|
+
<div
|
|
199
|
+
className={cn(
|
|
200
|
+
'min-w-0 truncate transition-[padding] duration-150',
|
|
201
|
+
showDrawerAction &&
|
|
202
|
+
'group-hover/cell:pr-16 group-focus-within/cell:pr-16',
|
|
203
|
+
)}
|
|
204
|
+
style={{ color: 'rgb(var(--foreground))' }}
|
|
205
|
+
>
|
|
206
|
+
{renderCell(row, column)}
|
|
207
|
+
</div>
|
|
208
|
+
{showDrawerAction ? (
|
|
209
|
+
<div className="pointer-events-none absolute top-1/2 right-1.5 z-10 -translate-y-1/2 opacity-0 transition-all group-focus-within/cell:pointer-events-auto group-focus-within/cell:opacity-100 group-hover/cell:pointer-events-auto group-hover/cell:opacity-100">
|
|
210
|
+
<Button
|
|
211
|
+
type="button"
|
|
212
|
+
variant="outline"
|
|
213
|
+
size="sm"
|
|
214
|
+
aria-label={`Open details for ${getRowLabel(row)} ${column.label}`}
|
|
215
|
+
title="Open drawer"
|
|
216
|
+
className="h-7 px-2.5"
|
|
217
|
+
onPointerDown={(event) => {
|
|
218
|
+
event.stopPropagation()
|
|
219
|
+
}}
|
|
220
|
+
onDoubleClick={(event) => {
|
|
221
|
+
event.stopPropagation()
|
|
222
|
+
}}
|
|
223
|
+
onClick={(event) => {
|
|
224
|
+
event.stopPropagation()
|
|
225
|
+
const originElement =
|
|
226
|
+
event.currentTarget.closest('td') ?? event.currentTarget
|
|
227
|
+
const originRect = originElement.getBoundingClientRect()
|
|
228
|
+
|
|
229
|
+
onOpenDrawer({
|
|
230
|
+
rowId: row.id,
|
|
231
|
+
columnId: column.id,
|
|
232
|
+
originRect: {
|
|
233
|
+
x: originRect.x,
|
|
234
|
+
y: originRect.y,
|
|
235
|
+
width: originRect.width,
|
|
236
|
+
height: originRect.height,
|
|
237
|
+
},
|
|
238
|
+
})
|
|
239
|
+
}}
|
|
240
|
+
leadingIcon={<ExternalLink className="h-3.5 w-3.5" />}
|
|
241
|
+
>
|
|
242
|
+
Open
|
|
243
|
+
</Button>
|
|
244
|
+
</div>
|
|
245
|
+
) : null}
|
|
246
|
+
</>
|
|
247
|
+
)}
|
|
248
|
+
</TableCell>
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
97
252
|
return (
|
|
98
253
|
<TableBody>
|
|
99
254
|
{visibleRows.map((row, rowIndex) => (
|
|
100
255
|
<TableRow key={row.id}>
|
|
101
256
|
<TableCell
|
|
102
257
|
className="h-10 px-0 text-center"
|
|
103
|
-
style={{
|
|
258
|
+
style={{
|
|
259
|
+
borderRight: '1px solid rgb(var(--border))',
|
|
260
|
+
...controlStyle,
|
|
261
|
+
}}
|
|
104
262
|
>
|
|
105
263
|
<Checkbox
|
|
106
264
|
aria-label={`Select ${getRowLabel(row)}`}
|
|
@@ -111,126 +269,15 @@ export function DataGridTableBody<
|
|
|
111
269
|
className="mx-auto"
|
|
112
270
|
/>
|
|
113
271
|
</TableCell>
|
|
114
|
-
{
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
<TableCell
|
|
124
|
-
key={`${row.id}-${column.id}`}
|
|
125
|
-
data-grid-cell="true"
|
|
126
|
-
data-row-index={rowIndex}
|
|
127
|
-
data-col-index={colIndex}
|
|
128
|
-
tabIndex={isEditing ? -1 : 0}
|
|
129
|
-
className={cn(
|
|
130
|
-
'group/cell relative h-10 overflow-hidden px-2 py-1 whitespace-nowrap outline-none',
|
|
131
|
-
editable && 'cursor-text',
|
|
132
|
-
)}
|
|
133
|
-
style={{
|
|
134
|
-
width: columnWidths[column.id],
|
|
135
|
-
minWidth: columnWidths[column.id],
|
|
136
|
-
borderRight: '1px solid rgb(var(--border))',
|
|
137
|
-
background: isDraggingThisColumn
|
|
138
|
-
? 'rgb(var(--surface-overlay-soft))'
|
|
139
|
-
: undefined,
|
|
140
|
-
}}
|
|
141
|
-
onDoubleClick={() => {
|
|
142
|
-
if (editable) startEditing(row, column.id)
|
|
143
|
-
}}
|
|
144
|
-
onKeyDown={(event) =>
|
|
145
|
-
onCellKeyDown(event, row, rowIndex, column, colIndex)
|
|
146
|
-
}
|
|
147
|
-
>
|
|
148
|
-
{isEditing ? (
|
|
149
|
-
<input
|
|
150
|
-
ref={inputRef}
|
|
151
|
-
value={draftValue}
|
|
152
|
-
onChange={(event) => setDraftValue(event.target.value)}
|
|
153
|
-
onBlur={commitEdit}
|
|
154
|
-
onKeyDown={(event) => {
|
|
155
|
-
if (event.key === 'Enter') {
|
|
156
|
-
event.preventDefault()
|
|
157
|
-
commitEdit()
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (event.key === 'Escape') {
|
|
161
|
-
event.preventDefault()
|
|
162
|
-
cancelEdit()
|
|
163
|
-
}
|
|
164
|
-
}}
|
|
165
|
-
className="h-7 w-full px-2 text-xs outline-none rounded-[var(--radius-sm)]"
|
|
166
|
-
style={{
|
|
167
|
-
background: 'rgb(var(--bg-input))',
|
|
168
|
-
border: '1px solid rgb(var(--border-strong))',
|
|
169
|
-
color: 'rgb(var(--foreground))',
|
|
170
|
-
}}
|
|
171
|
-
/>
|
|
172
|
-
) : (
|
|
173
|
-
<>
|
|
174
|
-
<span
|
|
175
|
-
aria-hidden
|
|
176
|
-
className="pointer-events-none absolute inset-0 transition-colors duration-150"
|
|
177
|
-
style={{ border: '1px solid transparent' }}
|
|
178
|
-
/>
|
|
179
|
-
<div
|
|
180
|
-
className={cn(
|
|
181
|
-
'min-w-0 truncate transition-[padding] duration-150',
|
|
182
|
-
showDrawerAction &&
|
|
183
|
-
'group-hover/cell:pr-16 group-focus-within/cell:pr-16',
|
|
184
|
-
)}
|
|
185
|
-
style={{ color: 'rgb(var(--foreground))' }}
|
|
186
|
-
>
|
|
187
|
-
{renderCell(row, column)}
|
|
188
|
-
</div>
|
|
189
|
-
{showDrawerAction ? (
|
|
190
|
-
<div className="pointer-events-none absolute top-1/2 right-1.5 z-10 -translate-y-1/2 opacity-0 transition-all group-focus-within/cell:pointer-events-auto group-focus-within/cell:opacity-100 group-hover/cell:pointer-events-auto group-hover/cell:opacity-100">
|
|
191
|
-
<Button
|
|
192
|
-
type="button"
|
|
193
|
-
variant="outline"
|
|
194
|
-
size="sm"
|
|
195
|
-
aria-label={`Open details for ${getRowLabel(row)} ${column.label}`}
|
|
196
|
-
title="Open drawer"
|
|
197
|
-
className="h-7 px-2.5"
|
|
198
|
-
onPointerDown={(event) => {
|
|
199
|
-
event.stopPropagation()
|
|
200
|
-
}}
|
|
201
|
-
onDoubleClick={(event) => {
|
|
202
|
-
event.stopPropagation()
|
|
203
|
-
}}
|
|
204
|
-
onClick={(event) => {
|
|
205
|
-
event.stopPropagation()
|
|
206
|
-
const originElement =
|
|
207
|
-
event.currentTarget.closest('td') ??
|
|
208
|
-
event.currentTarget
|
|
209
|
-
const originRect =
|
|
210
|
-
originElement.getBoundingClientRect()
|
|
211
|
-
|
|
212
|
-
onOpenDrawer({
|
|
213
|
-
rowId: row.id,
|
|
214
|
-
columnId: column.id,
|
|
215
|
-
originRect: {
|
|
216
|
-
x: originRect.x,
|
|
217
|
-
y: originRect.y,
|
|
218
|
-
width: originRect.width,
|
|
219
|
-
height: originRect.height,
|
|
220
|
-
},
|
|
221
|
-
})
|
|
222
|
-
}}
|
|
223
|
-
leadingIcon={<ExternalLink className="h-3.5 w-3.5" />}
|
|
224
|
-
>
|
|
225
|
-
Open
|
|
226
|
-
</Button>
|
|
227
|
-
</div>
|
|
228
|
-
) : null}
|
|
229
|
-
</>
|
|
230
|
-
)}
|
|
231
|
-
</TableCell>
|
|
232
|
-
)
|
|
233
|
-
})}
|
|
272
|
+
{leading.map((column, colIndex) =>
|
|
273
|
+
renderDataCell(row, rowIndex, column, colIndex),
|
|
274
|
+
)}
|
|
275
|
+
{fillWidth ? (
|
|
276
|
+
<td aria-hidden style={{ width: 'auto', padding: 0 }} />
|
|
277
|
+
) : null}
|
|
278
|
+
{rightPinned.map((column, i) =>
|
|
279
|
+
renderDataCell(row, rowIndex, column, leading.length + i),
|
|
280
|
+
)}
|
|
234
281
|
</TableRow>
|
|
235
282
|
))}
|
|
236
283
|
</TableBody>
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* • Token reskin per ADR-025 D7
|
|
12
12
|
* • Checkbox imported from workspace BUILD_FRESH primitive (DEV-2)
|
|
13
13
|
* • Table sub-components from local table-elements (DEV-1)
|
|
14
|
+
* • Sticky header + pinned columns + fill-width spacer (workspace
|
|
15
|
+
* addition) via the shared sticky helper.
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
import type { PointerEvent as ReactPointerEvent } from 'react'
|
|
@@ -19,10 +21,15 @@ import { Checkbox } from '../checkbox'
|
|
|
19
21
|
import { TableHead, TableHeader, TableRow } from './table-elements'
|
|
20
22
|
|
|
21
23
|
import { SortableHeaderCell } from './sortable-parts'
|
|
24
|
+
import {
|
|
25
|
+
CONTROL_COLUMN_WIDTH,
|
|
26
|
+
splitAtRightPinned,
|
|
27
|
+
stickyCellStyle,
|
|
28
|
+
type PinInfo,
|
|
29
|
+
type ScrollEdges,
|
|
30
|
+
} from './sticky'
|
|
22
31
|
import type { DataGridColumn, DataGridSortState } from './types'
|
|
23
32
|
|
|
24
|
-
const CONTROL_COLUMN_WIDTH = 40
|
|
25
|
-
|
|
26
33
|
type DataGridTableHeaderProps<ColumnId extends string> = {
|
|
27
34
|
visibleColumns: DataGridColumn<ColumnId>[]
|
|
28
35
|
columnWidths: Record<ColumnId, number>
|
|
@@ -35,6 +42,10 @@ type DataGridTableHeaderProps<ColumnId extends string> = {
|
|
|
35
42
|
) => void
|
|
36
43
|
sort: DataGridSortState<ColumnId> | null
|
|
37
44
|
onSortChange: ((columnId: ColumnId) => void) | undefined
|
|
45
|
+
pinOffsets: Map<ColumnId, PinInfo>
|
|
46
|
+
scrollEdges: ScrollEdges
|
|
47
|
+
stickyHeader: boolean
|
|
48
|
+
fillWidth: boolean
|
|
38
49
|
}
|
|
39
50
|
|
|
40
51
|
export function DataGridTableHeader<ColumnId extends string>({
|
|
@@ -46,16 +57,60 @@ export function DataGridTableHeader<ColumnId extends string>({
|
|
|
46
57
|
onResizeStart,
|
|
47
58
|
sort,
|
|
48
59
|
onSortChange,
|
|
60
|
+
pinOffsets,
|
|
61
|
+
scrollEdges,
|
|
62
|
+
stickyHeader,
|
|
63
|
+
fillWidth,
|
|
49
64
|
}: DataGridTableHeaderProps<ColumnId>) {
|
|
65
|
+
const { leading, rightPinned } = splitAtRightPinned(visibleColumns)
|
|
66
|
+
|
|
67
|
+
const styleFor = (column: DataGridColumn<ColumnId>) =>
|
|
68
|
+
stickyCellStyle({
|
|
69
|
+
role: 'header',
|
|
70
|
+
pin: pinOffsets.get(column.id) ?? null,
|
|
71
|
+
edges: scrollEdges,
|
|
72
|
+
stickyHeader,
|
|
73
|
+
stickyFooter: false,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const controlStyle = stickyCellStyle({
|
|
77
|
+
role: 'header',
|
|
78
|
+
pin: { side: 'left', offset: 0 },
|
|
79
|
+
edges: scrollEdges,
|
|
80
|
+
stickyHeader,
|
|
81
|
+
stickyFooter: false,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const spacerStyle = stickyCellStyle({
|
|
85
|
+
role: 'header',
|
|
86
|
+
pin: null,
|
|
87
|
+
edges: scrollEdges,
|
|
88
|
+
stickyHeader,
|
|
89
|
+
stickyFooter: false,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const renderHeadCell = (column: DataGridColumn<ColumnId>) => (
|
|
93
|
+
<SortableHeaderCell
|
|
94
|
+
key={column.id}
|
|
95
|
+
column={column}
|
|
96
|
+
width={columnWidths[column.id]}
|
|
97
|
+
onResize={onResizeStart}
|
|
98
|
+
sortDir={sort?.columnId === column.id ? sort.dir : null}
|
|
99
|
+
onSortChange={onSortChange}
|
|
100
|
+
cellStyle={styleFor(column)}
|
|
101
|
+
/>
|
|
102
|
+
)
|
|
103
|
+
|
|
50
104
|
return (
|
|
51
105
|
<>
|
|
52
106
|
<colgroup>
|
|
53
107
|
<col style={{ width: CONTROL_COLUMN_WIDTH }} />
|
|
54
|
-
{
|
|
55
|
-
<col
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
108
|
+
{leading.map((column) => (
|
|
109
|
+
<col key={`col-${column.id}`} style={{ width: columnWidths[column.id] }} />
|
|
110
|
+
))}
|
|
111
|
+
{fillWidth ? <col style={{ width: 'auto' }} /> : null}
|
|
112
|
+
{rightPinned.map((column) => (
|
|
113
|
+
<col key={`col-${column.id}`} style={{ width: columnWidths[column.id] }} />
|
|
59
114
|
))}
|
|
60
115
|
</colgroup>
|
|
61
116
|
|
|
@@ -66,6 +121,7 @@ export function DataGridTableHeader<ColumnId extends string>({
|
|
|
66
121
|
style={{
|
|
67
122
|
borderRight: '1px solid rgb(var(--border))',
|
|
68
123
|
background: 'rgb(var(--surface-overlay-soft))',
|
|
124
|
+
...controlStyle,
|
|
69
125
|
}}
|
|
70
126
|
>
|
|
71
127
|
<Checkbox
|
|
@@ -76,18 +132,11 @@ export function DataGridTableHeader<ColumnId extends string>({
|
|
|
76
132
|
className="mx-auto"
|
|
77
133
|
/>
|
|
78
134
|
</TableHead>
|
|
79
|
-
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
onResize={onResizeStart}
|
|
85
|
-
sortDir={
|
|
86
|
-
sort?.columnId === column.id ? sort.dir : null
|
|
87
|
-
}
|
|
88
|
-
onSortChange={onSortChange}
|
|
89
|
-
/>
|
|
90
|
-
))}
|
|
135
|
+
{leading.map(renderHeadCell)}
|
|
136
|
+
{fillWidth ? (
|
|
137
|
+
<th aria-hidden style={{ width: 'auto', padding: 0, ...spacerStyle }} />
|
|
138
|
+
) : null}
|
|
139
|
+
{rightPinned.map(renderHeadCell)}
|
|
91
140
|
</TableRow>
|
|
92
141
|
</TableHeader>
|
|
93
142
|
</>
|
|
@@ -6,14 +6,21 @@
|
|
|
6
6
|
* License: MIT (see packages/ui/licenses/gray-ui-csm-LICENSE.txt)
|
|
7
7
|
* Workspace ADR: ADR-025
|
|
8
8
|
*
|
|
9
|
-
* BRING_WITH_REFACTOR: token reskin per ADR-025 D7
|
|
9
|
+
* BRING_WITH_REFACTOR: token reskin per ADR-025 D7; sticky footer +
|
|
10
|
+
* pinned columns + fill-width spacer via the shared sticky helper.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
|
-
import type {
|
|
13
|
+
import type { ReactNode } from 'react'
|
|
13
14
|
|
|
14
15
|
import { cn } from '../lib/utils'
|
|
15
16
|
import { TableCell, TableFooter, TableRow } from './table-elements'
|
|
16
17
|
|
|
18
|
+
import {
|
|
19
|
+
splitAtRightPinned,
|
|
20
|
+
stickyCellStyle,
|
|
21
|
+
type PinInfo,
|
|
22
|
+
type ScrollEdges,
|
|
23
|
+
} from './sticky'
|
|
17
24
|
import type { DataGridColumn, DataGridRowBase } from './types'
|
|
18
25
|
|
|
19
26
|
type DataGridSummaryFooterProps<
|
|
@@ -33,6 +40,9 @@ type DataGridSummaryFooterProps<
|
|
|
33
40
|
columnWidths: Record<ColumnId, number>
|
|
34
41
|
draggingColumnId: ColumnId | null
|
|
35
42
|
isEmptyValue: (value: ReactNode) => boolean
|
|
43
|
+
pinOffsets: Map<ColumnId, PinInfo>
|
|
44
|
+
scrollEdges: ScrollEdges
|
|
45
|
+
fillWidth: boolean
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
export function DataGridSummaryFooter<
|
|
@@ -47,18 +57,48 @@ export function DataGridSummaryFooter<
|
|
|
47
57
|
columnWidths,
|
|
48
58
|
draggingColumnId,
|
|
49
59
|
isEmptyValue,
|
|
60
|
+
pinOffsets,
|
|
61
|
+
scrollEdges,
|
|
62
|
+
fillWidth,
|
|
50
63
|
}: DataGridSummaryFooterProps<Row, ColumnId>) {
|
|
51
64
|
if (!showSummaries || !renderSummary) return null
|
|
52
65
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
const { leading, rightPinned } = splitAtRightPinned(visibleColumns)
|
|
67
|
+
|
|
68
|
+
const footerStyle = (pin: PinInfo | null) =>
|
|
69
|
+
stickyCellStyle({
|
|
70
|
+
role: 'footer',
|
|
71
|
+
pin,
|
|
72
|
+
edges: scrollEdges,
|
|
73
|
+
stickyHeader: false,
|
|
74
|
+
stickyFooter: stickySummaryFooter,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const renderSummaryCell = (column: DataGridColumn<ColumnId>) => {
|
|
78
|
+
const summaryContent = renderSummary(column, visibleRows)
|
|
79
|
+
const isDraggingThisColumn = draggingColumnId === column.id
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<TableCell
|
|
83
|
+
key={`summary-${column.id}`}
|
|
84
|
+
className={cn('h-10 px-2 py-1 text-left text-xs whitespace-nowrap')}
|
|
85
|
+
style={{
|
|
86
|
+
width: columnWidths[column.id],
|
|
87
|
+
minWidth: columnWidths[column.id],
|
|
88
|
+
color: 'rgb(var(--text-muted))',
|
|
89
|
+
borderRight: '1px solid rgb(var(--border))',
|
|
90
|
+
...footerStyle(pinOffsets.get(column.id) ?? null),
|
|
91
|
+
...(isDraggingThisColumn
|
|
92
|
+
? { background: 'rgb(var(--surface-overlay-soft))' }
|
|
93
|
+
: null),
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
{!isEmptyValue(summaryContent) ? (
|
|
97
|
+
<span className="inline-block">{summaryContent}</span>
|
|
98
|
+
) : null}
|
|
99
|
+
</TableCell>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
62
102
|
|
|
63
103
|
return (
|
|
64
104
|
<TableFooter>
|
|
@@ -67,34 +107,17 @@ export function DataGridSummaryFooter<
|
|
|
67
107
|
className={cn('h-10 px-2 py-1')}
|
|
68
108
|
style={{
|
|
69
109
|
borderRight: '1px solid rgb(var(--border))',
|
|
70
|
-
...
|
|
110
|
+
...footerStyle({ side: 'left', offset: 0 }),
|
|
71
111
|
}}
|
|
72
112
|
/>
|
|
73
|
-
{
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
style={{
|
|
82
|
-
width: columnWidths[column.id],
|
|
83
|
-
minWidth: columnWidths[column.id],
|
|
84
|
-
color: 'rgb(var(--text-muted))',
|
|
85
|
-
borderRight: '1px solid rgb(var(--border))',
|
|
86
|
-
background: isDraggingThisColumn
|
|
87
|
-
? 'rgb(var(--surface-overlay-soft))'
|
|
88
|
-
: stickyCellStyle?.background,
|
|
89
|
-
...stickyCellStyle,
|
|
90
|
-
}}
|
|
91
|
-
>
|
|
92
|
-
{!isEmptyValue(summaryContent) ? (
|
|
93
|
-
<span className="inline-block">{summaryContent}</span>
|
|
94
|
-
) : null}
|
|
95
|
-
</TableCell>
|
|
96
|
-
)
|
|
97
|
-
})}
|
|
113
|
+
{leading.map(renderSummaryCell)}
|
|
114
|
+
{fillWidth ? (
|
|
115
|
+
<td
|
|
116
|
+
aria-hidden
|
|
117
|
+
style={{ width: 'auto', padding: 0, ...footerStyle(null) }}
|
|
118
|
+
/>
|
|
119
|
+
) : null}
|
|
120
|
+
{rightPinned.map(renderSummaryCell)}
|
|
98
121
|
</TableRow>
|
|
99
122
|
</TableFooter>
|
|
100
123
|
)
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
DataGridTableBody,
|
|
26
26
|
DataGridTableHeader,
|
|
27
27
|
} from './table-parts'
|
|
28
|
+
import { computePinOffsets, type ScrollEdges } from './sticky'
|
|
28
29
|
import type {
|
|
29
30
|
DataGridColumn,
|
|
30
31
|
DataGridRowBase,
|
|
@@ -73,6 +74,9 @@ type DataGridTableViewProps<
|
|
|
73
74
|
) => ReactNode)
|
|
74
75
|
| undefined
|
|
75
76
|
stickySummaryFooter: boolean
|
|
77
|
+
stickyHeader: boolean
|
|
78
|
+
fillWidth: boolean
|
|
79
|
+
scrollEdges: ScrollEdges
|
|
76
80
|
tableContainerClassName: string | undefined
|
|
77
81
|
isEmptyValue: (value: ReactNode) => boolean
|
|
78
82
|
onResizeStart: (
|
|
@@ -114,6 +118,9 @@ export function DataGridTableView<
|
|
|
114
118
|
showSummaries,
|
|
115
119
|
renderSummary,
|
|
116
120
|
stickySummaryFooter,
|
|
121
|
+
stickyHeader,
|
|
122
|
+
fillWidth,
|
|
123
|
+
scrollEdges,
|
|
117
124
|
tableContainerClassName,
|
|
118
125
|
isEmptyValue,
|
|
119
126
|
onResizeStart,
|
|
@@ -121,6 +128,7 @@ export function DataGridTableView<
|
|
|
121
128
|
sort,
|
|
122
129
|
onSortChange,
|
|
123
130
|
}: DataGridTableViewProps<Row, ColumnId>) {
|
|
131
|
+
const pinOffsets = computePinOffsets(visibleColumns, columnWidths)
|
|
124
132
|
return (
|
|
125
133
|
<Table
|
|
126
134
|
ref={tableRef}
|
|
@@ -137,6 +145,10 @@ export function DataGridTableView<
|
|
|
137
145
|
onResizeStart={onResizeStart}
|
|
138
146
|
sort={sort}
|
|
139
147
|
onSortChange={onSortChange}
|
|
148
|
+
pinOffsets={pinOffsets}
|
|
149
|
+
scrollEdges={scrollEdges}
|
|
150
|
+
stickyHeader={stickyHeader}
|
|
151
|
+
fillWidth={fillWidth}
|
|
140
152
|
/>
|
|
141
153
|
|
|
142
154
|
<DataGridTableBody
|
|
@@ -159,6 +171,9 @@ export function DataGridTableView<
|
|
|
159
171
|
onToggleRowSelection={onToggleRowSelection}
|
|
160
172
|
columnWidths={columnWidths}
|
|
161
173
|
draggingColumnId={draggingColumnId}
|
|
174
|
+
pinOffsets={pinOffsets}
|
|
175
|
+
scrollEdges={scrollEdges}
|
|
176
|
+
fillWidth={fillWidth}
|
|
162
177
|
/>
|
|
163
178
|
|
|
164
179
|
<DataGridSummaryFooter
|
|
@@ -170,6 +185,9 @@ export function DataGridTableView<
|
|
|
170
185
|
columnWidths={columnWidths}
|
|
171
186
|
draggingColumnId={draggingColumnId}
|
|
172
187
|
isEmptyValue={isEmptyValue}
|
|
188
|
+
pinOffsets={pinOffsets}
|
|
189
|
+
scrollEdges={scrollEdges}
|
|
190
|
+
fillWidth={fillWidth}
|
|
173
191
|
/>
|
|
174
192
|
</Table>
|
|
175
193
|
)
|
package/src/data-grid/types.ts
CHANGED
|
@@ -31,6 +31,12 @@ export type DataGridColumn<ColumnId extends string> = {
|
|
|
31
31
|
* `onSortChange` is provided; false when not. Set explicitly to disable
|
|
32
32
|
* sorting for columns that don't have a meaningful comparator. */
|
|
33
33
|
sortable?: boolean
|
|
34
|
+
/** Pin this column to the left or right edge so it stays visible while
|
|
35
|
+
* the grid scrolls horizontally. Pinned columns render opaque with a
|
|
36
|
+
* soft scroll-shadow on their inner edge. Place left-pinned columns
|
|
37
|
+
* first and right-pinned columns last. The built-in select column is
|
|
38
|
+
* always pinned left. */
|
|
39
|
+
pin?: 'left' | 'right'
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
/** Row-sort state (ADR-027). DataGrid renders the chevron + ARIA per
|
|
@@ -115,6 +121,15 @@ export type DataGridProps<
|
|
|
115
121
|
* Defaults to 520 (the FloatingDrawer side="right" default). */
|
|
116
122
|
drawerSize?: number
|
|
117
123
|
stickySummaryFooter?: boolean
|
|
124
|
+
/** Keep the header row pinned to the top while the body scrolls. Renders
|
|
125
|
+
* opaque with a scroll-shadow once scrolled. Default false. */
|
|
126
|
+
stickyHeader?: boolean
|
|
127
|
+
/** Stretch the table to fill its container width via a trailing flex
|
|
128
|
+
* "spacer" column that absorbs leftover space — real columns keep their
|
|
129
|
+
* exact widths, so column resize stays independent. When the columns
|
|
130
|
+
* outgrow the viewport the spacer collapses to 0 and the grid scrolls.
|
|
131
|
+
* Default false. */
|
|
132
|
+
fillWidth?: boolean
|
|
118
133
|
fillAvailableHeight?: boolean
|
|
119
134
|
tableContainerClassName?: string
|
|
120
135
|
onRowsChange?: (rows: Row[]) => void
|