@azrico/reactgrid 1.4.1 → 1.5.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/.claude/settings.local.json +8 -0
- package/README.md +28 -1
- package/dist/src/example.js +8 -2
- package/dist/src/func/func_a11y.d.ts +52 -0
- package/dist/src/func/func_a11y.js +139 -0
- package/dist/src/hooks/useGridEvents.js +44 -0
- package/dist/src/types/types.d.ts +14 -1
- package/dist/src/views/DataGrid.js +36 -6
- package/package.json +67 -67
- package/src/example.tsx +184 -178
- package/src/func/func_a11y.ts +159 -0
- package/src/hooks/useGridEvents.ts +141 -96
- package/src/types/types.ts +12 -1
- package/src/views/DataGrid.tsx +37 -5
package/src/example.tsx
CHANGED
|
@@ -1,178 +1,184 @@
|
|
|
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
|
-
name: 'job',
|
|
16
|
-
title: 'Job',
|
|
17
|
-
type: 'combo',
|
|
18
|
-
items: ['None', 'Engineer', 'Doctor'],
|
|
19
|
-
defaultFlex: 1,
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
name: 'age',
|
|
23
|
-
title: 'Age',
|
|
24
|
-
type: 'number',
|
|
25
|
-
defaultFlex: 1,
|
|
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
|
-
const [
|
|
66
|
-
const [
|
|
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
|
-
new_rd
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
name: 'job',
|
|
16
|
+
title: 'Job',
|
|
17
|
+
type: 'combo',
|
|
18
|
+
items: ['None', 'Engineer', 'Doctor'],
|
|
19
|
+
defaultFlex: 1,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'age',
|
|
23
|
+
title: 'Age',
|
|
24
|
+
type: 'number',
|
|
25
|
+
defaultFlex: 1,
|
|
26
|
+
ariaLabel: (value: any) => `${value} years`,
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const editableColumns = ['job', 'age'];
|
|
31
|
+
|
|
32
|
+
function getData(delta: any, max = 200) {
|
|
33
|
+
const fd: any[] = [];
|
|
34
|
+
for (let index = 0; index < max; index++) {
|
|
35
|
+
const currentObject = {
|
|
36
|
+
__table_id: String(index),
|
|
37
|
+
name: 'user-' + index,
|
|
38
|
+
age: 5 + index,
|
|
39
|
+
job: 'None',
|
|
40
|
+
};
|
|
41
|
+
if (delta[index] && typeof delta[index] === 'object') {
|
|
42
|
+
for (const key in delta[index]) {
|
|
43
|
+
currentObject[key] = delta[index][key];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const childrows: any[] = [];
|
|
48
|
+
if (index % 5 == 0) {
|
|
49
|
+
const sub_selfid = 's' + index;
|
|
50
|
+
childrows.push({
|
|
51
|
+
__table_id: sub_selfid,
|
|
52
|
+
__parent__table_id: String(index),
|
|
53
|
+
name: 'sub-' + index,
|
|
54
|
+
age: 20,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
currentObject['__children'] = childrows;
|
|
59
|
+
fd.push(currentObject);
|
|
60
|
+
}
|
|
61
|
+
return fd;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function ExampleGrid() {
|
|
65
|
+
const [ds_reset, set_ds_reset] = React.useState(0);
|
|
66
|
+
const [delta, set_delta] = React.useState({});
|
|
67
|
+
const [delayed_reload, set_delayed_reload] = React.useState(false);
|
|
68
|
+
|
|
69
|
+
const dataSource = React.useMemo(() => getData(delta), []);
|
|
70
|
+
// const dataSource = React.useCallback(
|
|
71
|
+
// async (dt) => {
|
|
72
|
+
// //console.log('Start loading');
|
|
73
|
+
// return await new Promise((resolve) => {
|
|
74
|
+
// setTimeout(() => {
|
|
75
|
+
// resolve(getData(delta));
|
|
76
|
+
// }, 1000);
|
|
77
|
+
// });
|
|
78
|
+
// },
|
|
79
|
+
// [ds_reset, delta]
|
|
80
|
+
// );
|
|
81
|
+
// const details_dataSource = React.useCallback(
|
|
82
|
+
// async (dt) => {
|
|
83
|
+
// //console.log('Start loading');
|
|
84
|
+
// return await new Promise((resolve) => {
|
|
85
|
+
// setTimeout(() => {
|
|
86
|
+
// resolve(getData(delta, 5));
|
|
87
|
+
// }, 1000);
|
|
88
|
+
// });
|
|
89
|
+
// },
|
|
90
|
+
// [ds_reset, delta]
|
|
91
|
+
// );
|
|
92
|
+
|
|
93
|
+
function onEditComplete(params) {
|
|
94
|
+
const { value, columnId, rowId, ...rest } = params;
|
|
95
|
+
let new_rd = delta[rowId];
|
|
96
|
+
if (!new_rd) new_rd = {};
|
|
97
|
+
new_rd[columnId] = value;
|
|
98
|
+
set_delta((d) => {
|
|
99
|
+
return { ...d, [rowId]: new_rd };
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const onRowDoubleClick = React.useCallback((index: number, rowdata: any) => {
|
|
104
|
+
console.log('dbl', rowdata?.data || rowdata);
|
|
105
|
+
alert('dbl: ' + index);
|
|
106
|
+
}, []);
|
|
107
|
+
const onRowClick = React.useCallback((index: number, rowdata: any) => {
|
|
108
|
+
console.log('click:', rowdata?.data || rowdata);
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
React.useEffect(() => {
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
set_delayed_reload(true);
|
|
114
|
+
}, 2000);
|
|
115
|
+
}, []);
|
|
116
|
+
const customColumns = React.useMemo(() => {
|
|
117
|
+
if (!delayed_reload) return [];
|
|
118
|
+
return [{ name: 'name', render: (r) => r.value + '^' }];
|
|
119
|
+
}, [delayed_reload]);
|
|
120
|
+
|
|
121
|
+
const contextMenuItems = React.useCallback((product: any) => {
|
|
122
|
+
return [
|
|
123
|
+
{
|
|
124
|
+
type: 'text',
|
|
125
|
+
content: product.name,
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div style={{ display: 'grid', gap: 20, height: '100%', overflow: 'auto' }}>
|
|
132
|
+
{['light', 'dark', 'deco'].map((theme) => (
|
|
133
|
+
<section key={theme} className={theme === 'deco' ? 'azgrid-example-deco' : undefined} style={{ height: 360, display: 'flex', flexDirection: 'column' }}>
|
|
134
|
+
<h3>{theme === 'deco' ? 'Art Deco (host variables)' : `Default ${theme}`}</h3>
|
|
135
|
+
<AzGrid
|
|
136
|
+
theme={theme}
|
|
137
|
+
ariaLabel={`Users (${theme})`}
|
|
138
|
+
delayedSize
|
|
139
|
+
// details_dataSource={details_dataSource}
|
|
140
|
+
dataSource={dataSource}
|
|
141
|
+
details_columns={columns}
|
|
142
|
+
onRowClick={onRowClick}
|
|
143
|
+
onRowDoubleClick={onRowDoubleClick}
|
|
144
|
+
allowChildRows
|
|
145
|
+
// onSelectionChange={onSelectionChange}
|
|
146
|
+
// selected={selected}
|
|
147
|
+
contextMenuItems={contextMenuItems as any}
|
|
148
|
+
customColumns={customColumns}
|
|
149
|
+
columns={columns}
|
|
150
|
+
editableColumns={editableColumns}
|
|
151
|
+
editable={true}
|
|
152
|
+
onEditComplete={onEditComplete}
|
|
153
|
+
advancedFilterFields={['name']}
|
|
154
|
+
// leftColumns={leftColumns}
|
|
155
|
+
gridOptions={
|
|
156
|
+
{
|
|
157
|
+
defaultFilterValue: [{ name: 'name', operator: 'contains', type: 'string', value: '' }],
|
|
158
|
+
pagination: 'local',
|
|
159
|
+
defaultLimit: 10,
|
|
160
|
+
pageSizes: [10, 25, 50],
|
|
161
|
+
checkboxColumn: true,
|
|
162
|
+
multiSelect: true,
|
|
163
|
+
// consumer onRenderRow runs after the internal one
|
|
164
|
+
onRenderRow: (rowProps: any) => {
|
|
165
|
+
rowProps['data-example-row'] = 'true';
|
|
166
|
+
},
|
|
167
|
+
// defaultFilterValue: filterValue,
|
|
168
|
+
// remoteFilter: false,
|
|
169
|
+
// multiSelect: true,
|
|
170
|
+
// pagination: 'local',
|
|
171
|
+
// defaultLimit: 100,
|
|
172
|
+
// limit: 100,
|
|
173
|
+
// pageSizes: [100],
|
|
174
|
+
// rowIndexColumn: true,
|
|
175
|
+
// checkboxColumn: true,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/>
|
|
179
|
+
</section>
|
|
180
|
+
))}
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
export default ExampleGrid;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { GridType } from '../types/types';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Accessibility helpers.
|
|
5
|
+
* Inovua's grid renders anonymous divs, so we fill in ARIA grid semantics here.
|
|
6
|
+
* Cell/header DOM attributes go through the column's cellDOMProps/headerDOMProps
|
|
7
|
+
* (attributes set on cellProps inside onRender are not forwarded to the DOM).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type ColumnA11yOptions = {
|
|
11
|
+
/** role of body cells, e.g. 'gridcell' or 'rowheader' */
|
|
12
|
+
role?: string;
|
|
13
|
+
/** grid level editable flag, columns are readonly when the grid isn't editable */
|
|
14
|
+
gridEditable?: boolean;
|
|
15
|
+
/** false skips aria-readonly (for interactive special columns like the checkbox column) */
|
|
16
|
+
markReadonly?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function set_default(target: any, key: string, value: any) {
|
|
20
|
+
if (target[key] === undefined && value !== undefined && value !== null) target[key] = value;
|
|
21
|
+
}
|
|
22
|
+
function resolve_dom_props(src: any, cellProps: any) {
|
|
23
|
+
const res = typeof src === 'function' ? src(cellProps) : src;
|
|
24
|
+
return res ? { ...res } : {};
|
|
25
|
+
}
|
|
26
|
+
function get_sort_dir(sortInfo: any) {
|
|
27
|
+
if (!sortInfo || !sortInfo.dir) return 'none';
|
|
28
|
+
return sortInfo.dir > 0 ? 'ascending' : 'descending';
|
|
29
|
+
}
|
|
30
|
+
function is_cell_readonly(cellProps: any, gridEditable: boolean) {
|
|
31
|
+
if (!gridEditable) return true;
|
|
32
|
+
const editable = cellProps.computedEditable;
|
|
33
|
+
//function editables are decided per edit, we can't know it here
|
|
34
|
+
if (typeof editable === 'function') return false;
|
|
35
|
+
return !editable;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* wraps column dom props to add ARIA attributes,
|
|
40
|
+
* consumer's cellDOMProps/headerDOMProps run first and win over ours
|
|
41
|
+
* @param col
|
|
42
|
+
* @param options
|
|
43
|
+
* @returns a new column object
|
|
44
|
+
*/
|
|
45
|
+
export function apply_column_a11y<T extends Record<string, any>>(
|
|
46
|
+
col: T,
|
|
47
|
+
options: ColumnA11yOptions = {}
|
|
48
|
+
): T {
|
|
49
|
+
const { role = 'gridcell', gridEditable = false, markReadonly = true } = options;
|
|
50
|
+
const consumerCellDOMProps = col.cellDOMProps;
|
|
51
|
+
const consumerHeaderDOMProps = col.headerDOMProps;
|
|
52
|
+
const ariaLabel = col.ariaLabel;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
...col,
|
|
56
|
+
cellDOMProps: (cellProps: any) => {
|
|
57
|
+
const res = resolve_dom_props(consumerCellDOMProps, cellProps);
|
|
58
|
+
set_default(res, 'role', role);
|
|
59
|
+
set_default(res, 'aria-colindex', cellProps.computedVisibleIndex + 1);
|
|
60
|
+
set_default(res, 'data-column', cellProps.name || cellProps.id);
|
|
61
|
+
if (markReadonly && is_cell_readonly(cellProps, gridEditable))
|
|
62
|
+
set_default(res, 'aria-readonly', 'true');
|
|
63
|
+
|
|
64
|
+
const data = cellProps.data;
|
|
65
|
+
if (typeof ariaLabel === 'function' && data && !data.__group && !data.__summary)
|
|
66
|
+
set_default(res, 'aria-label', ariaLabel(cellProps.value, data));
|
|
67
|
+
return res;
|
|
68
|
+
},
|
|
69
|
+
headerDOMProps: (cellProps: any) => {
|
|
70
|
+
const res = resolve_dom_props(consumerHeaderDOMProps, cellProps);
|
|
71
|
+
set_default(res, 'role', 'columnheader');
|
|
72
|
+
set_default(res, 'aria-colindex', cellProps.computedVisibleIndex + 1);
|
|
73
|
+
if (cellProps.computedSortable)
|
|
74
|
+
set_default(res, 'aria-sort', get_sort_dir(cellProps.computedSortInfo));
|
|
75
|
+
return res;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* adds ARIA attributes to a row, called from onRenderRow
|
|
82
|
+
* @param rowProps
|
|
83
|
+
* @param selectionEnabled
|
|
84
|
+
*/
|
|
85
|
+
export function apply_row_a11y(rowProps: any, selectionEnabled: boolean) {
|
|
86
|
+
//filler rows (showEmptyRows) are not data rows
|
|
87
|
+
if (rowProps.empty) return;
|
|
88
|
+
const index = rowProps.remoteRowIndex ?? rowProps.realIndex ?? 0;
|
|
89
|
+
rowProps['role'] = 'row';
|
|
90
|
+
rowProps['aria-rowindex'] = index + 2; // 1-based, header row is 1
|
|
91
|
+
if (selectionEnabled) rowProps['aria-selected'] = rowProps.selected ? 'true' : 'false';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function set_attr(el: Element, key: string, value: any) {
|
|
95
|
+
if (value === undefined || value === null || value === '') {
|
|
96
|
+
if (el.hasAttribute(key)) el.removeAttribute(key);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const str = String(value);
|
|
100
|
+
if (el.getAttribute(key) !== str) el.setAttribute(key, str);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* total number of data rows, uses the datasource count when paginated
|
|
105
|
+
* so the real size is reported, not just the visible page
|
|
106
|
+
* @param grid
|
|
107
|
+
*/
|
|
108
|
+
export function get_grid_row_count(grid: GridType): number {
|
|
109
|
+
const g = grid as any;
|
|
110
|
+
const rendered = Array.isArray(g.data) ? g.data.length : 0;
|
|
111
|
+
if (!g.computedPagination) return rendered;
|
|
112
|
+
const offset = g.computedLivePagination ? 0 : g.computedSkip || 0;
|
|
113
|
+
return Math.max(g.paginationCount || 0, offset + rendered);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export type RootA11yOptions = { ariaLabel?: string; editable?: boolean };
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* root element of the grid, null when it isn't mounted
|
|
120
|
+
* @param grid computed props of the grid
|
|
121
|
+
*/
|
|
122
|
+
export function get_grid_root(grid: GridType | null | undefined): Element | null {
|
|
123
|
+
return (grid as any)?.getDOMNode?.() || null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* key of the values apply_root_a11y writes, used to skip applying when nothing changed
|
|
128
|
+
* @param grid computed props of the grid
|
|
129
|
+
* @param options
|
|
130
|
+
*/
|
|
131
|
+
export function get_root_a11y_key(grid: GridType | null | undefined, options: RootA11yOptions) {
|
|
132
|
+
const g = grid as any;
|
|
133
|
+
if (!g) return '';
|
|
134
|
+
return [
|
|
135
|
+
get_grid_row_count(g),
|
|
136
|
+
(g.visibleColumns || []).length,
|
|
137
|
+
options.ariaLabel ?? '',
|
|
138
|
+
options.editable ? 1 : 0,
|
|
139
|
+
].join('|');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* sets ARIA attributes on the grid root element
|
|
144
|
+
* inovua doesn't forward props to its root, so this is applied on the DOM node
|
|
145
|
+
* @param grid computed props of the grid
|
|
146
|
+
* @param options
|
|
147
|
+
* @returns the element the attributes were applied to, null when the grid isn't mounted
|
|
148
|
+
*/
|
|
149
|
+
export function apply_root_a11y(grid: GridType | null | undefined, options: RootA11yOptions) {
|
|
150
|
+
const g = grid as any;
|
|
151
|
+
const el = get_grid_root(g);
|
|
152
|
+
if (!el) return null;
|
|
153
|
+
set_attr(el, 'role', 'grid');
|
|
154
|
+
set_attr(el, 'aria-rowcount', get_grid_row_count(g) + 1);
|
|
155
|
+
set_attr(el, 'aria-colcount', (g.visibleColumns || []).length);
|
|
156
|
+
set_attr(el, 'aria-label', options.ariaLabel);
|
|
157
|
+
set_attr(el, 'aria-readonly', options.editable ? undefined : 'true');
|
|
158
|
+
return el;
|
|
159
|
+
}
|