@@ -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,17 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
484490 updateFocusedElement ( event . target )
485491 }
486492 } )
493+ container . addEventListener (
494+ 'mousemove' ,
495+ event => {
496+ if ( event . target instanceof HTMLElement && focusableElements . includes ( event . target ) ) {
497+ updateFocusedElement ( event . target )
498+ }
499+ } ,
500+ { signal, capture : true }
501+ )
502+
503+ // Listeners specifically on the controlling element
487504 activeDescendantControl . addEventListener ( 'focusin' , ( ) => {
488505 // Focus moved into the active descendant input. Activate current or first descendant.
489506 if ( ! currentFocusedElement ) {
@@ -639,9 +656,11 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
639656
640657 if ( nextElementToFocus ) {
641658 if ( activeDescendantControl ) {
642- updateFocusedElement ( nextElementToFocus )
659+ updateFocusedElement ( nextElementToFocus , true )
643660 } else {
644661 lastKeyboardFocusDirection = direction
662+
663+ // updateFocusedElement will be called implicitly when focus moves, as long as the event isn't prevented somehow
645664 nextElementToFocus . focus ( )
646665 }
647666 }
0 commit comments