@azrico/reactgrid 1.6.4 → 1.6.5
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/CHANGELOG.md +4 -0
- package/__tests__/context-menu.config.cjs +14 -0
- package/__tests__/context-menu.test.js +150 -0
- package/dist/src/hooks/useGridEvents.js +14 -2
- package/dist/src/views/DataGrid.js +1 -1
- package/package.json +1 -1
- package/src/hooks/useGridEvents.ts +14 -3
- package/src/views/DataGrid.tsx +10 -8
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,10 @@ All notable changes to this project will be documented in this file. See [standa
|
|
|
12
12
|
* Fix the row context menu ignoring the grid theme with `@azrico/controls` 1.6.2, whose menu is no longer MUI-based and is portaled to `<body>`. The menu now gets the grid's theme class (`azgrid-theme-<name>`) and is styled through the controls' `--azrico-contextmenu-*` variables from the `--azgrid-menu-*` tokens. The MUI `ThemeProvider` around the menu is removed.
|
|
13
13
|
* Add `contextMenuClassName` to add your own classes to the context menu. Use it to bring host variables set on a wrapper element, which the portaled menu no longer inherits.
|
|
14
14
|
|
|
15
|
+
### 1.6.5 (2026-10-15)
|
|
16
|
+
|
|
17
|
+
* Leave the browser's context menu available when a row has no resolved `contextMenuItems`, and close any previously open grid menu. Respect right-clicks already handled by nested row details.
|
|
18
|
+
|
|
15
19
|
### 1.7.1 (2026-10-14)
|
|
16
20
|
|
|
17
21
|
* Fix two quick clicks on different rows firing `onRowDoubleClick`: the double-click check now keys clicks by the row's id instead of `undefined`.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
rootDir: '..',
|
|
3
|
+
cacheDirectory: '<rootDir>/node_modules/.cache/jest-context-menu',
|
|
4
|
+
testMatch: ['<rootDir>/__tests__/context-menu.test.js'],
|
|
5
|
+
moduleNameMapper: {
|
|
6
|
+
'\\.(css|less|scss|sass)$': '<rootDir>/config/file-mock.js',
|
|
7
|
+
'^@azrico/controls/context-menu$': '<rootDir>/node_modules/@azrico/controls/dist/src/ContextMenu/index.js',
|
|
8
|
+
},
|
|
9
|
+
transform: {
|
|
10
|
+
'^.+\\.[jt]sx?$': ['babel-jest', {
|
|
11
|
+
presets: ['@babel/preset-env', '@babel/preset-typescript', '@babel/preset-react'],
|
|
12
|
+
}],
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
const React = require('react');
|
|
3
|
+
const { createRoot } = require('react-dom/client');
|
|
4
|
+
const act = React.act || require('react-dom/test-utils').act;
|
|
5
|
+
const { ContextMenu } = require('@azrico/controls/context-menu');
|
|
6
|
+
const { AzGrid } = require('../src/views/DataGrid');
|
|
7
|
+
|
|
8
|
+
let mockGridProps;
|
|
9
|
+
jest.mock('@inovua/reactdatagrid-enterprise', () => {
|
|
10
|
+
const React = require('react');
|
|
11
|
+
class Grid extends React.Component {
|
|
12
|
+
render() {
|
|
13
|
+
mockGridProps = this.props;
|
|
14
|
+
return React.createElement('div', { 'data-row-id': 'real-row' },
|
|
15
|
+
React.createElement('span', { id: 'cell' }, 'Cell'));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
Grid.defaultProps = {};
|
|
19
|
+
return { __esModule: true, default: Grid };
|
|
20
|
+
});
|
|
21
|
+
jest.mock('@azrico/controls/context-menu', () => {
|
|
22
|
+
const actual = jest.requireActual('@azrico/controls/context-menu');
|
|
23
|
+
return { ...actual, ContextMenu: jest.fn(actual.ContextMenu) };
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const row = { __table_id: 'real-row', name: 'Row' };
|
|
27
|
+
const staleRow = { __table_id: 'stale-row' };
|
|
28
|
+
const rowProps = { data: staleRow, dataSourceArray: [row] };
|
|
29
|
+
let container, root;
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
global.IS_REACT_ACT_ENVIRONMENT = true;
|
|
33
|
+
container = document.createElement('div');
|
|
34
|
+
document.body.appendChild(container);
|
|
35
|
+
root = createRoot(container);
|
|
36
|
+
ContextMenu.mockClear();
|
|
37
|
+
});
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
act(() => root.unmount());
|
|
40
|
+
container.remove();
|
|
41
|
+
jest.restoreAllMocks();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
function render(contextMenuItems) {
|
|
45
|
+
act(() => root.render(React.createElement(AzGrid, {
|
|
46
|
+
columns: [], dataSource: [row], contextMenuItems,
|
|
47
|
+
})));
|
|
48
|
+
}
|
|
49
|
+
function rightClick(data = rowProps, alreadyHandled = false) {
|
|
50
|
+
const event = new MouseEvent('contextmenu', { cancelable: true, clientX: 120, clientY: 80 });
|
|
51
|
+
document.getElementById('cell').dispatchEvent(event);
|
|
52
|
+
if (alreadyHandled) event.preventDefault();
|
|
53
|
+
act(() => mockGridProps.onRowContextMenu(data, event));
|
|
54
|
+
return event;
|
|
55
|
+
}
|
|
56
|
+
function popup() {
|
|
57
|
+
// Controls 1.6.1 uses the first class; newer controls use the second.
|
|
58
|
+
return document.querySelector('.azrico-controls-popup, .azrico-controls-contextmenuPopup');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
test('omits ContextMenu entirely when there is no prop and leaves the native menu available', () => {
|
|
62
|
+
render();
|
|
63
|
+
expect(rightClick().defaultPrevented).toBe(false);
|
|
64
|
+
expect(ContextMenu).not.toHaveBeenCalled();
|
|
65
|
+
expect(popup()).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test.each([
|
|
69
|
+
['empty array', []],
|
|
70
|
+
['empty row items', () => []],
|
|
71
|
+
['null row items', () => null],
|
|
72
|
+
['entries resolving to nothing', () => [null, () => null, () => undefined, false, 7]],
|
|
73
|
+
])('%s leaves the native menu available without a popup', (_, items) => {
|
|
74
|
+
render(items);
|
|
75
|
+
expect(rightClick().defaultPrevented).toBe(false);
|
|
76
|
+
expect(popup()).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('resolves the real row before deciding whether to open and closes the previous menu for an empty row', () => {
|
|
80
|
+
const items = jest.fn((data) => data === row ? ['Open'] : []);
|
|
81
|
+
render(items);
|
|
82
|
+
expect(rightClick().defaultPrevented).toBe(true);
|
|
83
|
+
expect(items).toHaveBeenCalledWith(row);
|
|
84
|
+
expect(popup()).not.toBeNull();
|
|
85
|
+
expect(rightClick({ data: staleRow }).defaultPrevented).toBe(false);
|
|
86
|
+
expect(popup()).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('reads updated props through the same memoised handler', () => {
|
|
90
|
+
render(['Open']);
|
|
91
|
+
const handler = mockGridProps.onRowContextMenu;
|
|
92
|
+
rightClick();
|
|
93
|
+
render(() => null);
|
|
94
|
+
expect(mockGridProps.onRowContextMenu).toBe(handler);
|
|
95
|
+
expect(rightClick().defaultPrevented).toBe(false);
|
|
96
|
+
expect(popup()).toBeNull();
|
|
97
|
+
render(['Updated']);
|
|
98
|
+
expect(rightClick().defaultPrevented).toBe(true);
|
|
99
|
+
expect(popup().textContent).toContain('Updated');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test.each([
|
|
103
|
+
['string', 'Open'],
|
|
104
|
+
['object', { title: 'Open' }],
|
|
105
|
+
['entry function', [null, (data) => data === row ? 'Open' : null]],
|
|
106
|
+
])('opens a %s item at the cursor', (_, items) => {
|
|
107
|
+
render(items);
|
|
108
|
+
expect(rightClick().defaultPrevented).toBe(true);
|
|
109
|
+
expect(popup().textContent).toContain('Open');
|
|
110
|
+
const props = ContextMenu.mock.calls.slice(-1)[0][0];
|
|
111
|
+
expect(props.menuRow).toBe(row);
|
|
112
|
+
expect(props.menuAnchorEl.getBoundingClientRect()).toEqual({
|
|
113
|
+
width: 0, height: 0, top: 80, bottom: 80, left: 120, right: 120,
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('passes the real row to onClick and closes the menu', () => {
|
|
118
|
+
const onClick = jest.fn();
|
|
119
|
+
render([{ title: 'Open', onClick }]);
|
|
120
|
+
rightClick();
|
|
121
|
+
act(() => document.querySelector('[role="menuitem"]').click());
|
|
122
|
+
expect(onClick).toHaveBeenCalledWith(row, undefined);
|
|
123
|
+
expect(popup()).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('clicking outside closes the menu', async () => {
|
|
127
|
+
render(['Open']);
|
|
128
|
+
rightClick();
|
|
129
|
+
await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
|
|
130
|
+
act(() => document.body.click());
|
|
131
|
+
expect(popup()).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('does not open or evaluate grid items for a right-click already handled by row details', () => {
|
|
135
|
+
const items = jest.fn(() => ['Open']);
|
|
136
|
+
render(items);
|
|
137
|
+
rightClick();
|
|
138
|
+
items.mockClear();
|
|
139
|
+
rightClick(rowProps, true);
|
|
140
|
+
expect(items).not.toHaveBeenCalled();
|
|
141
|
+
expect(popup()).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('preserves the text-selection bailout and clears an open menu', () => {
|
|
145
|
+
render(['Open']);
|
|
146
|
+
rightClick();
|
|
147
|
+
jest.spyOn(window, 'getSelection').mockReturnValue({ type: 'Range' });
|
|
148
|
+
expect(rightClick().defaultPrevented).toBe(false);
|
|
149
|
+
expect(popup()).toBeNull();
|
|
150
|
+
});
|
|
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.useGridEvents = useGridEvents;
|
|
7
7
|
var _react = _interopRequireDefault(require("react"));
|
|
8
|
+
var _object = require("@azrico/object");
|
|
8
9
|
var _func_grid = require("../func/func_grid");
|
|
9
10
|
var _func_child = require("../func/func_child");
|
|
10
11
|
var _func_a11y = require("../func/func_a11y");
|
|
@@ -77,12 +78,23 @@ function useGridEvents(grid, props) {
|
|
|
77
78
|
}, [props.dataSource]);
|
|
78
79
|
/* ------------------------------- ContextMenu ------------------------------ */
|
|
79
80
|
const onRowContextMenu = _react.default.useCallback((rowProps, event) => {
|
|
80
|
-
if (!rowProps || !rowProps.data || _func_child.func_selection.has_selection()) {
|
|
81
|
+
if (event.defaultPrevented || !rowProps || !rowProps.data || _func_child.func_selection.has_selection()) {
|
|
81
82
|
update_menuRow(null, null);
|
|
82
83
|
return true;
|
|
83
84
|
}
|
|
84
|
-
event.preventDefault();
|
|
85
85
|
const realRow = (0, _func_grid.find_realData)(rowProps.dataSourceArray, event.target, rowProps.data);
|
|
86
|
+
const items = (0, _object.fn_eval)(latestProps.current.contextMenuItems, realRow) || [];
|
|
87
|
+
const resolvedItems = (Array.isArray(items) ? items : [items]).map(item => {
|
|
88
|
+
const value = (0, _object.fn_eval)(item, realRow);
|
|
89
|
+
return typeof value === 'string' ? {
|
|
90
|
+
title: value
|
|
91
|
+
} : value;
|
|
92
|
+
}).filter(item => item != null && typeof item === 'object');
|
|
93
|
+
if (!resolvedItems.length) {
|
|
94
|
+
update_menuRow(null, null);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
event.preventDefault();
|
|
86
98
|
const anEl = _func_child.func_selection.getVirtualElement(event.clientX, event.clientY);
|
|
87
99
|
update_menuRow(realRow, anEl);
|
|
88
100
|
return false;
|
|
@@ -185,7 +185,7 @@ function DataGridEventsWrapper(props) {
|
|
|
185
185
|
// The menu is portaled to <body>, so it carries the theme class itself to get the theme tokens.
|
|
186
186
|
return _react.default.createElement("div", {
|
|
187
187
|
className: (0, _clsx.default)('azgrid-scope azgrid-flexed azgrid-flexlayout', themeClass)
|
|
188
|
-
}, _react.default.createElement(_contextMenu.ContextMenu, {
|
|
188
|
+
}, props.contextMenuItems && _react.default.createElement(_contextMenu.ContextMenu, {
|
|
189
189
|
classname: (0, _clsx.default)('azgrid-context-menu', themeClass, props.contextMenuClassName),
|
|
190
190
|
menuAnchorEl: menuAnchorEl,
|
|
191
191
|
menuRow: menuRow,
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { fn_eval } from '@azrico/object';
|
|
2
3
|
import { DataGridWrapperProps, GridType } from '../types/types';
|
|
3
4
|
import { find_realData, get_row_id } from '../func/func_grid';
|
|
4
5
|
import { func_selection } from '../func/func_child';
|
|
@@ -74,13 +75,23 @@ export function useGridEvents(grid: GridType, props: DataGridWrapperProps) {
|
|
|
74
75
|
|
|
75
76
|
/* ------------------------------- ContextMenu ------------------------------ */
|
|
76
77
|
const onRowContextMenu = React.useCallback((rowProps, event) => {
|
|
77
|
-
if (!rowProps || !rowProps.data || func_selection.has_selection()) {
|
|
78
|
+
if (event.defaultPrevented || !rowProps || !rowProps.data || func_selection.has_selection()) {
|
|
78
79
|
update_menuRow(null, null);
|
|
79
80
|
return true;
|
|
80
81
|
}
|
|
81
|
-
event.preventDefault();
|
|
82
|
-
|
|
83
82
|
const realRow = find_realData(rowProps.dataSourceArray, event.target, rowProps.data);
|
|
83
|
+
const items = fn_eval(latestProps.current.contextMenuItems, realRow) || [];
|
|
84
|
+
const resolvedItems = (Array.isArray(items) ? items : [items])
|
|
85
|
+
.map((item) => {
|
|
86
|
+
const value = fn_eval(item, realRow);
|
|
87
|
+
return typeof value === 'string' ? { title: value } : value;
|
|
88
|
+
})
|
|
89
|
+
.filter((item) => item != null && typeof item === 'object');
|
|
90
|
+
if (!resolvedItems.length) {
|
|
91
|
+
update_menuRow(null, null);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
event.preventDefault();
|
|
84
95
|
const anEl = func_selection.getVirtualElement(event.clientX, event.clientY);
|
|
85
96
|
update_menuRow(realRow, anEl);
|
|
86
97
|
return false;
|
package/src/views/DataGrid.tsx
CHANGED
|
@@ -224,14 +224,16 @@ function DataGridEventsWrapper(props: DataGridWrapperProps) {
|
|
|
224
224
|
// The menu is portaled to <body>, so it carries the theme class itself to get the theme tokens.
|
|
225
225
|
return (
|
|
226
226
|
<div className={clsx('azgrid-scope azgrid-flexed azgrid-flexlayout', themeClass)}>
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
227
|
+
{props.contextMenuItems && (
|
|
228
|
+
<ContextMenu
|
|
229
|
+
classname={clsx('azgrid-context-menu', themeClass, props.contextMenuClassName)}
|
|
230
|
+
menuAnchorEl={menuAnchorEl}
|
|
231
|
+
menuRow={menuRow}
|
|
232
|
+
close={update_menuRow}
|
|
233
|
+
dontAutoClose={false}
|
|
234
|
+
menu_items={props.contextMenuItems as any}
|
|
235
|
+
/>
|
|
236
|
+
)}
|
|
235
237
|
{props.details_dataSource ? (
|
|
236
238
|
<DataGridDetailsWrapper {...props} static_gridOptions={static_gridOptions} />
|
|
237
239
|
) : (
|