@@ -189,7 +189,8 @@ export interface FocusZoneSettings {
189189 */
190190 onActiveDescendantChanged ?: (
191191 newActiveDescendant : HTMLElement | undefined ,
192- previousActiveDescendant : HTMLElement | undefined
192+ previousActiveDescendant : HTMLElement | undefined ,
193+ directlyActivated : boolean
193194 ) => void
194195
195196 /**
@@ -311,6 +312,10 @@ function shouldIgnoreFocusHandling(keyboardEvent: KeyboardEvent, activeElement:
311312 return false
312313}
313314
315+ export const isActiveDescendantAttribute = 'data-is-active-descendant'
316+ export const activeDescendantActivatedDirectly = 'activated-directly'
317+ export const activeDescendantActivatedIndirectly = 'activated-indirectly'
318+
314319/**
315320 * Sets up the arrow key focus behavior for all focusable elements in the given `container`.
316321 * @param container
@@ -337,13 +342,13 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
337342 return document . activeElement === activeDescendantControl
338343 }
339344
340- function updateFocusedElement ( to ?: HTMLElement ) {
345+ function updateFocusedElement ( to ?: HTMLElement , directlyActivated = false ) {
341346 const from = currentFocusedElement
342347 currentFocusedElement = to
343348
344349 if ( activeDescendantControl ) {
345350 if ( to && isActiveDescendantInputFocused ( ) ) {
346- setActiveDescendant ( from , to )
351+ setActiveDescendant ( from , to , directlyActivated )
347352 } else {
348353 clearActiveDescendant ( )
349354 }
@@ -358,13 +363,29 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
358363 to ?. setAttribute ( 'tabindex' , '0' )
359364 }
360365
361- function setActiveDescendant ( from : HTMLElement | undefined , to : HTMLElement ) {
366+ function setActiveDescendant ( from : HTMLElement | undefined , to : HTMLElement , directlyActivated = false ) {
362367 if ( ! to . id ) {
363368 to . setAttribute ( 'id' , uniqueId ( ) )
364369 }
365370
366- activeDescendantControl ?. setAttribute ( 'aria-activedescendant' , to . id )
367- activeDescendantCallback ?.( to , from === to ? undefined : from )
371+ if ( from && from !== to ) {
372+ from . removeAttribute ( isActiveDescendantAttribute )
373+ }
374+
375+ if (
376+ ! activeDescendantControl ||
377+ ( ! directlyActivated && activeDescendantControl . getAttribute ( 'aria-activedescendant' ) === to . id )
378+ ) {
379+ // prevent active descendant callback from being called repeatedly if the same element is activated (e.g. via mousemove)
380+ return
381+ }
382+
383+ activeDescendantControl . setAttribute ( 'aria-activedescendant' , to . id )
384+ to . setAttribute (
385+ isActiveDescendantAttribute ,
386+ directlyActivated ? activeDescendantActivatedDirectly : activeDescendantActivatedIndirectly
387+ )
388+ activeDescendantCallback ?.( to , from , directlyActivated )
368389 }
369390
370391 function clearActiveDescendant ( previouslyActiveElement = currentFocusedElement ) {
@@ -373,7 +394,8 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
373394 }
374395
375396 activeDescendantControl ?. removeAttribute ( 'aria-activedescendant' )
376- activeDescendantCallback ?.( undefined , previouslyActiveElement )
397+ previouslyActiveElement ?. removeAttribute ( isActiveDescendantAttribute )
398+ activeDescendantCallback ?.( undefined , previouslyActiveElement , false )
377399 }
378400
379401 function beginFocusManagement ( ...elements : HTMLElement [ ] ) {
@@ -484,6 +506,23 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
484506 updateFocusedElement ( event . target )
485507 }
486508 } )
509+ container . addEventListener (
510+ 'mousemove' ,
511+ ( { target} ) => {
512+ if ( ! ( target instanceof HTMLElement ) ) {
513+ return
514+ }
515+
516+ const focusableElement = focusableElements . find ( element => element . contains ( target ) )
517+
518+ if ( focusableElement ) {
519+ updateFocusedElement ( focusableElement )
520+ }
521+ } ,
522+ { signal, capture : true }
523+ )
524+
525+ // Listeners specifically on the controlling element
487526 activeDescendantControl . addEventListener ( 'focusin' , ( ) => {
488527 // Focus moved into the active descendant input. Activate current or first descendant.
489528 if ( ! currentFocusedElement ) {
@@ -637,13 +676,13 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings):
637676 }
638677 }
639678
640- if ( nextElementToFocus ) {
641- if ( activeDescendantControl ) {
642- updateFocusedElement ( nextElementToFocus )
643- } else {
644- lastKeyboardFocusDirection = direction
645- nextElementToFocus . focus ( )
646- }
679+ if ( activeDescendantControl ) {
680+ updateFocusedElement ( nextElementToFocus || currentFocusedElement , true )
681+ } else if ( nextElementToFocus ) {
682+ lastKeyboardFocusDirection = direction
683+
684+ // updateFocusedElement will be called implicitly when focus moves, as long as the event isn't prevented somehow
685+ nextElementToFocus . focus ( )
647686 }
648687 // Tab should always allow escaping from this container, so only
649688 // preventDefault if tab key press already resulted in a focus movement
0 commit comments