@idealyst/datagrid 1.0.40
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/README.md +217 -0
- package/package.json +81 -0
- package/src/DataGrid/DataGrid.tsx +196 -0
- package/src/DataGrid/index.ts +2 -0
- package/src/DataGrid/types.ts +59 -0
- package/src/examples/BasicExample.tsx +136 -0
- package/src/examples/index.ts +1 -0
- package/src/index.native.ts +2 -0
- package/src/index.ts +2 -0
- package/src/primitives/ScrollView/ScrollView.native.tsx +10 -0
- package/src/primitives/ScrollView/ScrollView.web.tsx +45 -0
- package/src/primitives/ScrollView/index.native.ts +1 -0
- package/src/primitives/ScrollView/index.ts +1 -0
- package/src/primitives/VirtualizedList/VirtualizedList.native.tsx +42 -0
- package/src/primitives/VirtualizedList/VirtualizedList.web.tsx +44 -0
- package/src/primitives/VirtualizedList/index.native.ts +1 -0
- package/src/primitives/VirtualizedList/index.ts +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# @idealyst/datagrid
|
|
2
|
+
|
|
3
|
+
High-performance, cross-platform datagrid component for React and React Native, built on top of @idealyst/components.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🚀 **Virtualized Rendering** - Powered by react-window for web, native FlatList for mobile
|
|
8
|
+
- 📱 **Cross-Platform** - Works seamlessly on React and React Native
|
|
9
|
+
- 🎨 **Theme Integration** - Built with @idealyst/theme and Unistyles
|
|
10
|
+
- 📊 **Rich Features** - Sorting, selection, custom rendering, and more
|
|
11
|
+
- âš¡ **Performance** - Optimized for large datasets with virtualization
|
|
12
|
+
- 🔧 **Customizable** - Flexible column configuration and styling
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @idealyst/datagrid
|
|
18
|
+
# or
|
|
19
|
+
yarn add @idealyst/datagrid
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Peer Dependencies
|
|
23
|
+
|
|
24
|
+
This package requires the following peer dependencies:
|
|
25
|
+
|
|
26
|
+
- `@idealyst/components` - Core component library
|
|
27
|
+
- `@idealyst/theme` - Theme system
|
|
28
|
+
- `react` >= 16.8.0
|
|
29
|
+
- `react-window` (for web virtualization)
|
|
30
|
+
- `react-native` >= 0.60.0 (for native)
|
|
31
|
+
- `react-native-unistyles` - Styling system
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
### Basic Example
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { DataGrid } from '@idealyst/datagrid';
|
|
39
|
+
|
|
40
|
+
const data = [
|
|
41
|
+
{ id: 1, name: 'John Doe', email: 'john@example.com', age: 30 },
|
|
42
|
+
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', age: 25 },
|
|
43
|
+
// ... more data
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const columns = [
|
|
47
|
+
{ key: 'id', header: 'ID', width: 60 },
|
|
48
|
+
{ key: 'name', header: 'Name', width: 150 },
|
|
49
|
+
{ key: 'email', header: 'Email', width: 200 },
|
|
50
|
+
{ key: 'age', header: 'Age', width: 80, sortable: true },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
function MyDataGrid() {
|
|
54
|
+
return (
|
|
55
|
+
<DataGrid
|
|
56
|
+
data={data}
|
|
57
|
+
columns={columns}
|
|
58
|
+
height={400}
|
|
59
|
+
virtualized={true}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Advanced Features
|
|
66
|
+
|
|
67
|
+
#### Custom Cell Rendering
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
const columns = [
|
|
71
|
+
{
|
|
72
|
+
key: 'status',
|
|
73
|
+
header: 'Status',
|
|
74
|
+
render: (value) => (
|
|
75
|
+
<Badge variant="filled" intent={value === 'active' ? 'success' : 'neutral'}>
|
|
76
|
+
{value}
|
|
77
|
+
</Badge>
|
|
78
|
+
),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
key: 'actions',
|
|
82
|
+
header: 'Actions',
|
|
83
|
+
render: (value, row) => (
|
|
84
|
+
<Button size="small" onPress={() => handleEdit(row)}>
|
|
85
|
+
Edit
|
|
86
|
+
</Button>
|
|
87
|
+
),
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
#### Row Selection
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
function SelectableDataGrid() {
|
|
96
|
+
const [selectedRows, setSelectedRows] = useState<number[]>([]);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<DataGrid
|
|
100
|
+
data={data}
|
|
101
|
+
columns={columns}
|
|
102
|
+
selectedRows={selectedRows}
|
|
103
|
+
onSelectionChange={setSelectedRows}
|
|
104
|
+
multiSelect={true}
|
|
105
|
+
onRowClick={(row, index) => console.log('Clicked:', row)}
|
|
106
|
+
/>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### Sorting
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
function SortableDataGrid() {
|
|
115
|
+
const [sortedData, setSortedData] = useState(data);
|
|
116
|
+
|
|
117
|
+
const handleSort = (column, direction) => {
|
|
118
|
+
const sorted = [...data].sort((a, b) => {
|
|
119
|
+
const aVal = a[column.key];
|
|
120
|
+
const bVal = b[column.key];
|
|
121
|
+
return direction === 'asc'
|
|
122
|
+
? aVal > bVal ? 1 : -1
|
|
123
|
+
: aVal < bVal ? 1 : -1;
|
|
124
|
+
});
|
|
125
|
+
setSortedData(sorted);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<DataGrid
|
|
130
|
+
data={sortedData}
|
|
131
|
+
columns={columns}
|
|
132
|
+
onSort={handleSort}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
#### Dynamic Styling
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
<DataGrid
|
|
142
|
+
data={data}
|
|
143
|
+
columns={columns}
|
|
144
|
+
rowStyle={(row, index) => ({
|
|
145
|
+
backgroundColor: index % 2 === 0 ? '#f5f5f5' : 'white',
|
|
146
|
+
})}
|
|
147
|
+
headerStyle={{
|
|
148
|
+
backgroundColor: '#e0e0e0',
|
|
149
|
+
fontWeight: 'bold',
|
|
150
|
+
}}
|
|
151
|
+
/>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## API Reference
|
|
155
|
+
|
|
156
|
+
### DataGrid Props
|
|
157
|
+
|
|
158
|
+
| Prop | Type | Default | Description |
|
|
159
|
+
|------|------|---------|-------------|
|
|
160
|
+
| `data` | `T[]` | Required | Array of data objects |
|
|
161
|
+
| `columns` | `Column<T>[]` | Required | Column configuration |
|
|
162
|
+
| `height` | `number \| string` | `400` | Height of the grid |
|
|
163
|
+
| `width` | `number \| string` | `'100%'` | Width of the grid |
|
|
164
|
+
| `rowHeight` | `number` | `48` | Height of each row |
|
|
165
|
+
| `headerHeight` | `number` | `56` | Height of the header |
|
|
166
|
+
| `virtualized` | `boolean` | `true` | Enable virtualization for large datasets |
|
|
167
|
+
| `stickyHeader` | `boolean` | `true` | Keep header visible when scrolling |
|
|
168
|
+
| `selectedRows` | `number[]` | `[]` | Array of selected row indices |
|
|
169
|
+
| `multiSelect` | `boolean` | `false` | Allow multiple row selection |
|
|
170
|
+
| `onRowClick` | `(row: T, index: number) => void` | - | Row click handler |
|
|
171
|
+
| `onSelectionChange` | `(rows: number[]) => void` | - | Selection change handler |
|
|
172
|
+
| `onSort` | `(column: Column, direction: 'asc' \| 'desc') => void` | - | Sort handler |
|
|
173
|
+
| `style` | `ViewStyle` | - | Container style |
|
|
174
|
+
| `headerStyle` | `ViewStyle` | - | Header style |
|
|
175
|
+
| `rowStyle` | `ViewStyle \| ((row: T, index: number) => ViewStyle)` | - | Row style |
|
|
176
|
+
|
|
177
|
+
### Column Configuration
|
|
178
|
+
|
|
179
|
+
| Property | Type | Description |
|
|
180
|
+
|----------|------|-------------|
|
|
181
|
+
| `key` | `string` | Unique column identifier |
|
|
182
|
+
| `header` | `string` | Column header text |
|
|
183
|
+
| `width` | `number` | Fixed column width |
|
|
184
|
+
| `minWidth` | `number` | Minimum column width |
|
|
185
|
+
| `maxWidth` | `number` | Maximum column width |
|
|
186
|
+
| `sortable` | `boolean` | Enable sorting for this column |
|
|
187
|
+
| `resizable` | `boolean` | Enable resizing (coming soon) |
|
|
188
|
+
| `accessor` | `(row: T) => any` | Custom value accessor |
|
|
189
|
+
| `render` | `(value: any, row: T, index: number) => ReactNode` | Custom cell renderer |
|
|
190
|
+
| `headerStyle` | `ViewStyle` | Header cell style |
|
|
191
|
+
| `cellStyle` | `ViewStyle \| ((value: any, row: T) => ViewStyle)` | Cell style |
|
|
192
|
+
|
|
193
|
+
## Architecture
|
|
194
|
+
|
|
195
|
+
The DataGrid is built using a layered architecture:
|
|
196
|
+
|
|
197
|
+
1. **Platform Primitives** - Low-level components that handle platform differences:
|
|
198
|
+
- `ScrollView` - Native scrolling for each platform
|
|
199
|
+
- `VirtualizedList` - react-window for web, FlatList for native
|
|
200
|
+
|
|
201
|
+
2. **DataGrid Component** - Unified component that uses primitives:
|
|
202
|
+
- Single implementation works on all platforms
|
|
203
|
+
- Composes @idealyst/components for UI elements
|
|
204
|
+
- Uses Unistyles for theming
|
|
205
|
+
|
|
206
|
+
This approach ensures maximum code reuse while optimizing for each platform's strengths.
|
|
207
|
+
|
|
208
|
+
## Performance Tips
|
|
209
|
+
|
|
210
|
+
1. **Use Virtualization** - Enable `virtualized={true}` for datasets > 100 rows
|
|
211
|
+
2. **Set Fixed Heights** - Provide `rowHeight` for better performance
|
|
212
|
+
3. **Optimize Renders** - Use `React.memo` for custom cell renderers
|
|
213
|
+
4. **Limit Columns** - Consider horizontal scrolling for many columns
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@idealyst/datagrid",
|
|
3
|
+
"version": "1.0.40",
|
|
4
|
+
"description": "High-performance datagrid component for React and React Native",
|
|
5
|
+
"documentation": "https://github.com/your-username/idealyst-framework/tree/main/packages/datagrid#readme",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"module": "src/index.ts",
|
|
8
|
+
"types": "src/index.ts",
|
|
9
|
+
"react-native": "src/index.native.ts",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/your-username/idealyst-framework.git",
|
|
13
|
+
"directory": "packages/datagrid"
|
|
14
|
+
},
|
|
15
|
+
"author": "Your Name <your.email@example.com>",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"react-native": "./src/index.native.ts",
|
|
23
|
+
"import": "./src/index.ts",
|
|
24
|
+
"require": "./src/index.ts",
|
|
25
|
+
"types": "./src/index.ts"
|
|
26
|
+
},
|
|
27
|
+
"./examples": {
|
|
28
|
+
"import": "./src/examples/index.ts",
|
|
29
|
+
"require": "./src/examples/index.ts",
|
|
30
|
+
"types": "./src/examples/index.ts"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"prepublishOnly": "echo 'Publishing TypeScript source directly'",
|
|
35
|
+
"publish:npm": "npm publish"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@idealyst/components": "^1.0.40",
|
|
39
|
+
"@idealyst/theme": "^1.0.40",
|
|
40
|
+
"react": ">=16.8.0",
|
|
41
|
+
"react-native": ">=0.60.0",
|
|
42
|
+
"react-native-unistyles": "^3.0.4",
|
|
43
|
+
"react-window": "^1.8.10"
|
|
44
|
+
},
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"@idealyst/components": {
|
|
47
|
+
"optional": false
|
|
48
|
+
},
|
|
49
|
+
"@idealyst/theme": {
|
|
50
|
+
"optional": false
|
|
51
|
+
},
|
|
52
|
+
"react-native": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"react-native-unistyles": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"react-window": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/react": "^19.1.0",
|
|
64
|
+
"@types/react-window": "^1.8.8",
|
|
65
|
+
"typescript": "^5.0.0"
|
|
66
|
+
},
|
|
67
|
+
"files": [
|
|
68
|
+
"src",
|
|
69
|
+
"README.md"
|
|
70
|
+
],
|
|
71
|
+
"keywords": [
|
|
72
|
+
"react",
|
|
73
|
+
"react-native",
|
|
74
|
+
"datagrid",
|
|
75
|
+
"table",
|
|
76
|
+
"grid",
|
|
77
|
+
"cross-platform",
|
|
78
|
+
"react-window",
|
|
79
|
+
"virtualization"
|
|
80
|
+
]
|
|
81
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import React, { useState, useMemo, useCallback } from 'react';
|
|
2
|
+
import { View, Text, Button } from '@idealyst/components';
|
|
3
|
+
import { createStyleSheet, useStyles } from 'react-native-unistyles';
|
|
4
|
+
import { ScrollView } from '../primitives/ScrollView';
|
|
5
|
+
import { VirtualizedList } from '../primitives/VirtualizedList';
|
|
6
|
+
import type { DataGridProps, Column } from './types';
|
|
7
|
+
|
|
8
|
+
export function DataGrid<T extends Record<string, any>>({
|
|
9
|
+
data,
|
|
10
|
+
columns,
|
|
11
|
+
rowHeight = 48,
|
|
12
|
+
headerHeight = 56,
|
|
13
|
+
onRowClick,
|
|
14
|
+
onSort,
|
|
15
|
+
virtualized = true,
|
|
16
|
+
height = 400,
|
|
17
|
+
width = '100%',
|
|
18
|
+
style,
|
|
19
|
+
headerStyle,
|
|
20
|
+
rowStyle,
|
|
21
|
+
selectedRows = [],
|
|
22
|
+
onSelectionChange,
|
|
23
|
+
multiSelect = false,
|
|
24
|
+
stickyHeader = true,
|
|
25
|
+
}: DataGridProps<T>) {
|
|
26
|
+
const { styles } = useStyles(stylesheet);
|
|
27
|
+
const [sortColumn, setSortColumn] = useState<string | null>(null);
|
|
28
|
+
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
|
29
|
+
|
|
30
|
+
const handleSort = useCallback((column: Column<T>) => {
|
|
31
|
+
if (!column.sortable) return;
|
|
32
|
+
|
|
33
|
+
const newDirection = sortColumn === column.key && sortDirection === 'asc' ? 'desc' : 'asc';
|
|
34
|
+
setSortColumn(column.key);
|
|
35
|
+
setSortDirection(newDirection);
|
|
36
|
+
onSort?.(column, newDirection);
|
|
37
|
+
}, [sortColumn, sortDirection, onSort]);
|
|
38
|
+
|
|
39
|
+
const handleRowClick = useCallback((row: T, index: number) => {
|
|
40
|
+
if (onSelectionChange) {
|
|
41
|
+
let newSelection: number[];
|
|
42
|
+
if (multiSelect) {
|
|
43
|
+
if (selectedRows.includes(index)) {
|
|
44
|
+
newSelection = selectedRows.filter(i => i !== index);
|
|
45
|
+
} else {
|
|
46
|
+
newSelection = [...selectedRows, index];
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
newSelection = selectedRows.includes(index) ? [] : [index];
|
|
50
|
+
}
|
|
51
|
+
onSelectionChange(newSelection);
|
|
52
|
+
}
|
|
53
|
+
onRowClick?.(row, index);
|
|
54
|
+
}, [selectedRows, onSelectionChange, multiSelect, onRowClick]);
|
|
55
|
+
|
|
56
|
+
const renderHeader = () => (
|
|
57
|
+
<View style={[styles.header, headerStyle]}>
|
|
58
|
+
{columns.map((column) => (
|
|
59
|
+
<View
|
|
60
|
+
key={column.key}
|
|
61
|
+
style={[
|
|
62
|
+
styles.headerCell,
|
|
63
|
+
{ width: column.width || 'auto', minWidth: column.minWidth, maxWidth: column.maxWidth },
|
|
64
|
+
column.headerStyle,
|
|
65
|
+
]}
|
|
66
|
+
>
|
|
67
|
+
<Text weight="bold" style={styles.headerText}>
|
|
68
|
+
{column.header}
|
|
69
|
+
</Text>
|
|
70
|
+
{column.sortable && (
|
|
71
|
+
<Text style={styles.sortIndicator}>
|
|
72
|
+
{sortColumn === column.key ? (sortDirection === 'asc' ? 'â–²' : 'â–¼') : ''}
|
|
73
|
+
</Text>
|
|
74
|
+
)}
|
|
75
|
+
</View>
|
|
76
|
+
))}
|
|
77
|
+
</View>
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const renderRow = ({ item, index }: { item: T; index: number }) => {
|
|
81
|
+
const isSelected = selectedRows.includes(index);
|
|
82
|
+
const computedRowStyle = typeof rowStyle === 'function' ? rowStyle(item, index) : rowStyle;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<View
|
|
86
|
+
style={[
|
|
87
|
+
styles.row,
|
|
88
|
+
isSelected && styles.selectedRow,
|
|
89
|
+
computedRowStyle,
|
|
90
|
+
]}
|
|
91
|
+
onPress={() => handleRowClick(item, index)}
|
|
92
|
+
>
|
|
93
|
+
{columns.map((column) => {
|
|
94
|
+
const value = column.accessor ? column.accessor(item) : item[column.key];
|
|
95
|
+
const cellContent = column.render ? column.render(value, item, index) : value;
|
|
96
|
+
const computedCellStyle = typeof column.cellStyle === 'function'
|
|
97
|
+
? column.cellStyle(value, item)
|
|
98
|
+
: column.cellStyle;
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<View
|
|
102
|
+
key={column.key}
|
|
103
|
+
style={[
|
|
104
|
+
styles.cell,
|
|
105
|
+
{ width: column.width || 'auto', minWidth: column.minWidth, maxWidth: column.maxWidth },
|
|
106
|
+
computedCellStyle,
|
|
107
|
+
]}
|
|
108
|
+
>
|
|
109
|
+
{typeof cellContent === 'string' || typeof cellContent === 'number' ? (
|
|
110
|
+
<Text>{cellContent}</Text>
|
|
111
|
+
) : (
|
|
112
|
+
cellContent
|
|
113
|
+
)}
|
|
114
|
+
</View>
|
|
115
|
+
);
|
|
116
|
+
})}
|
|
117
|
+
</View>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const containerHeight = typeof height === 'number' ? height : undefined;
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<View style={[styles.container, { width, height }, style]}>
|
|
125
|
+
{stickyHeader && renderHeader()}
|
|
126
|
+
{virtualized && containerHeight ? (
|
|
127
|
+
<VirtualizedList
|
|
128
|
+
data={data}
|
|
129
|
+
renderItem={renderRow}
|
|
130
|
+
itemHeight={rowHeight}
|
|
131
|
+
height={containerHeight - (stickyHeader ? headerHeight : 0)}
|
|
132
|
+
width={width}
|
|
133
|
+
/>
|
|
134
|
+
) : (
|
|
135
|
+
<ScrollView
|
|
136
|
+
style={styles.scrollView}
|
|
137
|
+
showsVerticalScrollIndicator={true}
|
|
138
|
+
>
|
|
139
|
+
{!stickyHeader && renderHeader()}
|
|
140
|
+
{data.map((item, index) => renderRow({ item, index }))}
|
|
141
|
+
</ScrollView>
|
|
142
|
+
)}
|
|
143
|
+
</View>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const stylesheet = createStyleSheet((theme) => ({
|
|
148
|
+
container: {
|
|
149
|
+
backgroundColor: theme.colors.background,
|
|
150
|
+
borderWidth: 1,
|
|
151
|
+
borderColor: theme.colors.neutral[200],
|
|
152
|
+
borderRadius: theme.radius.md,
|
|
153
|
+
overflow: 'hidden',
|
|
154
|
+
},
|
|
155
|
+
header: {
|
|
156
|
+
flexDirection: 'row',
|
|
157
|
+
backgroundColor: theme.colors.neutral[50],
|
|
158
|
+
borderBottomWidth: 2,
|
|
159
|
+
borderBottomColor: theme.colors.neutral[200],
|
|
160
|
+
},
|
|
161
|
+
headerCell: {
|
|
162
|
+
flexDirection: 'row',
|
|
163
|
+
alignItems: 'center',
|
|
164
|
+
justifyContent: 'space-between',
|
|
165
|
+
padding: theme.spacing.sm,
|
|
166
|
+
borderRightWidth: 1,
|
|
167
|
+
borderRightColor: theme.colors.neutral[200],
|
|
168
|
+
},
|
|
169
|
+
headerText: {
|
|
170
|
+
fontSize: 14,
|
|
171
|
+
color: theme.colors.neutral[700],
|
|
172
|
+
},
|
|
173
|
+
sortIndicator: {
|
|
174
|
+
fontSize: 10,
|
|
175
|
+
marginLeft: theme.spacing.xs,
|
|
176
|
+
color: theme.colors.primary[500],
|
|
177
|
+
},
|
|
178
|
+
row: {
|
|
179
|
+
flexDirection: 'row',
|
|
180
|
+
borderBottomWidth: 1,
|
|
181
|
+
borderBottomColor: theme.colors.neutral[100],
|
|
182
|
+
backgroundColor: theme.colors.background,
|
|
183
|
+
},
|
|
184
|
+
selectedRow: {
|
|
185
|
+
backgroundColor: theme.colors.primary[50],
|
|
186
|
+
},
|
|
187
|
+
cell: {
|
|
188
|
+
padding: theme.spacing.sm,
|
|
189
|
+
borderRightWidth: 1,
|
|
190
|
+
borderRightColor: theme.colors.neutral[100],
|
|
191
|
+
justifyContent: 'center',
|
|
192
|
+
},
|
|
193
|
+
scrollView: {
|
|
194
|
+
flex: 1,
|
|
195
|
+
},
|
|
196
|
+
}));
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ViewStyle, TextStyle } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export interface Column<T = any> {
|
|
4
|
+
key: string;
|
|
5
|
+
header: string;
|
|
6
|
+
width?: number;
|
|
7
|
+
minWidth?: number;
|
|
8
|
+
maxWidth?: number;
|
|
9
|
+
resizable?: boolean;
|
|
10
|
+
sortable?: boolean;
|
|
11
|
+
accessor?: (row: T) => any;
|
|
12
|
+
render?: (value: any, row: T, index: number) => React.ReactNode;
|
|
13
|
+
headerStyle?: ViewStyle;
|
|
14
|
+
cellStyle?: ViewStyle | ((value: any, row: T) => ViewStyle);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DataGridProps<T = any> {
|
|
18
|
+
data: T[];
|
|
19
|
+
columns: Column<T>[];
|
|
20
|
+
rowHeight?: number;
|
|
21
|
+
headerHeight?: number;
|
|
22
|
+
onRowClick?: (row: T, index: number) => void;
|
|
23
|
+
onSort?: (column: Column<T>, direction: 'asc' | 'desc') => void;
|
|
24
|
+
virtualized?: boolean;
|
|
25
|
+
height?: number | string;
|
|
26
|
+
width?: number | string;
|
|
27
|
+
style?: ViewStyle;
|
|
28
|
+
headerStyle?: ViewStyle;
|
|
29
|
+
rowStyle?: ViewStyle | ((row: T, index: number) => ViewStyle);
|
|
30
|
+
selectedRows?: number[];
|
|
31
|
+
onSelectionChange?: (selectedRows: number[]) => void;
|
|
32
|
+
multiSelect?: boolean;
|
|
33
|
+
stickyHeader?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CellProps {
|
|
37
|
+
children: React.ReactNode;
|
|
38
|
+
style?: ViewStyle;
|
|
39
|
+
width?: number;
|
|
40
|
+
onPress?: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface RowProps {
|
|
44
|
+
children: React.ReactNode;
|
|
45
|
+
style?: ViewStyle;
|
|
46
|
+
onPress?: () => void;
|
|
47
|
+
selected?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface HeaderCellProps {
|
|
51
|
+
children: React.ReactNode;
|
|
52
|
+
style?: ViewStyle;
|
|
53
|
+
width?: number;
|
|
54
|
+
sortable?: boolean;
|
|
55
|
+
sortDirection?: 'asc' | 'desc' | null;
|
|
56
|
+
onSort?: () => void;
|
|
57
|
+
resizable?: boolean;
|
|
58
|
+
onResize?: (width: number) => void;
|
|
59
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View, Text, Button } from '@idealyst/components';
|
|
3
|
+
import { DataGrid } from '../DataGrid';
|
|
4
|
+
import type { Column } from '../DataGrid/types';
|
|
5
|
+
|
|
6
|
+
interface User {
|
|
7
|
+
id: number;
|
|
8
|
+
name: string;
|
|
9
|
+
email: string;
|
|
10
|
+
role: string;
|
|
11
|
+
status: 'active' | 'inactive';
|
|
12
|
+
joinDate: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const sampleData: User[] = [
|
|
16
|
+
{ id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin', status: 'active', joinDate: '2023-01-15' },
|
|
17
|
+
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'User', status: 'active', joinDate: '2023-02-20' },
|
|
18
|
+
{ id: 3, name: 'Bob Johnson', email: 'bob@example.com', role: 'Moderator', status: 'inactive', joinDate: '2023-03-10' },
|
|
19
|
+
{ id: 4, name: 'Alice Brown', email: 'alice@example.com', role: 'User', status: 'active', joinDate: '2023-04-05' },
|
|
20
|
+
{ id: 5, name: 'Charlie Wilson', email: 'charlie@example.com', role: 'User', status: 'active', joinDate: '2023-05-12' },
|
|
21
|
+
// Add more sample data for testing virtualization
|
|
22
|
+
...Array.from({ length: 95 }, (_, i) => ({
|
|
23
|
+
id: i + 6,
|
|
24
|
+
name: `User ${i + 6}`,
|
|
25
|
+
email: `user${i + 6}@example.com`,
|
|
26
|
+
role: 'User',
|
|
27
|
+
status: (i % 2 === 0 ? 'active' : 'inactive') as 'active' | 'inactive',
|
|
28
|
+
joinDate: `2023-${String((i % 12) + 1).padStart(2, '0')}-${String((i % 28) + 1).padStart(2, '0')}`,
|
|
29
|
+
})),
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export function BasicExample() {
|
|
33
|
+
const [selectedRows, setSelectedRows] = useState<number[]>([]);
|
|
34
|
+
const [data, setData] = useState(sampleData);
|
|
35
|
+
|
|
36
|
+
const columns: Column<User>[] = [
|
|
37
|
+
{
|
|
38
|
+
key: 'id',
|
|
39
|
+
header: 'ID',
|
|
40
|
+
width: 60,
|
|
41
|
+
sortable: true,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: 'name',
|
|
45
|
+
header: 'Name',
|
|
46
|
+
width: 150,
|
|
47
|
+
sortable: true,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
key: 'email',
|
|
51
|
+
header: 'Email',
|
|
52
|
+
width: 200,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: 'role',
|
|
56
|
+
header: 'Role',
|
|
57
|
+
width: 100,
|
|
58
|
+
render: (value) => (
|
|
59
|
+
<View style={{ paddingHorizontal: 8, paddingVertical: 4, backgroundColor: '#f0f0f0', borderRadius: 4 }}>
|
|
60
|
+
<Text size="small">{value}</Text>
|
|
61
|
+
</View>
|
|
62
|
+
),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: 'status',
|
|
66
|
+
header: 'Status',
|
|
67
|
+
width: 100,
|
|
68
|
+
render: (value) => (
|
|
69
|
+
<Text
|
|
70
|
+
size="small"
|
|
71
|
+
weight="medium"
|
|
72
|
+
style={{ color: value === 'active' ? '#22c55e' : '#ef4444' }}
|
|
73
|
+
>
|
|
74
|
+
{value}
|
|
75
|
+
</Text>
|
|
76
|
+
),
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
key: 'joinDate',
|
|
80
|
+
header: 'Join Date',
|
|
81
|
+
width: 120,
|
|
82
|
+
sortable: true,
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const handleSort = (column: Column<User>, direction: 'asc' | 'desc') => {
|
|
87
|
+
const sorted = [...data].sort((a, b) => {
|
|
88
|
+
const aVal = a[column.key as keyof User];
|
|
89
|
+
const bVal = b[column.key as keyof User];
|
|
90
|
+
|
|
91
|
+
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
|
92
|
+
return direction === 'asc' ? aVal - bVal : bVal - aVal;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const aStr = String(aVal);
|
|
96
|
+
const bStr = String(bVal);
|
|
97
|
+
return direction === 'asc'
|
|
98
|
+
? aStr.localeCompare(bStr)
|
|
99
|
+
: bStr.localeCompare(aStr);
|
|
100
|
+
});
|
|
101
|
+
setData(sorted);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<View spacing="lg" style={{ padding: 20 }}>
|
|
106
|
+
<Text size="xlarge" weight="bold">DataGrid Example</Text>
|
|
107
|
+
|
|
108
|
+
<View spacing="sm">
|
|
109
|
+
<Text>Selected rows: {selectedRows.length}</Text>
|
|
110
|
+
{selectedRows.length > 0 && (
|
|
111
|
+
<Button
|
|
112
|
+
size="small"
|
|
113
|
+
variant="outlined"
|
|
114
|
+
onPress={() => setSelectedRows([])}
|
|
115
|
+
>
|
|
116
|
+
Clear Selection
|
|
117
|
+
</Button>
|
|
118
|
+
)}
|
|
119
|
+
</View>
|
|
120
|
+
|
|
121
|
+
<DataGrid
|
|
122
|
+
data={data}
|
|
123
|
+
columns={columns}
|
|
124
|
+
height={500}
|
|
125
|
+
virtualized={true}
|
|
126
|
+
selectedRows={selectedRows}
|
|
127
|
+
onSelectionChange={setSelectedRows}
|
|
128
|
+
multiSelect={true}
|
|
129
|
+
onSort={handleSort}
|
|
130
|
+
rowStyle={(row, index) => ({
|
|
131
|
+
backgroundColor: index % 2 === 0 ? '#fafafa' : '#ffffff',
|
|
132
|
+
})}
|
|
133
|
+
/>
|
|
134
|
+
</View>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { BasicExample } from './BasicExample';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ScrollView as RNScrollView, ScrollViewProps as RNScrollViewProps } from 'react-native';
|
|
3
|
+
|
|
4
|
+
interface ScrollViewProps extends RNScrollViewProps {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const ScrollView: React.FC<ScrollViewProps> = (props) => {
|
|
9
|
+
return <RNScrollView {...props} />;
|
|
10
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from '@idealyst/components';
|
|
3
|
+
|
|
4
|
+
interface ScrollViewProps {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
horizontal?: boolean;
|
|
7
|
+
style?: any;
|
|
8
|
+
contentContainerStyle?: any;
|
|
9
|
+
onScroll?: (event: any) => void;
|
|
10
|
+
scrollEventThrottle?: number;
|
|
11
|
+
showsHorizontalScrollIndicator?: boolean;
|
|
12
|
+
showsVerticalScrollIndicator?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const ScrollView: React.FC<ScrollViewProps> = ({
|
|
16
|
+
children,
|
|
17
|
+
horizontal = false,
|
|
18
|
+
style,
|
|
19
|
+
contentContainerStyle,
|
|
20
|
+
onScroll,
|
|
21
|
+
showsHorizontalScrollIndicator = true,
|
|
22
|
+
showsVerticalScrollIndicator = true,
|
|
23
|
+
...props
|
|
24
|
+
}) => {
|
|
25
|
+
const scrollStyle = {
|
|
26
|
+
...style,
|
|
27
|
+
overflow: 'auto',
|
|
28
|
+
WebkitOverflowScrolling: 'touch',
|
|
29
|
+
...(horizontal && { overflowY: 'hidden', overflowX: 'auto' }),
|
|
30
|
+
...(!showsHorizontalScrollIndicator && { scrollbarWidth: 'none', msOverflowStyle: 'none' }),
|
|
31
|
+
...(!showsVerticalScrollIndicator && { scrollbarWidth: 'none', msOverflowStyle: 'none' }),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
style={scrollStyle}
|
|
37
|
+
onScroll={onScroll}
|
|
38
|
+
{...props}
|
|
39
|
+
>
|
|
40
|
+
<View style={contentContainerStyle}>
|
|
41
|
+
{children}
|
|
42
|
+
</View>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ScrollView } from './ScrollView.native';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ScrollView } from './ScrollView.web';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { FlatList, VirtualizedList as RNVirtualizedList } from 'react-native';
|
|
3
|
+
|
|
4
|
+
interface VirtualizedListProps {
|
|
5
|
+
data: any[];
|
|
6
|
+
renderItem: ({ item, index }: { item: any; index: number }) => React.ReactElement;
|
|
7
|
+
itemHeight: number | ((index: number) => number);
|
|
8
|
+
height: number;
|
|
9
|
+
width?: number | string;
|
|
10
|
+
horizontal?: boolean;
|
|
11
|
+
onScroll?: (event: any) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const VirtualizedList: React.FC<VirtualizedListProps> = ({
|
|
15
|
+
data,
|
|
16
|
+
renderItem,
|
|
17
|
+
itemHeight,
|
|
18
|
+
height,
|
|
19
|
+
horizontal = false,
|
|
20
|
+
onScroll,
|
|
21
|
+
}) => {
|
|
22
|
+
const getItemLayout = typeof itemHeight === 'number'
|
|
23
|
+
? (data: any, index: number) => ({
|
|
24
|
+
length: itemHeight,
|
|
25
|
+
offset: itemHeight * index,
|
|
26
|
+
index,
|
|
27
|
+
})
|
|
28
|
+
: undefined;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<FlatList
|
|
32
|
+
data={data}
|
|
33
|
+
renderItem={renderItem}
|
|
34
|
+
getItemLayout={getItemLayout}
|
|
35
|
+
horizontal={horizontal}
|
|
36
|
+
onScroll={onScroll}
|
|
37
|
+
style={{ height, width }}
|
|
38
|
+
showsHorizontalScrollIndicator={false}
|
|
39
|
+
showsVerticalScrollIndicator={false}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { VariableSizeList, FixedSizeList } from 'react-window';
|
|
3
|
+
|
|
4
|
+
interface VirtualizedListProps {
|
|
5
|
+
data: any[];
|
|
6
|
+
renderItem: ({ item, index }: { item: any; index: number }) => React.ReactElement;
|
|
7
|
+
itemHeight: number | ((index: number) => number);
|
|
8
|
+
height: number;
|
|
9
|
+
width?: number | string;
|
|
10
|
+
horizontal?: boolean;
|
|
11
|
+
onScroll?: (event: any) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const VirtualizedList: React.FC<VirtualizedListProps> = ({
|
|
15
|
+
data,
|
|
16
|
+
renderItem,
|
|
17
|
+
itemHeight,
|
|
18
|
+
height,
|
|
19
|
+
width = '100%',
|
|
20
|
+
horizontal = false,
|
|
21
|
+
onScroll,
|
|
22
|
+
}) => {
|
|
23
|
+
const isVariableSize = typeof itemHeight === 'function';
|
|
24
|
+
const List = isVariableSize ? VariableSizeList : FixedSizeList;
|
|
25
|
+
|
|
26
|
+
const listProps = {
|
|
27
|
+
height: horizontal ? '100%' : height,
|
|
28
|
+
width: horizontal ? height : width,
|
|
29
|
+
itemCount: data.length,
|
|
30
|
+
itemSize: itemHeight,
|
|
31
|
+
layout: horizontal ? 'horizontal' : 'vertical',
|
|
32
|
+
onScroll,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<List {...listProps}>
|
|
37
|
+
{({ index, style }) => (
|
|
38
|
+
<div style={style}>
|
|
39
|
+
{renderItem({ item: data[index], index })}
|
|
40
|
+
</div>
|
|
41
|
+
)}
|
|
42
|
+
</List>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { VirtualizedList } from './VirtualizedList.native';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { VirtualizedList } from './VirtualizedList.web';
|