@azrico/reactgrid 1.4.1 → 1.5.2
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 +57 -0
- package/dist/src/func/func_a11y.js +148 -0
- package/dist/src/func/func_child.js +15 -7
- package/dist/src/hooks/useColumnData.d.ts +1 -1
- package/dist/src/hooks/useColumnData.js +8 -12
- package/dist/src/hooks/useGridEvents.d.ts +3 -0
- package/dist/src/hooks/useGridEvents.js +59 -3
- package/dist/src/hooks/useGridSearch.d.ts +3 -3
- package/dist/src/hooks/useGridSearch.js +68 -42
- package/dist/src/types/types.d.ts +14 -1
- package/dist/src/views/DataGrid.js +68 -19
- package/package.json +67 -67
- package/src/example.tsx +184 -178
- package/src/func/func_a11y.ts +169 -0
- package/src/func/func_child.ts +92 -83
- package/src/hooks/useColumnData.ts +28 -34
- package/src/hooks/useGridEvents.ts +157 -96
- package/src/hooks/useGridSearch.tsx +106 -79
- package/src/types/types.ts +12 -1
- package/src/views/DataGrid.tsx +362 -286
package/README.md
CHANGED
|
@@ -16,6 +16,33 @@ const rows = [{ __table_id: '1', name: 'Alice' }];
|
|
|
16
16
|
|
|
17
17
|
The token stylesheet is optional. Without it, the grid uses Inovua's default-light appearance.
|
|
18
18
|
|
|
19
|
+
## Accessibility
|
|
20
|
+
|
|
21
|
+
The grid exposes ARIA grid semantics, so screen readers and automation tools can read it from the accessibility tree.
|
|
22
|
+
|
|
23
|
+
| Element | Attributes |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| Grid root | `role="grid"`, `aria-rowcount` (total rows incl. header; the full data count when paginated), `aria-colcount` (visible columns), `aria-label`, `aria-readonly="true"` when the grid isn't editable, `aria-busy="true"` while loading |
|
|
26
|
+
| Header row | `role="row"`, `aria-rowindex="1"` |
|
|
27
|
+
| Header cells | `role="columnheader"`, `aria-colindex`, `aria-sort` (`ascending` / `descending` / `none`, sortable columns only) |
|
|
28
|
+
| Rows | `role="row"`, `aria-rowindex` (1-based, header is 1, includes the page offset), `aria-selected` when selection is enabled, `data-row-id` |
|
|
29
|
+
| Body cells | `role="gridcell"`, `aria-colindex`, `data-column="<column name>"`, `aria-readonly="true"` when the cell can't be edited |
|
|
30
|
+
| Row-number cells | `role="rowheader"` |
|
|
31
|
+
|
|
32
|
+
New props:
|
|
33
|
+
|
|
34
|
+
- `ariaLabel?: string` on the grid sets the accessible name of the grid.
|
|
35
|
+
- `ariaLabel?: (value, data) => string` on a column sets `aria-label` on its cells. Use it for icons, badges or values whose visible text isn't descriptive.
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
const columns = [{ name: 'status', header: 'Status', ariaLabel: (value) => `Status: ${value}` }];
|
|
39
|
+
|
|
40
|
+
<AzGrid ariaLabel="Orders" columns={columns} dataSource={rows} />;
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Override any cell attribute with the column's `cellDOMProps` / `headerDOMProps`. Values you set there are kept.
|
|
44
|
+
A `gridOptions.onRenderRow` runs after the grid's own row attributes are set, so it can change them too.
|
|
45
|
+
|
|
19
46
|
## Themes
|
|
20
47
|
|
|
21
48
|
Use `theme="light"` (default), `theme="dark"`, or a custom name. Custom names apply
|
|
@@ -134,4 +161,4 @@ Use dark defaults with an obsidian background, gold accents, square corners and
|
|
|
134
161
|
See [src/example.tsx](src/example.tsx) for light, dark and a complete custom `deco` palette.
|
|
135
162
|
|
|
136
163
|
Custom renderers must style their own content. External menu portal containers need
|
|
137
|
-
`azgrid-scope`, the matching `azgrid-theme-<name>` class and the host variables.
|
|
164
|
+
`azgrid-scope`, the matching `azgrid-theme-<name>` class and the host variables.
|
package/dist/src/example.js
CHANGED
|
@@ -39,7 +39,8 @@ const columns = [{
|
|
|
39
39
|
name: 'age',
|
|
40
40
|
title: 'Age',
|
|
41
41
|
type: 'number',
|
|
42
|
-
defaultFlex: 1
|
|
42
|
+
defaultFlex: 1,
|
|
43
|
+
ariaLabel: value => `${value} years`
|
|
43
44
|
}];
|
|
44
45
|
const editableColumns = ['job', 'age'];
|
|
45
46
|
function getData(delta, max = 200) {
|
|
@@ -157,6 +158,7 @@ function ExampleGrid() {
|
|
|
157
158
|
}
|
|
158
159
|
}, _react.default.createElement("h3", null, theme === 'deco' ? 'Art Deco (host variables)' : `Default ${theme}`), _react.default.createElement(_DataGrid.default, {
|
|
159
160
|
theme: theme,
|
|
161
|
+
ariaLabel: `Users (${theme})`,
|
|
160
162
|
delayedSize: true,
|
|
161
163
|
// details_dataSource={details_dataSource}
|
|
162
164
|
dataSource: dataSource,
|
|
@@ -185,7 +187,11 @@ function ExampleGrid() {
|
|
|
185
187
|
defaultLimit: 10,
|
|
186
188
|
pageSizes: [10, 25, 50],
|
|
187
189
|
checkboxColumn: true,
|
|
188
|
-
multiSelect: true
|
|
190
|
+
multiSelect: true,
|
|
191
|
+
// consumer onRenderRow runs after the internal one
|
|
192
|
+
onRenderRow: rowProps => {
|
|
193
|
+
rowProps['data-example-row'] = 'true';
|
|
194
|
+
}
|
|
189
195
|
// defaultFilterValue: filterValue,
|
|
190
196
|
// remoteFilter: false,
|
|
191
197
|
// multiSelect: true,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { GridType } from '../types/types';
|
|
2
|
+
export type ColumnA11yOptions = {
|
|
3
|
+
/** role of body cells, e.g. 'gridcell' or 'rowheader' */
|
|
4
|
+
role?: string;
|
|
5
|
+
/** grid level editable flag, columns are readonly when the grid isn't editable */
|
|
6
|
+
gridEditable?: boolean;
|
|
7
|
+
/** false skips aria-readonly (for interactive special columns like the checkbox column) */
|
|
8
|
+
markReadonly?: boolean;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* wraps column dom props to add ARIA attributes,
|
|
12
|
+
* consumer's cellDOMProps/headerDOMProps run first and win over ours
|
|
13
|
+
* @param col
|
|
14
|
+
* @param options
|
|
15
|
+
* @returns a new column object
|
|
16
|
+
*/
|
|
17
|
+
export declare function apply_column_a11y<T extends Record<string, any>>(col: T, options?: ColumnA11yOptions): T;
|
|
18
|
+
/**
|
|
19
|
+
* adds ARIA attributes to a row, called from onRenderRow
|
|
20
|
+
* @param rowProps
|
|
21
|
+
* @param selectionEnabled
|
|
22
|
+
*/
|
|
23
|
+
export declare function apply_row_a11y(rowProps: any, selectionEnabled: boolean): void;
|
|
24
|
+
/**
|
|
25
|
+
* total number of data rows, uses the datasource count when paginated
|
|
26
|
+
* so the real size is reported, not just the visible page
|
|
27
|
+
* @param grid
|
|
28
|
+
*/
|
|
29
|
+
export declare function get_grid_row_count(grid: GridType): number;
|
|
30
|
+
export type RootA11yOptions = {
|
|
31
|
+
ariaLabel?: string;
|
|
32
|
+
editable?: boolean;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* true while the grid is loading (loading prop or async datasource)
|
|
36
|
+
* @param grid computed props of the grid
|
|
37
|
+
*/
|
|
38
|
+
export declare function is_grid_busy(grid: GridType | null | undefined): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* root element of the grid, null when it isn't mounted
|
|
41
|
+
* @param grid computed props of the grid
|
|
42
|
+
*/
|
|
43
|
+
export declare function get_grid_root(grid: GridType | null | undefined): Element | null;
|
|
44
|
+
/**
|
|
45
|
+
* key of the values apply_root_a11y writes, used to skip applying when nothing changed
|
|
46
|
+
* @param grid computed props of the grid
|
|
47
|
+
* @param options
|
|
48
|
+
*/
|
|
49
|
+
export declare function get_root_a11y_key(grid: GridType | null | undefined, options: RootA11yOptions): string;
|
|
50
|
+
/**
|
|
51
|
+
* sets ARIA attributes on the grid root element
|
|
52
|
+
* inovua doesn't forward props to its root, so this is applied on the DOM node
|
|
53
|
+
* @param grid computed props of the grid
|
|
54
|
+
* @param options
|
|
55
|
+
* @returns the element the attributes were applied to, null when the grid isn't mounted
|
|
56
|
+
*/
|
|
57
|
+
export declare function apply_root_a11y(grid: GridType | null | undefined, options: RootA11yOptions): Element | null;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.apply_column_a11y = apply_column_a11y;
|
|
7
|
+
exports.apply_root_a11y = apply_root_a11y;
|
|
8
|
+
exports.apply_row_a11y = apply_row_a11y;
|
|
9
|
+
exports.get_grid_root = get_grid_root;
|
|
10
|
+
exports.get_grid_row_count = get_grid_row_count;
|
|
11
|
+
exports.get_root_a11y_key = get_root_a11y_key;
|
|
12
|
+
exports.is_grid_busy = is_grid_busy;
|
|
13
|
+
function set_default(target, key, value) {
|
|
14
|
+
if (target[key] === undefined && value !== undefined && value !== null) target[key] = value;
|
|
15
|
+
}
|
|
16
|
+
function resolve_dom_props(src, cellProps) {
|
|
17
|
+
const res = typeof src === 'function' ? src(cellProps) : src;
|
|
18
|
+
return res ? {
|
|
19
|
+
...res
|
|
20
|
+
} : {};
|
|
21
|
+
}
|
|
22
|
+
function get_sort_dir(sortInfo) {
|
|
23
|
+
if (!sortInfo || !sortInfo.dir) return 'none';
|
|
24
|
+
return sortInfo.dir > 0 ? 'ascending' : 'descending';
|
|
25
|
+
}
|
|
26
|
+
function is_cell_readonly(cellProps, gridEditable) {
|
|
27
|
+
if (!gridEditable) return true;
|
|
28
|
+
const editable = cellProps.computedEditable;
|
|
29
|
+
//function editables are decided per edit, we can't know it here
|
|
30
|
+
if (typeof editable === 'function') return false;
|
|
31
|
+
return !editable;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* wraps column dom props to add ARIA attributes,
|
|
35
|
+
* consumer's cellDOMProps/headerDOMProps run first and win over ours
|
|
36
|
+
* @param col
|
|
37
|
+
* @param options
|
|
38
|
+
* @returns a new column object
|
|
39
|
+
*/
|
|
40
|
+
function apply_column_a11y(col, options = {}) {
|
|
41
|
+
const {
|
|
42
|
+
role = 'gridcell',
|
|
43
|
+
gridEditable = false,
|
|
44
|
+
markReadonly = true
|
|
45
|
+
} = options;
|
|
46
|
+
const consumerCellDOMProps = col.cellDOMProps;
|
|
47
|
+
const consumerHeaderDOMProps = col.headerDOMProps;
|
|
48
|
+
const ariaLabel = col.ariaLabel;
|
|
49
|
+
return {
|
|
50
|
+
...col,
|
|
51
|
+
cellDOMProps: cellProps => {
|
|
52
|
+
const res = resolve_dom_props(consumerCellDOMProps, cellProps);
|
|
53
|
+
set_default(res, 'role', role);
|
|
54
|
+
set_default(res, 'aria-colindex', cellProps.computedVisibleIndex + 1);
|
|
55
|
+
set_default(res, 'data-column', cellProps.name || cellProps.id);
|
|
56
|
+
if (markReadonly && is_cell_readonly(cellProps, gridEditable)) set_default(res, 'aria-readonly', 'true');
|
|
57
|
+
const data = cellProps.data;
|
|
58
|
+
if (typeof ariaLabel === 'function' && data && !data.__group && !data.__summary) set_default(res, 'aria-label', ariaLabel(cellProps.value, data));
|
|
59
|
+
return res;
|
|
60
|
+
},
|
|
61
|
+
headerDOMProps: cellProps => {
|
|
62
|
+
const res = resolve_dom_props(consumerHeaderDOMProps, cellProps);
|
|
63
|
+
set_default(res, 'role', 'columnheader');
|
|
64
|
+
set_default(res, 'aria-colindex', cellProps.computedVisibleIndex + 1);
|
|
65
|
+
if (cellProps.computedSortable) set_default(res, 'aria-sort', get_sort_dir(cellProps.computedSortInfo));
|
|
66
|
+
return res;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* adds ARIA attributes to a row, called from onRenderRow
|
|
72
|
+
* @param rowProps
|
|
73
|
+
* @param selectionEnabled
|
|
74
|
+
*/
|
|
75
|
+
function apply_row_a11y(rowProps, selectionEnabled) {
|
|
76
|
+
var _ref, _rowProps$remoteRowIn;
|
|
77
|
+
//filler rows (showEmptyRows) are not data rows
|
|
78
|
+
if (rowProps.empty) return;
|
|
79
|
+
const index = (_ref = (_rowProps$remoteRowIn = rowProps.remoteRowIndex) !== null && _rowProps$remoteRowIn !== void 0 ? _rowProps$remoteRowIn : rowProps.realIndex) !== null && _ref !== void 0 ? _ref : 0;
|
|
80
|
+
rowProps['role'] = 'row';
|
|
81
|
+
rowProps['aria-rowindex'] = index + 2; // 1-based, header row is 1
|
|
82
|
+
if (selectionEnabled) rowProps['aria-selected'] = rowProps.selected ? 'true' : 'false';
|
|
83
|
+
}
|
|
84
|
+
function set_attr(el, key, value) {
|
|
85
|
+
if (value === undefined || value === null || value === '') {
|
|
86
|
+
if (el.hasAttribute(key)) el.removeAttribute(key);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const str = String(value);
|
|
90
|
+
if (el.getAttribute(key) !== str) el.setAttribute(key, str);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* total number of data rows, uses the datasource count when paginated
|
|
94
|
+
* so the real size is reported, not just the visible page
|
|
95
|
+
* @param grid
|
|
96
|
+
*/
|
|
97
|
+
function get_grid_row_count(grid) {
|
|
98
|
+
const g = grid;
|
|
99
|
+
const rendered = Array.isArray(g.data) ? g.data.length : 0;
|
|
100
|
+
if (!g.computedPagination) return rendered;
|
|
101
|
+
const offset = g.computedLivePagination ? 0 : g.computedSkip || 0;
|
|
102
|
+
return Math.max(g.paginationCount || 0, offset + rendered);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* true while the grid is loading (loading prop or async datasource)
|
|
106
|
+
* @param grid computed props of the grid
|
|
107
|
+
*/
|
|
108
|
+
function is_grid_busy(grid) {
|
|
109
|
+
return !!(grid !== null && grid !== void 0 && grid.computedLoading);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* root element of the grid, null when it isn't mounted
|
|
113
|
+
* @param grid computed props of the grid
|
|
114
|
+
*/
|
|
115
|
+
function get_grid_root(grid) {
|
|
116
|
+
var _grid$getDOMNode;
|
|
117
|
+
return (grid === null || grid === void 0 || (_grid$getDOMNode = grid.getDOMNode) === null || _grid$getDOMNode === void 0 ? void 0 : _grid$getDOMNode.call(grid)) || null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* key of the values apply_root_a11y writes, used to skip applying when nothing changed
|
|
121
|
+
* @param grid computed props of the grid
|
|
122
|
+
* @param options
|
|
123
|
+
*/
|
|
124
|
+
function get_root_a11y_key(grid, options) {
|
|
125
|
+
var _options$ariaLabel;
|
|
126
|
+
const g = grid;
|
|
127
|
+
if (!g) return '';
|
|
128
|
+
return [get_grid_row_count(g), (g.visibleColumns || []).length, (_options$ariaLabel = options.ariaLabel) !== null && _options$ariaLabel !== void 0 ? _options$ariaLabel : '', options.editable ? 1 : 0, is_grid_busy(g) ? 1 : 0].join('|');
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* sets ARIA attributes on the grid root element
|
|
132
|
+
* inovua doesn't forward props to its root, so this is applied on the DOM node
|
|
133
|
+
* @param grid computed props of the grid
|
|
134
|
+
* @param options
|
|
135
|
+
* @returns the element the attributes were applied to, null when the grid isn't mounted
|
|
136
|
+
*/
|
|
137
|
+
function apply_root_a11y(grid, options) {
|
|
138
|
+
const g = grid;
|
|
139
|
+
const el = get_grid_root(g);
|
|
140
|
+
if (!el) return null;
|
|
141
|
+
set_attr(el, 'role', 'grid');
|
|
142
|
+
set_attr(el, 'aria-rowcount', get_grid_row_count(g) + 1);
|
|
143
|
+
set_attr(el, 'aria-colcount', (g.visibleColumns || []).length);
|
|
144
|
+
set_attr(el, 'aria-label', options.ariaLabel);
|
|
145
|
+
set_attr(el, 'aria-readonly', options.editable ? undefined : 'true');
|
|
146
|
+
set_attr(el, 'aria-busy', is_grid_busy(g) ? 'true' : undefined);
|
|
147
|
+
return el;
|
|
148
|
+
}
|
|
@@ -27,23 +27,31 @@ function fix_parent_child_relations(rows) {
|
|
|
27
27
|
};
|
|
28
28
|
});
|
|
29
29
|
const top_level_rows = converted_rows.filter(r => !r.parent);
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
//group rows by parent id once, instead of scanning all rows for every parent
|
|
31
|
+
const children_by_parent = new Map();
|
|
32
|
+
for (let index = 0; index < converted_rows.length; index++) {
|
|
33
|
+
const r = converted_rows[index];
|
|
34
|
+
if (!r.parent) continue;
|
|
35
|
+
const key = String(r.parent);
|
|
36
|
+
const siblings = children_by_parent.get(key);
|
|
37
|
+
if (siblings) siblings.push(r);else children_by_parent.set(key, [r]);
|
|
38
|
+
}
|
|
39
|
+
if (children_by_parent.size > 0) {
|
|
32
40
|
//find children of top-level parents:
|
|
33
41
|
for (let index = 0; index < top_level_rows.length; index++) {
|
|
34
42
|
const mrow = top_level_rows[index];
|
|
35
|
-
assign_children(mrow,
|
|
43
|
+
assign_children(mrow, children_by_parent);
|
|
36
44
|
}
|
|
37
45
|
}
|
|
38
46
|
return top_level_rows.map(r => r.data);
|
|
39
47
|
}
|
|
40
|
-
function assign_children(row,
|
|
41
|
-
const my_children =
|
|
42
|
-
if (my_children.length > 0) {
|
|
48
|
+
function assign_children(row, children_by_parent) {
|
|
49
|
+
const my_children = row.self == null ? undefined : children_by_parent.get(String(row.self));
|
|
50
|
+
if (my_children && my_children.length > 0) {
|
|
43
51
|
//find children of my children:
|
|
44
52
|
for (let index = 0; index < my_children.length; index++) {
|
|
45
53
|
const child = my_children[index];
|
|
46
|
-
assign_children(child,
|
|
54
|
+
assign_children(child, children_by_parent);
|
|
47
55
|
}
|
|
48
56
|
row.data.__children = my_children.map(r => r.data);
|
|
49
57
|
} else if (!Array.isArray(row.data.__children) || row.data.__children.length == 0) row.data.__children = undefined;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { AzGridProps, LooseColumnDefinition } from '../types/types';
|
|
2
|
-
export declare function useColumnData(inputOrProps: undefined | any[] | LooseColumnDefinition[] | AzGridProps):
|
|
2
|
+
export declare function useColumnData(inputOrProps: undefined | any[] | LooseColumnDefinition[] | AzGridProps): any[];
|
|
@@ -7,18 +7,14 @@ exports.useColumnData = useColumnData;
|
|
|
7
7
|
var _react = _interopRequireDefault(require("react"));
|
|
8
8
|
var _func_grid = require("../func/func_grid");
|
|
9
9
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
const EMPTY_COLUMNS = [];
|
|
10
11
|
function useColumnData(inputOrProps) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
function useThisColumnData(cols) {
|
|
12
|
+
// one useMemo for every input kind, so switching between them keeps the hook order
|
|
13
|
+
const isProps = inputOrProps != null && !Array.isArray(inputOrProps);
|
|
14
|
+
const props = isProps ? inputOrProps : {};
|
|
15
|
+
const deps = isProps ? [props.columns, props.leftColumns, props.rightColumns, props.editableColumns, props.columnExtensions, props.customColumns] : [inputOrProps, null, null, null, null, null];
|
|
16
16
|
return _react.default.useMemo(() => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
function usePropsColumnData(props) {
|
|
21
|
-
return _react.default.useMemo(() => {
|
|
22
|
-
return (0, _func_grid.applyColumnData)(props);
|
|
23
|
-
}, [props.columns, props.leftColumns, props.rightColumns, props.editableColumns, props.columnExtensions, props.customColumns]);
|
|
17
|
+
if (inputOrProps == null) return EMPTY_COLUMNS;
|
|
18
|
+
return (0, _func_grid.applyColumnData)(inputOrProps);
|
|
19
|
+
}, [isProps, ...deps]);
|
|
24
20
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { DataGridWrapperProps, GridType } from '../types/types';
|
|
2
2
|
export declare function useGridEvents(grid: GridType, props: DataGridWrapperProps): {
|
|
3
3
|
onRowClick: (rowProps: any, event: any) => void | undefined;
|
|
4
|
+
onRenderLoadMask: (maskProps: {
|
|
5
|
+
visible?: boolean;
|
|
6
|
+
}) => void;
|
|
4
7
|
onRowContextMenu: (rowProps: any, event: any) => boolean;
|
|
5
8
|
onRenderRow: (rowProps: any) => void;
|
|
6
9
|
update_menuRow: (r?: any, el?: any) => void;
|
|
@@ -7,6 +7,7 @@ exports.useGridEvents = useGridEvents;
|
|
|
7
7
|
var _react = _interopRequireDefault(require("react"));
|
|
8
8
|
var _func_grid = require("../func/func_grid");
|
|
9
9
|
var _func_child = require("../func/func_child");
|
|
10
|
+
var _func_a11y = require("../func/func_a11y");
|
|
10
11
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
12
|
function useGridMenuRow() {
|
|
12
13
|
const [menuAnchorEl, set_menuAnchorEl] = _react.default.useState(null);
|
|
@@ -23,6 +24,9 @@ function useGridMenuRow() {
|
|
|
23
24
|
}
|
|
24
25
|
function useGridEvents(grid, props) {
|
|
25
26
|
const click_data = _react.default.useRef(null);
|
|
27
|
+
// latest props, so the callbacks below stay stable and don't re-render the grid
|
|
28
|
+
const latestProps = _react.default.useRef(props);
|
|
29
|
+
latestProps.current = props;
|
|
26
30
|
const {
|
|
27
31
|
update_menuRow,
|
|
28
32
|
menuAnchorEl,
|
|
@@ -51,18 +55,22 @@ function useGridEvents(grid, props) {
|
|
|
51
55
|
/* events */
|
|
52
56
|
/* -------------------------------------------------------------------------- */
|
|
53
57
|
const onRowClick = _react.default.useCallback((rowProps, event) => {
|
|
58
|
+
const {
|
|
59
|
+
onRowClick,
|
|
60
|
+
onRowDoubleClick
|
|
61
|
+
} = latestProps.current;
|
|
54
62
|
const nowDate = Date.now();
|
|
55
63
|
const lastclick = click_data.current[(0, _func_grid.get_row_id)(rowProps)];
|
|
56
64
|
if (lastclick) {
|
|
57
65
|
const diffTime = Math.abs(nowDate - lastclick);
|
|
58
66
|
if (diffTime < 400) {
|
|
59
67
|
click_data.current[(0, _func_grid.get_row_id)(rowProps)] = null;
|
|
60
|
-
return
|
|
68
|
+
return onRowDoubleClick && onRowDoubleClick(rowProps.rowIndex, rowProps.data);
|
|
61
69
|
}
|
|
62
70
|
}
|
|
63
71
|
click_data.current[(0, _func_grid.get_row_id)(rowProps)] = nowDate;
|
|
64
|
-
return
|
|
65
|
-
}, [
|
|
72
|
+
return onRowClick && onRowClick(rowProps.rowIndex, rowProps.data);
|
|
73
|
+
}, []);
|
|
66
74
|
_react.default.useEffect(() => {
|
|
67
75
|
click_data.current = {};
|
|
68
76
|
}, [props.dataSource]);
|
|
@@ -79,12 +87,60 @@ function useGridEvents(grid, props) {
|
|
|
79
87
|
return false;
|
|
80
88
|
}, []);
|
|
81
89
|
/* -------------------------------------------------------------------------- */
|
|
90
|
+
/* ------------------------------ Accessibility ----------------------------- */
|
|
91
|
+
const rootSyncFrame = _react.default.useRef(0);
|
|
92
|
+
// last applied root values, so row renders (e.g. while scrolling) don't re-apply them
|
|
93
|
+
const lastRoot = _react.default.useRef({
|
|
94
|
+
el: null,
|
|
95
|
+
key: '',
|
|
96
|
+
busy: false
|
|
97
|
+
});
|
|
98
|
+
const getRootOptions = () => ({
|
|
99
|
+
ariaLabel: latestProps.current.ariaLabel,
|
|
100
|
+
editable: !!latestProps.current.editable
|
|
101
|
+
});
|
|
102
|
+
// root attributes are set on the DOM node, batched to once per frame
|
|
103
|
+
const scheduleRootSync = _react.default.useCallback((force = false) => {
|
|
104
|
+
var _latestProps$current$;
|
|
105
|
+
if (rootSyncFrame.current || typeof requestAnimationFrame === 'undefined') return;
|
|
106
|
+
const grid = (_latestProps$current$ = latestProps.current.liveGridRef) === null || _latestProps$current$ === void 0 || (_latestProps$current$ = _latestProps$current$.current) === null || _latestProps$current$ === void 0 ? void 0 : _latestProps$current$.current;
|
|
107
|
+
const key = (0, _func_a11y.get_root_a11y_key)(grid, getRootOptions());
|
|
108
|
+
if (!force && key && key === lastRoot.current.key && (0, _func_a11y.get_grid_root)(grid) === lastRoot.current.el) return;
|
|
109
|
+
rootSyncFrame.current = requestAnimationFrame(() => {
|
|
110
|
+
var _latestProps$current$2;
|
|
111
|
+
rootSyncFrame.current = 0;
|
|
112
|
+
const liveGrid = (_latestProps$current$2 = latestProps.current.liveGridRef) === null || _latestProps$current$2 === void 0 || (_latestProps$current$2 = _latestProps$current$2.current) === null || _latestProps$current$2 === void 0 ? void 0 : _latestProps$current$2.current;
|
|
113
|
+
const options = getRootOptions();
|
|
114
|
+
const el = (0, _func_a11y.apply_root_a11y)(liveGrid, options);
|
|
115
|
+
lastRoot.current = {
|
|
116
|
+
el,
|
|
117
|
+
key: el ? (0, _func_a11y.get_root_a11y_key)(liveGrid, options) : '',
|
|
118
|
+
busy: el ? (0, _func_a11y.is_grid_busy)(liveGrid) : false
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}, []);
|
|
122
|
+
_react.default.useEffect(() => {
|
|
123
|
+
scheduleRootSync();
|
|
124
|
+
}, [scheduleRootSync, props.ariaLabel, props.editable, props.columns, props.dataSource]);
|
|
125
|
+
_react.default.useEffect(() => () => {
|
|
126
|
+
if (rootSyncFrame.current) cancelAnimationFrame(rootSyncFrame.current);
|
|
127
|
+
rootSyncFrame.current = 0;
|
|
128
|
+
}, []);
|
|
82
129
|
const onRenderRow = _react.default.useCallback(rowProps => {
|
|
130
|
+
var _latestProps$current$3;
|
|
83
131
|
// save the original handlers to be called later
|
|
84
132
|
rowProps['data-row-id'] = (0, _func_grid.get_row_id)(rowProps.data);
|
|
133
|
+
const liveGrid = (_latestProps$current$3 = latestProps.current.liveGridRef) === null || _latestProps$current$3 === void 0 || (_latestProps$current$3 = _latestProps$current$3.current) === null || _latestProps$current$3 === void 0 ? void 0 : _latestProps$current$3.current;
|
|
134
|
+
(0, _func_a11y.apply_row_a11y)(rowProps, !!(liveGrid !== null && liveGrid !== void 0 && liveGrid.computedRowSelectionEnabled));
|
|
135
|
+
scheduleRootSync();
|
|
136
|
+
}, []);
|
|
137
|
+
// called by the grid on every render with the load mask state, keeps aria-busy in sync
|
|
138
|
+
const onRenderLoadMask = _react.default.useCallback(maskProps => {
|
|
139
|
+
if (!!(maskProps !== null && maskProps !== void 0 && maskProps.visible) !== lastRoot.current.busy) scheduleRootSync(true);
|
|
85
140
|
}, []);
|
|
86
141
|
return {
|
|
87
142
|
onRowClick,
|
|
143
|
+
onRenderLoadMask,
|
|
88
144
|
onRowContextMenu,
|
|
89
145
|
onRenderRow,
|
|
90
146
|
update_menuRow,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { DataGridWrapperProps
|
|
3
|
-
export declare function useGridSearch(
|
|
4
|
-
filteredData: any;
|
|
2
|
+
import { DataGridWrapperProps } from '../types/types';
|
|
3
|
+
export declare function useGridSearch(props: DataGridWrapperProps): {
|
|
4
|
+
filteredData: any[] | ((args: any) => Promise<any>);
|
|
5
5
|
element: React.JSX.Element;
|
|
6
6
|
};
|
|
@@ -8,51 +8,77 @@ var _react = _interopRequireDefault(require("react"));
|
|
|
8
8
|
var _lodash = _interopRequireDefault(require("lodash.debounce"));
|
|
9
9
|
var _object = require("@azrico/object");
|
|
10
10
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
-
|
|
11
|
+
const SEARCH_DELAY = 300;
|
|
12
|
+
function to_rows(result) {
|
|
13
|
+
return result && typeof result === 'object' ? result.data || result : result;
|
|
14
|
+
}
|
|
15
|
+
//false when the result is only a page of the data (remote pagination)
|
|
16
|
+
function is_complete(result) {
|
|
17
|
+
if (!result || typeof result !== 'object' || Array.isArray(result)) return true;
|
|
18
|
+
const loaded = Array.isArray(result.data) ? result.data.length : 0;
|
|
19
|
+
return !(result.count > loaded);
|
|
20
|
+
}
|
|
21
|
+
function useGridSearch(props) {
|
|
12
22
|
const [search, set_search] = _react.default.useState('');
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
23
|
+
// null shows the datasource itself, an array shows search results
|
|
24
|
+
const [searchResult, set_searchResult] = _react.default.useState(null);
|
|
25
|
+
const latestProps = _react.default.useRef(props);
|
|
26
|
+
latestProps.current = props;
|
|
27
|
+
// last result the grid loaded, reused by search instead of loading the data again
|
|
28
|
+
const loaded = _react.default.useRef(null);
|
|
29
|
+
const searchId = _react.default.useRef(0);
|
|
30
|
+
const trackedDataSource = _react.default.useCallback(async args => {
|
|
31
|
+
const result = await (0, _object.fn_eval)(props.dataSource, args);
|
|
32
|
+
loaded.current = {
|
|
33
|
+
ds: props.dataSource,
|
|
34
|
+
result
|
|
35
|
+
};
|
|
36
|
+
return result;
|
|
37
|
+
}, [props.dataSource]);
|
|
38
|
+
const get_search_rows = _react.default.useCallback(async () => {
|
|
39
|
+
const ds = latestProps.current.dataSource;
|
|
40
|
+
const last = loaded.current;
|
|
41
|
+
if (last && last.ds === ds && is_complete(last.result)) return to_rows(last.result);
|
|
42
|
+
return to_rows(await (0, _object.fn_eval)(ds));
|
|
43
|
+
}, []);
|
|
44
|
+
const filter_rows = _react.default.useCallback((rows, text) => {
|
|
45
|
+
var _gridRef$current;
|
|
46
|
+
const {
|
|
47
|
+
gridRef,
|
|
48
|
+
advancedFilterFields
|
|
49
|
+
} = latestProps.current;
|
|
50
|
+
const visibleColumns = (_gridRef$current = gridRef.current) === null || _gridRef$current === void 0 ? void 0 : _gridRef$current.visibleColumns;
|
|
51
|
+
const columns_to_search = visibleColumns ? (0, _object.array_intersection)(advancedFilterFields || [], visibleColumns.map(r => r.id)) : advancedFilterFields || [];
|
|
52
|
+
const lowerSearchText = text.toLowerCase().trim();
|
|
53
|
+
if (!lowerSearchText || columns_to_search.length === 0) return rows;
|
|
54
|
+
return rows.filter(p => {
|
|
55
|
+
for (let index = 0; index < columns_to_search.length; index++) {
|
|
56
|
+
const col = columns_to_search[index];
|
|
57
|
+
const v = (p[col] + '').toLowerCase(); //value of column
|
|
58
|
+
if (v && v.indexOf(lowerSearchText) != -1) {
|
|
59
|
+
//if one column is matching then the row is a match
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
37
62
|
}
|
|
38
|
-
|
|
39
|
-
set_filteredData(newData);
|
|
40
|
-
}, 1000), [grid]);
|
|
41
|
-
_react.default.useEffect(() => {
|
|
42
|
-
(0, _object.fn_eval)(props.dataSource).then(r => {
|
|
43
|
-
currentDS.current = typeof r === 'object' ? r.data || r : r;
|
|
44
|
-
searchIfNeeded();
|
|
63
|
+
return false;
|
|
45
64
|
});
|
|
46
|
-
}, [
|
|
65
|
+
}, []);
|
|
66
|
+
const run_search = _react.default.useMemo(() => (0, _lodash.default)(async text => {
|
|
67
|
+
const id = ++searchId.current;
|
|
68
|
+
const rows = await get_search_rows();
|
|
69
|
+
if (id !== searchId.current || !Array.isArray(rows)) return; //a newer search started
|
|
70
|
+
set_searchResult(filter_rows(rows, text));
|
|
71
|
+
}, SEARCH_DELAY), []);
|
|
72
|
+
_react.default.useEffect(() => () => run_search.cancel(), [run_search]);
|
|
47
73
|
_react.default.useEffect(() => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
74
|
+
if (search) {
|
|
75
|
+
run_search(search);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
run_search.cancel();
|
|
79
|
+
searchId.current++;
|
|
80
|
+
set_searchResult(null);
|
|
81
|
+
}, [search, props.dataSource]);
|
|
56
82
|
const element = _react.default.createElement(_react.default.Fragment, null, _react.default.createElement("form", {
|
|
57
83
|
className: 'azgrid-searcharea'
|
|
58
84
|
}, _react.default.createElement("label", null, "Search", _react.default.createElement("input", {
|
|
@@ -62,7 +88,7 @@ function useGridSearch(grid, props) {
|
|
|
62
88
|
onChange: e => set_search(e.target.value)
|
|
63
89
|
}))));
|
|
64
90
|
return {
|
|
65
|
-
filteredData,
|
|
91
|
+
filteredData: searchResult !== null && searchResult !== void 0 ? searchResult : trackedDataSource,
|
|
66
92
|
element
|
|
67
93
|
};
|
|
68
94
|
}
|
|
@@ -28,19 +28,30 @@ export type loose_column_fields = loose_column_names & {
|
|
|
28
28
|
title: string;
|
|
29
29
|
type: string;
|
|
30
30
|
items: any[];
|
|
31
|
+
/** accessible name for cells whose visible text isn't descriptive (icons, badges, ...), set as aria-label */
|
|
32
|
+
ariaLabel: (value: any, data: any) => string;
|
|
31
33
|
};
|
|
32
34
|
export type LooseColumnDefinition = Partial<loose_column_fields & ColumnDefinition>;
|
|
33
35
|
export type CustomColumnDefinition = {
|
|
34
36
|
name: string;
|
|
35
37
|
render: (cell: any, formatterParams: any, onRendered: any) => React.ReactNode | undefined | null;
|
|
36
38
|
};
|
|
37
|
-
export type AzGridOptions = Partial<TypeDataGridProps
|
|
39
|
+
export type AzGridOptions = Partial<TypeDataGridProps> & {
|
|
40
|
+
/** called for every rendered row, after the grid's own row attributes are set */
|
|
41
|
+
onRenderRow?: (rowProps: any) => void;
|
|
42
|
+
/** extra DOM props for the header row */
|
|
43
|
+
headerProps?: {
|
|
44
|
+
[key: string]: any;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
38
47
|
export type AzGridProps = {
|
|
39
48
|
dataSource?: any[] | ((args: any) => any[] | Promise<any[] | any>);
|
|
40
49
|
details_dataSource?: any[] | ((args: any) => any[] | Promise<any[] | any>);
|
|
41
50
|
details_columns?: LooseColumnDefinition[];
|
|
42
51
|
details_filter?: FilterValues;
|
|
43
52
|
licenseKey?: string;
|
|
53
|
+
/** accessible name of the grid, set as aria-label on the grid root */
|
|
54
|
+
ariaLabel?: string;
|
|
44
55
|
gridRef?: React.MutableRefObject<TypeComputedProps>;
|
|
45
56
|
gridOptions?: AzGridOptions;
|
|
46
57
|
columns?: LooseColumnDefinition[];
|
|
@@ -81,6 +92,8 @@ export type DataGridProps = ModifyFields<AzGridProps, {
|
|
|
81
92
|
export type GridType = TypeComputedProps;
|
|
82
93
|
export type DataGridWrapperProps = ModifyFields<DataGridProps, {
|
|
83
94
|
gridRef: React.MutableRefObject<TypeComputedProps>;
|
|
95
|
+
/** internal: inovua's live computed props ref (gridRef holds a snapshot) */
|
|
96
|
+
liveGridRef: React.MutableRefObject<React.MutableRefObject<TypeComputedProps> | null>;
|
|
84
97
|
static_gridOptions: AzGridOptions;
|
|
85
98
|
dynamic_gridOptions: AzGridOptions;
|
|
86
99
|
children?: React.ReactNode;
|