@griddo/ax 12.1.1-rc.0 → 12.2.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.
Files changed (40) hide show
  1. package/package.json +2 -2
  2. package/src/__tests__/components/ContextMenu/ContextMenu.test.tsx +381 -0
  3. package/src/__tests__/components/FloatingMenu/FloatingMenu.test.tsx +3 -3
  4. package/src/__tests__/components/FloatingPanel/FloatingPanel.test.tsx +2 -2
  5. package/src/__tests__/components/SideModal/SideModal.test.tsx +2 -2
  6. package/src/__tests__/hooks/modals.test.tsx +86 -1
  7. package/src/components/ActionMenu/index.tsx +26 -11
  8. package/src/components/ActionMenu/style.tsx +15 -8
  9. package/src/components/ContextMenu/index.tsx +102 -0
  10. package/src/components/ContextMenu/style.tsx +15 -0
  11. package/src/components/Fields/ComponentContainer/index.tsx +9 -2
  12. package/src/components/FloatingMenu/index.tsx +1 -1
  13. package/src/components/FloatingMenu/style.tsx +1 -1
  14. package/src/components/index.tsx +4 -1
  15. package/src/hooks/index.tsx +2 -1
  16. package/src/hooks/modals.tsx +51 -4
  17. package/src/modules/Analytics/DimensionItem/index.tsx +5 -3
  18. package/src/modules/Analytics/GroupItem/index.tsx +5 -3
  19. package/src/modules/Categories/CategoriesList/CategoryItem/index.tsx +5 -2
  20. package/src/modules/Content/PageItem/index.tsx +15 -3
  21. package/src/modules/FileDrive/FolderItem/index.tsx +5 -3
  22. package/src/modules/FileDrive/GridItem/index.tsx +5 -3
  23. package/src/modules/FileDrive/ListItem/index.tsx +5 -3
  24. package/src/modules/Forms/FormCategoriesList/CategoryItem/index.tsx +5 -2
  25. package/src/modules/Forms/FormList/FormItem/index.tsx +5 -2
  26. package/src/modules/MediaGallery/FolderItem/index.tsx +5 -3
  27. package/src/modules/MediaGallery/GridItem/index.tsx +5 -3
  28. package/src/modules/MediaGallery/ListItem/index.tsx +5 -3
  29. package/src/modules/Navigation/Defaults/Item/index.tsx +5 -3
  30. package/src/modules/Navigation/Menus/List/Table/Item/index.tsx +5 -2
  31. package/src/modules/Redirects/RedirectItem/index.tsx +5 -3
  32. package/src/modules/Settings/Integrations/IntegrationForm/VariableItem/index.tsx +5 -3
  33. package/src/modules/Settings/Integrations/IntegrationItem/CopyModal/index.tsx +1 -0
  34. package/src/modules/Settings/Integrations/IntegrationItem/index.tsx +5 -3
  35. package/src/modules/Settings/Languages/Table/Item/index.tsx +5 -3
  36. package/src/modules/Sites/SitesList/GridView/GridSiteItem/index.tsx +5 -2
  37. package/src/modules/Sites/SitesList/ListView/ListSiteItem/index.tsx +5 -2
  38. package/src/modules/StructuredData/StructuredDataList/GlobalPageItem/index.tsx +5 -2
  39. package/src/modules/StructuredData/StructuredDataList/StructuredDataItem/index.tsx +5 -3
  40. 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.1.1-rc.0",
4
+ "version": "12.2.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": "fcd89237629c5295621f400628d06f2a3c3d3b21"
222
+ "gitHead": "e6e2eceb22292ff2b70c9454e4c0dd7c5256a1a6"
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.mouseDown(document);
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.mouseDown(document);
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.mouseDown(document);
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.mouseDown(document);
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.mouseDown(document);
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.mousedown({
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.mousedown({
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 ActionMenu = (props: IActionMenuProps): JSX.Element => {
7
- const { options, icon, className, tooltip, size } = props;
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
- <S.ActionMenu>
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
- export const Wrapper = styled.div`
3
+ const Wrapper = styled.div`
4
4
  position: relative;
5
5
  `;
6
6
 
7
- export const IconActionWrapper = styled.div`
7
+ const IconActionWrapper = styled.div`
8
8
  display: flex;
9
9
  justify-content: center;
10
10
  align-items: center;
11
11
  `;
12
12
 
13
- export const ActionMenu = styled.ul`
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
- export const ActionText = styled.div`
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
- export const Label = styled.div`
28
+ const Label = styled.div`
24
29
  &:first-letter {
25
30
  text-transform: capitalize;
26
31
  }
27
32
  `;
28
33
 
29
- export const HelpText = styled.div`
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
- export const IconWrapper = styled.div`
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
- export const ActionItem = styled.li<{ disabled?: boolean; color?: boolean }>`
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 };