@@ -20,22 +20,32 @@ import {
2020    lastInteractionType , 
2121}  from  './InteractionController.js' ; 
2222
23- const  HOVER_DELAY  =  300 ; 
24- 
2523export  class  HoverController  extends  InteractionController  { 
2624    override  type  =  InteractionTypes . hover ; 
2725
2826    private  elementIds : string [ ]  =  [ ] ; 
2927
30-     focusedin  =  false ; 
28+     private   targetFocused  =  false ; 
3129
3230    private  hoverTimeout ?: ReturnType < typeof  setTimeout > ; 
3331
34-     pointerentered  =  false ; 
32+     private  hovering  =  false ; 
33+ 
34+     private  overlayFocused  =  false ; 
3535
3636    handleKeyup ( event : KeyboardEvent ) : void { 
37-         if  ( event . code  ===  'Tab'   ||   event . code   ===   'Escape' )  { 
37+         if  ( event . code  ===  'Tab' )  { 
3838            this . open  =  true ; 
39+         }  else  if  ( event . code  ===  'Escape' )  { 
40+             if  ( this . open )  { 
41+                 event . preventDefault ( ) ; 
42+                 event . stopPropagation ( ) ; 
43+                 this . open  =  false ; 
44+                 // Return focus to trigger element 
45+                 if  ( this . target )  { 
46+                     this . target . focus ( ) ; 
47+                 } 
48+             } 
3949        } 
4050    } 
4151
@@ -52,23 +62,29 @@ export class HoverController extends InteractionController {
5262        } 
5363
5464        this . open  =  true ; 
55-         this . focusedin  =  true ; 
65+         this . targetFocused  =  true ; 
5666    } 
5767
5868    handleTargetFocusout ( ) : void { 
59-         this . focusedin  =  false ; 
60-         if  ( this . pointerentered )  return ; 
61-         this . open  =  false ; 
69+         this . targetFocused  =  false ; 
70+         // Don't close immediately if pointer is over the content 
71+         if  ( this . hovering )  return ; 
72+         // Use delay to allow focus to move into overlay content 
73+         this . doFocusleave ( ) ; 
6274    } 
6375
64-     handleTargetPointerenter ( ) : void { 
76+     private   clearCloseTimeout ( ) : void { 
6577        if  ( this . hoverTimeout )  { 
6678            clearTimeout ( this . hoverTimeout ) ; 
6779            this . hoverTimeout  =  undefined ; 
6880        } 
81+     } 
82+ 
83+     handleTargetPointerenter ( ) : void { 
84+         this . clearCloseTimeout ( ) ; 
6985        if  ( this . overlay ?. disabled )  return ; 
7086        this . open  =  true ; 
71-         this . pointerentered  =  true ; 
87+         this . hovering  =  true ; 
7288    } 
7389
7490    handleTargetPointerleave ( ) : void { 
@@ -78,16 +94,28 @@ export class HoverController extends InteractionController {
7894    // set a timeout once the pointer enters and the overlay is shown 
7995    // give the user time to enter the overlay 
8096    handleHostPointerenter ( ) : void { 
81-         if  ( this . hoverTimeout )  { 
82-             clearTimeout ( this . hoverTimeout ) ; 
83-             this . hoverTimeout  =  undefined ; 
84-         } 
97+         this . clearCloseTimeout ( ) ; 
8598    } 
8699
87100    handleHostPointerleave ( ) : void { 
88101        this . doPointerleave ( ) ; 
89102    } 
90103
104+     handleOverlayFocusin ( ) : void { 
105+         this . overlayFocused  =  true ; 
106+         // Clear any pending close timeout when focus enters overlay 
107+         this . clearCloseTimeout ( ) ; 
108+     } 
109+ 
110+     handleOverlayFocusout ( ) : void { 
111+         this . overlayFocused  =  false ; 
112+         // Don't close immediately if pointer is over the content or trigger has focus 
113+         if  ( this . hovering )  return ; 
114+         if  ( this . targetFocused  &&  this . target . matches ( ':focus-visible' ) )  return ; 
115+         // Use delay before closing 
116+         this . doFocusleave ( ) ; 
117+     } 
118+ 
91119    override  prepareDescription ( ) : void { 
92120        // require "content" to apply relationship 
93121        if  ( ! this . overlay . elements . length )  return ; 
@@ -138,14 +166,31 @@ export class HoverController extends InteractionController {
138166        } ; 
139167    } 
140168
141-     protected  doPointerleave ( ) : void { 
142-         this . pointerentered  =  false ; 
143-         const  triggerElement  =  this . target  as  HTMLElement ; 
144-         if  ( this . focusedin  &&  triggerElement . matches ( ':focus-visible' ) )  return ; 
145- 
169+     private  scheduleClose ( ) : void { 
146170        this . hoverTimeout  =  setTimeout ( ( )  =>  { 
147171            this . open  =  false ; 
148-         } ,  HOVER_DELAY ) ; 
172+         } ,  300 ) ; 
173+     } 
174+ 
175+     private  doPointerleave ( ) : void { 
176+         this . hovering  =  false ; 
177+         const  triggerElement  =  this . target  as  HTMLElement ; 
178+         if  ( this . targetFocused  &&  triggerElement . matches ( ':focus-visible' ) ) 
179+             return ; 
180+         // Don't close if focus is within overlay content 
181+         if  ( this . overlayFocused )  return ; 
182+ 
183+         this . scheduleClose ( ) ; 
184+     } 
185+ 
186+     private  doFocusleave ( ) : void { 
187+         // Clear any existing timeout 
188+         this . clearCloseTimeout ( ) ; 
189+ 
190+         // Use same delay as pointer interactions for consistency 
191+         if  ( ! this . targetFocused  &&  ! this . overlayFocused  &&  ! this . hovering )  { 
192+             this . scheduleClose ( ) ; 
193+         } 
149194    } 
150195
151196    override  init ( ) : void { 
@@ -198,5 +243,20 @@ export class HoverController extends InteractionController {
198243            ( )  =>  this . handleHostPointerleave ( ) , 
199244            {  signal } 
200245        ) ; 
246+         this . overlay . addEventListener ( 
247+             'focusin' , 
248+             ( )  =>  this . handleOverlayFocusin ( ) , 
249+             {  signal } 
250+         ) ; 
251+         this . overlay . addEventListener ( 
252+             'focusout' , 
253+             ( )  =>  this . handleOverlayFocusout ( ) , 
254+             {  signal } 
255+         ) ; 
256+         this . overlay . addEventListener ( 
257+             'keyup' , 
258+             ( event )  =>  this . handleKeyup ( event ) , 
259+             {  signal } 
260+         ) ; 
201261    } 
202262} 
0 commit comments