Repository: google/blockly
Pull request #9079: feat: Automatically manage focus tree tab indexes
Author: login: BenHenning
Labels: name: PR: feature
description: Adds a feature
Created: 2025-05-20T23:08:36Z | Merged: 2025-05-29T19:10:00Z

## The basics

- [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change)

## The details
### Resolves

Fixes #8965
Fixes #8978
Fixes #8970
Fixes https://github.com/google/blockly-keyboard-experimentation/issues/523
Fixes https://github.com/google/blockly-keyboard-experimentation/issues/547
Fixes part of #8910

### Proposed Changes

Fives groups of changes are included in this PR:
1. Support for automatic tab index management for focusable trees.
2. Support for automatic tab index management for focusable nodes.
3. Support for automatically hiding the flyout when back navigating from the toolbox.
4. A fix for `FocusManager` losing DOM syncing that was introduced in #9082.
5. Some cleanups for flyout and some tests for previous behavior changes to `FocusManager`.

### Reason for Changes

Infrastructure changes reasoning:
- Automatically managing tab indexes for both focusable trees and roots can largely reduce the difficulty of providing focusable nodes/trees and generally interacting with `FocusManager`. This facilitates a more automated navigation experience.
- The fix for losing DOM syncing is possibly not reliable, but there are at least now tests to cover for it. This may be a case where a `try{} finally{}` could be warranted, but the code will stay as-is unless requested otherwise.

