Unverified Commit 56a7c97f authored by Viren Khatri's avatar Viren Khatri Committed by GitHub

Adds ability to mark a subtree as not traversable (#94626)

parent 23e7449a
......@@ -1064,6 +1064,7 @@ class FocusableActionDetector extends StatefulWidget {
this.focusNode,
this.autofocus = false,
this.descendantsAreFocusable = true,
this.descendantsAreTraversable = true,
this.shortcuts,
this.actions,
this.onShowFocusHighlight,
......@@ -1095,6 +1096,9 @@ class FocusableActionDetector extends StatefulWidget {
/// {@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 @@ class _FocusableActionDetectorState extends State<FocusableActionDetector> {
focusNode: widget.focusNode,
autofocus: widget.autofocus,
descendantsAreFocusable: widget.descendantsAreFocusable,
descendantsAreTraversable: widget.descendantsAreTraversable,
canRequestFocus: _canRequestFocus,
onFocusChange: _handleFocusChange,
child: widget.child,
......
......@@ -409,12 +409,14 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
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 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// 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 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
///
/// 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 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
_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 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
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));
......
......@@ -124,6 +124,7 @@ class Focus extends StatefulWidget {
bool? canRequestFocus,
bool? skipTraversal,
bool? descendantsAreFocusable,
bool? descendantsAreTraversable,
this.includeSemantics = true,
String? debugLabel,
}) : _onKeyEvent = onKeyEvent,
......@@ -131,6 +132,7 @@ class Focus extends StatefulWidget {
_canRequestFocus = canRequestFocus,
_skipTraversal = skipTraversal,
_descendantsAreFocusable = descendantsAreFocusable,
_descendantsAreTraversable = descendantsAreTraversable,
_debugLabel = debugLabel,
assert(child != null),
assert(autofocus != null),
......@@ -279,10 +281,17 @@ class Focus extends StatefulWidget {
/// 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 @@ class Focus extends StatefulWidget {
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 @@ class Focus extends StatefulWidget {
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 @@ class _FocusWithExternalFocusNode extends Focus {
@override
bool get descendantsAreFocusable => focusNode!.descendantsAreFocusable;
@override
bool? get _descendantsAreTraversable => focusNode!.descendantsAreTraversable;
@override
String? get debugLabel => focusNode!.debugLabel;
}
......@@ -468,6 +504,7 @@ class _FocusState extends State<Focus> {
late bool _hadPrimaryFocus;
late bool _couldRequestFocus;
late bool _descendantsWereFocusable;
late bool _descendantsWereTraversable;
bool _didAutofocus = false;
FocusAttachment? _focusAttachment;
......@@ -485,6 +522,7 @@ class _FocusState extends State<Focus> {
_internalNode ??= _createNode();
}
focusNode.descendantsAreFocusable = widget.descendantsAreFocusable;
focusNode.descendantsAreTraversable = widget.descendantsAreTraversable;
if (widget.skipTraversal != null) {
focusNode.skipTraversal = widget.skipTraversal;
}
......@@ -493,6 +531,7 @@ class _FocusState extends State<Focus> {
}
_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 @@ class _FocusState extends State<Focus> {
debugLabel: widget.debugLabel,
canRequestFocus: widget.canRequestFocus,
descendantsAreFocusable: widget.descendantsAreFocusable,
descendantsAreTraversable: widget.descendantsAreTraversable,
skipTraversal: widget.skipTraversal,
);
}
......@@ -579,6 +619,7 @@ class _FocusState extends State<Focus> {
focusNode.canRequestFocus = widget._canRequestFocus!;
}
focusNode.descendantsAreFocusable = widget.descendantsAreFocusable;
focusNode.descendantsAreTraversable = widget.descendantsAreTraversable;
}
} else {
_focusAttachment!.detach();
......@@ -595,6 +636,7 @@ class _FocusState extends State<Focus> {
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 @@ class _FocusState extends State<Focus> {
_descendantsWereFocusable = descendantsAreFocusable;
});
}
if (_descendantsWereTraversable != descendantsAreTraversable) {
setState(() {
_descendantsWereTraversable = descendantsAreTraversable;
});
}
}
@override
......@@ -784,6 +831,8 @@ class _FocusScopeWithExternalFocusNode extends FocusScope {
@override
bool get descendantsAreFocusable => focusNode!.descendantsAreFocusable;
@override
bool get descendantsAreTraversable => focusNode!.descendantsAreTraversable;
@override
String? get debugLabel => focusNode!.debugLabel;
}
......
......@@ -388,6 +388,11 @@ abstract class FocusTraversalPolicy with Diagnosticable {
}
}
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 @@ class FocusTraversalGroup extends StatefulWidget {
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 @@ class FocusTraversalGroup extends StatefulWidget {
/// {@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 @@ class _FocusTraversalGroupState extends State<FocusTraversalGroup> {
skipTraversal: true,
includeSemantics: false,
descendantsAreFocusable: widget.descendantsAreFocusable,
descendantsAreTraversable: widget.descendantsAreTraversable,
child: widget.child,
),
);
......@@ -1737,3 +1748,62 @@ class DirectionalFocusAction extends Action<DirectionalFocusIntent> {
}
}
}
/// 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,
);
}
}
......@@ -962,6 +962,75 @@ void main() {
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', () {
......
......@@ -151,6 +151,39 @@ void main() {
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 @@ void main() {
expect(description, <String>[
'context: null',
'descendantsAreFocusable: true',
'descendantsAreTraversable: true',
'canRequestFocus: true',
'hasFocus: false',
'hasPrimaryFocus: false',
......@@ -1156,6 +1190,7 @@ void main() {
expect(description, <String>[
'context: null',
'descendantsAreFocusable: true',
'descendantsAreTraversable: true',
'canRequestFocus: true',
'hasFocus: false',
'hasPrimaryFocus: false',
......
......@@ -1085,6 +1085,7 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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()));
......
......@@ -2054,6 +2054,7 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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> {
......
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