| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:ui'; |
| |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import 'semantics_tester.dart'; |
| |
| /// Used to test removal of nodes while sorting. |
| class SkipAllButFirstAndLastPolicy extends FocusTraversalPolicy with DirectionalFocusTraversalPolicyMixin { |
| @override |
| Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode) { |
| return <FocusNode>[ |
| descendants.first, |
| if (currentNode != descendants.first && currentNode != descendants.last) currentNode, |
| descendants.last, |
| ]; |
| } |
| } |
| |
| void main() { |
| group(WidgetOrderTraversalPolicy, () { |
| 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'); |
| final GlobalKey key3 = GlobalKey(debugLabel: '3'); |
| final GlobalKey key4 = GlobalKey(debugLabel: '4'); |
| final GlobalKey key5 = GlobalKey(debugLabel: '5'); |
| await tester.pumpWidget(FocusTraversalGroup( |
| policy: WidgetOrderTraversalPolicy(), |
| child: FocusScope( |
| key: key1, |
| child: Column( |
| children: <Widget>[ |
| Focus( |
| key: key2, |
| child: SizedBox(key: key3, width: 100, height: 100), |
| ), |
| Focus( |
| key: key4, |
| child: SizedBox(key: key5, width: 100, height: 100), |
| ), |
| ], |
| ), |
| ), |
| )); |
| |
| final Element firstChild = tester.element(find.byKey(key3)); |
| final Element secondChild = tester.element(find.byKey(key5)); |
| 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, isTrue); |
| expect(secondFocusNode.hasFocus, isFalse); |
| expect(scope.hasFocus, isTrue); |
| }); |
| |
| testWidgets('Find the initial focus if there is none yet and traversing backwards.', (WidgetTester tester) async { |
| final GlobalKey key1 = GlobalKey(debugLabel: '1'); |
| final GlobalKey key2 = GlobalKey(debugLabel: '2'); |
| final GlobalKey key3 = GlobalKey(debugLabel: '3'); |
| final GlobalKey key4 = GlobalKey(debugLabel: '4'); |
| final GlobalKey key5 = GlobalKey(debugLabel: '5'); |
| await tester.pumpWidget(FocusTraversalGroup( |
| policy: WidgetOrderTraversalPolicy(), |
| child: FocusScope( |
| key: key1, |
| child: Column( |
| children: <Widget>[ |
| Focus( |
| key: key2, |
| child: SizedBox(key: key3, width: 100, height: 100), |
| ), |
| Focus( |
| key: key4, |
| child: SizedBox(key: key5, width: 100, height: 100), |
| ), |
| ], |
| ), |
| ), |
| )); |
| |
| final Element firstChild = tester.element(find.byKey(key3)); |
| final Element secondChild = tester.element(find.byKey(key5)); |
| final FocusNode firstFocusNode = Focus.of(firstChild); |
| final FocusNode secondFocusNode = Focus.of(secondChild); |
| final FocusNode scope = Focus.of(firstChild).enclosingScope!; |
| |
| expect(firstFocusNode.hasFocus, isFalse); |
| expect(secondFocusNode.hasFocus, isFalse); |
| |
| secondFocusNode.previousFocus(); |
| |
| await tester.pump(); |
| |
| expect(firstFocusNode.hasFocus, isFalse); |
| expect(secondFocusNode.hasFocus, isTrue); |
| expect(scope.hasFocus, isTrue); |
| }); |
| |
| testWidgets('Move focus to next node.', (WidgetTester tester) async { |
| final GlobalKey key1 = GlobalKey(debugLabel: '1'); |
| final GlobalKey key2 = GlobalKey(debugLabel: '2'); |
| final GlobalKey key3 = GlobalKey(debugLabel: '3'); |
| final GlobalKey key4 = GlobalKey(debugLabel: '4'); |
| final GlobalKey key5 = GlobalKey(debugLabel: '5'); |
| final GlobalKey key6 = GlobalKey(debugLabel: '6'); |
| bool? focus1; |
| bool? focus2; |
| bool? focus3; |
| bool? focus5; |
| await tester.pumpWidget( |
| FocusTraversalGroup( |
| policy: WidgetOrderTraversalPolicy(), |
| child: FocusScope( |
| debugLabel: 'key1', |
| key: key1, |
| onFocusChange: (bool focus) => focus1 = focus, |
| child: Column( |
| children: <Widget>[ |
| FocusScope( |
| debugLabel: 'key2', |
| key: key2, |
| onFocusChange: (bool focus) => focus2 = focus, |
| child: Column( |
| children: <Widget>[ |
| Focus( |
| debugLabel: 'key3', |
| key: key3, |
| onFocusChange: (bool focus) => focus3 = focus, |
| child: Container(key: key4), |
| ), |
| Focus( |
| debugLabel: 'key5', |
| key: key5, |
| onFocusChange: (bool focus) => focus5 = focus, |
| child: Container(key: key6), |
| ), |
| ], |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| final Element firstChild = tester.element(find.byKey(key4)); |
| final Element secondChild = tester.element(find.byKey(key6)); |
| final FocusNode firstFocusNode = Focus.of(firstChild); |
| final FocusNode secondFocusNode = Focus.of(secondChild); |
| final FocusNode scope = Focus.of(firstChild).enclosingScope!; |
| firstFocusNode.requestFocus(); |
| |
| await tester.pump(); |
| |
| expect(focus1, isTrue); |
| expect(focus2, isTrue); |
| expect(focus3, isTrue); |
| expect(focus5, isNull); |
| expect(firstFocusNode.hasFocus, isTrue); |
| expect(secondFocusNode.hasFocus, isFalse); |
| expect(scope.hasFocus, isTrue); |
| |
| focus1 = null; |
| focus2 = null; |
| focus3 = null; |
| focus5 = null; |
| |
| Focus.of(firstChild).nextFocus(); |
| |
| await tester.pump(); |
| |
| expect(focus1, isNull); |
| expect(focus2, isNull); |
| expect(focus3, isFalse); |
| expect(focus5, isTrue); |
| expect(firstFocusNode.hasFocus, isFalse); |
| expect(secondFocusNode.hasFocus, isTrue); |
| expect(scope.hasFocus, isTrue); |
| |
| focus1 = null; |
| focus2 = null; |
| focus3 = null; |
| focus5 = null; |
| |
| Focus.of(firstChild).nextFocus(); |
| |
| await tester.pump(); |
| |
| expect(focus1, isNull); |
| expect(focus2, isNull); |
| expect(focus3, isTrue); |
| expect(focus5, isFalse); |
| expect(firstFocusNode.hasFocus, isTrue); |
| expect(secondFocusNode.hasFocus, isFalse); |
| expect(scope.hasFocus, isTrue); |
| |
| focus1 = null; |
| focus2 = null; |
| focus3 = null; |
| focus5 = null; |
| |
| // Tests that can still move back to original node. |
| Focus.of(firstChild).previousFocus(); |
| |
| await tester.pump(); |
| |
| expect(focus1, isNull); |
| expect(focus2, isNull); |
| expect(focus3, isFalse); |
| expect(focus5, isTrue); |
| expect(firstFocusNode.hasFocus, isFalse); |
| expect(secondFocusNode.hasFocus, isTrue); |
| expect(scope.hasFocus, isTrue); |
| }); |
| |
| testWidgets('Move focus to previous node.', (WidgetTester tester) async { |
| final GlobalKey key1 = GlobalKey(debugLabel: '1'); |
| final GlobalKey key2 = GlobalKey(debugLabel: '2'); |
| final GlobalKey key3 = GlobalKey(debugLabel: '3'); |
| final GlobalKey key4 = GlobalKey(debugLabel: '4'); |
| final GlobalKey key5 = GlobalKey(debugLabel: '5'); |
| final GlobalKey key6 = GlobalKey(debugLabel: '6'); |
| await tester.pumpWidget( |
| FocusTraversalGroup( |
| policy: WidgetOrderTraversalPolicy(), |
| child: FocusScope( |
| key: key1, |
| child: Column( |
| children: <Widget>[ |
| FocusScope( |
| key: key2, |
| child: Column( |
| children: <Widget>[ |
| Focus( |
| key: key3, |
| child: Container(key: key4), |
| ), |
| Focus( |
| key: key5, |
| child: Container(key: key6), |
| ), |
| ], |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| final Element firstChild = tester.element(find.byKey(key4)); |
| final Element secondChild = tester.element(find.byKey(key6)); |
| final FocusNode firstFocusNode = Focus.of(firstChild); |
| final FocusNode secondFocusNode = Focus.of(secondChild); |
| final FocusNode scope = Focus.of(firstChild).enclosingScope!; |
| secondFocusNode.requestFocus(); |
| |
| await tester.pump(); |
| |
| expect(firstFocusNode.hasFocus, isFalse); |
| expect(secondFocusNode.hasFocus, isTrue); |
| expect(scope.hasFocus, isTrue); |
| |
| Focus.of(firstChild).previousFocus(); |
| |
| await tester.pump(); |
| |
| expect(firstFocusNode.hasFocus, isTrue); |
| expect(secondFocusNode.hasFocus, isFalse); |
| expect(scope.hasFocus, isTrue); |
| |
| Focus.of(firstChild).previousFocus(); |
| |
| await tester.pump(); |
| |
| expect(firstFocusNode.hasFocus, isFalse); |
| expect(secondFocusNode.hasFocus, isTrue); |
| expect(scope.hasFocus, isTrue); |
| |
| // Tests that can still move back to original node. |
| Focus.of(firstChild).nextFocus(); |
| |
| await tester.pump(); |
| |
| expect(firstFocusNode.hasFocus, isTrue); |
| expect(secondFocusNode.hasFocus, isFalse); |
| expect(scope.hasFocus, isTrue); |
| }); |
| |
| testWidgets('Move focus to next/previous node while skipping nodes in policy', (WidgetTester tester) async { |
| final List<FocusNode> nodes = |
| List<FocusNode>.generate(7, (int index) => FocusNode(debugLabel: 'Node $index')); |
| await tester.pumpWidget( |
| FocusTraversalGroup( |
| policy: SkipAllButFirstAndLastPolicy(), |
| child: Column( |
| children: List<Widget>.generate( |
| nodes.length, |
| (int index) => Focus( |
| focusNode: nodes[index], |
| child: const SizedBox(), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| nodes[2].requestFocus(); |
| await tester.pump(); |
| |
| expect(nodes[2].hasPrimaryFocus, isTrue); |
| |
| primaryFocus!.nextFocus(); |
| await tester.pump(); |
| |
| expect(nodes[6].hasPrimaryFocus, isTrue); |
| |
| primaryFocus!.previousFocus(); |
| await tester.pump(); |
| |
| expect(nodes[0].hasPrimaryFocus, isTrue); |
| }); |
| |
| 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: WidgetOrderTraversalPolicy(), |
| child: Center( |
| child: Builder(builder: (BuildContext context) { |
| return MaterialButton( |
| key: key1, |
| focusNode: testNode1, |
| autofocus: true, |
| onPressed: () { |
| Navigator.of(context).push<void>( |
| MaterialPageRoute<void>( |
| builder: (BuildContext context) { |
| return Center( |
| 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(ReadingOrderTraversalPolicy, () { |
| 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'); |
| final GlobalKey key3 = GlobalKey(debugLabel: '3'); |
| final GlobalKey key4 = GlobalKey(debugLabel: '4'); |
| final GlobalKey key5 = GlobalKey(debugLabel: '5'); |
| await tester.pumpWidget(FocusTraversalGroup( |
| policy: ReadingOrderTraversalPolicy(), |
| child: FocusScope( |
| key: key1, |
| child: Column( |
| children: <Widget>[ |
| Focus( |
| key: key2, |
| child: SizedBox(key: key3, width: 100, height: 100), |
| ), |
| Focus( |
| key: key4, |
| child: SizedBox(key: key5, width: 100, height: 100), |
| ), |
| ], |
| ), |
| ), |
| )); |
| |
| final Element firstChild = tester.element(find.byKey(key3)); |
| final Element secondChild = tester.element(find.byKey(key5)); |
| 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, isTrue); |
| expect(secondFocusNode.hasFocus, isFalse); |
| expect(scope.hasFocus, isTrue); |
| }); |
| |
| testWidgets('Move reading focus to next node.', (WidgetTester tester) async { |
| final GlobalKey key1 = GlobalKey(debugLabel: '1'); |
| final GlobalKey key2 = GlobalKey(debugLabel: '2'); |
| final GlobalKey key3 = GlobalKey(debugLabel: '3'); |
| final GlobalKey key4 = GlobalKey(debugLabel: '4'); |
| final GlobalKey key5 = GlobalKey(debugLabel: '5'); |
| final GlobalKey key6 = GlobalKey(debugLabel: '6'); |
| bool? focus1; |
| bool? focus2; |
| bool? focus3; |
| bool? focus5; |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusTraversalGroup( |
| policy: ReadingOrderTraversalPolicy(), |
| child: FocusScope( |
| debugLabel: 'key1', |
| key: key1, |
| onFocusChange: (bool focus) => focus1 = focus, |
| child: Column( |
| children: <Widget>[ |
| FocusScope( |
| debugLabel: 'key2', |
| key: key2, |
| onFocusChange: (bool focus) => focus2 = focus, |
| child: Row( |
| children: <Widget>[ |
| Focus( |
| debugLabel: 'key3', |
| key: key3, |
| onFocusChange: (bool focus) => focus3 = focus, |
| child: Container(key: key4), |
| ), |
| Focus( |
| debugLabel: 'key5', |
| key: key5, |
| onFocusChange: (bool focus) => focus5 = focus, |
| child: Container(key: key6), |
| ), |
| ], |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| void clear() { |
| focus1 = null; |
| focus2 = null; |
| focus3 = null; |
| focus5 = null; |
| } |
| |
| final Element firstChild = tester.element(find.byKey(key4)); |
| final Element secondChild = tester.element(find.byKey(key6)); |
| final FocusNode firstFocusNode = Focus.of(firstChild); |
| final FocusNode secondFocusNode = Focus.of(secondChild); |
| final FocusNode scope = Focus.of(firstChild).enclosingScope!; |
| firstFocusNode.requestFocus(); |
| |
| await tester.pump(); |
| |
| expect(focus1, isTrue); |
| expect(focus2, isTrue); |
| expect(focus3, isTrue); |
| expect(focus5, isNull); |
| expect(firstFocusNode.hasFocus, isTrue); |
| expect(secondFocusNode.hasFocus, isFalse); |
| expect(scope.hasFocus, isTrue); |
| clear(); |
| |
| Focus.of(firstChild).nextFocus(); |
| |
| await tester.pump(); |
| |
| expect(focus1, isNull); |
| expect(focus2, isNull); |
| expect(focus3, isFalse); |
| expect(focus5, isTrue); |
| expect(firstFocusNode.hasFocus, isFalse); |
| expect(secondFocusNode.hasFocus, isTrue); |
| expect(scope.hasFocus, isTrue); |
| clear(); |
| |
| Focus.of(firstChild).nextFocus(); |
| |
| await tester.pump(); |
| |
| expect(focus1, isNull); |
| expect(focus2, isNull); |
| expect(focus3, isTrue); |
| expect(focus5, isFalse); |
| expect(firstFocusNode.hasFocus, isTrue); |
| expect(secondFocusNode.hasFocus, isFalse); |
| expect(scope.hasFocus, isTrue); |
| clear(); |
| |
| // Tests that can still move back to original node. |
| Focus.of(firstChild).previousFocus(); |
| |
| await tester.pump(); |
| |
| expect(focus1, isNull); |
| expect(focus2, isNull); |
| expect(focus3, isFalse); |
| expect(focus5, isTrue); |
| expect(firstFocusNode.hasFocus, isFalse); |
| expect(secondFocusNode.hasFocus, isTrue); |
| expect(scope.hasFocus, isTrue); |
| }); |
| |
| testWidgets('Move reading focus to previous node.', (WidgetTester tester) async { |
| final GlobalKey key1 = GlobalKey(debugLabel: '1'); |
| final GlobalKey key2 = GlobalKey(debugLabel: '2'); |
| final GlobalKey key3 = GlobalKey(debugLabel: '3'); |
| final GlobalKey key4 = GlobalKey(debugLabel: '4'); |
| final GlobalKey key5 = GlobalKey(debugLabel: '5'); |
| final GlobalKey key6 = GlobalKey(debugLabel: '6'); |
| await tester.pumpWidget( |
| FocusTraversalGroup( |
| policy: ReadingOrderTraversalPolicy(), |
| child: FocusScope( |
| key: key1, |
| child: Column( |
| children: <Widget>[ |
| FocusScope( |
| key: key2, |
| child: Column( |
| children: <Widget>[ |
| Focus( |
| key: key3, |
| child: Container(key: key4), |
| ), |
| Focus( |
| key: key5, |
| child: Container(key: key6), |
| ), |
| ], |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| final Element firstChild = tester.element(find.byKey(key4)); |
| final Element secondChild = tester.element(find.byKey(key6)); |
| final FocusNode firstFocusNode = Focus.of(firstChild); |
| final FocusNode secondFocusNode = Focus.of(secondChild); |
| final FocusNode scope = Focus.of(firstChild).enclosingScope!; |
| secondFocusNode.requestFocus(); |
| |
| await tester.pump(); |
| |
| expect(firstFocusNode.hasFocus, isFalse); |
| expect(secondFocusNode.hasFocus, isTrue); |
| expect(scope.hasFocus, isTrue); |
| |
| Focus.of(firstChild).previousFocus(); |
| |
| await tester.pump(); |
| |
| expect(firstFocusNode.hasFocus, isTrue); |
| expect(secondFocusNode.hasFocus, isFalse); |
| expect(scope.hasFocus, isTrue); |
| |
| Focus.of(firstChild).previousFocus(); |
| |
| await tester.pump(); |
| |
| expect(firstFocusNode.hasFocus, isFalse); |
| expect(secondFocusNode.hasFocus, isTrue); |
| expect(scope.hasFocus, isTrue); |
| |
| // Tests that can still move back to original node. |
| Focus.of(firstChild).nextFocus(); |
| |
| await tester.pump(); |
| |
| expect(firstFocusNode.hasFocus, isTrue); |
| expect(secondFocusNode.hasFocus, isFalse); |
| 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: const SizedBox(width: 10, height: 10), |
| ), |
| Focus( |
| focusNode: nodes[1], |
| child: const SizedBox(width: 10, height: 10), |
| ), |
| Focus( |
| focusNode: nodes[2], |
| child: const SizedBox(width: 10, height: 10), |
| ), |
| ]), |
| ), |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: Row(children: <Widget>[ |
| Directionality( |
| textDirection: TextDirection.rtl, |
| child: Focus( |
| focusNode: nodes[3], |
| child: const SizedBox(width: 10, height: 10), |
| ), |
| ), |
| Directionality( |
| textDirection: TextDirection.rtl, |
| child: Focus( |
| focusNode: nodes[4], |
| child: const SizedBox(width: 10, height: 10), |
| ), |
| ), |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: Focus( |
| focusNode: nodes[5], |
| child: const SizedBox(width: 10, height: 10), |
| ), |
| ), |
| ]), |
| ), |
| Row(children: <Widget>[ |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: Focus( |
| focusNode: nodes[6], |
| child: const SizedBox(width: 10, height: 10), |
| ), |
| ), |
| Directionality( |
| textDirection: TextDirection.rtl, |
| child: Focus( |
| focusNode: nodes[7], |
| child: const SizedBox(width: 10, height: 10), |
| ), |
| ), |
| Directionality( |
| textDirection: TextDirection.rtl, |
| child: Focus( |
| focusNode: nodes[8], |
| child: const SizedBox(width: 10, height: 10), |
| ), |
| ), |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: Focus( |
| focusNode: nodes[9], |
| child: const SizedBox(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: Alignment.topLeft, |
| children: List<Widget>.generate(nodeCount, (int index) { |
| // Boxes that all have the same upper left origin corner. |
| return Focus( |
| focusNode: nodes[index], |
| child: SizedBox(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: Alignment.center, |
| children: List<Widget>.generate(nodeCount, (int index) { |
| return Focus( |
| focusNode: nodes[index], |
| child: SizedBox(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: Alignment.center, |
| 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: SizedBox(key: key1, width: 100, height: 100), |
| ), |
| ), |
| FocusTraversalOrder( |
| order: const NumericFocusOrder(1), |
| child: Focus( |
| child: SizedBox(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: const SizedBox(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: const SizedBox(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: const SizedBox(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: const SizedBox(width: 10, height: 10), |
| ), |
| ), |
| FocusTraversalOrder( |
| order: const NumericFocusOrder(8), |
| child: Focus( |
| focusNode: nodes[8], |
| child: const SizedBox(width: 10, height: 10), |
| ), |
| ), |
| FocusTraversalOrder( |
| order: const NumericFocusOrder(7), |
| child: Focus( |
| focusNode: nodes[7], |
| child: const SizedBox(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: const SizedBox(width: 10, height: 10), |
| ), |
| ), |
| FocusTraversalOrder( |
| order: const NumericFocusOrder(5), |
| child: Focus( |
| focusNode: nodes[5], |
| child: const SizedBox(width: 10, height: 10), |
| ), |
| ), |
| FocusTraversalOrder( |
| order: const NumericFocusOrder(6), |
| child: Focus( |
| focusNode: nodes[6], |
| child: const SizedBox(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: const SizedBox(width: 10, height: 10), |
| ), |
| ), |
| FocusTraversalOrder( |
| order: const LexicalFocusOrder('C'), |
| child: Focus( |
| focusNode: nodes[2], |
| child: const SizedBox(width: 10, height: 10), |
| ), |
| ), |
| FocusTraversalOrder( |
| order: const LexicalFocusOrder('B'), |
| child: Focus( |
| focusNode: nodes[1], |
| child: const SizedBox(width: 10, height: 10), |
| ), |
| ), |
| FocusTraversalOrder( |
| order: const LexicalFocusOrder('A'), |
| child: Focus( |
| focusNode: nodes[0], |
| child: const SizedBox(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, () { |
| testWidgets('Move focus in all directions.', (WidgetTester tester) async { |
| final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey'); |
| final GlobalKey upperRightKey = GlobalKey(debugLabel: 'upperRightKey'); |
| final GlobalKey lowerLeftKey = GlobalKey(debugLabel: 'lowerLeftKey'); |
| final GlobalKey lowerRightKey = GlobalKey(debugLabel: 'lowerRightKey'); |
| bool? focusUpperLeft; |
| bool? focusUpperRight; |
| bool? focusLowerLeft; |
| bool? focusLowerRight; |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusTraversalGroup( |
| policy: WidgetOrderTraversalPolicy(), |
| child: FocusScope( |
| debugLabel: 'Scope', |
| child: Column( |
| children: <Widget>[ |
| Row( |
| children: <Widget>[ |
| Focus( |
| debugLabel: 'upperLeft', |
| onFocusChange: (bool focus) => focusUpperLeft = focus, |
| child: SizedBox(width: 100, height: 100, key: upperLeftKey), |
| ), |
| Focus( |
| debugLabel: 'upperRight', |
| onFocusChange: (bool focus) => focusUpperRight = focus, |
| child: SizedBox(width: 100, height: 100, key: upperRightKey), |
| ), |
| ], |
| ), |
| Row( |
| children: <Widget>[ |
| Focus( |
| debugLabel: 'lowerLeft', |
| onFocusChange: (bool focus) => focusLowerLeft = focus, |
| child: SizedBox(width: 100, height: 100, key: lowerLeftKey), |
| ), |
| Focus( |
| debugLabel: 'lowerRight', |
| onFocusChange: (bool focus) => focusLowerRight = focus, |
| child: SizedBox(width: 100, height: 100, key: lowerRightKey), |
| ), |
| ], |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| void clear() { |
| focusUpperLeft = null; |
| focusUpperRight = null; |
| focusLowerLeft = null; |
| focusLowerRight = null; |
| } |
| |
| final FocusNode upperLeftNode = Focus.of(tester.element(find.byKey(upperLeftKey))); |
| final FocusNode upperRightNode = Focus.of(tester.element(find.byKey(upperRightKey))); |
| final FocusNode lowerLeftNode = Focus.of(tester.element(find.byKey(lowerLeftKey))); |
| final FocusNode lowerRightNode = Focus.of(tester.element(find.byKey(lowerRightKey))); |
| final FocusNode scope = upperLeftNode.enclosingScope!; |
| upperLeftNode.requestFocus(); |
| |
| await tester.pump(); |
| |
| expect(focusUpperLeft, isTrue); |
| expect(focusUpperRight, isNull); |
| expect(focusLowerLeft, isNull); |
| expect(focusLowerRight, isNull); |
| expect(upperLeftNode.hasFocus, isTrue); |
| expect(upperRightNode.hasFocus, isFalse); |
| expect(lowerLeftNode.hasFocus, isFalse); |
| expect(lowerRightNode.hasFocus, isFalse); |
| expect(scope.hasFocus, isTrue); |
| clear(); |
| |
| expect(scope.focusInDirection(TraversalDirection.right), isTrue); |
| |
| await tester.pump(); |
| |
| expect(focusUpperLeft, isFalse); |
| expect(focusUpperRight, isTrue); |
| expect(focusLowerLeft, isNull); |
| expect(focusLowerRight, isNull); |
| expect(upperLeftNode.hasFocus, isFalse); |
| expect(upperRightNode.hasFocus, isTrue); |
| expect(lowerLeftNode.hasFocus, isFalse); |
| expect(lowerRightNode.hasFocus, isFalse); |
| expect(scope.hasFocus, isTrue); |
| clear(); |
| |
| expect(scope.focusInDirection(TraversalDirection.down), isTrue); |
| |
| await tester.pump(); |
| |
| expect(focusUpperLeft, isNull); |
| expect(focusUpperRight, isFalse); |
| expect(focusLowerLeft, isNull); |
| expect(focusLowerRight, isTrue); |
| expect(upperLeftNode.hasFocus, isFalse); |
| expect(upperRightNode.hasFocus, isFalse); |
| expect(lowerLeftNode.hasFocus, isFalse); |
| expect(lowerRightNode.hasFocus, isTrue); |
| expect(scope.hasFocus, isTrue); |
| clear(); |
| |
| expect(scope.focusInDirection(TraversalDirection.left), isTrue); |
| |
| await tester.pump(); |
| |
| expect(focusUpperLeft, isNull); |
| expect(focusUpperRight, isNull); |
| expect(focusLowerLeft, isTrue); |
| expect(focusLowerRight, isFalse); |
| expect(upperLeftNode.hasFocus, isFalse); |
| expect(upperRightNode.hasFocus, isFalse); |
| expect(lowerLeftNode.hasFocus, isTrue); |
| expect(lowerRightNode.hasFocus, isFalse); |
| expect(scope.hasFocus, isTrue); |
| clear(); |
| |
| expect(scope.focusInDirection(TraversalDirection.up), isTrue); |
| |
| await tester.pump(); |
| |
| expect(focusUpperLeft, isTrue); |
| expect(focusUpperRight, isNull); |
| expect(focusLowerLeft, isFalse); |
| expect(focusLowerRight, isNull); |
| expect(upperLeftNode.hasFocus, isTrue); |
| expect(upperRightNode.hasFocus, isFalse); |
| expect(lowerLeftNode.hasFocus, isFalse); |
| expect(lowerRightNode.hasFocus, isFalse); |
| expect(scope.hasFocus, isTrue); |
| }); |
| |
| testWidgets('Directional focus avoids hysteresis.', (WidgetTester tester) async { |
| final List<GlobalKey> keys = <GlobalKey>[ |
| GlobalKey(debugLabel: 'row 1:1'), |
| GlobalKey(debugLabel: 'row 2:1'), |
| GlobalKey(debugLabel: 'row 2:2'), |
| GlobalKey(debugLabel: 'row 3:1'), |
| GlobalKey(debugLabel: 'row 3:2'), |
| GlobalKey(debugLabel: 'row 3:3'), |
| ]; |
| List<bool?> focus = List<bool?>.generate(keys.length, (int _) => null); |
| Focus makeFocus(int index) { |
| return Focus( |
| debugLabel: keys[index].toString(), |
| onFocusChange: (bool isFocused) => focus[index] = isFocused, |
| child: SizedBox(width: 100, height: 100, key: keys[index]), |
| ); |
| } |
| |
| /// Layout is: |
| /// keys[0] |
| /// keys[1] keys[2] |
| /// keys[3] keys[4] keys[5] |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusTraversalGroup( |
| policy: WidgetOrderTraversalPolicy(), |
| child: FocusScope( |
| debugLabel: 'Scope', |
| child: Column( |
| children: <Widget>[ |
| Row( |
| mainAxisAlignment: MainAxisAlignment.center, |
| children: <Widget>[ |
| makeFocus(0), |
| ], |
| ), |
| Row( |
| mainAxisAlignment: MainAxisAlignment.center, |
| children: <Widget>[ |
| makeFocus(1), |
| makeFocus(2), |
| ], |
| ), |
| Row( |
| mainAxisAlignment: MainAxisAlignment.center, |
| children: <Widget>[ |
| makeFocus(3), |
| makeFocus(4), |
| makeFocus(5), |
| ], |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| void clear() { |
| focus = List<bool?>.generate(keys.length, (int _) => null); |
| } |
| |
| final List<FocusNode> nodes = keys.map<FocusNode>((GlobalKey key) => Focus.of(tester.element(find.byKey(key)))).toList(); |
| final FocusNode scope = nodes[0].enclosingScope!; |
| nodes[4].requestFocus(); |
| |
| void expectState(List<bool?> states) { |
| for (int index = 0; index < states.length; ++index) { |
| expect(focus[index], states[index] == null ? isNull : (states[index]! ? isTrue : isFalse)); |
| if (states[index] == null) { |
| expect(nodes[index].hasFocus, isFalse); |
| } else { |
| expect(nodes[index].hasFocus, states[index]); |
| } |
| expect(scope.hasFocus, isTrue); |
| } |
| } |
| |
| // Test to make sure that the same path is followed backwards and forwards. |
| await tester.pump(); |
| expectState(<bool?>[null, null, null, null, true, null]); |
| clear(); |
| |
| expect(scope.focusInDirection(TraversalDirection.up), isTrue); |
| await tester.pump(); |
| |
| expectState(<bool?>[null, null, true, null, false, null]); |
| clear(); |
| |
| expect(scope.focusInDirection(TraversalDirection.up), isTrue); |
| await tester.pump(); |
| |
| expectState(<bool?>[true, null, false, null, null, null]); |
| clear(); |
| |
| expect(scope.focusInDirection(TraversalDirection.down), isTrue); |
| await tester.pump(); |
| |
| expectState(<bool?>[false, null, true, null, null, null]); |
| clear(); |
| |
| expect(scope.focusInDirection(TraversalDirection.down), isTrue); |
| await tester.pump(); |
| expectState(<bool?>[null, null, false, null, true, null]); |
| clear(); |
| |
| // Make sure that moving in a different axis clears the history. |
| expect(scope.focusInDirection(TraversalDirection.left), isTrue); |
| await tester.pump(); |
| expectState(<bool?>[null, null, null, true, false, null]); |
| clear(); |
| |
| expect(scope.focusInDirection(TraversalDirection.up), isTrue); |
| await tester.pump(); |
| |
| expectState(<bool?>[null, true, null, false, null, null]); |
| clear(); |
| |
| expect(scope.focusInDirection(TraversalDirection.up), isTrue); |
| await tester.pump(); |
| |
| expectState(<bool?>[true, false, null, null, null, null]); |
| clear(); |
| |
| expect(scope.focusInDirection(TraversalDirection.down), isTrue); |
| await tester.pump(); |
| |
| expectState(<bool?>[false, true, null, null, null, null]); |
| clear(); |
| |
| expect(scope.focusInDirection(TraversalDirection.down), isTrue); |
| await tester.pump(); |
| expectState(<bool?>[null, false, null, true, null, null]); |
| clear(); |
| }); |
| |
| testWidgets('Can find first focus in all directions.', (WidgetTester tester) async { |
| final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey'); |
| final GlobalKey upperRightKey = GlobalKey(debugLabel: 'upperRightKey'); |
| final GlobalKey lowerLeftKey = GlobalKey(debugLabel: 'lowerLeftKey'); |
| |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusTraversalGroup( |
| policy: WidgetOrderTraversalPolicy(), |
| child: FocusScope( |
| debugLabel: 'scope', |
| child: Column( |
| children: <Widget>[ |
| Row( |
| children: <Widget>[ |
| Focus( |
| debugLabel: 'upperLeft', |
| child: SizedBox(width: 100, height: 100, key: upperLeftKey), |
| ), |
| Focus( |
| debugLabel: 'upperRight', |
| child: SizedBox(width: 100, height: 100, key: upperRightKey), |
| ), |
| ], |
| ), |
| Row( |
| children: <Widget>[ |
| Focus( |
| debugLabel: 'lowerLeft', |
| child: SizedBox(width: 100, height: 100, key: lowerLeftKey), |
| ), |
| const Focus( |
| debugLabel: 'lowerRight', |
| child: SizedBox(width: 100, height: 100), |
| ), |
| ], |
| ), |
| ], |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| final FocusNode upperLeftNode = Focus.of(tester.element(find.byKey(upperLeftKey))); |
| final FocusNode upperRightNode = Focus.of(tester.element(find.byKey(upperRightKey))); |
| final FocusNode lowerLeftNode = Focus.of(tester.element(find.byKey(lowerLeftKey))); |
| final FocusNode scope = upperLeftNode.enclosingScope!; |
| |
| await tester.pump(); |
| |
| final FocusTraversalPolicy policy = FocusTraversalGroup.of(upperLeftKey.currentContext!); |
| |
| expect(policy.findFirstFocusInDirection(scope, TraversalDirection.up), equals(lowerLeftNode)); |
| expect(policy.findFirstFocusInDirection(scope, TraversalDirection.down), equals(upperLeftNode)); |
| expect(policy.findFirstFocusInDirection(scope, TraversalDirection.left), equals(upperRightNode)); |
| expect(policy.findFirstFocusInDirection(scope, TraversalDirection.right), equals(upperLeftNode)); |
| }); |
| |
| testWidgets('Can find focus when policy data dirty', (WidgetTester tester) async { |
| final FocusNode focusTop = FocusNode(debugLabel: 'top'); |
| final FocusNode focusCenter = FocusNode(debugLabel: 'center'); |
| final FocusNode focusBottom = FocusNode(debugLabel: 'bottom'); |
| |
| final FocusTraversalPolicy policy = ReadingOrderTraversalPolicy(); |
| await tester.pumpWidget(FocusTraversalGroup( |
| policy: policy, |
| child: FocusScope( |
| debugLabel: 'Scope', |
| child: Column( |
| children: <Widget>[ |
| Focus(focusNode: focusTop, child: const SizedBox(width: 100, height: 100)), |
| Focus(focusNode: focusCenter, child: const SizedBox(width: 100, height: 100)), |
| Focus(focusNode: focusBottom, child: const SizedBox(width: 100, height: 100)), |
| ], |
| ), |
| ), |
| )); |
| |
| focusTop.requestFocus(); |
| final FocusNode scope = focusTop.enclosingScope!; |
| |
| scope.focusInDirection(TraversalDirection.down); |
| scope.focusInDirection(TraversalDirection.down); |
| |
| await tester.pump(); |
| expect(focusBottom.hasFocus, isTrue); |
| |
| // Remove center focus node. |
| await tester.pumpWidget(FocusTraversalGroup( |
| policy: policy, |
| child: FocusScope( |
| debugLabel: 'Scope', |
| child: Column( |
| children: <Widget>[ |
| Focus(focusNode: focusTop, child: const SizedBox(width: 100, height: 100)), |
| Focus(focusNode: focusBottom, child: const SizedBox(width: 100, height: 100)), |
| ], |
| ), |
| ), |
| )); |
| |
| expect(focusBottom.hasFocus, isTrue); |
| scope.focusInDirection(TraversalDirection.up); |
| await tester.pump(); |
| |
| expect(focusCenter.hasFocus, isFalse); |
| expect(focusTop.hasFocus, isTrue); |
| }); |
| |
| testWidgets('Focus traversal actions are invoked when shortcuts are used.', (WidgetTester tester) async { |
| final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey'); |
| final GlobalKey upperRightKey = GlobalKey(debugLabel: 'upperRightKey'); |
| final GlobalKey lowerLeftKey = GlobalKey(debugLabel: 'lowerLeftKey'); |
| final GlobalKey lowerRightKey = GlobalKey(debugLabel: 'lowerRightKey'); |
| |
| await tester.pumpWidget( |
| WidgetsApp( |
| color: const Color(0xFFFFFFFF), |
| onGenerateRoute: (RouteSettings settings) { |
| return TestRoute( |
| child: Directionality( |
| textDirection: TextDirection.ltr, |
| child: FocusScope( |
| debugLabel: 'scope', |
| child: Column( |
| children: <Widget>[ |
| Row( |
| children: <Widget>[ |
| Focus( |
| autofocus: true, |
| debugLabel: 'upperLeft', |
| child: SizedBox(width: 100, height: 100, key: upperLeftKey), |
| ), |
| Focus( |
| debugLabel: 'upperRight', |
| child: SizedBox(width: 100, height: 100, key: upperRightKey), |
| ), |
| ], |
| ), |
| Row( |
| children: <Widget>[ |
| Focus( |
| debugLabel: 'lowerLeft', |
| child: SizedBox(width: 100, height: 100, key: lowerLeftKey), |
| ), |
| Focus( |
| debugLabel: 'lowerRight', |
| child: SizedBox(width: 100, height: 100, key: lowerRightKey), |
| ), |
| ], |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| }, |
| ), |
| ); |
| |
| expect(Focus.of(upperLeftKey.currentContext!).hasPrimaryFocus, isTrue); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| expect(Focus.of(upperRightKey.currentContext!).hasPrimaryFocus, isTrue); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| expect(Focus.of(lowerLeftKey.currentContext!).hasPrimaryFocus, isTrue); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| expect(Focus.of(lowerRightKey.currentContext!).hasPrimaryFocus, isTrue); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| expect(Focus.of(upperLeftKey.currentContext!).hasPrimaryFocus, isTrue); |
| |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.shift); |
| expect(Focus.of(lowerRightKey.currentContext!).hasPrimaryFocus, isTrue); |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.shift); |
| expect(Focus.of(lowerLeftKey.currentContext!).hasPrimaryFocus, isTrue); |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.shift); |
| expect(Focus.of(upperRightKey.currentContext!).hasPrimaryFocus, isTrue); |
| await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.sendKeyUpEvent(LogicalKeyboardKey.shift); |
| expect(Focus.of(upperLeftKey.currentContext!).hasPrimaryFocus, isTrue); |
| |
| // Traverse in a direction |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| expect(Focus.of(upperRightKey.currentContext!).hasPrimaryFocus, isTrue); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| expect(Focus.of(lowerRightKey.currentContext!).hasPrimaryFocus, isTrue); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| expect(Focus.of(lowerLeftKey.currentContext!).hasPrimaryFocus, isTrue); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); |
| expect(Focus.of(upperLeftKey.currentContext!).hasPrimaryFocus, isTrue); |
| }, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347 |
| |
| testWidgets('Focus traversal inside a vertical scrollable scrolls to stay visible.', (WidgetTester tester) async { |
| final List<int> items = List<int>.generate(11, (int index) => index).toList(); |
| final List<FocusNode> nodes = List<FocusNode>.generate(11, (int index) => FocusNode(debugLabel: 'Item ${index + 1}')).toList(); |
| final FocusNode topNode = FocusNode(debugLabel: 'Header'); |
| final FocusNode bottomNode = FocusNode(debugLabel: 'Footer'); |
| final ScrollController controller = ScrollController(); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Column( |
| children: <Widget>[ |
| Focus(focusNode: topNode, child: Container(height: 100)), |
| Expanded( |
| child: ListView( |
| controller: controller, |
| children: items.map<Widget>((int item) { |
| return Focus( |
| focusNode: nodes[item], |
| child: Container(height: 100), |
| ); |
| }).toList(), |
| ), |
| ), |
| Focus(focusNode: bottomNode, child: Container(height: 100)), |
| ], |
| ), |
| ), |
| ); |
| |
| // Start at the top |
| expect(controller.offset, equals(0.0)); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.pump(); |
| expect(topNode.hasPrimaryFocus, isTrue); |
| expect(controller.offset, equals(0.0)); |
| |
| // Enter the list. |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.pump(); |
| expect(nodes[0].hasPrimaryFocus, isTrue); |
| expect(controller.offset, equals(0.0)); |
| |
| // Go down until we hit the bottom of the visible area. |
| for (int i = 1; i <= 4; ++i) { |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.pump(); |
| expect(controller.offset, equals(0.0), reason: 'Focusing item $i caused a scroll'); |
| } |
| |
| // Now keep going down, and the scrollable should scroll automatically. |
| for (int i = 5; i <= 10; ++i) { |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.pump(); |
| final double expectedOffset = 100.0 * (i - 5) + 200.0; |
| expect(controller.offset, equals(expectedOffset), reason: "Focusing item $i didn't cause a scroll to $expectedOffset"); |
| } |
| |
| // Now go one more, and see that the footer gets focused. |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| await tester.pump(); |
| expect(bottomNode.hasPrimaryFocus, isTrue); |
| expect(controller.offset, equals(100.0 * (10 - 5) + 200.0)); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); |
| await tester.pump(); |
| expect(nodes[10].hasPrimaryFocus, isTrue); |
| expect(controller.offset, equals(100.0 * (10 - 5) + 200.0)); |
| |
| // Now reverse directions and go back to the top. |
| |
| // These should not cause a scroll. |
| final double lowestOffset = controller.offset; |
| for (int i = 10; i >= 8; --i) { |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); |
| await tester.pump(); |
| expect(controller.offset, equals(lowestOffset), reason: 'Focusing item $i caused a scroll'); |
| } |
| |
| // These should all cause a scroll. |
| for (int i = 7; i >= 1; --i) { |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); |
| await tester.pump(); |
| final double expectedOffset = 100.0 * (i - 1); |
| expect(controller.offset, equals(expectedOffset), reason: "Focusing item $i didn't cause a scroll"); |
| } |
| |
| // Back at the top. |
| expect(nodes[0].hasPrimaryFocus, isTrue); |
| expect(controller.offset, equals(0.0)); |
| |
| // Now we jump to the header. |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); |
| await tester.pump(); |
| expect(topNode.hasPrimaryFocus, isTrue); |
| expect(controller.offset, equals(0.0)); |
| }, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347 |
| |
| testWidgets('Focus traversal inside a horizontal scrollable scrolls to stay visible.', (WidgetTester tester) async { |
| final List<int> items = List<int>.generate(11, (int index) => index).toList(); |
| final List<FocusNode> nodes = List<FocusNode>.generate(11, (int index) => FocusNode(debugLabel: 'Item ${index + 1}')).toList(); |
| final FocusNode leftNode = FocusNode(debugLabel: 'Left Side'); |
| final FocusNode rightNode = FocusNode(debugLabel: 'Right Side'); |
| final ScrollController controller = ScrollController(); |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Row( |
| children: <Widget>[ |
| Focus(focusNode: leftNode, child: Container(width: 100)), |
| Expanded( |
| child: ListView( |
| scrollDirection: Axis.horizontal, |
| controller: controller, |
| children: items.map<Widget>((int item) { |
| return Focus( |
| focusNode: nodes[item], |
| child: Container(width: 100), |
| ); |
| }).toList(), |
| ), |
| ), |
| Focus(focusNode: rightNode, child: Container(width: 100)), |
| ], |
| ), |
| ), |
| ); |
| |
| // Start at the right |
| expect(controller.offset, equals(0.0)); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pump(); |
| expect(leftNode.hasPrimaryFocus, isTrue); |
| expect(controller.offset, equals(0.0)); |
| |
| // Enter the list. |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pump(); |
| expect(nodes[0].hasPrimaryFocus, isTrue); |
| expect(controller.offset, equals(0.0)); |
| |
| // Go right until we hit the right of the visible area. |
| for (int i = 1; i <= 6; ++i) { |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pump(); |
| expect(controller.offset, equals(0.0), reason: 'Focusing item $i caused a scroll'); |
| } |
| |
| // Now keep going right, and the scrollable should scroll automatically. |
| for (int i = 7; i <= 10; ++i) { |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pump(); |
| final double expectedOffset = 100.0 * (i - 5); |
| expect(controller.offset, equals(expectedOffset), reason: "Focusing item $i didn't cause a scroll to $expectedOffset"); |
| } |
| |
| // Now go one more, and see that the right edge gets focused. |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.pump(); |
| expect(rightNode.hasPrimaryFocus, isTrue); |
| expect(controller.offset, equals(100.0 * 5)); |
| |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.pump(); |
| expect(nodes[10].hasPrimaryFocus, isTrue); |
| expect(controller.offset, equals(100.0 * 5)); |
| |
| // Now reverse directions and go back to the left. |
| |
| // These should not cause a scroll. |
| final double lowestOffset = controller.offset; |
| for (int i = 10; i >= 7; --i) { |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.pump(); |
| expect(controller.offset, equals(lowestOffset), reason: 'Focusing item $i caused a scroll'); |
| } |
| |
| // These should all cause a scroll. |
| for (int i = 6; i >= 1; --i) { |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.pump(); |
| final double expectedOffset = 100.0 * (i - 1); |
| expect(controller.offset, equals(expectedOffset), reason: "Focusing item $i didn't cause a scroll"); |
| } |
| |
| // Back at the left side of the scrollable. |
| expect(nodes[0].hasPrimaryFocus, isTrue); |
| expect(controller.offset, equals(0.0)); |
| |
| // Now we jump to the left edge of the app. |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| await tester.pump(); |
| expect(leftNode.hasPrimaryFocus, isTrue); |
| expect(controller.offset, equals(0.0)); |
| }, skip: isBrowser, variant: KeySimulatorTransitModeVariant.all()); // https://github.com/flutter/flutter/issues/35347 |
| |
| testWidgets('Arrow focus traversal actions can be re-enabled for text fields.', (WidgetTester tester) async { |
| final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey'); |
| final GlobalKey upperRightKey = GlobalKey(debugLabel: 'upperRightKey'); |
| final GlobalKey lowerLeftKey = GlobalKey(debugLabel: 'lowerLeftKey'); |
| final GlobalKey lowerRightKey = GlobalKey(debugLabel: 'lowerRightKey'); |
| |
| final TextEditingController controller1 = TextEditingController(); |
| final TextEditingController controller2 = TextEditingController(); |
| final TextEditingController controller3 = TextEditingController(); |
| final TextEditingController controller4 = TextEditingController(); |
| |
| final FocusNode focusNodeUpperLeft = FocusNode(debugLabel: 'upperLeft'); |
| final FocusNode focusNodeUpperRight = FocusNode(debugLabel: 'upperRight'); |
| final FocusNode focusNodeLowerLeft = FocusNode(debugLabel: 'lowerLeft'); |
| final FocusNode focusNodeLowerRight = FocusNode(debugLabel: 'lowerRight'); |
| |
| Widget generateTestWidgets(bool ignoreTextFields) { |
| final Map<ShortcutActivator, Intent> shortcuts = <ShortcutActivator, Intent>{ |
| const SingleActivator(LogicalKeyboardKey.arrowLeft): DirectionalFocusIntent(TraversalDirection.left, ignoreTextFields: ignoreTextFields), |
| const SingleActivator(LogicalKeyboardKey.arrowRight): DirectionalFocusIntent(TraversalDirection.right, ignoreTextFields: ignoreTextFields), |
| const SingleActivator(LogicalKeyboardKey.arrowDown): DirectionalFocusIntent(TraversalDirection.down, ignoreTextFields: ignoreTextFields), |
| const SingleActivator(LogicalKeyboardKey.arrowUp): DirectionalFocusIntent(TraversalDirection.up, ignoreTextFields: ignoreTextFields), |
| }; |
| |
| return MaterialApp( |
| home: Shortcuts( |
| shortcuts: shortcuts, |
| child: FocusScope( |
| debugLabel: 'scope', |
| child: Column( |
| children: <Widget>[ |
| Row( |
| children: <Widget>[ |
| SizedBox( |
| width: 100, |
| height: 100, |
| child: EditableText( |
| autofocus: true, |
| key: upperLeftKey, |
| controller: controller1, |
| focusNode: focusNodeUpperLeft, |
| cursorColor: const Color(0xffffffff), |
| backgroundCursorColor: const Color(0xff808080), |
| style: const TextStyle(), |
| ), |
| ), |
| SizedBox( |
| width: 100, |
| height: 100, |
| child: EditableText( |
| key: upperRightKey, |
| controller: controller2, |
| focusNode: focusNodeUpperRight, |
| cursorColor: const Color(0xffffffff), |
| backgroundCursorColor: const Color(0xff808080), |
| style: const TextStyle(), |
| ), |
| ), |
| ], |
| ), |
| Row( |
| children: <Widget>[ |
| SizedBox( |
| width: 100, |
| height: 100, |
| child: EditableText( |
| key: lowerLeftKey, |
| controller: controller3, |
| focusNode: focusNodeLowerLeft, |
| cursorColor: const Color(0xffffffff), |
| backgroundCursorColor: const Color(0xff808080), |
| style: const TextStyle(), |
| ), |
| ), |
| SizedBox( |
| width: 100, |
| height: 100, |
| child: EditableText( |
| key: lowerRightKey, |
| controller: controller4, |
| focusNode: focusNodeLowerRight, |
| cursorColor: const Color(0xffffffff), |
| backgroundCursorColor: const Color(0xff808080), |
| style: const TextStyle(), |
| ), |
| ), |
| ], |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| await tester.pumpWidget(generateTestWidgets(false)); |
| |
| expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| expect(focusNodeUpperRight.hasPrimaryFocus, isTrue); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| expect(focusNodeLowerRight.hasPrimaryFocus, isTrue); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| expect(focusNodeLowerLeft.hasPrimaryFocus, isTrue); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); |
| expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); |
| |
| await tester.pumpWidget(generateTestWidgets(true)); |
| |
| expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| expect(focusNodeUpperRight.hasPrimaryFocus, isFalse); |
| expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); |
| expect(focusNodeLowerRight.hasPrimaryFocus, isFalse); |
| expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); |
| expect(focusNodeLowerLeft.hasPrimaryFocus, isFalse); |
| expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); |
| expect(focusNodeUpperLeft.hasPrimaryFocus, isTrue); |
| }, variant: KeySimulatorTransitModeVariant.all()); |
| |
| testWidgets('Focus traversal does not break when no focusable is available on a MaterialApp', (WidgetTester tester) async { |
| final List<Object> events = <Object>[]; |
| |
| await tester.pumpWidget(MaterialApp(home: Container())); |
| |
| RawKeyboard.instance.addListener((RawKeyEvent event) { |
| events.add(event); |
| }); |
| |
| await tester.idle(); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.idle(); |
| |
| expect(events.length, 2); |
| }, variant: KeySimulatorTransitModeVariant.all()); |
| |
| testWidgets('Focus traversal does not throw when no focusable is available in a group', (WidgetTester tester) async { |
| await tester.pumpWidget(const MaterialApp(home: Scaffold(body: ListTile(title: Text('title'))))); |
| final FocusNode? initialFocus = primaryFocus; |
| await tester.sendKeyEvent(LogicalKeyboardKey.tab); |
| await tester.pump(); |
| expect(primaryFocus, equals(initialFocus)); |
| }); |
| |
| testWidgets('Focus traversal does not break when no focusable is available on a WidgetsApp', (WidgetTester tester) async { |
| final List<RawKeyEvent> events = <RawKeyEvent>[]; |
| |
| await tester.pumpWidget( |
| WidgetsApp( |
| color: Colors.white, |
| onGenerateRoute: (RouteSettings settings) => PageRouteBuilder<void>( |
| settings: settings, |
| pageBuilder: (BuildContext context, Animation<double> animation1, Animation<double> animation2) { |
| return const Placeholder(); |
| }, |
| ), |
| ), |
| ); |
| |
| RawKeyboard.instance.addListener((RawKeyEvent event) { |
| events.add(event); |
| }); |
| |
| await tester.idle(); |
| await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); |
| await tester.idle(); |
| |
| expect(events.length, 2); |
| }, variant: KeySimulatorTransitModeVariant.all()); |
| }); |
| group(FocusTraversalGroup, () { |
| testWidgets("Focus traversal group doesn't introduce a Semantics node", (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| await tester.pumpWidget(FocusTraversalGroup(child: Container())); |
| 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'); |
| final FocusNode focusNode = FocusNode(); |
| bool? gotFocus; |
| await tester.pumpWidget( |
| FocusTraversalGroup( |
| descendantsAreFocusable: false, |
| child: Focus( |
| onFocusChange: (bool focused) => gotFocus = focused, |
| child: Focus( |
| key: key1, |
| focusNode: focusNode, |
| child: Container(key: key2), |
| ), |
| ), |
| ), |
| ); |
| |
| final Element childWidget = tester.element(find.byKey(key1)); |
| final FocusNode unfocusableNode = Focus.of(childWidget); |
| final Element containerWidget = tester.element(find.byKey(key2)); |
| final FocusNode containerNode = Focus.of(containerWidget); |
| |
| unfocusableNode.requestFocus(); |
| await tester.pump(); |
| |
| expect(gotFocus, isNull); |
| expect(containerNode.hasFocus, isFalse); |
| expect(unfocusableNode.hasFocus, isFalse); |
| |
| containerNode.requestFocus(); |
| await tester.pump(); |
| |
| expect(gotFocus, isNull); |
| expect(containerNode.hasFocus, isFalse); |
| expect(unfocusableNode.hasFocus, 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'); |
| final FocusNode focusNode = FocusNode(); |
| bool? gotFocus; |
| await tester.pumpWidget( |
| FocusTraversalGroup( |
| child: Column( |
| children: <Widget>[ |
| Focus( |
| autofocus: true, |
| child: Container(), |
| ), |
| FocusTraversalGroup( |
| descendantsAreFocusable: false, |
| child: Focus( |
| onFocusChange: (bool focused) => gotFocus = focused, |
| child: Focus( |
| key: key1, |
| focusNode: focusNode, |
| child: Container(key: key2), |
| ), |
| ), |
| ), |
| ], |
| ), |
| ), |
| ); |
| |
| final Element childWidget = tester.element(find.byKey(key1)); |
| final FocusNode unfocusableNode = Focus.of(childWidget); |
| final Element containerWidget = tester.element(find.byKey(key2)); |
| final FocusNode containerNode = Focus.of(containerWidget); |
| |
| await tester.pump(); |
| primaryFocus!.nextFocus(); |
| |
| expect(gotFocus, isNull); |
| expect(containerNode.hasFocus, isFalse); |
| expect(unfocusableNode.hasFocus, isFalse); |
| |
| containerNode.requestFocus(); |
| await tester.pump(); |
| |
| expect(gotFocus, isNull); |
| 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'); |
| await tester.pumpWidget( |
| FocusTraversalGroup( |
| child: Directionality( |
| textDirection: TextDirection.rtl, |
| child: Column( |
| children: <Widget>[ |
| FocusTraversalGroup( |
| child: Container(key: key), |
| ), |
| Focus( |
| focusNode: focusNode, |
| autofocus: true, |
| child: Container(), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| |
| await tester.pump(); |
| primaryFocus!.nextFocus(); |
| await tester.pump(); |
| expect(primaryFocus, equals(focusNode)); |
| }); |
| }); |
| group(RawKeyboardListener, () { |
| testWidgets('Raw keyboard listener introduces a Semantics node by default', (WidgetTester tester) async { |
| final SemanticsTester semantics = SemanticsTester(tester); |
| final FocusNode focusNode = FocusNode(); |
| await tester.pumpWidget( |
| RawKeyboardListener( |
| focusNode: focusNode, |
| child: Container(), |
| ), |
| ); |
| final TestSemantics expectedSemantics = TestSemantics.root( |
| children: <TestSemantics>[ |
| TestSemantics.rootChild( |
| flags: <SemanticsFlag>[ |
| SemanticsFlag.isFocusable, |
| ], |
| ), |
| ], |
| ); |
| expect(semantics, hasSemantics( |
| expectedSemantics, |
| ignoreId: true, |
| ignoreRect: true, |
| 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(); |
| await tester.pumpWidget( |
| RawKeyboardListener( |
| focusNode: focusNode, |
| includeSemantics: false, |
| child: Container(), |
| ), |
| ); |
| final TestSemantics expectedSemantics = TestSemantics.root(); |
| expect(semantics, hasSemantics(expectedSemantics)); |
| }); |
| }); |
| } |
| |
| class TestRoute extends PageRouteBuilder<void> { |
| TestRoute({required Widget child}) |
| : super( |
| pageBuilder: (BuildContext _, Animation<double> __, Animation<double> ___) { |
| return child; |
| }, |
| ); |
| } |