`Flyout` changes:
- `Flyout` no longer needs to be a focusable tree, but removing that would be an API breakage. Instead, it throws for most of the normal tree/node calls as it should no longer be used as such. Instead, its workspace has been made top-level tabbable (in addition to the  main workspace) which solves the extra tab stop issues and general confusing inconsistencies between the flyout, toolbox, and workspace.
- `Flyout` now correctly auto-selects the first block (#9103 notwithstanding). Technically it did before, however the extra `Flyout` tabstop before its workspace caused the inconsistency (since focusing the `Flyout` itself did not auto-select, only selecting its workspace did).

Important caveats:
- `getAttribute` is used in place of directly fetching `.tabIndex` since the latter can apparently default to `-1` (and possibly `0`) in cases when it's not actually set. This is a very surprising behavior that leads to incorrect test results.
- Sometimes tab index still needs to be introduced (such as in cases where native DOM focus is needed, e.g. via `focus()` calls or clicking). This is demonstrated both by updates to `FocusManager`'s tests as well as toolbox's category and separator. This can be slightly tricky to miss as large parts of Blockly now depend on focus to represent their state, so clicking either needs to be managed by Blockly (with corresponding `focusNode` calls) or automatic (with a tab index defined for the element that can be clicked, or which has a child that can be clicked).

Note that nearly all elements used for testing focus in the test `index.html` page have had their tab indexes removed to lean on `FocusManager`'s automatic tab management (though as mentioned above there is still some manual tab index management required for `focus()`-specific tests).

### Test Coverage

New tests were added for all of the updated behaviors to `FocusManager`, including a new need to explicitly provide (and reset) tab indexes for all `focus()`-esque tests. This also includes adding new tests for some behaviors introduced in past PRs (a la #8910).

Note that all of the new and affected conditionals in `FocusManager` have been verified as having at least 1 test that breaks when it's removed (inverted conditions weren't thoroughly tested, but it's expected that they should also be well covered now).

Additional tests to cover the actual navigation flows will be added to the keyboard experimentation plugin repository as part of https://github.com/google/blockly-keyboard-experimentation/pull/557 (this PR needs to be merged first).

For manual testing, I mainly verified keyboard navigation with some cursory mouse & click testing in the simple playground. @rachel-fenichel also performed more thorough mouse & click testing (that yielded an actual issue that was fixed--see discussion below).

The core webdriver tests have been verified to have seemingly the same existing failures with and without these changes.

All of the following new keyboard navigation plugin tests have been verified as failing without the fixes introduced in this branch (and passing with them):
- `Tab navigating to flyout should auto-select first block`
- `Keyboard nav to different toolbox category should auto-select first block`
- `Keyboard nav to different toolbox category and block should select different block`
- `Tab navigate away from toolbox restores focus to initial element`
- `Tab navigate away from toolbox closes flyout`
- `Tab navigate away from flyout to toolbox and away closes flyout`
- `Tabbing to the workspace after selecting flyout block should close the flyout`
- `Tabbing to the workspace after selecting flyout block via workspace toolbox shortcut should close the flyout`
- `Tabbing back from workspace should reopen the flyout`
- `Navigation position in workspace should be retained when tabbing to flyout and back`
- `Clicking outside Blockly with focused toolbox closes the flyout`
- `Clicking outside Blockly with focused flyout closes the flyout`
- `Clicking on toolbox category focuses it and opens flyout`

### Documentation

No documentation changes are needed beyond the code doc changes included in the PR.

### Additional Information

An additional PR will be introduced for the keyboard experimentation plugin repository to add tests there (see test coverage above). This description will be updated with a link to that PR once it exists.

Linked issue #8965: Shift+Tab after keyboard navigating to a block doesn’t open flyout as expected
### Check for duplicates

- [ ] I have searched for similar issues before opening a new one.

### Description

Shift+Tab after keyboard navigating to a block doesn’t open flyout as expected. There seems to be an extra workspace tab stop that causes the workspace outline to appear.

Ben: this is not a simple fix, and probably needs discussion. The workspace is a browser-level tab stop that we have some control on overriding to switch focus, but it's unclear if the user wants to be able to navigate back to the workspace. If we want to provide that functionality, it has to stay as a tabstop. We do override first-time focus on the workspace to select the first block (if one is present), but it is possible to navigate back to the workspace via shift-tab as described.


### Reproduction steps

1. 
2. 
3.


### Stack trace

```shell

```

### Screenshots

_No response_

### Browsers

_No response_
Linked issue comments: user: login: maribethb
created_at: 2025-05-09T16:30:44Z
body: Needs more discussion -- some disagreement over whether the workspace should be a tab stop between the flyout and block
author_association: CONTRIBUTOR
user: login: maribethb
created_at: 2025-05-09T16:31:04Z
body: I think the workspace should remain a valid tab stop
author_association: CONTRIBUTOR
user: login: rachel-fenichel
created_at: 2025-05-09T16:35:42Z
body: @microbit-matt-hillsdon and @kmcnaught to discuss--Maribeth disagrees with the request (see above).
author_association: COLLABORATOR
user: login: BenHenning
created_at: 2025-05-12T20:18:35Z
body: In https://github.com/google/blockly-keyboard-experimentation/pull/511#issuecomment-2873831020 I mentioned some thoughts on this. Specifically this bit:

> > * Extra tab stop issue for the workspace
> 
> Just to check, this is only for shift tabbing once in the workspace, correct? If so, this will require more discussion as there may be an explicit need to be able to tab to the workspace (to open the workspace context menu with redundancy/consistency with regular keyboard navigation).
> 
> There's a small chance a proper long-term fix here would require breaking API changes. The unknown is how to let `WorkspaceSvg` signal to `FocusManager` what to do when the root is focused (which we have no control over--this is a DOM event). In some contexts we want to auto-focus a node, but in others we want to either focus the workspace or skip it. This could get complicated.
author_association: COLLABORATOR
user: login: microbit-matt-hillsdon
created_at: 2025-05-14T13:23:33Z
body: At the moment if you tab to the workspace en route to the toolbox/flyout you lose your cursor position as it moves to the workspace which is unlikely to be what you want.

I think the workspace position should be more like a special block position and not itself accessible via tab.

I've been using [this branch](https://alt-passive-focus.blockly-keyboard-experimentation.pages.dev/) to visualise the current behaviour more clearly with a custom focus ring for the workspace when it's the active tree and a selection-style ring when its the active node.

Perhaps this plan could require making something like that latter ring represent the workspace in the DOM similarly to a block.
author_association: COLLABORATOR
user: login: microbit-matt-hillsdon
created_at: 2025-05-14T14:03:25Z
body: FWIW I think it's OK for it not to open the flyout, understandable given its transient nature. But I do think the behaviour we used to have (still in deployed demo at time of writing) where you could shift tab back to the toolbox was really helpful.
author_association: COLLABORATOR
user: login: BenHenning
created_at: 2025-05-15T20:56:19Z
body: Idea: From an implementation perspective, one way to customize or change tab behavior might be to follow the implementation of #9049 and move away from tab stops entirely in favor of overriding tab behavior directly and using the navigation + focus systems.

I'm not entirely sure what the screen reader implications of that are yet (tabindex gives potential for other ways to traverse a page than actual tabbing for instance), and it will require us to back to the idea of global keyboard intercepting for navigation rather than tying it to specific Blockly components when they have focus (which I have mixed feelings about).
author_association: COLLABORATOR
user: login: BenHenning
created_at: 2025-05-20T23:12:26Z
body: I do want to further discuss the shift+tab opening the flyout a bit more. https://github.com/google/blockly/pull/9079 demonstrates a solution for fixing the tabbing back to toolbox behavior specifically, but automatically opening the flyout is extra tricky in this case (and I'm not certain is 100% predictable--we don't automatically focus the flyout when tabbing in but, rather, it's the case that it opens incidentally from focusing the toolbox which now makes it tab-focusable).
author_association: COLLABORATOR
user: login: kmcnaught
created_at: 2025-05-22T09:12:59Z
body: I'm happy with Shift+Tab not opening the flyout. 

I'm not really very happy with Shift+Tab losing the cursor position you selected. 
author_association: NONE
user: login: BenHenning
created_at: 2025-05-22T18:13:47Z
body: > I'm happy with Shift+Tab not opening the flyout.
> 
> I'm not really very happy with Shift+Tab losing the cursor position you selected.

It's the case that fixing the extra tab stop hop does default back to a good "restore" behavior when the user returns to the workspace, so position is properly kept per my local testing.
author_association: COLLABORATOR

Diff:
diff --git a/core/bubbles/bubble.ts b/core/bubbles/bubble.ts
index 64060fe7888..20e730abb18 100644
--- a/core/bubbles/bubble.ts
+++ b/core/bubbles/bubble.ts
@@ -98,8 +98,8 @@ export abstract class Bubble implements IBubble, ISelectable {
    *     when automatically positioning.
    * @param overriddenFocusableElement An optional replacement to the focusable
    *     element that's represented by this bubble (as a focusable node). This
-   *     element will have its ID and tabindex overwritten. If not provided, the
-   *     focusable element of this node will default to the bubble's SVG root.
+   *     element will have its ID overwritten. If not provided, the focusable
+   *     element of this node will default to the bubble's SVG root.
    */
   constructor(
     public readonly workspace: WorkspaceSvg,
@@ -138,7 +138,6 @@ export abstract class Bubble implements IBubble, ISelectable {
 
     this.focusableElement = overriddenFocusableElement ?? this.svgRoot;
     this.focusableElement.setAttribute('id', this.id);
-    this.focusableElement.setAttribute('tabindex', '-1');
 
     browserEvents.conditionalBind(
       this.background,
diff --git a/core/comments/rendered_workspace_comment.ts b/core/comments/rendered_workspace_comment.ts
index 00359b07011..3a3d57a441d 100644
--- a/core/comments/rendered_workspace_comment.ts
+++ b/core/comments/rendered_workspace_comment.ts
@@ -65,7 +65,6 @@ export class RenderedWorkspaceComment
     this.view.setEditable(this.isEditable());
     this.view.getSvgRoot().setAttribute('data-id', this.id);
     this.view.getSvgRoot().setAttribute('id', this.id);
-    this.view.getSvgRoot().setAttribute('tabindex', '-1');
 
     this.addModelUpdateBindings();
 
diff --git a/core/field.ts b/core/field.ts
index f7e01527e5d..c4b6514785e 100644
--- a/core/field.ts
+++ b/core/field.ts
@@ -312,7 +312,6 @@ export abstract class Field<T = any>
     const id = this.id_;
     if (!id) throw new Error('Expected ID to be defined prior to init.');
     this.fieldGroup_ = dom.createSvgElement(Svg.G, {
-      'tabindex': '-1',
       'id': id,
     });
     if (!this.isVisible()) {
diff --git a/core/flyout_base.ts b/core/flyout_base.ts
index 9f94ec30905..492d3341762 100644
--- a/core/flyout_base.ts
+++ b/core/flyout_base.ts
@@ -22,7 +22,6 @@ import {FlyoutItem} from './flyout_item.js';
 import {FlyoutMetricsManager} from './flyout_metrics_manager.js';
 import {FlyoutNavigator} from './flyout_navigator.js';
 import {FlyoutSeparator, SeparatorAxis} from './flyout_separator.js';
-import {getFocusManager} from './focus_manager.js';
 import {IAutoHideable} from './interfaces/i_autohideable.js';
 import type {IFlyout} from './interfaces/i_flyout.js';
 import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js';
@@ -308,7 +307,6 @@ export abstract class Flyout
     // hide/show code will set up proper visibility and size later.
     this.svgGroup_ = dom.createSvgElement(tagName, {
       'class': 'blocklyFlyout',
-      'tabindex': '0',
     });
     this.svgGroup_.style.display = 'none';
     this.svgBackground_ = dom.createSvgElement(
@@ -324,8 +322,6 @@ export abstract class Flyout
       .getThemeManager()
       .subscribe(this.svgBackground_, 'flyoutOpacity', 'fill-opacity');
 
-    getFocusManager().registerTree(this);
-
     return this.svgGroup_;
   }
 
@@ -407,7 +403,6 @@ export abstract class Flyout
     if (this.svgGroup_) {
       dom.removeNode(this.svgGroup_);
     }
-    getFocusManager().unregisterTree(this);
   }
 
   /**
@@ -971,15 +966,22 @@ export abstract class Flyout
     return null;
   }
 
-  /** See IFocusableNode.getFocusableElement. */
+  /**
+   * See IFocusableNode.getFocusableElement.
+   *
+   * @deprecated v12: Use the Flyout's workspace for focus operations, instead.
+   */
   getFocusableElement(): HTMLElement | SVGElement {
-    if (!this.svgGroup_) throw new Error('Flyout DOM is not yet created.');
-    return this.svgGroup_;
+    throw new Error('Flyouts are not directly focusable.');
   }
 
-  /** See IFocusableNode.getFocusableTree. */
+  /**
+   * See IFocusableNode.getFocusableTree.
+   *
+   * @deprecated v12: Use the Flyout's workspace for focus operations, instead.
+   */
   getFocusableTree(): IFocusableTree {
-    return this;
+    throw new Error('Flyouts are not directly focusable.');
   }
 
   /** See IFocusableNode.onNodeFocus. */
@@ -990,31 +992,45 @@ export abstract class Flyout
 
   /** See IFocusableNode.canBeFocused. */
   canBeFocused(): boolean {
-    return true;
+    return false;
   }
 
-  /** See IFocusableTree.getRootFocusableNode. */
+  /**
+   * See IFocusableNode.getRootFocusableNode.
+   *
+   * @deprecated v12: Use the Flyout's workspace for focus operations, instead.
+   */
   getRootFocusableNode(): IFocusableNode {
-    return this;
+    throw new Error('Flyouts are not directly focusable.');
   }
 
-  /** See IFocusableTree.getRestoredFocusableNode. */
+  /**
+   * See IFocusableNode.getRestoredFocusableNode.
+   *
+   * @deprecated v12: Use the Flyout's workspace for focus operations, instead.
+   */
   getRestoredFocusableNode(
     _previousNode: IFocusableNode | null,
   ): IFocusableNode | null {
-    return null;
+    throw new Error('Flyouts are not directly focusable.');
   }
 
-  /** See IFocusableTree.getNestedTrees. */
+  /**
+   * See IFocusableNode.getNestedTrees.
+   *
+   * @deprecated v12: Use the Flyout's workspace for focus operations, instead.
+   */
   getNestedTrees(): Array<IFocusableTree> {
-    return [this.workspace_];
+    throw new Error('Flyouts are not directly focusable.');
   }
 
-  /** See IFocusableTree.lookUpFocusableNode. */
+  /**
+   * See IFocusableNode.lookUpFocusableNode.
+   *
+   * @deprecated v12: Use the Flyout's workspace for focus operations, instead.
+   */
   lookUpFocusableNode(_id: string): IFocusableNode | null {
-    // No focusable node needs to be returned since the flyout's subtree is a
-    // workspace that will manage its own focusable state.
-    return null;
+    throw new Error('Flyouts are not directly focusable.');
   }
 
   /** See IFocusableTree.onTreeFocus. */
@@ -1023,15 +1039,12 @@ export abstract class Flyout
     _previousTree: IFocusableTree | null,
   ): void {}
 
-  /** See IFocusableTree.onTreeBlur. */
-  onTreeBlur(nextTree: IFocusableTree | null): void {
-    const toolbox = this.targetWorkspace.getToolbox();
-    // If focus is moving to either the toolbox or the flyout's workspace, do
-    // not close the flyout. For anything else, do close it since the flyout is
-    // no longer focused.
-    if (toolbox && nextTree === toolbox) return;
-    if (nextTree === this.workspace_) return;
-    if (toolbox) toolbox.clearSelection();
-    this.autoHide(false);
+  /**
+   * See IFocusableNode.onTreeBlur.
+   *
+   * @deprecated v12: Use the Flyout's workspace for focus operations, instead.
+   */
+  onTreeBlur(_nextTree: IFocusableTree | null): void {
+    throw new Error('Flyouts are not directly focusable.');
   }
 }
diff --git a/core/flyout_button.ts b/core/flyout_button.ts
index 823b57be765..c9afb8b0159 100644
--- a/core/flyout_button.ts
+++ b/core/flyout_button.ts
@@ -113,7 +113,7 @@ export class FlyoutButton
     this.id = idGenerator.getNextUniqueId();
     this.svgGroup = dom.createSvgElement(
       Svg.G,
-      {'id': this.id, 'class': cssClass, 'tabindex': '-1'},
+      {'id': this.id, 'class': cssClass},
       this.workspace.getCanvas(),
     );
 
diff --git a/core/focus_manager.ts b/core/focus_manager.ts
index 198e1f0747d..01be4813f90 100644
--- a/core/focus_manager.ts
+++ b/core/focus_manager.ts
@@ -17,6 +17,24 @@ import {FocusableTreeTraverser} from './utils/focusable_tree_traverser.js';
  */
 export type ReturnEphemeralFocus = () => void;
 
+/**
+ * Represents an IFocusableTree that has been registered for focus management in
+ * FocusManager.
+ */
+class TreeRegistration {
+  /**
+   * Constructs a new TreeRegistration.
+   *
+   * @param tree The tree being registered.
+   * @param rootShouldBeAutoTabbable Whether the tree should have automatic
+   *     top-level tab management.
+   */
+  constructor(
+    readonly tree: IFocusableTree,
+    readonly rootShouldBeAutoTabbable: boolean,
+  ) {}
+}
+
 /**
  * A per-page singleton that manages Blockly focus across one or more
  * IFocusableTrees, and bidirectionally synchronizes this focus with the DOM.
@@ -58,7 +76,7 @@ export class FocusManager {
 
   private focusedNode: IFocusableNode | null = null;
   private previouslyFocusedNode: IFocusableNode | null = null;
-  private registeredTrees: Array<IFocusableTree> = [];
+  private registeredTrees: Array<TreeRegistration> = [];
 
   private currentlyHoldsEphemeralFocus: boolean = false;
   private lockFocusStateChanges: boolean = false;
@@ -79,7 +97,8 @@ export class FocusManager {
         // If the target losing or gaining focus maps to any tree, then it
         // should be updated. Per the contract of findFocusableNodeFor only one
         // tree should claim the element, so the search can be exited early.
-        for (const tree of this.registeredTrees) {
+        for (const reg of this.registeredTrees) {
+          const tree = reg.tree;
           newNode = FocusableTreeTraverser.findFocusableNodeFor(element, tree);
           if (newNode) break;
         }
@@ -132,13 +151,32 @@ export class FocusManager {
    * This function throws if the provided tree is already currently registered
    * in this manager. Use isRegistered to check in cases when it can't be
    * certain whether the tree has been registered.
+   *
+   * The tree's registration can be customized to configure automatic tab stops.
+   * This specifically provides capability for the user to be able to tab
+   * navigate to the root of the tree but only when the tree doesn't hold active
+   * focus. If this functionality is disabled then the tree's root will
+   * automatically be made focusable (but not tabbable) when it is first focused
+   * in the same way as any other focusable node.
+   *
+   * @param tree The IFocusableTree to register.
+   * @param rootShouldBeAutoTabbable Whether the root of this tree should be
+   *     added as a top-level page tab stop when it doesn't hold active focus.
    */
-  registerTree(tree: IFocusableTree): void {
+  registerTree(
+    tree: IFocusableTree,
+    rootShouldBeAutoTabbable: boolean = false,
+  ): void {
     this.ensureManagerIsUnlocked();
     if (this.isRegistered(tree)) {
       throw Error(`Attempted to re-register already registered tree: ${tree}.`);
     }
-    this.registeredTrees.push(tree);
+    this.registeredTrees.push(
+      new TreeRegistration(tree, rootShouldBeAutoTabbable),
+    );
+    if (rootShouldBeAutoTabbable) {
+      tree.getRootFocusableNode().getFocusableElement().tabIndex = 0;
+    }
   }
 
   /**
@@ -147,7 +185,15 @@ export class FocusManager {
    * unregisterTree.
    */
   isRegistered(tree: IFocusableTree): boolean {
-    return this.registeredTrees.findIndex((reg) => reg === tree) !== -1;
+    return !!this.lookUpRegistration(tree);
+  }
+
+  /**
+   * Returns the TreeRegistration for the specified tree, or null if the tree is
+   * not currently registered.
+   */
+  private lookUpRegistration(tree: IFocusableTree): TreeRegistration | null {
+    return this.registeredTrees.find((reg) => reg.tree === tree) ?? null;
   }
 
   /**
@@ -158,13 +204,19 @@ export class FocusManager {
    *
    * This function throws if the provided tree is not currently registered in
    * this manager.
+   *
+   * This function will reset the tree's root element tabindex if the tree was
+   * registered with automatic tab management.
    */
   unregisterTree(tree: IFocusableTree): void {
     this.ensureManagerIsUnlocked();
     if (!this.isRegistered(tree)) {
       throw Error(`Attempted to unregister not registered tree: ${tree}.`);
     }
-    const treeIndex = this.registeredTrees.findIndex((reg) => reg === tree);
+    const treeIndex = this.registeredTrees.findIndex(
+      (reg) => reg.tree === tree,
+    );
+    const registration = this.registeredTrees[treeIndex];
     this.registeredTrees.splice(treeIndex, 1);
 
     const focusedNode = FocusableTreeTraverser.findFocusedNode(tree);
@@ -174,6 +226,13 @@ export class FocusManager {
       this.updateFocusedNode(null);
     }
     this.removeHighlight(root);
+
+    if (registration.rootShouldBeAutoTabbable) {
+      tree
+        .getRootFocusableNode()
+        .getFocusableElement()
+        .removeAttribute('tabindex');
+    }
   }
 
   /**
@@ -240,11 +299,15 @@ export class FocusManager {
    * canBeFocused() method returns false), it will be ignored and any existing
    * focus state will remain unchanged.
    *
+   * Note that this may update the specified node's element's tabindex to ensure
+   * that it can be properly read out by screenreaders while focused.
+   *
    * @param focusableNode The node that should receive active focus.
    */
   focusNode(focusableNode: IFocusableNode): void {
     this.ensureManagerIsUnlocked();
-    if (!this.currentlyHoldsEphemeralFocus) {
+    const mustRestoreUpdatingNode = !this.currentlyHoldsEphemeralFocus;
+    if (mustRestoreUpdatingNode) {
       // Disable state syncing from DOM events since possible calls to focus()
       // below will loop a call back to focusNode().
       this.isUpdatingFocusedNode = true;
@@ -258,12 +321,21 @@ export class FocusManager {
     const prevFocusedElement = this.focusedNode?.getFocusableElement();
     const hasDesyncedState = prevFocusedElement !== document.activeElement;
     if (this.focusedNode === focusableNode && !hasDesyncedState) {
+      if (mustRestoreUpdatingNode) {
+        // Reenable state syncing from DOM events.
+        this.isUpdatingFocusedNode = false;
+      }
       return; // State is unchanged.
     }
 
     if (!focusableNode.canBeFocused()) {
       // This node can't be focused.
       console.warn("Trying to focus a node that can't be focused.");
+
+      if (mustRestoreUpdatingNode) {
+        // Reenable state syncing from DOM events.
+        this.isUpdatingFocusedNode = false;
+      }
       return;
     }
 
@@ -312,7 +384,7 @@ export class FocusManager {
       this.activelyFocusNode(nodeToFocus, prevTree ?? null);
     }
     this.updateFocusedNode(nodeToFocus);
-    if (!this.currentlyHoldsEphemeralFocus) {
+    if (mustRestoreUpdatingNode) {
       // Reenable state syncing from DOM events.
       this.isUpdatingFocusedNode = false;
     }
@@ -448,14 +520,38 @@ export class FocusManager {
     // node's focusable element (which *is* allowed to be invisible until the
     // node needs to be focused).
     this.lockFocusStateChanges = true;
-    if (node.getFocusableTree() !== prevTree) {
-      node.getFocusableTree().onTreeFocus(node, prevTree);
+    const tree = node.getFocusableTree();
+    const elem = node.getFocusableElement();
+    const nextTreeReg = this.lookUpRegistration(tree);
+    const treeIsTabManaged = nextTreeReg?.rootShouldBeAutoTabbable;
+    if (tree !== prevTree) {
+      tree.onTreeFocus(node, prevTree);
+
+      if (treeIsTabManaged) {
+        // If this node's tree has its tab auto-managed, ensure that it's no
+        // longer tabbable now that it holds active focus.
+        tree.getRootFocusableNode().getFocusableElement().tabIndex = -1;
+      }
     }
     node.onNodeFocus();
     this.lockFocusStateChanges = false;
 
+    // The tab index should be set in all cases where:
+    // - It doesn't overwrite an pre-set tab index for the node.
+    // - The node is part of a tree whose tab index is unmanaged.
+    // OR
+    // - The node is part of a managed tree but this isn't the root. Managed
+    //   roots are ignored since they are always overwritten to have a tab index
+    //   of -1 with active focus so that they cannot be tab navigated.
+    //
+    // Setting the tab index ensures that the node's focusable element can
+    // actually receive DOM focus.
+    if (!treeIsTabManaged || node !== tree.getRootFocusableNode()) {
+      if (!elem.hasAttribute('tabindex')) elem.tabIndex = -1;
+    }
+
     this.setNodeToVisualActiveFocus(node);
-    node.getFocusableElement().focus();
+    elem.focus();
   }
 
   /**
@@ -475,13 +571,21 @@ export class FocusManager {
     nextTree: IFocusableTree | null,
   ): void {
     this.lockFocusStateChanges = true;
-    if (node.getFocusableTree() !== nextTree) {
-      node.getFocusableTree().onTreeBlur(nextTree);
+    const tree = node.getFocusableTree();
+    if (tree !== nextTree) {
+      tree.onTreeBlur(nextTree);
+
+      const reg = this.lookUpRegistration(tree);
+      if (reg?.rootShouldBeAutoTabbable) {
+        // If this node's tree has its tab auto-managed, ensure that it's now
+        // tabbable since it no longer holds active focus.
+        tree.getRootFocusableNode().getFocusableElement().tabIndex = 0;
+      }
     }
     node.onNodeBlur();
     this.lockFocusStateChanges = false;
 
-    if (node.getFocusableTree() !== nextTree) {
+    if (tree !== nextTree) {
       this.setNodeToVisualPassiveFocus(node);
     }
   }
diff --git a/core/icons/icon.ts b/core/icons/icon.ts
index 67547ee313e..8f8ff70fc32 100644
--- a/core/icons/icon.ts
+++ b/core/icons/icon.ts
@@ -59,7 +59,6 @@ export abstract class Icon implements IIcon {
     const svgBlock = this.sourceBlock as BlockSvg;
     this.svgRoot = dom.createSvgElement(Svg.G, {
       'class': 'blocklyIconGroup',
-      'tabindex': '-1',
       'id': this.id,
     });
     svgBlock.getSvgRoot().appendChild(this.svgRoot);
diff --git a/core/interfaces/i_focusable_node.ts b/core/interfaces/i_focusable_node.ts
index b21d7741a5c..00557168afa 100644
--- a/core/interfaces/i_focusable_node.ts
+++ b/core/interfaces/i_focusable_node.ts
@@ -19,13 +19,11 @@ export interface IFocusableNode {
    * - blocklyActiveFocus
    * - blocklyPassiveFocus
    *
-   * The returned element must also have a valid ID specified, and unique across
-   * the entire page. Failing to have a properly unique ID could result in
-   * trying to focus one node (such as via a mouse click) leading to another
-   * node with the same ID actually becoming focused by FocusManager. The
-   * returned element must also have a negative tabindex (since the focus
-   * manager itself will manage its tab index and a tab index must be present in
-   * order for the element to be focusable in the DOM).
+   * The returned element must also have a valid ID specified, and this ID
+   * should be unique across the entire page. Failing to have a properly unique
+   * ID could result in trying to focus one node (such as via a mouse click)
+   * leading to another node with the same ID actually becoming focused by
+   * FocusManager.
    *
    * The returned element must be visible if the node is ever focused via
    * FocusManager.focusNode() or FocusManager.focusTree(). It's allowed for an
@@ -34,7 +32,11 @@ export interface IFocusableNode {
    *
    * It's expected the actual returned element will not change for the lifetime
    * of the node (that is, its properties can change but a new element should
-   * never be returned).
+   * never be returned). Also, the returned element will have its tabindex
+   * overwritten throughout the lifecycle of this node and FocusManager.
+   *
+   * If a node requires the ability to be focused directly without first being
+   * focused via FocusManager then it must set its own tab index.
    *
    * @returns The HTMLElement or SVGElement which can both receive focus and be
    *     visually represented as actively or passively focused for this node.
diff --git a/core/renderers/common/path_object.ts b/core/renderers/common/path_object.ts
index 7efc6318a31..f6291b9f0fa 100644
--- a/core/renderers/common/path_object.ts
+++ b/core/renderers/common/path_object.ts
@@ -50,7 +50,7 @@ export class PathObject implements IPathObject {
     /** The primary path of the block. */
     this.svgPath = dom.createSvgElement(
       Svg.PATH,
-      {'class': 'blocklyPath', 'tabindex': '-1'},
+      {'class': 'blocklyPath'},
       this.svgRoot,
     );
 
@@ -239,7 +239,6 @@ export class PathObject implements IPathObject {
         'id': connection.id,
         'class': 'blocklyHighlightedConnectionPath',
         'style': 'display: none;',
-        'tabindex': '-1',
         'd': connectionPath,
         'transform': transformation,
       },
diff --git a/core/toolbox/category.ts b/core/toolbox/category.ts
index fc7d1aa03cf..7b0db7b3fcd 100644
--- a/core/toolbox/category.ts
+++ b/core/toolbox/category.ts
@@ -225,6 +225,8 @@ export class ToolboxCategory
    */
   protected createContainer_(): HTMLDivElement {
     const container = document.createElement('div');
+    // Ensure that the category has a tab index to ensure it receives focus when
+    // clicked (since clicking isn't managed by the toolbox).
     container.tabIndex = -1;
     container.id = this.getId();
     const className = this.cssConfig_['container'];
diff --git a/core/toolbox/separator.ts b/core/toolbox/separator.ts
index 44ae358cf53..cd5ed245a04 100644
--- a/core/toolbox/separator.ts
+++ b/core/toolbox/separator.ts
@@ -54,6 +54,8 @@ export class ToolboxSeparator extends ToolboxItem {
    */
   protected createDom_(): HTMLDivElement {
     const container = document.createElement('div');
+    // Ensure that the separator has a tab index to ensure it receives focus
+    // when clicked (since clicking isn't managed by the toolbox).
     container.tabIndex = -1;
     container.id = this.getId();
     const className = this.cssConfig_['container'];
diff --git a/core/toolbox/toolbox.ts b/core/toolbox/toolbox.ts
index 0fbb231dc56..57e849ce264 100644
--- a/core/toolbox/toolbox.ts
+++ b/core/toolbox/toolbox.ts
@@ -22,7 +22,10 @@ import '../events/events_toolbox_item_select.js';
 import {EventType} from '../events/type.js';
 import * as eventUtils from '../events/utils.js';
 import {getFocusManager} from '../focus_manager.js';
-import type {IAutoHideable} from '../interfaces/i_autohideable.js';
+import {
+  isAutoHideable,
+  type IAutoHideable,
+} from '../interfaces/i_autohideable.js';
 import type {ICollapsibleToolboxItem} from '../interfaces/i_collapsible_toolbox_item.js';
 import {isDeletable} from '../interfaces/i_deletable.js';
 import type {IDraggable} from '../interfaces/i_draggable.js';
@@ -169,7 +172,7 @@ export class Toolbox
         ComponentManager.Capability.DRAG_TARGET,
       ],
     });
-    getFocusManager().registerTree(this);
+    getFocusManager().registerTree(this, true);
   }
 
   /**
@@ -200,7 +203,6 @@ export class Toolbox
    */
   protected createContainer_(): HTMLDivElement {
     const toolboxContainer = document.createElement('div');
-    toolboxContainer.tabIndex = 0;
     toolboxContainer.setAttribute('layout', this.isHorizontal() ? 'h' : 'v');
     dom.addClass(toolboxContainer, 'blocklyToolbox');
     toolboxContainer.setAttribute('dir', this.RTL ? 'RTL' : 'LTR');
@@ -1142,7 +1144,16 @@ export class Toolbox
   }
 
   /** See IFocusableTree.onTreeBlur. */
-  onTreeBlur(_nextTree: IFocusableTree | null): void {}
+  onTreeBlur(nextTree: IFocusableTree | null): void {
+    // If navigating to anything other than the toolbox's flyout then clear the
+    // selection so that the toolbox's flyout can automatically close.
+    if (!nextTree || nextTree !== this.flyout?.getWorkspace()) {
+      this.clearSelection();
+      if (this.flyout && isAutoHideable(this.flyout)) {
+        this.flyout.autoHide(false);
+      }
+    }
+  }
 }
 
 /** CSS for Toolbox.  See css.js for use. */
diff --git a/core/workspace_svg.ts b/core/workspace_svg.ts
index 3e8731afd4b..5d5a40ccc5f 100644
--- a/core/workspace_svg.ts
+++ b/core/workspace_svg.ts
@@ -762,8 +762,6 @@ export class WorkspaceSvg
      */
     this.svgGroup_ = dom.createSvgElement(Svg.G, {
       'class': 'blocklyWorkspace',
-      // Only the top-level workspace should be tabbable.
-      'tabindex': injectionDiv ? '0' : '-1',
       'id': this.id,
     });
     if (injectionDiv) {
@@ -849,7 +847,8 @@ export class WorkspaceSvg
       isParentWorkspace ? this.getInjectionDiv() : undefined,
     );
 
-    getFocusManager().registerTree(this);
+    // Only the top-level and flyout workspaces should be tabbable.
+    getFocusManager().registerTree(this, !!this.injectionDiv || this.isFlyout);
 
     return this.svgGroup_;
   }
@@ -2807,13 +2806,12 @@ export class WorkspaceSvg
   /** See IFocusableTree.onTreeBlur. */
   onTreeBlur(nextTree: IFocusableTree | null): void {
     // If the flyout loses focus, make sure to close it unless focus is being
-    // lost to a different element on the page.
-    if (nextTree && this.isFlyout && this.targetWorkspace) {
+    // lost to the toolbox.
+    if (this.isFlyout && this.targetWorkspace) {
       // Only hide the flyout if the flyout's workspace is losing focus and that
       // focus isn't returning to the flyout itself or the toolbox.
       const flyout = this.targetWorkspace.getFlyout();
       const toolbox = this.targetWorkspace.getToolbox();
-      if (flyout && nextTree === flyout) return;
       if (toolbox && nextTree === toolbox) return;
       if (toolbox) toolbox.clearSelection();
       if (flyout && isAutoHideable(flyout)) flyout.autoHide(false);
diff --git a/tests/mocha/focus_manager_test.js b/tests/mocha/focus_manager_test.js
index cd89d1351b2..3a1fc98a7e5 100644
--- a/tests/mocha/focus_manager_test.js
+++ b/tests/mocha/focus_manager_test.js
@@ -80,81 +80,86 @@ suite('FocusManager', function () {
   const ACTIVE_FOCUS_NODE_CSS_SELECTOR = `.${FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME}`;
   const PASSIVE_FOCUS_NODE_CSS_SELECTOR = `.${FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME}`;
 
-  const createFocusableTree = function (rootElementId, nestedTrees) {
-    return new FocusableTreeImpl(
-      document.getElementById(rootElementId),
-      nestedTrees || [],
-    );
-  };
-  const createFocusableNode = function (tree, elementId) {
-    return tree.addNode(document.getElementById(elementId));
-  };
-
   setup(function () {
     sharedTestSetup.call(this);
-
     this.focusManager = getFocusManager();
 
-    this.testFocusableTree1 = createFocusableTree('testFocusableTree1');
-    this.testFocusableTree1Node1 = createFocusableNode(
+    this.allFocusableTrees = [];
+    this.allFocusableNodes = [];
+    this.createFocusableTree = function (rootElementId, nestedTrees) {
+      const tree = new FocusableTreeImpl(
+        document.getElementById(rootElementId),
+        nestedTrees || [],
+      );
+      this.allFocusableTrees.push(tree);
+      return tree;
+    };
+    this.createFocusableNode = function (tree, elementId) {
+      const node = tree.addNode(document.getElementById(elementId));
+      this.allFocusableNodes.push(node);
+      return node;
+    };
+
+    this.testFocusableTree1 = this.createFocusableTree('testFocusableTree1');
+    this.testFocusableTree1Node1 = this.createFocusableNode(
       this.testFocusableTree1,
       'testFocusableTree1.node1',
     );
-    this.testFocusableTree1Node1Child1 = createFocusableNode(
+    this.testFocusableTree1Node1Child1 = this.createFocusableNode(
       this.testFocusableTree1,
       'testFocusableTree1.node1.child1',
     );
-    this.testFocusableTree1Node2 = createFocusableNode(
+    this.testFocusableTree1Node2 = this.createFocusableNode(
       this.testFocusableTree1,
       'testFocusableTree1.node2',
     );
-    this.testFocusableNestedTree4 = createFocusableTree(
+    this.testFocusableNestedTree4 = this.createFocusableTree(
       'testFocusableNestedTree4',
     );
-    this.testFocusableNestedTree4Node1 = createFocusableNode(
+    this.testFocusableNestedTree4Node1 = this.createFocusableNode(
       this.testFocusableNestedTree4,
       'testFocusableNestedTree4.node1',
     );
-    this.testFocusableNestedTree5 = createFocusableTree(
+    this.testFocusableNestedTree5 = this.createFocusableTree(
       'testFocusableNestedTree5',
     );
-    this.testFocusableNestedTree5Node1 = createFocusableNode(
+    this.testFocusableNestedTree5Node1 = this.createFocusableNode(
       this.testFocusableNestedTree5,
       'testFocusableNestedTree5.node1',
     );
-    this.testFocusableTree2 = createFocusableTree('testFocusableTree2', [
+    this.testFocusableTree2 = this.createFocusableTree('testFocusableTree2', [
       this.testFocusableNestedTree4,
       this.testFocusableNestedTree5,
     ]);
-    this.testFocusableTree2Node1 = createFocusableNode(
+    this.testFocusableTree2Node1 = this.createFocusableNode(
       this.testFocusableTree2,
       'testFocusableTree2.node1',
     );
 
-    this.testFocusableGroup1 = createFocusableTree('testFocusableGroup1');
-    this.testFocusableGroup1Node1 = createFocusableNode(
+    this.testFocusableGroup1 = this.createFocusableTree('testFocusableGroup1');
+    this.testFocusableGroup1Node1 = this.createFocusableNode(
       this.testFocusableGroup1,
       'testFocusableGroup1.node1',
     );
-    this.testFocusableGroup1Node1Child1 = createFocusableNode(
+    this.testFocusableGroup1Node1Child1 = this.createFocusableNode(
       this.testFocusableGroup1,
       'testFocusableGroup1.node1.child1',
     );
-    this.testFocusableGroup1Node2 = createFocusableNode(
+    this.testFocusableGroup1Node2 = this.createFocusableNode(
       this.testFocusableGroup1,
       'testFocusableGroup1.node2',
     );
-    this.testFocusableNestedGroup4 = createFocusableTree(
+    this.testFocusableNestedGroup4 = this.createFocusableTree(
       'testFocusableNestedGroup4',
     );
-    this.testFocusableNestedGroup4Node1 = createFocusableNode(
+    this.testFocusableNestedGroup4Node1 = this.createFocusableNode(
       this.testFocusableNestedGroup4,
       'testFocusableNestedGroup4.node1',
     );
-    this.testFocusableGroup2 = createFocusableTree('testFocusableGroup2', [
+    this.testFocusableGroup2 = this.createFocusableTree('testFocusableGroup2', [
       this.testFocusableNestedGroup4,
     ]);
-    this.testFocusableGroup2Node1 = createFocusableNode(
+    this.testFocusableGroup2Node1 = this.createFocusableNode(
       this.testFocusableGroup2,
       'testFocusableGroup2.node1',
     );
@@ -177,6 +182,19 @@ suite('FocusManager', function () {
       elem.classList.remove(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME);
     }
 
+    // Ensure any set tab indexes are properly reset between tests.
+    for (const tree of this.allFocusableTrees) {
+      tree
+        .getRootFocusableNode()
+        .getFocusableElement()
+        .removeAttribute('tabindex');
+    }
+    for (const node of this.allFocusableNodes) {
+      node.getFocusableElement().removeAttribute('tabindex');
+    }
+    this.allFocusableTrees = [];
+    this.allFocusableNodes = [];
+
     // Reset the current active element.
     document.body.focus();
   });
@@ -230,6 +248,44 @@ suite('FocusManager', function () {
 
       // The second register should not fail since the tree was previously unregistered.
     });
+
+    test('for unmanaged tree does not overwrite tab index', function () {
+      this.focusManager.registerTree(this.testFocusableTree1, false);
+
+      const rootNode = this.testFocusableTree1.getRootFocusableNode();
+      const rootElem = rootNode.getFocusableElement();
+      assert.isNull(rootElem.getAttribute('tabindex'));
+    });
+
+    test('for unmanaged tree with custom tab index does not overwrite tab index', function () {
+      const rootNode = this.testFocusableTree1.getRootFocusableNode();
+      const rootElem = rootNode.getFocusableElement();
+      rootElem.tabIndex = -1;
+
+      this.focusManager.registerTree(this.testFocusableTree1, false);
+
+      // The custom tab index shouldn't be overwritten for an unmanaged tree.
+      assert.strictEqual(rootElem.getAttribute('tabindex'), '-1');
+    });
+
+    test('for managed tree overwrites root tab index to be tab navigable', function () {
+      this.focusManager.registerTree(this.testFocusableTree1, true);
+
+      const rootNode = this.testFocusableTree1.getRootFocusableNode();
+      const rootElem = rootNode.getFocusableElement();
+      assert.strictEqual(rootElem.getAttribute('tabindex'), '0');
+    });
+
+    test('for managed tree with custom tab index overwrites root tab index to be tab navigable', function () {
+      const rootNode = this.testFocusableTree1.getRootFocusableNode();
+      const rootElem = rootNode.getFocusableElement();
+      rootElem.tabIndex = -1;
+
+      this.focusManager.registerTree(this.testFocusableTree1, true);
+
+      // A custom tab index should be overwritten for a managed tree.
+      assert.strictEqual(rootElem.getAttribute('tabindex'), '0');
+    });
   });
 
   suite('unregisterTree()', function () {
@@ -259,6 +315,41 @@ suite('FocusManager', function () {
         errorMsgRegex,
       );
     });
+
+    test('for unmanaged tree with custom tab index does not change tab index', function () {
+      this.focusManager.registerTree(this.testFocusableTree1, false);
+      const rootNode = this.testFocusableTree1.getRootFocusableNode();
+      const rootElem = rootNode.getFocusableElement();
+      rootElem.tabIndex = -1;
+
+      this.focusManager.unregisterTree(this.testFocusableTree1);
+
+      // Unregistering an unmanaged tree shouldn't change its tab index.
+      assert.strictEqual(rootElem.getAttribute('tabindex'), '-1');
+    });
+
+    test('for managed tree removes tab index', function () {
+      this.focusManager.registerTree(this.testFocusableTree1, true);
+
+      this.focusManager.unregisterTree(this.testFocusableTree1);
+
+      // Unregistering a managed tree should remove its tab index.
+      const rootNode = this.testFocusableTree1.getRootFocusableNode();
+      const rootElem = rootNode.getFocusableElement();
+      assert.isNull(rootElem.getAttribute('tabindex'));
+    });
+
+    test('for managed tree with custom tab index removes tab index', function () {
+      this.focusManager.registerTree(this.testFocusableTree1, true);
+      const rootNode = this.testFocusableTree1.getRootFocusableNode();
+      const rootElem = rootNode.getFocusableElement();
+      rootElem.tabIndex = -1;
+
+      this.focusManager.unregisterTree(this.testFocusableTree1);
+
+      // Unregistering a managed tree should remove its tab index.
+      assert.isNull(rootElem.getAttribute('tabindex'));
+    });
   });
 
   suite('isRegistered()', function () {
@@ -330,6 +421,17 @@ suite('FocusManager', function () {
 
       assert.isNull(focusedNode);
     });
+
+    test('after focusing unfocusable node returns null', function () {
+      this.testFocusableTree1Node1.canBeFocused = () => false;
+      this.focusManager.registerTree(this.testFocusableTree1);
+      this.focusManager.focusNode(this.testFocusableTree1Node1);
+
+      const focusedNode = this.focusManager.getFocusedNode();
+
+      // Unfocusable nodes should not be focused.
+      assert.isNull(focusedNode);
+    });
   });
 
   suite('focusTree()', function () {
@@ -353,6 +455,15 @@ suite('FocusManager', function () {
     });
   });
 
+  test('unfocused node does not have a tab index by default', function () {
+    const elem = this.testFocusableTree1Node1.getFocusableElement();
+
+    // This is slightly testing the test setup, but it acts as a precondition sanity test for the
+    // other tab index tests below. Important: 'getAttribute' is used here since direct access to
+    // 'tabIndex' can default the value returned even when the tab index isn't set.
+    assert.isNull(elem.getAttribute('tabindex'));
+  });
+
   suite('focusNode()', function () {
     test('for not registered node throws', function () {
       const errorMsgRegex = /Attempted to focus unregistered node.+?/;
@@ -504,6 +615,210 @@ suite('FocusManager', function () {
 
       assert.strictEqual(this.testFocusableTree1.onTreeBlur.callCount, 1);
     });
+
+    test('for same node twice calls onNodeFocus once', function () {
+      sinon.spy(this.testFocusableTree1Node1, 'onNodeFocus');
+      this.focusManager.registerTree(this.testFocusableTree1);
+      this.focusManager.focusNode(this.testFocusableTree1Node1);
+
+      // Call focus for the same node a second time.
+      this.focusManager.focusNode(this.testFocusableTree1Node1);
+
+      // Despite two calls to focus the node should only focus once.
+      assert.strictEqual(this.testFocusableTree1Node1.onNodeFocus.callCount, 1);
+    });
+
+    test('for unfocusable node does not call onNodeFocus', function () {
+      sinon.spy(this.testFocusableTree1Node1, 'onNodeFocus');
+      this.testFocusableTree1Node1.canBeFocused = () => false;
+      this.focusManager.registerTree(this.testFocusableTree1);
+
+      this.focusManager.focusNode(this.testFocusableTree1Node1);
+
+      // Unfocusable nodes should not be focused, nor have their callbacks called.
+      assert.strictEqual(this.testFocusableTree1Node1.onNodeFocus.callCount, 0);
+    });
+
+    test('for unfocused node overwrites tab index', function () {
+      this.focusManager.registerTree(this.testFocusableTree1);
+
+      this.focusManager.focusNode(this.testFocusableTree1Node1);
+
+      // Focusing an element should overwrite its tab index.
+      const elem = this.testFocusableTree1Node1.getFocusableElement();
+      assert.strictEqual(elem.getAttribute('tabindex'), '-1');
+    });
+
+    test('for previously focused node keeps new tab index', function () {
+      this.focusManager.registerTree(this.testFocusableTree1);
+      this.focusManager.focusNode(this.testFocusableTree1Node1);
+
+      this.focusManager.focusNode(this.testFocusableTree1Node2);
+
+      // The previously focused element should retain its tab index.
+      const elem = this.testFocusableTree1Node1.getFocusableElement();
+      assert.strictEqual(elem.getAttribute('tabindex'), '-1');
+    });
+
+    test('for node with custom tab index does not change tab index', function () {
+      this.focusManager.registerTree(this.testFocusableTree1);
+      const elem = this.testFocusableTree1Node1.getFocusableElement();
+      elem.tabIndex = 0;
+
+      this.focusManager.focusNode(this.testFocusableTree1Node1);
+
+      // If the node already has a tab index set then it should retain that index.
+      assert.strictEqual(elem.getAttribute('tabindex'), '0');
+    });
+
+    suite('for unmanaged tree', function () {
+      test('focused root overwrites tab index', function () {
+        this.focusManager.registerTree(this.testFocusableTree1, false);
+        const rootNode = this.testFocusableTree1.getRootFocusableNode();
+
+        this.focusManager.focusNode(rootNode);
+
+        // Focusing an unmanaged tree's root should overwrite its tab index.
+        const rootElem = rootNode.getFocusableElement();
+        assert.strictEqual(rootElem.getAttribute('tabindex'), '-1');
+      });
+
+      test('focused root with custom tab index does not change tab index', function () {
+        this.focusManager.registerTree(this.testFocusableTree1, false);
+        const rootNode = this.testFocusableTree1.getRootFocusableNode();
+        const rootElem = rootNode.getFocusableElement();
+        rootElem.tabIndex = 0;
+
+        this.focusManager.focusNode(rootNode);
+
+        // If the node already has a tab index set then it should retain that index.
+        assert.strictEqual(rootElem.getAttribute('tabindex'), '0');
+      });
+
+      test('focused node in a tree after unmanaged was focused should keep previous root unchanged', function () {
+        this.focusManager.registerTree(this.testFocusableTree1, false);
+        this.focusManager.registerTree(this.testFocusableTree2, false);
+        const rootNode = this.testFocusableTree1.getRootFocusableNode();
+        this.focusManager.focusNode(this.testFocusableTree1Node1);
+
+        this.focusManager.focusNode(this.testFocusableTree2Node1);
+
+        // Focusing a different tree shouldn't change the root of the previous tree if it's unmanaged.
+        const rootElem = rootNode.getFocusableElement();
+        assert.isNull(rootElem.getAttribute('tabindex'));
+      });
+
+      test('focused node in a tree after unmanaged was root focused should make previous root tab navigable', function () {
+        this.focusManager.registerTree(this.testFocusableTree1, false);
+        this.focusManager.registerTree(this.testFocusableTree2, false);
+        const rootNode = this.testFocusableTree1.getRootFocusableNode();
+        this.focusManager.focusTree(this.testFocusableTree1);
+
+        this.focusManager.focusNode(this.testFocusableTree2Node1);
+
+        // The previous tree's root should be kept unchanged (since it was managed).
+        const rootElem = rootNode.getFocusableElement();
+        assert.strictEqual(rootElem.getAttribute('tabindex'), '-1');
+      });
+    });
+
+    suite('for managed tree', function () {
+      test('for unfocused node in managed tree overwrites tab index', function () {
+        this.focusManager.registerTree(this.testFocusableTree1, true);
+
+        this.focusManager.focusNode(this.testFocusableTree1Node1);
+
+        // Focusing an element should overwrite its tab index.
+        const elem = this.testFocusableTree1Node1.getFocusableElement();
+        assert.strictEqual(elem.getAttribute('tabindex'), '-1');
+      });
+
+      test('for previously focused node in managed tree keeps new tab index', function () {
+        this.focusManager.registerTree(this.testFocusableTree1, true);
+        this.focusManager.focusNode(this.testFocusableTree1Node1);
+
+        this.focusManager.focusNode(this.testFocusableTree1Node2);
+
+        // The previously focused element should retain its tab index.
+        const elem = this.testFocusableTree1Node1.getFocusableElement();
+        assert.strictEqual(elem.getAttribute('tabindex'), '-1');
+      });
+
+      test('focused root makes root non-tab navigable', function () {
+        this.focusManager.registerTree(this.testFocusableTree1, true);
+        const rootNode = this.testFocusableTree1.getRootFocusableNode();
+
+        this.focusManager.focusNode(rootNode);
+
+        // Focusing the root in a managed tree should make it non-tab navigable.
+        const rootElem = rootNode.getFocusableElement();
+        assert.strictEqual(rootElem.getAttribute('tabindex'), '-1');
+      });
+
+      test('focused root with custom tab index should overwrite tab index', function () {
+        this.focusManager.registerTree(this.testFocusableTree1, true);
+        const rootNode = this.testFocusableTree1.getRootFocusableNode();
+        const rootElem = rootNode.getFocusableElement();
+        rootElem.tabIndex = 0;
+
+        this.focusManager.focusNode(rootNode);
+
+        // Custom tab indexes are overwritten for the root in a managed tree.
+        assert.strictEqual(rootElem.getAttribute('tabindex'), '-1');
+      });
+
+      test('focused node tree root makes root non-tab navigable', function () {
+        this.focusManager.registerTree(this.testFocusableTree1, true);
+        const rootNode = this.testFocusableTree1.getRootFocusableNode();
+
+        this.focusManager.focusNode(this.testFocusableTree1Node1);
+
+        // Focusing a node of a managed tree should make the root non-tab navigable.
+        const rootElem = rootNode.getFocusableElement();
+        assert.strictEqual(rootElem.getAttribute('tabindex'), '-1');
+      });
+
+      test('focused node root with custom tab index should overwrite tab index', function () {
+        this.focusManager.registerTree(this.testFocusableTree1, true);
+        const rootNode = this.testFocusableTree1.getRootFocusableNode();
+        const rootElem = rootNode.getFocusableElement();
+        rootElem.tabIndex = 0;
+
+        this.focusManager.focusNode(this.testFocusableTree1Node1);
+
+        // Custom tab indexes are overwritten for the root in a managed tree even when a tree's node
+        // is focused.
+        assert.strictEqual(rootElem.getAttribute('tabindex'), '-1');
+      });
+
+      test('focused node in a tree after managed was focused should make previous root tab navigable', function () {
+        this.focusManager.registerTree(this.testFocusableTree1, true);
+        this.focusManager.registerTree(this.testFocusableTree2, false);
+        const rootNode = this.testFocusableTree1.getRootFocusableNode();
+        this.focusManager.focusNode(this.testFocusableTree1Node1);
+
+        this.focusManager.focusNode(this.testFocusableTree2Node1);
+
+        // Focusing a different tree shouldn't after a managed tree should make the managed tree tab
+        // navigable.
+        const rootElem = rootNode.getFocusableElement();
+        assert.strictEqual(rootElem.getAttribute('tabindex'), '0');
+      });
+
+      test('focused node in a tree after managed was root focused should make previous root tab navigable', function () {
+        this.focusManager.registerTree(this.testFocusableTree1, true);
+        this.focusManager.registerTree(this.testFocusableTree2, false);
+        const rootNode = this.testFocusableTree1.getRootFocusableNode();
+        this.focusManager.focusTree(this.testFocusableTree1);
+
+        this.focusManager.focusNode(this.testFocusableTree2Node1);
+
+        // Focusing a different tree shouldn't after a managed tree should make the managed tree tab
+        // navigable.
+        const rootElem = rootNode.getFocusableElement();
+        assert.strictEqual(rootElem.getAttribute('tabindex'), '0');
+      });
+    });
   });
 
   suite('getFocusManager()', function () {
@@ -950,8 +1265,8 @@ suite('FocusManager', function () {
         nodeElem.textContent = 'Focusable node';
         rootElem.appendChild(nodeElem);
         document.body.appendChild(rootElem);
-        const root = createFocusableTree('focusRoot');
-        const node = createFocusableNode(root, 'focusNode');
+        const root = this.createFocusableTree('focusRoot');
+        const node = this.createFocusableNode(root, 'focusNode');
         this.focusManager.registerTree(root);
         this.focusManager.focusNode(node);
 
@@ -1424,6 +1739,7 @@ suite('FocusManager', function () {
     suite('getFocusedTree()', function () {
       test('registered root focus()ed no prev focus returns tree', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1').tabIndex = -1;
 
         document.getElementById('testFocusableTree1').focus();
 
@@ -1435,6 +1751,7 @@ suite('FocusManager', function () {
 
       test("registered node focus()ed no prev focus returns node's tree", function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
 
         document.getElementById('testFocusableTree1.node1').focus();
 
@@ -1446,6 +1763,8 @@ suite('FocusManager', function () {
 
       test("registered subnode focus()ed no prev focus returns node's tree", function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1.node1.child1').tabIndex =
+          -1;
 
         document.getElementById('testFocusableTree1.node1.child1').focus();
 
@@ -1457,6 +1776,8 @@ suite('FocusManager', function () {
 
       test('registered node focus()ed after prev node focus returns same tree', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree1.node2').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
 
         document.getElementById('testFocusableTree1.node2').focus();
@@ -1470,6 +1791,8 @@ suite('FocusManager', function () {
       test("registered node focus()ed after prev node focus diff tree returns new node's tree", function () {
         this.focusManager.registerTree(this.testFocusableTree1);
         this.focusManager.registerTree(this.testFocusableTree2);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
 
         document.getElementById('testFocusableTree2.node1').focus();
@@ -1483,6 +1806,8 @@ suite('FocusManager', function () {
       test("registered tree root focus()ed after prev node focus diff tree returns new node's tree", function () {
         this.focusManager.registerTree(this.testFocusableTree1);
         this.focusManager.registerTree(this.testFocusableTree2);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree2').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
 
         document.getElementById('testFocusableTree2').focus();
@@ -1495,6 +1820,9 @@ suite('FocusManager', function () {
 
       test("non-registered node subelement focus()ed returns node's tree", function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById(
+          'testFocusableTree1.node2.unregisteredChild1',
+        ).tabIndex = -1;
 
         document
           .getElementById('testFocusableTree1.node2.unregisteredChild1')
@@ -1508,12 +1836,18 @@ suite('FocusManager', function () {
       });
 
       test('non-registered tree focus()ed returns null', function () {
+        document.getElementById('testUnregisteredFocusableTree3').tabIndex = -1;
+
         document.getElementById('testUnregisteredFocusableTree3').focus();
 
         assert.isNull(this.focusManager.getFocusedTree());
       });
 
       test('non-registered tree node focus()ed returns null', function () {
+        document.getElementById(
+          'testUnregisteredFocusableTree3.node1',
+        ).tabIndex = -1;
+
         document.getElementById('testUnregisteredFocusableTree3.node1').focus();
 
         assert.isNull(this.focusManager.getFocusedTree());
@@ -1521,6 +1855,10 @@ suite('FocusManager', function () {
 
       test('non-registered tree node focus()ed after registered node focused returns null', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
+        document.getElementById(
+          'testUnregisteredFocusableTree3.node1',
+        ).tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
 
         document.getElementById('testUnregisteredFocusableTree3.node1').focus();
@@ -1530,6 +1868,7 @@ suite('FocusManager', function () {
 
       test('unfocusable element focus()ed after registered node focused returns original tree', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
 
         document.getElementById('testUnfocusableElement').focus();
@@ -1542,6 +1881,7 @@ suite('FocusManager', function () {
 
       test('unregistered tree focus()ed with no prev focus returns null', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1').tabIndex = -1;
         document.getElementById('testFocusableTree1').focus();
 
         this.focusManager.unregisterTree(this.testFocusableTree1);
@@ -1551,6 +1891,7 @@ suite('FocusManager', function () {
 
       test('unregistered tree focus()ed with no prev focus returns null', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
 
         this.focusManager.unregisterTree(this.testFocusableTree1);
@@ -1561,6 +1902,8 @@ suite('FocusManager', function () {
       test('unregistered tree focus()ed with prev node prior focused returns null', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
         this.focusManager.registerTree(this.testFocusableTree2);
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
         document.getElementById('testFocusableTree2.node1').focus();
         document.getElementById('testFocusableTree1.node1').focus();
 
@@ -1573,6 +1916,8 @@ suite('FocusManager', function () {
       test('unregistered tree focus()ed with prev node recently focused returns new tree', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
         this.focusManager.registerTree(this.testFocusableTree2);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
         document.getElementById('testFocusableTree2.node1').focus();
 
@@ -1588,6 +1933,9 @@ suite('FocusManager', function () {
       test('unregistered tree focus()ed with prev node after unregistering returns null', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
         this.focusManager.registerTree(this.testFocusableTree2);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
         document.getElementById('testFocusableTree2.node1').focus();
         this.focusManager.unregisterTree(this.testFocusableTree1);
@@ -1603,6 +1951,7 @@ suite('FocusManager', function () {
       test('nested tree focusTree()ed with no prev focus returns nested tree', function () {
         this.focusManager.registerTree(this.testFocusableTree2);
         this.focusManager.registerTree(this.testFocusableNestedTree4);
+        document.getElementById('testFocusableNestedTree4').tabIndex = -1;
 
         document.getElementById('testFocusableNestedTree4').focus();
 
@@ -1615,6 +1964,7 @@ suite('FocusManager', function () {
       test('nested tree node focusNode()ed with no prev focus returns nested tree', function () {
         this.focusManager.registerTree(this.testFocusableTree2);
         this.focusManager.registerTree(this.testFocusableNestedTree4);
+        document.getElementById('testFocusableNestedTree4.node1').tabIndex = -1;
 
         document.getElementById('testFocusableNestedTree4.node1').focus();
 
@@ -1627,6 +1977,8 @@ suite('FocusManager', function () {
       test('nested tree node focusNode()ed after parent focused returns nested tree', function () {
         this.focusManager.registerTree(this.testFocusableTree2);
         this.focusManager.registerTree(this.testFocusableNestedTree4);
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
+        document.getElementById('testFocusableNestedTree4.node1').tabIndex = -1;
         document.getElementById('testFocusableTree2.node1').focus();
 
         document.getElementById('testFocusableNestedTree4.node1').focus();
@@ -1640,6 +1992,7 @@ suite('FocusManager', function () {
     suite('getFocusedNode()', function () {
       test('registered root focus()ed no prev focus returns root node', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1').tabIndex = -1;
 
         document.getElementById('testFocusableTree1').focus();
 
@@ -1651,6 +2004,7 @@ suite('FocusManager', function () {
 
       test('registered node focus()ed no prev focus returns node', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
 
         document.getElementById('testFocusableTree1.node1').focus();
 
@@ -1662,6 +2016,8 @@ suite('FocusManager', function () {
 
       test('registered subnode focus()ed no prev focus returns subnode', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1.node1.child1').tabIndex =
+          -1;
 
         document.getElementById('testFocusableTree1.node1.child1').focus();
 
@@ -1673,6 +2029,8 @@ suite('FocusManager', function () {
 
       test('registered node focus()ed after prev node focus returns new node', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree1.node2').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
 
         document.getElementById('testFocusableTree1.node2').focus();
@@ -1686,6 +2044,8 @@ suite('FocusManager', function () {
       test('registered node focus()ed after prev node focus diff tree returns new node', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
         this.focusManager.registerTree(this.testFocusableTree2);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
 
         document.getElementById('testFocusableTree2.node1').focus();
@@ -1699,6 +2059,8 @@ suite('FocusManager', function () {
       test('registered tree root focus()ed after prev node focus diff tree returns new root', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
         this.focusManager.registerTree(this.testFocusableTree2);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree2').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
 
         document.getElementById('testFocusableTree2').focus();
@@ -1711,6 +2073,9 @@ suite('FocusManager', function () {
 
       test('non-registered node subelement focus()ed returns nearest node', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById(
+          'testFocusableTree1.node2.unregisteredChild1',
+        ).tabIndex = -1;
 
         document
           .getElementById('testFocusableTree1.node2.unregisteredChild1')
@@ -1724,12 +2089,18 @@ suite('FocusManager', function () {
       });
 
       test('non-registered tree focus()ed returns null', function () {
+        document.getElementById('testUnregisteredFocusableTree3').tabIndex = -1;
+
         document.getElementById('testUnregisteredFocusableTree3').focus();
 
         assert.isNull(this.focusManager.getFocusedNode());
       });
 
       test('non-registered tree node focus()ed returns null', function () {
+        document.getElementById(
+          'testUnregisteredFocusableTree3.node1',
+        ).tabIndex = -1;
+
         document.getElementById('testUnregisteredFocusableTree3.node1').focus();
 
         assert.isNull(this.focusManager.getFocusedNode());
@@ -1737,6 +2108,10 @@ suite('FocusManager', function () {
 
       test('non-registered tree node focus()ed after registered node focused returns null', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
+        document.getElementById(
+          'testUnregisteredFocusableTree3.node1',
+        ).tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
 
         document.getElementById('testUnregisteredFocusableTree3.node1').focus();
@@ -1746,6 +2121,7 @@ suite('FocusManager', function () {
 
       test('unfocusable element focus()ed after registered node focused returns original node', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
 
         document.getElementById('testUnfocusableElement').focus();
@@ -1758,6 +2134,7 @@ suite('FocusManager', function () {
 
       test('unregistered tree focus()ed with no prev focus returns null', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1').tabIndex = -1;
         document.getElementById('testFocusableTree1').focus();
 
         this.focusManager.unregisterTree(this.testFocusableTree1);
@@ -1767,6 +2144,7 @@ suite('FocusManager', function () {
 
       test('unregistered tree focus()ed with no prev focus returns null', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
 
         this.focusManager.unregisterTree(this.testFocusableTree1);
@@ -1777,6 +2155,8 @@ suite('FocusManager', function () {
       test('unregistered tree focus()ed with prev node prior focused returns null', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
         this.focusManager.registerTree(this.testFocusableTree2);
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
         document.getElementById('testFocusableTree2.node1').focus();
         document.getElementById('testFocusableTree1.node1').focus();
 
@@ -1789,6 +2169,8 @@ suite('FocusManager', function () {
       test('unregistered tree focus()ed with prev node recently focused returns new node', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
         this.focusManager.registerTree(this.testFocusableTree2);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
         document.getElementById('testFocusableTree2.node1').focus();
 
@@ -1804,6 +2186,9 @@ suite('FocusManager', function () {
       test('unregistered tree focus()ed with prev node after unregistering returns null', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
         this.focusManager.registerTree(this.testFocusableTree2);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
         document.getElementById('testFocusableTree2.node1').focus();
         this.focusManager.unregisterTree(this.testFocusableTree1);
@@ -1819,6 +2204,7 @@ suite('FocusManager', function () {
       test('nested tree focus()ed with no prev focus returns nested root', function () {
         this.focusManager.registerTree(this.testFocusableTree2);
         this.focusManager.registerTree(this.testFocusableNestedTree4);
+        document.getElementById('testFocusableNestedTree4').tabIndex = -1;
 
         document.getElementById('testFocusableNestedTree4').focus();
 
@@ -1831,6 +2217,7 @@ suite('FocusManager', function () {
       test('nested tree node focus()ed with no prev focus returns focused node', function () {
         this.focusManager.registerTree(this.testFocusableTree2);
         this.focusManager.registerTree(this.testFocusableNestedTree4);
+        document.getElementById('testFocusableNestedTree4.node1').tabIndex = -1;
 
         document.getElementById('testFocusableNestedTree4.node1').focus();
 
@@ -1843,6 +2230,8 @@ suite('FocusManager', function () {
       test('nested tree node focus()ed after parent focused returns focused node', function () {
         this.focusManager.registerTree(this.testFocusableTree2);
         this.focusManager.registerTree(this.testFocusableNestedTree4);
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
+        document.getElementById('testFocusableNestedTree4.node1').tabIndex = -1;
         document.getElementById('testFocusableTree2.node1').focus();
 
         document.getElementById('testFocusableNestedTree4.node1').focus();
@@ -1863,9 +2252,10 @@ suite('FocusManager', function () {
         nodeElem.textContent = 'Focusable node';
         rootElem.appendChild(nodeElem);
         document.body.appendChild(rootElem);
-        const root = createFocusableTree('focusRoot');
-        const node = createFocusableNode(root, 'focusNode');
+        const root = this.createFocusableTree('focusRoot');
+        const node = this.createFocusableNode(root, 'focusNode');
         this.focusManager.registerTree(root);
+        document.getElementById('focusNode').tabIndex = -1;
         document.getElementById('focusNode').focus();
 
         node.getFocusableElement().remove();
@@ -1873,10 +2263,44 @@ suite('FocusManager', function () {
         assert.notStrictEqual(this.focusManager.getFocusedNode(), node);
         rootElem.remove(); // Cleanup.
       });
+
+      test('after focus() after trying to focusNode() an unfocusable node updates returns focus()ed node', function () {
+        this.testFocusableTree1Node1.canBeFocused = () => false;
+        document.getElementById('testFocusableTree1.node2').tabIndex = -1;
+        this.focusManager.registerTree(this.testFocusableTree1);
+        this.focusManager.focusNode(this.testFocusableTree1Node1);
+
+        document.getElementById('testFocusableTree1.node2').focus();
+
+        // focus()ing a new node should overwrite a failed attempt to focusNode() an unfocusable
+        // node. This verifies that DOM focus syncing is properly reenabled by FocusManager.
+        assert.strictEqual(
+          this.focusManager.getFocusedNode(),
+          this.testFocusableTree1Node2,
+        );
+      });
+
+      test('after focus() after trying to focusNode() the same node twice returns focus()ed node', function () {
+        document.getElementById('testFocusableTree1.node2').tabIndex = -1;
+        this.focusManager.registerTree(this.testFocusableTree1);
+        // Intentionally try to focus the same node twice.
+        this.focusManager.focusNode(this.testFocusableTree1Node1);
+        this.focusManager.focusNode(this.testFocusableTree1Node1);
+
+        document.getElementById('testFocusableTree1.node2').focus();
+
+        // focus()ing a new node should overwrite a failed attempt to focusNode() the same node
+        // twice. This verifies that DOM focus syncing is properly reenabled by FocusManager.
+        assert.strictEqual(
+          this.focusManager.getFocusedNode(),
+          this.testFocusableTree1Node2,
+        );
+      });
     });
     suite('CSS classes', function () {
       test('registered root focus()ed no prev focus returns root elem has active property', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1').tabIndex = -1;
 
         document.getElementById('testFocusableTree1').focus();
 
@@ -1895,6 +2319,7 @@ suite('FocusManager', function () {
 
       test('registered node focus()ed no prev focus node elem has active property', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
 
         document.getElementById('testFocusableTree1.node1').focus();
 
@@ -1911,6 +2336,8 @@ suite('FocusManager', function () {
 
       test('registered node focus()ed after prev node focus same tree old node elem has no focus property', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree1.node2').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
 
         document.getElementById('testFocusableTree1.node2').focus();
@@ -1928,6 +2355,8 @@ suite('FocusManager', function () {
 
       test('registered node focus()ed after prev node focus same tree new node elem has active property', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree1.node2').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
 
         document.getElementById('testFocusableTree1.node2').focus();
@@ -1946,6 +2375,8 @@ suite('FocusManager', function () {
       test('registered node focus()ed after prev node focus diff tree old node elem has passive property', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
         this.focusManager.registerTree(this.testFocusableTree2);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
 
         document.getElementById('testFocusableTree2.node1').focus();
@@ -1964,6 +2395,8 @@ suite('FocusManager', function () {
       test('registered node focus()ed after prev node focus diff tree new node elem has active property', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
         this.focusManager.registerTree(this.testFocusableTree2);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
 
         document.getElementById('testFocusableTree2.node1').focus();
@@ -1982,6 +2415,8 @@ suite('FocusManager', function () {
       test('registered tree root focus()ed after prev node focus diff tree new root has active property', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
         this.focusManager.registerTree(this.testFocusableTree2);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree2').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
 
         document.getElementById('testFocusableTree2').focus();
@@ -2001,6 +2436,9 @@ suite('FocusManager', function () {
 
       test('non-registered node subelement focus()ed nearest node has active property', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById(
+          'testFocusableTree1.node2.unregisteredChild1',
+        ).tabIndex = -1;
 
         document
           .getElementById('testFocusableTree1.node2.unregisteredChild1')
@@ -2019,10 +2457,11 @@ suite('FocusManager', function () {
       });
 
       test('non-registered tree focus()ed has no focus', function () {
+        document.getElementById('testUnregisteredFocusableTree3').tabIndex = -1;
+
         document.getElementById('testUnregisteredFocusableTree3').focus();
 
         assert.isNull(this.focusManager.getFocusedNode());
-
         const rootElem = document.getElementById(
           'testUnregisteredFocusableTree3',
         );
@@ -2037,10 +2476,13 @@ suite('FocusManager', function () {
       });
 
       test('non-registered tree node focus()ed has no focus', function () {
+        document.getElementById(
+          'testUnregisteredFocusableTree3.node1',
+        ).tabIndex = -1;
+
         document.getElementById('testUnregisteredFocusableTree3.node1').focus();
 
         assert.isNull(this.focusManager.getFocusedNode());
-
         const nodeElem = document.getElementById(
           'testUnregisteredFocusableTree3.node1',
         );
@@ -2056,6 +2498,7 @@ suite('FocusManager', function () {
 
       test('unfocsable element focus()ed after registered node focused original node has active focus', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
 
         document.getElementById('testUnfocusableElement').focus();
@@ -2086,6 +2529,7 @@ suite('FocusManager', function () {
 
       test('unregistered tree focus()ed with no prev focus removes focus', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1').tabIndex = -1;
         document.getElementById('testFocusableTree1').focus();
 
         this.focusManager.unregisterTree(this.testFocusableTree1);
@@ -2106,6 +2550,7 @@ suite('FocusManager', function () {
 
       test('unregistered tree focus()ed with no prev focus removes focus', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
 
         this.focusManager.unregisterTree(this.testFocusableTree1);
@@ -2125,6 +2570,8 @@ suite('FocusManager', function () {
       test('unregistered tree focus()ed with prev node prior removes focus from removed tree', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
         this.focusManager.registerTree(this.testFocusableTree2);
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
         document.getElementById('testFocusableTree2.node1').focus();
         document.getElementById('testFocusableTree1.node1').focus();
 
@@ -2157,6 +2604,8 @@ suite('FocusManager', function () {
       test('unregistered tree focus()ed with prev node recently removes focus from removed tree', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
         this.focusManager.registerTree(this.testFocusableTree2);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
         document.getElementById('testFocusableTree2.node1').focus();
 
@@ -2189,6 +2638,9 @@ suite('FocusManager', function () {
       test('unregistered tree focus()ed with prev node after unregistering removes active indicator', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
         this.focusManager.registerTree(this.testFocusableTree2);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
         document.getElementById('testFocusableTree2.node1').focus();
         this.focusManager.unregisterTree(this.testFocusableTree1);
@@ -2221,6 +2673,9 @@ suite('FocusManager', function () {
       test('focus() multiple nodes in same tree with switches ensure passive focus has gone', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
         this.focusManager.registerTree(this.testFocusableTree2);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree1.node2').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
         document.getElementById('testFocusableTree2.node1').focus();
 
@@ -2243,6 +2698,9 @@ suite('FocusManager', function () {
       test('registered tree focus()ed other tree node passively focused tree node now has active property', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
         this.focusManager.registerTree(this.testFocusableTree2);
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree1').tabIndex = -1;
         document.getElementById('testFocusableTree1.node1').focus();
         document.getElementById('testFocusableTree2.node1').focus();
 
@@ -2276,6 +2734,9 @@ suite('FocusManager', function () {
       test('focus on root, node in diff tree, then node in first tree; root should have focus gone', function () {
         this.focusManager.registerTree(this.testFocusableTree1);
         this.focusManager.registerTree(this.testFocusableTree2);
+        document.getElementById('testFocusableTree1').tabIndex = -1;
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree1.node1').tabIndex = -1;
         document.getElementById('testFocusableTree1').focus();
         document.getElementById('testFocusableTree2.node1').focus();
 
@@ -2306,6 +2767,7 @@ suite('FocusManager', function () {
       test('nested tree focus()ed with no prev root has active focus', function () {
         this.focusManager.registerTree(this.testFocusableTree2);
         this.focusManager.registerTree(this.testFocusableNestedTree4);
+        document.getElementById('testFocusableNestedTree4').tabIndex = -1;
 
         document.getElementById('testFocusableNestedTree4').focus();
 
@@ -2325,6 +2787,7 @@ suite('FocusManager', function () {
       test('nested tree node focus()ed with no prev focus node has active focus', function () {
         this.focusManager.registerTree(this.testFocusableTree2);
         this.focusManager.registerTree(this.testFocusableNestedTree4);
+        document.getElementById('testFocusableNestedTree4.node1').tabIndex = -1;
 
         document.getElementById('testFocusableNestedTree4.node1').focus();
 
@@ -2343,6 +2806,8 @@ suite('FocusManager', function () {
       test('nested tree node focus()ed after parent focused prev has passive node has active', function () {
         this.focusManager.registerTree(this.testFocusableTree2);
         this.focusManager.registerTree(this.testFocusableNestedTree4);
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
+        document.getElementById('testFocusableNestedTree4.node1').tabIndex = -1;
         document.getElementById('testFocusableTree2.node1').focus();
 
         document.getElementById('testFocusableNestedTree4.node1').focus();
@@ -3255,6 +3720,7 @@ suite('FocusManager', function () {
     suite('getFocusedTree()', function () {
       test('registered root focus()ed no prev focus returns tree', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById('testFocusableGroup1').tabIndex = -1;
 
         document.getElementById('testFocusableGroup1').focus();
 
@@ -3266,6 +3732,7 @@ suite('FocusManager', function () {
 
       test("registered node focus()ed no prev focus returns node's tree", function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
 
         document.getElementById('testFocusableGroup1.node1').focus();
 
@@ -3277,6 +3744,8 @@ suite('FocusManager', function () {
 
       test("registered subnode focus()ed no prev focus returns node's tree", function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById('testFocusableGroup1.node1.child1').tabIndex =
+          -1;
 
         document.getElementById('testFocusableGroup1.node1.child1').focus();
 
@@ -3288,6 +3757,8 @@ suite('FocusManager', function () {
 
       test('registered node focus()ed after prev node focus returns same tree', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup1.node2').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
 
         document.getElementById('testFocusableGroup1.node2').focus();
@@ -3301,6 +3772,8 @@ suite('FocusManager', function () {
       test("registered node focus()ed after prev node focus diff tree returns new node's tree", function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
 
         document.getElementById('testFocusableGroup2.node1').focus();
@@ -3314,6 +3787,8 @@ suite('FocusManager', function () {
       test("registered tree root focus()ed after prev node focus diff tree returns new node's tree", function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup2').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
 
         document.getElementById('testFocusableGroup2').focus();
@@ -3326,6 +3801,9 @@ suite('FocusManager', function () {
 
       test("non-registered node subelement focus()ed returns node's tree", function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById(
+          'testFocusableGroup1.node2.unregisteredChild1',
+        ).tabIndex = -1;
 
         document
           .getElementById('testFocusableGroup1.node2.unregisteredChild1')
@@ -3339,12 +3817,19 @@ suite('FocusManager', function () {
       });
 
       test('non-registered tree focus()ed returns null', function () {
+        document.getElementById('testUnregisteredFocusableGroup3').tabIndex =
+          -1;
+
         document.getElementById('testUnregisteredFocusableGroup3').focus();
 
         assert.isNull(this.focusManager.getFocusedTree());
       });
 
       test('non-registered tree node focus()ed returns null', function () {
+        document.getElementById(
+          'testUnregisteredFocusableGroup3.node1',
+        ).tabIndex = -1;
+
         document
           .getElementById('testUnregisteredFocusableGroup3.node1')
           .focus();
@@ -3354,6 +3839,10 @@ suite('FocusManager', function () {
 
       test('non-registered tree node focus()ed after registered node focused returns null', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
+        document.getElementById(
+          'testUnregisteredFocusableGroup3.node1',
+        ).tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
 
         document
@@ -3368,6 +3857,7 @@ suite('FocusManager', function () {
 
       test('unregistered tree focus()ed with no prev focus returns null', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById('testFocusableGroup1').tabIndex = -1;
         document.getElementById('testFocusableGroup1').focus();
 
         this.focusManager.unregisterTree(this.testFocusableGroup1);
@@ -3377,6 +3867,7 @@ suite('FocusManager', function () {
 
       test('unregistered tree focus()ed with no prev focus returns null', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
 
         this.focusManager.unregisterTree(this.testFocusableGroup1);
@@ -3387,6 +3878,8 @@ suite('FocusManager', function () {
       test('unregistered tree focus()ed with prev node prior focused returns null', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
         document.getElementById('testFocusableGroup2.node1').focus();
         document.getElementById('testFocusableGroup1.node1').focus();
 
@@ -3399,6 +3892,8 @@ suite('FocusManager', function () {
       test('unregistered tree focus()ed with prev node recently focused returns new tree', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
         document.getElementById('testFocusableGroup2.node1').focus();
 
@@ -3414,6 +3909,9 @@ suite('FocusManager', function () {
       test('unregistered tree focus()ed with prev node after unregistering returns null', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
         document.getElementById('testFocusableGroup2.node1').focus();
         this.focusManager.unregisterTree(this.testFocusableGroup1);
@@ -3429,6 +3927,7 @@ suite('FocusManager', function () {
       test('nested tree focusTree()ed with no prev focus returns nested tree', function () {
         this.focusManager.registerTree(this.testFocusableGroup2);
         this.focusManager.registerTree(this.testFocusableNestedGroup4);
+        document.getElementById('testFocusableNestedGroup4').tabIndex = -1;
 
         document.getElementById('testFocusableNestedGroup4').focus();
 
@@ -3441,6 +3940,8 @@ suite('FocusManager', function () {
       test('nested tree node focusNode()ed with no prev focus returns nested tree', function () {
         this.focusManager.registerTree(this.testFocusableGroup2);
         this.focusManager.registerTree(this.testFocusableNestedGroup4);
+        document.getElementById('testFocusableNestedGroup4.node1').tabIndex =
+          -1;
 
         document.getElementById('testFocusableNestedGroup4.node1').focus();
 
@@ -3453,6 +3954,9 @@ suite('FocusManager', function () {
       test('nested tree node focusNode()ed after parent focused returns nested tree', function () {
         this.focusManager.registerTree(this.testFocusableGroup2);
         this.focusManager.registerTree(this.testFocusableNestedGroup4);
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
+        document.getElementById('testFocusableNestedGroup4.node1').tabIndex =
+          -1;
         document.getElementById('testFocusableGroup2.node1').focus();
 
         document.getElementById('testFocusableNestedGroup4.node1').focus();
@@ -3466,6 +3970,7 @@ suite('FocusManager', function () {
     suite('getFocusedNode()', function () {
       test('registered root focus()ed no prev focus returns root node', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById('testFocusableGroup1').tabIndex = -1;
 
         document.getElementById('testFocusableGroup1').focus();
 
@@ -3477,6 +3982,7 @@ suite('FocusManager', function () {
 
       test('registered node focus()ed no prev focus returns node', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
 
         document.getElementById('testFocusableGroup1.node1').focus();
 
@@ -3488,6 +3994,8 @@ suite('FocusManager', function () {
 
       test('registered subnode focus()ed no prev focus returns subnode', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById('testFocusableGroup1.node1.child1').tabIndex =
+          -1;
 
         document.getElementById('testFocusableGroup1.node1.child1').focus();
 
@@ -3499,6 +4007,8 @@ suite('FocusManager', function () {
 
       test('registered node focus()ed after prev node focus returns new node', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup1.node2').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
 
         document.getElementById('testFocusableGroup1.node2').focus();
@@ -3512,6 +4022,8 @@ suite('FocusManager', function () {
       test('registered node focus()ed after prev node focus diff tree returns new node', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
 
         document.getElementById('testFocusableGroup2.node1').focus();
@@ -3525,6 +4037,8 @@ suite('FocusManager', function () {
       test('registered tree root focus()ed after prev node focus diff tree returns new root', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup2').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
 
         document.getElementById('testFocusableGroup2').focus();
@@ -3537,6 +4051,9 @@ suite('FocusManager', function () {
 
       test('non-registered node subelement focus()ed returns nearest node', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById(
+          'testFocusableGroup1.node2.unregisteredChild1',
+        ).tabIndex = -1;
 
         document
           .getElementById('testFocusableGroup1.node2.unregisteredChild1')
@@ -3550,12 +4067,19 @@ suite('FocusManager', function () {
       });
 
       test('non-registered tree focus()ed returns null', function () {
+        document.getElementById('testUnregisteredFocusableGroup3').tabIndex =
+          -1;
+
         document.getElementById('testUnregisteredFocusableGroup3').focus();
 
         assert.isNull(this.focusManager.getFocusedNode());
       });
 
       test('non-registered tree node focus()ed returns null', function () {
+        document.getElementById(
+          'testUnregisteredFocusableGroup3.node1',
+        ).tabIndex = -1;
+
         document
           .getElementById('testUnregisteredFocusableGroup3.node1')
           .focus();
@@ -3565,6 +4089,10 @@ suite('FocusManager', function () {
 
       test('non-registered tree node focus()ed after registered node focused returns null', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
+        document.getElementById(
+          'testUnregisteredFocusableGroup3.node1',
+        ).tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
 
         document
@@ -3576,6 +4104,7 @@ suite('FocusManager', function () {
 
       test('unfocusable element focus()ed after registered node focused returns original node', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
 
         document.getElementById('testUnfocusableElement').focus();
@@ -3588,6 +4117,7 @@ suite('FocusManager', function () {
 
       test('unregistered tree focus()ed with no prev focus returns null', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById('testFocusableGroup1').tabIndex = -1;
         document.getElementById('testFocusableGroup1').focus();
 
         this.focusManager.unregisterTree(this.testFocusableGroup1);
@@ -3597,6 +4127,7 @@ suite('FocusManager', function () {
 
       test('unregistered tree focus()ed with no prev focus returns null', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
 
         this.focusManager.unregisterTree(this.testFocusableGroup1);
@@ -3607,6 +4138,8 @@ suite('FocusManager', function () {
       test('unregistered tree focus()ed with prev node prior focused returns null', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
         document.getElementById('testFocusableGroup2.node1').focus();
         document.getElementById('testFocusableGroup1.node1').focus();
 
@@ -3619,6 +4152,8 @@ suite('FocusManager', function () {
       test('unregistered tree focus()ed with prev node recently focused returns new node', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
         document.getElementById('testFocusableGroup2.node1').focus();
 
@@ -3634,6 +4169,9 @@ suite('FocusManager', function () {
       test('unregistered tree focus()ed with prev node after unregistering returns null', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
         document.getElementById('testFocusableGroup2.node1').focus();
         this.focusManager.unregisterTree(this.testFocusableGroup1);
@@ -3649,6 +4187,7 @@ suite('FocusManager', function () {
       test('nested tree focus()ed with no prev focus returns nested root', function () {
         this.focusManager.registerTree(this.testFocusableGroup2);
         this.focusManager.registerTree(this.testFocusableNestedGroup4);
+        document.getElementById('testFocusableNestedGroup4').tabIndex = -1;
 
         document.getElementById('testFocusableNestedGroup4').focus();
 
@@ -3661,6 +4200,8 @@ suite('FocusManager', function () {
       test('nested tree node focus()ed with no prev focus returns focused node', function () {
         this.focusManager.registerTree(this.testFocusableGroup2);
         this.focusManager.registerTree(this.testFocusableNestedGroup4);
+        document.getElementById('testFocusableNestedGroup4.node1').tabIndex =
+          -1;
 
         document.getElementById('testFocusableNestedGroup4.node1').focus();
 
@@ -3673,6 +4214,9 @@ suite('FocusManager', function () {
       test('nested tree node focus()ed after parent focused returns focused node', function () {
         this.focusManager.registerTree(this.testFocusableGroup2);
         this.focusManager.registerTree(this.testFocusableNestedGroup4);
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
+        document.getElementById('testFocusableNestedGroup4.node1').tabIndex =
+          -1;
         document.getElementById('testFocusableGroup2.node1').focus();
 
         document.getElementById('testFocusableNestedGroup4.node1').focus();
@@ -3682,10 +4226,44 @@ suite('FocusManager', function () {
           this.testFocusableNestedGroup4Node1,
         );
       });
+
+      test('after focus() after trying to focusNode() an unfocusable node updates returns focus()ed node', function () {
+        this.testFocusableGroup1Node1.canBeFocused = () => false;
+        document.getElementById('testFocusableGroup1.node2').tabIndex = -1;
+        this.focusManager.registerTree(this.testFocusableGroup1);
+        this.focusManager.focusNode(this.testFocusableGroup1Node1);
+
+        document.getElementById('testFocusableGroup1.node2').focus();
+
+        // focus()ing a new node should overwrite a failed attempt to focusNode() an unfocusable
+        // node. This verifies that DOM focus syncing is properly reenabled by FocusManager.
+        assert.strictEqual(
+          this.focusManager.getFocusedNode(),
+          this.testFocusableGroup1Node2,
+        );
+      });
+
+      test('after focus() after trying to focusNode() the same node twice returns focus()ed node', function () {
+        document.getElementById('testFocusableGroup1.node2').tabIndex = -1;
+        this.focusManager.registerTree(this.testFocusableGroup1);
+        // Intentionally try to focus the same node twice.
+        this.focusManager.focusNode(this.testFocusableGroup1Node1);
+        this.focusManager.focusNode(this.testFocusableGroup1Node1);
+
+        document.getElementById('testFocusableGroup1.node2').focus();
+
+        // focus()ing a new node should overwrite a failed attempt to focusNode() the same node
+        // twice. This verifies that DOM focus syncing is properly reenabled by FocusManager.
+        assert.strictEqual(
+          this.focusManager.getFocusedNode(),
+          this.testFocusableGroup1Node2,
+        );
+      });
     });
     suite('CSS classes', function () {
       test('registered root focus()ed no prev focus returns root elem has active property', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById('testFocusableGroup1').tabIndex = -1;
 
         document.getElementById('testFocusableGroup1').focus();
 
@@ -3704,6 +4282,7 @@ suite('FocusManager', function () {
 
       test('registered node focus()ed no prev focus node elem has active property', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
 
         document.getElementById('testFocusableGroup1.node1').focus();
 
@@ -3720,6 +4299,8 @@ suite('FocusManager', function () {
 
       test('registered node focus()ed after prev node focus same tree old node elem has no focus property', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup1.node2').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
 
         document.getElementById('testFocusableGroup1.node2').focus();
@@ -3738,6 +4319,8 @@ suite('FocusManager', function () {
 
       test('registered node focus()ed after prev node focus same tree new node elem has active property', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup1.node2').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
 
         document.getElementById('testFocusableGroup1.node2').focus();
@@ -3756,6 +4339,8 @@ suite('FocusManager', function () {
       test('registered node focus()ed after prev node focus diff tree old node elem has passive property', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
 
         document.getElementById('testFocusableGroup2.node1').focus();
@@ -3775,6 +4360,8 @@ suite('FocusManager', function () {
       test('registered node focus()ed after prev node focus diff tree new node elem has active property', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
 
         document.getElementById('testFocusableGroup2.node1').focus();
@@ -3793,6 +4380,8 @@ suite('FocusManager', function () {
       test('registered tree root focus()ed after prev node focus diff tree new root has active property', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup2').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
 
         document.getElementById('testFocusableGroup2').focus();
@@ -3812,6 +4401,9 @@ suite('FocusManager', function () {
 
       test('non-registered node subelement focus()ed nearest node has active property', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById(
+          'testFocusableGroup1.node2.unregisteredChild1',
+        ).tabIndex = -1;
 
         document
           .getElementById('testFocusableGroup1.node2.unregisteredChild1')
@@ -3830,10 +4422,12 @@ suite('FocusManager', function () {
       });
 
       test('non-registered tree focus()ed has no focus', function () {
+        document.getElementById('testUnregisteredFocusableGroup3').tabIndex =
+          -1;
+
         document.getElementById('testUnregisteredFocusableGroup3').focus();
 
         assert.isNull(this.focusManager.getFocusedNode());
-
         const rootElem = document.getElementById(
           'testUnregisteredFocusableGroup3',
         );
@@ -3848,12 +4442,15 @@ suite('FocusManager', function () {
       });
 
       test('non-registered tree node focus()ed has no focus', function () {
+        document.getElementById(
+          'testUnregisteredFocusableGroup3.node1',
+        ).tabIndex = -1;
+
         document
           .getElementById('testUnregisteredFocusableGroup3.node1')
           .focus();
 
         assert.isNull(this.focusManager.getFocusedNode());
-
         const nodeElem = document.getElementById(
           'testUnregisteredFocusableGroup3.node1',
         );
@@ -3869,6 +4466,7 @@ suite('FocusManager', function () {
 
       test('unfocusable element focus()ed after registered node focused original node has active focus', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
 
         document.getElementById('testUnfocusableElement').focus();
@@ -3899,6 +4497,7 @@ suite('FocusManager', function () {
 
       test('unregistered tree focus()ed with no prev focus removes focus', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById('testFocusableGroup1').tabIndex = -1;
         document.getElementById('testFocusableGroup1').focus();
 
         this.focusManager.unregisterTree(this.testFocusableGroup1);
@@ -3919,6 +4518,7 @@ suite('FocusManager', function () {
 
       test('unregistered tree focus()ed with no prev focus removes focus', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
 
         this.focusManager.unregisterTree(this.testFocusableGroup1);
@@ -3938,6 +4538,8 @@ suite('FocusManager', function () {
       test('unregistered tree focus()ed with prev node prior removes focus from removed tree', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
         document.getElementById('testFocusableGroup2.node1').focus();
         document.getElementById('testFocusableGroup1.node1').focus();
 
@@ -3970,6 +4572,8 @@ suite('FocusManager', function () {
       test('unregistered tree focus()ed with prev node recently removes focus from removed tree', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
         document.getElementById('testFocusableGroup2.node1').focus();
 
@@ -4002,6 +4606,9 @@ suite('FocusManager', function () {
       test('unregistered tree focus()ed with prev node after unregistering removes active indicator', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
         document.getElementById('testFocusableGroup2.node1').focus();
         this.focusManager.unregisterTree(this.testFocusableGroup1);
@@ -4034,6 +4641,9 @@ suite('FocusManager', function () {
       test('focus() multiple nodes in same tree with switches ensure passive focus has gone', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup1.node2').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
         document.getElementById('testFocusableGroup2.node1').focus();
 
@@ -4056,6 +4666,9 @@ suite('FocusManager', function () {
       test('registered tree focus()ed other tree node passively focused tree node now has active property', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup1').tabIndex = -1;
         document.getElementById('testFocusableGroup1.node1').focus();
         document.getElementById('testFocusableGroup2.node1').focus();
 
@@ -4089,6 +4702,9 @@ suite('FocusManager', function () {
       test('focus on root, node in diff tree, then node in first tree; root should have focus gone', function () {
         this.focusManager.registerTree(this.testFocusableGroup1);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableGroup1').tabIndex = -1;
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup1.node1').tabIndex = -1;
         document.getElementById('testFocusableGroup1').focus();
         document.getElementById('testFocusableGroup2.node1').focus();
 
@@ -4119,6 +4735,7 @@ suite('FocusManager', function () {
       test('nested tree focus()ed with no prev root has active focus', function () {
         this.focusManager.registerTree(this.testFocusableGroup2);
         this.focusManager.registerTree(this.testFocusableNestedGroup4);
+        document.getElementById('testFocusableNestedGroup4').tabIndex = -1;
 
         document.getElementById('testFocusableNestedGroup4').focus();
 
@@ -4138,6 +4755,8 @@ suite('FocusManager', function () {
       test('nested tree node focus()ed with no prev focus node has active focus', function () {
         this.focusManager.registerTree(this.testFocusableGroup2);
         this.focusManager.registerTree(this.testFocusableNestedGroup4);
+        document.getElementById('testFocusableNestedGroup4.node1').tabIndex =
+          -1;
 
         document.getElementById('testFocusableNestedGroup4.node1').focus();
 
@@ -4156,6 +4775,9 @@ suite('FocusManager', function () {
       test('nested tree node focus()ed after parent focused prev has passive node has active', function () {
         this.focusManager.registerTree(this.testFocusableGroup2);
         this.focusManager.registerTree(this.testFocusableNestedGroup4);
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
+        document.getElementById('testFocusableNestedGroup4.node1').tabIndex =
+          -1;
         document.getElementById('testFocusableGroup2.node1').focus();
 
         document.getElementById('testFocusableNestedGroup4.node1').focus();
@@ -4189,6 +4811,7 @@ suite('FocusManager', function () {
     test('Defocusing actively focused root HTML tree switches to passive highlight', function () {
       this.focusManager.registerTree(this.testFocusableTree2);
       this.focusManager.focusTree(this.testFocusableTree2);
+      document.getElementById('testUnregisteredFocusableTree3').tabIndex = -1;
 
       document.getElementById('testUnregisteredFocusableTree3').focus();
 
@@ -4209,6 +4832,7 @@ suite('FocusManager', function () {
     test('Defocusing actively focused HTML tree node switches to passive highlight', function () {
       this.focusManager.registerTree(this.testFocusableTree2);
       this.focusManager.focusNode(this.testFocusableTree2Node1);
+      document.getElementById('testUnregisteredFocusableTree3').tabIndex = -1;
 
       document.getElementById('testUnregisteredFocusableTree3').focus();
 
@@ -4229,6 +4853,7 @@ suite('FocusManager', function () {
       this.focusManager.registerTree(this.testFocusableTree2);
       this.focusManager.registerTree(this.testFocusableNestedTree4);
       this.focusManager.focusNode(this.testFocusableNestedTree4Node1);
+      document.getElementById('testUnregisteredFocusableTree3').tabIndex = -1;
 
       document.getElementById('testUnregisteredFocusableTree3').focus();
 
@@ -4248,6 +4873,8 @@ suite('FocusManager', function () {
     test('Refocusing actively focused root HTML tree restores to active highlight', function () {
       this.focusManager.registerTree(this.testFocusableTree2);
       this.focusManager.focusTree(this.testFocusableTree2);
+      document.getElementById('testUnregisteredFocusableTree3').tabIndex = -1;
+      document.getElementById('testFocusableTree2').tabIndex = -1;
       document.getElementById('testUnregisteredFocusableTree3').focus();
 
       document.getElementById('testFocusableTree2').focus();
@@ -4272,6 +4899,8 @@ suite('FocusManager', function () {
     test('Refocusing actively focused HTML tree node restores to active highlight', function () {
       this.focusManager.registerTree(this.testFocusableTree2);
       this.focusManager.focusNode(this.testFocusableTree2Node1);
+      document.getElementById('testUnregisteredFocusableTree3').tabIndex = -1;
+      document.getElementById('testFocusableTree2.node1').tabIndex = -1;
       document.getElementById('testUnregisteredFocusableTree3').focus();
 
       document.getElementById('testFocusableTree2.node1').focus();
@@ -4299,6 +4928,8 @@ suite('FocusManager', function () {
       this.focusManager.registerTree(this.testFocusableTree2);
       this.focusManager.registerTree(this.testFocusableNestedTree4);
       this.focusManager.focusNode(this.testFocusableNestedTree4Node1);
+      document.getElementById('testUnregisteredFocusableTree3').tabIndex = -1;
+      document.getElementById('testFocusableNestedTree4.node1').tabIndex = -1;
       document.getElementById('testUnregisteredFocusableTree3').focus();
 
       document.getElementById('testFocusableNestedTree4.node1').focus();
@@ -4401,6 +5032,7 @@ suite('FocusManager', function () {
         this.focusManager.registerTree(this.testFocusableTree2);
         this.focusManager.registerTree(this.testFocusableGroup2);
         this.focusManager.focusTree(this.testFocusableTree2);
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
 
         document.getElementById('testFocusableGroup2.node1').focus();
 
@@ -4501,6 +5133,7 @@ suite('FocusManager', function () {
         this.focusManager.registerTree(this.testFocusableTree2);
         this.focusManager.registerTree(this.testFocusableGroup2);
         this.focusManager.focusNode(this.testFocusableTree2Node1);
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
 
         document.getElementById('testFocusableGroup2.node1').focus();
 
@@ -4532,6 +5165,7 @@ suite('FocusManager', function () {
       test('HTML DOM focus()ed then SVG focusTree()ed correctly updates getFocusedTree() and indicators', function () {
         this.focusManager.registerTree(this.testFocusableTree2);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
         document.getElementById('testFocusableTree2.node1').focus();
 
         this.focusManager.focusTree(this.testFocusableGroup2);
@@ -4566,6 +5200,7 @@ suite('FocusManager', function () {
       test('HTML DOM focus()ed then SVG focusNode()ed correctly updates getFocusedNode() and indicators', function () {
         this.focusManager.registerTree(this.testFocusableTree2);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
         document.getElementById('testFocusableTree2.node1').focus();
 
         this.focusManager.focusNode(this.testFocusableGroup2Node1);
@@ -4598,6 +5233,8 @@ suite('FocusManager', function () {
       test('HTML DOM focus()ed then SVG DOM focus()ed correctly updates getFocusedNode() and indicators', function () {
         this.focusManager.registerTree(this.testFocusableTree2);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
         document.getElementById('testFocusableTree2.node1').focus();
 
         document.getElementById('testFocusableGroup2.node1').focus();
@@ -4702,6 +5339,7 @@ suite('FocusManager', function () {
         this.focusManager.registerTree(this.testFocusableTree2);
         this.focusManager.registerTree(this.testFocusableGroup2);
         this.focusManager.focusTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
 
         document.getElementById('testFocusableTree2.node1').focus();
 
@@ -4802,6 +5440,7 @@ suite('FocusManager', function () {
         this.focusManager.registerTree(this.testFocusableTree2);
         this.focusManager.registerTree(this.testFocusableGroup2);
         this.focusManager.focusNode(this.testFocusableGroup2Node1);
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
 
         document.getElementById('testFocusableTree2.node1').focus();
 
@@ -4833,6 +5472,7 @@ suite('FocusManager', function () {
       test('SVG DOM focus()ed then HTML focusTree()ed correctly updates getFocusedTree() and indicators', function () {
         this.focusManager.registerTree(this.testFocusableTree2);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
         document.getElementById('testFocusableGroup2.node1').focus();
 
         this.focusManager.focusTree(this.testFocusableTree2);
@@ -4867,6 +5507,7 @@ suite('FocusManager', function () {
       test('SVG DOM focus()ed then HTML focusNode()ed correctly updates getFocusedNode() and indicators', function () {
         this.focusManager.registerTree(this.testFocusableTree2);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
         document.getElementById('testFocusableGroup2.node1').focus();
 
         this.focusManager.focusNode(this.testFocusableTree2Node1);
@@ -4899,6 +5540,8 @@ suite('FocusManager', function () {
       test('SVG DOM focus()ed then HTML DOM focus()ed correctly updates getFocusedNode() and indicators', function () {
         this.focusManager.registerTree(this.testFocusableTree2);
         this.focusManager.registerTree(this.testFocusableGroup2);
+        document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
+        document.getElementById('testFocusableTree2.node1').tabIndex = -1;
         document.getElementById('testFocusableGroup2.node1').focus();
 
         document.getElementById('testFocusableTree2.node1').focus();
@@ -4933,6 +5576,21 @@ suite('FocusManager', function () {
   /* Ephemeral focus tests. */
 
   suite('takeEphemeralFocus()', function () {
+    setup(function () {
+      // Ensure ephemeral-specific elements are focusable.
+      document.getElementById('nonTreeElementForEphemeralFocus').tabIndex = -1;
+      document.getElementById('nonTreeGroupForEphemeralFocus').tabIndex = -1;
+    });
+    teardown(function () {
+      // Ensure ephemeral-specific elements have their tab indexes reset for a clean state.
+      document
+        .getElementById('nonTreeElementForEphemeralFocus')
+        .removeAttribute('tabindex');
+      document
+        .getElementById('nonTreeGroupForEphemeralFocus')
+        .removeAttribute('tabindex');
+    });
+
     test('with no focused node does not change states', function () {
       this.focusManager.registerTree(this.testFocusableTree2);
       this.focusManager.registerTree(this.testFocusableGroup2);
@@ -5067,6 +5725,7 @@ suite('FocusManager', function () {
       this.focusManager.registerTree(this.testFocusableTree2);
       this.focusManager.registerTree(this.testFocusableGroup2);
       this.focusManager.focusNode(this.testFocusableTree2Node1);
+      document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
       const ephemeralElement = document.getElementById(
         'nonTreeGroupForEphemeralFocus',
       );
@@ -5241,6 +5900,7 @@ suite('FocusManager', function () {
       this.focusManager.registerTree(this.testFocusableTree2);
       this.focusManager.registerTree(this.testFocusableGroup2);
       this.focusManager.focusNode(this.testFocusableTree2Node1);
+      document.getElementById('testFocusableGroup2.node1').tabIndex = -1;
       const ephemeralElement = document.getElementById(
         'nonTreeGroupForEphemeralFocus',
       );
diff --git a/tests/mocha/index.html b/tests/mocha/index.html
index 09ef8820f0e..8b1124d06a6 100644
--- a/tests/mocha/index.html
+++ b/tests/mocha/index.html
@@ -39,97 +39,76 @@
     <div id="mocha"></div>
     <div id="failureCount" style="display: none" tests_failed="unset"></div>
     <div id="failureMessages" style="display: none"></div>
-    <div id="testFocusableTree1" tabindex="-1">
+    <div id="testFocusableTree1">
       Focusable tree 1
-      <div id="testFocusableTree1.node1" style="margin-left: 1em" tabindex="-1">
+      <div id="testFocusableTree1.node1" style="margin-left: 1em">
         Tree 1 node 1
-        <div
-          id="testFocusableTree1.node1.child1"
-          style="margin-left: 2em"
-          tabindex="-1">
+        <div id="testFocusableTree1.node1.child1" style="margin-left: 2em">
           Tree 1 node 1 child 1
           <div
             id="testFocusableTree1.node1.child1.unregisteredChild1"
-            style="margin-left: 3em"
-            tabindex="-1">
+            style="margin-left: 3em">
             Tree 1 node 1 child 1 child 1 (unregistered)
           </div>
         </div>
       </div>
-      <div id="testFocusableTree1.node2" style="margin-left: 1em" tabindex="-1">
+      <div id="testFocusableTree1.node2" style="margin-left: 1em">
         Tree 1 node 2
         <div
           id="testFocusableTree1.node2.unregisteredChild1"
-          style="margin-left: 2em"
-          tabindex="-1">
+          style="margin-left: 2em">
           Tree 1 node 2 child 2 (unregistered)
         </div>
       </div>
-      <div
-        id="testFocusableTree1.unregisteredChild1"
-        style="margin-left: 1em"
-        tabindex="-1">
+      <div id="testFocusableTree1.unregisteredChild1" style="margin-left: 1em">
         Tree 1 child 1 (unregistered)
       </div>
     </div>
-    <div id="testFocusableTree2" tabindex="-1">
+    <div id="testFocusableTree2">
       Focusable tree 2
-      <div id="testFocusableTree2.node1" style="margin-left: 1em" tabindex="-1">
+      <div id="testFocusableTree2.node1" style="margin-left: 1em">
         Tree 2 node 1
-        <div
-          id="testFocusableNestedTree4"
-          style="margin-left: 2em"
-          tabindex="-1">
+        <div id="testFocusableNestedTree4" style="margin-left: 2em">
           Nested tree 4
-          <div
-            id="testFocusableNestedTree4.node1"
-            style="margin-left: 3em"
-            tabindex="-1">
+          <div id="testFocusableNestedTree4.node1" style="margin-left: 3em">
             Tree 4 node 1 (nested)
             <div
               id="testFocusableNestedTree4.node1.unregisteredChild1"
-              style="margin-left: 4em"
-              tabindex="-1">
+              style="margin-left: 4em">
               Tree 4 node 1 child 1 (unregistered)
             </div>
           </div>
         </div>
       </div>
-      <div id="testFocusableNestedTree5" style="margin-left: 1em" tabindex="-1">
+      <div id="testFocusableNestedTree5" style="margin-left: 1em">
         Nested tree 5
-        <div
-          id="testFocusableNestedTree5.node1"
-          style="margin-left: 2em"
-          tabindex="-1">
+        <div id="testFocusableNestedTree5.node1" style="margin-left: 2em">
           Tree 5 node 1 (nested)
         </div>
       </div>
     </div>
-    <div id="testUnregisteredFocusableTree3" tabindex="-1">
+    <div id="testUnregisteredFocusableTree3">
       Unregistered tree 3
-      <div
-        id="testUnregisteredFocusableTree3.node1"
-        style="margin-left: 1em"
-        tabindex="-1">
+      <div id="testUnregisteredFocusableTree3.node1" style="margin-left: 1em">
         Tree 3 node 1 (unregistered)
       </div>
     </div>
     <div id="testUnfocusableElement">Unfocusable element</div>
-    <div id="nonTreeElementForEphemeralFocus" tabindex="-1" />
+    <div id="nonTreeElementForEphemeralFocus" />
     <svg width="250" height="250">
-      <g id="testFocusableGroup1" tabindex="-1">
-        <g id="testFocusableGroup1.node1" tabindex="-1">
+      <g id="testFocusableGroup1">
+        <g id="testFocusableGroup1.node1">
           <rect x="0" y="0" width="250" height="30" fill="grey" />
           <text x="10" y="20" class="svgText">Group 1 node 1</text>
-          <g id="testFocusableGroup1.node1.child1" tabindex="-1">
+          <g id="testFocusableGroup1.node1.child1">
             <rect x="0" y="30" width="250" height="30" fill="lightgrey" />
             <text x="10" y="50" class="svgText">Tree 1 node 1 child 1</text>
           </g>
         </g>
-        <g id="testFocusableGroup1.node2" tabindex="-1">
+        <g id="testFocusableGroup1.node2">
           <rect x="0" y="60" width="250" height="30" fill="grey" />
           <text x="10" y="80" class="svgText">Group 1 node 2</text>
-          <g id="testFocusableGroup1.node2.unregisteredChild1" tabindex="-1">
+          <g id="testFocusableGroup1.node2.unregisteredChild1">
             <rect x="0" y="90" width="250" height="30" fill="lightgrey" />
             <text x="10" y="110" class="svgText">
               Tree 1 node 2 child 2 (unregistered)
@@ -137,27 +116,27 @@
           </g>
         </g>
       </g>
-      <g id="testFocusableGroup2" tabindex="-1">
-        <g id="testFocusableGroup2.node1" tabindex="-1">
+      <g id="testFocusableGroup2">
+        <g id="testFocusableGroup2.node1">
           <rect x="0" y="120" width="250" height="30" fill="grey" />
           <text x="10" y="140" class="svgText">Group 2 node 1</text>
         </g>
-        <g id="testFocusableNestedGroup4" tabindex="-1">
-          <g id="testFocusableNestedGroup4.node1" tabindex="-1">
+        <g id="testFocusableNestedGroup4">
+          <g id="testFocusableNestedGroup4.node1">
             <rect x="0" y="150" width="250" height="30" fill="lightgrey" />
             <text x="10" y="170" class="svgText">Group 4 node 1 (nested)</text>
           </g>
         </g>
       </g>
-      <g id="testUnregisteredFocusableGroup3" tabindex="-1">
-        <g id="testUnregisteredFocusableGroup3.node1" tabindex="-1">
+      <g id="testUnregisteredFocusableGroup3">
+        <g id="testUnregisteredFocusableGroup3.node1">
           <rect x="0" y="180" width="250" height="30" fill="grey" />
           <text x="10" y="200" class="svgText">
             Tree 3 node 1 (unregistered)
           </text>
         </g>
       </g>
-      <g id="nonTreeGroupForEphemeralFocus" tabindex="-1"></g>
+      <g id="nonTreeGroupForEphemeralFocus"></g>
     </svg>
     <!-- Load mocha et al. before Blockly and the test modules so that
          we can safely import the test modules that make calls


Reviews:
user: login: BenHenning
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-28T01:10:30Z
user: login: BenHenning
body: Self-reviewed (mostly a spot check for anything obvious).
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-28T01:39:59Z
user: login: maribethb
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T18:24:34Z
user: login: BenHenning
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-29T01:12:24Z
user: login: BenHenning
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-29T01:15:33Z
user: login: BenHenning
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-29T01:17:55Z
user: login: BenHenning
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-29T01:18:51Z
user: login: BenHenning
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-29T01:30:19Z
user: login: BenHenning
body: Self-reviewed all the latest changes.
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-29T01:34:58Z
user: login: maribethb
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-29T19:05:28Z

Review comments:
diff_hunk: @@ -225,7 +225,6 @@ export class ToolboxCategory
    */
   protected createContainer_(): HTMLDivElement {
     const container = document.createElement('div');
-    container.tabIndex = -1;
path: core/toolbox/category.ts
user: login: BenHenning
body: This is the cause of the bug @rachel-fenichel found.

The problem comes from the fact that there's no automatic focusing when clicking on categories now, whereas there was before. This results in there not being a focused node upon click, so toolbox defaults to its first category when it receives focus (since it has no child receive focus).

It's actually correct to put this line back as the fix. I'll add a line comment here & for separator to try and make it extra clear why this is needed.
created_at: 2025-05-28T01:10:29Z
author_association: COLLABORATOR
diff_hunk: @@ -225,7 +225,6 @@ export class ToolboxCategory
    */
   protected createContainer_(): HTMLDivElement {
     const container = document.createElement('div');
-    container.tabIndex = -1;
path: core/toolbox/category.ts
user: login: BenHenning
body: Fixed in latest.
created_at: 2025-05-28T01:18:56Z
author_association: COLLABORATOR
diff_hunk: @@ -225,6 +225,8 @@ export class ToolboxCategory
    */
   protected createContainer_(): HTMLDivElement {
     const container = document.createElement('div');
+    // Ensure that the category has a tab index to ensure it receives focus when
+    // clicked (since clicking isn't managed by the toolbox).
     container.tabIndex = -1;
path: core/toolbox/category.ts
user: login: BenHenning
body: Maybe consider adding a test for this & the separator cases in core.
created_at: 2025-05-28T01:34:42Z
author_association: COLLABORATOR
diff_hunk: @@ -990,31 +984,29 @@ export abstract class Flyout
 
   /** See IFocusableNode.canBeFocused. */
   canBeFocused(): boolean {
-    return true;
+    return false;
   }
 
   /** See IFocusableTree.getRootFocusableNode. */
   getRootFocusableNode(): IFocusableNode {
-    return this;
+    throw new Error('Flyouts are not directly focusable.');
path: core/flyout_base.ts
user: login: maribethb
body: Can you add a deprecation warning to these so we can remove them in v13?
created_at: 2025-05-28T17:55:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -147,7 +185,16 @@ export class FocusManager {
    * unregisterTree.
    */
   isRegistered(tree: IFocusableTree): boolean {
-    return this.registeredTrees.findIndex((reg) => reg === tree) !== -1;
+    return !!this.lookUpRegistration(tree);
+  }
+
+  /**
+   * Returns the TreeRegistration for the specified tree, or null if the tree is
+   * not currently registered.
+   */
+  private lookUpRegistration(tree: IFocusableTree): TreeRegistration | null {
+    const index = this.registeredTrees.findIndex((reg) => reg.tree === tree);
path: core/focus_manager.ts
user: login: maribethb
body: Why are you using `findIndex` instead of `find` since you're returning the element anyway, not the index?

Though I'm wondering if this should be a map of registered trees to IDs and not an array. Does the order in which trees are registered matter?
created_at: 2025-05-28T17:58:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -240,11 +300,15 @@ export class FocusManager {
    * canBeFocused() method returns false), it will be ignored and any existing
    * focus state will remain unchanged.
    *
+   * Note that this may update the specified node's element's tabindex to ensure
+   * that it can be properly read out by screenreaders while focused.
+   *
    * @param focusableNode The node that should receive active focus.
    */
   focusNode(focusableNode: IFocusableNode): void {
     this.ensureManagerIsUnlocked();
-    if (!this.currentlyHoldsEphemeralFocus) {
+    const mustRestoreUpdatingNodeFlag = !this.currentlyHoldsEphemeralFocus;
path: core/focus_manager.ts
user: login: maribethb
body: nit: don't use the word `flag` in the name, we already know it's a boolean
created_at: 2025-05-28T18:04:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -448,14 +521,29 @@ export class FocusManager {
     // node's focusable element (which *is* allowed to be invisible until the
     // node needs to be focused).
     this.lockFocusStateChanges = true;
-    if (node.getFocusableTree() !== prevTree) {
-      node.getFocusableTree().onTreeFocus(node, prevTree);
+    const tree = node.getFocusableTree();
+    const elem = node.getFocusableElement();
+    const nextTreeReg = this.lookUpRegistration(tree);
+    const treeIsTabManaged = nextTreeReg?.rootShouldBeAutoTabbable;
+    if (tree !== prevTree) {
+      tree.onTreeFocus(node, prevTree);
+
+      if (treeIsTabManaged) {
+        // If this node's tree has its tab auto-managed, ensure that it's no
+        // longer tabbable now that it holds active focus.
+        tree.getRootFocusableNode().getFocusableElement().tabIndex = -1;
+      }
     }
     node.onNodeFocus();
     this.lockFocusStateChanges = false;
 
+    // Only overwrite the tab index if it isn't a tree root that's auto managed.
path: core/focus_manager.ts
user: login: maribethb
body: I find this confusing, could you explain it and consider rewriting the comment?

You say you're going to overwrite the tab index, but you only set it if there wasn't a tab index attribute to start with. It's hard for me to call that overwriting. I think you have to add this because you can't focus an element that doesn't have a tabindex, and you removed the tabindex from a bunch of the html for core blockly elements like fields. Is that right? If so, that would be helpful context to put in the comment instead of saying "overwrite"


On a separate confusion, it would help if you explained why you don't set it if it *is* a tree root that's auto managed. Is the only reason because you already set it to -1 above?
created_at: 2025-05-28T18:16:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -225,6 +225,8 @@ export class ToolboxCategory
    */
   protected createContainer_(): HTMLDivElement {
     const container = document.createElement('div');
+    // Ensure that the category has a tab index to ensure it receives focus when
+    // clicked (since clicking isn't managed by the toolbox).
     container.tabIndex = -1;
path: core/toolbox/category.ts
user: login: BenHenning
body: Decided to add a test in kbe instead as it was much easier (ran into a lot of problems with click/focus in toolbox's test unfortunately). Didn't bother testing separator as it seems less important.
created_at: 2025-05-29T01:12:24Z
author_association: COLLABORATOR
diff_hunk: @@ -990,31 +984,29 @@ export abstract class Flyout
 
   /** See IFocusableNode.canBeFocused. */
   canBeFocused(): boolean {
-    return true;
+    return false;
   }
 
   /** See IFocusableTree.getRootFocusableNode. */
   getRootFocusableNode(): IFocusableNode {
-    return this;
+    throw new Error('Flyouts are not directly focusable.');
path: core/flyout_base.ts
user: login: BenHenning
body: Done in latest.
created_at: 2025-05-29T01:15:33Z
author_association: COLLABORATOR
diff_hunk: @@ -147,7 +185,16 @@ export class FocusManager {
    * unregisterTree.
    */
   isRegistered(tree: IFocusableTree): boolean {
-    return this.registeredTrees.findIndex((reg) => reg === tree) !== -1;
+    return !!this.lookUpRegistration(tree);
+  }
+
+  /**
+   * Returns the TreeRegistration for the specified tree, or null if the tree is
+   * not currently registered.
+   */
+  private lookUpRegistration(tree: IFocusableTree): TreeRegistration | null {
+    const index = this.registeredTrees.findIndex((reg) => reg.tree === tree);
path: core/focus_manager.ts
user: login: BenHenning
body: No reason not to use `find` honestly--updated.

As for order, it doesn't matter\* and the size of the array will be quite small (there aren't many trees). I'm also not keen on relying on the stability of the tree's root's element ID for registration correctness (though it should technically be reliable per contracts).

\* The order can theoretically matter, but only if trees are violating their contracts.
created_at: 2025-05-29T01:17:54Z
author_association: COLLABORATOR
diff_hunk: @@ -240,11 +300,15 @@ export class FocusManager {
    * canBeFocused() method returns false), it will be ignored and any existing
    * focus state will remain unchanged.
    *
+   * Note that this may update the specified node's element's tabindex to ensure
+   * that it can be properly read out by screenreaders while focused.
+   *
    * @param focusableNode The node that should receive active focus.
    */
   focusNode(focusableNode: IFocusableNode): void {
     this.ensureManagerIsUnlocked();
-    if (!this.currentlyHoldsEphemeralFocus) {
+    const mustRestoreUpdatingNodeFlag = !this.currentlyHoldsEphemeralFocus;
path: core/focus_manager.ts
user: login: BenHenning
body: Ah nice catch--surprised I did that. Fixed!
created_at: 2025-05-29T01:18:51Z
author_association: COLLABORATOR
diff_hunk: @@ -448,14 +521,29 @@ export class FocusManager {
     // node's focusable element (which *is* allowed to be invisible until the
     // node needs to be focused).
     this.lockFocusStateChanges = true;
-    if (node.getFocusableTree() !== prevTree) {
-      node.getFocusableTree().onTreeFocus(node, prevTree);
+    const tree = node.getFocusableTree();
+    const elem = node.getFocusableElement();
+    const nextTreeReg = this.lookUpRegistration(tree);
+    const treeIsTabManaged = nextTreeReg?.rootShouldBeAutoTabbable;
+    if (tree !== prevTree) {
+      tree.onTreeFocus(node, prevTree);
+
+      if (treeIsTabManaged) {
+        // If this node's tree has its tab auto-managed, ensure that it's no
+        // longer tabbable now that it holds active focus.
+        tree.getRootFocusableNode().getFocusableElement().tabIndex = -1;
+      }
     }
     node.onNodeFocus();
     this.lockFocusStateChanges = false;
 
+    // Only overwrite the tab index if it isn't a tree root that's auto managed.
path: core/focus_manager.ts
user: login: BenHenning
body: This is, admittedly, a very confusing comment. :)


> ... I think you have to add this because you can't focus an element that doesn't have a tabindex, and you removed the tabindex from a bunch of the html for core blockly elements like fields. Is that right? If so, that would be helpful context to put in the comment instead of saying "overwrite"

This is correct. It's documented in `IFocusableNode` (where I think it's much more important to explain), but I completely agree that 'overwrite' is the wrong terminology here and the context is important. I've tried to clarify the different cases that this bit affects, and why.

> On a separate confusion, it would help if you explained why you don't set it if it _is_ a tree root that's auto managed. Is the only reason because you already set it to -1 above?

Yep, this is the reason, and I've attempted to also clarify it in the latest changes.

Hopefully this is much more clear now.
created_at: 2025-05-29T01:30:19Z
author_association: COLLABORATOR

PR comments:
user: login: rachel-fenichel
created_at: 2025-05-23T01:10:57Z
body: @BenHenning what are the next steps for this to be mergeable?
author_association: COLLABORATOR
user: login: BenHenning
created_at: 2025-05-27T19:24:08Z
body: > @BenHenning what are the next steps for this to be mergeable?

@rachel-fenichel it can probably go into review now (and I will do that later today). I want to spend a bit of time seeing if I can get automatically managed node tab indexes working before fully giving up on that.

Edit: Actually there is one bug I'm re-remembering now that I'm testing this locally that still needs to be fixed yet (sometimes the flyout doesn't auto-collapse).
author_association: COLLABORATOR
user: login: BenHenning
created_at: 2025-05-27T19:28:32Z
body: The following scenario breaks still:
- Tab-navigate to the workspace
- Press 'T' to open the flyout (via toolbox focus)
- Press 'right' to navigate to the flyout
- Press 'tab'
- Observe that there's a weird in-between state with the flyout still focused but the workspace having an outline (local debugging confirms that the workspace svg holds DOM focus but the `FocusManager` still thinks the flyout block should hold active focus)

This is related to https://github.com/google/blockly-keyboard-experimentation/issues/547.
author_association: COLLABORATOR
user: login: BenHenning
created_at: 2025-05-27T20:31:30Z
body: Uh oh. `FocusManager` can get stuck in a state updating the focused node which basically disables DOM syncing. 🙃 This could cause all sorts of interesting issues. I'll fix it in this PR alongside the main issue.
author_association: COLLABORATOR
user: login: BenHenning
created_at: 2025-05-27T21:35:18Z
body: NB: This should be fully working now, but tests & some clean-up need to come yet.

I also think that I'm going to be a bit stricter on tab management in `FocusManager`. Right now trees need to make themselves focusable but non-tabbable if they disable auto management, but nodes no longer need to. This inconsistency feels weird and a bit bad--it's preferable just to have `FocusManager` be entirely responsible for managing tabs, I think.

Edit: Actually, I just realized that `focusNode` already automatically solves this, so setting a tree's root to a `-1` tab index is simply redundant. Cool.
author_association: COLLABORATOR
user: login: BenHenning
created_at: 2025-05-27T22:45:39Z
body: Decided to include a fix for #8970 since I was in the neighborhood.
author_association: COLLABORATOR
user: login: BenHenning
created_at: 2025-05-27T23:53:30Z
body: From @rachel-fenichel's testing:

> Interacting with the workspace and then clicking on a toolbox category opens the first category instead of the one I clicked
author_association: COLLABORATOR
user: login: BenHenning
created_at: 2025-05-28T01:43:18Z
body: Things left to do in/for this PR:
- [x] Add the tests mentioned above for toolbox. Added 1 new test in https://github.com/google/blockly-keyboard-experimentation/pull/557.
- [x] Fix behaviors to get the keyboard nav tests to pass. Fix was simple: need to use `focusNode()` now instead of `.focus()`.
- [x] Add new keyboard nav tests. Done in https://github.com/google/blockly-keyboard-experimentation/pull/557.
- [x] Verify core webdriver tests with these changes. This is tricky since they're still failing at tip-of-tree (I see 28-29 pass on each run without these changes). The results appear the same with these changes. Unfortunately some of the failing tests are related to toolbox categories so we can't test those effectively.
- [x] ~Add video demonstrations of each of the linked bugs now being fixed.~ (skipping since the behavior can be observed via the tests).
- [x] Fix that clicking out of the injection div doesn't auto-close the `Flyout` when it's focused (but does work fine when toolbox is focused). Found by @microbit-robert. Fixed this without too much trouble, but still need to add a test for it. Tests have been added to keyboard nav and are now tracked as part of that item above.
- [x] Address feedback & get back into review (after everything else is done).
author_association: COLLABORATOR
user: login: BenHenning
created_at: 2025-05-28T01:43:36Z
body: I think this can be reviewed now, though please note there will need to probably be 1 more pass once all of the above items are addressed.
author_association: COLLABORATOR
user: login: BenHenning
created_at: 2025-05-28T22:25:59Z
body: NB: The plan to fix #9105 is no longer part of this PR. It's not blocking MakeCode (since they use their own flyout implementation), and the fix is actually likely to be a bit tricky with the way we automatically open/close the flyout based on focus.
author_association: COLLABORATOR
user: login: BenHenning
created_at: 2025-05-29T01:36:02Z
body: With https://github.com/google/blockly-keyboard-experimentation/pull/557 in place and all the tasks listed above addressed (including reviewer comments), I think this is ready again for review. Fortunately, there weren't many changes needed here.

PTAL @maribethb.
author_association: COLLABORATOR
user: login: BenHenning
created_at: 2025-05-29T19:10:14Z
body: Thanks @maribethb for the review, and @rachel-fenichel for helping with testing!
author_association: COLLABORATOR