Unverified Commit 1bd8b580 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Make `FocusNode.traversalChildren` not be affected by parent's `canRequestFocus` (#95061)

parent ee328e35
...@@ -446,15 +446,15 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -446,15 +446,15 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// If set to false on a [FocusScopeNode], will cause all of the children of /// If set to false on a [FocusScopeNode], will cause all of the children of
/// the scope node to not be focusable. /// the scope node to not be focusable.
/// ///
/// If set to false on a [FocusNode], it will not affect the children of the /// If set to false on a [FocusNode], it will not affect the focusability of
/// node. /// children of the node.
/// ///
/// The [hasFocus] member can still return true if this node is the ancestor /// The [hasFocus] member can still return true if this node is the ancestor
/// of a node with primary focus. /// of a node with primary focus.
/// ///
/// This is different than [skipTraversal] because [skipTraversal] still /// This is different than [skipTraversal] because [skipTraversal] still
/// allows the node to be focused, just not traversed to via the /// allows the node to be focused, just not traversed to via the
/// [FocusTraversalPolicy] /// [FocusTraversalPolicy].
/// ///
/// Setting [canRequestFocus] to false implies that the node will also be /// Setting [canRequestFocus] to false implies that the node will also be
/// skipped for traversal purposes. /// skipped for traversal purposes.
...@@ -575,9 +575,18 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -575,9 +575,18 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// An iterator over the children that are allowed to be traversed by the /// An iterator over the children that are allowed to be traversed by the
/// [FocusTraversalPolicy]. /// [FocusTraversalPolicy].
///
/// Returns the list of focusable, traversable children of this node,
/// regardless of those settings on this focus node. Will return an empty
/// iterable if [descendantsAreFocusable] is false.
///
/// See also
///
/// * [traversalDescendants], which traverses all of the node's descendants,
/// not just the immediate children.
Iterable<FocusNode> get traversalChildren { Iterable<FocusNode> get traversalChildren {
if (!canRequestFocus) { if (!descendantsAreFocusable) {
return const <FocusNode>[]; return const Iterable<FocusNode>.empty();
} }
return children.where( return children.where(
(FocusNode node) => !node.skipTraversal && node.canRequestFocus, (FocusNode node) => !node.skipTraversal && node.canRequestFocus,
...@@ -615,7 +624,12 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -615,7 +624,12 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// Returns all descendants which do not have the [skipTraversal] and do have /// Returns all descendants which do not have the [skipTraversal] and do have
/// the [canRequestFocus] flag set. /// the [canRequestFocus] flag set.
Iterable<FocusNode> get traversalDescendants => descendants.where((FocusNode node) => !node.skipTraversal && node.canRequestFocus); Iterable<FocusNode> get traversalDescendants {
if (!descendantsAreFocusable) {
return const Iterable<FocusNode>.empty();
}
return descendants.where((FocusNode node) => !node.skipTraversal && node.canRequestFocus);
}
/// An [Iterable] over the ancestors of this node. /// An [Iterable] over the ancestors of this node.
/// ///
...@@ -1184,6 +1198,37 @@ class FocusScopeNode extends FocusNode { ...@@ -1184,6 +1198,37 @@ class FocusScopeNode extends FocusNode {
// last (which is the top of the stack). // last (which is the top of the stack).
final List<FocusNode> _focusedChildren = <FocusNode>[]; final List<FocusNode> _focusedChildren = <FocusNode>[];
/// An iterator over the children that are allowed to be traversed by the
/// [FocusTraversalPolicy].
///
/// Will return an empty iterable if this scope node is not focusable, or if
/// [descendantsAreFocusable] is false.
///
/// See also:
///
/// * [traversalDescendants], which traverses all of the node's descendants,
/// not just the immediate children.
@override
Iterable<FocusNode> get traversalChildren {
if (!canRequestFocus) {
return const Iterable<FocusNode>.empty();
}
return super.traversalChildren;
}
/// Returns all descendants which do not have the [skipTraversal] and do have
/// the [canRequestFocus] flag set.
///
/// Will return an empty iterable if this scope node is not focusable, or if
/// [descendantsAreFocusable] is false.
@override
Iterable<FocusNode> get traversalDescendants {
if (!canRequestFocus) {
return const Iterable<FocusNode>.empty();
}
return super.traversalDescendants;
}
/// Make the given [scope] the active child scope for this scope. /// Make the given [scope] the active child scope for this scope.
/// ///
/// If the given [scope] is not yet a part of the focus tree, then add it to /// If the given [scope] is not yet a part of the focus tree, then add it to
...@@ -1259,7 +1304,7 @@ class FocusScopeNode extends FocusNode { ...@@ -1259,7 +1304,7 @@ class FocusScopeNode extends FocusNode {
final List<String> childList = _focusedChildren.reversed.map<String>((FocusNode child) { final List<String> childList = _focusedChildren.reversed.map<String>((FocusNode child) {
return child.toStringShort(); return child.toStringShort();
}).toList(); }).toList();
properties.add(IterableProperty<String>('focusedChildren', childList, defaultValue: <String>[])); properties.add(IterableProperty<String>('focusedChildren', childList, defaultValue: const Iterable<String>.empty()));
} }
} }
......
...@@ -151,6 +151,37 @@ void main() { ...@@ -151,6 +151,37 @@ void main() {
expect(scope.traversalDescendants.contains(child2), isFalse); expect(scope.traversalDescendants.contains(child2), isFalse);
}); });
testWidgets("canRequestFocus doesn't affect traversalChildren", (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope);
parent2Attachment.reparent(parent: scope);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent2);
child1.requestFocus();
await tester.pump();
expect(tester.binding.focusManager.primaryFocus, equals(child1));
expect(scope.focusedChild, equals(child1));
expect(parent2.traversalChildren.contains(child2), isTrue);
expect(scope.traversalChildren.contains(parent2), isTrue);
parent2.canRequestFocus = false;
await tester.pump();
expect(parent2.traversalChildren.contains(child2), isTrue);
expect(scope.traversalChildren.contains(parent2), isFalse);
});
testWidgets('implements debugFillProperties', (WidgetTester tester) async { testWidgets('implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
FocusNode( FocusNode(
...@@ -502,6 +533,8 @@ void main() { ...@@ -502,6 +533,8 @@ void main() {
expect(scope.focusedChild, equals(child1)); expect(scope.focusedChild, equals(child1));
expect(scope.traversalDescendants.contains(child1), isTrue); expect(scope.traversalDescendants.contains(child1), isTrue);
expect(scope.traversalDescendants.contains(child2), isTrue); expect(scope.traversalDescendants.contains(child2), isTrue);
expect(scope.traversalChildren.contains(parent1), isTrue);
expect(parent1.traversalChildren.contains(child2), isTrue);
scope.canRequestFocus = false; scope.canRequestFocus = false;
await tester.pump(); await tester.pump();
...@@ -512,6 +545,8 @@ void main() { ...@@ -512,6 +545,8 @@ void main() {
expect(scope.focusedChild, equals(child1)); expect(scope.focusedChild, equals(child1));
expect(scope.traversalDescendants.contains(child1), isFalse); expect(scope.traversalDescendants.contains(child1), isFalse);
expect(scope.traversalDescendants.contains(child2), isFalse); expect(scope.traversalDescendants.contains(child2), isFalse);
expect(scope.traversalChildren.contains(parent1), isFalse);
expect(parent1.traversalChildren.contains(child2), isFalse);
}); });
testWidgets("skipTraversal doesn't affect children.", (WidgetTester tester) async { testWidgets("skipTraversal doesn't affect children.", (WidgetTester tester) async {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment