@@ -6,46 +6,109 @@ import { useContextMenu } from './use-context-menu';
66import { ContextMenuProvider } from './context-menu-provider' ;
77import type { MenuItem } from './types' ;
88
9- type TestMenuItem = MenuItem & { id : number } ;
10-
119describe ( 'useContextMenu' , function ( ) {
12- const TestMenu : React . FC < { items : TestMenuItem [ ] } > = ( { items } ) => (
10+ const TestMenu : React . FC < { items : MenuItem [ ] } > = ( { items } ) => (
1311 < div data-testid = "test-menu" >
1412 { 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+ >
1625 { item . label }
1726 </ div >
1827 ) ) }
1928 </ div >
2029 ) ;
2130
22- const TestComponent = ( ) => {
31+ const TestComponent = ( {
32+ onRegister,
33+ onAction,
34+ } : {
35+ onRegister ?: ( ref : any ) => void ;
36+ onAction ?: ( id ) => void ;
37+ } ) => {
2338 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 [ ] = [
3240 {
33- id : 2 ,
34- label : 'Test B' ,
35- onAction : ( ) => {
36- /* noop */
37- } ,
41+ label : 'Test Item' ,
42+ onAction : ( ) => onAction ?.( 1 ) ,
3843 } ,
3944 ] ;
4045 const ref = contextMenu . registerItems ( items ) ;
4146
47+ React . useEffect ( ( ) => {
48+ onRegister ?.( ref ) ;
49+ } , [ ref , onRegister ] ) ;
50+
4251 return (
4352 < div data-testid = "test-trigger" ref = { ref } >
4453 Test Component
4554 </ div >
4655 ) ;
4756 } ;
4857
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+
49112 describe ( 'when used outside provider' , function ( ) {
50113 it ( 'throws an error' , function ( ) {
51114 expect ( ( ) => {
@@ -54,7 +117,7 @@ describe('useContextMenu', function () {
54117 } ) ;
55118 } ) ;
56119
57- describe ( 'with valid provider' , function ( ) {
120+ describe ( 'with a valid provider' , function ( ) {
58121 beforeEach ( ( ) => {
59122 // Create the container for the context menu portal
60123 const container = document . createElement ( 'div' ) ;
@@ -80,51 +143,117 @@ describe('useContextMenu', function () {
80143 expect ( screen . getByTestId ( 'test-trigger' ) ) . to . exist ;
81144 } ) ;
82145
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+
83159 it ( 'shows context menu on right click' , function ( ) {
84160 render (
85161 < ContextMenuProvider >
86162 < TestComponent />
87163 </ ContextMenuProvider >
88164 ) ;
89165
90- expect ( screen . queryByTestId ( 'menu-item-1' ) ) . not . to . exist ;
91- expect ( screen . queryByTestId ( 'menu-item-2' ) ) . not . to . exist ;
92-
93166 const trigger = screen . getByTestId ( 'test-trigger' ) ;
94167 userEvent . click ( trigger , { button : 2 } ) ;
95168
96169 // 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 ;
99171 } ) ;
100172
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+ ) ;
104180
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 ;
110187
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 ;
115191 } ) ;
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 ;
118210 } ) ;
119211
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 ) ;
125229
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+ } ) ;
128257 } ) ;
129258 } ) ;
130259} ) ;
0 commit comments