Revert "Add OrderedFocusTraversalPolicy and FocusTraversalGrou… (#50660)

This reverts commit 8ef5e2f046ed18ca32827e1a715ca5e405c646ac because it breaks some semantics tests.
This commit is contained in:
Greg Spencer 2020-02-12 13:37:36 -08:00 committed by GitHub
parent 7f819c935b
commit c132c0faa9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 331 additions and 1661 deletions

View File

@ -435,7 +435,7 @@ class _FocusDemoState extends State<FocusDemo> {
kUndoActionKey: () => kUndoAction, kUndoActionKey: () => kUndoAction,
kRedoActionKey: () => kRedoAction, kRedoActionKey: () => kRedoAction,
}, },
child: FocusTraversalGroup( child: DefaultFocusTraversal(
policy: ReadingOrderTraversalPolicy(), policy: ReadingOrderTraversalPolicy(),
child: Shortcuts( child: Shortcuts(
shortcuts: <LogicalKeySet, Intent>{ shortcuts: <LogicalKeySet, Intent>{

View File

@ -142,7 +142,7 @@ class _FocusDemoState extends State<FocusDemo> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme; final TextTheme textTheme = Theme.of(context).textTheme;
return FocusTraversalGroup( return DefaultFocusTraversal(
policy: ReadingOrderTraversalPolicy(), policy: ReadingOrderTraversalPolicy(),
child: FocusScope( child: FocusScope(
debugLabel: 'Scope', debugLabel: 'Scope',

View File

@ -1395,7 +1395,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
debugLabel: '<Default WidgetsApp Shortcuts>', debugLabel: '<Default WidgetsApp Shortcuts>',
child: Actions( child: Actions(
actions: widget.actions ?? WidgetsApp.defaultActions, actions: widget.actions ?? WidgetsApp.defaultActions,
child: FocusTraversalGroup( child: DefaultFocusTraversal(
policy: ReadingOrderTraversalPolicy(), policy: ReadingOrderTraversalPolicy(),
child: _MediaQueryFromWindow( child: _MediaQueryFromWindow(
child: Localizations( child: Localizations(

View File

@ -230,12 +230,12 @@ class FocusAttachment {
/// particular direction, is determined by the [FocusTraversalPolicy] in force. /// particular direction, is determined by the [FocusTraversalPolicy] in force.
/// ///
/// The ambient policy is determined by looking up the widget hierarchy for a /// The ambient policy is determined by looking up the widget hierarchy for a
/// [FocusTraversalGroup] widget, and obtaining the focus traversal policy /// [DefaultFocusTraversal] widget, and obtaining the focus traversal policy
/// from it. Different focus nodes can inherit difference policies, so part of /// from it. Different focus nodes can inherit difference policies, so part of
/// the app can go in widget order, and part can go in reading order, depending /// the app can go in widget order, and part can go in reading order, depending
/// upon the use case. /// upon the use case.
/// ///
/// Predefined policies include [WidgetOrderTraversalPolicy], /// Predefined policies include [WidgetOrderFocusTraversalPolicy],
/// [ReadingOrderTraversalPolicy], and [DirectionalFocusTraversalPolicyMixin], /// [ReadingOrderTraversalPolicy], and [DirectionalFocusTraversalPolicyMixin],
/// but custom policies can be built based upon these policies. /// but custom policies can be built based upon these policies.
/// ///
@ -361,8 +361,8 @@ class FocusAttachment {
/// events to focused nodes. /// events to focused nodes.
/// * [FocusTraversalPolicy], a class used to determine how to move the focus /// * [FocusTraversalPolicy], a class used to determine how to move the focus
/// to other nodes. /// to other nodes.
/// * [FocusTraversalGroup], a widget used to group together and configure the /// * [DefaultFocusTraversal], a widget used to configure the default focus
/// focus traversal policy for a widget subtree. /// traversal policy for a widget subtree.
class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// Creates a focus node. /// Creates a focus node.
/// ///
@ -426,8 +426,8 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// ///
/// See also: /// See also:
/// ///
/// * [FocusTraversalGroup], a widget used to group together and configure the /// * [DefaultFocusTraversal], a widget that sets the traversal policy for
/// focus traversal policy for a widget subtree. /// its descendants.
/// * [FocusTraversalPolicy], a class that can be extended to describe a /// * [FocusTraversalPolicy], a class that can be extended to describe a
/// traversal policy. /// traversal policy.
bool get canRequestFocus { bool get canRequestFocus {
@ -518,8 +518,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
return _descendants; return _descendants;
} }
/// Returns all descendants which do not have the [skipTraversal] and do have /// Returns all descendants which do not have the [skipTraversal] flag set.
/// the [canRequestFocus] flag set.
Iterable<FocusNode> get traversalDescendants => descendants.where((FocusNode node) => !node.skipTraversal && node.canRequestFocus); Iterable<FocusNode> get traversalDescendants => descendants.where((FocusNode node) => !node.skipTraversal && node.canRequestFocus);
/// An [Iterable] over the ancestors of this node. /// An [Iterable] over the ancestors of this node.
@ -777,7 +776,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
_manager?.primaryFocus?._setAsFocusedChild(); _manager?.primaryFocus?._setAsFocusedChild();
} }
if (oldScope != null && child.context != null && child.enclosingScope != oldScope) { if (oldScope != null && child.context != null && child.enclosingScope != oldScope) {
FocusTraversalGroup.of(child.context, nullOk: true)?.changedScope(node: child, oldScope: oldScope); DefaultFocusTraversal.of(child.context, nullOk: true)?.changedScope(node: child, oldScope: oldScope);
} }
if (child._requestFocusWhenReparented) { if (child._requestFocusWhenReparented) {
child._doRequestFocus(); child._doRequestFocus();
@ -916,19 +915,19 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// [FocusTraversalPolicy.next] method. /// [FocusTraversalPolicy.next] method.
/// ///
/// Returns true if it successfully found a node and requested focus. /// Returns true if it successfully found a node and requested focus.
bool nextFocus() => FocusTraversalGroup.of(context).next(this); bool nextFocus() => DefaultFocusTraversal.of(context).next(this);
/// Request to move the focus to the previous focus node, by calling the /// Request to move the focus to the previous focus node, by calling the
/// [FocusTraversalPolicy.previous] method. /// [FocusTraversalPolicy.previous] method.
/// ///
/// Returns true if it successfully found a node and requested focus. /// Returns true if it successfully found a node and requested focus.
bool previousFocus() => FocusTraversalGroup.of(context).previous(this); bool previousFocus() => DefaultFocusTraversal.of(context).previous(this);
/// Request to move the focus to the nearest focus node in the given /// Request to move the focus to the nearest focus node in the given
/// direction, by calling the [FocusTraversalPolicy.inDirection] method. /// direction, by calling the [FocusTraversalPolicy.inDirection] method.
/// ///
/// Returns true if it successfully found a node and requested focus. /// Returns true if it successfully found a node and requested focus.
bool focusInDirection(TraversalDirection direction) => FocusTraversalGroup.of(context).inDirection(this, direction); bool focusInDirection(TraversalDirection direction) => DefaultFocusTraversal.of(context).inDirection(this, direction);
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {

View File

@ -462,10 +462,8 @@ class _FocusState extends State<Focus> {
return _FocusMarker( return _FocusMarker(
node: focusNode, node: focusNode,
child: Semantics( child: Semantics(
// If these values are false, then just don't set them, so they don't focusable: _canRequestFocus,
// eclipse values set by children. focused: _hasPrimaryFocus,
focusable: _canRequestFocus ? true : null,
focused: _hasPrimaryFocus ? true : null,
child: widget.child, child: widget.child,
), ),
); );

File diff suppressed because it is too large Load Diff

View File

@ -2191,8 +2191,6 @@ abstract class BuildContext {
/// Obtains the element corresponding to the nearest widget of the given type [T], /// Obtains the element corresponding to the nearest widget of the given type [T],
/// which must be the type of a concrete [InheritedWidget] subclass. /// which must be the type of a concrete [InheritedWidget] subclass.
/// ///
/// Returns null if no such element is found.
///
/// Calling this method is O(1) with a small constant factor. /// Calling this method is O(1) with a small constant factor.
/// ///
/// This method does not establish a relationship with the target in the way /// This method does not establish a relationship with the target in the way

View File

@ -68,7 +68,7 @@ void main() {
), ),
)); ));
expect(tester.getSemantics(find.byType(Focus)), matchesSemantics( expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
hasCheckedState: true, hasCheckedState: true,
hasEnabledState: true, hasEnabledState: true,
isEnabled: true, isEnabled: true,
@ -83,7 +83,7 @@ void main() {
), ),
)); ));
expect(tester.getSemantics(find.byType(Focus)), matchesSemantics( expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
hasCheckedState: true, hasCheckedState: true,
hasEnabledState: true, hasEnabledState: true,
isChecked: true, isChecked: true,
@ -99,7 +99,7 @@ void main() {
), ),
)); ));
expect(tester.getSemantics(find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_CheckboxRenderObjectWidget')), matchesSemantics( expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
hasCheckedState: true, hasCheckedState: true,
hasEnabledState: true, hasEnabledState: true,
)); ));
@ -111,7 +111,7 @@ void main() {
), ),
)); ));
expect(tester.getSemantics(find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_CheckboxRenderObjectWidget')), matchesSemantics( expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
hasCheckedState: true, hasCheckedState: true,
hasEnabledState: true, hasEnabledState: true,
isChecked: true, isChecked: true,

View File

@ -116,7 +116,7 @@ void main() {
' The ancestors of this widget were:\n' ' The ancestors of this widget were:\n'
' Semantics\n' ' Semantics\n'
' Builder\n' ' Builder\n'
' RepaintBoundary-[GlobalKey#00000]\n' ' RepaintBoundary-[GlobalKey#2d465]\n'
' IgnorePointer\n' ' IgnorePointer\n'
' AnimatedBuilder\n' ' AnimatedBuilder\n'
' FadeTransition\n' ' FadeTransition\n'
@ -131,19 +131,19 @@ void main() {
' PageStorage\n' ' PageStorage\n'
' Offstage\n' ' Offstage\n'
' _ModalScopeStatus\n' ' _ModalScopeStatus\n'
' _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>>#00000]\n' ' _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>>#969b7]\n'
' _EffectiveTickerMode\n' ' _EffectiveTickerMode\n'
' TickerMode\n' ' TickerMode\n'
' _OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState>#00000]\n' ' _OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState>#545d0]\n'
' _Theatre\n' ' _Theatre\n'
' Overlay-[LabeledGlobalKey<OverlayState>#00000]\n' ' Overlay-[LabeledGlobalKey<OverlayState>#31a52]\n'
' _FocusMarker\n' ' _FocusMarker\n'
' Semantics\n' ' Semantics\n'
' FocusScope\n' ' FocusScope\n'
' AbsorbPointer\n' ' AbsorbPointer\n'
' _PointerListener\n' ' _PointerListener\n'
' Listener\n' ' Listener\n'
' Navigator-[GlobalObjectKey<NavigatorState> _WidgetsAppState#00000]\n' ' Navigator-[GlobalObjectKey<NavigatorState> _WidgetsAppState#10579]\n'
' IconTheme\n' ' IconTheme\n'
' IconTheme\n' ' IconTheme\n'
' _InheritedCupertinoTheme\n' ' _InheritedCupertinoTheme\n'
@ -158,23 +158,19 @@ void main() {
' CheckedModeBanner\n' ' CheckedModeBanner\n'
' Title\n' ' Title\n'
' Directionality\n' ' Directionality\n'
' _LocalizationsScope-[GlobalKey#00000]\n' ' _LocalizationsScope-[GlobalKey#a51e3]\n'
' Semantics\n' ' Semantics\n'
' Localizations\n' ' Localizations\n'
' MediaQuery\n' ' MediaQuery\n'
' _MediaQueryFromWindow\n' ' _MediaQueryFromWindow\n'
' Semantics\n' ' DefaultFocusTraversal\n'
' _FocusMarker\n'
' Focus\n'
' _FocusTraversalGroupMarker\n'
' FocusTraversalGroup\n'
' Actions\n' ' Actions\n'
' _ShortcutsMarker\n' ' _ShortcutsMarker\n'
' Semantics\n' ' Semantics\n'
' _FocusMarker\n' ' _FocusMarker\n'
' Focus\n' ' Focus\n'
' Shortcuts\n' ' Shortcuts\n'
' WidgetsApp-[GlobalObjectKey _MaterialAppState#00000]\n' ' WidgetsApp-[GlobalObjectKey _MaterialAppState#38e79]\n'
' ScrollConfiguration\n' ' ScrollConfiguration\n'
' MaterialApp\n' ' MaterialApp\n'
' [root]\n' ' [root]\n'

View File

@ -570,7 +570,7 @@ void main() {
} }
Widget wrap({ Widget child }) { Widget wrap({ Widget child }) {
return FocusTraversalGroup( return DefaultFocusTraversal(
policy: ReadingOrderTraversalPolicy(), policy: ReadingOrderTraversalPolicy(),
child: Directionality( child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,

View File

@ -598,7 +598,7 @@ void main() {
// This checks both FocusScopes that have their own nodes, as well as those // This checks both FocusScopes that have their own nodes, as well as those
// that use external nodes. // that use external nodes.
await tester.pumpWidget( await tester.pumpWidget(
FocusTraversalGroup( DefaultFocusTraversal(
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
FocusScope( FocusScope(
@ -661,7 +661,7 @@ void main() {
final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2'); final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2');
await tester.pumpWidget( await tester.pumpWidget(
FocusTraversalGroup( DefaultFocusTraversal(
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
FocusScope( FocusScope(
@ -711,7 +711,7 @@ void main() {
expect(find.text('b'), findsOneWidget); expect(find.text('b'), findsOneWidget);
await tester.pumpWidget( await tester.pumpWidget(
FocusTraversalGroup( DefaultFocusTraversal(
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
FocusScope( FocusScope(
@ -746,7 +746,7 @@ void main() {
final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2'); final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2');
await tester.pumpWidget( await tester.pumpWidget(
FocusTraversalGroup( DefaultFocusTraversal(
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
FocusScope( FocusScope(
@ -794,7 +794,7 @@ void main() {
expect(find.text('b'), findsOneWidget); expect(find.text('b'), findsOneWidget);
await tester.pumpWidget( await tester.pumpWidget(
FocusTraversalGroup( DefaultFocusTraversal(
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
FocusScope( FocusScope(

View File

@ -12,15 +12,15 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
void main() { void main() {
group(WidgetOrderTraversalPolicy, () { group(WidgetOrderFocusTraversalPolicy, () {
testWidgets('Find the initial focus if there is none yet.', (WidgetTester tester) async { testWidgets('Find the initial focus if there is none yet.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2'); final GlobalKey key2 = GlobalKey(debugLabel: '2');
final GlobalKey key3 = GlobalKey(debugLabel: '3'); final GlobalKey key3 = GlobalKey(debugLabel: '3');
final GlobalKey key4 = GlobalKey(debugLabel: '4'); final GlobalKey key4 = GlobalKey(debugLabel: '4');
final GlobalKey key5 = GlobalKey(debugLabel: '5'); final GlobalKey key5 = GlobalKey(debugLabel: '5');
await tester.pumpWidget(FocusTraversalGroup( await tester.pumpWidget(DefaultFocusTraversal(
policy: WidgetOrderTraversalPolicy(), policy: WidgetOrderFocusTraversalPolicy(),
child: FocusScope( child: FocusScope(
key: key1, key: key1,
child: Column( child: Column(
@ -64,8 +64,8 @@ void main() {
bool focus3; bool focus3;
bool focus5; bool focus5;
await tester.pumpWidget( await tester.pumpWidget(
FocusTraversalGroup( DefaultFocusTraversal(
policy: WidgetOrderTraversalPolicy(), policy: WidgetOrderFocusTraversalPolicy(),
child: FocusScope( child: FocusScope(
debugLabel: 'key1', debugLabel: 'key1',
key: key1, key: key1,
@ -177,8 +177,8 @@ void main() {
final GlobalKey key5 = GlobalKey(debugLabel: '5'); final GlobalKey key5 = GlobalKey(debugLabel: '5');
final GlobalKey key6 = GlobalKey(debugLabel: '6'); final GlobalKey key6 = GlobalKey(debugLabel: '6');
await tester.pumpWidget( await tester.pumpWidget(
FocusTraversalGroup( DefaultFocusTraversal(
policy: WidgetOrderTraversalPolicy(), policy: WidgetOrderFocusTraversalPolicy(),
child: FocusScope( child: FocusScope(
key: key1, key: key1,
child: Column( child: Column(
@ -250,8 +250,8 @@ void main() {
final FocusNode testNode2 = FocusNode(debugLabel: 'Second Focus Node'); final FocusNode testNode2 = FocusNode(debugLabel: 'Second Focus Node');
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: FocusTraversalGroup( home: DefaultFocusTraversal(
policy: WidgetOrderTraversalPolicy(), policy: WidgetOrderFocusTraversalPolicy(),
child: Center( child: Center(
child: Builder(builder: (BuildContext context) { child: Builder(builder: (BuildContext context) {
return MaterialButton( return MaterialButton(
@ -317,7 +317,7 @@ void main() {
final GlobalKey key3 = GlobalKey(debugLabel: '3'); final GlobalKey key3 = GlobalKey(debugLabel: '3');
final GlobalKey key4 = GlobalKey(debugLabel: '4'); final GlobalKey key4 = GlobalKey(debugLabel: '4');
final GlobalKey key5 = GlobalKey(debugLabel: '5'); final GlobalKey key5 = GlobalKey(debugLabel: '5');
await tester.pumpWidget(FocusTraversalGroup( await tester.pumpWidget(DefaultFocusTraversal(
policy: ReadingOrderTraversalPolicy(), policy: ReadingOrderTraversalPolicy(),
child: FocusScope( child: FocusScope(
key: key1, key: key1,
@ -364,7 +364,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: FocusTraversalGroup( child: DefaultFocusTraversal(
policy: ReadingOrderTraversalPolicy(), policy: ReadingOrderTraversalPolicy(),
child: FocusScope( child: FocusScope(
debugLabel: 'key1', debugLabel: 'key1',
@ -473,7 +473,7 @@ void main() {
final GlobalKey key5 = GlobalKey(debugLabel: '5'); final GlobalKey key5 = GlobalKey(debugLabel: '5');
final GlobalKey key6 = GlobalKey(debugLabel: '6'); final GlobalKey key6 = GlobalKey(debugLabel: '6');
await tester.pumpWidget( await tester.pumpWidget(
FocusTraversalGroup( DefaultFocusTraversal(
policy: ReadingOrderTraversalPolicy(), policy: ReadingOrderTraversalPolicy(),
child: FocusScope( child: FocusScope(
key: key1, key: key1,
@ -538,579 +538,6 @@ void main() {
expect(secondFocusNode.hasFocus, isFalse); expect(secondFocusNode.hasFocus, isFalse);
expect(scope.hasFocus, isTrue); expect(scope.hasFocus, isTrue);
}); });
testWidgets('Focus order is correct in the presence of different directionalities.', (WidgetTester tester) async {
const int nodeCount = 10;
final FocusScopeNode scopeNode = FocusScopeNode();
final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index'));
Widget buildTest(TextDirection topDirection) {
return Directionality(
textDirection: topDirection,
child: FocusTraversalGroup(
policy: ReadingOrderTraversalPolicy(),
child: FocusScope(
node: scopeNode,
child: Column(
children: <Widget>[
Directionality(
textDirection: TextDirection.ltr,
child: Row(children: <Widget>[
Focus(
focusNode: nodes[0],
child: Container(width: 10, height: 10),
),
Focus(
focusNode: nodes[1],
child: Container(width: 10, height: 10),
),
Focus(
focusNode: nodes[2],
child: Container(width: 10, height: 10),
),
]),
),
Directionality(
textDirection: TextDirection.ltr,
child: Row(children: <Widget>[
Directionality(
textDirection: TextDirection.rtl,
child: Focus(
focusNode: nodes[3],
child: Container(width: 10, height: 10),
),
),
Directionality(
textDirection: TextDirection.rtl,
child: Focus(
focusNode: nodes[4],
child: Container(width: 10, height: 10),
),
),
Directionality(
textDirection: TextDirection.ltr,
child: Focus(
focusNode: nodes[5],
child: Container(width: 10, height: 10),
),
),
]),
),
Row(children: <Widget>[
Directionality(
textDirection: TextDirection.ltr,
child: Focus(
focusNode: nodes[6],
child: Container(width: 10, height: 10),
),
),
Directionality(
textDirection: TextDirection.rtl,
child: Focus(
focusNode: nodes[7],
child: Container(width: 10, height: 10),
),
),
Directionality(
textDirection: TextDirection.rtl,
child: Focus(
focusNode: nodes[8],
child: Container(width: 10, height: 10),
),
),
Directionality(
textDirection: TextDirection.ltr,
child: Focus(
focusNode: nodes[9],
child: Container(width: 10, height: 10),
),
),
]),
],
),
),
),
);
}
await tester.pumpWidget(buildTest(TextDirection.rtl));
// The last four *are* correct: the Row is sensitive to the directionality
// too, so it swaps the positions of 7 and 8.
final List<int> order = <int>[];
for (int i = 0; i < nodeCount; ++i) {
nodes.first.nextFocus();
await tester.pump();
order.add(nodes.indexOf(primaryFocus));
}
expect(order, orderedEquals(<int>[0, 1, 2, 4, 3, 5, 6, 7, 8, 9]));
await tester.pumpWidget(buildTest(TextDirection.ltr));
order.clear();
for (int i = 0; i < nodeCount; ++i) {
nodes.first.nextFocus();
await tester.pump();
order.add(nodes.indexOf(primaryFocus));
}
expect(order, orderedEquals(<int>[0, 1, 2, 4, 3, 5, 6, 8, 7, 9]));
});
testWidgets('Focus order is reading order regardless of widget order, even when overlapping.', (WidgetTester tester) async {
const int nodeCount = 10;
final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index'));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.rtl,
child: FocusTraversalGroup(
policy: ReadingOrderTraversalPolicy(),
child: Stack(
alignment: const Alignment(-1, -1),
children: List<Widget>.generate(nodeCount, (int index) {
// Boxes that all have the same upper left origin corner.
return Focus(
focusNode: nodes[index],
child: Container(width: 10.0 * (index + 1), height: 10.0 * (index + 1)),
);
}),
),
),
),
);
final List<int> order = <int>[];
for (int i = 0; i < nodeCount; ++i) {
nodes.first.nextFocus();
await tester.pump();
order.add(nodes.indexOf(primaryFocus));
}
expect(order, orderedEquals(<int>[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]));
// Concentric boxes.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.rtl,
child: FocusTraversalGroup(
policy: ReadingOrderTraversalPolicy(),
child: Stack(
alignment: const Alignment(0, 0),
children: List<Widget>.generate(nodeCount, (int index) {
return Focus(
focusNode: nodes[index],
child: Container(width: 10.0 * (index + 1), height: 10.0 * (index + 1)),
);
}),
),
),
),
);
order.clear();
for (int i = 0; i < nodeCount; ++i) {
nodes.first.nextFocus();
await tester.pump();
order.add(nodes.indexOf(primaryFocus));
}
expect(order, orderedEquals(<int>[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]));
// Stacked (vertically) and centered (horizontally, on each other)
// widgets, not overlapping.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.rtl,
child: FocusTraversalGroup(
policy: ReadingOrderTraversalPolicy(),
child: Stack(
alignment: const Alignment(0, 0),
children: List<Widget>.generate(nodeCount, (int index) {
return Positioned(
top: 5.0 * index * (index + 1),
left: 5.0 * (9 - index),
child: Focus(
focusNode: nodes[index],
child: Container(
decoration: BoxDecoration(border: Border.all()),
width: 10.0 * (index + 1),
height: 10.0 * (index + 1),
),
),
);
}),
),
),
),
);
order.clear();
for (int i = 0; i < nodeCount; ++i) {
nodes.first.nextFocus();
await tester.pump();
order.add(nodes.indexOf(primaryFocus));
}
expect(order, orderedEquals(<int>[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]));
});
});
group(OrderedTraversalPolicy, () {
testWidgets('Find the initial focus if there is none yet.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2');
await tester.pumpWidget(FocusTraversalGroup(
policy: OrderedTraversalPolicy(secondary: ReadingOrderTraversalPolicy()),
child: FocusScope(
child: Column(
children: <Widget>[
FocusTraversalOrder(
order: const NumericFocusOrder(2),
child: Focus(
child: Container(key: key1, width: 100, height: 100),
),
),
FocusTraversalOrder(
order: const NumericFocusOrder(1),
child: Focus(
child: Container(key: key2, width: 100, height: 100),
),
),
],
),
),
));
final Element firstChild = tester.element(find.byKey(key1));
final Element secondChild = tester.element(find.byKey(key2));
final FocusNode firstFocusNode = Focus.of(firstChild);
final FocusNode secondFocusNode = Focus.of(secondChild);
final FocusNode scope = Focus.of(firstChild).enclosingScope;
secondFocusNode.nextFocus();
await tester.pump();
expect(firstFocusNode.hasFocus, isFalse);
expect(secondFocusNode.hasFocus, isTrue);
expect(scope.hasFocus, isTrue);
});
testWidgets('Fall back to the secondary sort if no FocusTraversalOrder exists.', (WidgetTester tester) async {
const int nodeCount = 10;
final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index'));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.rtl,
child: FocusTraversalGroup(
policy: OrderedTraversalPolicy(secondary: WidgetOrderTraversalPolicy()),
child: FocusScope(
child: Row(
children: List<Widget>.generate(
nodeCount,
(int index) => Focus(
focusNode: nodes[index],
child: Container(width: 10, height: 10),
),
),
),
),
),
),
);
// Because it should be using widget order, this shouldn't be affected by
// the directionality.
for (int i = 0; i < nodeCount; ++i) {
nodes.first.nextFocus();
await tester.pump();
expect(nodes[i].hasPrimaryFocus, isTrue, reason: "node $i doesn't have focus, but should");
}
// Now check backwards.
for (int i = nodeCount - 1; i > 0; --i) {
nodes.first.previousFocus();
await tester.pump();
expect(nodes[i - 1].hasPrimaryFocus, isTrue, reason: "node ${i - 1} doesn't have focus, but should");
}
});
testWidgets('Move focus to next/previous node using numerical order.', (WidgetTester tester) async {
const int nodeCount = 10;
final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index'));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FocusTraversalGroup(
policy: OrderedTraversalPolicy(secondary: WidgetOrderTraversalPolicy()),
child: FocusScope(
child: Row(
children: List<Widget>.generate(
nodeCount,
(int index) => FocusTraversalOrder(
order: NumericFocusOrder(nodeCount - index.toDouble()),
child: Focus(
focusNode: nodes[index],
child: Container(width: 10, height: 10),
),
),
),
),
),
),
),
);
// The orders are assigned to be backwards from normal, so should go backwards.
for (int i = nodeCount - 1; i >= 0; --i) {
nodes.first.nextFocus();
await tester.pump();
expect(nodes[i].hasPrimaryFocus, isTrue, reason: "node $i doesn't have focus, but should");
}
// Now check backwards.
for (int i = 1; i < nodeCount; ++i) {
nodes.first.previousFocus();
await tester.pump();
expect(nodes[i].hasPrimaryFocus, isTrue, reason: "node $i doesn't have focus, but should");
}
});
testWidgets('Move focus to next/previous node using lexical order.', (WidgetTester tester) async {
const int nodeCount = 10;
/// Generate ['J' ... 'A'];
final List<String> keys = List<String>.generate(nodeCount, (int index) => String.fromCharCode('A'.codeUnits[0] + nodeCount - index - 1));
final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node ${keys[index]}'));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FocusTraversalGroup(
policy: OrderedTraversalPolicy(secondary: WidgetOrderTraversalPolicy()),
child: FocusScope(
child: Row(
children: List<Widget>.generate(
nodeCount,
(int index) => FocusTraversalOrder(
order: LexicalFocusOrder(keys[index]),
child: Focus(
focusNode: nodes[index],
child: Container(width: 10, height: 10),
),
),
),
),
),
),
),
);
// The orders are assigned to be backwards from normal, so should go backwards.
for (int i = nodeCount - 1; i >= 0; --i) {
nodes.first.nextFocus();
await tester.pump();
expect(nodes[i].hasPrimaryFocus, isTrue, reason: "node $i doesn't have focus, but should");
}
// Now check backwards.
for (int i = 1; i < nodeCount; ++i) {
nodes.first.previousFocus();
await tester.pump();
expect(nodes[i].hasPrimaryFocus, isTrue, reason: "node $i doesn't have focus, but should");
}
});
testWidgets('Focus order is correct in the presence of FocusTraversalPolicyGroups.', (WidgetTester tester) async {
const int nodeCount = 10;
final FocusScopeNode scopeNode = FocusScopeNode();
final List<FocusNode> nodes = List<FocusNode>.generate(nodeCount, (int index) => FocusNode(debugLabel: 'Node $index'));
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: FocusTraversalGroup(
policy: WidgetOrderTraversalPolicy(),
child: FocusScope(
node: scopeNode,
child: FocusTraversalGroup(
policy: OrderedTraversalPolicy(secondary: WidgetOrderTraversalPolicy()),
child: Row(
children: <Widget>[
FocusTraversalOrder(
order: const NumericFocusOrder(0),
child: FocusTraversalGroup(
policy: WidgetOrderTraversalPolicy(),
child: Row(children: <Widget>[
FocusTraversalOrder(
order: const NumericFocusOrder(9),
child: Focus(
focusNode: nodes[9],
child: Container(width: 10, height: 10),
),
),
FocusTraversalOrder(
order: const NumericFocusOrder(8),
child: Focus(
focusNode: nodes[8],
child: Container(width: 10, height: 10),
),
),
FocusTraversalOrder(
order: const NumericFocusOrder(7),
child: Focus(
focusNode: nodes[7],
child: Container(width: 10, height: 10),
),
),
]),
),
),
FocusTraversalOrder(
order: const NumericFocusOrder(1),
child: FocusTraversalGroup(
policy: OrderedTraversalPolicy(secondary: WidgetOrderTraversalPolicy()),
child: Row(children: <Widget>[
FocusTraversalOrder(
order: const NumericFocusOrder(4),
child: Focus(
focusNode: nodes[4],
child: Container(width: 10, height: 10),
),
),
FocusTraversalOrder(
order: const NumericFocusOrder(5),
child: Focus(
focusNode: nodes[5],
child: Container(width: 10, height: 10),
),
),
FocusTraversalOrder(
order: const NumericFocusOrder(6),
child: Focus(
focusNode: nodes[6],
child: Container(width: 10, height: 10),
),
),
]),
),
),
FocusTraversalOrder(
order: const NumericFocusOrder(2),
child: FocusTraversalGroup(
policy: OrderedTraversalPolicy(secondary: WidgetOrderTraversalPolicy()),
child: Row(children: <Widget>[
FocusTraversalOrder(
order: const LexicalFocusOrder('D'),
child: Focus(
focusNode: nodes[3],
child: Container(width: 10, height: 10),
),
),
FocusTraversalOrder(
order: const LexicalFocusOrder('C'),
child: Focus(
focusNode: nodes[2],
child: Container(width: 10, height: 10),
),
),
FocusTraversalOrder(
order: const LexicalFocusOrder('B'),
child: Focus(
focusNode: nodes[1],
child: Container(width: 10, height: 10),
),
),
FocusTraversalOrder(
order: const LexicalFocusOrder('A'),
child: Focus(
focusNode: nodes[0],
child: Container(width: 10, height: 10),
),
),
]),
),
),
],
),
),
),
),
),
);
final List<int> expectedOrder = <int>[9, 8, 7, 4, 5, 6, 0, 1, 2, 3];
final List<int> order = <int>[];
for (int i = 0; i < nodeCount; ++i) {
nodes.first.nextFocus();
await tester.pump();
order.add(nodes.indexOf(primaryFocus));
}
expect(order, orderedEquals(expectedOrder));
});
testWidgets('Find the initial focus when a route is pushed or popped.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2');
final FocusNode testNode1 = FocusNode(debugLabel: 'First Focus Node');
final FocusNode testNode2 = FocusNode(debugLabel: 'Second Focus Node');
await tester.pumpWidget(
MaterialApp(
home: FocusTraversalGroup(
policy: OrderedTraversalPolicy(secondary: WidgetOrderTraversalPolicy()),
child: Center(
child: Builder(builder: (BuildContext context) {
return FocusTraversalOrder(
order: const NumericFocusOrder(0),
child: MaterialButton(
key: key1,
focusNode: testNode1,
autofocus: true,
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return Center(
child: FocusTraversalOrder(
order: const NumericFocusOrder(0),
child: MaterialButton(
key: key2,
focusNode: testNode2,
autofocus: true,
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Go Back'),
),
),
);
},
),
);
},
child: const Text('Go Forward'),
),
);
}),
),
),
),
);
final Element firstChild = tester.element(find.text('Go Forward'));
final FocusNode firstFocusNode = Focus.of(firstChild);
final FocusNode scope = Focus.of(firstChild).enclosingScope;
await tester.pump();
expect(firstFocusNode.hasFocus, isTrue);
expect(scope.hasFocus, isTrue);
await tester.tap(find.text('Go Forward'));
await tester.pumpAndSettle();
final Element secondChild = tester.element(find.text('Go Back'));
final FocusNode secondFocusNode = Focus.of(secondChild);
expect(firstFocusNode.hasFocus, isFalse);
expect(secondFocusNode.hasFocus, isTrue);
await tester.tap(find.text('Go Back'));
await tester.pumpAndSettle();
expect(firstFocusNode.hasFocus, isTrue);
expect(scope.hasFocus, isTrue);
});
}); });
group(DirectionalFocusTraversalPolicyMixin, () { group(DirectionalFocusTraversalPolicyMixin, () {
@ -1126,8 +553,8 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: FocusTraversalGroup( child: DefaultFocusTraversal(
policy: WidgetOrderTraversalPolicy(), policy: WidgetOrderFocusTraversalPolicy(),
child: FocusScope( child: FocusScope(
debugLabel: 'Scope', debugLabel: 'Scope',
child: Column( child: Column(
@ -1279,8 +706,8 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: FocusTraversalGroup( child: DefaultFocusTraversal(
policy: WidgetOrderTraversalPolicy(), policy: WidgetOrderFocusTraversalPolicy(),
child: FocusScope( child: FocusScope(
debugLabel: 'Scope', debugLabel: 'Scope',
child: Column( child: Column(
@ -1400,8 +827,8 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: FocusTraversalGroup( child: DefaultFocusTraversal(
policy: WidgetOrderTraversalPolicy(), policy: WidgetOrderFocusTraversalPolicy(),
child: FocusScope( child: FocusScope(
debugLabel: 'scope', debugLabel: 'scope',
child: Column( child: Column(
@ -1444,7 +871,7 @@ void main() {
await tester.pump(); await tester.pump();
final FocusTraversalPolicy policy = FocusTraversalGroup.of(upperLeftKey.currentContext); final FocusTraversalPolicy policy = DefaultFocusTraversal.of(upperLeftKey.currentContext);
expect(policy.findFirstFocusInDirection(scope, TraversalDirection.up), equals(lowerLeftNode)); expect(policy.findFirstFocusInDirection(scope, TraversalDirection.up), equals(lowerLeftNode));
expect(policy.findFirstFocusInDirection(scope, TraversalDirection.down), equals(upperLeftNode)); expect(policy.findFirstFocusInDirection(scope, TraversalDirection.down), equals(upperLeftNode));
@ -1458,7 +885,7 @@ void main() {
final FocusNode focusBottom = FocusNode(debugLabel: 'bottom'); final FocusNode focusBottom = FocusNode(debugLabel: 'bottom');
final FocusTraversalPolicy policy = ReadingOrderTraversalPolicy(); final FocusTraversalPolicy policy = ReadingOrderTraversalPolicy();
await tester.pumpWidget(FocusTraversalGroup( await tester.pumpWidget(DefaultFocusTraversal(
policy: policy, policy: policy,
child: FocusScope( child: FocusScope(
debugLabel: 'Scope', debugLabel: 'Scope',
@ -1482,7 +909,7 @@ void main() {
expect(focusBottom.hasFocus, isTrue); expect(focusBottom.hasFocus, isTrue);
// Remove center focus node. // Remove center focus node.
await tester.pumpWidget(FocusTraversalGroup( await tester.pumpWidget(DefaultFocusTraversal(
policy: policy, policy: policy,
child: FocusScope( child: FocusScope(
debugLabel: 'Scope', debugLabel: 'Scope',