@idealyst/datagrid 1.0.99 → 1.1.0

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.0.99",
3
+ "version": "1.1.0",
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.0.99",
40
- "@idealyst/theme": "^1.0.99",
39
+ "@idealyst/components": "^1.1.0",
40
+ "@idealyst/theme": "^1.1.0",
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.0.99",
65
- "@idealyst/theme": "^1.0.99",
64
+ "@idealyst/components": "^1.1.0",
65
+ "@idealyst/theme": "^1.1.0",
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, useRef } from 'react';
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 startIndex = Math.floor(scrollTop / rowHeight);
38
- const visibleCount = Math.ceil(containerHeight / rowHeight) + 2; // Add buffer
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 baseStyle = {
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
- <Text
137
- weight="bold"
138
- style={dataGridStyles.headerText({ clickable: column.sortable || false })}
139
- >
140
- {column.header}
141
- {column.sortable && (
142
- <Text style={{ marginLeft: 4 }}>
143
- {sortColumn === column.key ? ` ${sortDirection === 'asc' ? '▲' : '▼'}` : ''}
144
- </Text>
145
- )}
146
- </Text>
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
- ...dataGridStyles.scrollViewContent,
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
- <Table style={{
220
- ...dataGridStyles.table,
221
- width: minTableWidth,
222
- ...(virtualized ? { height: totalHeight } : {})
223
- }}>
224
- <TableHeader style={dataGridStyles.header({ stickyHeader })}>
225
- {renderHeader()}
226
- </TableHeader>
227
- <TableBody>
228
- {virtualized && visibleRange.offsetY > 0 && (
229
- <TableRow style={{ ...dataGridStyles.spacerRow, height: visibleRange.offsetY }}>
230
- <TableCell
231
- style={{ ...dataGridStyles.spacerCell, height: visibleRange.offsetY }}
232
- colSpan={columns.length}
233
- >
234
- <View />
235
- </TableCell>
236
- </TableRow>
237
- )}
238
- {visibleData.map((item, index) => renderRow(item, index))}
239
- {virtualized && (data.length - visibleRange.end - 1) > 0 && (
240
- <TableRow style={{ ...dataGridStyles.spacerRow, height: (data.length - visibleRange.end - 1) * rowHeight }}>
241
- <TableCell
242
- style={{ ...dataGridStyles.spacerCell, height: (data.length - visibleRange.end - 1) * rowHeight }}
243
- colSpan={columns.length}
244
- >
245
- <View />
246
- </TableCell>
247
- </TableRow>
248
- )}
249
- </TableBody>
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
-
@@ -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
- ...resolvedStyle,
85
+ ...style,
96
86
  };
97
-
87
+
98
88
  return (
99
89
  <td
100
90
  style={combinedStyle}