diff --git a/core/blockly.ts b/core/blockly.ts index a743ca5a7..cf77bca3f 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -140,6 +140,8 @@ import { } from './interfaces/i_draggable.js'; import {IDragger} from './interfaces/i_dragger.js'; import {IFlyout} from './interfaces/i_flyout.js'; +import {IFocusableNode} from './interfaces/i_focusable_node.js'; +import {IFocusableTree} from './interfaces/i_focusable_tree.js'; import {IHasBubble, hasBubble} from './interfaces/i_has_bubble.js'; import {IIcon, isIcon} from './interfaces/i_icon.js'; import {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.js'; @@ -544,6 +546,8 @@ export { IDragger, IFlyout, IFlyoutInflater, + IFocusableNode, + IFocusableTree, IHasBubble, IIcon, IKeyboardAccessible, diff --git a/core/interfaces/i_focusable_node.ts b/core/interfaces/i_focusable_node.ts new file mode 100644 index 000000000..87a0293ae --- /dev/null +++ b/core/interfaces/i_focusable_node.ts @@ -0,0 +1,36 @@ +/** + * @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 to the + * element relative to its nearest IFocusableTree parent. + * + * It's expected the return element will not change for the lifetime of the + * 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. + */ + getFocusableTree(): IFocusableTree; +} diff --git a/core/interfaces/i_focusable_tree.ts b/core/interfaces/i_focusable_tree.ts new file mode 100644 index 000000000..21f87678d --- /dev/null +++ b/core/interfaces/i_focusable_tree.ts @@ -0,0 +1,53 @@ +/** + * @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. + */ +export interface IFocusableTree { + /** + * Returns the current node with focus in this tree, or null if none (or if + * the root has focus). + * + * Note that this will never return a node from a nested sub-tree as that tree + * should specifically be called in order to retrieve its focused node. + */ + getFocusedNode(): IFocusableNode | null; + + /** + * 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 corresponding to the select element, or null if + * the element does not have such a node. + * + * The provided element must have a non-null ID that conforms to the contract + * mentioned in IFocusableNode. + */ + findFocusableNodeFor( + element: HTMLElement | SVGElement, + ): IFocusableNode | null; +}