@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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "ClaudeInChromeDomain(localhost:3123)",
5
+ "mcp__claude-in-chrome__tabs_context_mcp"
6
+ ]
7
+ }
8
+ }
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.
@@ -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
- const any_child = converted_rows.find(r => Boolean(r.parent));
31
- if (any_child) {
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, converted_rows);
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, all_rows) {
41
- const my_children = all_rows.filter(s => row.self == s.parent);
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, all_rows);
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): import("@inovua/reactdatagrid-community/types").TypeColumn[];
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
- if (inputOrProps == null) return [];
12
- if (Array.isArray(inputOrProps)) return useThisColumnData(inputOrProps);
13
- return usePropsColumnData(inputOrProps);
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
- return (0, _func_grid.applyColumnData)(cols);
18
- }, [cols]);
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 props.onRowDoubleClick && props.onRowDoubleClick(rowProps.rowIndex, rowProps.data);
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 props.onRowClick && props.onRowClick(rowProps.rowIndex, rowProps.data);
65
- }, [props.onRowClick, props.onRowDoubleClick]);
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, GridType } from '../types/types';
3
- export declare function useGridSearch(grid: GridType, props: DataGridWrapperProps): {
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
- function useGridSearch(grid, props) {
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
- const [filteredData, set_filteredData] = _react.default.useState([]);
14
- const currentSearch = _react.default.useRef('');
15
- const currentDS = _react.default.useRef([]);
16
- const try_search = _react.default.useCallback((0, _lodash.default)(() => {
17
- if (!Array.isArray(currentDS.current)) {
18
- return;
19
- }
20
- let newData = [];
21
- if (currentSearch.current) {
22
- const visibleColumns = grid.visibleColumns;
23
- const columns_to_search = (0, _object.array_intersection)(props.advancedFilterFields || [], visibleColumns.map(r => r.id));
24
- const lowerSearchText = currentSearch.current.toLowerCase().trim();
25
- if (!lowerSearchText || columns_to_search.length === 0) newData = currentDS.current;else {
26
- newData = currentDS.current.filter(p => {
27
- for (let index = 0; index < columns_to_search.length; index++) {
28
- const col = columns_to_search[index];
29
- const v = (p[col] + '').toLowerCase(); //value of column
30
- if (v && v.indexOf(lowerSearchText) != -1) {
31
- //if one column is matching then the row is a match
32
- return true;
33
- }
34
- }
35
- return false;
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
- } else newData = currentDS.current;
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
- }, [props.dataSource]);
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
- currentSearch.current = search;
49
- searchIfNeeded();
50
- }, [search]);
51
- function searchIfNeeded() {
52
- if (!search) {
53
- set_filteredData(props.dataSource);
54
- } else try_search();
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;