Files
blockly/core/utils/focusable_tree_traverser.ts
Ben Henning d9beacddb4 feat: add FocusManager
This is the bulk of the work for introducing the central logical unit
for managing and sychronizing focus as a first-class Blockly concept
with that of DOM focus.

There's a lot to do yet, including:
- Ensuring clicks within Blockly's scope correctly sync back to focus
  changes.
- Adding support for, and testing, cases when focus is lost from all
  registered trees.
- Testing nested tree propagation.
- Testing the traverser utility class.
- Adding implementations for IFocusableTree and IFocusableNode
  throughout Blockly.
2025-03-21 00:33:51 +00:00

85 lines
3.2 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {IFocusableNode} from '../interfaces/i_focusable_node.js';
import type {IFocusableTree} from '../interfaces/i_focusable_tree.js';
/**
* A helper utility for IFocusableTree implementations to aid with common
* tree traversals.
*/
export class FocusableTreeTraverser {
/**
* Returns the current IFocusableNode that either has the CSS class
* 'blocklyActiveFocus' or 'blocklyPassiveFocus', only considering HTML and
* SVG elements.
*
* This can match against the tree's root.
*
* @param tree The IFocusableTree in which to search for a focused node.
* @returns The IFocusableNode currently with focus, or null if none.
*/
static findFocusedNode(tree: IFocusableTree): IFocusableNode | null {
const root = tree.getRootFocusableNode().getFocusableElement();
const activeElem = root.querySelector('.blocklyActiveFocus');
let active: IFocusableNode | null = null;
if (activeElem instanceof HTMLElement || activeElem instanceof SVGElement) {
active = tree.findFocusableNodeFor(activeElem);
}
const passiveElems = Array.from(
root.querySelectorAll('.blocklyPassiveFocus'),
);
const passive = passiveElems.map((elem) => {
if (elem instanceof HTMLElement || elem instanceof SVGElement) {
return tree.findFocusableNodeFor(elem);
} else return null;
});
return active || passive.find((node) => !!node) || null;
}
/**
* Returns the IFocusableNode corresponding to the specified HTML or SVG
* element iff it's the root element or a descendent of the root element of
* the specified IFocusableTree.
*
* If the tree contains another nested IFocusableTree, the nested tree may be
* traversed but its nodes will never be returned here per the contract of
* findChildById.
*
* findChildById is a provided callback that takes an element ID and maps it
* back to the corresponding IFocusableNode within the provided
* IFocusableTree. These IDs will match the contract specified in the
* documentation for IFocusableNode. This function must not return any node
* that doesn't directly belong to the node's nearest parent tree.
*
* @param element The HTML or SVG element being sought.
* @param tree The tree under which the provided element may be a descendant.
* @param findChildById The ID->IFocusableNode mapping callback that must
* follow the contract mentioned above.
* @returns The matching IFocusableNode, or null if there is no match.
*/
static findFocusableNodeFor(
element: HTMLElement | SVGElement,
tree: IFocusableTree,
findChildById: (id: string) => IFocusableNode | null,
): IFocusableNode | null {
if (element === tree.getRootFocusableNode().getFocusableElement()) {
return tree.getRootFocusableNode();
}
const matchedChildNode = findChildById(element.id);
const elementParent = element.parentElement;
if (!matchedChildNode && elementParent) {
// Recurse up to find the nearest tree/node.
return FocusableTreeTraverser.findFocusableNodeFor(
elementParent,
tree,
findChildById,
);
}
return matchedChildNode;
}
}