@@ -6,46 +6,109 @@ import { useContextMenu } from './use-context-menu';
6
6
import { ContextMenuProvider } from './context-menu-provider' ;
7
7
import type { MenuItem } from './types' ;
8
8
9
- type TestMenuItem = MenuItem & { id : number } ;
10
-
11
9
describe ( 'useContextMenu' , function ( ) {
12
- const TestMenu : React . FC < { items : TestMenuItem [ ] } > = ( { items } ) => (
10
+ const TestMenu : React . FC < { items : MenuItem [ ] } > = ( { items } ) => (
13
11
< div data-testid = "test-menu" >
14
12
{ items . map ( ( item , idx ) => (
15
- < div key = { idx } data-testid = { `menu-item-${ item . id } ` } >
13
+ < div
14
+ key = { idx }
15
+ data-testid = { `menu-item-${ item . label } ` }
16
+ role = "menuitem"
17
+ tabIndex = { 0 }
18
+ onClick = { ( event ) => item . onAction ?.( event ) }
19
+ onKeyDown = { ( event ) => {
20
+ if ( event . key === 'Enter' ) {
21
+ item . onAction ?.( event ) ;
22
+ }
23
+ } }
24
+ >
16
25
{ item . label }
17
26
</ div >
18
27
) ) }
19
28
</ div >
20
29
) ;
21
30
22
- const TestComponent = ( ) => {
31
+ const TestComponent = ( {
32
+ onRegister,
33
+ onAction,
34
+ } : {
35
+ onRegister ?: ( ref : any ) => void ;
36
+ onAction ?: ( id ) => void ;
37
+ } ) => {
23
38
const contextMenu = useContextMenu ( { Menu : TestMenu } ) ;
24
- const items : TestMenuItem [ ] = [
25
- {
26
- id : 1 ,
27
- label : 'Test A' ,
28
- onAction : ( ) => {
29
- /* noop */
30
- } ,
31
- } ,
39
+ const items : MenuItem [ ] = [
32
40
{
33
- id : 2 ,
34
- label : 'Test B' ,
35
- onAction : ( ) => {
36
- /* noop */
37
- } ,
41
+ label : 'Test Item' ,
42
+ onAction : ( ) => onAction ?.( 1 ) ,
38
43
} ,
39
44
] ;
40
45
const ref = contextMenu . registerItems ( items ) ;
41
46
47
+ React . useEffect ( ( ) => {
48
+ onRegister ?.( ref ) ;
49
+ } , [ ref , onRegister ] ) ;
50
+
42
51
return (
43
52
< div data-testid = "test-trigger" ref = { ref } >
44
53
Test Component
45
54
</ div >
46
55
) ;
47
56
} ;
48
57
58
+ // Add new test components for nested context menu scenario
59
+ const ParentComponent = ( {
60
+ onAction,
61
+ children,
62
+ } : {
63
+ onAction ?: ( id : number ) => void ;
64
+ children ?: React . ReactNode ;
65
+ } ) => {
66
+ const contextMenu = useContextMenu ( { Menu : TestMenu } ) ;
67
+ const parentItems : MenuItem [ ] = [
68
+ {
69
+ label : 'Parent Item 1' ,
70
+ onAction : ( ) => onAction ?.( 1 ) ,
71
+ } ,
72
+ {
73
+ label : 'Parent Item 2' ,
74
+ onAction : ( ) => onAction ?.( 2 ) ,
75
+ } ,
76
+ ] ;
77
+ const ref = contextMenu . registerItems ( parentItems ) ;
78
+
79
+ return (
80
+ < div data-testid = "parent-trigger" ref = { ref } >
81
+ < div > Parent Component</ div >
82
+ { children }
83
+ </ div >
84
+ ) ;
85
+ } ;
86
+
87
+ const ChildComponent = ( {
88
+ onAction,
89
+ } : {
90
+ onAction ?: ( id : number ) => void ;
91
+ } ) => {
92
+ const contextMenu = useContextMenu ( { Menu : TestMenu } ) ;
93
+ const childItems : MenuItem [ ] = [
94
+ {
95
+ label : 'Child Item 1' ,
96
+ onAction : ( ) => onAction ?.( 1 ) ,
97
+ } ,
98
+ {
99
+ label : 'Child Item 2' ,
100
+ onAction : ( ) => onAction ?.( 2 ) ,
101
+ } ,
102
+ ] ;
103
+ const ref = contextMenu . registerItems ( childItems ) ;
104
+
105
+ return (
106
+ < div data-testid = "child-trigger" ref = { ref } >
107
+ Child Component
108
+ </ div >
109
+ ) ;
110
+ } ;
111
+
49
112
describe ( 'when used outside provider' , function ( ) {
50
113
it ( 'throws an error' , function ( ) {
51
114
expect ( ( ) => {
@@ -54,7 +117,7 @@ describe('useContextMenu', function () {
54
117
} ) ;
55
118
} ) ;
56
119
57
- describe ( 'with valid provider' , function ( ) {
120
+ describe ( 'with a valid provider' , function ( ) {
58
121
beforeEach ( ( ) => {
59
122
// Create the container for the context menu portal
60
123
const container = document . createElement ( 'div' ) ;
@@ -80,51 +143,117 @@ describe('useContextMenu', function () {
80
143
expect ( screen . getByTestId ( 'test-trigger' ) ) . to . exist ;
81
144
} ) ;
82
145
146
+ it ( 'registers context menu event listener' , function ( ) {
147
+ const onRegister = sinon . spy ( ) ;
148
+
149
+ render (
150
+ < ContextMenuProvider >
151
+ < TestComponent onRegister = { onRegister } />
152
+ </ ContextMenuProvider >
153
+ ) ;
154
+
155
+ expect ( onRegister ) . to . have . been . calledOnce ;
156
+ expect ( onRegister . firstCall . args [ 0 ] ) . to . be . a ( 'function' ) ;
157
+ } ) ;
158
+
83
159
it ( 'shows context menu on right click' , function ( ) {
84
160
render (
85
161
< ContextMenuProvider >
86
162
< TestComponent />
87
163
</ ContextMenuProvider >
88
164
) ;
89
165
90
- expect ( screen . queryByTestId ( 'menu-item-1' ) ) . not . to . exist ;
91
- expect ( screen . queryByTestId ( 'menu-item-2' ) ) . not . to . exist ;
92
-
93
166
const trigger = screen . getByTestId ( 'test-trigger' ) ;
94
167
userEvent . click ( trigger , { button : 2 } ) ;
95
168
96
169
// The menu should be rendered in the portal
97
- expect ( screen . getByTestId ( 'menu-item-1' ) ) . to . exist ;
98
- expect ( screen . getByTestId ( 'menu-item-2' ) ) . to . exist ;
170
+ expect ( screen . getByTestId ( 'menu-item-Test Item' ) ) . to . exist ;
99
171
} ) ;
100
172
101
- it ( 'cleans up previous event listener when ref changes' , function ( ) {
102
- const removeEventListenerSpy = sinon . spy ( ) ;
103
- const addEventListenerSpy = sinon . spy ( ) ;
173
+ describe ( 'with nested context menus' , function ( ) {
174
+ it ( 'shows only parent items when right clicking parent area' , function ( ) {
175
+ render (
176
+ < ContextMenuProvider >
177
+ < ParentComponent />
178
+ </ ContextMenuProvider >
179
+ ) ;
104
180
105
- const { rerender } = render (
106
- < ContextMenuProvider >
107
- < TestComponent />
108
- </ ContextMenuProvider >
109
- ) ;
181
+ const parentTrigger = screen . getByTestId ( 'parent-trigger' ) ;
182
+ userEvent . click ( parentTrigger , { button : 2 } ) ;
183
+
184
+ // Should show parent items
185
+ expect ( screen . getByTestId ( 'menu-item-Parent Item 1' ) ) . to . exist ;
186
+ expect ( screen . getByTestId ( 'menu-item-Parent Item 2' ) ) . to . exist ;
110
187
111
- // Simulate ref change
112
- const ref = screen . getByTestId ( 'test-trigger' ) ;
113
- Object . defineProperty ( ref , 'addEventListener' , {
114
- value : addEventListenerSpy ,
188
+ // Should not show child items
189
+ expect ( ( ) => screen . getByTestId ( 'menu-item-Child Item 1' ) ) . to . throw ;
190
+ expect ( ( ) => screen . getByTestId ( 'menu-item-Child Item 2' ) ) . to . throw ;
115
191
} ) ;
116
- Object . defineProperty ( ref , 'removeEventListener' , {
117
- value : removeEventListenerSpy ,
192
+
193
+ it ( 'shows both parent and child items when right clicking child area' , function ( ) {
194
+ render (
195
+ < ContextMenuProvider >
196
+ < ParentComponent >
197
+ < ChildComponent />
198
+ </ ParentComponent >
199
+ </ ContextMenuProvider >
200
+ ) ;
201
+
202
+ const childTrigger = screen . getByTestId ( 'child-trigger' ) ;
203
+ userEvent . click ( childTrigger , { button : 2 } ) ;
204
+
205
+ // Should show both parent and child items
206
+ expect ( screen . getByTestId ( 'menu-item-Parent Item 1' ) ) . to . exist ;
207
+ expect ( screen . getByTestId ( 'menu-item-Parent Item 2' ) ) . to . exist ;
208
+ expect ( screen . getByTestId ( 'menu-item-Child Item 1' ) ) . to . exist ;
209
+ expect ( screen . getByTestId ( 'menu-item-Child Item 2' ) ) . to . exist ;
118
210
} ) ;
119
211
120
- rerender (
121
- < ContextMenuProvider >
122
- < TestComponent />
123
- </ ContextMenuProvider >
124
- ) ;
212
+ it ( 'triggers only the child action when clicking child menu item' , function ( ) {
213
+ const parentOnAction = sinon . spy ( ) ;
214
+ const childOnAction = sinon . spy ( ) ;
215
+
216
+ render (
217
+ < ContextMenuProvider >
218
+ < ParentComponent onAction = { parentOnAction } >
219
+ < ChildComponent onAction = { childOnAction } />
220
+ </ ParentComponent >
221
+ </ ContextMenuProvider >
222
+ ) ;
223
+
224
+ const childTrigger = screen . getByTestId ( 'child-trigger' ) ;
225
+ userEvent . click ( childTrigger , { button : 2 } ) ;
226
+
227
+ const childItem1 = screen . getByTestId ( 'menu-item-Child Item 1' ) ;
228
+ userEvent . click ( childItem1 ) ;
125
229
126
- expect ( removeEventListenerSpy ) . to . have . been . calledWith ( 'contextmenu' ) ;
127
- expect ( addEventListenerSpy ) . to . have . been . calledWith ( 'contextmenu' ) ;
230
+ expect ( childOnAction ) . to . have . been . calledOnceWithExactly ( 1 ) ;
231
+ expect ( parentOnAction ) . to . not . have . been . called ;
232
+ expect ( ( ) => screen . getByTestId ( 'test-menu' ) ) . to . throw ;
233
+ } ) ;
234
+
235
+ it ( 'triggers only the parent action when clicking a parent menu item from child context' , function ( ) {
236
+ const parentOnAction = sinon . spy ( ) ;
237
+ const childOnAction = sinon . spy ( ) ;
238
+
239
+ render (
240
+ < ContextMenuProvider >
241
+ < ParentComponent onAction = { parentOnAction } >
242
+ < ChildComponent onAction = { childOnAction } />
243
+ </ ParentComponent >
244
+ </ ContextMenuProvider >
245
+ ) ;
246
+
247
+ const childTrigger = screen . getByTestId ( 'child-trigger' ) ;
248
+ userEvent . click ( childTrigger , { button : 2 } ) ;
249
+
250
+ const parentItem1 = screen . getByTestId ( 'menu-item-Parent Item 1' ) ;
251
+ userEvent . click ( parentItem1 ) ;
252
+
253
+ expect ( parentOnAction ) . to . have . been . calledOnceWithExactly ( 1 ) ;
254
+ expect ( childOnAction ) . to . not . have . been . called ;
255
+ expect ( ( ) => screen . getByTestId ( 'test-menu' ) ) . to . throw ;
256
+ } ) ;
128
257
} ) ;
129
258
} ) ;
130
259
} ) ;
0 commit comments