@azrico/reactgrid 1.6.2 → 1.6.5
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/CHANGELOG.md +33 -19
- package/README.md +165 -164
- package/__tests__/context-menu.config.cjs +14 -0
- package/__tests__/context-menu.test.js +150 -0
- package/config/generate-theme.cjs +1 -1
- package/config/theme-extras.css +11 -16
- package/dist/src/example.js +27 -2
- package/dist/src/func/func_a11y.d.ts +14 -0
- package/dist/src/func/func_a11y.js +63 -10
- package/dist/src/func/func_grid.d.ts +6 -0
- package/dist/src/func/func_grid.js +15 -4
- package/dist/src/hooks/useGridEvents.js +16 -4
- package/dist/src/themes/azgrid-theme.css +12 -17
- package/dist/src/types/types.d.ts +4 -0
- package/dist/src/views/DataGrid.js +10 -22
- package/package.json +1 -1
- package/src/example.tsx +202 -186
- package/src/func/func_a11y.ts +62 -4
- package/src/func/func_grid.ts +351 -340
- package/src/hooks/useGridEvents.ts +171 -158
- package/src/themes/azgrid-theme.css +12 -17
- package/src/types/types.ts +157 -153
- package/src/views/DataGrid.tsx +9 -17
package/src/example.tsx
CHANGED
|
@@ -1,186 +1,202 @@
|
|
|
1
|
-
export * from './views/DataGrid';
|
|
2
|
-
|
|
3
|
-
import AzGrid from './views/DataGrid';
|
|
4
|
-
import React from 'react';
|
|
5
|
-
import './themes/azgrid-theme.css';
|
|
6
|
-
import './themes/example-palettes.css';
|
|
7
|
-
|
|
8
|
-
const columns = [
|
|
9
|
-
{
|
|
10
|
-
name: 'name',
|
|
11
|
-
title: 'Name',
|
|
12
|
-
defaultFlex: 1,
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
1
|
+
export * from './views/DataGrid';
|
|
2
|
+
|
|
3
|
+
import AzGrid from './views/DataGrid';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import './themes/azgrid-theme.css';
|
|
6
|
+
import './themes/example-palettes.css';
|
|
7
|
+
|
|
8
|
+
const columns = [
|
|
9
|
+
{
|
|
10
|
+
name: 'name',
|
|
11
|
+
title: 'Name',
|
|
12
|
+
defaultFlex: 1,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
// typed columns are editable without editableColumns, items keep their case
|
|
16
|
+
name: 'job',
|
|
17
|
+
title: 'Job',
|
|
18
|
+
type: 'combo',
|
|
19
|
+
items: ['None', 'Engineer', 'Doctor'],
|
|
20
|
+
defaultFlex: 1,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'age',
|
|
24
|
+
title: 'Age',
|
|
25
|
+
type: 'number',
|
|
26
|
+
defaultFlex: 1,
|
|
27
|
+
ariaLabel: (value: any) => `${value} years`,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
// editable only for rows that are not saved yet (no id)
|
|
31
|
+
name: 'location',
|
|
32
|
+
title: 'Location',
|
|
33
|
+
type: 'select',
|
|
34
|
+
items: ['Shelf A', 'Shelf B', 'Back Room'],
|
|
35
|
+
editable: (value: any, cellProps: any) => !cellProps?.data?.id,
|
|
36
|
+
defaultFlex: 1,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
// plain columns still need editableColumns
|
|
41
|
+
const editableColumns = ['name'];
|
|
42
|
+
|
|
43
|
+
function getData(delta: any, max = 200) {
|
|
44
|
+
const fd: any[] = [];
|
|
45
|
+
for (let index = 0; index < max; index++) {
|
|
46
|
+
const currentObject = {
|
|
47
|
+
__table_id: String(index),
|
|
48
|
+
name: 'user-' + index,
|
|
49
|
+
age: 5 + index,
|
|
50
|
+
job: 'None',
|
|
51
|
+
location: 'Shelf A',
|
|
52
|
+
// every third row is saved, its location is readonly
|
|
53
|
+
id: index % 3 == 0 ? 'saved-' + index : undefined,
|
|
54
|
+
};
|
|
55
|
+
if (delta[index] && typeof delta[index] === 'object') {
|
|
56
|
+
for (const key in delta[index]) {
|
|
57
|
+
currentObject[key] = delta[index][key];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const childrows: any[] = [];
|
|
62
|
+
if (index % 5 == 0) {
|
|
63
|
+
const sub_selfid = 's' + index;
|
|
64
|
+
childrows.push({
|
|
65
|
+
__table_id: sub_selfid,
|
|
66
|
+
__parent__table_id: String(index),
|
|
67
|
+
name: 'sub-' + index,
|
|
68
|
+
age: 20,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
currentObject['__children'] = childrows;
|
|
73
|
+
fd.push(currentObject);
|
|
74
|
+
}
|
|
75
|
+
return fd;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function ExampleGrid() {
|
|
79
|
+
const [ds_reset, set_ds_reset] = React.useState(0);
|
|
80
|
+
const [delta, set_delta] = React.useState({});
|
|
81
|
+
const [delayed_reload, set_delayed_reload] = React.useState(false);
|
|
82
|
+
|
|
83
|
+
const dataSource = React.useMemo(() => getData(delta), []);
|
|
84
|
+
// const dataSource = React.useCallback(
|
|
85
|
+
// async (dt) => {
|
|
86
|
+
// //console.log('Start loading');
|
|
87
|
+
// return await new Promise((resolve) => {
|
|
88
|
+
// setTimeout(() => {
|
|
89
|
+
// resolve(getData(delta));
|
|
90
|
+
// }, 1000);
|
|
91
|
+
// });
|
|
92
|
+
// },
|
|
93
|
+
// [ds_reset, delta]
|
|
94
|
+
// );
|
|
95
|
+
// const details_dataSource = React.useCallback(
|
|
96
|
+
// async (dt) => {
|
|
97
|
+
// //console.log('Start loading');
|
|
98
|
+
// return await new Promise((resolve) => {
|
|
99
|
+
// setTimeout(() => {
|
|
100
|
+
// resolve(getData(delta, 5));
|
|
101
|
+
// }, 1000);
|
|
102
|
+
// });
|
|
103
|
+
// },
|
|
104
|
+
// [ds_reset, delta]
|
|
105
|
+
// );
|
|
106
|
+
|
|
107
|
+
function onEditComplete(params) {
|
|
108
|
+
const { value, columnId, rowId, ...rest } = params;
|
|
109
|
+
let new_rd = delta[rowId];
|
|
110
|
+
if (!new_rd) new_rd = {};
|
|
111
|
+
new_rd[columnId] = value;
|
|
112
|
+
set_delta((d) => {
|
|
113
|
+
return { ...d, [rowId]: new_rd };
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const onRowDoubleClick = React.useCallback((index: number, rowdata: any) => {
|
|
118
|
+
console.log('dbl', rowdata?.data || rowdata);
|
|
119
|
+
alert('dbl: ' + index);
|
|
120
|
+
}, []);
|
|
121
|
+
const onRowClick = React.useCallback((index: number, rowdata: any) => {
|
|
122
|
+
console.log('click:', rowdata?.data || rowdata);
|
|
123
|
+
}, []);
|
|
124
|
+
|
|
125
|
+
React.useEffect(() => {
|
|
126
|
+
setTimeout(() => {
|
|
127
|
+
set_delayed_reload(true);
|
|
128
|
+
}, 2000);
|
|
129
|
+
}, []);
|
|
130
|
+
const customColumns = React.useMemo(() => {
|
|
131
|
+
if (!delayed_reload) return [];
|
|
132
|
+
return [{ name: 'name', render: (r) => r.value + '^' }];
|
|
133
|
+
}, [delayed_reload]);
|
|
134
|
+
|
|
135
|
+
const contextMenuItems = React.useCallback((product: any) => {
|
|
136
|
+
return [
|
|
137
|
+
{
|
|
138
|
+
type: 'text',
|
|
139
|
+
content: product.name,
|
|
140
|
+
},
|
|
141
|
+
{ title: 'Log row', onClick: (row) => console.log('context menu: log', row) },
|
|
142
|
+
{ title: 'Copy name', onClick: (row) => navigator.clipboard?.writeText(String(row?.name ?? '')) },
|
|
143
|
+
];
|
|
144
|
+
}, []);
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div style={{ display: 'grid', gap: 20, height: '100%', overflow: 'auto' }}>
|
|
148
|
+
{['light', 'dark', 'deco'].map((theme) => (
|
|
149
|
+
<section key={theme} className={theme === 'deco' ? 'azgrid-example-deco' : undefined} style={{ height: 360, display: 'flex', flexDirection: 'column' }}>
|
|
150
|
+
<h3>{theme === 'deco' ? 'Art Deco (host variables)' : `Default ${theme}`}</h3>
|
|
151
|
+
<AzGrid
|
|
152
|
+
theme={theme}
|
|
153
|
+
ariaLabel={`Users (${theme})`}
|
|
154
|
+
delayedSize
|
|
155
|
+
// details_dataSource={details_dataSource}
|
|
156
|
+
dataSource={dataSource}
|
|
157
|
+
details_columns={columns}
|
|
158
|
+
onRowClick={onRowClick}
|
|
159
|
+
onRowDoubleClick={onRowDoubleClick}
|
|
160
|
+
allowChildRows
|
|
161
|
+
// onSelectionChange={onSelectionChange}
|
|
162
|
+
// selected={selected}
|
|
163
|
+
contextMenuItems={contextMenuItems as any}
|
|
164
|
+
customColumns={customColumns}
|
|
165
|
+
columns={columns}
|
|
166
|
+
editableColumns={editableColumns}
|
|
167
|
+
editable={true}
|
|
168
|
+
onEditComplete={onEditComplete}
|
|
169
|
+
advancedFilterFields={['name']}
|
|
170
|
+
advancedFilterLabel="Search Name"
|
|
171
|
+
advancedFilterPlaceholder={<i>Type a name</i>}
|
|
172
|
+
// leftColumns={leftColumns}
|
|
173
|
+
gridOptions={
|
|
174
|
+
{
|
|
175
|
+
defaultFilterValue: [{ name: 'name', operator: 'contains', type: 'string', value: '' }],
|
|
176
|
+
pagination: 'local',
|
|
177
|
+
defaultLimit: 10,
|
|
178
|
+
pageSizes: [10, 25, 50],
|
|
179
|
+
checkboxColumn: true,
|
|
180
|
+
multiSelect: true,
|
|
181
|
+
// consumer onRenderRow runs after the internal one
|
|
182
|
+
onRenderRow: (rowProps: any) => {
|
|
183
|
+
rowProps['data-example-row'] = 'true';
|
|
184
|
+
},
|
|
185
|
+
// defaultFilterValue: filterValue,
|
|
186
|
+
// remoteFilter: false,
|
|
187
|
+
// multiSelect: true,
|
|
188
|
+
// pagination: 'local',
|
|
189
|
+
// defaultLimit: 100,
|
|
190
|
+
// limit: 100,
|
|
191
|
+
// pageSizes: [100],
|
|
192
|
+
// rowIndexColumn: true,
|
|
193
|
+
// checkboxColumn: true,
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/>
|
|
197
|
+
</section>
|
|
198
|
+
))}
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
export default ExampleGrid;
|
package/src/func/func_a11y.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { GridType } from '../types/types';
|
|
|
5
5
|
* Inovua's grid renders anonymous divs, so we fill in ARIA grid semantics here.
|
|
6
6
|
* Cell/header DOM attributes go through the column's cellDOMProps/headerDOMProps
|
|
7
7
|
* (attributes set on cellProps inside onRender are not forwarded to the DOM).
|
|
8
|
+
* Inovua's prop types only accept an object for headerDOMProps, so the header's
|
|
9
|
+
* aria-colindex/aria-sort are set on the DOM by apply_header_a11y.
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
export type ColumnA11yOptions = {
|
|
@@ -14,6 +16,8 @@ export type ColumnA11yOptions = {
|
|
|
14
16
|
gridEditable?: boolean;
|
|
15
17
|
/** false skips aria-readonly (for interactive special columns like the checkbox column) */
|
|
16
18
|
markReadonly?: boolean;
|
|
19
|
+
/** column id when the column object has no id/name (inovua's special columns) */
|
|
20
|
+
id?: string;
|
|
17
21
|
};
|
|
18
22
|
|
|
19
23
|
function set_default(target: any, key: string, value: any) {
|
|
@@ -47,6 +51,7 @@ export function apply_column_a11y<T extends Record<string, any>>(
|
|
|
47
51
|
options: ColumnA11yOptions = {}
|
|
48
52
|
): T {
|
|
49
53
|
const { role = 'gridcell', gridEditable = false, markReadonly = true } = options;
|
|
54
|
+
const id = col.id ?? col.name ?? options.id;
|
|
50
55
|
const consumerCellDOMProps = col.cellDOMProps;
|
|
51
56
|
const consumerHeaderDOMProps = col.headerDOMProps;
|
|
52
57
|
const ariaLabel = col.ariaLabel;
|
|
@@ -66,15 +71,32 @@ export function apply_column_a11y<T extends Record<string, any>>(
|
|
|
66
71
|
set_default(res, 'aria-label', ariaLabel(cellProps.value, data));
|
|
67
72
|
return res;
|
|
68
73
|
},
|
|
69
|
-
headerDOMProps: (
|
|
70
|
-
|
|
74
|
+
headerDOMProps: get_header_dom_props(consumerHeaderDOMProps, id),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* header DOM props as an object (inovua's prop types reject a function),
|
|
80
|
+
* aria-colindex/aria-sort change with the grid state and are set by apply_header_a11y
|
|
81
|
+
* @param consumer consumer's headerDOMProps
|
|
82
|
+
* @param id column id, looked up in the grid's columnsMap
|
|
83
|
+
*/
|
|
84
|
+
function get_header_dom_props(consumer: any, id: string | undefined) {
|
|
85
|
+
//a consumer function is kept (inovua runs it), it gets the attributes from the cell props
|
|
86
|
+
if (typeof consumer === 'function')
|
|
87
|
+
return (cellProps: any) => {
|
|
88
|
+
const res = resolve_dom_props(consumer, cellProps);
|
|
71
89
|
set_default(res, 'role', 'columnheader');
|
|
90
|
+
set_default(res, 'data-column', id);
|
|
72
91
|
set_default(res, 'aria-colindex', cellProps.computedVisibleIndex + 1);
|
|
73
92
|
if (cellProps.computedSortable)
|
|
74
93
|
set_default(res, 'aria-sort', get_sort_dir(cellProps.computedSortInfo));
|
|
75
94
|
return res;
|
|
76
|
-
}
|
|
77
|
-
|
|
95
|
+
};
|
|
96
|
+
const res = resolve_dom_props(consumer, undefined);
|
|
97
|
+
set_default(res, 'role', 'columnheader');
|
|
98
|
+
set_default(res, 'data-column', id);
|
|
99
|
+
return res;
|
|
78
100
|
}
|
|
79
101
|
|
|
80
102
|
/**
|
|
@@ -115,6 +137,39 @@ export function get_grid_row_count(grid: GridType): number {
|
|
|
115
137
|
|
|
116
138
|
export type RootA11yOptions = { ariaLabel?: string; editable?: boolean };
|
|
117
139
|
|
|
140
|
+
const HEADER_CELL_SELECTOR = '[role="columnheader"][data-column]';
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* sets aria-colindex/aria-sort on the header cells of the grid,
|
|
144
|
+
* attributes the consumer set in an object headerDOMProps are left alone
|
|
145
|
+
* @param grid computed props of the grid
|
|
146
|
+
* @param root root element of the grid
|
|
147
|
+
*/
|
|
148
|
+
export function apply_header_a11y(grid: GridType | null | undefined, root: Element) {
|
|
149
|
+
const columnsMap = (grid as any)?.columnsMap;
|
|
150
|
+
if (!columnsMap) return;
|
|
151
|
+
root.querySelectorAll(HEADER_CELL_SELECTOR).forEach((el) => {
|
|
152
|
+
//skip headers of grids nested in row details
|
|
153
|
+
if (el.closest('[role="grid"]') !== root) return;
|
|
154
|
+
const col = columnsMap[el.getAttribute('data-column') as string];
|
|
155
|
+
const own = col?.headerDOMProps;
|
|
156
|
+
//a function headerDOMProps sets the attributes itself
|
|
157
|
+
if (!own || typeof own !== 'object') return;
|
|
158
|
+
if (own['aria-colindex'] === undefined)
|
|
159
|
+
set_attr(el, 'aria-colindex', col.computedVisibleIndex + 1);
|
|
160
|
+
if (own['aria-sort'] === undefined)
|
|
161
|
+
set_attr(el, 'aria-sort', col.computedSortable ? get_sort_dir(col.computedSortInfo) : undefined);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* true when a header cell was (re)mounted without its aria-colindex
|
|
167
|
+
* @param root root element of the grid
|
|
168
|
+
*/
|
|
169
|
+
export function has_unsynced_header(root: Element | null) {
|
|
170
|
+
return !!root?.querySelector(`${HEADER_CELL_SELECTOR}:not([aria-colindex])`);
|
|
171
|
+
}
|
|
172
|
+
|
|
118
173
|
/**
|
|
119
174
|
* true while the grid is loading (loading prop or async datasource)
|
|
120
175
|
* @param grid computed props of the grid
|
|
@@ -145,6 +200,8 @@ export function get_root_a11y_key(grid: GridType | null | undefined, options: Ro
|
|
|
145
200
|
options.ariaLabel ?? '',
|
|
146
201
|
options.editable ? 1 : 0,
|
|
147
202
|
is_grid_busy(g) ? 1 : 0,
|
|
203
|
+
//header cells: order and sort direction
|
|
204
|
+
(g.visibleColumns || []).map((c) => `${c.id}:${c.computedSortInfo?.dir || 0}`).join(','),
|
|
148
205
|
].join('|');
|
|
149
206
|
}
|
|
150
207
|
|
|
@@ -165,5 +222,6 @@ export function apply_root_a11y(grid: GridType | null | undefined, options: Root
|
|
|
165
222
|
set_attr(el, 'aria-label', options.ariaLabel);
|
|
166
223
|
set_attr(el, 'aria-readonly', options.editable ? undefined : 'true');
|
|
167
224
|
set_attr(el, 'aria-busy', is_grid_busy(g) ? 'true' : undefined);
|
|
225
|
+
apply_header_a11y(g, el);
|
|
168
226
|
return el;
|
|
169
227
|
}
|