From d86af0356abba235de640bd85448bbe963832f1e Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 13 May 2025 23:49:36 +0000 Subject: [PATCH 1/3] feat: Add CSS classes for more focus states. This adds better tracking support and is not yet completed. --- core/focus_manager.ts | 57 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/core/focus_manager.ts b/core/focus_manager.ts index c0139aec08d..a062ed32d37 100644 --- a/core/focus_manager.ts +++ b/core/focus_manager.ts @@ -56,6 +56,21 @@ 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'; + + static readonly WAITING_FOR_EPHEMERAL_FOCUS_CSS_CLASS_NAME = + 'blocklyWaitingForEphemeralFocus'; + private focusedNode: IFocusableNode | null = null; private previouslyFocusedNode: IFocusableNode | null = null; private registeredTrees: Array = []; @@ -324,9 +339,16 @@ export class FocusManager { } this.currentlyHoldsEphemeralFocus = true; + const currentFocusedElement = this.focusedNode?.getFocusableElement(); if (this.focusedNode) { this.passivelyFocusNode(this.focusedNode, null); } + if (currentFocusedElement) { + dom.addClass( + currentFocusedElement, + FocusManager.WAITING_FOR_EPHEMERAL_FOCUS_CSS_CLASS_NAME, + ); + } focusableElement.focus(); let hasFinishedEphemeralFocus = false; @@ -340,6 +362,13 @@ export class FocusManager { hasFinishedEphemeralFocus = true; this.currentlyHoldsEphemeralFocus = false; + if (currentFocusedElement) { + dom.removeClass( + currentFocusedElement, + FocusManager.WAITING_FOR_EPHEMERAL_FOCUS_CSS_CLASS_NAME, + ); + } + if (this.focusedNode) { this.activelyFocusNode(this.focusedNode, null); @@ -424,8 +453,18 @@ 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 nodeTree = node.getFocusableTree(); + if (nodeTree !== prevTree) { + const treeRoot = nodeTree.getRootFocusableNode().getFocusableElement(); + nodeTree.onTreeFocus(node, prevTree); + 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; @@ -451,8 +490,18 @@ export class FocusManager { nextTree: IFocusableTree | null, ): void { this.lockFocusStateChanges = true; - if (node.getFocusableTree() !== nextTree) { - node.getFocusableTree().onTreeBlur(nextTree); + const nodeTree = node.getFocusableTree(); + if (nodeTree !== nextTree) { + const treeRoot = nodeTree.getRootFocusableNode().getFocusableElement(); + nodeTree.onTreeBlur(nextTree); + 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; From 2981857d2b6b0dfc5a1299ac31ca3b849383647c Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 15 May 2025 20:58:13 +0000 Subject: [PATCH 2/3] chore: Start adding some of the subtree support. This is not finished or working yet. --- core/focus_manager.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/focus_manager.ts b/core/focus_manager.ts index a062ed32d37..6dc61427ac3 100644 --- a/core/focus_manager.ts +++ b/core/focus_manager.ts @@ -547,6 +547,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; /** From 70e2a8bb418b7658ff9bdbc331758c343b6fb286 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 27 Jun 2025 01:09:57 +0000 Subject: [PATCH 3/3] fix: Make ephemeral focus CSS update correctly. This ensures that it updates when the focused node changes. This will, of course, need thorough testing due to the bunch of possible edge cases. --- core/focus_manager.ts | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/core/focus_manager.ts b/core/focus_manager.ts index c1683a958f3..392b2e87c47 100644 --- a/core/focus_manager.ts +++ b/core/focus_manager.ts @@ -86,6 +86,7 @@ export class FocusManager { 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'; @@ -380,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. @@ -397,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) { @@ -435,13 +447,10 @@ export class FocusManager { } this.currentlyHoldsEphemeralFocus = true; - const currentFocusedElement = this.focusedNode?.getFocusableElement(); if (this.focusedNode) { this.passivelyFocusNode(this.focusedNode, null); - } - if (currentFocusedElement) { dom.addClass( - currentFocusedElement, + this.focusedNode.getFocusableElement(), FocusManager.WAITING_FOR_EPHEMERAL_FOCUS_CSS_CLASS_NAME, ); } @@ -458,15 +467,12 @@ export class FocusManager { hasFinishedEphemeralFocus = true; this.currentlyHoldsEphemeralFocus = false; - if (currentFocusedElement) { + if (this.focusedNode) { + this.activelyFocusNode(this.focusedNode, null); dom.removeClass( - currentFocusedElement, + this.focusedNode.getFocusableElement(), FocusManager.WAITING_FOR_EPHEMERAL_FOCUS_CSS_CLASS_NAME, ); - } - - if (this.focusedNode) { - this.activelyFocusNode(this.focusedNode, null); // 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 @@ -684,11 +690,11 @@ export class FocusManager { 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]; - + // 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. }