@@ -65,6 +65,7 @@ interface IFocusContext {
6565}
6666
6767const FocusContext = React . createContext < IFocusContext | null > ( null ) ;
68+ const RESTORE_FOCUS_EVENT = 'react-aria-focus-scope-restore' ;
6869
6970let activeScope : ScopeRef = null ;
7071
@@ -117,12 +118,21 @@ export function FocusScope(props: FocusScopeProps) {
117118 // Find all rendered nodes between the sentinels and add them to the scope.
118119 let node = startRef . current ?. nextSibling ! ;
119120 let nodes : Element [ ] = [ ] ;
121+ let stopPropagation = e => e . stopPropagation ( ) ;
120122 while ( node && node !== endRef . current ) {
121123 nodes . push ( node as Element ) ;
124+ // Stop custom restore focus event from propagating to parent focus scopes.
125+ node . addEventListener ( RESTORE_FOCUS_EVENT , stopPropagation ) ;
122126 node = node . nextSibling as Element ;
123127 }
124128
125129 scopeRef . current = nodes ;
130+
131+ return ( ) => {
132+ for ( let node of nodes ) {
133+ node . removeEventListener ( RESTORE_FOCUS_EVENT , stopPropagation ) ;
134+ }
135+ } ;
126136 } , [ children ] ) ;
127137
128138 useActiveScopeTracker ( scopeRef , restoreFocus , contain ) ;
@@ -470,7 +480,7 @@ function focusElement(element: FocusableElement | null, scroll = false) {
470480 }
471481}
472482
473- function focusFirstInScope ( scope : Element [ ] , tabbable : boolean = true ) {
483+ function getFirstInScope ( scope : Element [ ] , tabbable = true ) {
474484 let sentinel = scope [ 0 ] . previousElementSibling ! ;
475485 let scopeRoot = getScopeRoot ( scope ) ;
476486 let walker = getFocusableTreeWalker ( scopeRoot , { tabbable} , scope ) ;
@@ -485,7 +495,11 @@ function focusFirstInScope(scope: Element[], tabbable:boolean = true) {
485495 nextNode = walker . nextNode ( ) ;
486496 }
487497
488- focusElement ( nextNode as FocusableElement ) ;
498+ return nextNode as FocusableElement ;
499+ }
500+
501+ function focusFirstInScope ( scope : Element [ ] , tabbable :boolean = true ) {
502+ focusElement ( getFirstInScope ( scope , tabbable ) ) ;
489503}
490504
491505function useAutoFocus ( scopeRef : RefObject < Element [ ] > , autoFocus ?: boolean ) {
@@ -692,7 +706,7 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus?: boolean,
692706 let treeNode = clonedTree . getTreeNode ( scopeRef ) ;
693707 while ( treeNode ) {
694708 if ( treeNode . nodeToRestore && treeNode . nodeToRestore . isConnected ) {
695- focusElement ( treeNode . nodeToRestore ) ;
709+ restoreFocusToElement ( treeNode . nodeToRestore ) ;
696710 return ;
697711 }
698712 treeNode = treeNode . parent ;
@@ -703,7 +717,8 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus?: boolean,
703717 treeNode = clonedTree . getTreeNode ( scopeRef ) ;
704718 while ( treeNode ) {
705719 if ( treeNode . scopeRef && treeNode . scopeRef . current && focusScopeTree . getTreeNode ( treeNode . scopeRef ) ) {
706- focusFirstInScope ( treeNode . scopeRef . current , true ) ;
720+ let node = getFirstInScope ( treeNode . scopeRef . current , true ) ;
721+ restoreFocusToElement ( node ) ;
707722 return ;
708723 }
709724 treeNode = treeNode . parent ;
@@ -715,6 +730,15 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus?: boolean,
715730 } , [ scopeRef , restoreFocus ] ) ;
716731}
717732
733+ function restoreFocusToElement ( node : FocusableElement ) {
734+ // Dispatch a custom event that parent elements can intercept to customize focus restoration.
735+ // For example, virtualized collection components reuse DOM elements, so the original element
736+ // might still exist in the DOM but representing a different item.
737+ if ( node . dispatchEvent ( new CustomEvent ( RESTORE_FOCUS_EVENT , { bubbles : true , cancelable : true } ) ) ) {
738+ focusElement ( node ) ;
739+ }
740+ }
741+
718742/**
719743 * Create a [TreeWalker]{@link https://developer.mozilla.org/en-US/docs/Web/API/TreeWalker}
720744 * that matches all focusable/tabbable elements.
0 commit comments