11import type { RenderResult } from '@testing-library/vue'
22import userEvent from '@testing-library/user-event'
3- import { render , waitFor } from '@testing-library/vue'
3+ import { render } from '@testing-library/vue'
44import { beforeEach , describe , expect , it , vi } from 'vitest'
5- import { defineComponent } from 'vue'
5+ import { defineComponent , nextTick } from 'vue'
66import { FocusScope } from '.'
7+ import { DialogContent , DialogRoot , DialogTitle , DialogTrigger } from '../Dialog'
8+ import { SelectContent , SelectItem , SelectRoot , SelectTrigger , SelectValue } from '../Select'
79
810const INNER_NAME_INPUT_LABEL = 'Name'
911const INNER_EMAIL_INPUT_LABEL = 'Email'
@@ -24,6 +26,7 @@ const TestField = ({
2426describe ( 'focusScope' , ( ) => {
2527 describe ( 'given a default FocusScope' , ( ) => {
2628 let rendered : RenderResult
29+ let focusContainer : HTMLElement
2730 let tabbableFirst : HTMLInputElement
2831 let tabbableSecond : HTMLInputElement
2932 let tabbableLast : HTMLButtonElement
@@ -33,7 +36,7 @@ describe('focusScope', () => {
3336 components : { TestField, FocusScope } ,
3437 template : `<div>
3538 <FocusScope asChild loop trapped>
36- <form>
39+ <form data-testid="focus-scope" >
3740 <TestField label=${ INNER_NAME_INPUT_LABEL } />
3841 <TestField label=${ INNER_EMAIL_INPUT_LABEL } />
3942 <button>${ INNER_SUBMIT_LABEL } </button>
@@ -43,6 +46,7 @@ describe('focusScope', () => {
4346 <button>some outer button</button>
4447 </div>` ,
4548 } ) )
49+ focusContainer = rendered . getByTestId ( 'focus-scope' ) as HTMLElement
4650 tabbableFirst = rendered . getByLabelText ( INNER_NAME_INPUT_LABEL ) as HTMLInputElement
4751 tabbableSecond = rendered . getByLabelText ( INNER_EMAIL_INPUT_LABEL ) as HTMLInputElement
4852 tabbableLast = rendered . getByText ( INNER_SUBMIT_LABEL ) as HTMLButtonElement
@@ -57,13 +61,20 @@ describe('focusScope', () => {
5761 it ( 'should focus the last element in the scope on shift+tab from the first element in scope' , async ( ) => {
5862 tabbableFirst . focus ( )
5963 await userEvent . tab ( { shift : true } )
60- waitFor ( ( ) => expect ( tabbableLast ) . toBe ( document . activeElement ) )
64+ expect ( tabbableLast ) . toHaveFocus ( )
6165 } )
6266
6367 it ( 'should focus the first element in scope on tab from the last element in scope' , async ( ) => {
6468 tabbableLast . focus ( )
6569 await userEvent . tab ( )
66- expect ( tabbableFirst ) . toBe ( document . activeElement )
70+ expect ( tabbableFirst ) . toHaveFocus ( )
71+ } )
72+
73+ it ( 'should focus container when focused element is removed from the DOM' , async ( ) => {
74+ tabbableFirst . focus ( )
75+ tabbableFirst . remove ( )
76+ await nextTick ( )
77+ expect ( focusContainer ) . toHaveFocus ( )
6778 } )
6879 } )
6980
@@ -94,13 +105,13 @@ describe('focusScope', () => {
94105 it ( 'should skip the element with a negative tabindex on tab' , async ( ) => {
95106 tabbableLast . focus ( )
96107 await userEvent . tab ( )
97- expect ( tabbableSecond ) . toBe ( document . activeElement )
108+ expect ( tabbableSecond ) . toHaveFocus ( )
98109 } )
99110
100111 it ( 'should skip the element with a negative tabindex on shift+tab' , async ( ) => {
101112 tabbableSecond . focus ( )
102113 await userEvent . tab ( { shift : true } )
103- waitFor ( ( ) => expect ( tabbableLast ) . toBe ( document . activeElement ) )
114+ expect ( tabbableLast ) . toHaveFocus ( )
104115 } )
105116 } )
106117
@@ -111,7 +122,9 @@ describe('focusScope', () => {
111122 beforeEach ( ( ) => {
112123 rendered = render ( defineComponent ( {
113124 components : { TestField, FocusScope } ,
114- props : { handleLastFocusableElementBlur } ,
125+ setup ( ) {
126+ return { handleLastFocusableElementBlur }
127+ } ,
115128 template : `<div>
116129 <FocusScope asChild loop trapped>
117130 <form>
@@ -130,7 +143,48 @@ describe('focusScope', () => {
130143 tabbableFirst . focus ( )
131144 await userEvent . tab ( { shift : true } )
132145 await userEvent . tab ( )
133- waitFor ( ( ) => expect ( handleLastFocusableElementBlur ) . toHaveBeenCalledTimes ( 1 ) )
146+ expect ( handleLastFocusableElementBlur ) . toHaveBeenCalledTimes ( 1 )
147+ } )
148+ } )
149+
150+ // https://github.com/unovue/reka-ui/issues/2550
151+ describe ( 'given a FocusScope with SelectTrigger inside Dialog (#2550)' , ( ) => {
152+ const DialogWithSelect = defineComponent ( {
153+ components : { DialogRoot, DialogTrigger, DialogContent, DialogTitle, SelectRoot, SelectTrigger, SelectValue, SelectContent, SelectItem } ,
154+ template : `
155+ <DialogRoot>
156+ <DialogTrigger>Open</DialogTrigger>
157+ <DialogContent>
158+ <DialogTitle>Test Dialog</DialogTitle>
159+ <input data-testid="email-input" type="text" placeholder="you@example.com" />
160+ <SelectRoot>
161+ <SelectTrigger>
162+ <SelectValue placeholder="Select" />
163+ </SelectTrigger>
164+ <SelectContent>
165+ <SelectItem value="a">Option A</SelectItem>
166+ <SelectItem value="b">Option B</SelectItem>
167+ </SelectContent>
168+ </SelectRoot>
169+ </DialogContent>
170+ </DialogRoot>
171+ ` ,
172+ } )
173+
174+ it ( 'should auto-focus the first tabbable element when SelectTrigger is present' , async ( ) => {
175+ const rendered = render ( DialogWithSelect )
176+
177+ const trigger = rendered . getByRole ( 'button' , { name : 'Open' } )
178+ await userEvent . click ( trigger )
179+
180+ const input1 = rendered . getByTestId ( 'email-input' )
181+ expect ( input1 ) . toHaveFocus ( )
182+
183+ // close and reopen, and ensure the input is focused again, not the SelectTrigger
184+ await userEvent . keyboard ( '{Escape}' )
185+ await userEvent . click ( trigger )
186+ const inputReopen = rendered . getByTestId ( 'email-input' )
187+ expect ( inputReopen ) . toHaveFocus ( )
134188 } )
135189 } )
136190} )
0 commit comments