@idealyst/datagrid 1.0.99 → 1.1.1
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/datagrid",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "High-performance datagrid component for React and React Native",
|
|
5
5
|
"documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/datagrid#readme",
|
|
6
6
|
"readme": "README.md",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"publish:npm": "npm publish"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
|
-
"@idealyst/components": "^1.
|
|
40
|
-
"@idealyst/theme": "^1.
|
|
39
|
+
"@idealyst/components": "^1.1.1",
|
|
40
|
+
"@idealyst/theme": "^1.1.1",
|
|
41
41
|
"react": ">=16.8.0",
|
|
42
42
|
"react-native": ">=0.60.0",
|
|
43
43
|
"react-native-unistyles": "^3.0.4",
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
}
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"@idealyst/components": "^1.
|
|
65
|
-
"@idealyst/theme": "^1.
|
|
64
|
+
"@idealyst/components": "^1.1.1",
|
|
65
|
+
"@idealyst/theme": "^1.1.1",
|
|
66
66
|
"@types/react": "^19.1.0",
|
|
67
67
|
"@types/react-window": "^1.8.8",
|
|
68
68
|
"react": "^19.1.0",
|
|
@@ -84,4 +84,4 @@
|
|
|
84
84
|
"react-window",
|
|
85
85
|
"virtualization"
|
|
86
86
|
]
|
|
87
|
-
}
|
|
87
|
+
}
|
|
@@ -22,6 +22,7 @@ export type DataGridStylesheet = {
|
|
|
22
22
|
headerRow: ExpandedDataGridStyles;
|
|
23
23
|
headerCell: ExpandedDataGridStyles;
|
|
24
24
|
headerText: ExpandedDataGridStyles;
|
|
25
|
+
stickyHeaderWrapper: ExpandedDataGridStyles;
|
|
25
26
|
row: ExpandedDataGridStyles;
|
|
26
27
|
cell: ExpandedDataGridStyles;
|
|
27
28
|
spacerRow: ExpandedDataGridStyles;
|
|
@@ -129,6 +130,29 @@ export const dataGridStyles = StyleSheet.create((theme: Theme) => {
|
|
|
129
130
|
},
|
|
130
131
|
}),
|
|
131
132
|
|
|
133
|
+
stickyHeaderWrapper: ({ stickyHeader }: DataGridVariants) => ({
|
|
134
|
+
backgroundColor: theme.colors.surface.primary,
|
|
135
|
+
variants: {
|
|
136
|
+
stickyHeader: {
|
|
137
|
+
true: {
|
|
138
|
+
_web: {
|
|
139
|
+
position: 'sticky',
|
|
140
|
+
top: 0,
|
|
141
|
+
zIndex: 100,
|
|
142
|
+
backgroundColor: theme.colors.surface.primary,
|
|
143
|
+
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
|
144
|
+
},
|
|
145
|
+
_native: {
|
|
146
|
+
// Native doesn't support sticky positioning in the same way
|
|
147
|
+
// We'd need a different implementation for RN
|
|
148
|
+
zIndex: 100,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
false: {},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
}),
|
|
155
|
+
|
|
132
156
|
row: ({ selected }: DataGridVariants) => ({
|
|
133
157
|
borderBottomWidth: 1,
|
|
134
158
|
borderBottomColor: theme.colors.border.primary,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import React, { useState, useCallback, useMemo
|
|
1
|
+
import React, { useState, useCallback, useMemo } from 'react';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
2
3
|
import { View, Text } from '@idealyst/components';
|
|
3
4
|
import { ScrollView } from '../primitives/ScrollView';
|
|
4
5
|
import { Table, TableRow, TableCell, TableHeader, TableBody } from '../primitives/Table';
|
|
@@ -18,6 +19,7 @@ export function DataGrid<T extends Record<string, any>>({
|
|
|
18
19
|
style,
|
|
19
20
|
headerStyle,
|
|
20
21
|
rowStyle,
|
|
22
|
+
cellStyle,
|
|
21
23
|
selectedRows = [],
|
|
22
24
|
onSelectionChange,
|
|
23
25
|
multiSelect = false,
|
|
@@ -27,6 +29,13 @@ export function DataGrid<T extends Record<string, any>>({
|
|
|
27
29
|
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
|
28
30
|
const [scrollTop, setScrollTop] = useState(0);
|
|
29
31
|
|
|
32
|
+
// Calculate minimum table width for horizontal scrolling
|
|
33
|
+
const minTableWidth = useMemo(() => {
|
|
34
|
+
return columns.reduce((total, column) => {
|
|
35
|
+
return total + (column.width ? (typeof column.width === 'number' ? column.width : 120) : 120);
|
|
36
|
+
}, 0);
|
|
37
|
+
}, [columns]);
|
|
38
|
+
|
|
30
39
|
// Virtualization calculations
|
|
31
40
|
const visibleRange = useMemo(() => {
|
|
32
41
|
if (!virtualized || typeof height !== 'number') {
|
|
@@ -34,9 +43,19 @@ export function DataGrid<T extends Record<string, any>>({
|
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
const containerHeight = height - headerHeight;
|
|
37
|
-
const
|
|
38
|
-
|
|
46
|
+
const overscan = 3; // Render extra rows above and below visible area to prevent flickering
|
|
47
|
+
|
|
48
|
+
// Calculate the raw start index based on scroll position
|
|
49
|
+
const rawStartIndex = Math.floor(scrollTop / rowHeight);
|
|
50
|
+
|
|
51
|
+
// Apply overscan to start (but don't go below 0)
|
|
52
|
+
const startIndex = Math.max(0, rawStartIndex - overscan);
|
|
53
|
+
|
|
54
|
+
// Calculate visible count plus overscan on both ends
|
|
55
|
+
const visibleCount = Math.ceil(containerHeight / rowHeight) + (overscan * 2);
|
|
39
56
|
const endIndex = Math.min(startIndex + visibleCount, data.length - 1);
|
|
57
|
+
|
|
58
|
+
// Offset should be based on the actual start index (with overscan applied)
|
|
40
59
|
const offsetY = startIndex * rowHeight;
|
|
41
60
|
|
|
42
61
|
return { start: startIndex, end: endIndex, offsetY };
|
|
@@ -51,13 +70,6 @@ export function DataGrid<T extends Record<string, any>>({
|
|
|
51
70
|
return data.slice(visibleRange.start, visibleRange.end + 1);
|
|
52
71
|
}, [data, visibleRange.start, visibleRange.end, virtualized]);
|
|
53
72
|
|
|
54
|
-
// Calculate minimum table width for horizontal scrolling
|
|
55
|
-
const minTableWidth = useMemo(() => {
|
|
56
|
-
return columns.reduce((total, column) => {
|
|
57
|
-
return total + (column.width ? (typeof column.width === 'number' ? column.width : 120) : 120);
|
|
58
|
-
}, 0);
|
|
59
|
-
}, [columns]);
|
|
60
|
-
|
|
61
73
|
const handleScroll = useCallback((e: any) => {
|
|
62
74
|
if (virtualized) {
|
|
63
75
|
// Handle both web and React Native scroll events
|
|
@@ -67,33 +79,22 @@ export function DataGrid<T extends Record<string, any>>({
|
|
|
67
79
|
}, [virtualized]);
|
|
68
80
|
|
|
69
81
|
// Helper function to get consistent column styles
|
|
82
|
+
// Always use fixed widths to ensure header and body tables stay aligned
|
|
70
83
|
const getColumnStyle = (column: Column<T>) => {
|
|
71
|
-
const
|
|
84
|
+
const width = column.width || column.minWidth || 120;
|
|
85
|
+
return {
|
|
72
86
|
boxSizing: 'border-box' as const,
|
|
73
87
|
flexShrink: 0,
|
|
88
|
+
flexGrow: 0,
|
|
89
|
+
width,
|
|
90
|
+
minWidth: width,
|
|
91
|
+
maxWidth: column.maxWidth || width,
|
|
74
92
|
};
|
|
75
|
-
|
|
76
|
-
if (column.width) {
|
|
77
|
-
return {
|
|
78
|
-
...baseStyle,
|
|
79
|
-
width: column.width,
|
|
80
|
-
flexGrow: 0,
|
|
81
|
-
flexBasis: column.width,
|
|
82
|
-
};
|
|
83
|
-
} else {
|
|
84
|
-
return {
|
|
85
|
-
...baseStyle,
|
|
86
|
-
flexGrow: 1,
|
|
87
|
-
flexBasis: 0,
|
|
88
|
-
minWidth: column.minWidth || 120,
|
|
89
|
-
...(column.maxWidth ? { maxWidth: column.maxWidth } : {}),
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
93
|
};
|
|
93
94
|
|
|
94
95
|
const handleSort = useCallback((column: Column<T>) => {
|
|
95
96
|
if (!column.sortable) return;
|
|
96
|
-
|
|
97
|
+
|
|
97
98
|
const newDirection = sortColumn === column.key && sortDirection === 'asc' ? 'desc' : 'asc';
|
|
98
99
|
setSortColumn(column.key);
|
|
99
100
|
setSortDirection(newDirection);
|
|
@@ -130,25 +131,40 @@ export function DataGrid<T extends Record<string, any>>({
|
|
|
130
131
|
style={{
|
|
131
132
|
...dataGridStyles.headerCell,
|
|
132
133
|
...getColumnStyle(column),
|
|
134
|
+
...cellStyle,
|
|
133
135
|
}}
|
|
134
136
|
onPress={column.sortable ? () => handleSort(column) : undefined}
|
|
135
137
|
>
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
138
|
+
{column.renderHeader ? (
|
|
139
|
+
column.renderHeader()
|
|
140
|
+
) : (
|
|
141
|
+
<Text
|
|
142
|
+
weight="bold"
|
|
143
|
+
style={dataGridStyles.headerText({ clickable: column.sortable || false })}
|
|
144
|
+
>
|
|
145
|
+
{column.header}
|
|
146
|
+
{column.sortable && (
|
|
147
|
+
<Text style={{ marginLeft: 4 }}>
|
|
148
|
+
{sortColumn === column.key ? ` ${sortDirection === 'asc' ? '▲' : '▼'}` : ''}
|
|
149
|
+
</Text>
|
|
150
|
+
)}
|
|
151
|
+
</Text>
|
|
152
|
+
)}
|
|
147
153
|
</TableCell>
|
|
148
154
|
))}
|
|
149
155
|
</TableRow>
|
|
150
156
|
);
|
|
151
157
|
|
|
158
|
+
// Render colgroup to define fixed column widths for table-layout: fixed
|
|
159
|
+
const renderColGroup = () => (
|
|
160
|
+
<colgroup>
|
|
161
|
+
{columns.map((column) => {
|
|
162
|
+
const width = column.width || column.minWidth || 120;
|
|
163
|
+
return <col key={column.key} style={{ width, minWidth: width, maxWidth: width }} />;
|
|
164
|
+
})}
|
|
165
|
+
</colgroup>
|
|
166
|
+
);
|
|
167
|
+
|
|
152
168
|
const renderRow = (item: T, virtualIndex: number) => {
|
|
153
169
|
const actualIndex = virtualized ? visibleRange.start + virtualIndex : virtualIndex;
|
|
154
170
|
const isSelected = selectedRows.includes(actualIndex);
|
|
@@ -171,15 +187,18 @@ export function DataGrid<T extends Record<string, any>>({
|
|
|
171
187
|
? column.cellStyle(value, item)
|
|
172
188
|
: column.cellStyle;
|
|
173
189
|
|
|
190
|
+
|
|
191
|
+
const styles = {
|
|
192
|
+
...getColumnStyle(column),
|
|
193
|
+
...computedCellStyle,
|
|
194
|
+
...cellStyle,
|
|
195
|
+
}
|
|
196
|
+
|
|
174
197
|
return (
|
|
175
198
|
<TableCell
|
|
176
199
|
key={column.key}
|
|
177
200
|
width={column.width}
|
|
178
|
-
style={
|
|
179
|
-
...dataGridStyles.cell({ alignment: column.align || 'left' }),
|
|
180
|
-
...getColumnStyle(column),
|
|
181
|
-
...computedCellStyle,
|
|
182
|
-
}}
|
|
201
|
+
style={styles}
|
|
183
202
|
>
|
|
184
203
|
{typeof cellContent === 'string' || typeof cellContent === 'number' ? (
|
|
185
204
|
<Text>{cellContent}</Text>
|
|
@@ -194,7 +213,68 @@ export function DataGrid<T extends Record<string, any>>({
|
|
|
194
213
|
};
|
|
195
214
|
|
|
196
215
|
const containerHeight = typeof height === 'number' ? height : undefined;
|
|
216
|
+
const isWeb = Platform.OS === 'web';
|
|
197
217
|
|
|
218
|
+
// For web with sticky header, use a single table with sticky thead
|
|
219
|
+
if (isWeb && stickyHeader) {
|
|
220
|
+
return (
|
|
221
|
+
<View style={{
|
|
222
|
+
...dataGridStyles.container,
|
|
223
|
+
width,
|
|
224
|
+
height,
|
|
225
|
+
...style,
|
|
226
|
+
}}>
|
|
227
|
+
<div
|
|
228
|
+
style={{
|
|
229
|
+
flex: 1,
|
|
230
|
+
overflow: 'auto',
|
|
231
|
+
maxHeight: containerHeight,
|
|
232
|
+
WebkitOverflowScrolling: 'touch',
|
|
233
|
+
} as React.CSSProperties}
|
|
234
|
+
onScroll={handleScroll as any}
|
|
235
|
+
>
|
|
236
|
+
<Table style={{ width: minTableWidth, minWidth: minTableWidth }}>
|
|
237
|
+
{renderColGroup()}
|
|
238
|
+
<TableHeader style={{
|
|
239
|
+
...dataGridStyles.header({ stickyHeader: true }),
|
|
240
|
+
position: 'sticky',
|
|
241
|
+
top: 0,
|
|
242
|
+
zIndex: 100,
|
|
243
|
+
backgroundColor: '#fff',
|
|
244
|
+
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
|
245
|
+
}}>
|
|
246
|
+
{renderHeader()}
|
|
247
|
+
</TableHeader>
|
|
248
|
+
<TableBody>
|
|
249
|
+
{virtualized && visibleRange.offsetY > 0 && (
|
|
250
|
+
<TableRow style={{ ...dataGridStyles.spacerRow, height: visibleRange.offsetY }}>
|
|
251
|
+
<TableCell
|
|
252
|
+
style={{ ...dataGridStyles.spacerCell, height: visibleRange.offsetY }}
|
|
253
|
+
colSpan={columns.length}
|
|
254
|
+
>
|
|
255
|
+
<View />
|
|
256
|
+
</TableCell>
|
|
257
|
+
</TableRow>
|
|
258
|
+
)}
|
|
259
|
+
{visibleData.map((item, index) => renderRow(item, index))}
|
|
260
|
+
{virtualized && (data.length - visibleRange.end - 1) > 0 && (
|
|
261
|
+
<TableRow style={{ ...dataGridStyles.spacerRow, height: (data.length - visibleRange.end - 1) * rowHeight }}>
|
|
262
|
+
<TableCell
|
|
263
|
+
style={{ ...dataGridStyles.spacerCell, height: (data.length - visibleRange.end - 1) * rowHeight }}
|
|
264
|
+
colSpan={columns.length}
|
|
265
|
+
>
|
|
266
|
+
<View />
|
|
267
|
+
</TableCell>
|
|
268
|
+
</TableRow>
|
|
269
|
+
)}
|
|
270
|
+
</TableBody>
|
|
271
|
+
</Table>
|
|
272
|
+
</div>
|
|
273
|
+
</View>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Native or non-sticky: use ScrollView abstraction
|
|
198
278
|
return (
|
|
199
279
|
<View style={{
|
|
200
280
|
...dataGridStyles.container,
|
|
@@ -205,51 +285,48 @@ export function DataGrid<T extends Record<string, any>>({
|
|
|
205
285
|
<ScrollView
|
|
206
286
|
style={{
|
|
207
287
|
...dataGridStyles.scrollView,
|
|
208
|
-
...(containerHeight ? { maxHeight: containerHeight } : {})
|
|
288
|
+
...(containerHeight ? { maxHeight: containerHeight } : {}),
|
|
209
289
|
}}
|
|
210
290
|
contentContainerStyle={{
|
|
211
|
-
|
|
212
|
-
width: minTableWidth,
|
|
291
|
+
minWidth: minTableWidth,
|
|
213
292
|
}}
|
|
214
293
|
showsVerticalScrollIndicator={true}
|
|
215
294
|
showsHorizontalScrollIndicator={true}
|
|
216
295
|
onScroll={handleScroll}
|
|
217
296
|
scrollEventThrottle={16}
|
|
218
297
|
>
|
|
219
|
-
<
|
|
220
|
-
...
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
</Table>
|
|
298
|
+
<View style={{ minWidth: minTableWidth }}>
|
|
299
|
+
<Table style={{ width: minTableWidth, ...(virtualized ? { height: totalHeight } : {}) }}>
|
|
300
|
+
{renderColGroup()}
|
|
301
|
+
<TableHeader style={dataGridStyles.header({ stickyHeader: false })}>
|
|
302
|
+
{renderHeader()}
|
|
303
|
+
</TableHeader>
|
|
304
|
+
<TableBody>
|
|
305
|
+
{virtualized && visibleRange.offsetY > 0 && (
|
|
306
|
+
<TableRow style={{ ...dataGridStyles.spacerRow, height: visibleRange.offsetY }}>
|
|
307
|
+
<TableCell
|
|
308
|
+
style={{ ...dataGridStyles.spacerCell, height: visibleRange.offsetY }}
|
|
309
|
+
colSpan={columns.length}
|
|
310
|
+
>
|
|
311
|
+
<View />
|
|
312
|
+
</TableCell>
|
|
313
|
+
</TableRow>
|
|
314
|
+
)}
|
|
315
|
+
{visibleData.map((item, index) => renderRow(item, index))}
|
|
316
|
+
{virtualized && (data.length - visibleRange.end - 1) > 0 && (
|
|
317
|
+
<TableRow style={{ ...dataGridStyles.spacerRow, height: (data.length - visibleRange.end - 1) * rowHeight }}>
|
|
318
|
+
<TableCell
|
|
319
|
+
style={{ ...dataGridStyles.spacerCell, height: (data.length - visibleRange.end - 1) * rowHeight }}
|
|
320
|
+
colSpan={columns.length}
|
|
321
|
+
>
|
|
322
|
+
<View />
|
|
323
|
+
</TableCell>
|
|
324
|
+
</TableRow>
|
|
325
|
+
)}
|
|
326
|
+
</TableBody>
|
|
327
|
+
</Table>
|
|
328
|
+
</View>
|
|
251
329
|
</ScrollView>
|
|
252
330
|
</View>
|
|
253
331
|
);
|
|
254
332
|
}
|
|
255
|
-
|
package/src/DataGrid/types.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface Column<T = any> {
|
|
|
11
11
|
align?: 'left' | 'center' | 'right';
|
|
12
12
|
accessor?: (row: T) => any;
|
|
13
13
|
render?: (value: any, row: T, index: number) => React.ReactNode;
|
|
14
|
+
/** Custom header renderer - if provided, renders instead of header string */
|
|
15
|
+
renderHeader?: () => React.ReactNode;
|
|
14
16
|
headerStyle?: ViewStyle;
|
|
15
17
|
cellStyle?: ViewStyle | ((value: any, row: T) => ViewStyle);
|
|
16
18
|
}
|
|
@@ -27,6 +29,7 @@ export interface DataGridProps<T = any> {
|
|
|
27
29
|
width?: number | string;
|
|
28
30
|
style?: ViewStyle;
|
|
29
31
|
headerStyle?: ViewStyle;
|
|
32
|
+
cellStyle?: ViewStyle;
|
|
30
33
|
rowStyle?: ViewStyle | ((row: T, index: number) => ViewStyle);
|
|
31
34
|
selectedRows?: number[];
|
|
32
35
|
onSelectionChange?: (selectedRows: number[]) => void;
|
|
@@ -78,23 +78,13 @@ interface TableCellProps {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
export const TableCell: React.FC<TableCellProps> = ({ children, style, width, colSpan, onPress }) => {
|
|
81
|
-
let resolvedStyle = {};
|
|
82
|
-
if (typeof style === 'function') {
|
|
83
|
-
try {
|
|
84
|
-
resolvedStyle = style(UnistylesRuntime.theme);
|
|
85
|
-
} catch (error) {
|
|
86
|
-
resolvedStyle = {};
|
|
87
|
-
}
|
|
88
|
-
} else if (style) {
|
|
89
|
-
resolvedStyle = style;
|
|
90
|
-
}
|
|
91
81
|
|
|
92
82
|
const combinedStyle = {
|
|
93
83
|
verticalAlign: 'middle',
|
|
94
84
|
...(width && { width }),
|
|
95
|
-
...
|
|
85
|
+
...style,
|
|
96
86
|
};
|
|
97
|
-
|
|
87
|
+
|
|
98
88
|
return (
|
|
99
89
|
<td
|
|
100
90
|
style={combinedStyle}
|