@@ -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,21 @@ 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 (
368+ ! activeDescendantControl ||
369+ ( ! activatedByKeyboardNavigation && activeDescendantControl . getAttribute ( 'aria-activedescendant' ) === to . id )
370+ ) {
371+ // prevent active descendant callback from being called repeatedly if the same element is activated (e.g. via mousemove)
372+ return
373+ }
374+
375+ activeDescendantControl . setAttribute ( 'aria-activedescendant' , to . id )
376+ activeDescendantCallback ?.( to , from , activatedByKeyboardNavigation )
368377 }
369378
370379 function clearActiveDescendant ( previouslyActiveElement = currentFocusedElement ) {
@@ -373,7 +382,7 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
373382 }
374383
375384 activeDescendantControl ?. removeAttribute ( 'aria-activedescendant' )
376- activeDescendantCallback ?.( undefined , previouslyActiveElement )
385+ activeDescendantCallback ?.( undefined , previouslyActiveElement , false )
377386 }
378387
379388 function beginFocusManagement ( ...elements : HTMLElement [ ] ) {
@@ -484,6 +493,23 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
484493 updateFocusedElement ( event . target )
485494 }
486495 } )
496+ container . addEventListener (
497+ 'mousemove' ,
498+ ( { target} ) => {
499+ if ( ! ( target instanceof HTMLElement ) ) {
500+ return
501+ }
502+
503+ const focusableElement = focusableElements . find ( element => element . contains ( target ) )
504+
505+ if ( focusableElement ) {
506+ updateFocusedElement ( focusableElement )
507+ }
508+ } ,
509+ { signal, capture : true }
510+ )
511+
512+ // Listeners specifically on the controlling element
487513 activeDescendantControl . addEventListener ( 'focusin' , ( ) => {
488514 // Focus moved into the active descendant input. Activate current or first descendant.
489515 if ( ! currentFocusedElement ) {
@@ -637,13 +663,13 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
637663 }
638664 }
639665
640- if ( nextElementToFocus ) {
641- if ( activeDescendantControl ) {
642- updateFocusedElement ( nextElementToFocus )
643- } else {
644- lastKeyboardFocusDirection = direction
645- nextElementToFocus . focus ( )
646- }
666+ if ( activeDescendantControl ) {
667+ updateFocusedElement ( nextElementToFocus || currentFocusedElement , true )
668+ } else if ( nextElementToFocus ) {
669+ lastKeyboardFocusDirection = direction
670+
671+ // updateFocusedElement will be called implicitly when focus moves, as long as the event isn't prevented somehow
672+ nextElementToFocus . focus ( )
647673 }
648674 // Tab should always allow escaping from this container, so only
649675 // preventDefault if tab key press already resulted in a focus movement
0 commit comments