mirror of
https://github.com/google/blockly.git
synced 2026-01-11 02:47:09 +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 #9043 Fixes https://github.com/google/blockly-samples/issues/2512 ### Proposed Changes This replaces using BlockSvg's own ID for focus management since that's not guaranteed to be unique across all workspaces on the page. ### Reason for Changes Both https://github.com/google/blockly-samples/issues/2512 covers the user-facing issue in more detail, but from a technical perspective it's possible for blocks to share IDs across workspaces. One easy demonstration of this is the flyout: the first block created from the flyout to the main workspace will share an ID. The workspace minimap plugin just makes the underlying problem more obvious. The reason this introduces a breakage is due to the inherent ordering that `FocusManager` uses when trying to find a matching tree for a given DOM element that has received focus. These trees are iterated in the order of their registration, so it's quite possible for some cases (like main workspace vs. flyout) to resolve such that the behavior looks correct to users, vs. others (such as the workspace minimap) not behaving as expected. Guaranteeing ID uniqueness across all workspaces fixes the problem entirely. ### Test Coverage This has been manually tested in core Blockly's simple test playground and in Blockly samples' workspace minimap plugin test environment (linked against this change). See the new behavior for the minimap plugin: [Screen recording 2025-05-13 4.31.31 PM.webm](https://github.com/user-attachments/assets/d2ec3621-6e86-4932-ae85-333b0e7015e1) Note that this is a regression to v11 behavior in that the blocks in the minimap now show as selected. This has been verified as working with the latest version of the keyboard navigation plugin (tip-of-tree). Keyboard-based block operations and movement seem to work as expected. For automated testing this is expected to largely be covered by future tests added as part of resolving #8915. ### Documentation No public documentation changes should be needed, though `IFocusableNode`'s documentation has been refined to be clearer on the uniqueness property for focusable element IDs. ### Additional Information There's a separate open design question here about whether `BlockSvg`'s descendants should use the new focus ID vs. the block ID. Here is what I consider to be the trade-off analysis in this decision: | | Pros | Cons | |------------------------|-------------------------------------------------|------------------------------------------------------------------------------| | Use `BlockSvg.id` | Can use fast `WorkspaceSvg.getBlockById`. | `WorkspaceSvg.lookUpFocusableNode` now uses 2 different IDs. | | Use `BlockSvg.focusId` | Consistency in IDs use for block-related focus. | Requires more expensive block look-up in `WorkspaceSvg.lookUpFocusableNode`. |
116 lines
4.7 KiB
TypeScript
116 lines
4.7 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import type {IFocusableTree} from './i_focusable_tree.js';
|
|
|
|
/** Represents anything that can have input focus. */
|
|
export interface IFocusableNode {
|
|
/**
|
|
* Returns the DOM element that can be explicitly requested to receive focus.
|
|
*
|
|
* IMPORTANT: Please note that this element is expected to have a visual
|
|
* presence on the page as it will both be explicitly focused and have its
|
|
* style changed depending on its current focus state (i.e. blurred, actively
|
|
* focused, and passively focused). The element will have one of two styles
|
|
* attached (where no style indicates blurred/not focused):
|
|
* - 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 be visible if the node is ever focused via
|
|
* FocusManager.focusNode() or FocusManager.focusTree(). It's allowed for an
|
|
* element to be hidden until onNodeFocus() is called, or become hidden with a
|
|
* call to onNodeBlur().
|
|
*
|
|
* 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).
|
|
*
|
|
* @returns The HTMLElement or SVGElement which can both receive focus and be
|
|
* visually represented as actively or passively focused for this node.
|
|
*/
|
|
getFocusableElement(): HTMLElement | SVGElement;
|
|
|
|
/**
|
|
* Returns the closest parent tree of this node (in cases where a tree has
|
|
* distinct trees underneath it), which represents the tree to which this node
|
|
* belongs.
|
|
*
|
|
* @returns The node's IFocusableTree.
|
|
*/
|
|
getFocusableTree(): IFocusableTree;
|
|
|
|
/**
|
|
* Called when this node receives active focus.
|
|
*
|
|
* Note that it's fine for implementations to change visibility modifiers, but
|
|
* they should avoid the following:
|
|
* - Creating or removing DOM elements (including via the renderer or drawer).
|
|
* - Affecting focus via DOM focus() calls or the FocusManager.
|
|
*/
|
|
onNodeFocus(): void;
|
|
|
|
/**
|
|
* Called when this node loses active focus. It may still have passive focus.
|
|
*
|
|
* This has the same implementation restrictions as onNodeFocus().
|
|
*/
|
|
onNodeBlur(): void;
|
|
|
|
/**
|
|
* Indicates whether this node allows focus. If this returns false then none
|
|
* of the other IFocusableNode methods will be called.
|
|
*
|
|
* Note that special care must be taken if implementations of this function
|
|
* dynamically change their return value value over the lifetime of the node
|
|
* as certain environment conditions could affect the focusability of this
|
|
* node's DOM element (such as whether the element has a positive or zero
|
|
* tabindex). Also, changing from a true to a false value while the node holds
|
|
* focus will not immediately change the current focus of the node nor
|
|
* FocusManager's internal state, and thus may result in some of the node's
|
|
* functions being called later on when defocused (since it was previously
|
|
* considered focusable at the time of being focused).
|
|
*
|
|
* Implementations should generally always return true here unless there are
|
|
* circumstances under which this node should be skipped for focus
|
|
* considerations. Examples may include being disabled, read-only, a purely
|
|
* visual decoration, or a node with no visual representation that must
|
|
* implement this interface (e.g. due to a parent interface extending it).
|
|
* Keep in mind accessibility best practices when determining whether a node
|
|
* should be focusable since even disabled and read-only elements are still
|
|
* often relevant to providing organizational context to users (particularly
|
|
* when using a screen reader).
|
|
*
|
|
* @returns Whether this node can be focused by FocusManager.
|
|
*/
|
|
canBeFocused(): boolean;
|
|
}
|
|
|
|
/**
|
|
* Determines whether the provided object fulfills the contract of
|
|
* IFocusableNode.
|
|
*
|
|
* @param object The object to test.
|
|
* @returns Whether the provided object can be used as an IFocusableNode.
|
|
*/
|
|
export function isFocusableNode(object: any | null): object is IFocusableNode {
|
|
return (
|
|
object &&
|
|
'getFocusableElement' in object &&
|
|
'getFocusableTree' in object &&
|
|
'onNodeFocus' in object &&
|
|
'onNodeBlur' in object &&
|
|
'canBeFocused' in object
|
|
);
|
|
}
|