@@ -60,6 +60,7 @@ export class FocusManager {
6060 registeredTrees : Array < IFocusableTree > = [ ] ;
6161
6262 private currentlyHoldsEphemeralFocus : boolean = false ;
63+ private lockFocusStateChanges : boolean = false ;
6364
6465 constructor (
6566 addGlobalEventListener : ( type : string , listener : EventListener ) => void ,
@@ -89,7 +90,16 @@ export class FocusManager {
8990 }
9091
9192 if ( newNode ) {
92- this . focusNode ( newNode ) ;
93+ const newTree = newNode . getFocusableTree ( ) ;
94+ const oldTree = this . focusedNode ?. getFocusableTree ( ) ;
95+ if ( newNode === newTree . getRootFocusableNode ( ) && newTree !== oldTree ) {
96+ // If the root of the tree is the one taking focus (such as due to
97+ // being tabbed), try to focus the whole tree explicitly to ensure the
98+ // correct node re-receives focus.
99+ this . focusTree ( newTree ) ;
100+ } else {
101+ this . focusNode ( newNode ) ;
102+ }
93103 } else {
94104 this . defocusCurrentFocusedNode ( ) ;
95105 }
@@ -108,6 +118,7 @@ export class FocusManager {
108118 * certain whether the tree has been registered.
109119 */
110120 registerTree ( tree : IFocusableTree ) : void {
121+ this . ensureManagerIsUnlocked ( ) ;
111122 if ( this . isRegistered ( tree ) ) {
112123 throw Error ( `Attempted to re-register already registered tree: ${ tree } .` ) ;
113124 }
@@ -133,6 +144,7 @@ export class FocusManager {
133144 * this manager.
134145 */
135146 unregisterTree ( tree : IFocusableTree ) : void {
147+ this . ensureManagerIsUnlocked ( ) ;
136148 if ( ! this . isRegistered ( tree ) ) {
137149 throw Error ( `Attempted to unregister not registered tree: ${ tree } .` ) ;
138150 }
@@ -192,11 +204,14 @@ export class FocusManager {
192204 * focus.
193205 */
194206 focusTree ( focusableTree : IFocusableTree ) : void {
207+ this . ensureManagerIsUnlocked ( ) ;
195208 if ( ! this . isRegistered ( focusableTree ) ) {
196209 throw Error ( `Attempted to focus unregistered tree: ${ focusableTree } .` ) ;
197210 }
198211 const currNode = FocusableTreeTraverser . findFocusedNode ( focusableTree ) ;
199- this . focusNode ( currNode ?? focusableTree . getRootFocusableNode ( ) ) ;
212+ const nodeToRestore = focusableTree . getRestoredFocusableNode ( currNode ) ;
213+ const rootFallback = focusableTree . getRootFocusableNode ( ) ;
214+ this . focusNode ( nodeToRestore ?? currNode ?? rootFallback ) ;
200215 }
201216
202217 /**
@@ -205,18 +220,37 @@ export class FocusManager {
205220 * Any previously focused node will be updated to be passively highlighted (if
206221 * it's in a different focusable tree) or blurred (if it's in the same one).
207222 *
208- * @param focusableNode The node that should receive active
209- * focus.
223+ * @param focusableNode The node that should receive active focus.
210224 */
211225 focusNode ( focusableNode : IFocusableNode ) : void {
226+ this . ensureManagerIsUnlocked ( ) ;
227+ if ( this . focusedNode == focusableNode ) return ; // State is unchanged.
228+
212229 const nextTree = focusableNode . getFocusableTree ( ) ;
213230 if ( ! this . isRegistered ( nextTree ) ) {
214231 throw Error ( `Attempted to focus unregistered node: ${ focusableNode } .` ) ;
215232 }
233+
234+ // Safety check for ensuring focusNode() doesn't get called for a node that
235+ // isn't actually hooked up to its parent tree correctly (since this can
236+ // cause weird inconsistencies).
237+ const matchedNode = FocusableTreeTraverser . findFocusableNodeFor (
238+ focusableNode . getFocusableElement ( ) ,
239+ nextTree ,
240+ ) ;
241+ if ( matchedNode !== focusableNode ) {
242+ throw Error (
243+ `Attempting to focus node which isn't recognized by its parent tree: ` +
244+ `${ focusableNode } .` ,
245+ ) ;
246+ }
247+
216248 const prevNode = this . focusedNode ;
217- if ( prevNode && prevNode . getFocusableTree ( ) !== nextTree ) {
218- this . setNodeToPassive ( prevNode ) ;
249+ const prevTree = prevNode ?. getFocusableTree ( ) ;
250+ if ( prevNode && prevTree !== nextTree ) {
251+ this . passivelyFocusNode ( prevNode , nextTree ) ;
219252 }
253+
220254 // If there's a focused node in the new node's tree, ensure it's reset.
221255 const prevNodeNextTree = FocusableTreeTraverser . findFocusedNode ( nextTree ) ;
222256 const nextTreeRoot = nextTree . getRootFocusableNode ( ) ;
@@ -229,9 +263,10 @@ export class FocusManager {
229263 if ( nextTreeRoot !== focusableNode ) {
230264 this . removeHighlight ( nextTreeRoot ) ;
231265 }
266+
232267 if ( ! this . currentlyHoldsEphemeralFocus ) {
233268 // Only change the actively focused node if ephemeral state isn't held.
234- this . setNodeToActive ( focusableNode ) ;
269+ this . activelyFocusNode ( focusableNode , prevTree ?? null ) ;
235270 }
236271 this . focusedNode = focusableNode ;
237272 }
@@ -257,6 +292,7 @@ export class FocusManager {
257292 takeEphemeralFocus (
258293 focusableElement : HTMLElement | SVGElement ,
259294 ) : ReturnEphemeralFocus {
295+ this . ensureManagerIsUnlocked ( ) ;
260296 if ( this . currentlyHoldsEphemeralFocus ) {
261297 throw Error (
262298 `Attempted to take ephemeral focus when it's already held, ` +
@@ -266,7 +302,7 @@ export class FocusManager {
266302 this . currentlyHoldsEphemeralFocus = true ;
267303
268304 if ( this . focusedNode ) {
269- this . setNodeToPassive ( this . focusedNode ) ;
305+ this . passivelyFocusNode ( this . focusedNode , null ) ;
270306 }
271307 focusableElement . focus ( ) ;
272308
@@ -282,29 +318,66 @@ export class FocusManager {
282318 this . currentlyHoldsEphemeralFocus = false ;
283319
284320 if ( this . focusedNode ) {
285- this . setNodeToActive ( this . focusedNode ) ;
321+ this . activelyFocusNode ( this . focusedNode , null ) ;
286322 }
287323 } ;
288324 }
289325
326+ private ensureManagerIsUnlocked ( ) : void {
327+ if ( this . lockFocusStateChanges ) {
328+ throw Error (
329+ 'FocusManager state changes cannot happen in a tree/node focus/blur ' +
330+ 'callback.' ,
331+ ) ;
332+ }
333+ }
334+
290335 private defocusCurrentFocusedNode ( ) : void {
291336 // The current node will likely be defocused while ephemeral focus is held,
292337 // but internal manager state shouldn't change since the node should be
293338 // restored upon exiting ephemeral focus mode.
294339 if ( this . focusedNode && ! this . currentlyHoldsEphemeralFocus ) {
295- this . setNodeToPassive ( this . focusedNode ) ;
340+ this . passivelyFocusNode ( this . focusedNode , null ) ;
296341 this . focusedNode = null ;
297342 }
298343 }
299344
300- private setNodeToActive ( node : IFocusableNode ) : void {
345+ private activelyFocusNode (
346+ node : IFocusableNode ,
347+ prevTree : IFocusableTree | null ,
348+ ) : void {
349+ // Note that order matters here. Focus callbacks are allowed to change
350+ // element visibility which can influence focusability, including for a
351+ // node's focusable element (which *is* allowed to be invisible until the
352+ // node needs to be focused).
353+ this . lockFocusStateChanges = true ;
354+ node . getFocusableTree ( ) . onTreeFocus ( node , prevTree ) ;
355+ node . onNodeFocus ( ) ;
356+ this . lockFocusStateChanges = false ;
357+
358+ this . setNodeToVisualActiveFocus ( node ) ;
359+ node . getFocusableElement ( ) . focus ( ) ;
360+ }
361+
362+ private passivelyFocusNode (
363+ node : IFocusableNode ,
364+ nextTree : IFocusableTree | null ,
365+ ) : void {
366+ this . lockFocusStateChanges = true ;
367+ node . getFocusableTree ( ) . onTreeBlur ( nextTree ) ;
368+ node . onNodeBlur ( ) ;
369+ this . lockFocusStateChanges = false ;
370+
371+ this . setNodeToVisualPassiveFocus ( node ) ;
372+ }
373+
374+ private setNodeToVisualActiveFocus ( node : IFocusableNode ) : void {
301375 const element = node . getFocusableElement ( ) ;
302376 dom . addClass ( element , FocusManager . ACTIVE_FOCUS_NODE_CSS_CLASS_NAME ) ;
303377 dom . removeClass ( element , FocusManager . PASSIVE_FOCUS_NODE_CSS_CLASS_NAME ) ;
304- element . focus ( ) ;
305378 }
306379
307- private setNodeToPassive ( node : IFocusableNode ) : void {
380+ private setNodeToVisualPassiveFocus ( node : IFocusableNode ) : void {
308381 const element = node . getFocusableElement ( ) ;
309382 dom . removeClass ( element , FocusManager . ACTIVE_FOCUS_NODE_CSS_CLASS_NAME ) ;
310383 dom . addClass ( element , FocusManager . PASSIVE_FOCUS_NODE_CSS_CLASS_NAME ) ;
0 commit comments