@azrico/reactgrid 1.5.1 → 1.6.0

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.
@@ -40,11 +40,17 @@ const forcedOptions = {
40
40
  theme: 'default-light'
41
41
  };
42
42
  const dynamic_option_names = ['selected', 'cellSelection', 'loading', 'limit', 'skip'];
43
+ // consumer callbacks forwarded to the grid; wrapped so a new function doesn't re-render the grid
44
+ const callback_option_names = ['onSkipChange', 'onLimitChange', 'onEditComplete', 'onSelectionChange', 'onCellSelectionChange'];
45
+ // props DataGridCore renders from, other props don't need to re-render the grid
46
+ const core_prop_names = ['className', 'static_gridOptions', 'dynamic_gridOptions', 'columns', 'dataSource', 'gridRef', 'liveGridRef', 'allowChildRows', 'theme', 'dark', 'children'];
43
47
  /* -------------------------------------------------------------------------- */
44
48
  function AzGrid(props) {
45
49
  const gridRefBackup = _react.default.useRef(null);
46
50
  const gridRef = props.gridRef || gridRefBackup;
47
51
  const liveGridRef = _react.default.useRef(null);
52
+ const latestProps = _react.default.useRef(props);
53
+ latestProps.current = props;
48
54
  /* -------------------------------------------------------------------------- */
49
55
  /* Columns */
50
56
  /* -------------------------------------------------------------------------- */
@@ -59,14 +65,15 @@ function AzGrid(props) {
59
65
  const base_gridOptions = _react.default.useMemo(() => {
60
66
  const firstcolumn = cached_columns[0];
61
67
  const licenseKey = props.licenseKey || (0, _func_grid.get_default_key)();
62
- const events = {
63
- onSkipChange: props.onSkipChange,
64
- onLimitChange: props.onLimitChange,
65
- onEditComplete: props.onEditComplete
66
- };
68
+ // call the latest consumer callback, only passed when the consumer gave one
69
+ const events = {};
70
+ callback_option_names.forEach(name => {
71
+ if (props[name]) events[name] = (...args) => {
72
+ var _latestProps$current$, _latestProps$current;
73
+ return (_latestProps$current$ = (_latestProps$current = latestProps.current)[name]) === null || _latestProps$current$ === void 0 ? void 0 : _latestProps$current$.call(_latestProps$current, ...args);
74
+ };
75
+ });
67
76
  const extra_ops = {
68
- onSelectionChange: props.onSelectionChange,
69
- onCellSelectionChange: props.onCellSelectionChange,
70
77
  licenseKey: licenseKey,
71
78
  editable: props.editable,
72
79
  treeColumn: props.allowChildRows ? firstcolumn.name : undefined,
@@ -77,7 +84,7 @@ function AzGrid(props) {
77
84
  ...events,
78
85
  ...extra_ops
79
86
  };
80
- }, [cached_columns, props.allowChildRows, props.editable, props.RowDetail, props.onEditComplete]);
87
+ }, [cached_columns, props.allowChildRows, props.editable, props.RowDetail, props.licenseKey, ...callback_option_names.map(name => !!props[name])]);
81
88
  const static_gridOptions = _react.default.useMemo(() => {
82
89
  const finalres = {
83
90
  ...base_gridOptions,
@@ -132,6 +139,7 @@ function DataGridEventsWrapper(props) {
132
139
  onRowClick,
133
140
  onRowContextMenu,
134
141
  onRenderRow,
142
+ onRenderLoadMask,
135
143
  update_menuRow,
136
144
  menuAnchorEl,
137
145
  menuRow
@@ -152,7 +160,12 @@ function DataGridEventsWrapper(props) {
152
160
  onRenderRow: consumerOnRenderRow ? rowProps => {
153
161
  onRenderRow(rowProps);
154
162
  consumerOnRenderRow(rowProps);
155
- } : onRenderRow
163
+ } : onRenderRow,
164
+ // track loading for aria-busy, the consumer's renderLoadMask (if any) still renders the mask
165
+ renderLoadMask: maskProps => {
166
+ onRenderLoadMask(maskProps);
167
+ return opts.renderLoadMask ? opts.renderLoadMask(maskProps) : undefined;
168
+ }
156
169
  };
157
170
  const gridEditable = !!props.editable;
158
171
  const a11yProps = {
@@ -176,7 +189,7 @@ function DataGridEventsWrapper(props) {
176
189
  ...renderProps,
177
190
  ...a11yProps
178
191
  };
179
- }, [props.static_gridOptions, props.editable, onRowClick, onRowContextMenu, onRenderRow]);
192
+ }, [props.static_gridOptions, props.editable, onRowClick, onRowContextMenu, onRenderRow, onRenderLoadMask]);
180
193
  /* -------------------------------------------------------------------------- */
181
194
  //TODO ContextMenu not from mui...
182
195
  return _react.default.createElement("div", {
@@ -271,7 +284,7 @@ function DataGridSearchWrapper(props) {
271
284
  const {
272
285
  element,
273
286
  filteredData
274
- } = (0, _useGridSearch.useGridSearch)(props.gridRef.current, props);
287
+ } = (0, _useGridSearch.useGridSearch)(props);
275
288
  return _react.default.createElement("div", {
276
289
  className: 'azgrid-flexed azgrid-flexlayout azgrid-nooverflow'
277
290
  }, element, _react.default.createElement(DataGridCore, {
@@ -279,18 +292,24 @@ function DataGridSearchWrapper(props) {
279
292
  dataSource: filteredData
280
293
  }));
281
294
  }
282
- function DataGridCore(props) {
295
+ const DataGridCore = _react.default.memo(DataGridCoreView, (prev, next) => core_prop_names.every(name => prev[name] === next[name]));
296
+ function DataGridCoreView(props) {
283
297
  var _props$theme3;
298
+ const {
299
+ gridRef,
300
+ liveGridRef
301
+ } = props;
302
+ const handle = _react.default.useCallback(ref => {
303
+ if (ref) gridRef.current = ref.current;
304
+ if (liveGridRef) liveGridRef.current = ref;
305
+ }, [gridRef, liveGridRef]);
284
306
  return _react.default.createElement("div", {
285
307
  className: (0, _clsx.default)(props.className, 'azgrid-flexed', 'azgrid-flexlayout')
286
308
  }, _react.default.createElement(_reactdatagridEnterprise.default, {
287
309
  ...props.static_gridOptions,
288
310
  ...props.dynamic_gridOptions,
289
311
  rowIndexColumn: props.static_gridOptions.rowIndexColumn || true,
290
- handle: ref => {
291
- if (ref) props.gridRef.current = ref.current;
292
- if (props.liveGridRef) props.liveGridRef.current = ref;
293
- },
312
+ handle: handle,
294
313
  theme: "default-light",
295
314
  className: (0, _clsx.default)('azgrid azgrid-flexed', props.allowChildRows && 'azgrid-tree', `azgrid-theme-${(_props$theme3 = props.theme) !== null && _props$theme3 !== void 0 ? _props$theme3 : props.dark ? 'dark' : 'light'}`),
296
315
  columns: props.columns,
package/index.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  export * from './src/views/DataGrid';
2
2
  export * from './src/types/types';
3
3
  export { get_row_id, set_row_id, get_col_name, get_col_header } from './src/func/func_grid';
4
+ // filter editors for column.filterEditor, so consumers don't import @inovua directly
5
+ export { default as SelectFilter } from '@inovua/reactdatagrid-enterprise/SelectFilter';
6
+ export { default as NumberFilter } from '@inovua/reactdatagrid-enterprise/NumberFilter';
7
+ export { default as DateFilter } from '@inovua/reactdatagrid-enterprise/DateFilter';
8
+ export { default as BoolFilter } from '@inovua/reactdatagrid-enterprise/BoolFilter';
4
9
 
5
10
  import { AzGrid } from './src/views/DataGrid';
6
11
  export default AzGrid;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azrico/reactgrid",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "main": "dist/index.js",
5
5
  "description": "React Datagrid",
6
6
  "author": "Azrideus <MehrdadTavangar@gmail.com>",
@@ -115,6 +115,14 @@ export function get_grid_row_count(grid: GridType): number {
115
115
 
116
116
  export type RootA11yOptions = { ariaLabel?: string; editable?: boolean };
117
117
 
118
+ /**
119
+ * true while the grid is loading (loading prop or async datasource)
120
+ * @param grid computed props of the grid
121
+ */
122
+ export function is_grid_busy(grid: GridType | null | undefined) {
123
+ return !!(grid as any)?.computedLoading;
124
+ }
125
+
118
126
  /**
119
127
  * root element of the grid, null when it isn't mounted
120
128
  * @param grid computed props of the grid
@@ -136,6 +144,7 @@ export function get_root_a11y_key(grid: GridType | null | undefined, options: Ro
136
144
  (g.visibleColumns || []).length,
137
145
  options.ariaLabel ?? '',
138
146
  options.editable ? 1 : 0,
147
+ is_grid_busy(g) ? 1 : 0,
139
148
  ].join('|');
140
149
  }
141
150
 
@@ -155,5 +164,6 @@ export function apply_root_a11y(grid: GridType | null | undefined, options: Root
155
164
  set_attr(el, 'aria-colcount', (g.visibleColumns || []).length);
156
165
  set_attr(el, 'aria-label', options.ariaLabel);
157
166
  set_attr(el, 'aria-readonly', options.editable ? undefined : 'true');
167
+ set_attr(el, 'aria-busy', is_grid_busy(g) ? 'true' : undefined);
158
168
  return el;
159
169
  }
@@ -1,83 +1,92 @@
1
- type mrow = { __parent__table_id: any; __table_id: any; __children?: any[] };
2
- type converted_row = { parent: any; self: any; data: mrow };
3
-
4
- function getParentId(obj) {
5
- return obj.__parent__table_id ?? obj.parentid ?? obj.parent;
6
- }
7
- function getSelfId(obj) {
8
- return obj.__table_id ?? obj._id;
9
- }
10
- //given an array of rows, fixes parent,child relations
11
- export function fix_parent_child_relations<T extends mrow>(rows: T[]): T[] {
12
- if (!Array.isArray(rows)) rows = [];
13
- const anyparentid = rows.find((r) => getParentId(r));
14
- const anyselfid = rows.find((r) => getSelfId(r));
15
- if (!anyparentid || !anyselfid) return rows;
16
-
17
- const converted_rows: converted_row[] = rows
18
- .filter((r) => r != null)
19
- .map((r) => {
20
- return { parent: getParentId(r), self: getSelfId(r), data: r };
21
- });
22
-
23
- const top_level_rows: converted_row[] = converted_rows.filter((r) => !r.parent);
24
- const any_child = converted_rows.find((r) => Boolean(r.parent));
25
- if (any_child) {
26
- //find children of top-level parents:
27
- for (let index = 0; index < top_level_rows.length; index++) {
28
- const mrow = top_level_rows[index];
29
- assign_children(mrow, converted_rows);
30
- }
31
- }
32
- return top_level_rows.map((r) => r.data) as T[];
33
- }
34
-
35
- function assign_children(row: converted_row, all_rows: converted_row[]) {
36
- const my_children = all_rows.filter((s) => row.self == s.parent);
37
- if (my_children.length > 0) {
38
- //find children of my children:
39
- for (let index = 0; index < my_children.length; index++) {
40
- const child = my_children[index];
41
- assign_children(child, all_rows);
42
- }
43
- row.data.__children = my_children.map((r) => r.data);
44
- } else if (!Array.isArray(row.data.__children) || row.data.__children.length == 0)
45
- row.data.__children = undefined;
46
- }
47
- export class func_selection {
48
- static getVirtualElement(x = 0, y = 0) {
49
- const boundsRect = func_selection.generateGetBoundingClientRect(x, y);
50
- return {
51
- getBoundingClientRect: boundsRect,
52
- };
53
- }
54
- static generateGetBoundingClientRect(x = 0, y = 0) {
55
- return () => ({
56
- width: 0,
57
- height: 0,
58
- top: y,
59
- right: x,
60
- bottom: y,
61
- left: x,
62
- });
63
- }
64
- static get_selectedText(): string {
65
- var text: any = '';
66
- if (window.getSelection) {
67
- text = window.getSelection()?.toString();
68
- } else if (
69
- (document as any).selection &&
70
- (document as any).selection.type != 'Control'
71
- ) {
72
- text = (document as any).selection.createRange()?.text;
73
- }
74
- return text || '';
75
- }
76
- static has_selection(): boolean {
77
- const slct = window.getSelection();
78
- if (slct == null) return false;
79
- if (slct.type == null) return func_selection.get_selectedText().length !== 0;
80
- if (slct.type === 'Range') return true;
81
- return false;
82
- }
83
- }
1
+ type mrow = { __parent__table_id: any; __table_id: any; __children?: any[] };
2
+ type converted_row = { parent: any; self: any; data: mrow };
3
+
4
+ function getParentId(obj) {
5
+ return obj.__parent__table_id ?? obj.parentid ?? obj.parent;
6
+ }
7
+ function getSelfId(obj) {
8
+ return obj.__table_id ?? obj._id;
9
+ }
10
+ //given an array of rows, fixes parent,child relations
11
+ export function fix_parent_child_relations<T extends mrow>(rows: T[]): T[] {
12
+ if (!Array.isArray(rows)) rows = [];
13
+ const anyparentid = rows.find((r) => getParentId(r));
14
+ const anyselfid = rows.find((r) => getSelfId(r));
15
+ if (!anyparentid || !anyselfid) return rows;
16
+
17
+ const converted_rows: converted_row[] = rows
18
+ .filter((r) => r != null)
19
+ .map((r) => {
20
+ return { parent: getParentId(r), self: getSelfId(r), data: r };
21
+ });
22
+
23
+ const top_level_rows: converted_row[] = converted_rows.filter((r) => !r.parent);
24
+ //group rows by parent id once, instead of scanning all rows for every parent
25
+ const children_by_parent = new Map<string, converted_row[]>();
26
+ for (let index = 0; index < converted_rows.length; index++) {
27
+ const r = converted_rows[index];
28
+ if (!r.parent) continue;
29
+ const key = String(r.parent);
30
+ const siblings = children_by_parent.get(key);
31
+ if (siblings) siblings.push(r);
32
+ else children_by_parent.set(key, [r]);
33
+ }
34
+ if (children_by_parent.size > 0) {
35
+ //find children of top-level parents:
36
+ for (let index = 0; index < top_level_rows.length; index++) {
37
+ const mrow = top_level_rows[index];
38
+ assign_children(mrow, children_by_parent);
39
+ }
40
+ }
41
+ return top_level_rows.map((r) => r.data) as T[];
42
+ }
43
+
44
+ function assign_children(row: converted_row, children_by_parent: Map<string, converted_row[]>) {
45
+ const my_children = row.self == null ? undefined : children_by_parent.get(String(row.self));
46
+ if (my_children && my_children.length > 0) {
47
+ //find children of my children:
48
+ for (let index = 0; index < my_children.length; index++) {
49
+ const child = my_children[index];
50
+ assign_children(child, children_by_parent);
51
+ }
52
+ row.data.__children = my_children.map((r) => r.data);
53
+ } else if (!Array.isArray(row.data.__children) || row.data.__children.length == 0)
54
+ row.data.__children = undefined;
55
+ }
56
+ export class func_selection {
57
+ static getVirtualElement(x = 0, y = 0) {
58
+ const boundsRect = func_selection.generateGetBoundingClientRect(x, y);
59
+ return {
60
+ getBoundingClientRect: boundsRect,
61
+ };
62
+ }
63
+ static generateGetBoundingClientRect(x = 0, y = 0) {
64
+ return () => ({
65
+ width: 0,
66
+ height: 0,
67
+ top: y,
68
+ right: x,
69
+ bottom: y,
70
+ left: x,
71
+ });
72
+ }
73
+ static get_selectedText(): string {
74
+ var text: any = '';
75
+ if (window.getSelection) {
76
+ text = window.getSelection()?.toString();
77
+ } else if (
78
+ (document as any).selection &&
79
+ (document as any).selection.type != 'Control'
80
+ ) {
81
+ text = (document as any).selection.createRange()?.text;
82
+ }
83
+ return text || '';
84
+ }
85
+ static has_selection(): boolean {
86
+ const slct = window.getSelection();
87
+ if (slct == null) return false;
88
+ if (slct.type == null) return func_selection.get_selectedText().length !== 0;
89
+ if (slct.type === 'Range') return true;
90
+ return false;
91
+ }
92
+ }
@@ -17,12 +17,24 @@ const defaultColumnOptions: ColumnDefinition = {
17
17
  editable: false,
18
18
  };
19
19
 
20
- export async function ApplyDataSource(datasource, props) {
20
+ export async function ApplyDataSource(
21
+ datasource,
22
+ props,
23
+ remote: { remoteFilter?: boolean; remoteSort?: boolean } = {}
24
+ ) {
21
25
  let result_data: any = undefined;
22
26
 
23
27
  let isUsingCache = false;
24
28
 
25
- const cacheInfo = { ds: datasource, skip: props.skip, limit: props.limit };
29
+ //filter/sort are only part of the key when they are sent to a remote dataSource,
30
+ //local filtering/sorting keeps reusing the cached rows
31
+ const cacheInfo = {
32
+ ds: datasource,
33
+ skip: props.skip,
34
+ limit: props.limit,
35
+ filterValue: remote.remoteFilter ? props.filterValue : undefined,
36
+ sortInfo: remote.remoteSort ? props.sortInfo : undefined,
37
+ };
26
38
  //check if we can used cached data
27
39
  //if the datasource is changed since last render, we should invalidate cache
28
40
  if (!validateCache(props.last_cache_info.current, cacheInfo, props.looseCacheCheck)) {
@@ -289,7 +301,8 @@ export function get_default_key() {
289
301
 
290
302
  //check if two cache objects are the same
291
303
  export function validateCache(current: any, old: any, looseCheck = false) {
292
- if (!old || !object_equals(current, old)) return false;
304
+ //deep compare skip/limit/filterValue/sortInfo, array order matters (sort priority)
305
+ if (!old || !object_equals(current, old, { strictArrayCheck: true })) return false;
293
306
 
294
307
  if (looseCheck) return true;
295
308
 
@@ -1,34 +1,28 @@
1
- import React from 'react';
2
- import {
3
- AzGridProps,
4
- DataGridWrapperProps,
5
- GridType,
6
- LooseColumnDefinition,
7
- } from '../types/types';
8
- import { applyColumnData, find_realData, get_row_id } from '../func/func_grid';
9
- import { func_selection } from '../func/func_child';
10
-
11
- export function useColumnData(
12
- inputOrProps: undefined | any[] | LooseColumnDefinition[] | AzGridProps
13
- ) {
14
- if (inputOrProps == null) return [];
15
- if (Array.isArray(inputOrProps)) return useThisColumnData(inputOrProps);
16
- return usePropsColumnData(inputOrProps);
17
- }
18
- function useThisColumnData(cols: any[]) {
19
- return React.useMemo(() => {
20
- return applyColumnData(cols);
21
- }, [cols]);
22
- }
23
- function usePropsColumnData(props: AzGridProps) {
24
- return React.useMemo(() => {
25
- return applyColumnData(props);
26
- }, [
27
- props.columns,
28
- props.leftColumns,
29
- props.rightColumns,
30
- props.editableColumns,
31
- props.columnExtensions,
32
- props.customColumns,
33
- ]);
34
- }
1
+ import React from 'react';
2
+ import { AzGridProps, LooseColumnDefinition } from '../types/types';
3
+ import { applyColumnData } from '../func/func_grid';
4
+
5
+ const EMPTY_COLUMNS: any[] = [];
6
+
7
+ export function useColumnData(
8
+ inputOrProps: undefined | any[] | LooseColumnDefinition[] | AzGridProps
9
+ ) {
10
+ // one useMemo for every input kind, so switching between them keeps the hook order
11
+ const isProps = inputOrProps != null && !Array.isArray(inputOrProps);
12
+ const props = (isProps ? inputOrProps : {}) as AzGridProps;
13
+ const deps = isProps
14
+ ? [
15
+ props.columns,
16
+ props.leftColumns,
17
+ props.rightColumns,
18
+ props.editableColumns,
19
+ props.columnExtensions,
20
+ props.customColumns,
21
+ ]
22
+ : [inputOrProps, null, null, null, null, null];
23
+
24
+ return React.useMemo(() => {
25
+ if (inputOrProps == null) return EMPTY_COLUMNS;
26
+ return applyColumnData(inputOrProps);
27
+ }, [isProps, ...deps]);
28
+ }
@@ -1,25 +1,32 @@
1
- import React from 'react';
2
- import { AzGridProps, DataGridWrapperProps, GridType } from '../types/types';
3
- import {
4
- applyColumnData,
5
- ApplyDataSource,
6
- find_realData,
7
- get_row_id,
8
- } from '../func/func_grid';
9
- import { func_selection } from '../func/func_child';
10
-
11
- export function useDataSource(ds: any, props: AzGridProps) {
12
- const cached_data = React.useRef<any>();
13
- const last_cache_info = React.useRef<any>();
14
- return React.useCallback(
15
- async (dt) => {
16
- return await ApplyDataSource(ds, {
17
- skip: dt?.skip,
18
- limit: dt?.limit,
19
- cached_data: cached_data,
20
- last_cache_info: last_cache_info,
21
- });
22
- },
23
- [ds]
24
- );
25
- }
1
+ import React from 'react';
2
+ import { AzDataSourceParams, AzGridProps, DataGridWrapperProps, GridType } from '../types/types';
3
+ import {
4
+ applyColumnData,
5
+ ApplyDataSource,
6
+ find_realData,
7
+ get_row_id,
8
+ } from '../func/func_grid';
9
+ import { func_selection } from '../func/func_child';
10
+
11
+ export function useDataSource(ds: any, props: AzGridProps) {
12
+ const cached_data = React.useRef<any>();
13
+ const last_cache_info = React.useRef<any>();
14
+ // filter/sort only change the result when the grid sends them to the dataSource
15
+ const remoteFilter = !!props?.gridOptions?.remoteFilter;
16
+ const remoteSort = !!props?.gridOptions?.remoteSort;
17
+ return React.useCallback(
18
+ async (dt) => {
19
+ const params: AzDataSourceParams = {
20
+ skip: dt?.skip,
21
+ limit: dt?.limit,
22
+ filterValue: dt?.filterValue,
23
+ sortInfo: dt?.sortInfo,
24
+ groupBy: dt?.groupBy,
25
+ cached_data: cached_data,
26
+ last_cache_info: last_cache_info,
27
+ };
28
+ return await ApplyDataSource(ds, params, { remoteFilter, remoteSort });
29
+ },
30
+ [ds, remoteFilter, remoteSort]
31
+ );
32
+ }
@@ -7,6 +7,7 @@ import {
7
7
  apply_row_a11y,
8
8
  get_grid_root,
9
9
  get_root_a11y_key,
10
+ is_grid_busy,
10
11
  } from '../func/func_a11y';
11
12
 
12
13
  function useGridMenuRow() {
@@ -20,6 +21,9 @@ function useGridMenuRow() {
20
21
  }
21
22
  export function useGridEvents(grid: GridType, props: DataGridWrapperProps) {
22
23
  const click_data = React.useRef<any>(null);
24
+ // latest props, so the callbacks below stay stable and don't re-render the grid
25
+ const latestProps = React.useRef(props);
26
+ latestProps.current = props;
23
27
  const { update_menuRow, menuAnchorEl, menuRow } = useGridMenuRow();
24
28
 
25
29
  const refreshColumn = React.useCallback(
@@ -46,26 +50,21 @@ export function useGridEvents(grid: GridType, props: DataGridWrapperProps) {
46
50
  /* -------------------------------------------------------------------------- */
47
51
  /* events */
48
52
  /* -------------------------------------------------------------------------- */
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
- }
53
+ const onRowClick = React.useCallback((rowProps, event) => {
54
+ const { onRowClick, onRowDoubleClick } = latestProps.current;
55
+ const nowDate = Date.now();
56
+ const lastclick: number = click_data.current[get_row_id(rowProps)];
57
+ if (lastclick) {
58
+ const diffTime = Math.abs(nowDate - lastclick);
59
+ if (diffTime < 400) {
60
+ click_data.current[get_row_id(rowProps)] = null;
61
+ return onRowDoubleClick && onRowDoubleClick(rowProps.rowIndex, rowProps.data);
62
62
  }
63
+ }
63
64
 
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
- );
65
+ click_data.current[get_row_id(rowProps)] = nowDate;
66
+ return onRowClick && onRowClick(rowProps.rowIndex, rowProps.data);
67
+ }, []);
69
68
 
70
69
  React.useEffect(() => {
71
70
  click_data.current = {};
@@ -87,28 +86,39 @@ export function useGridEvents(grid: GridType, props: DataGridWrapperProps) {
87
86
 
88
87
  /* -------------------------------------------------------------------------- */
89
88
  /* ------------------------------ Accessibility ----------------------------- */
90
- const latestProps = React.useRef(props);
91
- latestProps.current = props;
92
89
  const rootSyncFrame = React.useRef(0);
93
90
  // 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: '' });
91
+ const lastRoot = React.useRef<{ el: Element | null; key: string; busy: boolean }>({
92
+ el: null,
93
+ key: '',
94
+ busy: false,
95
+ });
95
96
  const getRootOptions = () => ({
96
97
  ariaLabel: latestProps.current.ariaLabel,
97
98
  editable: !!latestProps.current.editable,
98
99
  });
99
100
  // root attributes are set on the DOM node, batched to once per frame
100
- const scheduleRootSync = React.useCallback(() => {
101
+ const scheduleRootSync = React.useCallback((force = false) => {
101
102
  if (rootSyncFrame.current || typeof requestAnimationFrame === 'undefined') return;
102
103
  const grid = latestProps.current.liveGridRef?.current?.current;
103
104
  const key = get_root_a11y_key(grid, getRootOptions());
104
- if (key && key === lastRoot.current.key && get_grid_root(grid) === lastRoot.current.el)
105
+ if (
106
+ !force &&
107
+ key &&
108
+ key === lastRoot.current.key &&
109
+ get_grid_root(grid) === lastRoot.current.el
110
+ )
105
111
  return;
106
112
  rootSyncFrame.current = requestAnimationFrame(() => {
107
113
  rootSyncFrame.current = 0;
108
114
  const liveGrid = latestProps.current.liveGridRef?.current?.current;
109
115
  const options = getRootOptions();
110
116
  const el = apply_root_a11y(liveGrid, options);
111
- lastRoot.current = { el, key: el ? get_root_a11y_key(liveGrid, options) : '' };
117
+ lastRoot.current = {
118
+ el,
119
+ key: el ? get_root_a11y_key(liveGrid, options) : '',
120
+ busy: el ? is_grid_busy(liveGrid) : false,
121
+ };
112
122
  });
113
123
  }, []);
114
124
  React.useEffect(() => {
@@ -130,8 +140,14 @@ export function useGridEvents(grid: GridType, props: DataGridWrapperProps) {
130
140
  scheduleRootSync();
131
141
  }, []);
132
142
 
143
+ // called by the grid on every render with the load mask state, keeps aria-busy in sync
144
+ const onRenderLoadMask = React.useCallback((maskProps: { visible?: boolean }) => {
145
+ if (!!maskProps?.visible !== lastRoot.current.busy) scheduleRootSync(true);
146
+ }, []);
147
+
133
148
  return {
134
149
  onRowClick,
150
+ onRenderLoadMask,
135
151
  onRowContextMenu,
136
152
  onRenderRow,
137
153
  update_menuRow,