diff --git a/core/focus_manager.ts b/core/focus_manager.ts index 3d0a9347f85..392b2e87c47 100644 --- a/core/focus_manager.ts +++ b/core/focus_manager.ts @@ -74,6 +74,22 @@ export class FocusManager { */ static readonly PASSIVE_FOCUS_NODE_CSS_CLASS_NAME = 'blocklyPassiveFocus'; + static readonly ACTIVE_FOCUS_WITHIN_TREE_CSS_CLASS_NAME = + 'blocklyTreeHasActiveFocus'; + + static readonly PASSIVE_FOCUS_WITHIN_TREE_CSS_CLASS_NAME = + 'blocklyTreeHasPassiveFocus'; + + static readonly ACTIVE_FOCUS_WITHIN_SUBTREE_CSS_CLASS_NAME = + 'blocklySubtreeHasActiveFocus'; + + static readonly PASSIVE_FOCUS_WITHIN_SUBTREE_CSS_CLASS_NAME = + 'blocklySubtreeHasPassiveFocus'; + + // Represents the single node that will receive active focus when ephemeral focus ends. + static readonly WAITING_FOR_EPHEMERAL_FOCUS_CSS_CLASS_NAME = + 'blocklyWaitingForEphemeralFocus'; + private focusedNode: IFocusableNode | null = null; private previouslyFocusedNode: IFocusableNode | null = null; private registeredTrees: Array = []; @@ -365,6 +381,12 @@ export class FocusManager { const prevTree = prevNode?.getFocusableTree(); if (prevNode) { this.passivelyFocusNode(prevNode, nextTree); + if (this.currentlyHoldsEphemeralFocus) { + dom.removeClass( + prevNode.getFocusableElement(), + FocusManager.WAITING_FOR_EPHEMERAL_FOCUS_CSS_CLASS_NAME, + ); + } } // If there's a focused node in the new node's tree, ensure it's reset. @@ -382,6 +404,11 @@ export class FocusManager { if (!this.currentlyHoldsEphemeralFocus) { // Only change the actively focused node if ephemeral state isn't held. this.activelyFocusNode(nodeToFocus, prevTree ?? null); + } else { + dom.addClass( + nodeToFocus.getFocusableElement(), + FocusManager.WAITING_FOR_EPHEMERAL_FOCUS_CSS_CLASS_NAME, + ); } this.updateFocusedNode(nodeToFocus); if (mustRestoreUpdatingNode) { @@ -422,6 +449,10 @@ export class FocusManager { if (this.focusedNode) { this.passivelyFocusNode(this.focusedNode, null); + dom.addClass( + this.focusedNode.getFocusableElement(), + FocusManager.WAITING_FOR_EPHEMERAL_FOCUS_CSS_CLASS_NAME, + ); } focusableElement.focus(); @@ -438,6 +469,10 @@ export class FocusManager { if (this.focusedNode) { this.activelyFocusNode(this.focusedNode, null); + dom.removeClass( + this.focusedNode.getFocusableElement(), + FocusManager.WAITING_FOR_EPHEMERAL_FOCUS_CSS_CLASS_NAME, + ); // Even though focus was restored, check if it's lost again. It's // possible for the browser to force focus away from all elements once @@ -539,6 +574,16 @@ export class FocusManager { // longer tabbable now that it holds active focus. tree.getRootFocusableNode().getFocusableElement().tabIndex = -1; } + + const treeRoot = tree.getRootFocusableNode().getFocusableElement(); + dom.addClass( + treeRoot, + FocusManager.ACTIVE_FOCUS_WITHIN_TREE_CSS_CLASS_NAME, + ); + dom.removeClass( + treeRoot, + FocusManager.PASSIVE_FOCUS_WITHIN_TREE_CSS_CLASS_NAME, + ); } node.onNodeFocus(); this.lockFocusStateChanges = false; @@ -588,6 +633,16 @@ export class FocusManager { // tabbable since it no longer holds active focus. tree.getRootFocusableNode().getFocusableElement().tabIndex = 0; } + + const treeRoot = tree.getRootFocusableNode().getFocusableElement(); + dom.addClass( + treeRoot, + FocusManager.PASSIVE_FOCUS_WITHIN_TREE_CSS_CLASS_NAME, + ); + dom.removeClass( + treeRoot, + FocusManager.ACTIVE_FOCUS_WITHIN_TREE_CSS_CLASS_NAME, + ); } node.onNodeBlur(); this.lockFocusStateChanges = false; @@ -633,6 +688,16 @@ export class FocusManager { dom.removeClass(element, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); } + private recomputeSubtreeCssClasses(): void { + // Collect all focused elements. + // const passiveElems = document.querySelectorAll( + // FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + // ); + // const activeElem = this.focusedNode?.getFocusableElement() ?? null; + // const focusedElems = [...(activeElem ? [activeElem] : []), ...passiveElems]; + // For each element, collect all... TODO: finish. + } + private static focusManager: FocusManager | null = null; /**