mirror of
https://github.com/google/blockly.git
synced 2026-01-08 09:30:06 +01:00
## The basics - [x] I [validated my changes](https://developers.google.com/blockly/guides/contribute/core#making_and_verifying_a_change) ## The details ### Resolves Fixes #8994 ### Proposed Changes This removes an error that was previously thrown by `FocusManager` when attempting to focus an invalid node (such as one that's been removed from its parent). ### Reason for Changes https://github.com/google/blockly/issues/8994#issuecomment-2855447539 goes into more detail. While this error did cover legitimately wrong cases to try and focus things (and helped to catch some real problems), fixing this 'properly' may become a leaky boat problem where we have to track down every possible asynchronous scenario that could produce such a case. One class of this is ephemeral focus which had robustness improvements itself in #8981 that, by effect, caused this issue in the first place. Holistically fixing this with enforced API contracts alone isn't simple due to the nature of how these components interact. This change ensures that there's a sane default to fall back on if an invalid node is passed in. Note that `FocusManager` was designed specifically to disallow defocusing a node (since fallbacks can get messy and introduce unpredictable user experiences), and this sort of allows that now. However, this seems like a reasonable approach as it defaults to the behavior when focusing a tree explicitly which allows the tree to fallback to a more suitable default (such as the first item to select in the toolbox for that particular tree). In many cases this will default back to the tree's root node (such as the workspace root group) since sometimes the removed node is still the "last focused node" of the tree (and is considered valid for the purpose of determining a fallback; tree implementations could further specialize by checking whether that node is still valid). ### Test Coverage Some new tests were added to cover this case, but more may be useful to add as part of #8910. ### Documentation No documentation needs to be added or updated as part of this (beyond code documentation changes). ### Additional Information This original issue was found by @RoboErikG when testing #8995. I also verified this against the keyboard navigation plugin repository.
145 lines
5.7 KiB
TypeScript
145 lines
5.7 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import type {IFocusableNode} from './i_focusable_node.js';
|
|
|
|
/**
|
|
* Represents a tree of focusable elements with its own active/passive focus
|
|
* context.
|
|
*
|
|
* Note that focus is handled by FocusManager, and tree implementations can have
|
|
* at most one IFocusableNode focused at one time. If the tree itself has focus,
|
|
* then the tree's focused node is considered 'active' ('passive' if another
|
|
* tree has focus).
|
|
*
|
|
* Focus is shared between one or more trees, where each tree can have exactly
|
|
* one active or passive node (and only one active node can exist on the whole
|
|
* page at any given time). The idea of passive focus is to provide context to
|
|
* users on where their focus will be restored upon navigating back to a
|
|
* previously focused tree.
|
|
*
|
|
* Note that if the tree's current focused node (passive or active) is needed,
|
|
* FocusableTreeTraverser.findFocusedNode can be used.
|
|
*
|
|
* Note that if specific nodes are needed to be retrieved for this tree, either
|
|
* use lookUpFocusableNode or FocusableTreeTraverser.findFocusableNodeFor.
|
|
*/
|
|
export interface IFocusableTree {
|
|
/**
|
|
* Returns the top-level focusable node of the tree.
|
|
*
|
|
* It's expected that the returned node will be focused in cases where
|
|
* FocusManager wants to focus a tree in a situation where it does not
|
|
* currently have a focused node.
|
|
*/
|
|
getRootFocusableNode(): IFocusableNode;
|
|
|
|
/**
|
|
* Returns the IFocusableNode of this tree that should receive active focus
|
|
* when the tree itself has focus returned to it.
|
|
*
|
|
* There are some very important notes to consider about a tree's focus
|
|
* lifecycle when implementing a version of this method that doesn't return
|
|
* null:
|
|
* 1. A null previousNode does not guarantee first-time focus state as nodes
|
|
* can be deleted.
|
|
* 2. This method is only used when the tree itself is focused, either through
|
|
* tab navigation or via FocusManager.focusTree(). In many cases, the
|
|
* previously focused node will be directly focused instead which will
|
|
* bypass this method.
|
|
* 3. The default behavior (i.e. returning null here) involves either
|
|
* restoring the previous node (previousNode) or focusing the tree's root.
|
|
* 4. The provided node may sometimes no longer be valid, such as in the case
|
|
* an attempt is made to focus a node that has been recently removed from
|
|
* its parent tree. Implementations can check for the validity of the node
|
|
* in order to specialize the node to which focus should fall back.
|
|
*
|
|
* This method is largely intended to provide tree implementations with the
|
|
* means of specifying a better default node than their root.
|
|
*
|
|
* @param previousNode The node that previously held passive focus for this
|
|
* tree, or null if the tree hasn't yet been focused.
|
|
* @returns The IFocusableNode that should now receive focus, or null if
|
|
* default behavior should be used, instead.
|
|
*/
|
|
getRestoredFocusableNode(
|
|
previousNode: IFocusableNode | null,
|
|
): IFocusableNode | null;
|
|
|
|
/**
|
|
* Returns all directly nested trees under this tree.
|
|
*
|
|
* Note that the returned list of trees doesn't need to be stable, however all
|
|
* returned trees *do* need to be registered with FocusManager. Additionally,
|
|
* this must return actual nested trees as omitting a nested tree will affect
|
|
* how focus changes map to a specific node and its tree, potentially leading
|
|
* to user confusion.
|
|
*/
|
|
getNestedTrees(): Array<IFocusableTree>;
|
|
|
|
/**
|
|
* Returns the IFocusableNode corresponding to the specified element ID, or
|
|
* null if there's no exact node within this tree with that ID or if the ID
|
|
* corresponds to the root of the tree.
|
|
*
|
|
* This will never match against nested trees.
|
|
*
|
|
* @param id The ID of the node's focusable HTMLElement or SVGElement.
|
|
*/
|
|
lookUpFocusableNode(id: string): IFocusableNode | null;
|
|
|
|
/**
|
|
* Called when a node of this tree has received active focus.
|
|
*
|
|
* Note that a null previousTree does not necessarily indicate that this is
|
|
* the first time Blockly is receiving focus. In fact, few assumptions can be
|
|
* made about previous focus state as a previous null tree simply indicates
|
|
* that Blockly did not hold active focus prior to this tree becoming focused
|
|
* (which can happen due to focus exiting the Blockly injection div, or for
|
|
* other cases like ephemeral focus).
|
|
*
|
|
* See IFocusableNode.onNodeFocus() as implementations have the same
|
|
* restrictions as with that method.
|
|
*
|
|
* @param node The node receiving active focus.
|
|
* @param previousTree The previous tree that held active focus, or null if
|
|
* none.
|
|
*/
|
|
onTreeFocus(node: IFocusableNode, previousTree: IFocusableTree | null): void;
|
|
|
|
/**
|
|
* Called when the previously actively focused node of this tree is now
|
|
* passively focused and there is no other active node of this tree taking its
|
|
* place.
|
|
*
|
|
* This has the same implementation restrictions and considerations as
|
|
* onTreeFocus().
|
|
*
|
|
* @param nextTree The next tree receiving active focus, or null if none (such
|
|
* as in the case that Blockly is entirely losing DOM focus).
|
|
*/
|
|
onTreeBlur(nextTree: IFocusableTree | null): void;
|
|
}
|
|
|
|
/**
|
|
* Determines whether the provided object fulfills the contract of
|
|
* IFocusableTree.
|
|
*
|
|
* @param object The object to test.
|
|
* @returns Whether the provided object can be used as an IFocusableTree.
|
|
*/
|
|
export function isFocusableTree(object: any | null): object is IFocusableTree {
|
|
return (
|
|
object &&
|
|
'getRootFocusableNode' in object &&
|
|
'getRestoredFocusableNode' in object &&
|
|
'getNestedTrees' in object &&
|
|
'lookUpFocusableNode' in object &&
|
|
'onTreeFocus' in object &&
|
|
'onTreeBlur' in object
|
|
);
|
|
}
|