@@ -189,7 +189,8 @@ export interface FocusZoneSettings {
189189 */
190190 onActiveDescendantChanged ?: (
191191 newActiveDescendant : HTMLElement | undefined ,
192- previousActiveDescendant : HTMLElement | undefined
192+ previousActiveDescendant : HTMLElement | undefined ,
193+ activatedByKeyboardNavigation : boolean
193194 ) => void
194195
195196 /**
@@ -337,13 +338,13 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
337338 return document . activeElement === activeDescendantControl
338339 }
339340
340- function updateFocusedElement ( to ?: HTMLElement ) {
341+ function updateFocusedElement ( to ?: HTMLElement , causedByKeyboardNavigation = false ) {
341342 const from = currentFocusedElement
342343 currentFocusedElement = to
343344
344345 if ( activeDescendantControl ) {
345346 if ( to && isActiveDescendantInputFocused ( ) ) {
346- setActiveDescendant ( from , to )
347+ setActiveDescendant ( from , to , causedByKeyboardNavigation )
347348 } else {
348349 clearActiveDescendant ( )
349350 }
@@ -358,13 +359,18 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
358359 to ?. setAttribute ( 'tabindex' , '0' )
359360 }
360361
361- function setActiveDescendant ( from : HTMLElement | undefined , to : HTMLElement ) {
362+ function setActiveDescendant ( from : HTMLElement | undefined , to : HTMLElement , activatedByKeyboardNavigation = false ) {
362363 if ( ! to . id ) {
363364 to . setAttribute ( 'id' , uniqueId ( ) )
364365 }
365366
366- activeDescendantControl ?. setAttribute ( 'aria-activedescendant' , to . id )
367- activeDescendantCallback ?.( to , from === to ? undefined : from )
367+ if ( ! activeDescendantControl || activeDescendantControl . getAttribute ( 'aria-activedescendant' ) === to . id ) {
368+ // prevent active descendant callback from being called repeatedly if the same element is activated (e.g. via mousemove)
369+ return
370+ }
371+
372+ activeDescendantControl . setAttribute ( 'aria-activedescendant' , to . id )
373+ activeDescendantCallback ?.( to , from === to ? undefined : from , activatedByKeyboardNavigation )
368374 }
369375
370376 function clearActiveDescendant ( previouslyActiveElement = currentFocusedElement ) {
@@ -373,7 +379,7 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
373379 }
374380
375381 activeDescendantControl ?. removeAttribute ( 'aria-activedescendant' )
376- activeDescendantCallback ?.( undefined , previouslyActiveElement )
382+ activeDescendantCallback ?.( undefined , previouslyActiveElement , false )
377383 }
378384
379385 function beginFocusManagement ( ...elements : HTMLElement [ ] ) {
@@ -484,6 +490,23 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
484490 updateFocusedElement ( event . target )
485491 }
486492 } )
493+ container . addEventListener (
494+ 'mousemove' ,
495+ ( { target} ) => {
496+ if ( ! ( target instanceof HTMLElement ) ) {
497+ return
498+ }
499+
500+ const focusableElement = focusableElements . find ( element => element . contains ( target ) )
501+
502+ if ( focusableElement ) {
503+ updateFocusedElement ( focusableElement )
504+ }
505+ } ,
506+ { signal, capture : true }
507+ )
508+
509+ // Listeners specifically on the controlling element
487510 activeDescendantControl . addEventListener ( 'focusin' , ( ) => {
488511 // Focus moved into the active descendant input. Activate current or first descendant.
489512 if ( ! currentFocusedElement ) {
@@ -639,9 +662,11 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
639662
640663 if ( nextElementToFocus ) {
641664 if ( activeDescendantControl ) {
642- updateFocusedElement ( nextElementToFocus )
665+ updateFocusedElement ( nextElementToFocus , true )
643666 } else {
644667 lastKeyboardFocusDirection = direction
668+
669+ // updateFocusedElement will be called implicitly when focus moves, as long as the event isn't prevented somehow
645670 nextElementToFocus . focus ( )
646671 }
647672 }
0 commit comments