@@ -306,26 +306,47 @@ describe('given a Combobox with openOnFocus', () => {
306306 } )
307307
308308 it ( 'should close content when focus moves to an element outside' , async ( ) => {
309- // Add an external focusable element
310309 const externalButton = document . createElement ( 'button' )
311310 externalButton . textContent = 'External'
312311 document . body . appendChild ( externalButton )
313312
314313 const input = wrapper . find ( 'input' )
315314
316- // Focus input to open via openOnFocus
317- await input . trigger ( 'focus' )
315+ input . element . focus ( )
318316 await nextTick ( )
319317 expect ( wrapper . find ( '[role=group]' ) . exists ( ) ) . toBe ( true )
320318
321- // Simulate blur with relatedTarget pointing to external element
322- await input . trigger ( 'blur' , { relatedTarget : externalButton } )
319+ externalButton . focus ( )
320+ await new Promise < void > ( resolve => requestAnimationFrame ( ( ) => resolve ( ) ) )
323321 await nextTick ( )
324322
325323 expect ( wrapper . find ( '[role=group]' ) . exists ( ) ) . toBe ( false )
326324
327325 externalButton . remove ( )
328326 } )
327+
328+ it ( 'should not close when focus is restored inside before deferred close fires' , async ( ) => {
329+ const externalButton = document . createElement ( 'button' )
330+ externalButton . textContent = 'External'
331+ document . body . appendChild ( externalButton )
332+
333+ const input = wrapper . find ( 'input' )
334+
335+ input . element . focus ( )
336+ await nextTick ( )
337+ expect ( wrapper . find ( '[role=group]' ) . exists ( ) ) . toBe ( true )
338+
339+ // Synthetic blur: fires handleBlur with relatedTarget outside,
340+ // but doesn't change document.activeElement — simulates FocusScope
341+ // restoring focus back inside before the deferred close callback runs
342+ await input . trigger ( 'blur' , { relatedTarget : externalButton } )
343+ await new Promise < void > ( resolve => requestAnimationFrame ( ( ) => resolve ( ) ) )
344+ await nextTick ( )
345+
346+ expect ( wrapper . find ( '[role=group]' ) . exists ( ) ) . toBe ( true )
347+
348+ externalButton . remove ( )
349+ } )
329350} )
330351
331352describe ( 'given combobox in a form' , async ( ) => {
@@ -458,3 +479,66 @@ describe('given Combobox with TagsInput and addOnBlur', () => {
458479 expect ( document . activeElement ) . toBe ( input . element )
459480 } )
460481} )
482+
483+ describe ( 'given combobox handleBlur with deferred close' , ( ) => {
484+ let wrapper : VueWrapper < InstanceType < typeof Combobox > >
485+
486+ window . HTMLElement . prototype . releasePointerCapture = vi . fn ( )
487+ window . HTMLElement . prototype . hasPointerCapture = vi . fn ( )
488+ window . HTMLElement . prototype . scrollIntoView = vi . fn ( )
489+
490+ beforeEach ( ( ) => {
491+ document . body . innerHTML = ''
492+ wrapper = mount ( Combobox , { attachTo : document . body , props : { resetSearchTermOnBlur : true } } )
493+ } )
494+
495+ it ( 'should not close when focus is restored inside before deferred close fires' , async ( ) => {
496+ const externalButton = document . createElement ( 'button' )
497+ externalButton . textContent = 'External'
498+ document . body . appendChild ( externalButton )
499+
500+ // Open combobox
501+ await wrapper . find ( 'button' ) . trigger ( 'click' )
502+ await nextTick ( )
503+ expect ( wrapper . find ( '[role=group]' ) . exists ( ) ) . toBe ( true )
504+
505+ const input = wrapper . find ( 'input' )
506+ input . element . focus ( )
507+
508+ // Synthetic blur: fires handleBlur with relatedTarget outside,
509+ // but doesn't change document.activeElement — simulates FocusScope
510+ // restoring focus back inside before the deferred close callback runs
511+ await input . trigger ( 'blur' , { relatedTarget : externalButton } )
512+
513+ await new Promise < void > ( resolve => requestAnimationFrame ( ( ) => resolve ( ) ) )
514+ await nextTick ( )
515+
516+ expect ( wrapper . find ( '[role=group]' ) . exists ( ) ) . toBe ( true )
517+
518+ externalButton . remove ( )
519+ } )
520+
521+ it ( 'should close when focus stays outside after rAF' , async ( ) => {
522+ const externalButton = document . createElement ( 'button' )
523+ externalButton . textContent = 'External'
524+ document . body . appendChild ( externalButton )
525+
526+ // Open combobox
527+ await wrapper . find ( 'button' ) . trigger ( 'click' )
528+ await nextTick ( )
529+ expect ( wrapper . find ( '[role=group]' ) . exists ( ) ) . toBe ( true )
530+
531+ const input = wrapper . find ( 'input' )
532+ input . element . focus ( )
533+
534+ // Focus moves outside and stays there
535+ externalButton . focus ( )
536+
537+ await new Promise < void > ( resolve => requestAnimationFrame ( ( ) => resolve ( ) ) )
538+ await nextTick ( )
539+
540+ expect ( wrapper . find ( '[role=group]' ) . exists ( ) ) . toBe ( false )
541+
542+ externalButton . remove ( )
543+ } )
544+ } )
0 commit comments