feat!: Introduce new focus tree/node functions.

This introduces new callback methods for IFocusableTree and
IFocusableNode for providing a basis of synchronizing domain state with
focus changes. It also introduces support for implementations of
IFocusableTree to better manage initial state cases, especially when a
tree is focused using tab navigation.

FocusManager has also been updated to ensure functional parity between
tab-navigating to a tree and using focusTree() on that tree. This means
that tab navigating to a tree will actually restore focus back to that
tree's previous focused node rather than the root (unless the root is
navigated to from within the tree itself). This is meant to provide
better consistency between tab and non-tab keyboard navigation.

Note that these changes originally came from #8875 and are required for
later PRs that will introduce IFocusableNode and IFocusableTree
implementations.
This commit is contained in:
Ben Henning
2025-04-21 20:37:26 +00:00
parent 9d127698d6
commit 0772a29824
5 changed files with 248 additions and 30 deletions

View File

@@ -27,6 +27,10 @@ class FocusableNodeImpl {
getFocusableTree() {
return this.tree;
}
onNodeFocus() {}
onNodeBlur() {}
}
class FocusableTreeImpl {
@@ -46,6 +50,10 @@ class FocusableTreeImpl {
return this.rootNode;
}
getRestoredFocusableNode() {
return null;
}
getNestedTrees() {
return this.nestedTrees;
}
@@ -53,6 +61,10 @@ class FocusableTreeImpl {
lookUpFocusableNode(id) {
return this.idToNodeMap[id];
}
onTreeFocus() {}
onTreeBlur() {}
}
suite('FocusManager', function () {
@@ -2067,7 +2079,7 @@ suite('FocusManager', function () {
);
});
test('registered tree focus()ed other tree node passively focused tree root now has active property', function () {
test('registered tree focus()ed other tree node passively focused tree node now has active property', function () {
this.focusManager.registerTree(this.testFocusableTree1);
this.focusManager.registerTree(this.testFocusableTree2);
document.getElementById('testFocusableTree1.node1').focus();
@@ -2075,26 +2087,27 @@ suite('FocusManager', function () {
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.
// Directly refocusing a tree's root should have functional parity with focusTree(). That
// means the tree's previous node should now have active focus again and its root should
// have no focus indication.
const rootElem = this.testFocusableTree1
.getRootFocusableNode()
.getFocusableElement();
const nodeElem = this.testFocusableTree1Node1.getFocusableElement();
assert.includesClass(
rootElem.classList,
nodeElem.classList,
FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME,
);
assert.notIncludesClass(
rootElem.classList,
nodeElem.classList,
FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME,
);
assert.notIncludesClass(
nodeElem.classList,
rootElem.classList,
FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME,
);
assert.notIncludesClass(
nodeElem.classList,
rootElem.classList,
FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME,
);
});
@@ -3879,7 +3892,7 @@ suite('FocusManager', function () {
);
});
test('registered tree focus()ed other tree node passively focused tree root now has active property', function () {
test('registered tree focus()ed other tree node passively focused tree node now has active property', function () {
this.focusManager.registerTree(this.testFocusableGroup1);
this.focusManager.registerTree(this.testFocusableGroup2);
document.getElementById('testFocusableGroup1.node1').focus();
@@ -3887,26 +3900,27 @@ suite('FocusManager', function () {
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.
// Directly refocusing a tree's root should have functional parity with focusTree(). That
// means the tree's previous node should now have active focus again and its root should
// have no focus indication.
const rootElem = this.testFocusableGroup1
.getRootFocusableNode()
.getFocusableElement();
const nodeElem = this.testFocusableGroup1Node1.getFocusableElement();
assert.includesClass(
rootElem.classList,
nodeElem.classList,
FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME,
);
assert.notIncludesClass(
rootElem.classList,
nodeElem.classList,
FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME,
);
assert.notIncludesClass(
nodeElem.classList,
rootElem.classList,
FocusManager.ACTIVE_FOCUS_NODE_CSS_CLASS_NAME,
);
assert.notIncludesClass(
nodeElem.classList,
rootElem.classList,
FocusManager.PASSIVE_FOCUS_NODE_CSS_CLASS_NAME,
);
});

View File

@@ -25,6 +25,10 @@ class FocusableNodeImpl {
getFocusableTree() {
return this.tree;
}
onNodeFocus() {}
onNodeBlur() {}
}
class FocusableTreeImpl {
@@ -44,6 +48,10 @@ class FocusableTreeImpl {
return this.rootNode;
}
getRestoredFocusableNode() {
return null;
}
getNestedTrees() {
return this.nestedTrees;
}
@@ -51,6 +59,10 @@ class FocusableTreeImpl {
lookUpFocusableNode(id) {
return this.idToNodeMap[id];
}
onTreeFocus() {}
onTreeBlur() {}
}
suite('FocusableTreeTraverser', function () {