@@ -2,6 +2,7 @@ import type { FloatingEvents, OpenChangeReason } from '@floating-ui/react';
22import { renderHook } from '@testing-library/react' ;
33import { afterEach , beforeEach , describe , expect , it } from 'vitest' ;
44
5+ import { withInteractionOrigin } from '../utils/interaction-origin' ;
56import { useReturnFocus } from './use-return-focus' ;
67
78function createEvents ( ) : FloatingEvents & { close : ( event ?: Event , reason ?: OpenChangeReason ) => void } {
@@ -26,7 +27,7 @@ function createEvents(): FloatingEvents & { close: (event?: Event, reason?: Open
2627 } ;
2728}
2829
29- function renderReturnFocus ( trigger : HTMLElement ) {
30+ function renderReturnFocus ( trigger : HTMLElement | null ) {
3031 const events = createEvents ( ) ;
3132
3233 const { result, rerender } = renderHook (
@@ -35,7 +36,14 @@ function renderReturnFocus(trigger: HTMLElement) {
3536 { initialProps : { open : false } } ,
3637 ) ;
3738
38- return { events, result, open : ( open : boolean ) => rerender ( { open } ) } ;
39+ return {
40+ events,
41+ result,
42+ open : ( open : boolean ) => {
43+ events . emit ( 'openchange' , { open } ) ;
44+ rerender ( { open } ) ;
45+ } ,
46+ } ;
3947}
4048
4149let trigger : HTMLElement ;
@@ -105,4 +113,59 @@ describe('useReturnFocus', () => {
105113
106114 expect ( result . current . current ) . toBe ( trigger ) ;
107115 } ) ;
116+
117+ describe ( 'opened from inside another floating element' , ( ) => {
118+ let menuTrigger : HTMLElement ;
119+
120+ beforeEach ( ( ) => {
121+ menuTrigger = document . createElement ( 'button' ) ;
122+ document . body . append ( menuTrigger ) ;
123+ } ) ;
124+
125+ afterEach ( ( ) => menuTrigger . remove ( ) ) ;
126+
127+ it ( 'falls back to the interaction origin when it has no trigger of its own' , ( ) => {
128+ const { result, open } = renderReturnFocus ( null ) ;
129+
130+ withInteractionOrigin ( menuTrigger , ( ) => open ( true ) ) ;
131+
132+ expect ( result . current . current ) . toBe ( menuTrigger ) ;
133+ } ) ;
134+
135+ it ( 'prefers its own trigger over the interaction origin' , ( ) => {
136+ const { result, open } = renderReturnFocus ( trigger ) ;
137+
138+ withInteractionOrigin ( menuTrigger , ( ) => open ( true ) ) ;
139+
140+ expect ( result . current . current ) . toBe ( trigger ) ;
141+ } ) ;
142+
143+ it ( 'falls back to the origin once its own trigger has left the page' , ( ) => {
144+ const { result, open } = renderReturnFocus ( trigger ) ;
145+ withInteractionOrigin ( menuTrigger , ( ) => open ( true ) ) ;
146+
147+ trigger . remove ( ) ;
148+
149+ expect ( result . current . current ) . toBe ( menuTrigger ) ;
150+ } ) ;
151+
152+ it ( 'resolves to nothing once the origin has left the page too' , ( ) => {
153+ const { result, open } = renderReturnFocus ( null ) ;
154+ withInteractionOrigin ( menuTrigger , ( ) => open ( true ) ) ;
155+
156+ menuTrigger . remove ( ) ;
157+
158+ expect ( result . current . current ) . toBeNull ( ) ;
159+ } ) ;
160+
161+ it ( 'forgets the origin on an open that has none' , ( ) => {
162+ const { result, open } = renderReturnFocus ( null ) ;
163+ withInteractionOrigin ( menuTrigger , ( ) => open ( true ) ) ;
164+ open ( false ) ;
165+
166+ open ( true ) ;
167+
168+ expect ( result . current . current ) . toBeNull ( ) ;
169+ } ) ;
170+ } ) ;
108171} ) ;
0 commit comments