diff --git a/core/blockly.ts b/core/blockly.ts index e14a89e74..46ea1fcaf 100644 --- a/core/blockly.ts +++ b/core/blockly.ts @@ -106,6 +106,11 @@ import {FlyoutItem} from './flyout_item.js'; import {FlyoutMetricsManager} from './flyout_metrics_manager.js'; import {FlyoutSeparator} from './flyout_separator.js'; import {VerticalFlyout} from './flyout_vertical.js'; +import { + FocusManager, + ReturnEphemeralFocus, + getFocusManager, +} from './focus_manager.js'; import {CodeGenerator} from './generator.js'; import {Gesture} from './gesture.js'; import {Grid} from './grid.js'; @@ -519,6 +524,7 @@ export { FlyoutItem, FlyoutMetricsManager, FlyoutSeparator, + FocusManager, CodeGenerator as Generator, Gesture, Grid, @@ -583,6 +589,7 @@ export { Names, Options, RenderedConnection, + ReturnEphemeralFocus, Scrollbar, ScrollbarPair, SeparatorFlyoutInflater, @@ -604,6 +611,7 @@ export { WorkspaceSvg, ZoomControls, config, + getFocusManager, hasBubble, icons, inject, diff --git a/core/css.ts b/core/css.ts index 9b3cbd94a..eec45df1a 100644 --- a/core/css.ts +++ b/core/css.ts @@ -494,4 +494,13 @@ input[type=number] { .blocklyDragging .blocklyIconGroup { cursor: grabbing; } + +.blocklyActiveFocus { + outline-color: #2ae; + outline-width: 2px; +} +.blocklyPassiveFocus { + outline-color: #3fdfff; + outline-width: 1.5px; +} `; diff --git a/core/focus_manager.ts b/core/focus_manager.ts new file mode 100644 index 000000000..c1fc295b9 --- /dev/null +++ b/core/focus_manager.ts @@ -0,0 +1,333 @@ +/** + * @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'; +import * as dom from './utils/dom.js'; +import {FocusableTreeTraverser} from './utils/focusable_tree_traverser.js'; + +/** + * Type declaration for returning focus to FocusManager upon completing an + * ephemeral UI flow (such as a dialog). + * + * See FocusManager.takeEphemeralFocus for more details. + */ +export type ReturnEphemeralFocus = () => void; + +/** + * A per-page singleton that manages Blockly focus across one or more + * IFocusableTrees, and bidirectionally synchronizes this focus with the DOM. + * + * Callers that wish to explicitly change input focus for select Blockly + * components on the page should use the focus functions in this manager. + * + * The manager is responsible for handling focus events from the DOM (which may + * may arise from users clicking on page elements) and ensuring that + * corresponding IFocusableNodes are clearly marked as actively/passively + * highlighted in the same way that this would be represented with calls to + * focusNode(). + */ +export class FocusManager { + /** + * The CSS class assigned to IFocusableNode elements that presently have + * active DOM and Blockly focus. + * + * This should never be used directly. Instead, rely on FocusManager to ensure + * nodes have active focus (either automatically through DOM focus or manually + * through the various focus* methods provided by this class). + * + * It's recommended to not query using this class name, either. Instead, use + * FocusableTreeTraverser or IFocusableTree's methods to find a specific node. + */ + static readonly ACTIVE_FOCUS_NODE_CSS_CLASS_NAME = 'blocklyActiveFocus'; + + /** + * The CSS class assigned to IFocusableNode elements that presently have + * passive focus (that is, they were the most recent node in their relative + * tree to have active focus--see ACTIVE_FOCUS_NODE_CSS_CLASS_NAME--and will + * receive active focus again if their surrounding tree is requested to become + * focused, i.e. using focusTree below). + * + * See ACTIVE_FOCUS_NODE_CSS_CLASS_NAME for caveats and limitations around + * using this constant directly (generally it never should need to be used). + */ + static readonly PASSIVE_FOCUS_NODE_CSS_CLASS_NAME = 'blocklyPassiveFocus'; + + focusedNode: IFocusableNode | null = null; + registeredTrees: Array = []; + + private currentlyHoldsEphemeralFocus: boolean = false; + + constructor( + addGlobalEventListener: (type: string, listener: EventListener) => void, + ) { + // Register root document focus listeners for tracking when focus leaves all + // tracked focusable trees. + addGlobalEventListener('focusin', (event) => { + if (!(event instanceof FocusEvent)) return; + + // The target that now has focus. + const activeElement = document.activeElement; + let newNode: IFocusableNode | null | undefined = null; + if ( + activeElement instanceof HTMLElement || + activeElement instanceof SVGElement + ) { + // If the target losing focus maps to any tree, then it should be + // updated. Per the contract of findFocusableNodeFor only one tree + // should claim the element. + for (const tree of this.registeredTrees) { + newNode = FocusableTreeTraverser.findFocusableNodeFor( + activeElement, + tree, + ); + if (newNode) break; + } + } + + if (newNode) { + this.focusNode(newNode); + } else { + this.defocusCurrentFocusedNode(); + } + }); + } + + /** + * Registers a new IFocusableTree for automatic focus management. + * + * If the tree currently has an element with DOM focus, it will not affect the + * internal state in this manager until the focus changes to a new, + * now-monitored element/node. + * + * This function throws if the provided tree is already currently registered + * in this manager. Use isRegistered to check in cases when it can't be + * certain whether the tree has been registered. + */ + registerTree(tree: IFocusableTree): void { + if (this.isRegistered(tree)) { + throw Error(`Attempted to re-register already registered tree: ${tree}.`); + } + this.registeredTrees.push(tree); + } + + /** + * Returns whether the specified tree has already been registered in this + * manager using registerTree and hasn't yet been unregistered using + * unregisterTree. + */ + isRegistered(tree: IFocusableTree): boolean { + return this.registeredTrees.findIndex((reg) => reg === tree) !== -1; + } + + /** + * Unregisters a IFocusableTree from automatic focus management. + * + * If the tree had a previous focused node, it will have its highlight + * removed. This function does NOT change DOM focus. + * + * This function throws if the provided tree is not currently registered in + * this manager. + */ + unregisterTree(tree: IFocusableTree): void { + if (!this.isRegistered(tree)) { + throw Error(`Attempted to unregister not registered tree: ${tree}.`); + } + const treeIndex = this.registeredTrees.findIndex((tree) => tree === tree); + this.registeredTrees.splice(treeIndex, 1); + + const focusedNode = FocusableTreeTraverser.findFocusedNode(tree); + const root = tree.getRootFocusableNode(); + if (focusedNode) this.removeHighlight(focusedNode); + if (this.focusedNode === focusedNode || this.focusedNode === root) { + this.focusedNode = null; + } + this.removeHighlight(root); + } + + /** + * Returns the current IFocusableTree that has focus, or null if none + * currently do. + * + * Note also that if ephemeral focus is currently captured (e.g. using + * takeEphemeralFocus) then the returned tree here may not currently have DOM + * focus. + */ + getFocusedTree(): IFocusableTree | null { + return this.focusedNode?.getFocusableTree() ?? null; + } + + /** + * Returns the current IFocusableNode with focus (which is always tied to a + * focused IFocusableTree), or null if there isn't one. + * + * Note that this function will maintain parity with + * IFocusableTree.getFocusedNode(). That is, if a tree itself has focus but + * none of its non-root children do, this will return null but + * getFocusedTree() will not. + * + * Note also that if ephemeral focus is currently captured (e.g. using + * takeEphemeralFocus) then the returned node here may not currently have DOM + * focus. + */ + getFocusedNode(): IFocusableNode | null { + return this.focusedNode; + } + + /** + * Focuses the specific IFocusableTree. This either means restoring active + * focus to the tree's passively focused node, or focusing the tree's root + * node. + * + * Note that if the specified tree already has a focused node then this will + * not change any existing focus (unless that node has passive focus, then it + * will be restored to active focus). + * + * See getFocusedNode for details on how other nodes are affected. + * + * @param focusableTree The tree that should receive active + * focus. + */ + focusTree(focusableTree: IFocusableTree): void { + if (!this.isRegistered(focusableTree)) { + throw Error(`Attempted to focus unregistered tree: ${focusableTree}.`); + } + const currNode = FocusableTreeTraverser.findFocusedNode(focusableTree); + this.focusNode(currNode ?? focusableTree.getRootFocusableNode()); + } + + /** + * Focuses DOM input on the selected node, and marks it as actively focused. + * + * Any previously focused node will be updated to be passively highlighted (if + * it's in a different focusable tree) or blurred (if it's in the same one). + * + * @param focusableNode The node that should receive active + * focus. + */ + focusNode(focusableNode: IFocusableNode): void { + const nextTree = focusableNode.getFocusableTree(); + if (!this.isRegistered(nextTree)) { + throw Error(`Attempted to focus unregistered node: ${focusableNode}.`); + } + const prevNode = this.focusedNode; + if (prevNode && prevNode.getFocusableTree() !== nextTree) { + this.setNodeToPassive(prevNode); + } + // If there's a focused node in the new node's tree, ensure it's reset. + const prevNodeNextTree = FocusableTreeTraverser.findFocusedNode(nextTree); + const nextTreeRoot = nextTree.getRootFocusableNode(); + if (prevNodeNextTree) { + this.removeHighlight(prevNodeNextTree); + } + // For caution, ensure that the root is always reset since getFocusedNode() + // is expected to return null if the root was highlighted, if the root is + // not the node now being set to active. + if (nextTreeRoot !== focusableNode) { + this.removeHighlight(nextTreeRoot); + } + if (!this.currentlyHoldsEphemeralFocus) { + // Only change the actively focused node if ephemeral state isn't held. + this.setNodeToActive(focusableNode); + } + this.focusedNode = focusableNode; + } + + /** + * Ephemerally captures focus for a selected element until the returned lambda + * is called. This is expected to be especially useful for ephemeral UI flows + * like dialogs. + * + * IMPORTANT: the returned lambda *must* be called, otherwise automatic focus + * will no longer work anywhere on the page. It is highly recommended to tie + * the lambda call to the closure of the corresponding UI so that if input is + * manually changed to an element outside of the ephemeral UI, the UI should + * close and automatic input restored. Note that this lambda must be called + * exactly once and that subsequent calls will throw an error. + * + * Note that the manager will continue to track DOM input signals even when + * ephemeral focus is active, but it won't actually change node state until + * the returned lambda is called. Additionally, only 1 ephemeral focus context + * can be active at any given time (attempting to activate more than one + * simultaneously will result in an error being thrown). + */ + takeEphemeralFocus( + focusableElement: HTMLElement | SVGElement, + ): ReturnEphemeralFocus { + if (this.currentlyHoldsEphemeralFocus) { + throw Error( + `Attempted to take ephemeral focus when it's already held, ` + + `with new element: ${focusableElement}.`, + ); + } + this.currentlyHoldsEphemeralFocus = true; + + if (this.focusedNode) { + this.setNodeToPassive(this.focusedNode); + } + focusableElement.focus(); + + let hasFinishedEphemeralFocus = false; + return () => { + if (hasFinishedEphemeralFocus) { + throw Error( + `Attempted to finish ephemeral focus twice for element: ` + + `${focusableElement}.`, + ); + } + hasFinishedEphemeralFocus = true; + this.currentlyHoldsEphemeralFocus = false; + + if (this.focusedNode) { + this.setNodeToActive(this.focusedNode); + } + }; + } + + private defocusCurrentFocusedNode(): void { + // The current node will likely be defocused while ephemeral focus is held, + // but internal manager state shouldn't change since the node should be + // restored upon exiting ephemeral focus mode. + if (this.focusedNode && !this.currentlyHoldsEphemeralFocus) { + this.setNodeToPassive(this.focusedNode); + this.focusedNode = null; + } + } + + private setNodeToActive(node: IFocusableNode): void { + const element = node.getFocusableElement(); + dom.addClass(element, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + dom.removeClass(element, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + element.focus(); + } + + private setNodeToPassive(node: IFocusableNode): void { + const element = node.getFocusableElement(); + dom.removeClass(element, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + dom.addClass(element, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + } + + private removeHighlight(node: IFocusableNode): void { + const element = node.getFocusableElement(); + dom.removeClass(element, FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + dom.removeClass(element, FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + } +} + +let focusManager: FocusManager | null = null; + +/** + * Returns the page-global FocusManager. + * + * The returned instance is guaranteed to not change across function calls, but + * may change across page loads. + */ +export function getFocusManager(): FocusManager { + if (!focusManager) { + focusManager = new FocusManager(document.addEventListener); + } + return focusManager; +} diff --git a/core/interfaces/i_focusable_node.ts b/core/interfaces/i_focusable_node.ts index 87a0293ae..14100d44c 100644 --- a/core/interfaces/i_focusable_node.ts +++ b/core/interfaces/i_focusable_node.ts @@ -20,7 +20,10 @@ export interface IFocusableNode { * - blocklyPassiveFocus * * The returned element must also have a valid ID specified, and unique to the - * element relative to its nearest IFocusableTree parent. + * element relative to its nearest IFocusableTree parent. It 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). * * It's expected the return element will not change for the lifetime of the * node. diff --git a/core/interfaces/i_focusable_tree.ts b/core/interfaces/i_focusable_tree.ts index 21f87678d..bc0c38849 100644 --- a/core/interfaces/i_focusable_tree.ts +++ b/core/interfaces/i_focusable_tree.ts @@ -20,17 +20,14 @@ import type {IFocusableNode} from './i_focusable_node.js'; * 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 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. * @@ -41,13 +38,24 @@ export interface IFocusableTree { getRootFocusableNode(): IFocusableNode; /** - * Returns the IFocusableNode corresponding to the select element, or null if - * the element does not have such a node. + * Returns all directly nested trees under this tree. * - * The provided element must have a non-null ID that conforms to the contract - * mentioned in IFocusableNode. + * 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. */ - findFocusableNodeFor( - element: HTMLElement | SVGElement, - ): IFocusableNode | null; + getNestedTrees(): Array; + + /** + * 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; } diff --git a/core/utils/focusable_tree_traverser.ts b/core/utils/focusable_tree_traverser.ts new file mode 100644 index 000000000..94603edd0 --- /dev/null +++ b/core/utils/focusable_tree_traverser.ts @@ -0,0 +1,119 @@ +/** + * @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'; +import * as dom from '../utils/dom.js'; + +/** + * A helper utility for IFocusableTree implementations to aid with common + * tree traversals. + */ +export class FocusableTreeTraverser { + private static readonly ACTIVE_CLASS_NAME = 'blocklyActiveFocus'; + private static readonly PASSIVE_CSS_CLASS_NAME = 'blocklyPassiveFocus'; + private static readonly ACTIVE_FOCUS_NODE_CSS_SELECTOR = `.${FocusableTreeTraverser.ACTIVE_CLASS_NAME}`; + private static readonly PASSIVE_FOCUS_NODE_CSS_SELECTOR = `.${FocusableTreeTraverser.PASSIVE_CSS_CLASS_NAME}`; + + /** + * Returns the current IFocusableNode that is styled (and thus represented) as + * having either passive or active focus, only considering HTML and SVG + * elements. + * + * This can match against the tree's root. + * + * Note that this will never return a node from a nested sub-tree as that tree + * should specifically be used to retrieve its focused node. + * + * @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(); + if ( + dom.hasClass(root, FocusableTreeTraverser.ACTIVE_CLASS_NAME) || + dom.hasClass(root, FocusableTreeTraverser.PASSIVE_CSS_CLASS_NAME) + ) { + // The root has focus. + return tree.getRootFocusableNode(); + } + + const activeEl = root.querySelector(this.ACTIVE_FOCUS_NODE_CSS_SELECTOR); + if (activeEl instanceof HTMLElement || activeEl instanceof SVGElement) { + const active = FocusableTreeTraverser.findFocusableNodeFor( + activeEl, + tree, + ); + if (active) return active; + } + + // At most there should be one passive indicator per tree (not considering + // subtrees). + const passiveEl = root.querySelector(this.PASSIVE_FOCUS_NODE_CSS_SELECTOR); + if (passiveEl instanceof HTMLElement || passiveEl instanceof SVGElement) { + const passive = FocusableTreeTraverser.findFocusableNodeFor( + passiveEl, + tree, + ); + if (passive) return passive; + } + + return 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 element exists within the specified tree's DOM structure but does + * not directly correspond to a node, the nearest parent node (or the tree's + * root) will be returned to represent the provided element. + * + * 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 + * IFocusableTree.lookUpFocusableNode. + * + * The provided element must have a non-null ID that conforms to the contract + * mentioned in IFocusableNode. + * + * @param element The HTML or SVG element being sought. + * @param tree The tree under which the provided element may be a descendant. + * @returns The matching IFocusableNode, or null if there is no match. + */ + static findFocusableNodeFor( + element: HTMLElement | SVGElement, + tree: IFocusableTree, + ): IFocusableNode | null { + // First, match against subtrees. + const subTreeMatches = tree.getNestedTrees().map((tree) => { + return FocusableTreeTraverser.findFocusableNodeFor(element, tree); + }); + if (subTreeMatches.findIndex((match) => !!match) !== -1) { + // At least one subtree has a match for the element so it cannot be part + // of the outer tree. + return null; + } + + // Second, check against the tree's root. + if (element === tree.getRootFocusableNode().getFocusableElement()) { + return tree.getRootFocusableNode(); + } + + // Third, check if the element has a node. + const matchedChildNode = tree.lookUpFocusableNode(element.id) ?? null; + if (matchedChildNode) return matchedChildNode; + + // Fourth, recurse up to find the nearest tree/node if it's possible. + const elementParent = element.parentElement; + if (!matchedChildNode && elementParent) { + return FocusableTreeTraverser.findFocusableNodeFor(elementParent, tree); + } + + // Otherwise, there's no matching node. + return null; + } +} diff --git a/tests/mocha/focus_manager_test.js b/tests/mocha/focus_manager_test.js new file mode 100644 index 000000000..4a3f6b3ad --- /dev/null +++ b/tests/mocha/focus_manager_test.js @@ -0,0 +1,5096 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + FocusManager, + getFocusManager, +} from '../../build/src/core/focus_manager.js'; +import {assert} from '../../node_modules/chai/chai.js'; +import { + sharedTestSetup, + sharedTestTeardown, +} from './test_helpers/setup_teardown.js'; + +class FocusableNodeImpl { + constructor(element, tree) { + this.element = element; + this.tree = tree; + } + + getFocusableElement() { + return this.element; + } + + getFocusableTree() { + return this.tree; + } +} + +class FocusableTreeImpl { + constructor(rootElement, nestedTrees) { + this.nestedTrees = nestedTrees; + this.idToNodeMap = {}; + this.rootNode = this.addNode(rootElement); + } + + addNode(element) { + const node = new FocusableNodeImpl(element, this); + this.idToNodeMap[element.id] = node; + return node; + } + + getRootFocusableNode() { + return this.rootNode; + } + + getNestedTrees() { + return this.nestedTrees; + } + + lookUpFocusableNode(id) { + return this.idToNodeMap[id]; + } +} + +suite('FocusManager', function () { + const ACTIVE_FOCUS_NODE_CSS_SELECTOR = `.${FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME}`; + const PASSIVE_FOCUS_NODE_CSS_SELECTOR = `.${FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME}`; + + setup(function () { + sharedTestSetup.call(this); + + const testState = this; + const addDocumentEventListener = function (type, listener) { + testState.globalDocumentEventListenerType = type; + testState.globalDocumentEventListener = listener; + document.addEventListener(type, listener); + }; + this.focusManager = new FocusManager(addDocumentEventListener); + + const createFocusableTree = function (rootElementId, nestedTrees) { + return new FocusableTreeImpl( + document.getElementById(rootElementId), + nestedTrees || [], + ); + }; + const createFocusableNode = function (tree, elementId) { + return tree.addNode(document.getElementById(elementId)); + }; + + this.testFocusableTree1 = createFocusableTree('testFocusableTree1'); + this.testFocusableTree1Node1 = createFocusableNode( + this.testFocusableTree1, + 'testFocusableTree1.node1', + ); + this.testFocusableTree1Node1Child1 = createFocusableNode( + this.testFocusableTree1, + 'testFocusableTree1.node1.child1', + ); + this.testFocusableTree1Node2 = createFocusableNode( + this.testFocusableTree1, + 'testFocusableTree1.node2', + ); + this.testFocusableNestedTree4 = createFocusableTree( + 'testFocusableNestedTree4', + ); + this.testFocusableNestedTree4Node1 = createFocusableNode( + this.testFocusableNestedTree4, + 'testFocusableNestedTree4.node1', + ); + this.testFocusableNestedTree5 = createFocusableTree( + 'testFocusableNestedTree5', + ); + this.testFocusableNestedTree5Node1 = createFocusableNode( + this.testFocusableNestedTree5, + 'testFocusableNestedTree5.node1', + ); + this.testFocusableTree2 = createFocusableTree('testFocusableTree2', [ + this.testFocusableNestedTree4, + this.testFocusableNestedTree5, + ]); + this.testFocusableTree2Node1 = createFocusableNode( + this.testFocusableTree2, + 'testFocusableTree2.node1', + ); + + this.testFocusableGroup1 = createFocusableTree('testFocusableGroup1'); + this.testFocusableGroup1Node1 = createFocusableNode( + this.testFocusableGroup1, + 'testFocusableGroup1.node1', + ); + this.testFocusableGroup1Node1Child1 = createFocusableNode( + this.testFocusableGroup1, + 'testFocusableGroup1.node1.child1', + ); + this.testFocusableGroup1Node2 = createFocusableNode( + this.testFocusableGroup1, + 'testFocusableGroup1.node2', + ); + this.testFocusableNestedGroup4 = createFocusableTree( + 'testFocusableNestedGroup4', + ); + this.testFocusableNestedGroup4Node1 = createFocusableNode( + this.testFocusableNestedGroup4, + 'testFocusableNestedGroup4.node1', + ); + this.testFocusableGroup2 = createFocusableTree('testFocusableGroup2', [ + this.testFocusableNestedGroup4, + ]); + this.testFocusableGroup2Node1 = createFocusableNode( + this.testFocusableGroup2, + 'testFocusableGroup2.node1', + ); + }); + + teardown(function () { + sharedTestTeardown.call(this); + + // Remove the globally registered listener from FocusManager to avoid state being shared across + // test boundaries. + const eventType = this.globalDocumentEventListenerType; + const eventListener = this.globalDocumentEventListener; + document.removeEventListener(eventType, eventListener); + + // Ensure all node CSS styles are reset so that state isn't leaked between tests. + const activeElems = document.querySelectorAll( + ACTIVE_FOCUS_NODE_CSS_SELECTOR, + ); + const passiveElems = document.querySelectorAll( + PASSIVE_FOCUS_NODE_CSS_SELECTOR, + ); + for (const elem of activeElems) { + elem.classList.remove(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + } + for (const elem of passiveElems) { + elem.classList.remove(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + } + + // Reset the current active element. + document.body.focus(); + }); + + assert.includesClass = function (classList, className) { + assert.isTrue( + classList.contains(className), + 'Expected class list to include: ' + className, + ); + }; + + assert.notIncludesClass = function (classList, className) { + assert.isFalse( + classList.contains(className), + 'Expected class list to not include: ' + className, + ); + }; + + /* Basic lifecycle tests. */ + + suite('registerTree()', function () { + test('once does not throw', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + // The test should pass due to no exception being thrown. + }); + + test('twice for same tree throws error', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + const errorMsgRegex = + /Attempted to re-register already registered tree.+?/; + assert.throws( + () => this.focusManager.registerTree(this.testFocusableTree1), + errorMsgRegex, + ); + }); + + test('twice with different trees does not throw', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableGroup1); + + // The test shouldn't throw since two different trees were registered. + }); + + test('register after an unregister does not throw', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.unregisterTree(this.testFocusableTree1); + + this.focusManager.registerTree(this.testFocusableTree1); + + // The second register should not fail since the tree was previously unregistered. + }); + }); + + suite('unregisterTree()', function () { + test('for not yet registered tree throws', function () { + const errorMsgRegex = /Attempted to unregister not registered tree.+?/; + assert.throws( + () => this.focusManager.unregisterTree(this.testFocusableTree1), + errorMsgRegex, + ); + }); + + test('for registered tree does not throw', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Unregistering a registered tree should not fail. + }); + + test('twice for registered tree throws', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.unregisterTree(this.testFocusableTree1); + + const errorMsgRegex = /Attempted to unregister not registered tree.+?/; + assert.throws( + () => this.focusManager.unregisterTree(this.testFocusableTree1), + errorMsgRegex, + ); + }); + }); + + suite('isRegistered()', function () { + test('for not registered tree returns false', function () { + const isRegistered = this.focusManager.isRegistered( + this.testFocusableTree1, + ); + + assert.isFalse(isRegistered); + }); + + test('for registered tree returns true', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + const isRegistered = this.focusManager.isRegistered( + this.testFocusableTree1, + ); + + assert.isTrue(isRegistered); + }); + + test('for unregistered tree returns false', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.unregisterTree(this.testFocusableTree1); + + const isRegistered = this.focusManager.isRegistered( + this.testFocusableTree1, + ); + + assert.isFalse(isRegistered); + }); + + test('for re-registered tree returns true', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.unregisterTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree1); + + const isRegistered = this.focusManager.isRegistered( + this.testFocusableTree1, + ); + + assert.isTrue(isRegistered); + }); + }); + + suite('getFocusedTree()', function () { + test('by default returns null', function () { + const focusedTree = this.focusManager.getFocusedTree(); + + assert.isNull(focusedTree); + }); + }); + + suite('getFocusedNode()', function () { + test('by default returns null', function () { + const focusedNode = this.focusManager.getFocusedNode(); + + assert.isNull(focusedNode); + }); + }); + + suite('focusTree()', function () { + test('for not registered tree throws', function () { + const errorMsgRegex = /Attempted to focus unregistered tree.+?/; + assert.throws( + () => this.focusManager.focusTree(this.testFocusableTree1), + errorMsgRegex, + ); + }); + + test('for unregistered tree throws', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.unregisterTree(this.testFocusableTree1); + + const errorMsgRegex = /Attempted to focus unregistered tree.+?/; + assert.throws( + () => this.focusManager.focusTree(this.testFocusableTree1), + errorMsgRegex, + ); + }); + }); + + suite('focusNode()', function () { + test('for not registered node throws', function () { + const errorMsgRegex = /Attempted to focus unregistered node.+?/; + assert.throws( + () => this.focusManager.focusNode(this.testFocusableTree1Node1), + errorMsgRegex, + ); + }); + + test('for unregistered node throws', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.unregisterTree(this.testFocusableTree1); + + const errorMsgRegex = /Attempted to focus unregistered node.+?/; + assert.throws( + () => this.focusManager.focusNode(this.testFocusableTree1Node1), + errorMsgRegex, + ); + }); + + test('focuses element', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.strictEqual(document.activeElement, nodeElem); + }); + + test('fires focusin event', function () { + let focusCount = 0; + const focusListener = () => focusCount++; + document.addEventListener('focusin', focusListener); + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + document.removeEventListener('focusin', focusListener); + + // There should be exactly 1 focus event fired from focusNode(). + assert.strictEqual(focusCount, 1); + }); + }); + + suite('getFocusManager()', function () { + test('returns non-null manager', function () { + const manager = getFocusManager(); + + assert.isNotNull(manager); + }); + + test('returns the exact same instance in subsequent calls', function () { + const manager1 = getFocusManager(); + const manager2 = getFocusManager(); + + assert.strictEqual(manager2, manager1); + }); + }); + + /* Focus tests for HTML trees. */ + + suite('focus*() switching in HTML tree', function () { + suite('getFocusedTree()', function () { + test('registered tree focusTree()ed no prev focus returns tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusTree(this.testFocusableTree1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test('registered tree focusTree()ed prev node focused returns tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusTree(this.testFocusableTree1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test('registered tree focusTree()ed diff tree prev focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusTree(this.testFocusableTree1); + + this.focusManager.focusTree(this.testFocusableTree2); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test('registered tree focusTree()ed diff tree node prev focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusTree(this.testFocusableTree2); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test('registered root focusNode()ed no prev focus returns tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode( + this.testFocusableTree1.getRootFocusableNode(), + ); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test("registered node focusNode()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test("registered subnode focusNode()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1Child1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test('registered node focusNode()ed after prev node focus returns same tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node2); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test("registered node focusNode()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test("registered tree root focusNode()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode( + this.testFocusableTree2.getRootFocusableNode(), + ); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test('unregistered tree focusTree()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusTree(this.testFocusableTree1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focusNode()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focusNode()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focusNode()ed with prev node recently focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the most recent tree still exists, it still has focus. + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test('nested tree focusTree()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + this.focusManager.focusTree(this.testFocusableNestedTree4); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedTree4, + ); + }); + + test('nested tree node focusNode()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedTree4, + ); + }); + + test('nested tree node focusNode()ed after parent focused returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedTree4, + ); + }); + }); + suite('getFocusedNode()', function () { + test('registered tree focusTree()ed no prev focus returns root node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusTree(this.testFocusableTree1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1.getRootFocusableNode(), + ); + }); + + test('registered tree focusTree()ed prev node focused returns original node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusTree(this.testFocusableTree1); + + // The original node retains focus since the tree already holds focus (per focusTree's + // contract). + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node1, + ); + }); + + test('registered tree focusTree()ed diff tree prev focused returns new root node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusTree(this.testFocusableTree1); + + this.focusManager.focusTree(this.testFocusableTree2); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2.getRootFocusableNode(), + ); + }); + + test('registered tree focusTree()ed diff tree node prev focused returns new root node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusTree(this.testFocusableTree2); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2.getRootFocusableNode(), + ); + }); + + test('registered root focusNode()ed no prev focus returns root node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode( + this.testFocusableTree1.getRootFocusableNode(), + ); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1.getRootFocusableNode(), + ); + }); + + test('registered node focusNode()ed no prev focus returns node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node1, + ); + }); + + test('registered subnode focusNode()ed no prev focus returns subnode', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1Child1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node1Child1, + ); + }); + + test('registered node focusNode()ed after prev node focus returns new node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node2); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node2, + ); + }); + + test('registered node focusNode()ed after prev node focus diff tree returns new node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + }); + + test('registered tree root focusNode()ed after prev node focus diff tree returns new root', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode( + this.testFocusableTree2.getRootFocusableNode(), + ); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2.getRootFocusableNode(), + ); + }); + + test('unregistered tree focusTree()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusTree(this.testFocusableTree1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focusNode()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focusNode()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focusNode()ed with prev node recently focused returns new node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the most recent tree still exists, it still has focus. + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + }); + + test('nested tree focusTree()ed with no prev focus returns nested root', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + this.focusManager.focusTree(this.testFocusableNestedTree4); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedTree4.getRootFocusableNode(), + ); + }); + + test('nested tree node focusNode()ed with no prev focus returns focused node', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedTree4Node1, + ); + }); + + test('nested tree node focusNode()ed after parent focused returns focused node', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedTree4Node1, + ); + }); + }); + suite('CSS classes', function () { + test('registered tree focusTree()ed no prev focus root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusTree(this.testFocusableTree1); + + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree focusTree()ed prev node focused original elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusTree(this.testFocusableTree1); + + // The original node retains active focus since the tree already holds focus (per + // focusTree's contract). + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree focusTree()ed diff tree prev focused new root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusTree(this.testFocusableTree1); + + this.focusManager.focusTree(this.testFocusableTree2); + + const rootElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree focusTree()ed diff tree node prev focused new root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusTree(this.testFocusableTree2); + + const rootElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered root focusNode()ed no prev focus returns root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode( + this.testFocusableTree1.getRootFocusableNode(), + ); + + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focusNode()ed no prev focus node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focusNode()ed after prev node focus same tree old node elem has no focus property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node2); + + const prevNodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focusNode()ed after prev node focus same tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node2); + + const newNodeElem = this.testFocusableTree1Node2.getFocusableElement(); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focusNode()ed after prev node focus diff tree old node elem has passive property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + const prevNodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focusNode()ed after prev node focus diff tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + const newNodeElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree root focusNode()ed after prev node focus diff tree new root has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.focusNode( + this.testFocusableTree2.getRootFocusableNode(), + ); + + const rootElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focusTree()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusTree(this.testFocusableTree1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focusNode()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focusNode()ed with prev node prior removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + this.focusManager.focusNode(this.testFocusableTree1Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. However, the old node + // should still have passive indication. + const otherNodeElem = + this.testFocusableTree2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableTree1Node1.getFocusableElement(); + assert.includesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focusNode()ed with prev node recently removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. However, the new node + // should still have active indication. + const otherNodeElem = + this.testFocusableTree2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableTree1Node1.getFocusableElement(); + assert.includesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('focusNode() multiple nodes in same tree with switches ensure passive focus has gone', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node2); + + // When switching back to the first tree, ensure the original passive node is no longer + // passive now that the new node is active. + const node1 = this.testFocusableTree1Node1.getFocusableElement(); + const node2 = this.testFocusableTree1Node2.getFocusableElement(); + assert.notIncludesClass( + node1.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + node2.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree focusTree()ed other tree node passively focused tree node now has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree1Node1); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusTree(this.testFocusableTree1); + + // The original node in the tree should be moved from passive to active focus per the + // contract of focusTree). Also, the root of the tree should have no focus indication. + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('focus on root, node in diff tree, then node in first tree; root should have focus gone', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusTree(this.testFocusableTree1); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusNode(this.testFocusableTree1Node1); + + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree focusTree()ed with no prev root has active focus', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + this.focusManager.focusTree(this.testFocusableNestedTree4); + + const rootElem = this.testFocusableNestedTree4 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree node focusNode()ed with no prev focus node has active focus', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + + const nodeElem = + this.testFocusableNestedTree4Node1.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree node focusNode()ed after parent focused prev has passive node has active', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + + const prevNodeElem = this.testFocusableTree2Node1.getFocusableElement(); + const currNodeElem = + this.testFocusableNestedTree4Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + currNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + }); + }); + + suite('DOM focus() switching in HTML tree', function () { + suite('getFocusedTree()', function () { + test('registered root focus()ed no prev focus returns tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test("registered node focus()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test("registered subnode focus()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1.node1.child1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test('registered node focus()ed after prev node focus returns same tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree1.node2').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test("registered node focus()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree2.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test("registered tree root focus()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree2').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test("non-registered node subelement focus()ed returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableTree1); + + document + .getElementById('testFocusableTree1.node2.unregisteredChild1') + .focus(); + + // The tree of the unregistered child element should take focus. + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test('non-registered tree focus()ed returns null', function () { + document.getElementById('testUnregisteredFocusableTree3').focus(); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('non-registered tree node focus()ed returns null', function () { + document.getElementById('testUnregisteredFocusableTree3.node1').focus(); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('non-registered tree node focus()ed after registered node focused returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testUnregisteredFocusableTree3.node1').focus(); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unfocusable element focus()ed after registered node focused returns original tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testUnfocusableElement').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree1, + ); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focus()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree2.node1').focus(); + document.getElementById('testFocusableTree1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focus()ed with prev node recently focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the most recent tree still exists, it still has focus. + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + }); + + test('unregistered tree focus()ed with prev node after unregistering returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + this.focusManager.unregisterTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1.node1').focus(); + + // Attempting to focus a now removed tree should result in nothing being + // focused since the removed tree can have DOM focus, but that focus is + // ignored by FocusManager. + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('nested tree focusTree()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + document.getElementById('testFocusableNestedTree4').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedTree4, + ); + }); + + test('nested tree node focusNode()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + document.getElementById('testFocusableNestedTree4.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedTree4, + ); + }); + + test('nested tree node focusNode()ed after parent focused returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + document.getElementById('testFocusableTree2.node1').focus(); + + document.getElementById('testFocusableNestedTree4.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedTree4, + ); + }); + }); + suite('getFocusedNode()', function () { + test('registered root focus()ed no prev focus returns root node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1.getRootFocusableNode(), + ); + }); + + test('registered node focus()ed no prev focus returns node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node1, + ); + }); + + test('registered subnode focus()ed no prev focus returns subnode', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1.node1.child1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node1Child1, + ); + }); + + test('registered node focus()ed after prev node focus returns new node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree1.node2').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node2, + ); + }); + + test('registered node focus()ed after prev node focus diff tree returns new node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree2.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + }); + + test('registered tree root focus()ed after prev node focus diff tree returns new root', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree2').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2.getRootFocusableNode(), + ); + }); + + test('non-registered node subelement focus()ed returns nearest node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + document + .getElementById('testFocusableTree1.node2.unregisteredChild1') + .focus(); + + // The nearest node of the unregistered child element should take focus. + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node2, + ); + }); + + test('non-registered tree focus()ed returns null', function () { + document.getElementById('testUnregisteredFocusableTree3').focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('non-registered tree node focus()ed returns null', function () { + document.getElementById('testUnregisteredFocusableTree3.node1').focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('non-registered tree node focus()ed after registered node focused returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testUnregisteredFocusableTree3.node1').focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unfocusable element focus()ed after registered node focused returns original node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testUnfocusableElement').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree1Node1, + ); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focus()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree2.node1').focus(); + document.getElementById('testFocusableTree1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focus()ed with prev node recently focused returns new node', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the most recent tree still exists, it still has focus. + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + }); + + test('unregistered tree focus()ed with prev node after unregistering returns null', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + this.focusManager.unregisterTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1.node1').focus(); + + // Attempting to focus a now removed tree should result in nothing being + // focused since the removed tree can have DOM focus, but that focus is + // ignored by FocusManager. + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('nested tree focus()ed with no prev focus returns nested root', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + document.getElementById('testFocusableNestedTree4').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedTree4.getRootFocusableNode(), + ); + }); + + test('nested tree node focus()ed with no prev focus returns focused node', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + document.getElementById('testFocusableNestedTree4.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedTree4Node1, + ); + }); + + test('nested tree node focus()ed after parent focused returns focused node', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + document.getElementById('testFocusableTree2.node1').focus(); + + document.getElementById('testFocusableNestedTree4.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedTree4Node1, + ); + }); + }); + suite('CSS classes', function () { + test('registered root focus()ed no prev focus returns root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1').focus(); + + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focus()ed no prev focus node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1.node1').focus(); + + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focus()ed after prev node focus same tree old node elem has no focus property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree1.node2').focus(); + + const prevNodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focus()ed after prev node focus same tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree1.node2').focus(); + + const newNodeElem = this.testFocusableTree1Node2.getFocusableElement(); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focus()ed after prev node focus diff tree old node elem has passive property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree2.node1').focus(); + + const prevNodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focus()ed after prev node focus diff tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree2.node1').focus(); + + const newNodeElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree root focus()ed after prev node focus diff tree new root has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testFocusableTree2').focus(); + + const rootElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('non-registered node subelement focus()ed nearest node has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + + document + .getElementById('testFocusableTree1.node2.unregisteredChild1') + .focus(); + + // The nearest node of the unregistered child element should be actively focused. + const nodeElem = this.testFocusableTree1Node2.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('non-registered tree focus()ed has no focus', function () { + document.getElementById('testUnregisteredFocusableTree3').focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + + const rootElem = document.getElementById( + 'testUnregisteredFocusableTree3', + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('non-registered tree node focus()ed has no focus', function () { + document.getElementById('testUnregisteredFocusableTree3.node1').focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + + const nodeElem = document.getElementById( + 'testUnregisteredFocusableTree3.node1', + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unfocsable element focus()ed after registered node focused original node has active focus', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + document.getElementById('testUnfocusableElement').focus(); + + // The original node should be unchanged, and the unregistered node should not have any + // focus indicators. + const nodeElem = document.getElementById('testFocusableTree1.node1'); + const attemptedNewNodeElem = document.getElementById( + 'testUnfocusableElement', + ); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + attemptedNewNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + attemptedNewNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focus()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focus()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableTree1); + document.getElementById('testFocusableTree1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focus()ed with prev node prior removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree2.node1').focus(); + document.getElementById('testFocusableTree1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. However, the old node + // should still have passive indication. + const otherNodeElem = + this.testFocusableTree2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableTree1Node1.getFocusableElement(); + assert.includesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focus()ed with prev node recently removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableTree1); + + // Since the tree was unregistered it no longer has focus indicators. However, the new node + // should still have active indication. + const otherNodeElem = + this.testFocusableTree2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableTree1Node1.getFocusableElement(); + assert.includesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focus()ed with prev node after unregistering removes active indicator', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + this.focusManager.unregisterTree(this.testFocusableTree1); + + document.getElementById('testFocusableTree1.node1').focus(); + + // Attempting to focus a now removed tree should remove active. + const otherNodeElem = + this.testFocusableTree2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableTree1Node1.getFocusableElement(); + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('focus() multiple nodes in same tree with switches ensure passive focus has gone', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + + document.getElementById('testFocusableTree1.node2').focus(); + + // When switching back to the first tree, ensure the original passive node is no longer + // passive now that the new node is active. + const node1 = this.testFocusableTree1Node1.getFocusableElement(); + const node2 = this.testFocusableTree1Node2.getFocusableElement(); + assert.notIncludesClass( + node1.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + node2.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree focus()ed other tree node passively focused tree root now has active property', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1.node1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + + document.getElementById('testFocusableTree1').focus(); + + // This differs from the behavior of focusTree() since directly focusing a tree's root will + // coerce it to now have focus. + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('focus on root, node in diff tree, then node in first tree; root should have focus gone', function () { + this.focusManager.registerTree(this.testFocusableTree1); + this.focusManager.registerTree(this.testFocusableTree2); + document.getElementById('testFocusableTree1').focus(); + document.getElementById('testFocusableTree2.node1').focus(); + + document.getElementById('testFocusableTree1.node1').focus(); + + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + const rootElem = this.testFocusableTree1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree focus()ed with no prev root has active focus', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + document.getElementById('testFocusableNestedTree4').focus(); + + const rootElem = this.testFocusableNestedTree4 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree node focus()ed with no prev focus node has active focus', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + + document.getElementById('testFocusableNestedTree4.node1').focus(); + + const nodeElem = + this.testFocusableNestedTree4Node1.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree node focus()ed after parent focused prev has passive node has active', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + document.getElementById('testFocusableTree2.node1').focus(); + + document.getElementById('testFocusableNestedTree4.node1').focus(); + + const prevNodeElem = this.testFocusableTree2Node1.getFocusableElement(); + const currNodeElem = + this.testFocusableNestedTree4Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + currNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + }); + }); + + /* Focus tests for SVG trees. */ + + suite('focus*() switching in SVG tree', function () { + suite('getFocusedTree()', function () { + test('registered tree focusTree()ed no prev focus returns tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusTree(this.testFocusableGroup1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test('registered tree focusTree()ed prev node focused returns tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusTree(this.testFocusableGroup1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test('registered tree focusTree()ed diff tree prev focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableGroup1); + + this.focusManager.focusTree(this.testFocusableGroup2); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test('registered tree focusTree()ed diff tree node prev focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusTree(this.testFocusableGroup2); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test('registered root focusNode()ed no prev focus returns tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode( + this.testFocusableGroup1.getRootFocusableNode(), + ); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test("registered node focusNode()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test("registered subnode focusNode()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode(this.testFocusableGroup1Node1Child1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test('registered node focusNode()ed after prev node focus returns same tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup1Node2); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test("registered node focusNode()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test("registered tree root focusNode()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode( + this.testFocusableGroup2.getRootFocusableNode(), + ); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test('unregistered tree focusTree()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusTree(this.testFocusableGroup1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focusNode()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focusNode()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focusNode()ed with prev node recently focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the most recent tree still exists, it still has focus. + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test('nested tree focusTree()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + this.focusManager.focusTree(this.testFocusableNestedGroup4); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedGroup4, + ); + }); + + test('nested tree node focusNode()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + this.focusManager.focusNode(this.testFocusableNestedGroup4Node1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedGroup4, + ); + }); + + test('nested tree node focusNode()ed after parent focused returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusNode(this.testFocusableNestedGroup4Node1); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedGroup4, + ); + }); + }); + suite('getFocusedNode()', function () { + test('registered tree focusTree()ed no prev focus returns root node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusTree(this.testFocusableGroup1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1.getRootFocusableNode(), + ); + }); + + test('registered tree focusTree()ed prev node focused returns original node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusTree(this.testFocusableGroup1); + + // The original node retains focus since the tree already holds focus (per focusTree's + // contract). + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node1, + ); + }); + + test('registered tree focusTree()ed diff tree prev focused returns new root node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableGroup1); + + this.focusManager.focusTree(this.testFocusableGroup2); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2.getRootFocusableNode(), + ); + }); + + test('registered tree focusTree()ed diff tree node prev focused returns new root node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusTree(this.testFocusableGroup2); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2.getRootFocusableNode(), + ); + }); + + test('registered root focusNode()ed no prev focus returns root node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode( + this.testFocusableGroup1.getRootFocusableNode(), + ); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1.getRootFocusableNode(), + ); + }); + + test('registered node focusNode()ed no prev focus returns node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node1, + ); + }); + + test('registered subnode focusNode()ed no prev focus returns subnode', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode(this.testFocusableGroup1Node1Child1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node1Child1, + ); + }); + + test('registered node focusNode()ed after prev node focus returns new node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup1Node2); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node2, + ); + }); + + test('registered node focusNode()ed after prev node focus diff tree returns new node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + }); + + test('registered tree root focusNode()ed after prev node focus diff tree returns new root', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode( + this.testFocusableGroup2.getRootFocusableNode(), + ); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2.getRootFocusableNode(), + ); + }); + + test('unregistered tree focusTree()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusTree(this.testFocusableGroup1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focusNode()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focusNode()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focusNode()ed with prev node recently focused returns new node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the most recent tree still exists, it still has focus. + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + }); + + test('nested tree focusTree()ed with no prev focus returns nested root', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + this.focusManager.focusTree(this.testFocusableNestedGroup4); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedGroup4.getRootFocusableNode(), + ); + }); + + test('nested tree node focusNode()ed with no prev focus returns focused node', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + this.focusManager.focusNode(this.testFocusableNestedGroup4Node1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedGroup4Node1, + ); + }); + + test('nested tree node focusNode()ed after parent focused returns focused node', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusNode(this.testFocusableNestedGroup4Node1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedGroup4Node1, + ); + }); + }); + suite('CSS classes', function () { + test('registered tree focusTree()ed no prev focus root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusTree(this.testFocusableGroup1); + + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree focusTree()ed prev node focused original elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusTree(this.testFocusableGroup1); + + // The original node retains active focus since the tree already holds focus (per + // focusTree's contract). + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree focusTree()ed diff tree prev focused new root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableGroup1); + + this.focusManager.focusTree(this.testFocusableGroup2); + + const rootElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree focusTree()ed diff tree node prev focused new root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusTree(this.testFocusableGroup2); + + const rootElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered root focusNode()ed no prev focus returns root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode( + this.testFocusableGroup1.getRootFocusableNode(), + ); + + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focusNode()ed no prev focus node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focusNode()ed after prev node focus same tree old node elem has no focus property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup1Node2); + + const prevNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focusNode()ed after prev node focus same tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup1Node2); + + const newNodeElem = this.testFocusableGroup1Node2.getFocusableElement(); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focusNode()ed after prev node focus diff tree old node elem has passive property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + const prevNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focusNode()ed after prev node focus diff tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + const newNodeElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree root focusNode()ed after prev node focus diff tree new root has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.focusNode( + this.testFocusableGroup2.getRootFocusableNode(), + ); + + const rootElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focusTree()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusTree(this.testFocusableGroup1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focusNode()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focusNode()ed with prev node prior removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. However, the old node + // should still have passive indication. + const otherNodeElem = + this.testFocusableGroup2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.includesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focusNode()ed with prev node recently removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. However, the new node + // should still have active indication. + const otherNodeElem = + this.testFocusableGroup2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.includesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('focusNode() multiple nodes in same tree with switches ensure passive focus has gone', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusNode(this.testFocusableGroup1Node2); + + // When switching back to the first tree, ensure the original passive node is no longer + // passive now that the new node is active. + const node1 = this.testFocusableGroup1Node1.getFocusableElement(); + const node2 = this.testFocusableGroup1Node2.getFocusableElement(); + assert.notIncludesClass( + node1.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + node2.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree focusTree()ed other tree node passively focused tree node now has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup1Node1); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusTree(this.testFocusableGroup1); + + // The original node in the tree should be moved from passive to active focus per the + // contract of focusTree). Also, the root of the tree should have no focus indication. + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('focus on root, node in diff tree, then node in first tree; root should have focus gone', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableGroup1); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusNode(this.testFocusableGroup1Node1); + + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree focusTree()ed with no prev root has active focus', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + this.focusManager.focusTree(this.testFocusableNestedGroup4); + + const rootElem = this.testFocusableNestedGroup4 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree node focusNode()ed with no prev focus node has active focus', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + this.focusManager.focusNode(this.testFocusableNestedGroup4Node1); + + const nodeElem = + this.testFocusableNestedGroup4Node1.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree node focusNode()ed after parent focused prev has passive node has active', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusNode(this.testFocusableNestedGroup4Node1); + + const prevNodeElem = + this.testFocusableGroup2Node1.getFocusableElement(); + const currNodeElem = + this.testFocusableNestedGroup4Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + currNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + }); + }); + + suite('DOM focus() switching in SVG tree', function () { + suite('getFocusedTree()', function () { + test('registered root focus()ed no prev focus returns tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test("registered node focus()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test("registered subnode focus()ed no prev focus returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1.node1.child1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test('registered node focus()ed after prev node focus returns same tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup1.node2').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test("registered node focus()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup2.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test("registered tree root focus()ed after prev node focus diff tree returns new node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup2').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test("non-registered node subelement focus()ed returns node's tree", function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + document + .getElementById('testFocusableGroup1.node2.unregisteredChild1') + .focus(); + + // The tree of the unregistered child element should take focus. + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup1, + ); + }); + + test('non-registered tree focus()ed returns null', function () { + document.getElementById('testUnregisteredFocusableGroup3').focus(); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('non-registered tree node focus()ed returns null', function () { + document + .getElementById('testUnregisteredFocusableGroup3.node1') + .focus(); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('non-registered tree node focus()ed after registered node focused returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').focus(); + + document + .getElementById('testUnregisteredFocusableGroup3.node1') + .focus(); + + // Attempting to focus a now removed tree should result in nothing being + // focused since the removed tree can have DOM focus, but that focus is + // ignored by FocusManager. + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focus()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup2.node1').focus(); + document.getElementById('testFocusableGroup1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('unregistered tree focus()ed with prev node recently focused returns new tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the most recent tree still exists, it still has focus. + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + }); + + test('unregistered tree focus()ed with prev node after unregistering returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + this.focusManager.unregisterTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1.node1').focus(); + + // Attempting to focus a now removed tree should result in nothing being + // focused since the removed tree can have DOM focus, but that focus is + // ignored by FocusManager. + assert.isNull(this.focusManager.getFocusedTree()); + }); + + test('nested tree focusTree()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + document.getElementById('testFocusableNestedGroup4').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedGroup4, + ); + }); + + test('nested tree node focusNode()ed with no prev focus returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + document.getElementById('testFocusableNestedGroup4.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedGroup4, + ); + }); + + test('nested tree node focusNode()ed after parent focused returns nested tree', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + document.getElementById('testFocusableGroup2.node1').focus(); + + document.getElementById('testFocusableNestedGroup4.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedGroup4, + ); + }); + }); + suite('getFocusedNode()', function () { + test('registered root focus()ed no prev focus returns root node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1.getRootFocusableNode(), + ); + }); + + test('registered node focus()ed no prev focus returns node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node1, + ); + }); + + test('registered subnode focus()ed no prev focus returns subnode', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1.node1.child1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node1Child1, + ); + }); + + test('registered node focus()ed after prev node focus returns new node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup1.node2').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node2, + ); + }); + + test('registered node focus()ed after prev node focus diff tree returns new node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup2.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + }); + + test('registered tree root focus()ed after prev node focus diff tree returns new root', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup2').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2.getRootFocusableNode(), + ); + }); + + test('non-registered node subelement focus()ed returns nearest node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + document + .getElementById('testFocusableGroup1.node2.unregisteredChild1') + .focus(); + + // The nearest node of the unregistered child element should take focus. + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node2, + ); + }); + + test('non-registered tree focus()ed returns null', function () { + document.getElementById('testUnregisteredFocusableGroup3').focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('non-registered tree node focus()ed returns null', function () { + document + .getElementById('testUnregisteredFocusableGroup3.node1') + .focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('non-registered tree node focus()ed after registered node focused returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').focus(); + + document + .getElementById('testUnregisteredFocusableGroup3.node1') + .focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unfocusable element focus()ed after registered node focused returns original node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testUnfocusableElement').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup1Node1, + ); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focus()ed with no prev focus returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focus()ed with prev node prior focused returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup2.node1').focus(); + document.getElementById('testFocusableGroup1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the more recent tree was removed, there's no tree currently focused. + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('unregistered tree focus()ed with prev node recently focused returns new node', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the most recent tree still exists, it still has focus. + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + }); + + test('unregistered tree focus()ed with prev node after unregistering returns null', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + this.focusManager.unregisterTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1.node1').focus(); + + // Attempting to focus a now removed tree should result in nothing being + // focused since the removed tree can have DOM focus, but that focus is + // ignored by FocusManager. + assert.isNull(this.focusManager.getFocusedNode()); + }); + + test('nested tree focus()ed with no prev focus returns nested root', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + document.getElementById('testFocusableNestedGroup4').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedGroup4.getRootFocusableNode(), + ); + }); + + test('nested tree node focus()ed with no prev focus returns focused node', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + document.getElementById('testFocusableNestedGroup4.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedGroup4Node1, + ); + }); + + test('nested tree node focus()ed after parent focused returns focused node', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + document.getElementById('testFocusableGroup2.node1').focus(); + + document.getElementById('testFocusableNestedGroup4.node1').focus(); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedGroup4Node1, + ); + }); + }); + suite('CSS classes', function () { + test('registered root focus()ed no prev focus returns root elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1').focus(); + + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focus()ed no prev focus node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1.node1').focus(); + + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focus()ed after prev node focus same tree old node elem has no focus property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup1.node2').focus(); + + const prevNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focus()ed after prev node focus same tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup1.node2').focus(); + + const newNodeElem = this.testFocusableGroup1Node2.getFocusableElement(); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focus()ed after prev node focus diff tree old node elem has passive property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup2.node1').focus(); + + const prevNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered node focus()ed after prev node focus diff tree new node elem has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup2.node1').focus(); + + const newNodeElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.includesClass( + newNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + newNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree root focus()ed after prev node focus diff tree new root has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testFocusableGroup2').focus(); + + const rootElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('non-registered node subelement focus()ed nearest node has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + + document + .getElementById('testFocusableGroup1.node2.unregisteredChild1') + .focus(); + + // The nearest node of the unregistered child element should be actively focused. + const nodeElem = this.testFocusableGroup1Node2.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('non-registered tree focus()ed has no focus', function () { + document.getElementById('testUnregisteredFocusableGroup3').focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + + const rootElem = document.getElementById( + 'testUnregisteredFocusableGroup3', + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('non-registered tree node focus()ed has no focus', function () { + document + .getElementById('testUnregisteredFocusableGroup3.node1') + .focus(); + + assert.isNull(this.focusManager.getFocusedNode()); + + const nodeElem = document.getElementById( + 'testUnregisteredFocusableGroup3.node1', + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unfocusable element focus()ed after registered node focused original node has active focus', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').focus(); + + document.getElementById('testUnfocusableElement').focus(); + + // The original node should be unchanged, and the unregistered node should not have any + // focus indicators. + const nodeElem = document.getElementById('testFocusableGroup1.node1'); + const attemptedNewNodeElem = document.getElementById( + 'testUnfocusableElement', + ); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + attemptedNewNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + attemptedNewNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focus()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focus()ed with no prev focus removes focus', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + document.getElementById('testFocusableGroup1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focus()ed with prev node prior removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup2.node1').focus(); + document.getElementById('testFocusableGroup1.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. However, the old node + // should still have passive indication. + const otherNodeElem = + this.testFocusableGroup2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.includesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focus()ed with prev node recently removes focus from removed tree', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + + this.focusManager.unregisterTree(this.testFocusableGroup1); + + // Since the tree was unregistered it no longer has focus indicators. However, the new node + // should still have active indication. + const otherNodeElem = + this.testFocusableGroup2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.includesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('unregistered tree focus()ed with prev node after unregistering removes active indicator', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + this.focusManager.unregisterTree(this.testFocusableGroup1); + + document.getElementById('testFocusableGroup1.node1').focus(); + + // Attempting to focus a now removed tree should remove active. + const otherNodeElem = + this.testFocusableGroup2Node1.getFocusableElement(); + const removedNodeElem = + this.testFocusableGroup1Node1.getFocusableElement(); + assert.notIncludesClass( + otherNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + otherNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + removedNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('focus() multiple nodes in same tree with switches ensure passive focus has gone', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + + document.getElementById('testFocusableGroup1.node2').focus(); + + // When switching back to the first tree, ensure the original passive node is no longer + // passive now that the new node is active. + const node1 = this.testFocusableGroup1Node1.getFocusableElement(); + const node2 = this.testFocusableGroup1Node2.getFocusableElement(); + assert.notIncludesClass( + node1.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + node2.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('registered tree focus()ed other tree node passively focused tree root now has active property', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1.node1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + + document.getElementById('testFocusableGroup1').focus(); + + // This differs from the behavior of focusTree() since directly focusing a tree's root will + // coerce it to now have focus. + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('focus on root, node in diff tree, then node in first tree; root should have focus gone', function () { + this.focusManager.registerTree(this.testFocusableGroup1); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup1').focus(); + document.getElementById('testFocusableGroup2.node1').focus(); + + document.getElementById('testFocusableGroup1.node1').focus(); + + const nodeElem = this.testFocusableGroup1Node1.getFocusableElement(); + const rootElem = this.testFocusableGroup1 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree focus()ed with no prev root has active focus', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + document.getElementById('testFocusableNestedGroup4').focus(); + + const rootElem = this.testFocusableNestedGroup4 + .getRootFocusableNode() + .getFocusableElement(); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree node focus()ed with no prev focus node has active focus', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + + document.getElementById('testFocusableNestedGroup4.node1').focus(); + + const nodeElem = + this.testFocusableNestedGroup4Node1.getFocusableElement(); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('nested tree node focus()ed after parent focused prev has passive node has active', function () { + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.registerTree(this.testFocusableNestedGroup4); + document.getElementById('testFocusableGroup2.node1').focus(); + + document.getElementById('testFocusableNestedGroup4.node1').focus(); + + const prevNodeElem = + this.testFocusableGroup2Node1.getFocusableElement(); + const currNodeElem = + this.testFocusableNestedGroup4Node1.getFocusableElement(); + assert.notIncludesClass( + prevNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + currNodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currNodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + }); + }); + + /* High-level focus/defocusing tests. */ + suite('Defocusing and refocusing', function () { + test('Defocusing actively focused root HTML tree switches to passive highlight', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusTree(this.testFocusableTree2); + + document.getElementById('testUnregisteredFocusableTree3').focus(); + + const rootNode = this.testFocusableTree2.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + assert.isNull(this.focusManager.getFocusedTree()); + assert.isNull(this.focusManager.getFocusedNode()); + assert.includesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('Defocusing actively focused HTML tree node switches to passive highlight', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + document.getElementById('testUnregisteredFocusableTree3').focus(); + + const nodeElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.isNull(this.focusManager.getFocusedTree()); + assert.isNull(this.focusManager.getFocusedNode()); + assert.includesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('Defocusing actively focused HTML subtree node switches to passive highlight', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + + document.getElementById('testUnregisteredFocusableTree3').focus(); + + const nodeElem = this.testFocusableNestedTree4Node1.getFocusableElement(); + assert.isNull(this.focusManager.getFocusedTree()); + assert.isNull(this.focusManager.getFocusedNode()); + assert.includesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('Refocusing actively focused root HTML tree restores to active highlight', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusTree(this.testFocusableTree2); + document.getElementById('testUnregisteredFocusableTree3').focus(); + + document.getElementById('testFocusableTree2').focus(); + + const rootNode = this.testFocusableTree2.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + assert.strictEqual(this.focusManager.getFocusedNode(), rootNode); + assert.notIncludesClass( + rootElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('Refocusing actively focused HTML tree node restores to active highlight', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + document.getElementById('testUnregisteredFocusableTree3').focus(); + + document.getElementById('testFocusableTree2.node1').focus(); + + const nodeElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('Refocusing actively focused HTML subtree node restores to active highlight', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableNestedTree4); + this.focusManager.focusNode(this.testFocusableNestedTree4Node1); + document.getElementById('testUnregisteredFocusableTree3').focus(); + + document.getElementById('testFocusableNestedTree4.node1').focus(); + + const nodeElem = this.testFocusableNestedTree4Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableNestedTree4, + ); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableNestedTree4Node1, + ); + assert.notIncludesClass( + nodeElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + }); + + /* Combined HTML/SVG tree focus tests. */ + + suite('HTML/SVG focus tree switching', function () { + suite('Focus HTML tree then SVG tree', function () { + test('HTML focusTree()ed then SVG focusTree()ed correctly updates getFocusedTree() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableTree2); + + this.focusManager.focusTree(this.testFocusableGroup2); + + const prevElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + const currElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('HTML focusTree()ed then SVG focusNode()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableTree2); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + const prevElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + const currElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('HTML focusTree()ed then SVG DOM focus()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableTree2); + + document.getElementById('testFocusableGroup2.node1').focus(); + + const prevElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + const currElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('HTML focusNode()ed then SVG focusTree()ed correctly updates getFocusedTree() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusTree(this.testFocusableGroup2); + + const prevElem = this.testFocusableTree2Node1.getFocusableElement(); + const currElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('HTML focusNode()ed then SVG focusNode()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + const prevElem = this.testFocusableTree2Node1.getFocusableElement(); + const currElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('HTML focusNode()ed then SVG DOM focus()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + document.getElementById('testFocusableGroup2.node1').focus(); + + const prevElem = this.testFocusableTree2Node1.getFocusableElement(); + const currElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('HTML DOM focus()ed then SVG focusTree()ed correctly updates getFocusedTree() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableTree2.node1').focus(); + + this.focusManager.focusTree(this.testFocusableGroup2); + + const prevElem = this.testFocusableTree2Node1.getFocusableElement(); + const currElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('HTML DOM focus()ed then SVG focusNode()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableTree2.node1').focus(); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + const prevElem = this.testFocusableTree2Node1.getFocusableElement(); + const currElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('HTML DOM focus()ed then SVG DOM focus()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableTree2.node1').focus(); + + document.getElementById('testFocusableGroup2.node1').focus(); + + const prevElem = this.testFocusableTree2Node1.getFocusableElement(); + const currElem = this.testFocusableGroup2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + }); + suite('Focus SVG tree then HTML tree', function () { + test('SVG focusTree()ed then HTML focusTree()ed correctly updates getFocusedTree() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableGroup2); + + this.focusManager.focusTree(this.testFocusableTree2); + + const prevElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + const currElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('SVG focusTree()ed then HTML focusNode()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableGroup2); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + const prevElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + const currElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('SVG focusTree()ed then HTML DOM focus()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableGroup2); + + document.getElementById('testFocusableTree2.node1').focus(); + + const prevElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + const currElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('SVG focusNode()ed then HTML focusTree()ed correctly updates getFocusedTree() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusTree(this.testFocusableTree2); + + const prevElem = this.testFocusableGroup2Node1.getFocusableElement(); + const currElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('SVG focusNode()ed then HTML focusNode()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + const prevElem = this.testFocusableGroup2Node1.getFocusableElement(); + const currElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('SVG focusNode()ed then HTML DOM focus()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + document.getElementById('testFocusableTree2.node1').focus(); + + const prevElem = this.testFocusableGroup2Node1.getFocusableElement(); + const currElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('SVG DOM focus()ed then HTML focusTree()ed correctly updates getFocusedTree() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup2.node1').focus(); + + this.focusManager.focusTree(this.testFocusableTree2); + + const prevElem = this.testFocusableGroup2Node1.getFocusableElement(); + const currElem = this.testFocusableTree2 + .getRootFocusableNode() + .getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableTree2, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('SVG DOM focus()ed then HTML focusNode()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup2.node1').focus(); + + this.focusManager.focusNode(this.testFocusableTree2Node1); + + const prevElem = this.testFocusableGroup2Node1.getFocusableElement(); + const currElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('SVG DOM focus()ed then HTML DOM focus()ed correctly updates getFocusedNode() and indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + document.getElementById('testFocusableGroup2.node1').focus(); + + document.getElementById('testFocusableTree2.node1').focus(); + + const prevElem = this.testFocusableGroup2Node1.getFocusableElement(); + const currElem = this.testFocusableTree2Node1.getFocusableElement(); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.strictEqual(document.activeElement, currElem); + assert.includesClass( + currElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + currElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.notIncludesClass( + prevElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.includesClass( + prevElem.classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + }); + }); + + /* Ephemeral focus tests. */ + + suite('takeEphemeralFocus()', function () { + test('with no focused node does not change states', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + + const ephemeralElement = document.getElementById( + 'nonTreeElementForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralElement); + + // Taking focus without an existing node having focus should change no focus indicators. + const activeElems = Array.from( + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), + ); + const passiveElems = Array.from( + document.querySelectorAll(PASSIVE_FOCUS_NODE_CSS_SELECTOR), + ); + assert.isEmpty(activeElems); + assert.isEmpty(passiveElems); + }); + + test('with focused node changes focused node to passive', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + + const ephemeralElement = document.getElementById( + 'nonTreeElementForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralElement); + + // Taking focus without an existing node having focus should change no focus indicators. + const activeElems = Array.from( + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), + ); + const passiveElems = Array.from( + document.querySelectorAll(PASSIVE_FOCUS_NODE_CSS_SELECTOR), + ); + assert.isEmpty(activeElems); + assert.strictEqual(passiveElems.length, 1); + assert.includesClass( + this.testFocusableTree2Node1.getFocusableElement().classList, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }); + + test('focuses provided HTML element', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + + const ephemeralElement = document.getElementById( + 'nonTreeElementForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralElement); + + assert.strictEqual(document.activeElement, ephemeralElement); + }); + + test('focuses provided SVG element', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralElement); + + assert.strictEqual(document.activeElement, ephemeralElement); + }); + + test('twice for without finishing previous throws error', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + const ephemeralGroupElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + const ephemeralDivElement = document.getElementById( + 'nonTreeElementForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralGroupElement); + + const errorMsgRegex = + /Attempted to take ephemeral focus when it's already held+?/; + assert.throws( + () => this.focusManager.takeEphemeralFocus(ephemeralDivElement), + errorMsgRegex, + ); + }); + + test('then focusTree() changes getFocusedTree() but not active state', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableTree2); + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralElement); + + this.focusManager.focusTree(this.testFocusableGroup2); + + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + const activeElems = Array.from( + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), + ); + assert.isEmpty(activeElems); + }); + + test('then focusNode() changes getFocusedNode() but not active state', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralElement); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + const activeElems = Array.from( + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), + ); + assert.isEmpty(activeElems); + }); + + test('then DOM refocus changes getFocusedNode() but not active state', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + this.focusManager.takeEphemeralFocus(ephemeralElement); + + // Force focus to change via the DOM. + document.getElementById('testFocusableGroup2.node1').focus(); + + // The focus() state change will affect getFocusedNode() but it will not cause the node to now + // be active. + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + const activeElems = Array.from( + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), + ); + assert.isEmpty(activeElems); + }); + + test('then finish ephemeral callback with no node does not change indicators', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + const ephemeralElement = document.getElementById( + 'nonTreeElementForEphemeralFocus', + ); + const finishFocusCallback = + this.focusManager.takeEphemeralFocus(ephemeralElement); + + finishFocusCallback(); + + // Finishing ephemeral focus without a previously focused node should not change indicators. + const activeElems = Array.from( + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), + ); + const passiveElems = Array.from( + document.querySelectorAll(PASSIVE_FOCUS_NODE_CSS_SELECTOR), + ); + assert.isEmpty(activeElems); + assert.isEmpty(passiveElems); + }); + + test('again after finishing previous empheral focus should focus new element', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + const ephemeralGroupElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + const ephemeralDivElement = document.getElementById( + 'nonTreeElementForEphemeralFocus', + ); + const finishFocusCallback = this.focusManager.takeEphemeralFocus( + ephemeralGroupElement, + ); + + finishFocusCallback(); + this.focusManager.takeEphemeralFocus(ephemeralDivElement); + + // An exception should not be thrown and the new element should receive focus. + assert.strictEqual(document.activeElement, ephemeralDivElement); + }); + + test('calling ephemeral callback twice throws error', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + const ephemeralElement = document.getElementById( + 'nonTreeElementForEphemeralFocus', + ); + const finishFocusCallback = + this.focusManager.takeEphemeralFocus(ephemeralElement); + finishFocusCallback(); + + const errorMsgRegex = + /Attempted to finish ephemeral focus twice for element+?/; + assert.throws(() => finishFocusCallback(), errorMsgRegex); + }); + + test('then finish ephemeral callback should restore focused node', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + const finishFocusCallback = + this.focusManager.takeEphemeralFocus(ephemeralElement); + + finishFocusCallback(); + + // The original focused node should be restored. + const nodeElem = this.testFocusableTree2Node1.getFocusableElement(); + const activeElems = Array.from( + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), + ); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableTree2Node1, + ); + assert.strictEqual(activeElems.length, 1); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.strictEqual(document.activeElement, nodeElem); + }); + + test('then focusTree() and finish ephemeral callback correctly sets new active state', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusTree(this.testFocusableTree2); + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + const finishFocusCallback = + this.focusManager.takeEphemeralFocus(ephemeralElement); + + this.focusManager.focusTree(this.testFocusableGroup2); + finishFocusCallback(); + + // The tree's root should now be the active element since focus changed between the start and + // end of the ephemeral flow. + const rootElem = this.testFocusableGroup2 + .getRootFocusableNode() + .getFocusableElement(); + const activeElems = Array.from( + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), + ); + assert.strictEqual( + this.focusManager.getFocusedTree(), + this.testFocusableGroup2, + ); + assert.strictEqual(activeElems.length, 1); + assert.includesClass( + rootElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.strictEqual(document.activeElement, rootElem); + }); + + test('then focusNode() and finish ephemeral callback correctly sets new active state', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + const finishFocusCallback = + this.focusManager.takeEphemeralFocus(ephemeralElement); + + this.focusManager.focusNode(this.testFocusableGroup2Node1); + finishFocusCallback(); + + // The tree's root should now be the active element since focus changed between the start and + // end of the ephemeral flow. + const nodeElem = this.testFocusableGroup2Node1.getFocusableElement(); + const activeElems = Array.from( + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), + ); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.strictEqual(activeElems.length, 1); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.strictEqual(document.activeElement, nodeElem); + }); + + test('then DOM focus change and finish ephemeral callback correctly sets new active state', function () { + this.focusManager.registerTree(this.testFocusableTree2); + this.focusManager.registerTree(this.testFocusableGroup2); + this.focusManager.focusNode(this.testFocusableTree2Node1); + const ephemeralElement = document.getElementById( + 'nonTreeGroupForEphemeralFocus', + ); + const finishFocusCallback = + this.focusManager.takeEphemeralFocus(ephemeralElement); + + document.getElementById('testFocusableGroup2.node1').focus(); + finishFocusCallback(); + + // The tree's root should now be the active element since focus changed between the start and + // end of the ephemeral flow. + const nodeElem = this.testFocusableGroup2Node1.getFocusableElement(); + const activeElems = Array.from( + document.querySelectorAll(ACTIVE_FOCUS_NODE_CSS_SELECTOR), + ); + assert.strictEqual( + this.focusManager.getFocusedNode(), + this.testFocusableGroup2Node1, + ); + assert.strictEqual(activeElems.length, 1); + assert.includesClass( + nodeElem.classList, + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + assert.strictEqual(document.activeElement, nodeElem); + }); + }); +}); diff --git a/tests/mocha/focusable_tree_traverser_test.js b/tests/mocha/focusable_tree_traverser_test.js new file mode 100644 index 000000000..b6674573e --- /dev/null +++ b/tests/mocha/focusable_tree_traverser_test.js @@ -0,0 +1,512 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {FocusManager} from '../../build/src/core/focus_manager.js'; +import {FocusableTreeTraverser} from '../../build/src/core/utils/focusable_tree_traverser.js'; +import {assert} from '../../node_modules/chai/chai.js'; +import { + sharedTestSetup, + sharedTestTeardown, +} from './test_helpers/setup_teardown.js'; + +class FocusableNodeImpl { + constructor(element, tree) { + this.element = element; + this.tree = tree; + } + + getFocusableElement() { + return this.element; + } + + getFocusableTree() { + return this.tree; + } +} + +class FocusableTreeImpl { + constructor(rootElement, nestedTrees) { + this.nestedTrees = nestedTrees; + this.idToNodeMap = {}; + this.rootNode = this.addNode(rootElement); + } + + addNode(element) { + const node = new FocusableNodeImpl(element, this); + this.idToNodeMap[element.id] = node; + return node; + } + + getRootFocusableNode() { + return this.rootNode; + } + + getNestedTrees() { + return this.nestedTrees; + } + + lookUpFocusableNode(id) { + return this.idToNodeMap[id]; + } +} + +suite('FocusableTreeTraverser', function () { + setup(function () { + sharedTestSetup.call(this); + + const createFocusableTree = function (rootElementId, nestedTrees) { + return new FocusableTreeImpl( + document.getElementById(rootElementId), + nestedTrees || [], + ); + }; + const createFocusableNode = function (tree, elementId) { + return tree.addNode(document.getElementById(elementId)); + }; + + this.testFocusableTree1 = createFocusableTree('testFocusableTree1'); + this.testFocusableTree1Node1 = createFocusableNode( + this.testFocusableTree1, + 'testFocusableTree1.node1', + ); + this.testFocusableTree1Node1Child1 = createFocusableNode( + this.testFocusableTree1, + 'testFocusableTree1.node1.child1', + ); + this.testFocusableTree1Node2 = createFocusableNode( + this.testFocusableTree1, + 'testFocusableTree1.node2', + ); + this.testFocusableNestedTree4 = createFocusableTree( + 'testFocusableNestedTree4', + ); + this.testFocusableNestedTree4Node1 = createFocusableNode( + this.testFocusableNestedTree4, + 'testFocusableNestedTree4.node1', + ); + this.testFocusableNestedTree5 = createFocusableTree( + 'testFocusableNestedTree5', + ); + this.testFocusableNestedTree5Node1 = createFocusableNode( + this.testFocusableNestedTree5, + 'testFocusableNestedTree5.node1', + ); + this.testFocusableTree2 = createFocusableTree('testFocusableTree2', [ + this.testFocusableNestedTree4, + this.testFocusableNestedTree5, + ]); + this.testFocusableTree2Node1 = createFocusableNode( + this.testFocusableTree2, + 'testFocusableTree2.node1', + ); + }); + + teardown(function () { + sharedTestTeardown.call(this); + + const removeFocusIndicators = function (element) { + element.classList.remove( + FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME, + FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME, + ); + }; + + // Ensure all node CSS styles are reset so that state isn't leaked between tests. + removeFocusIndicators(document.getElementById('testFocusableTree1')); + removeFocusIndicators(document.getElementById('testFocusableTree1.node1')); + removeFocusIndicators( + document.getElementById('testFocusableTree1.node1.child1'), + ); + removeFocusIndicators(document.getElementById('testFocusableTree1.node2')); + removeFocusIndicators(document.getElementById('testFocusableTree2')); + removeFocusIndicators(document.getElementById('testFocusableTree2.node1')); + removeFocusIndicators(document.getElementById('testFocusableNestedTree4')); + removeFocusIndicators( + document.getElementById('testFocusableNestedTree4.node1'), + ); + removeFocusIndicators(document.getElementById('testFocusableNestedTree5')); + removeFocusIndicators( + document.getElementById('testFocusableNestedTree5.node1'), + ); + }); + + suite('findFocusedNode()', function () { + test('for tree with no highlights returns null', function () { + const tree = this.testFocusableTree1; + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.isNull(finding); + }); + + test('for tree with root active highlight returns root node', function () { + const tree = this.testFocusableTree1; + const rootNode = tree.getRootFocusableNode(); + rootNode + .getFocusableElement() + .classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.strictEqual(finding, rootNode); + }); + + test('for tree with root passive highlight returns root node', function () { + const tree = this.testFocusableTree1; + const rootNode = tree.getRootFocusableNode(); + rootNode + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.strictEqual(finding, rootNode); + }); + + test('for tree with node active highlight returns node', function () { + const tree = this.testFocusableTree1; + const node = this.testFocusableTree1Node1; + node + .getFocusableElement() + .classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.strictEqual(finding, node); + }); + + test('for tree with node passive highlight returns node', function () { + const tree = this.testFocusableTree1; + const node = this.testFocusableTree1Node1; + node + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.strictEqual(finding, node); + }); + + test('for tree with nested node active highlight returns node', function () { + const tree = this.testFocusableTree1; + const node = this.testFocusableTree1Node1Child1; + node + .getFocusableElement() + .classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.strictEqual(finding, node); + }); + + test('for tree with nested node passive highlight returns node', function () { + const tree = this.testFocusableTree1; + const node = this.testFocusableTree1Node1Child1; + node + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.strictEqual(finding, node); + }); + + test('for tree with nested tree root active no parent highlights returns root', function () { + const tree = this.testFocusableNestedTree4; + const rootNode = this.testFocusableNestedTree4.getRootFocusableNode(); + rootNode + .getFocusableElement() + .classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.strictEqual(finding, rootNode); + }); + + test('for tree with nested tree root passive no parent highlights returns root', function () { + const tree = this.testFocusableNestedTree4; + const rootNode = this.testFocusableNestedTree4.getRootFocusableNode(); + rootNode + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.strictEqual(finding, rootNode); + }); + + test('for tree with nested tree node active no parent highlights returns node', function () { + const tree = this.testFocusableNestedTree4; + const node = this.testFocusableNestedTree4Node1; + node + .getFocusableElement() + .classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.strictEqual(finding, node); + }); + + test('for tree with nested tree root passive no parent highlights returns null', function () { + const tree = this.testFocusableNestedTree4; + const node = this.testFocusableNestedTree4Node1; + node + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode(tree); + + assert.strictEqual(finding, node); + }); + + test('for tree with nested tree root active parent node passive returns parent node', function () { + const tree = this.testFocusableNestedTree4; + const rootNode = this.testFocusableNestedTree4.getRootFocusableNode(); + this.testFocusableTree2Node1 + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + rootNode + .getFocusableElement() + .classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode( + this.testFocusableTree2, + ); + + assert.strictEqual(finding, this.testFocusableTree2Node1); + }); + + test('for tree with nested tree root passive parent node passive returns parent node', function () { + const tree = this.testFocusableNestedTree4; + const rootNode = this.testFocusableNestedTree4.getRootFocusableNode(); + this.testFocusableTree2Node1 + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + rootNode + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode( + this.testFocusableTree2, + ); + + assert.strictEqual(finding, this.testFocusableTree2Node1); + }); + + test('for tree with nested tree node active parent node passive returns parent node', function () { + const tree = this.testFocusableNestedTree4; + const node = this.testFocusableNestedTree4Node1; + this.testFocusableTree2Node1 + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + node + .getFocusableElement() + .classList.add(FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode( + this.testFocusableTree2, + ); + + assert.strictEqual(finding, this.testFocusableTree2Node1); + }); + + test('for tree with nested tree node passive parent node passive returns parent node', function () { + const tree = this.testFocusableNestedTree4; + const node = this.testFocusableNestedTree4Node1; + this.testFocusableTree2Node1 + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + node + .getFocusableElement() + .classList.add(FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME); + + const finding = FocusableTreeTraverser.findFocusedNode( + this.testFocusableTree2, + ); + + assert.strictEqual(finding, this.testFocusableTree2Node1); + }); + }); + + suite('findFocusableNodeFor()', function () { + test('for root element returns root', function () { + const tree = this.testFocusableTree1; + const rootNode = tree.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + rootElem, + tree, + ); + + assert.strictEqual(finding, rootNode); + }); + + test('for element for different tree root returns null', function () { + const tree = this.testFocusableTree1; + const rootNode = this.testFocusableTree2.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + rootElem, + tree, + ); + + assert.isNull(finding); + }); + + test('for element for different tree node returns null', function () { + const tree = this.testFocusableTree1; + const nodeElem = this.testFocusableTree2Node1.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + nodeElem, + tree, + ); + + assert.isNull(finding); + }); + + test('for node element in tree returns node', function () { + const tree = this.testFocusableTree1; + const nodeElem = this.testFocusableTree1Node1.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + nodeElem, + tree, + ); + + assert.strictEqual(finding, this.testFocusableTree1Node1); + }); + + test('for non-node element in tree returns root', function () { + const tree = this.testFocusableTree1; + const unregElem = document.getElementById( + 'testFocusableTree1.node2.unregisteredChild1', + ); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + unregElem, + tree, + ); + + // An unregistered element should map to the closest node. + assert.strictEqual(finding, this.testFocusableTree1Node2); + }); + + test('for nested node element in tree returns node', function () { + const tree = this.testFocusableTree1; + const nodeElem = this.testFocusableTree1Node1Child1.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + nodeElem, + tree, + ); + + // The nested node should be returned. + assert.strictEqual(finding, this.testFocusableTree1Node1Child1); + }); + + test('for nested node element in tree returns node', function () { + const tree = this.testFocusableTree1; + const unregElem = document.getElementById( + 'testFocusableTree1.node1.child1.unregisteredChild1', + ); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + unregElem, + tree, + ); + + // An unregistered element should map to the closest node. + assert.strictEqual(finding, this.testFocusableTree1Node1Child1); + }); + + test('for nested node element in tree returns node', function () { + const tree = this.testFocusableTree1; + const unregElem = document.getElementById( + 'testFocusableTree1.unregisteredChild1', + ); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + unregElem, + tree, + ); + + // An unregistered element should map to the closest node (or root). + assert.strictEqual(finding, tree.getRootFocusableNode()); + }); + + test('for nested tree root returns nested tree root', function () { + const tree = this.testFocusableNestedTree4; + const rootNode = tree.getRootFocusableNode(); + const rootElem = rootNode.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + rootElem, + tree, + ); + + assert.strictEqual(finding, rootNode); + }); + + test('for nested tree node returns nested tree node', function () { + const tree = this.testFocusableNestedTree4; + const nodeElem = this.testFocusableNestedTree4Node1.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + nodeElem, + tree, + ); + + // The node of the nested tree should be returned. + assert.strictEqual(finding, this.testFocusableNestedTree4Node1); + }); + + test('for nested element in nested tree node returns nearest nested node', function () { + const tree = this.testFocusableNestedTree4; + const unregElem = document.getElementById( + 'testFocusableNestedTree4.node1.unregisteredChild1', + ); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + unregElem, + tree, + ); + + // An unregistered element should map to the closest node. + assert.strictEqual(finding, this.testFocusableNestedTree4Node1); + }); + + test('for nested tree node under root with different tree base returns null', function () { + const tree = this.testFocusableTree2; + const nodeElem = this.testFocusableNestedTree5Node1.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + nodeElem, + tree, + ); + + // The nested node hierarchically sits below the outer tree, but using + // that tree as the basis should yield null since it's not a direct child. + assert.isNull(finding); + }); + + test('for nested tree node under node with different tree base returns null', function () { + const tree = this.testFocusableTree2; + const nodeElem = this.testFocusableNestedTree4Node1.getFocusableElement(); + + const finding = FocusableTreeTraverser.findFocusableNodeFor( + nodeElem, + tree, + ); + + // The nested node hierarchically sits below the outer tree, but using + // that tree as the basis should yield null since it's not a direct child. + assert.isNull(finding); + }); + }); +}); diff --git a/tests/mocha/index.html b/tests/mocha/index.html index 008d1f1b1..690b75a77 100644 --- a/tests/mocha/index.html +++ b/tests/mocha/index.html @@ -13,11 +13,152 @@ visibility: hidden; width: 1000px; } + + .blocklyActiveFocus { + outline-color: #0f0; + outline-width: 2px; + } + .blocklyPassiveFocus { + outline-color: #00f; + outline-width: 1.5px; + } + div.blocklyActiveFocus { + color: #0f0; + } + div.blocklyPassiveFocus { + color: #00f; + } + g.blocklyActiveFocus { + fill: #0f0; + } + g.blocklyPassiveFocus { + fill: #00f; + } - +
+
+ Focusable tree 1 +
+ Tree 1 node 1 +
+ Tree 1 node 1 child 1 +
+ Tree 1 node 1 child 1 child 1 (unregistered) +
+
+
+
+ Tree 1 node 2 +
+ Tree 1 node 2 child 2 (unregistered) +
+
+
+ Tree 1 child 1 (unregistered) +
+
+
+ Focusable tree 2 +
+ Tree 2 node 1 +
+ Nested tree 4 +
+ Tree 4 node 1 (nested) +
+ Tree 4 node 1 child 1 (unregistered) +
+
+
+
+
+ Nested tree 5 +
+ Tree 5 node 1 (nested) +
+
+
+
+ Unregistered tree 3 +
+ Tree 3 node 1 (unregistered) +
+
+
Unfocusable element
+
+ + + + + Group 1 node 1 + + + Tree 1 node 1 child 1 + + + + + Group 1 node 2 + + + + Tree 1 node 2 child 2 (unregistered) + + + + + + + + Group 2 node 1 + + + + + Group 4 node 1 (nested) + + + + + + + + Tree 3 node 1 (unregistered) + + + + + @@ -90,6 +231,8 @@ import './field_textinput_test.js'; import './field_variable_test.js'; import './flyout_test.js'; + import './focus_manager_test.js'; + import './focusable_tree_traverser_test.js'; import './generator_test.js'; import './gesture_test.js'; import './icon_test.js';