@griddo/ax 12.1.0 → 12.2.0-rc.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.
- package/package.json +2 -2
- package/src/__tests__/components/ContextMenu/ContextMenu.test.tsx +381 -0
- package/src/__tests__/components/FloatingMenu/FloatingMenu.test.tsx +3 -3
- package/src/__tests__/components/FloatingPanel/FloatingPanel.test.tsx +2 -2
- package/src/__tests__/components/SideModal/SideModal.test.tsx +2 -2
- package/src/__tests__/hooks/modals.test.tsx +86 -1
- package/src/components/ActionMenu/index.tsx +26 -11
- package/src/components/ActionMenu/style.tsx +15 -8
- package/src/components/ContextMenu/index.tsx +102 -0
- package/src/components/ContextMenu/style.tsx +15 -0
- package/src/components/Fields/ComponentContainer/index.tsx +9 -2
- package/src/components/FloatingMenu/index.tsx +1 -1
- package/src/components/FloatingMenu/style.tsx +1 -1
- package/src/components/index.tsx +4 -1
- package/src/hooks/index.tsx +2 -1
- package/src/hooks/modals.tsx +51 -4
- package/src/modules/Analytics/DimensionItem/index.tsx +5 -3
- package/src/modules/Analytics/GroupItem/index.tsx +5 -3
- package/src/modules/Categories/CategoriesList/CategoryItem/index.tsx +5 -2
- package/src/modules/Content/PageItem/index.tsx +15 -3
- package/src/modules/FileDrive/FolderItem/index.tsx +5 -3
- package/src/modules/FileDrive/GridItem/index.tsx +5 -3
- package/src/modules/FileDrive/ListItem/index.tsx +5 -3
- package/src/modules/Forms/FormCategoriesList/CategoryItem/index.tsx +5 -2
- package/src/modules/Forms/FormList/FormItem/index.tsx +5 -2
- package/src/modules/MediaGallery/FolderItem/index.tsx +5 -3
- package/src/modules/MediaGallery/GridItem/index.tsx +5 -3
- package/src/modules/MediaGallery/ListItem/index.tsx +5 -3
- package/src/modules/Navigation/Defaults/Item/index.tsx +5 -3
- package/src/modules/Navigation/Menus/List/Table/Item/index.tsx +5 -2
- package/src/modules/Redirects/RedirectItem/index.tsx +5 -3
- package/src/modules/Settings/Integrations/IntegrationForm/VariableItem/index.tsx +5 -3
- package/src/modules/Settings/Integrations/IntegrationItem/CopyModal/index.tsx +1 -0
- package/src/modules/Settings/Integrations/IntegrationItem/index.tsx +5 -3
- package/src/modules/Settings/Languages/Table/Item/index.tsx +5 -3
- package/src/modules/Sites/SitesList/GridView/GridSiteItem/index.tsx +5 -2
- package/src/modules/Sites/SitesList/ListView/ListSiteItem/index.tsx +5 -2
- package/src/modules/StructuredData/StructuredDataList/GlobalPageItem/index.tsx +5 -2
- package/src/modules/StructuredData/StructuredDataList/StructuredDataItem/index.tsx +5 -3
- package/src/modules/Users/UserList/UserItem/index.tsx +5 -3
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@griddo/ax",
|
|
3
3
|
"description": "Griddo Author Experience",
|
|
4
|
-
"version": "12.
|
|
4
|
+
"version": "12.2.0-rc.0",
|
|
5
5
|
"authors": [
|
|
6
6
|
"Álvaro Sánchez' <alvaro.sanches@secuoyas.com>",
|
|
7
7
|
"Diego M. Béjar <diego.bejar@secuoyas.com>",
|
|
@@ -219,5 +219,5 @@
|
|
|
219
219
|
"publishConfig": {
|
|
220
220
|
"access": "public"
|
|
221
221
|
},
|
|
222
|
-
"gitHead": "
|
|
222
|
+
"gitHead": "f61b757318f36a2d86e9c13df08d7c9ac654b0c7"
|
|
223
223
|
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { ThemeProvider } from "styled-components";
|
|
3
|
+
|
|
4
|
+
import "@testing-library/jest-dom";
|
|
5
|
+
|
|
6
|
+
import ContextMenu from "@ax/components/ContextMenu";
|
|
7
|
+
import { parseTheme } from "@ax/helpers";
|
|
8
|
+
import globalTheme from "@ax/themes/theme.json";
|
|
9
|
+
import type { IActionMenuOption } from "@ax/types";
|
|
10
|
+
|
|
11
|
+
afterEach(cleanup);
|
|
12
|
+
|
|
13
|
+
describe("ContextMenu component rendering", () => {
|
|
14
|
+
it("should return null when position is null", () => {
|
|
15
|
+
const props = {
|
|
16
|
+
position: null,
|
|
17
|
+
options: [],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const { container } = render(
|
|
21
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
22
|
+
<ContextMenu {...props} />
|
|
23
|
+
</ThemeProvider>,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
expect(container.firstChild).toBe(null);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should render menu container when position is provided", () => {
|
|
30
|
+
const options: IActionMenuOption[] = [{ label: "Edit", action: jest.fn(), icon: "edit" }];
|
|
31
|
+
|
|
32
|
+
const props = {
|
|
33
|
+
position: { x: 100, y: 200 },
|
|
34
|
+
options,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
render(
|
|
38
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
39
|
+
<ContextMenu {...props} />
|
|
40
|
+
</ThemeProvider>,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const container = screen.getByText("Edit").closest("div");
|
|
44
|
+
expect(container).toBeTruthy();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should render all provided options", () => {
|
|
48
|
+
const options: IActionMenuOption[] = [
|
|
49
|
+
{ label: "Edit", action: jest.fn(), icon: "edit" },
|
|
50
|
+
{ label: "Delete", action: jest.fn(), icon: "delete" },
|
|
51
|
+
{ label: "Duplicate", action: jest.fn(), icon: "duplicate" },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const props = {
|
|
55
|
+
position: { x: 100, y: 200 },
|
|
56
|
+
options,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
render(
|
|
60
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
61
|
+
<ContextMenu {...props} />
|
|
62
|
+
</ThemeProvider>,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(screen.getByText("Edit")).toBeInTheDocument();
|
|
66
|
+
expect(screen.getByText("Delete")).toBeInTheDocument();
|
|
67
|
+
expect(screen.getByText("Duplicate")).toBeInTheDocument();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should handle null and undefined options in array", () => {
|
|
71
|
+
const options: (IActionMenuOption | undefined | null)[] = [
|
|
72
|
+
{ label: "Edit", action: jest.fn(), icon: "edit" },
|
|
73
|
+
null,
|
|
74
|
+
undefined,
|
|
75
|
+
{ label: "Delete", action: jest.fn(), icon: "delete" },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const props = {
|
|
79
|
+
position: { x: 100, y: 200 },
|
|
80
|
+
options,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
render(
|
|
84
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
85
|
+
<ContextMenu {...props} />
|
|
86
|
+
</ThemeProvider>,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(screen.getByText("Edit")).toBeInTheDocument();
|
|
90
|
+
expect(screen.getByText("Delete")).toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("ContextMenu component positioning", () => {
|
|
95
|
+
it("should apply correct position coordinates", () => {
|
|
96
|
+
const options: IActionMenuOption[] = [{ label: "Edit", action: jest.fn(), icon: "edit" }];
|
|
97
|
+
|
|
98
|
+
const props = {
|
|
99
|
+
position: { x: 150, y: 250 },
|
|
100
|
+
options,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const { container } = render(
|
|
104
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
105
|
+
<ContextMenu {...props} />
|
|
106
|
+
</ThemeProvider>,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const menuContainer = container.querySelector("div");
|
|
110
|
+
// The menu should be positioned
|
|
111
|
+
expect(menuContainer).toHaveStyle("position: fixed");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should adjust position when menu would overflow viewport right edge", () => {
|
|
115
|
+
const options: IActionMenuOption[] = [{ label: "Edit", action: jest.fn(), icon: "edit" }];
|
|
116
|
+
|
|
117
|
+
// Position near right edge with large X coordinate
|
|
118
|
+
const props = {
|
|
119
|
+
position: { x: window.innerWidth - 50, y: 200 },
|
|
120
|
+
options,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
render(
|
|
124
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
125
|
+
<ContextMenu {...props} />
|
|
126
|
+
</ThemeProvider>,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Component should render without error
|
|
130
|
+
expect(screen.getByText("Edit")).toBeInTheDocument();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("should adjust position when menu would overflow viewport bottom edge", () => {
|
|
134
|
+
const options: IActionMenuOption[] = [{ label: "Edit", action: jest.fn(), icon: "edit" }];
|
|
135
|
+
|
|
136
|
+
// Position near bottom edge with large Y coordinate
|
|
137
|
+
const props = {
|
|
138
|
+
position: { x: 100, y: window.innerHeight - 50 },
|
|
139
|
+
options,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
render(
|
|
143
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
144
|
+
<ContextMenu {...props} />
|
|
145
|
+
</ThemeProvider>,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Component should render without error
|
|
149
|
+
expect(screen.getByText("Edit")).toBeInTheDocument();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("ContextMenu component events", () => {
|
|
154
|
+
it("should call action when option is clicked", () => {
|
|
155
|
+
const actionSpy = jest.fn();
|
|
156
|
+
const options: IActionMenuOption[] = [{ label: "Edit", action: actionSpy, icon: "edit" }];
|
|
157
|
+
|
|
158
|
+
const props = {
|
|
159
|
+
position: { x: 100, y: 200 },
|
|
160
|
+
options,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
render(
|
|
164
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
165
|
+
<ContextMenu {...props} />
|
|
166
|
+
</ThemeProvider>,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const editButton = screen.getByText("Edit");
|
|
170
|
+
fireEvent.click(editButton);
|
|
171
|
+
|
|
172
|
+
expect(actionSpy).toHaveBeenCalled();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("should close menu and call onClose when option is clicked", () => {
|
|
176
|
+
const actionSpy = jest.fn();
|
|
177
|
+
const onCloseSpy = jest.fn();
|
|
178
|
+
const options: IActionMenuOption[] = [{ label: "Edit", action: actionSpy, icon: "edit" }];
|
|
179
|
+
|
|
180
|
+
const props = {
|
|
181
|
+
position: { x: 100, y: 200 },
|
|
182
|
+
options,
|
|
183
|
+
onClose: onCloseSpy,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
render(
|
|
187
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
188
|
+
<ContextMenu {...props} />
|
|
189
|
+
</ThemeProvider>,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const editButton = screen.getByText("Edit");
|
|
193
|
+
fireEvent.click(editButton);
|
|
194
|
+
|
|
195
|
+
expect(actionSpy).toHaveBeenCalled();
|
|
196
|
+
expect(onCloseSpy).toHaveBeenCalled();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("should call onClose when clicking outside the menu", () => {
|
|
200
|
+
const onCloseSpy = jest.fn();
|
|
201
|
+
const options: IActionMenuOption[] = [{ label: "Edit", action: jest.fn(), icon: "edit" }];
|
|
202
|
+
|
|
203
|
+
const props = {
|
|
204
|
+
position: { x: 100, y: 200 },
|
|
205
|
+
options,
|
|
206
|
+
onClose: onCloseSpy,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
render(
|
|
210
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
211
|
+
<div data-testid="outside">Outside content</div>
|
|
212
|
+
<ContextMenu {...props} />
|
|
213
|
+
</ThemeProvider>,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const outsideElement = screen.getByTestId("outside");
|
|
217
|
+
fireEvent.click(outsideElement);
|
|
218
|
+
|
|
219
|
+
expect(onCloseSpy).toHaveBeenCalled();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("should not call onClose when clicking inside the menu", () => {
|
|
223
|
+
const onCloseSpy = jest.fn();
|
|
224
|
+
const options: IActionMenuOption[] = [{ label: "Edit", action: jest.fn(), icon: "edit" }];
|
|
225
|
+
|
|
226
|
+
const props = {
|
|
227
|
+
position: { x: 100, y: 200 },
|
|
228
|
+
options,
|
|
229
|
+
onClose: onCloseSpy,
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
render(
|
|
233
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
234
|
+
<ContextMenu {...props} />
|
|
235
|
+
</ThemeProvider>,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
const editButton = screen.getByText("Edit");
|
|
239
|
+
fireEvent.mouseDown(editButton);
|
|
240
|
+
|
|
241
|
+
// onClose should not be called for clicks inside menu
|
|
242
|
+
// (only triggered by action click handler which then calls onClose)
|
|
243
|
+
expect(onCloseSpy).not.toHaveBeenCalled();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("should handle multiple actions independently", () => {
|
|
247
|
+
const editSpy = jest.fn();
|
|
248
|
+
const deleteSpy = jest.fn();
|
|
249
|
+
const onCloseSpy = jest.fn();
|
|
250
|
+
const options: IActionMenuOption[] = [
|
|
251
|
+
{ label: "Edit", action: editSpy, icon: "edit" },
|
|
252
|
+
{ label: "Delete", action: deleteSpy, icon: "delete" },
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
const props = {
|
|
256
|
+
position: { x: 100, y: 200 },
|
|
257
|
+
options,
|
|
258
|
+
onClose: onCloseSpy,
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const { rerender } = render(
|
|
262
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
263
|
+
<ContextMenu {...props} />
|
|
264
|
+
</ThemeProvider>,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const editButton = screen.getByText("Edit");
|
|
268
|
+
fireEvent.click(editButton);
|
|
269
|
+
|
|
270
|
+
expect(editSpy).toHaveBeenCalledTimes(1);
|
|
271
|
+
expect(deleteSpy).not.toHaveBeenCalled();
|
|
272
|
+
expect(onCloseSpy).toHaveBeenCalledTimes(1);
|
|
273
|
+
|
|
274
|
+
// Reset spies and rerender with position to simulate reopening
|
|
275
|
+
onCloseSpy.mockClear();
|
|
276
|
+
rerender(
|
|
277
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
278
|
+
<ContextMenu {...props} />
|
|
279
|
+
</ThemeProvider>,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const deleteButton = screen.getByText("Delete");
|
|
283
|
+
fireEvent.click(deleteButton);
|
|
284
|
+
|
|
285
|
+
expect(deleteSpy).toHaveBeenCalledTimes(1);
|
|
286
|
+
expect(onCloseSpy).toHaveBeenCalled();
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe("ContextMenu component state transitions", () => {
|
|
291
|
+
it("should hide menu when position changes to null", () => {
|
|
292
|
+
const options: IActionMenuOption[] = [{ label: "Edit", action: jest.fn(), icon: "edit" }];
|
|
293
|
+
|
|
294
|
+
const { rerender, container } = render(
|
|
295
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
296
|
+
<ContextMenu position={{ x: 100, y: 200 }} options={options} />
|
|
297
|
+
</ThemeProvider>,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
expect(screen.getByText("Edit")).toBeInTheDocument();
|
|
301
|
+
|
|
302
|
+
rerender(
|
|
303
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
304
|
+
<ContextMenu position={null} options={options} />
|
|
305
|
+
</ThemeProvider>,
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
expect(container.firstChild).toBe(null);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("should reopen menu at new position", () => {
|
|
312
|
+
const options: IActionMenuOption[] = [{ label: "Edit", action: jest.fn(), icon: "edit" }];
|
|
313
|
+
|
|
314
|
+
const { rerender } = render(
|
|
315
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
316
|
+
<ContextMenu position={{ x: 100, y: 200 }} options={options} />
|
|
317
|
+
</ThemeProvider>,
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
expect(screen.getByText("Edit")).toBeInTheDocument();
|
|
321
|
+
|
|
322
|
+
rerender(
|
|
323
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
324
|
+
<ContextMenu position={null} options={options} />
|
|
325
|
+
</ThemeProvider>,
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
expect(screen.queryByText("Edit")).not.toBeInTheDocument();
|
|
329
|
+
|
|
330
|
+
rerender(
|
|
331
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
332
|
+
<ContextMenu position={{ x: 300, y: 400 }} options={options} />
|
|
333
|
+
</ThemeProvider>,
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
expect(screen.getByText("Edit")).toBeInTheDocument();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("should update options without closing menu", () => {
|
|
340
|
+
const { rerender } = render(
|
|
341
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
342
|
+
<ContextMenu position={{ x: 100, y: 200 }} options={[{ label: "Edit", action: jest.fn(), icon: "edit" }]} />
|
|
343
|
+
</ThemeProvider>,
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
expect(screen.getByText("Edit")).toBeInTheDocument();
|
|
347
|
+
|
|
348
|
+
rerender(
|
|
349
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
350
|
+
<ContextMenu
|
|
351
|
+
position={{ x: 100, y: 200 }}
|
|
352
|
+
options={[
|
|
353
|
+
{ label: "Edit", action: jest.fn(), icon: "edit" },
|
|
354
|
+
{ label: "Delete", action: jest.fn(), icon: "delete" },
|
|
355
|
+
]}
|
|
356
|
+
/>
|
|
357
|
+
</ThemeProvider>,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
expect(screen.getByText("Edit")).toBeInTheDocument();
|
|
361
|
+
expect(screen.getByText("Delete")).toBeInTheDocument();
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe("ContextMenu component empty state", () => {
|
|
366
|
+
it("should render with empty options array", () => {
|
|
367
|
+
const props = {
|
|
368
|
+
position: { x: 100, y: 200 },
|
|
369
|
+
options: [] as IActionMenuOption[],
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const { container } = render(
|
|
373
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
374
|
+
<ContextMenu {...props} />
|
|
375
|
+
</ThemeProvider>,
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
// Menu container should still render
|
|
379
|
+
expect(container.querySelector("div")).toBeTruthy();
|
|
380
|
+
});
|
|
381
|
+
});
|
|
@@ -129,7 +129,7 @@ describe("FloatingMenu component rendering", () => {
|
|
|
129
129
|
<FloatingMenu {...defaultProps} />
|
|
130
130
|
</ThemeProvider>,
|
|
131
131
|
);
|
|
132
|
-
fireEvent.
|
|
132
|
+
fireEvent.click(document);
|
|
133
133
|
expect(screen.getByTestId("floating-menu-wrapper")).toBeInTheDocument();
|
|
134
134
|
});
|
|
135
135
|
|
|
@@ -221,7 +221,7 @@ describe("FloatingMenu component rendering", () => {
|
|
|
221
221
|
</ThemeProvider>,
|
|
222
222
|
);
|
|
223
223
|
|
|
224
|
-
fireEvent.
|
|
224
|
+
fireEvent.click(document);
|
|
225
225
|
|
|
226
226
|
expect(screen.queryByTestId("floating-menu-wrapper")).not.toBeInTheDocument();
|
|
227
227
|
});
|
|
@@ -799,7 +799,7 @@ describe("FloatingMenu component rendering", () => {
|
|
|
799
799
|
expect(screen.getByTestId("floating-menu-wrapper")).toBeInTheDocument();
|
|
800
800
|
|
|
801
801
|
// Click outside — document is not inside wrapper
|
|
802
|
-
fireEvent.
|
|
802
|
+
fireEvent.click(document);
|
|
803
803
|
|
|
804
804
|
expect(actionOnClose).toHaveBeenCalledTimes(1);
|
|
805
805
|
expect(screen.queryByTestId("floating-menu-wrapper")).not.toBeInTheDocument();
|
|
@@ -112,7 +112,7 @@ describe("FloatingPanel component events trigger", () => {
|
|
|
112
112
|
const iconAction = screen.getByTestId("icon-action-component");
|
|
113
113
|
expect(iconAction).toBeTruthy();
|
|
114
114
|
|
|
115
|
-
fireEvent.
|
|
115
|
+
fireEvent.click(document);
|
|
116
116
|
expect(toggleModalMock).toBeCalled();
|
|
117
117
|
expect(handlePanelMock).toBeCalledWith(false);
|
|
118
118
|
});
|
|
@@ -142,7 +142,7 @@ describe("FloatingPanel component events trigger", () => {
|
|
|
142
142
|
const iconAction = screen.getByTestId("icon-action-component");
|
|
143
143
|
expect(iconAction).toBeTruthy();
|
|
144
144
|
|
|
145
|
-
fireEvent.
|
|
145
|
+
fireEvent.click(document);
|
|
146
146
|
expect(toggleModalMock).not.toBeCalled();
|
|
147
147
|
expect(handlePanelMock).not.toBeCalled();
|
|
148
148
|
});
|
|
@@ -326,7 +326,7 @@ describe("SideModal component events", () => {
|
|
|
326
326
|
</ThemeProvider>,
|
|
327
327
|
);
|
|
328
328
|
|
|
329
|
-
map.
|
|
329
|
+
map.click({
|
|
330
330
|
target: document.body,
|
|
331
331
|
});
|
|
332
332
|
|
|
@@ -348,7 +348,7 @@ describe("SideModal component events", () => {
|
|
|
348
348
|
|
|
349
349
|
const title = screen.getByTestId("side-modal-title");
|
|
350
350
|
|
|
351
|
-
map.
|
|
351
|
+
map.click({
|
|
352
352
|
target: title,
|
|
353
353
|
});
|
|
354
354
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { act, renderHook } from "@testing-library/react";
|
|
2
|
+
|
|
2
3
|
import "@testing-library/jest-dom";
|
|
3
4
|
|
|
4
|
-
import { useModal, useModals } from "@ax/hooks";
|
|
5
|
+
import { useContextMenu, useModal, useModals } from "@ax/hooks";
|
|
5
6
|
|
|
6
7
|
describe("useModal hook", () => {
|
|
7
8
|
beforeEach(() => {
|
|
@@ -301,3 +302,87 @@ describe("useModal and useModals working together", () => {
|
|
|
301
302
|
expect(document.body.classList.contains("modal-open")).toBe(false);
|
|
302
303
|
});
|
|
303
304
|
});
|
|
305
|
+
|
|
306
|
+
describe("useContextMenu hook", () => {
|
|
307
|
+
it("should initialize with contextMenu as null", () => {
|
|
308
|
+
const { result } = renderHook(() => useContextMenu());
|
|
309
|
+
expect(result.current.contextMenu).toBe(null);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("should open context menu with coordinates when handleContextMenu is called", () => {
|
|
313
|
+
const { result } = renderHook(() => useContextMenu());
|
|
314
|
+
|
|
315
|
+
const mouseEvent = new MouseEvent("contextmenu", {
|
|
316
|
+
bubbles: true,
|
|
317
|
+
clientX: 100,
|
|
318
|
+
clientY: 200,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
act(() => {
|
|
322
|
+
result.current.handleContextMenu(mouseEvent as any);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
expect(result.current.contextMenu).toEqual({ x: 100, y: 200 });
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("should prevent default when handleContextMenu is called", () => {
|
|
329
|
+
const { result } = renderHook(() => useContextMenu());
|
|
330
|
+
const preventDefaultSpy = jest.fn();
|
|
331
|
+
|
|
332
|
+
const mouseEvent = {
|
|
333
|
+
preventDefault: preventDefaultSpy,
|
|
334
|
+
clientX: 50,
|
|
335
|
+
clientY: 75,
|
|
336
|
+
} as any;
|
|
337
|
+
|
|
338
|
+
act(() => {
|
|
339
|
+
result.current.handleContextMenu(mouseEvent);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(preventDefaultSpy).toHaveBeenCalled();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("should close context menu when closeContextMenu is called", () => {
|
|
346
|
+
const { result } = renderHook(() => useContextMenu());
|
|
347
|
+
|
|
348
|
+
act(() => {
|
|
349
|
+
result.current.handleContextMenu({
|
|
350
|
+
preventDefault: jest.fn(),
|
|
351
|
+
clientX: 100,
|
|
352
|
+
clientY: 200,
|
|
353
|
+
} as any);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
expect(result.current.contextMenu).not.toBe(null);
|
|
357
|
+
|
|
358
|
+
act(() => {
|
|
359
|
+
result.current.closeContextMenu();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
expect(result.current.contextMenu).toBe(null);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("should update position when handleContextMenu is called multiple times", () => {
|
|
366
|
+
const { result } = renderHook(() => useContextMenu());
|
|
367
|
+
|
|
368
|
+
act(() => {
|
|
369
|
+
result.current.handleContextMenu({
|
|
370
|
+
preventDefault: jest.fn(),
|
|
371
|
+
clientX: 100,
|
|
372
|
+
clientY: 200,
|
|
373
|
+
} as any);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
expect(result.current.contextMenu).toEqual({ x: 100, y: 200 });
|
|
377
|
+
|
|
378
|
+
act(() => {
|
|
379
|
+
result.current.handleContextMenu({
|
|
380
|
+
preventDefault: jest.fn(),
|
|
381
|
+
clientX: 300,
|
|
382
|
+
clientY: 400,
|
|
383
|
+
} as any);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
expect(result.current.contextMenu).toEqual({ x: 300, y: 400 });
|
|
387
|
+
});
|
|
388
|
+
});
|
|
@@ -3,17 +3,11 @@ import type { IActionMenuOption } from "@ax/types";
|
|
|
3
3
|
|
|
4
4
|
import * as S from "./style";
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
const { options,
|
|
6
|
+
export const ActionMenuContent = (props: IActionMenuContentProps): JSX.Element => {
|
|
7
|
+
const { options, isContextMenu = true } = props;
|
|
8
8
|
|
|
9
9
|
const filteredOptions = options.filter((option: IActionMenuOption | undefined | null) => !!option);
|
|
10
10
|
|
|
11
|
-
const MoreInfoButton = () => (
|
|
12
|
-
<S.IconActionWrapper data-testid="more-info-button">
|
|
13
|
-
<IconAction icon={icon} size={size} />
|
|
14
|
-
</S.IconActionWrapper>
|
|
15
|
-
);
|
|
16
|
-
|
|
17
11
|
const ActionMenuItem = (item: IActionMenuOption) => (
|
|
18
12
|
<S.ActionItem
|
|
19
13
|
key={item.icon}
|
|
@@ -32,14 +26,30 @@ const ActionMenu = (props: IActionMenuProps): JSX.Element => {
|
|
|
32
26
|
</S.ActionItem>
|
|
33
27
|
);
|
|
34
28
|
|
|
29
|
+
return (
|
|
30
|
+
<S.ActionMenu isContextMenu={isContextMenu}>
|
|
31
|
+
{filteredOptions.map((item: IActionMenuOption | undefined | null) => item && ActionMenuItem(item))}
|
|
32
|
+
</S.ActionMenu>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const ActionMenu = (props: IActionMenuProps): JSX.Element => {
|
|
37
|
+
const { options, icon, className, tooltip, size } = props;
|
|
38
|
+
|
|
39
|
+
const filteredOptions = options.filter((option: IActionMenuOption | undefined | null) => !!option);
|
|
40
|
+
|
|
41
|
+
const MoreInfoButton = () => (
|
|
42
|
+
<S.IconActionWrapper data-testid="more-info-button">
|
|
43
|
+
<IconAction icon={icon} size={size} />
|
|
44
|
+
</S.IconActionWrapper>
|
|
45
|
+
);
|
|
46
|
+
|
|
35
47
|
return options.length > 0 ? (
|
|
36
48
|
<S.Wrapper className={className} data-testid="action-menu-wrapper">
|
|
37
49
|
{filteredOptions.length > 0 && (
|
|
38
50
|
<Tooltip content={tooltip} hideOnClick>
|
|
39
51
|
<FloatingMenu Button={MoreInfoButton}>
|
|
40
|
-
<
|
|
41
|
-
{filteredOptions.map((item: IActionMenuOption | undefined | null) => item && ActionMenuItem(item))}
|
|
42
|
-
</S.ActionMenu>
|
|
52
|
+
<ActionMenuContent options={options} isContextMenu={false} />
|
|
43
53
|
</FloatingMenu>
|
|
44
54
|
</Tooltip>
|
|
45
55
|
)}
|
|
@@ -57,4 +67,9 @@ export interface IActionMenuProps {
|
|
|
57
67
|
size?: "s" | "m";
|
|
58
68
|
}
|
|
59
69
|
|
|
70
|
+
export interface IActionMenuContentProps {
|
|
71
|
+
options: (IActionMenuOption | undefined | null)[];
|
|
72
|
+
isContextMenu?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
60
75
|
export default ActionMenu;
|
|
@@ -1,43 +1,48 @@
|
|
|
1
1
|
import styled from "styled-components";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const Wrapper = styled.div`
|
|
4
4
|
position: relative;
|
|
5
5
|
`;
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
const IconActionWrapper = styled.div`
|
|
8
8
|
display: flex;
|
|
9
9
|
justify-content: center;
|
|
10
10
|
align-items: center;
|
|
11
11
|
`;
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
const ActionMenu = styled.ul<{ isContextMenu: boolean }>`
|
|
14
14
|
padding: 0;
|
|
15
15
|
cursor: pointer;
|
|
16
|
+
background-color: ${(p) => p.theme.color.uiBackground02};
|
|
17
|
+
border-radius: ${(p) => p.theme.radii.s};
|
|
18
|
+
padding: ${(p) => (p.isContextMenu ? `${p.theme.spacing.xs} 0` : "0")};
|
|
19
|
+
max-width: ${(p) => `calc(${p.theme.spacing.l} * 5)`};
|
|
20
|
+
min-width: ${(p) => `calc(${p.theme.spacing.m} * 7)`};
|
|
16
21
|
`;
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
const ActionText = styled.div`
|
|
19
24
|
padding-left: ${(p) => p.theme.spacing.xs};
|
|
20
25
|
margin-left: ${(p) => p.theme.spacing.xxs};
|
|
21
26
|
`;
|
|
22
27
|
|
|
23
|
-
|
|
28
|
+
const Label = styled.div`
|
|
24
29
|
&:first-letter {
|
|
25
30
|
text-transform: capitalize;
|
|
26
31
|
}
|
|
27
32
|
`;
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
const HelpText = styled.div`
|
|
30
35
|
${(p) => p.theme.textStyle.uiXS};
|
|
31
36
|
white-space: normal;
|
|
32
37
|
width: 180px;
|
|
33
38
|
`;
|
|
34
39
|
|
|
35
|
-
|
|
40
|
+
const IconWrapper = styled.div`
|
|
36
41
|
height: ${(p) => p.theme.spacing.m};
|
|
37
42
|
width: ${(p) => p.theme.spacing.m};
|
|
38
43
|
`;
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
const ActionItem = styled.li<{ disabled?: boolean; color?: boolean }>`
|
|
41
46
|
display: flex;
|
|
42
47
|
align-items: flex-start;
|
|
43
48
|
${(p) => p.theme.textStyle.uiM};
|
|
@@ -62,3 +67,5 @@ export const ActionItem = styled.li<{ disabled?: boolean; color?: boolean }>`
|
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
`;
|
|
70
|
+
|
|
71
|
+
export { Wrapper, IconActionWrapper, ActionMenu, ActionText, Label, HelpText, IconWrapper, ActionItem };
|