Adds ability to mark a subtree as not traversable (#94626)
diff --git a/packages/flutter/lib/src/widgets/actions.dart b/packages/flutter/lib/src/widgets/actions.dart
index a600271..d52dbfd 100644
--- a/packages/flutter/lib/src/widgets/actions.dart
+++ b/packages/flutter/lib/src/widgets/actions.dart
@@ -1064,6 +1064,7 @@
this.focusNode,
this.autofocus = false,
this.descendantsAreFocusable = true,
+ this.descendantsAreTraversable = true,
this.shortcuts,
this.actions,
this.onShowFocusHighlight,
@@ -1095,6 +1096,9 @@
/// {@macro flutter.widgets.Focus.descendantsAreFocusable}
final bool descendantsAreFocusable;
+ /// {@macro flutter.widgets.Focus.descendantsAreTraversable}
+ final bool descendantsAreTraversable;
+
/// {@macro flutter.widgets.actions.actions}
final Map<Type, Action<Intent>>? actions;
@@ -1281,6 +1285,7 @@
focusNode: widget.focusNode,
autofocus: widget.autofocus,
descendantsAreFocusable: widget.descendantsAreFocusable,
+ descendantsAreTraversable: widget.descendantsAreTraversable,
canRequestFocus: _canRequestFocus,
onFocusChange: _handleFocusChange,
child: widget.child,
diff --git a/packages/flutter/lib/src/widgets/focus_manager.dart b/packages/flutter/lib/src/widgets/focus_manager.dart
index a9dc624..cd1c2e3 100644
--- a/packages/flutter/lib/src/widgets/focus_manager.dart
+++ b/packages/flutter/lib/src/widgets/focus_manager.dart
@@ -409,12 +409,14 @@
bool skipTraversal = false,
bool canRequestFocus = true,
bool descendantsAreFocusable = true,
+ bool descendantsAreTraversable = true,
}) : assert(skipTraversal != null),
assert(canRequestFocus != null),
assert(descendantsAreFocusable != null),
_skipTraversal = skipTraversal,
_canRequestFocus = canRequestFocus,
- _descendantsAreFocusable = descendantsAreFocusable {
+ _descendantsAreFocusable = descendantsAreFocusable,
+ _descendantsAreTraversable = descendantsAreTraversable {
// Set it via the setter so that it does nothing on release builds.
this.debugLabel = debugLabel;
}
@@ -429,7 +431,17 @@
/// This is different from [canRequestFocus] because it only implies that the
/// node can't be reached via traversal, not that it can't be focused. It may
/// still be focused explicitly.
- bool get skipTraversal => _skipTraversal;
+ bool get skipTraversal {
+ if (_skipTraversal) {
+ return true;
+ }
+ for (final FocusNode ancestor in ancestors) {
+ if (!ancestor.descendantsAreTraversable) {
+ return true;
+ }
+ }
+ return false;
+ }
bool _skipTraversal;
set skipTraversal(bool value) {
if (value != _skipTraversal) {
@@ -511,13 +523,17 @@
///
/// See also:
///
- /// * [ExcludeFocus], a widget that uses this property to conditionally
- /// exclude focus for a subtree.
- /// * [Focus], a widget that exposes this setting as a parameter.
- /// * [FocusTraversalGroup], a widget used to group together and configure
- /// the focus traversal policy for a widget subtree that also has an
- /// `descendantsAreFocusable` parameter that prevents its children from
- /// being focused.
+ /// * [ExcludeFocus], a widget that uses this property to conditionally
+ /// exclude focus for a subtree.
+ /// * [descendantsAreTraversable], which makes this widget's descendants
+ /// untraversable.
+ /// * [ExcludeFocusTraversal], a widget that conditionally excludes focus
+ /// traversal for a subtree.
+ /// * [Focus], a widget that exposes this setting as a parameter.
+ /// * [FocusTraversalGroup], a widget used to group together and configure
+ /// the focus traversal policy for a widget subtree that also has an
+ /// `descendantsAreFocusable` parameter that prevents its children from
+ /// being focused.
bool get descendantsAreFocusable => _descendantsAreFocusable;
bool _descendantsAreFocusable;
@mustCallSuper
@@ -534,6 +550,36 @@
_manager?._markPropertiesChanged(this);
}
+ /// If false, tells the focus traversal policy to skip over for all of this
+ /// node's descendants for purposes of the traversal algorithm.
+ ///
+ /// Defaults to true. Does not affect the focus traversal of this node: for
+ /// that, use [skipTraversal].
+ ///
+ /// Does not affect the value of [FocusNode.skipTraversal] on the
+ /// descendants. Does not affect focusability of the descendants.
+ ///
+ /// See also:
+ ///
+ /// * [ExcludeFocusTraversal], a widget that uses this property to conditionally
+ /// exclude focus traversal for a subtree.
+ /// * [descendantsAreFocusable], which makes this widget's descendants
+ /// unfocusable.
+ /// * [ExcludeFocus], a widget that conditionally excludes focus for a subtree.
+ /// * [FocusTraversalGroup], a widget used to group together and configure
+ /// the focus traversal policy for a widget subtree that also has an
+ /// `descendantsAreFocusable` parameter that prevents its children from
+ /// being focused.
+ bool get descendantsAreTraversable => _descendantsAreTraversable;
+ bool _descendantsAreTraversable;
+ @mustCallSuper
+ set descendantsAreTraversable(bool value) {
+ if (value != _descendantsAreTraversable) {
+ _descendantsAreTraversable = value;
+ _manager?._markPropertiesChanged(this);
+ }
+ }
+
/// The context that was supplied to [attach].
///
/// This is typically the context for the widget that is being focused, as it
@@ -1105,6 +1151,7 @@
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<BuildContext>('context', context, defaultValue: null));
properties.add(FlagProperty('descendantsAreFocusable', value: descendantsAreFocusable, ifFalse: 'DESCENDANTS UNFOCUSABLE', defaultValue: true));
+ properties.add(FlagProperty('descendantsAreTraversable', value: descendantsAreTraversable, ifFalse: 'DESCENDANTS UNTRAVERSABLE', defaultValue: true));
properties.add(FlagProperty('canRequestFocus', value: canRequestFocus, ifFalse: 'NOT FOCUSABLE', defaultValue: true));
properties.add(FlagProperty('hasFocus', value: hasFocus && !hasPrimaryFocus, ifTrue: 'IN FOCUS PATH', defaultValue: false));
properties.add(FlagProperty('hasPrimaryFocus', value: hasPrimaryFocus, ifTrue: 'PRIMARY FOCUS', defaultValue: false));
diff --git a/packages/flutter/lib/src/widgets/focus_scope.dart b/packages/flutter/lib/src/widgets/focus_scope.dart
index 3cb56be..fa78734 100644
--- a/packages/flutter/lib/src/widgets/focus_scope.dart
+++ b/packages/flutter/lib/src/widgets/focus_scope.dart
@@ -124,6 +124,7 @@
bool? canRequestFocus,
bool? skipTraversal,
bool? descendantsAreFocusable,
+ bool? descendantsAreTraversable,
this.includeSemantics = true,
String? debugLabel,
}) : _onKeyEvent = onKeyEvent,
@@ -131,6 +132,7 @@
_canRequestFocus = canRequestFocus,
_skipTraversal = skipTraversal,
_descendantsAreFocusable = descendantsAreFocusable,
+ _descendantsAreTraversable = descendantsAreTraversable,
_debugLabel = debugLabel,
assert(child != null),
assert(autofocus != null),
@@ -279,10 +281,17 @@
/// Does not affect the value of [FocusNode.canRequestFocus] on the
/// descendants.
///
+ /// If a descendant node loses focus when this value is changed, the focus
+ /// will move to the scope enclosing this node.
+ ///
/// See also:
///
/// * [ExcludeFocus], a widget that uses this property to conditionally
/// exclude focus for a subtree.
+ /// * [descendantsAreTraversable], which makes this widget's descendants
+ /// untraversable.
+ /// * [ExcludeFocusTraversal], a widget that conditionally excludes focus
+ /// traversal for a subtree.
/// * [FocusTraversalGroup], a widget used to group together and configure the
/// focus traversal policy for a widget subtree that has a
/// `descendantsAreFocusable` parameter to conditionally block focus for a
@@ -291,6 +300,30 @@
bool get descendantsAreFocusable => _descendantsAreFocusable ?? focusNode?.descendantsAreFocusable ?? true;
final bool? _descendantsAreFocusable;
+ /// {@template flutter.widgets.Focus.descendantsAreTraversable}
+ /// If false, will make this widget's descendants untraversable.
+ ///
+ /// Defaults to true. Does not affect traversablility of this node (just its
+ /// descendants): for that, use [FocusNode.skipTraversal].
+ ///
+ /// Does not affect the value of [FocusNode.skipTraversal] on the
+ /// descendants. Does not affect focusability of the descendants.
+ ///
+ /// See also:
+ ///
+ /// * [ExcludeFocusTraversal], a widget that uses this property to
+ /// conditionally exclude focus traversal for a subtree.
+ /// * [descendantsAreFocusable], which makes this widget's descendants
+ /// unfocusable.
+ /// * [ExcludeFocus], a widget that conditionally excludes focus for a subtree.
+ /// * [FocusTraversalGroup], a widget used to group together and configure the
+ /// focus traversal policy for a widget subtree that has a
+ /// `descendantsAreFocusable` parameter to conditionally block focus for a
+ /// subtree.
+ /// {@endtemplate}
+ bool get descendantsAreTraversable => _descendantsAreTraversable ?? focusNode?.descendantsAreTraversable ?? true;
+ final bool? _descendantsAreTraversable;
+
/// {@template flutter.widgets.Focus.includeSemantics}
/// Include semantics information in this widget.
///
@@ -420,6 +453,7 @@
properties.add(FlagProperty('autofocus', value: autofocus, ifTrue: 'AUTOFOCUS', defaultValue: false));
properties.add(FlagProperty('canRequestFocus', value: canRequestFocus, ifFalse: 'NOT FOCUSABLE', defaultValue: false));
properties.add(FlagProperty('descendantsAreFocusable', value: descendantsAreFocusable, ifFalse: 'DESCENDANTS UNFOCUSABLE', defaultValue: true));
+ properties.add(FlagProperty('descendantsAreTraversable', value: descendantsAreTraversable, ifFalse: 'DESCENDANTS UNTRAVERSABLE', defaultValue: true));
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null));
}
@@ -459,6 +493,8 @@
@override
bool get descendantsAreFocusable => focusNode!.descendantsAreFocusable;
@override
+ bool? get _descendantsAreTraversable => focusNode!.descendantsAreTraversable;
+ @override
String? get debugLabel => focusNode!.debugLabel;
}
@@ -468,6 +504,7 @@
late bool _hadPrimaryFocus;
late bool _couldRequestFocus;
late bool _descendantsWereFocusable;
+ late bool _descendantsWereTraversable;
bool _didAutofocus = false;
FocusAttachment? _focusAttachment;
@@ -485,6 +522,7 @@
_internalNode ??= _createNode();
}
focusNode.descendantsAreFocusable = widget.descendantsAreFocusable;
+ focusNode.descendantsAreTraversable = widget.descendantsAreTraversable;
if (widget.skipTraversal != null) {
focusNode.skipTraversal = widget.skipTraversal;
}
@@ -493,6 +531,7 @@
}
_couldRequestFocus = focusNode.canRequestFocus;
_descendantsWereFocusable = focusNode.descendantsAreFocusable;
+ _descendantsWereTraversable = focusNode.descendantsAreTraversable;
_hadPrimaryFocus = focusNode.hasPrimaryFocus;
_focusAttachment = focusNode.attach(context, onKeyEvent: widget.onKeyEvent, onKey: widget.onKey);
@@ -507,6 +546,7 @@
debugLabel: widget.debugLabel,
canRequestFocus: widget.canRequestFocus,
descendantsAreFocusable: widget.descendantsAreFocusable,
+ descendantsAreTraversable: widget.descendantsAreTraversable,
skipTraversal: widget.skipTraversal,
);
}
@@ -579,6 +619,7 @@
focusNode.canRequestFocus = widget._canRequestFocus!;
}
focusNode.descendantsAreFocusable = widget.descendantsAreFocusable;
+ focusNode.descendantsAreTraversable = widget.descendantsAreTraversable;
}
} else {
_focusAttachment!.detach();
@@ -595,6 +636,7 @@
final bool hasPrimaryFocus = focusNode.hasPrimaryFocus;
final bool canRequestFocus = focusNode.canRequestFocus;
final bool descendantsAreFocusable = focusNode.descendantsAreFocusable;
+ final bool descendantsAreTraversable = focusNode.descendantsAreTraversable;
widget.onFocusChange?.call(focusNode.hasFocus);
// Check the cached states that matter here, and call setState if they have
// changed.
@@ -613,6 +655,11 @@
_descendantsWereFocusable = descendantsAreFocusable;
});
}
+ if (_descendantsWereTraversable != descendantsAreTraversable) {
+ setState(() {
+ _descendantsWereTraversable = descendantsAreTraversable;
+ });
+ }
}
@override
@@ -784,6 +831,8 @@
@override
bool get descendantsAreFocusable => focusNode!.descendantsAreFocusable;
@override
+ bool get descendantsAreTraversable => focusNode!.descendantsAreTraversable;
+ @override
String? get debugLabel => focusNode!.debugLabel;
}
diff --git a/packages/flutter/lib/src/widgets/focus_traversal.dart b/packages/flutter/lib/src/widgets/focus_traversal.dart
index 9cddb98..7a2d490 100644
--- a/packages/flutter/lib/src/widgets/focus_traversal.dart
+++ b/packages/flutter/lib/src/widgets/focus_traversal.dart
@@ -388,6 +388,11 @@
}
}
final List<FocusNode> sortedNodes = _sortAllDescendants(nearestScope, currentNode);
+ if (sortedNodes.isEmpty) {
+ // If there are no nodes to traverse to, like when descendantsAreTraversable
+ // is false or skipTraversal for all the nodes is true.
+ return false;
+ }
if (forward && focusedChild == sortedNodes.last) {
_focusAndEnsureVisible(sortedNodes.first, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd);
return true;
@@ -1446,10 +1451,12 @@
Key? key,
FocusTraversalPolicy? policy,
this.descendantsAreFocusable = true,
+ this.descendantsAreTraversable = true,
required this.child,
- }) : assert(descendantsAreFocusable != null),
- policy = policy ?? ReadingOrderTraversalPolicy(),
- super(key: key);
+ }) : assert(descendantsAreFocusable != null),
+ assert(descendantsAreTraversable != null),
+ policy = policy ?? ReadingOrderTraversalPolicy(),
+ super(key: key);
/// The policy used to move the focus from one focus node to another when
/// traversing them using a keyboard.
@@ -1471,6 +1478,9 @@
/// {@macro flutter.widgets.Focus.descendantsAreFocusable}
final bool descendantsAreFocusable;
+ /// {@macro flutter.widgets.Focus.descendantsAreTraversable}
+ final bool descendantsAreTraversable;
+
/// The child widget of this [FocusTraversalGroup].
///
/// {@macro flutter.widgets.ProxyWidget.child}
@@ -1573,6 +1583,7 @@
skipTraversal: true,
includeSemantics: false,
descendantsAreFocusable: widget.descendantsAreFocusable,
+ descendantsAreTraversable: widget.descendantsAreTraversable,
child: widget.child,
),
);
@@ -1737,3 +1748,62 @@
}
}
}
+
+/// A widget that controls whether or not the descendants of this widget are
+/// traversable.
+///
+/// Does not affect the value of [FocusNode.skipTraversal] of the descendants.
+///
+/// See also:
+///
+/// * [Focus], a widget for adding and managing a [FocusNode] in the widget tree.
+/// * [ExcludeFocus], a widget that excludes its descendants from focusability.
+/// * [FocusTraversalGroup], a widget that groups widgets for focus traversal,
+/// and can also be used in the same way as this widget by setting its
+/// `descendantsAreFocusable` attribute.
+class ExcludeFocusTraversal extends StatelessWidget {
+ /// Const constructor for [ExcludeFocusTraversal] widget.
+ ///
+ /// The [excluding] argument must not be null.
+ ///
+ /// The [child] argument is required, and must not be null.
+ const ExcludeFocusTraversal({
+ Key? key,
+ this.excluding = true,
+ required this.child,
+ }) : assert(excluding != null),
+ assert(child != null),
+ super(key: key);
+
+ /// If true, will make this widget's descendants untraversable.
+ ///
+ /// Defaults to true.
+ ///
+ /// Does not affect the value of [FocusNode.skipTraversal] on the descendants.
+ ///
+ /// See also:
+ ///
+ /// * [Focus.descendantsAreTraversable], the attribute of a [Focus] widget that
+ /// controls this same property for focus widgets.
+ /// * [FocusTraversalGroup], a widget used to group together and configure the
+ /// focus traversal policy for a widget subtree that has a
+ /// `descendantsAreFocusable` parameter to conditionally block focus for a
+ /// subtree.
+ final bool excluding;
+
+ /// The child widget of this [ExcludeFocusTraversal].
+ ///
+ /// {@macro flutter.widgets.ProxyWidget.child}
+ final Widget child;
+
+ @override
+ Widget build(BuildContext context) {
+ return Focus(
+ canRequestFocus: false,
+ skipTraversal: true,
+ includeSemantics: false,
+ descendantsAreTraversable: !excluding,
+ child: child,
+ );
+ }
+}
diff --git a/packages/flutter/test/widgets/actions_test.dart b/packages/flutter/test/widgets/actions_test.dart
index c476ac5..0a7879d 100644
--- a/packages/flutter/test/widgets/actions_test.dart
+++ b/packages/flutter/test/widgets/actions_test.dart
@@ -962,6 +962,75 @@
expect(buttonNode.hasFocus, isFalse);
},
);
+
+ testWidgets(
+ 'FocusableActionDetector can prevent its descendants from being traversable',
+ (WidgetTester tester) async {
+ final FocusNode buttonNode1 = FocusNode(debugLabel: 'Button Node 1');
+ final FocusNode buttonNode2 = FocusNode(debugLabel: 'Button Node 2');
+
+ await tester.pumpWidget(
+ MaterialApp(
+ home: FocusableActionDetector(
+ child: Column(
+ children: <Widget>[
+ MaterialButton(
+ focusNode: buttonNode1,
+ child: const Text('Node 1'),
+ onPressed: () {},
+ ),
+ MaterialButton(
+ focusNode: buttonNode2,
+ child: const Text('Node 2'),
+ onPressed: () {},
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+
+ buttonNode1.requestFocus();
+ await tester.pump();
+ expect(buttonNode1.hasFocus, isTrue);
+ expect(buttonNode2.hasFocus, isFalse);
+ primaryFocus!.nextFocus();
+ await tester.pump();
+ expect(buttonNode1.hasFocus, isFalse);
+ expect(buttonNode2.hasFocus, isTrue);
+
+ await tester.pumpWidget(
+ MaterialApp(
+ home: FocusableActionDetector(
+ descendantsAreTraversable: false,
+ child: Column(
+ children: <Widget>[
+ MaterialButton(
+ focusNode: buttonNode1,
+ child: const Text('Node 1'),
+ onPressed: () {},
+ ),
+ MaterialButton(
+ focusNode: buttonNode2,
+ child: const Text('Node 2'),
+ onPressed: () {},
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+
+ buttonNode1.requestFocus();
+ await tester.pump();
+ expect(buttonNode1.hasFocus, isTrue);
+ expect(buttonNode2.hasFocus, isFalse);
+ primaryFocus!.nextFocus();
+ await tester.pump();
+ expect(buttonNode1.hasFocus, isTrue);
+ expect(buttonNode2.hasFocus, isFalse);
+ },
+ );
});
group('Diagnostics', () {
diff --git a/packages/flutter/test/widgets/focus_manager_test.dart b/packages/flutter/test/widgets/focus_manager_test.dart
index d8bcfd5..052bd88 100644
--- a/packages/flutter/test/widgets/focus_manager_test.dart
+++ b/packages/flutter/test/widgets/focus_manager_test.dart
@@ -151,6 +151,39 @@
expect(scope.traversalDescendants.contains(child2), isFalse);
});
+ testWidgets('descendantsAreTraversable disables traversal for descendants.', (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);
+
+ expect(scope.traversalDescendants, equals(<FocusNode>[child1, parent1, child2, parent2]));
+
+ parent2.descendantsAreTraversable = false;
+ expect(scope.traversalDescendants, equals(<FocusNode>[child1, parent1, parent2]));
+
+ parent1.descendantsAreTraversable = false;
+ expect(scope.traversalDescendants, equals(<FocusNode>[parent1, parent2]));
+
+ parent1.descendantsAreTraversable = true;
+ parent2.descendantsAreTraversable = true;
+ scope.descendantsAreTraversable = false;
+ expect(scope.traversalDescendants, equals(<FocusNode>[]));
+ });
+
testWidgets("canRequestFocus doesn't affect traversalChildren", (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope');
@@ -191,6 +224,7 @@
expect(description, <String>[
'context: null',
'descendantsAreFocusable: true',
+ 'descendantsAreTraversable: true',
'canRequestFocus: true',
'hasFocus: false',
'hasPrimaryFocus: false',
@@ -1156,6 +1190,7 @@
expect(description, <String>[
'context: null',
'descendantsAreFocusable: true',
+ 'descendantsAreTraversable: true',
'canRequestFocus: true',
'hasFocus: false',
'hasPrimaryFocus: false',
diff --git a/packages/flutter/test/widgets/focus_scope_test.dart b/packages/flutter/test/widgets/focus_scope_test.dart
index 0e92cc2..8920314 100644
--- a/packages/flutter/test/widgets/focus_scope_test.dart
+++ b/packages/flutter/test/widgets/focus_scope_test.dart
@@ -1085,6 +1085,7 @@
focusScopeNode.onKey = ignoreCallback;
focusScopeNode.onKeyEvent = ignoreEventCallback;
focusScopeNode.descendantsAreFocusable = false;
+ focusScopeNode.descendantsAreTraversable = false;
focusScopeNode.skipTraversal = false;
focusScopeNode.canRequestFocus = true;
FocusScope focusScopeWidget = FocusScope.withExternalFocusNode(
@@ -1095,11 +1096,13 @@
expect(focusScopeNode.onKey, equals(ignoreCallback));
expect(focusScopeNode.onKeyEvent, equals(ignoreEventCallback));
expect(focusScopeNode.descendantsAreFocusable, isFalse);
+ expect(focusScopeNode.descendantsAreTraversable, isFalse);
expect(focusScopeNode.skipTraversal, isFalse);
expect(focusScopeNode.canRequestFocus, isTrue);
expect(focusScopeWidget.onKey, equals(focusScopeNode.onKey));
expect(focusScopeWidget.onKeyEvent, equals(focusScopeNode.onKeyEvent));
expect(focusScopeWidget.descendantsAreFocusable, equals(focusScopeNode.descendantsAreFocusable));
+ expect(focusScopeWidget.descendantsAreTraversable, equals(focusScopeNode.descendantsAreTraversable));
expect(focusScopeWidget.skipTraversal, equals(focusScopeNode.skipTraversal));
expect(focusScopeWidget.canRequestFocus, equals(focusScopeNode.canRequestFocus));
@@ -1111,6 +1114,7 @@
focusScopeNode.onKey = handleCallback;
focusScopeNode.onKeyEvent = handleEventCallback;
focusScopeNode.descendantsAreFocusable = true;
+ focusScopeNode.descendantsAreTraversable = true;
focusScopeWidget = FocusScope.withExternalFocusNode(
focusScopeNode: focusScopeNode,
child: Container(key: key1),
@@ -1119,11 +1123,13 @@
expect(focusScopeNode.onKey, equals(handleCallback));
expect(focusScopeNode.onKeyEvent, equals(handleEventCallback));
expect(focusScopeNode.descendantsAreFocusable, isTrue);
+ expect(focusScopeNode.descendantsAreTraversable, isTrue);
expect(focusScopeNode.skipTraversal, isFalse);
expect(focusScopeNode.canRequestFocus, isTrue);
expect(focusScopeWidget.onKey, equals(focusScopeNode.onKey));
expect(focusScopeWidget.onKeyEvent, equals(focusScopeNode.onKeyEvent));
expect(focusScopeWidget.descendantsAreFocusable, equals(focusScopeNode.descendantsAreFocusable));
+ expect(focusScopeWidget.descendantsAreTraversable, equals(focusScopeNode.descendantsAreTraversable));
expect(focusScopeWidget.skipTraversal, equals(focusScopeNode.skipTraversal));
expect(focusScopeWidget.canRequestFocus, equals(focusScopeNode.canRequestFocus));
@@ -1639,12 +1645,47 @@
expect(containerNode.hasFocus, isFalse);
expect(unfocusableNode.hasFocus, isFalse);
});
+
+ testWidgets('descendantsAreTraversable works as expected.', (WidgetTester tester) async {
+ final FocusScopeNode scopeNode = FocusScopeNode(debugLabel: 'scope');
+ final FocusNode node1 = FocusNode(debugLabel: 'node 1');
+ final FocusNode node2 = FocusNode(debugLabel: 'node 2');
+ final FocusNode node3 = FocusNode(debugLabel: 'node 3');
+
+ await tester.pumpWidget(
+ FocusScope(
+ node: scopeNode,
+ child: Column(
+ children: <Widget>[
+ Focus(
+ focusNode: node1,
+ child: Container(),
+ ),
+ Focus(
+ focusNode: node2,
+ descendantsAreTraversable: false,
+ child: Focus(
+ focusNode: node3,
+ child: Container(),
+ )
+ ),
+ ],
+ ),
+ ),
+ );
+ await tester.pump();
+
+ expect(scopeNode.traversalDescendants, equals(<FocusNode>[node1, node2]));
+ expect(node2.traversalDescendants, equals(<FocusNode>[]));
+ });
+
testWidgets("Focus doesn't introduce a Semantics node when includeSemantics is false", (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(Focus(includeSemantics: false, child: Container()));
final TestSemantics expectedSemantics = TestSemantics.root();
expect(semantics, hasSemantics(expectedSemantics));
});
+
testWidgets('Focus updates the onKey handler when the widget updates', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
final FocusNode focusNode = FocusNode();
@@ -1695,6 +1736,7 @@
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
expect(keyEventHandled, isTrue);
});
+
testWidgets('Focus updates the onKeyEvent handler when the widget updates', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
final FocusNode focusNode = FocusNode();
@@ -1745,6 +1787,7 @@
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
expect(keyEventHandled, isTrue);
});
+
testWidgets("Focus doesn't update the focusNode attributes when the widget updates if withExternalFocusNode is used", (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
final FocusNode focusNode = FocusNode();
@@ -1766,6 +1809,7 @@
focusNode.onKey = ignoreCallback;
focusNode.onKeyEvent = ignoreEventCallback;
focusNode.descendantsAreFocusable = false;
+ focusNode.descendantsAreTraversable = false;
focusNode.skipTraversal = false;
focusNode.canRequestFocus = true;
Focus focusWidget = Focus.withExternalFocusNode(
@@ -1776,11 +1820,13 @@
expect(focusNode.onKey, equals(ignoreCallback));
expect(focusNode.onKeyEvent, equals(ignoreEventCallback));
expect(focusNode.descendantsAreFocusable, isFalse);
+ expect(focusNode.descendantsAreTraversable, isFalse);
expect(focusNode.skipTraversal, isFalse);
expect(focusNode.canRequestFocus, isTrue);
expect(focusWidget.onKey, equals(focusNode.onKey));
expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent));
expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable));
+ expect(focusWidget.descendantsAreTraversable, equals(focusNode.descendantsAreTraversable));
expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal));
expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus));
@@ -1792,6 +1838,7 @@
focusNode.onKey = handleCallback;
focusNode.onKeyEvent = handleEventCallback;
focusNode.descendantsAreFocusable = true;
+ focusNode.descendantsAreTraversable = true;
focusWidget = Focus.withExternalFocusNode(
focusNode: focusNode,
child: Container(key: key1),
@@ -1800,18 +1847,29 @@
expect(focusNode.onKey, equals(handleCallback));
expect(focusNode.onKeyEvent, equals(handleEventCallback));
expect(focusNode.descendantsAreFocusable, isTrue);
+ expect(focusNode.descendantsAreTraversable, isTrue);
expect(focusNode.skipTraversal, isFalse);
expect(focusNode.canRequestFocus, isTrue);
expect(focusWidget.onKey, equals(focusNode.onKey));
expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent));
expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable));
+ expect(focusWidget.descendantsAreTraversable, equals(focusNode.descendantsAreTraversable));
expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal));
expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus));
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
expect(keyEventHandled, isTrue);
});
+
+ testWidgets('Focus passes changes in attribute values to its focus node', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ Focus(
+ child: Container(),
+ ),
+ );
+ });
});
+
group('ExcludeFocus', () {
testWidgets("Descendants of ExcludeFocus aren't focusable.", (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
@@ -1919,6 +1977,7 @@
expect(parentFocusNode.hasFocus, isFalse);
expect(parentFocusNode.enclosingScope!.hasPrimaryFocus, isTrue);
});
+
testWidgets("ExcludeFocus doesn't introduce a Semantics node", (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(ExcludeFocus(child: Container()));
diff --git a/packages/flutter/test/widgets/focus_traversal_test.dart b/packages/flutter/test/widgets/focus_traversal_test.dart
index af77ebd..ff4c6e8 100644
--- a/packages/flutter/test/widgets/focus_traversal_test.dart
+++ b/packages/flutter/test/widgets/focus_traversal_test.dart
@@ -2054,6 +2054,7 @@
final TestSemantics expectedSemantics = TestSemantics.root();
expect(semantics, hasSemantics(expectedSemantics));
});
+
testWidgets("Descendants of FocusTraversalGroup aren't focusable if descendantsAreFocusable is false.", (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2');
@@ -2092,6 +2093,78 @@
expect(containerNode.hasFocus, isFalse);
expect(unfocusableNode.hasFocus, isFalse);
});
+
+ testWidgets("Descendants of FocusTraversalGroup aren't traversable if descendantsAreTraversable is false.", (WidgetTester tester) async {
+ final FocusNode node1 = FocusNode();
+ final FocusNode node2 = FocusNode();
+
+ await tester.pumpWidget(
+ FocusTraversalGroup(
+ descendantsAreTraversable: false,
+ child: Column(
+ children: <Widget>[
+ Focus(
+ focusNode: node1,
+ child: Container(),
+ ),
+ Focus(
+ focusNode: node2,
+ child: Container(),
+ ),
+ ],
+ ),
+ ),
+ );
+
+ node1.requestFocus();
+ await tester.pump();
+
+ expect(node1.hasPrimaryFocus, isTrue);
+ expect(node2.hasPrimaryFocus, isFalse);
+
+ expect(primaryFocus!.nextFocus(), isFalse);
+ await tester.pump();
+
+ expect(node1.hasPrimaryFocus, isTrue);
+ expect(node2.hasPrimaryFocus, isFalse);
+ });
+
+ testWidgets("FocusTraversalGroup with skipTraversal for all descendents set to true doesn't cause an exception.", (WidgetTester tester) async {
+ final FocusNode node1 = FocusNode();
+ final FocusNode node2 = FocusNode();
+
+ await tester.pumpWidget(
+ FocusTraversalGroup(
+ child: Column(
+ children: <Widget>[
+ Focus(
+ skipTraversal: true,
+ focusNode: node1,
+ child: Container(),
+ ),
+ Focus(
+ skipTraversal: true,
+ focusNode: node2,
+ child: Container(),
+ ),
+ ],
+ ),
+ ),
+ );
+
+ node1.requestFocus();
+ await tester.pump();
+
+ expect(node1.hasPrimaryFocus, isTrue);
+ expect(node2.hasPrimaryFocus, isFalse);
+
+ expect(primaryFocus!.nextFocus(), isFalse);
+ await tester.pump();
+
+ expect(node1.hasPrimaryFocus, isTrue);
+ expect(node2.hasPrimaryFocus, isFalse);
+ });
+
testWidgets("Nested FocusTraversalGroup with unfocusable children doesn't assert.", (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2');
@@ -2140,6 +2213,7 @@
expect(containerNode.hasFocus, isFalse);
expect(unfocusableNode.hasFocus, isFalse);
});
+
testWidgets("Empty FocusTraversalGroup doesn't cause an exception.", (WidgetTester tester) async {
final GlobalKey key = GlobalKey(debugLabel: 'Test Key');
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
@@ -2169,6 +2243,7 @@
expect(primaryFocus, equals(focusNode));
});
});
+
group(RawKeyboardListener, () {
testWidgets('Raw keyboard listener introduces a Semantics node by default', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
@@ -2195,6 +2270,7 @@
ignoreTransform: true,
));
});
+
testWidgets("Raw keyboard listener doesn't introduce a Semantics node when specified", (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final FocusNode focusNode = FocusNode();
@@ -2209,6 +2285,63 @@
expect(semantics, hasSemantics(expectedSemantics));
});
});
+
+ group(ExcludeFocusTraversal, () {
+ testWidgets("Descendants aren't traversable", (WidgetTester tester) async {
+ final FocusNode node1 = FocusNode(debugLabel: 'node 1');
+ final FocusNode node2 = FocusNode(debugLabel: 'node 2');
+ final FocusNode node3 = FocusNode(debugLabel: 'node 3');
+ final FocusNode node4 = FocusNode(debugLabel: 'node 4');
+
+ await tester.pumpWidget(
+ FocusTraversalGroup(
+ child: Column(
+ children: <Widget>[
+ Focus(
+ autofocus: true,
+ focusNode: node1,
+ child: Container(),
+ ),
+ ExcludeFocusTraversal(
+ child: Focus(
+ focusNode: node2,
+ child: Focus(
+ focusNode: node3,
+ child: Container(),
+ ),
+ ),
+ ),
+ Focus(
+ focusNode: node4,
+ child: Container(),
+ ),
+ ],
+ ),
+ ),
+ );
+ await tester.pump();
+
+ expect(node1.hasPrimaryFocus, isTrue);
+ expect(node2.hasPrimaryFocus, isFalse);
+ expect(node3.hasPrimaryFocus, isFalse);
+ expect(node4.hasPrimaryFocus, isFalse);
+
+ node1.nextFocus();
+ await tester.pump();
+
+ expect(node1.hasPrimaryFocus, isFalse);
+ expect(node2.hasPrimaryFocus, isFalse);
+ expect(node3.hasPrimaryFocus, isFalse);
+ expect(node4.hasPrimaryFocus, isTrue);
+ });
+
+ testWidgets("Doesn't introduce a Semantics node", (WidgetTester tester) async {
+ final SemanticsTester semantics = SemanticsTester(tester);
+ await tester.pumpWidget(ExcludeFocusTraversal(child: Container()));
+ final TestSemantics expectedSemantics = TestSemantics.root();
+ expect(semantics, hasSemantics(expectedSemantics));
+ });
+ });
}
class TestRoute extends PageRouteBuilder<void> {