@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
|
@@ -1,96 +1,141 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { DataGridWrapperProps, GridType } from '../types/types';
|
|
3
|
-
import { find_realData, get_row_id } from '../func/func_grid';
|
|
4
|
-
import { func_selection } from '../func/func_child';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
}, [grid
|
|
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
|
-
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { DataGridWrapperProps, GridType } from '../types/types';
|
|
3
|
+
import { find_realData, get_row_id } from '../func/func_grid';
|
|
4
|
+
import { func_selection } from '../func/func_child';
|
|
5
|
+
import {
|
|
6
|
+
apply_root_a11y,
|
|
7
|
+
apply_row_a11y,
|
|
8
|
+
get_grid_root,
|
|
9
|
+
get_root_a11y_key,
|
|
10
|
+
} from '../func/func_a11y';
|
|
11
|
+
|
|
12
|
+
function useGridMenuRow() {
|
|
13
|
+
const [menuAnchorEl, set_menuAnchorEl] = React.useState<any>(null);
|
|
14
|
+
const [menuRow, set_menuRow] = React.useState<any>(null);
|
|
15
|
+
const update_menuRow = React.useCallback((r: any = null, el: any = null) => {
|
|
16
|
+
set_menuRow(r || null);
|
|
17
|
+
set_menuAnchorEl(el || null);
|
|
18
|
+
}, []);
|
|
19
|
+
return { update_menuRow, menuAnchorEl, menuRow };
|
|
20
|
+
}
|
|
21
|
+
export function useGridEvents(grid: GridType, props: DataGridWrapperProps) {
|
|
22
|
+
const click_data = React.useRef<any>(null);
|
|
23
|
+
const { update_menuRow, menuAnchorEl, menuRow } = useGridMenuRow();
|
|
24
|
+
|
|
25
|
+
const refreshColumn = React.useCallback(
|
|
26
|
+
(col: string) => {
|
|
27
|
+
const dt = grid.ungroupedData;
|
|
28
|
+
for (let index = 0; index < dt.length; index++) {
|
|
29
|
+
(grid as any).setItemPropertyAt(index, col, dt[index][col]);
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
[grid]
|
|
33
|
+
);
|
|
34
|
+
const refresh = React.useCallback(() => {
|
|
35
|
+
const dt = grid.ungroupedData;
|
|
36
|
+
for (let index = 0; index < dt.length; index++) {
|
|
37
|
+
(grid as any).setItemAt(index, {}, { replace: false });
|
|
38
|
+
}
|
|
39
|
+
}, [grid]);
|
|
40
|
+
|
|
41
|
+
React.useEffect(() => {
|
|
42
|
+
if (!grid) return;
|
|
43
|
+
grid['refresh'] = refresh;
|
|
44
|
+
grid['refreshColumn'] = refreshColumn;
|
|
45
|
+
}, [grid, props.dataSource, refresh, refreshColumn]);
|
|
46
|
+
/* -------------------------------------------------------------------------- */
|
|
47
|
+
/* events */
|
|
48
|
+
/* -------------------------------------------------------------------------- */
|
|
49
|
+
const onRowClick = React.useCallback(
|
|
50
|
+
(rowProps, event) => {
|
|
51
|
+
const nowDate = Date.now();
|
|
52
|
+
const lastclick: number = click_data.current[get_row_id(rowProps)];
|
|
53
|
+
if (lastclick) {
|
|
54
|
+
const diffTime = Math.abs(nowDate - lastclick);
|
|
55
|
+
if (diffTime < 400) {
|
|
56
|
+
click_data.current[get_row_id(rowProps)] = null;
|
|
57
|
+
return (
|
|
58
|
+
props.onRowDoubleClick &&
|
|
59
|
+
props.onRowDoubleClick(rowProps.rowIndex, rowProps.data)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
click_data.current[get_row_id(rowProps)] = nowDate;
|
|
65
|
+
return props.onRowClick && props.onRowClick(rowProps.rowIndex, rowProps.data);
|
|
66
|
+
},
|
|
67
|
+
[props.onRowClick, props.onRowDoubleClick]
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
React.useEffect(() => {
|
|
71
|
+
click_data.current = {};
|
|
72
|
+
}, [props.dataSource]);
|
|
73
|
+
|
|
74
|
+
/* ------------------------------- ContextMenu ------------------------------ */
|
|
75
|
+
const onRowContextMenu = React.useCallback((rowProps, event) => {
|
|
76
|
+
if (!rowProps || !rowProps.data || func_selection.has_selection()) {
|
|
77
|
+
update_menuRow(null, null);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
|
|
82
|
+
const realRow = find_realData(rowProps.dataSourceArray, event.target, rowProps.data);
|
|
83
|
+
const anEl = func_selection.getVirtualElement(event.clientX, event.clientY);
|
|
84
|
+
update_menuRow(realRow, anEl);
|
|
85
|
+
return false;
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
/* -------------------------------------------------------------------------- */
|
|
89
|
+
/* ------------------------------ Accessibility ----------------------------- */
|
|
90
|
+
const latestProps = React.useRef(props);
|
|
91
|
+
latestProps.current = props;
|
|
92
|
+
const rootSyncFrame = React.useRef(0);
|
|
93
|
+
// last applied root values, so row renders (e.g. while scrolling) don't re-apply them
|
|
94
|
+
const lastRoot = React.useRef<{ el: Element | null; key: string }>({ el: null, key: '' });
|
|
95
|
+
const getRootOptions = () => ({
|
|
96
|
+
ariaLabel: latestProps.current.ariaLabel,
|
|
97
|
+
editable: !!latestProps.current.editable,
|
|
98
|
+
});
|
|
99
|
+
// root attributes are set on the DOM node, batched to once per frame
|
|
100
|
+
const scheduleRootSync = React.useCallback(() => {
|
|
101
|
+
if (rootSyncFrame.current || typeof requestAnimationFrame === 'undefined') return;
|
|
102
|
+
const grid = latestProps.current.liveGridRef?.current?.current;
|
|
103
|
+
const key = get_root_a11y_key(grid, getRootOptions());
|
|
104
|
+
if (key && key === lastRoot.current.key && get_grid_root(grid) === lastRoot.current.el)
|
|
105
|
+
return;
|
|
106
|
+
rootSyncFrame.current = requestAnimationFrame(() => {
|
|
107
|
+
rootSyncFrame.current = 0;
|
|
108
|
+
const liveGrid = latestProps.current.liveGridRef?.current?.current;
|
|
109
|
+
const options = getRootOptions();
|
|
110
|
+
const el = apply_root_a11y(liveGrid, options);
|
|
111
|
+
lastRoot.current = { el, key: el ? get_root_a11y_key(liveGrid, options) : '' };
|
|
112
|
+
});
|
|
113
|
+
}, []);
|
|
114
|
+
React.useEffect(() => {
|
|
115
|
+
scheduleRootSync();
|
|
116
|
+
}, [scheduleRootSync, props.ariaLabel, props.editable, props.columns, props.dataSource]);
|
|
117
|
+
React.useEffect(
|
|
118
|
+
() => () => {
|
|
119
|
+
if (rootSyncFrame.current) cancelAnimationFrame(rootSyncFrame.current);
|
|
120
|
+
rootSyncFrame.current = 0;
|
|
121
|
+
},
|
|
122
|
+
[]
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const onRenderRow = React.useCallback((rowProps) => {
|
|
126
|
+
// save the original handlers to be called later
|
|
127
|
+
rowProps['data-row-id'] = get_row_id(rowProps.data);
|
|
128
|
+
const liveGrid: any = latestProps.current.liveGridRef?.current?.current;
|
|
129
|
+
apply_row_a11y(rowProps, !!liveGrid?.computedRowSelectionEnabled);
|
|
130
|
+
scheduleRootSync();
|
|
131
|
+
}, []);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
onRowClick,
|
|
135
|
+
onRowContextMenu,
|
|
136
|
+
onRenderRow,
|
|
137
|
+
update_menuRow,
|
|
138
|
+
menuAnchorEl,
|
|
139
|
+
menuRow,
|
|
140
|
+
};
|
|
141
|
+
}
|
package/src/types/types.ts
CHANGED
|
@@ -42,6 +42,8 @@ export type loose_column_fields = loose_column_names & {
|
|
|
42
42
|
title: string;
|
|
43
43
|
type: string;
|
|
44
44
|
items: any[];
|
|
45
|
+
/** accessible name for cells whose visible text isn't descriptive (icons, badges, ...), set as aria-label */
|
|
46
|
+
ariaLabel: (value: any, data: any) => string;
|
|
45
47
|
};
|
|
46
48
|
export type LooseColumnDefinition = Partial<loose_column_fields & ColumnDefinition>;
|
|
47
49
|
//
|
|
@@ -50,7 +52,12 @@ export type CustomColumnDefinition = {
|
|
|
50
52
|
name: string;
|
|
51
53
|
render: (cell: any, formatterParams, onRendered) => React.ReactNode | undefined | null;
|
|
52
54
|
};
|
|
53
|
-
export type AzGridOptions = Partial<TypeDataGridProps
|
|
55
|
+
export type AzGridOptions = Partial<TypeDataGridProps> & {
|
|
56
|
+
/** called for every rendered row, after the grid's own row attributes are set */
|
|
57
|
+
onRenderRow?: (rowProps: any) => void;
|
|
58
|
+
/** extra DOM props for the header row */
|
|
59
|
+
headerProps?: { [key: string]: any };
|
|
60
|
+
};
|
|
54
61
|
/* -------------------------------------------------------------------------- */
|
|
55
62
|
export type AzGridProps = {
|
|
56
63
|
dataSource?: any[] | ((args: any) => any[] | Promise<any[] | any>);
|
|
@@ -59,6 +66,8 @@ export type AzGridProps = {
|
|
|
59
66
|
details_filter?: FilterValues;
|
|
60
67
|
|
|
61
68
|
licenseKey?: string;
|
|
69
|
+
/** accessible name of the grid, set as aria-label on the grid root */
|
|
70
|
+
ariaLabel?: string;
|
|
62
71
|
|
|
63
72
|
gridRef?: React.MutableRefObject<TypeComputedProps>;
|
|
64
73
|
gridOptions?: AzGridOptions;
|
|
@@ -117,6 +126,8 @@ export type DataGridWrapperProps = ModifyFields<
|
|
|
117
126
|
DataGridProps,
|
|
118
127
|
{
|
|
119
128
|
gridRef: React.MutableRefObject<TypeComputedProps>;
|
|
129
|
+
/** internal: inovua's live computed props ref (gridRef holds a snapshot) */
|
|
130
|
+
liveGridRef: React.MutableRefObject<React.MutableRefObject<TypeComputedProps> | null>;
|
|
120
131
|
static_gridOptions: AzGridOptions;
|
|
121
132
|
dynamic_gridOptions: AzGridOptions;
|
|
122
133
|
children?: React.ReactNode;
|
package/src/views/DataGrid.tsx
CHANGED
|
@@ -15,6 +15,7 @@ import { useGridEvents } from '../hooks/useGridEvents';
|
|
|
15
15
|
import { useColumnData } from '../hooks/useColumnData';
|
|
16
16
|
import { useDataSource } from '../hooks/useDatasource';
|
|
17
17
|
import { useGridSearch } from '../hooks/useGridSearch';
|
|
18
|
+
import { apply_column_a11y } from '../func/func_a11y';
|
|
18
19
|
/* -------------------------------------------------------------------------- */
|
|
19
20
|
|
|
20
21
|
/* -------------------------------------------------------------------------- */
|
|
@@ -41,12 +42,17 @@ const dynamic_option_names = ['selected', 'cellSelection', 'loading', 'limit', '
|
|
|
41
42
|
export function AzGrid(props: AzGridProps) {
|
|
42
43
|
const gridRefBackup = React.useRef<any>(null);
|
|
43
44
|
const gridRef = props.gridRef || gridRefBackup;
|
|
45
|
+
const liveGridRef = React.useRef<any>(null);
|
|
44
46
|
|
|
45
47
|
/* -------------------------------------------------------------------------- */
|
|
46
48
|
/* Columns */
|
|
47
49
|
/* -------------------------------------------------------------------------- */
|
|
48
50
|
const cached_columns = useColumnData(props);
|
|
49
51
|
const dataSource = useDataSource(props.dataSource, props);
|
|
52
|
+
const a11y_columns = React.useMemo(
|
|
53
|
+
() => cached_columns.map((col) => apply_column_a11y(col, { gridEditable: !!props.editable })),
|
|
54
|
+
[cached_columns, props.editable]
|
|
55
|
+
);
|
|
50
56
|
/* -------------------------------------------------------------------------- */
|
|
51
57
|
/* Row changes */
|
|
52
58
|
/* -------------------------------------------------------------------------- */
|
|
@@ -106,9 +112,10 @@ export function AzGrid(props: AzGridProps) {
|
|
|
106
112
|
<DataGridEventsWrapper
|
|
107
113
|
{...props}
|
|
108
114
|
gridRef={gridRef}
|
|
115
|
+
liveGridRef={liveGridRef}
|
|
109
116
|
static_gridOptions={static_gridOptions}
|
|
110
117
|
dynamic_gridOptions={dynamic_gridOptions}
|
|
111
|
-
columns={
|
|
118
|
+
columns={a11y_columns}
|
|
112
119
|
dataSource={dataSource}
|
|
113
120
|
highlightedRow={props.highlightedRow}
|
|
114
121
|
/>
|
|
@@ -146,20 +153,44 @@ function DataGridEventsWrapper(props: DataGridWrapperProps) {
|
|
|
146
153
|
* Merge click events into grid options
|
|
147
154
|
*/
|
|
148
155
|
const static_gridOptions = React.useMemo<AzGridOptions>(() => {
|
|
156
|
+
const opts: any = props.static_gridOptions || {};
|
|
149
157
|
const events: AzGridOptions = {
|
|
150
158
|
onRowClick: onRowClick,
|
|
151
159
|
onRowContextMenu: onRowContextMenu,
|
|
152
160
|
};
|
|
153
161
|
|
|
162
|
+
// internal row attributes first, consumer's onRenderRow last so it can override them
|
|
163
|
+
const consumerOnRenderRow = opts.onRenderRow;
|
|
154
164
|
const renderProps: any = {
|
|
155
|
-
onRenderRow:
|
|
165
|
+
onRenderRow: consumerOnRenderRow
|
|
166
|
+
? (rowProps) => {
|
|
167
|
+
onRenderRow(rowProps);
|
|
168
|
+
consumerOnRenderRow(rowProps);
|
|
169
|
+
}
|
|
170
|
+
: onRenderRow,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const gridEditable = !!props.editable;
|
|
174
|
+
const a11yProps: any = {
|
|
175
|
+
headerProps: { role: 'row', 'aria-rowindex': 1, ...opts.headerProps },
|
|
176
|
+
rowIndexColumn: apply_column_a11y(
|
|
177
|
+
typeof opts.rowIndexColumn === 'object' ? opts.rowIndexColumn : {},
|
|
178
|
+
{ role: 'rowheader', gridEditable }
|
|
179
|
+
),
|
|
156
180
|
};
|
|
181
|
+
if (opts.checkboxColumn)
|
|
182
|
+
a11yProps.checkboxColumn = apply_column_a11y(
|
|
183
|
+
typeof opts.checkboxColumn === 'object' ? opts.checkboxColumn : {},
|
|
184
|
+
{ gridEditable, markReadonly: false }
|
|
185
|
+
);
|
|
186
|
+
|
|
157
187
|
return {
|
|
158
|
-
...
|
|
188
|
+
...opts,
|
|
159
189
|
...events,
|
|
160
190
|
...renderProps,
|
|
191
|
+
...a11yProps,
|
|
161
192
|
};
|
|
162
|
-
}, [props.static_gridOptions]);
|
|
193
|
+
}, [props.static_gridOptions, props.editable, onRowClick, onRowContextMenu, onRenderRow]);
|
|
163
194
|
|
|
164
195
|
/* -------------------------------------------------------------------------- */
|
|
165
196
|
//TODO ContextMenu not from mui...
|
|
@@ -260,9 +291,10 @@ function DataGridCore(props: DataGridWrapperProps) {
|
|
|
260
291
|
<ReactDataGrid
|
|
261
292
|
{...props.static_gridOptions}
|
|
262
293
|
{...props.dynamic_gridOptions}
|
|
263
|
-
rowIndexColumn
|
|
294
|
+
rowIndexColumn={props.static_gridOptions.rowIndexColumn || true}
|
|
264
295
|
handle={(ref) => {
|
|
265
296
|
if (ref) props.gridRef.current = ref.current as any;
|
|
297
|
+
if (props.liveGridRef) props.liveGridRef.current = ref as any;
|
|
266
298
|
}}
|
|
267
299
|
theme="default-light"
|
|
268
300
|
className={clsx(
|