
## Description This causes the `Focus` widget to request focus on its focus node if the accessibility system (screen reader) focuses a widget via the `SemanticsAction.focus` action. ## Related Issues - https://github.com/flutter/flutter/issues/83809 ## Tests - Added a test to make sure that focus is requested when `SemanticsAction.focus` is sent by the engine.
2307 lines
81 KiB
Dart
2307 lines
81 KiB
Dart
// 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 'package:flutter/semantics.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'semantics_tester.dart';
|
|
|
|
void main() {
|
|
group('FocusScope', () {
|
|
testWidgets('Can focus', (WidgetTester tester) async {
|
|
final GlobalKey<TestFocusState> key = GlobalKey();
|
|
|
|
await tester.pumpWidget(
|
|
TestFocus(key: key),
|
|
);
|
|
|
|
expect(key.currentState!.focusNode.hasFocus, isFalse);
|
|
|
|
FocusScope.of(key.currentContext!).requestFocus(key.currentState!.focusNode);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(key.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('A FOCUSED'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Can unfocus', (WidgetTester tester) async {
|
|
final GlobalKey<TestFocusState> keyA = GlobalKey();
|
|
final GlobalKey<TestFocusState> keyB = GlobalKey();
|
|
await tester.pumpWidget(
|
|
Column(
|
|
children: <Widget>[
|
|
TestFocus(key: keyA),
|
|
TestFocus(key: keyB, name: 'b'),
|
|
],
|
|
),
|
|
);
|
|
|
|
expect(keyA.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('a'), findsOneWidget);
|
|
expect(keyB.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('b'), findsOneWidget);
|
|
|
|
FocusScope.of(keyA.currentContext!).requestFocus(keyA.currentState!.focusNode);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(keyA.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('A FOCUSED'), findsOneWidget);
|
|
expect(keyB.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('b'), findsOneWidget);
|
|
|
|
// Set focus to the "B" node to unfocus the "A" node.
|
|
FocusScope.of(keyB.currentContext!).requestFocus(keyB.currentState!.focusNode);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(keyA.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('a'), findsOneWidget);
|
|
expect(keyB.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('B FOCUSED'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Autofocus works', (WidgetTester tester) async {
|
|
final GlobalKey<TestFocusState> keyA = GlobalKey();
|
|
final GlobalKey<TestFocusState> keyB = GlobalKey();
|
|
await tester.pumpWidget(
|
|
Column(
|
|
children: <Widget>[
|
|
TestFocus(key: keyA),
|
|
TestFocus(key: keyB, name: 'b', autofocus: true),
|
|
],
|
|
),
|
|
);
|
|
|
|
await tester.pump();
|
|
|
|
expect(keyA.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('a'), findsOneWidget);
|
|
expect(keyB.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('B FOCUSED'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Can have multiple focused children and they update accordingly', (WidgetTester tester) async {
|
|
final GlobalKey<TestFocusState> keyA = GlobalKey();
|
|
final GlobalKey<TestFocusState> keyB = GlobalKey();
|
|
|
|
await tester.pumpWidget(
|
|
Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
key: keyA,
|
|
autofocus: true,
|
|
),
|
|
TestFocus(
|
|
key: keyB,
|
|
name: 'b',
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
// Autofocus is delayed one frame.
|
|
await tester.pump();
|
|
expect(keyA.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('A FOCUSED'), findsOneWidget);
|
|
expect(keyB.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('b'), findsOneWidget);
|
|
await tester.tap(find.text('A FOCUSED'));
|
|
await tester.pump();
|
|
expect(keyA.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('A FOCUSED'), findsOneWidget);
|
|
expect(keyB.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('b'), findsOneWidget);
|
|
await tester.tap(find.text('b'));
|
|
await tester.pump();
|
|
expect(keyA.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('a'), findsOneWidget);
|
|
expect(keyB.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('B FOCUSED'), findsOneWidget);
|
|
await tester.tap(find.text('a'));
|
|
await tester.pump();
|
|
expect(keyA.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('A FOCUSED'), findsOneWidget);
|
|
expect(keyB.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('b'), findsOneWidget);
|
|
});
|
|
|
|
// This moves a focus node first into a focus scope that is added to its
|
|
// parent, and then out of that focus scope again.
|
|
testWidgets('Can move focus in and out of FocusScope', (WidgetTester tester) async {
|
|
final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node');
|
|
addTearDown(parentFocusScope.dispose);
|
|
final FocusScopeNode childFocusScope = FocusScopeNode(debugLabel: 'Child Scope Node');
|
|
addTearDown(childFocusScope.dispose);
|
|
final GlobalKey<TestFocusState> key = GlobalKey();
|
|
|
|
// Initially create the focus inside of the parent FocusScope.
|
|
await tester.pumpWidget(
|
|
FocusScope(
|
|
debugLabel: 'Parent Scope',
|
|
node: parentFocusScope,
|
|
autofocus: true,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
key: key,
|
|
debugLabel: 'Child',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(key.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('a'), findsOneWidget);
|
|
FocusScope.of(key.currentContext!).requestFocus(key.currentState!.focusNode);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(key.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('A FOCUSED'), findsOneWidget);
|
|
|
|
expect(parentFocusScope, hasAGoodToStringDeep);
|
|
expect(
|
|
parentFocusScope.toStringDeep(),
|
|
equalsIgnoringHashCodes(
|
|
'FocusScopeNode#00000(Parent Scope Node [IN FOCUS PATH])\n'
|
|
' │ context: FocusScope\n'
|
|
' │ IN FOCUS PATH\n'
|
|
' │ focusedChildren: FocusNode#00000(Child [PRIMARY FOCUS])\n'
|
|
' │\n'
|
|
' └─Child 1: FocusNode#00000(Child [PRIMARY FOCUS])\n'
|
|
' context: Focus\n'
|
|
' PRIMARY FOCUS\n',
|
|
),
|
|
);
|
|
|
|
expect(FocusManager.instance.rootScope, hasAGoodToStringDeep);
|
|
expect(
|
|
FocusManager.instance.rootScope.toStringDeep(minLevel: DiagnosticLevel.info),
|
|
equalsIgnoringHashCodes(
|
|
'FocusScopeNode#00000(Root Focus Scope [IN FOCUS PATH])\n'
|
|
' │ IN FOCUS PATH\n'
|
|
' │ focusedChildren: FocusScopeNode#00000(View Scope [IN FOCUS PATH])\n'
|
|
' │\n'
|
|
' └─Child 1: _FocusTraversalGroupNode#00000(FocusTraversalGroup [IN FOCUS PATH])\n'
|
|
' │ context: Focus\n'
|
|
' │ NOT FOCUSABLE\n'
|
|
' │ IN FOCUS PATH\n'
|
|
' │\n'
|
|
' └─Child 1: FocusScopeNode#00000(View Scope [IN FOCUS PATH])\n'
|
|
' │ context: _FocusScopeWithExternalFocusNode\n'
|
|
' │ IN FOCUS PATH\n'
|
|
' │ focusedChildren: FocusScopeNode#00000(Parent Scope Node [IN FOCUS\n'
|
|
' │ PATH])\n'
|
|
' │\n'
|
|
' └─Child 1: FocusScopeNode#00000(Parent Scope Node [IN FOCUS PATH])\n'
|
|
' │ context: FocusScope\n'
|
|
' │ IN FOCUS PATH\n'
|
|
' │ focusedChildren: FocusNode#00000(Child [PRIMARY FOCUS])\n'
|
|
' │\n'
|
|
' └─Child 1: FocusNode#00000(Child [PRIMARY FOCUS])\n'
|
|
' context: Focus\n'
|
|
' PRIMARY FOCUS\n'
|
|
),
|
|
);
|
|
|
|
// Add the child focus scope to the focus tree.
|
|
final FocusAttachment childAttachment = childFocusScope.attach(key.currentContext);
|
|
parentFocusScope.setFirstFocus(childFocusScope);
|
|
await tester.pumpAndSettle();
|
|
expect(childFocusScope.isFirstFocus, isTrue);
|
|
|
|
// Now add the child focus scope with no child focusable in it to the tree.
|
|
await tester.pumpWidget(
|
|
FocusScope(
|
|
debugLabel: 'Parent Scope',
|
|
node: parentFocusScope,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
key: key,
|
|
debugLabel: 'Child',
|
|
),
|
|
FocusScope(
|
|
debugLabel: 'Child Scope',
|
|
node: childFocusScope,
|
|
child: Container(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(key.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('a'), findsOneWidget);
|
|
|
|
// Now move the existing focus node into the child focus scope.
|
|
await tester.pumpWidget(
|
|
FocusScope(
|
|
debugLabel: 'Parent Scope',
|
|
node: parentFocusScope,
|
|
child: Column(
|
|
children: <Widget>[
|
|
FocusScope(
|
|
debugLabel: 'Child Scope',
|
|
node: childFocusScope,
|
|
child: TestFocus(
|
|
key: key,
|
|
debugLabel: 'Child',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(key.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('a'), findsOneWidget);
|
|
|
|
// Now remove the child focus scope.
|
|
await tester.pumpWidget(
|
|
FocusScope(
|
|
debugLabel: 'Parent Scope',
|
|
node: parentFocusScope,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
key: key,
|
|
debugLabel: 'Child',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpAndSettle();
|
|
expect(key.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('a'), findsOneWidget);
|
|
|
|
// Must detach the child because we had to attach it in order to call
|
|
// setFirstFocus before adding to the widget.
|
|
childAttachment.detach();
|
|
});
|
|
|
|
testWidgets('Setting first focus requests focus for the scope properly.', (WidgetTester tester) async {
|
|
final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node');
|
|
addTearDown(parentFocusScope.dispose);
|
|
final FocusScopeNode childFocusScope1 = FocusScopeNode(debugLabel: 'Child Scope Node 1');
|
|
addTearDown(childFocusScope1.dispose);
|
|
final FocusScopeNode childFocusScope2 = FocusScopeNode(debugLabel: 'Child Scope Node 2');
|
|
addTearDown(childFocusScope2.dispose);
|
|
final GlobalKey<TestFocusState> keyA = GlobalKey(debugLabel: 'Key A');
|
|
final GlobalKey<TestFocusState> keyB = GlobalKey(debugLabel: 'Key B');
|
|
final GlobalKey<TestFocusState> keyC = GlobalKey(debugLabel: 'Key C');
|
|
|
|
await tester.pumpWidget(
|
|
FocusScope(
|
|
debugLabel: 'Parent Scope',
|
|
node: parentFocusScope,
|
|
child: Column(
|
|
children: <Widget>[
|
|
FocusScope(
|
|
debugLabel: 'Child Scope 1',
|
|
node: childFocusScope1,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
key: keyA,
|
|
autofocus: true,
|
|
debugLabel: 'Child A',
|
|
),
|
|
TestFocus(
|
|
key: keyB,
|
|
name: 'b',
|
|
debugLabel: 'Child B',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
FocusScope(
|
|
debugLabel: 'Child Scope 2',
|
|
node: childFocusScope2,
|
|
child: TestFocus(
|
|
key: keyC,
|
|
name: 'c',
|
|
debugLabel: 'Child C',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(keyA.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('A FOCUSED'), findsOneWidget);
|
|
|
|
parentFocusScope.setFirstFocus(childFocusScope2);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(keyA.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('a'), findsOneWidget);
|
|
|
|
parentFocusScope.setFirstFocus(childFocusScope1);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(keyA.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('A FOCUSED'), findsOneWidget);
|
|
|
|
keyB.currentState!.focusNode.requestFocus();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(keyB.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('B FOCUSED'), findsOneWidget);
|
|
expect(parentFocusScope.isFirstFocus, isTrue);
|
|
expect(childFocusScope1.isFirstFocus, isTrue);
|
|
|
|
parentFocusScope.setFirstFocus(childFocusScope2);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(keyB.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('b'), findsOneWidget);
|
|
expect(parentFocusScope.isFirstFocus, isTrue);
|
|
expect(childFocusScope1.isFirstFocus, isFalse);
|
|
expect(childFocusScope2.isFirstFocus, isTrue);
|
|
|
|
keyC.currentState!.focusNode.requestFocus();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(keyB.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('b'), findsOneWidget);
|
|
expect(keyC.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('C FOCUSED'), findsOneWidget);
|
|
expect(parentFocusScope.isFirstFocus, isTrue);
|
|
expect(childFocusScope1.isFirstFocus, isFalse);
|
|
expect(childFocusScope2.isFirstFocus, isTrue);
|
|
|
|
childFocusScope1.requestFocus();
|
|
await tester.pumpAndSettle();
|
|
expect(keyB.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('B FOCUSED'), findsOneWidget);
|
|
expect(keyC.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('c'), findsOneWidget);
|
|
expect(parentFocusScope.isFirstFocus, isTrue);
|
|
expect(childFocusScope1.isFirstFocus, isTrue);
|
|
expect(childFocusScope2.isFirstFocus, isFalse);
|
|
});
|
|
|
|
testWidgets('Removing focused widget moves focus to next widget', (WidgetTester tester) async {
|
|
final GlobalKey<TestFocusState> keyA = GlobalKey();
|
|
final GlobalKey<TestFocusState> keyB = GlobalKey();
|
|
|
|
await tester.pumpWidget(
|
|
Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
key: keyA,
|
|
),
|
|
TestFocus(
|
|
key: keyB,
|
|
name: 'b',
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
FocusScope.of(keyA.currentContext!).requestFocus(keyA.currentState!.focusNode);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(keyA.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('A FOCUSED'), findsOneWidget);
|
|
expect(keyB.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('b'), findsOneWidget);
|
|
|
|
await tester.pumpWidget(
|
|
Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
key: keyB,
|
|
name: 'b',
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
await tester.pump();
|
|
|
|
expect(keyB.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('b'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Adding a new FocusScope attaches the child to its parent.', (WidgetTester tester) async {
|
|
final GlobalKey<TestFocusState> keyA = GlobalKey();
|
|
final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node');
|
|
addTearDown(parentFocusScope.dispose);
|
|
final FocusScopeNode childFocusScope = FocusScopeNode(debugLabel: 'Child Scope Node');
|
|
addTearDown(childFocusScope.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
FocusScope(
|
|
node: childFocusScope,
|
|
child: TestFocus(
|
|
debugLabel: 'Child',
|
|
key: keyA,
|
|
),
|
|
),
|
|
);
|
|
|
|
FocusScope.of(keyA.currentContext!).requestFocus(keyA.currentState!.focusNode);
|
|
expect(FocusScope.of(keyA.currentContext!), equals(childFocusScope));
|
|
expect(Focus.of(keyA.currentContext!, scopeOk: true), equals(childFocusScope));
|
|
FocusManager.instance.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext!));
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(keyA.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('A FOCUSED'), findsOneWidget);
|
|
expect(childFocusScope.isFirstFocus, isTrue);
|
|
|
|
await tester.pumpWidget(
|
|
FocusScope(
|
|
node: parentFocusScope,
|
|
child: FocusScope(
|
|
node: childFocusScope,
|
|
child: TestFocus(
|
|
debugLabel: 'Child',
|
|
key: keyA,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pump();
|
|
expect(childFocusScope.isFirstFocus, isTrue);
|
|
// Node keeps it's focus when moved to the new scope.
|
|
expect(keyA.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('A FOCUSED'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Setting parentNode determines focus tree hierarchy.', (WidgetTester tester) async {
|
|
final FocusNode topNode = FocusNode(debugLabel: 'Top');
|
|
addTearDown(topNode.dispose);
|
|
final FocusNode parentNode = FocusNode(debugLabel: 'Parent');
|
|
addTearDown(parentNode.dispose);
|
|
final FocusNode childNode = FocusNode(debugLabel: 'Child');
|
|
addTearDown(childNode.dispose);
|
|
final FocusNode insertedNode = FocusNode(debugLabel: 'Inserted');
|
|
addTearDown(insertedNode.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
FocusScope(
|
|
child: Focus.withExternalFocusNode(
|
|
focusNode: topNode,
|
|
child: Column(
|
|
children: <Widget>[
|
|
Focus.withExternalFocusNode(
|
|
focusNode: parentNode,
|
|
child: const SizedBox(),
|
|
),
|
|
Focus.withExternalFocusNode(
|
|
focusNode: childNode,
|
|
parentNode: parentNode,
|
|
autofocus: true,
|
|
child: const SizedBox(),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(childNode.hasPrimaryFocus, isTrue);
|
|
expect(parentNode.hasFocus, isTrue);
|
|
expect(topNode.hasFocus, isTrue);
|
|
|
|
// Check that inserting a Focus in between doesn't reparent the child.
|
|
await tester.pumpWidget(
|
|
FocusScope(
|
|
child: Focus.withExternalFocusNode(
|
|
focusNode: topNode,
|
|
child: Column(
|
|
children: <Widget>[
|
|
Focus.withExternalFocusNode(
|
|
focusNode: parentNode,
|
|
child: const SizedBox(),
|
|
),
|
|
Focus.withExternalFocusNode(
|
|
focusNode: insertedNode,
|
|
child: Focus.withExternalFocusNode(
|
|
focusNode: childNode,
|
|
parentNode: parentNode,
|
|
autofocus: true,
|
|
child: const SizedBox(),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(childNode.hasPrimaryFocus, isTrue);
|
|
expect(parentNode.hasFocus, isTrue);
|
|
expect(topNode.hasFocus, isTrue);
|
|
expect(insertedNode.hasFocus, isFalse);
|
|
});
|
|
|
|
testWidgets('Setting parentNode determines focus scope tree hierarchy.', (WidgetTester tester) async {
|
|
final FocusScopeNode topNode = FocusScopeNode(debugLabel: 'Top');
|
|
addTearDown(topNode.dispose);
|
|
final FocusScopeNode parentNode = FocusScopeNode(debugLabel: 'Parent');
|
|
addTearDown(parentNode.dispose);
|
|
final FocusScopeNode childNode = FocusScopeNode(debugLabel: 'Child');
|
|
addTearDown(childNode.dispose);
|
|
final FocusScopeNode insertedNode = FocusScopeNode(debugLabel: 'Inserted');
|
|
addTearDown(insertedNode.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
FocusScope.withExternalFocusNode(
|
|
focusScopeNode: topNode,
|
|
child: Column(
|
|
children: <Widget>[
|
|
FocusScope.withExternalFocusNode(
|
|
focusScopeNode: parentNode,
|
|
child: const SizedBox(),
|
|
),
|
|
FocusScope.withExternalFocusNode(
|
|
focusScopeNode: childNode,
|
|
parentNode: parentNode,
|
|
child: const Focus(
|
|
autofocus: true,
|
|
child: SizedBox(),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(childNode.hasFocus, isTrue);
|
|
expect(parentNode.hasFocus, isTrue);
|
|
expect(topNode.hasFocus, isTrue);
|
|
|
|
// Check that inserting a Focus in between doesn't reparent the child.
|
|
await tester.pumpWidget(
|
|
FocusScope.withExternalFocusNode(
|
|
focusScopeNode: topNode,
|
|
child: Column(
|
|
children: <Widget>[
|
|
FocusScope.withExternalFocusNode(
|
|
focusScopeNode: parentNode,
|
|
child: const SizedBox(),
|
|
),
|
|
FocusScope.withExternalFocusNode(
|
|
focusScopeNode: insertedNode,
|
|
child: FocusScope.withExternalFocusNode(
|
|
focusScopeNode: childNode,
|
|
parentNode: parentNode,
|
|
child: const Focus(
|
|
autofocus: true,
|
|
child: SizedBox(),
|
|
),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(childNode.hasFocus, isTrue);
|
|
expect(parentNode.hasFocus, isTrue);
|
|
expect(topNode.hasFocus, isTrue);
|
|
expect(insertedNode.hasFocus, isFalse);
|
|
});
|
|
|
|
// Arguably, this isn't correct behavior, but it is what happens now.
|
|
testWidgets("Removing focused widget doesn't move focus to next widget within FocusScope", (WidgetTester tester) async {
|
|
final GlobalKey<TestFocusState> keyA = GlobalKey();
|
|
final GlobalKey<TestFocusState> keyB = GlobalKey();
|
|
final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope');
|
|
addTearDown(parentFocusScope.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
FocusScope(
|
|
debugLabel: 'Parent Scope',
|
|
node: parentFocusScope,
|
|
autofocus: true,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
debugLabel: 'Widget A',
|
|
key: keyA,
|
|
),
|
|
TestFocus(
|
|
debugLabel: 'Widget B',
|
|
key: keyB,
|
|
name: 'b',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
FocusScope.of(keyA.currentContext!).requestFocus(keyA.currentState!.focusNode);
|
|
final FocusScopeNode scope = FocusScope.of(keyA.currentContext!);
|
|
FocusManager.instance.rootScope.setFirstFocus(scope);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(keyA.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('A FOCUSED'), findsOneWidget);
|
|
expect(keyB.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('b'), findsOneWidget);
|
|
|
|
await tester.pumpWidget(
|
|
FocusScope(
|
|
node: parentFocusScope,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
key: keyB,
|
|
name: 'b',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pump();
|
|
|
|
expect(keyB.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('b'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Removing a FocusScope removes its node from the tree', (WidgetTester tester) async {
|
|
final GlobalKey<TestFocusState> keyA = GlobalKey();
|
|
final GlobalKey<TestFocusState> keyB = GlobalKey();
|
|
final GlobalKey<TestFocusState> scopeKeyA = GlobalKey();
|
|
final GlobalKey<TestFocusState> scopeKeyB = GlobalKey();
|
|
final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope');
|
|
addTearDown(parentFocusScope.dispose);
|
|
|
|
// This checks both FocusScopes that have their own nodes, as well as those
|
|
// that use external nodes.
|
|
await tester.pumpWidget(
|
|
FocusTraversalGroup(
|
|
child: Column(
|
|
children: <Widget>[
|
|
FocusScope(
|
|
key: scopeKeyA,
|
|
node: parentFocusScope,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
debugLabel: 'Child A',
|
|
key: keyA,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
FocusScope(
|
|
key: scopeKeyB,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
debugLabel: 'Child B',
|
|
key: keyB,
|
|
name: 'b',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
FocusScope.of(keyB.currentContext!).requestFocus(keyB.currentState!.focusNode);
|
|
FocusScope.of(keyA.currentContext!).requestFocus(keyA.currentState!.focusNode);
|
|
final FocusScopeNode aScope = FocusScope.of(keyA.currentContext!);
|
|
final FocusScopeNode bScope = FocusScope.of(keyB.currentContext!);
|
|
FocusManager.instance.rootScope.setFirstFocus(bScope);
|
|
FocusManager.instance.rootScope.setFirstFocus(aScope);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(FocusScope.of(keyA.currentContext!).isFirstFocus, isTrue);
|
|
expect(keyA.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('A FOCUSED'), findsOneWidget);
|
|
expect(keyB.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('b'), findsOneWidget);
|
|
|
|
expect(FocusManager.instance.rootScope.descendants.length, equals(7));
|
|
await tester.pumpWidget(Container());
|
|
expect(FocusManager.instance.rootScope.descendants.length, equals(2));
|
|
expect(FocusManager.instance.rootScope.descendants, isNot(contains(aScope)));
|
|
expect(FocusManager.instance.rootScope.descendants, isNot(contains(bScope)));
|
|
});
|
|
|
|
// By "pinned", it means kept in the tree by a GlobalKey.
|
|
testWidgets("Removing pinned focused scope doesn't move focus to focused widget within next FocusScope", (WidgetTester tester) async {
|
|
final GlobalKey<TestFocusState> keyA = GlobalKey();
|
|
final GlobalKey<TestFocusState> keyB = GlobalKey();
|
|
final GlobalKey<TestFocusState> scopeKeyA = GlobalKey();
|
|
final GlobalKey<TestFocusState> scopeKeyB = GlobalKey();
|
|
final FocusScopeNode parentFocusScope1 = FocusScopeNode(debugLabel: 'Parent Scope 1');
|
|
addTearDown(parentFocusScope1.dispose);
|
|
final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2');
|
|
addTearDown(parentFocusScope2.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
FocusTraversalGroup(
|
|
child: Column(
|
|
children: <Widget>[
|
|
FocusScope(
|
|
key: scopeKeyA,
|
|
node: parentFocusScope1,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
debugLabel: 'Child A',
|
|
key: keyA,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
FocusScope(
|
|
key: scopeKeyB,
|
|
node: parentFocusScope2,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
debugLabel: 'Child B',
|
|
key: keyB,
|
|
name: 'b',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
FocusScope.of(keyB.currentContext!).requestFocus(keyB.currentState!.focusNode);
|
|
FocusScope.of(keyA.currentContext!).requestFocus(keyA.currentState!.focusNode);
|
|
final FocusScopeNode bScope = FocusScope.of(keyB.currentContext!);
|
|
final FocusScopeNode aScope = FocusScope.of(keyA.currentContext!);
|
|
FocusManager.instance.rootScope.setFirstFocus(bScope);
|
|
FocusManager.instance.rootScope.setFirstFocus(aScope);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(FocusScope.of(keyA.currentContext!).isFirstFocus, isTrue);
|
|
expect(keyA.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('A FOCUSED'), findsOneWidget);
|
|
expect(keyB.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('b'), findsOneWidget);
|
|
|
|
await tester.pumpWidget(
|
|
FocusTraversalGroup(
|
|
child: Column(
|
|
children: <Widget>[
|
|
FocusScope(
|
|
key: scopeKeyB,
|
|
node: parentFocusScope2,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
debugLabel: 'Child B',
|
|
key: keyB,
|
|
name: 'b',
|
|
autofocus: true,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pump();
|
|
|
|
expect(keyB.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('B FOCUSED'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets("Removing unpinned focused scope doesn't move focus to focused widget within next FocusScope", (WidgetTester tester) async {
|
|
final GlobalKey<TestFocusState> keyA = GlobalKey();
|
|
final GlobalKey<TestFocusState> keyB = GlobalKey();
|
|
final FocusScopeNode parentFocusScope1 = FocusScopeNode(debugLabel: 'Parent Scope 1');
|
|
addTearDown(parentFocusScope1.dispose);
|
|
final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2');
|
|
addTearDown(parentFocusScope2.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
FocusTraversalGroup(
|
|
child: Column(
|
|
children: <Widget>[
|
|
FocusScope(
|
|
node: parentFocusScope1,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
debugLabel: 'Child A',
|
|
key: keyA,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
FocusScope(
|
|
node: parentFocusScope2,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
debugLabel: 'Child B',
|
|
key: keyB,
|
|
name: 'b',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
FocusScope.of(keyB.currentContext!).requestFocus(keyB.currentState!.focusNode);
|
|
FocusScope.of(keyA.currentContext!).requestFocus(keyA.currentState!.focusNode);
|
|
final FocusScopeNode bScope = FocusScope.of(keyB.currentContext!);
|
|
final FocusScopeNode aScope = FocusScope.of(keyA.currentContext!);
|
|
FocusManager.instance.rootScope.setFirstFocus(bScope);
|
|
FocusManager.instance.rootScope.setFirstFocus(aScope);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(FocusScope.of(keyA.currentContext!).isFirstFocus, isTrue);
|
|
expect(keyA.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('A FOCUSED'), findsOneWidget);
|
|
expect(keyB.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('b'), findsOneWidget);
|
|
|
|
await tester.pumpWidget(
|
|
FocusTraversalGroup(
|
|
child: Column(
|
|
children: <Widget>[
|
|
FocusScope(
|
|
node: parentFocusScope2,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
debugLabel: 'Child B',
|
|
key: keyB,
|
|
name: 'b',
|
|
autofocus: true,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(keyB.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('B FOCUSED'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Moving widget from one scope to another retains focus', (WidgetTester tester) async {
|
|
final FocusScopeNode parentFocusScope1 = FocusScopeNode();
|
|
addTearDown(parentFocusScope1.dispose);
|
|
final FocusScopeNode parentFocusScope2 = FocusScopeNode();
|
|
addTearDown(parentFocusScope2.dispose);
|
|
final GlobalKey<TestFocusState> keyA = GlobalKey();
|
|
final GlobalKey<TestFocusState> keyB = GlobalKey();
|
|
|
|
await tester.pumpWidget(
|
|
Column(
|
|
children: <Widget>[
|
|
FocusScope(
|
|
node: parentFocusScope1,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
key: keyA,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
FocusScope(
|
|
node: parentFocusScope2,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
key: keyB,
|
|
name: 'b',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
FocusScope.of(keyA.currentContext!).requestFocus(keyA.currentState!.focusNode);
|
|
final FocusScopeNode aScope = FocusScope.of(keyA.currentContext!);
|
|
FocusManager.instance.rootScope.setFirstFocus(aScope);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(keyA.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('A FOCUSED'), findsOneWidget);
|
|
expect(keyB.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('b'), findsOneWidget);
|
|
|
|
await tester.pumpWidget(
|
|
Column(
|
|
children: <Widget>[
|
|
FocusScope(
|
|
node: parentFocusScope1,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
key: keyB,
|
|
name: 'b',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
FocusScope(
|
|
node: parentFocusScope2,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
key: keyA,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
await tester.pump();
|
|
|
|
expect(keyA.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('A FOCUSED'), findsOneWidget);
|
|
expect(keyB.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('b'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Moving FocusScopeNodes retains focus', (WidgetTester tester) async {
|
|
final FocusScopeNode parentFocusScope1 = FocusScopeNode(debugLabel: 'Scope 1');
|
|
addTearDown(parentFocusScope1.dispose);
|
|
final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Scope 2');
|
|
addTearDown(parentFocusScope2.dispose);
|
|
final GlobalKey<TestFocusState> keyA = GlobalKey();
|
|
final GlobalKey<TestFocusState> keyB = GlobalKey();
|
|
|
|
await tester.pumpWidget(
|
|
Column(
|
|
children: <Widget>[
|
|
FocusScope(
|
|
node: parentFocusScope1,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
debugLabel: 'Child A',
|
|
key: keyA,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
FocusScope(
|
|
node: parentFocusScope2,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
debugLabel: 'Child B',
|
|
key: keyB,
|
|
name: 'b',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
FocusScope.of(keyA.currentContext!).requestFocus(keyA.currentState!.focusNode);
|
|
final FocusScopeNode aScope = FocusScope.of(keyA.currentContext!);
|
|
FocusManager.instance.rootScope.setFirstFocus(aScope);
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(keyA.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('A FOCUSED'), findsOneWidget);
|
|
expect(keyB.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('b'), findsOneWidget);
|
|
|
|
// This just swaps the FocusScopeNodes that the FocusScopes have in them.
|
|
await tester.pumpWidget(
|
|
Column(
|
|
children: <Widget>[
|
|
FocusScope(
|
|
node: parentFocusScope2,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
debugLabel: 'Child A',
|
|
key: keyA,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
FocusScope(
|
|
node: parentFocusScope1,
|
|
child: Column(
|
|
children: <Widget>[
|
|
TestFocus(
|
|
debugLabel: 'Child B',
|
|
key: keyB,
|
|
name: 'b',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
await tester.pump();
|
|
|
|
expect(keyA.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(find.text('A FOCUSED'), findsOneWidget);
|
|
expect(keyB.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(find.text('b'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Can focus root node.', (WidgetTester tester) async {
|
|
final GlobalKey key1 = GlobalKey(debugLabel: '1');
|
|
await tester.pumpWidget(
|
|
Focus(
|
|
key: key1,
|
|
child: Container(),
|
|
),
|
|
);
|
|
|
|
final Element firstElement = tester.element(find.byKey(key1));
|
|
final FocusScopeNode rootNode = FocusScope.of(firstElement);
|
|
rootNode.requestFocus();
|
|
|
|
await tester.pump();
|
|
|
|
expect(rootNode.hasFocus, isTrue);
|
|
expect(rootNode, equals(FocusManager.instance.rootScope.descendants.toList()[1]));
|
|
});
|
|
|
|
testWidgets('Can autofocus a node.', (WidgetTester tester) async {
|
|
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
|
|
addTearDown(focusNode.dispose);
|
|
await tester.pumpWidget(
|
|
Focus(
|
|
focusNode: focusNode,
|
|
child: Container(),
|
|
),
|
|
);
|
|
|
|
await tester.pump();
|
|
expect(focusNode.hasPrimaryFocus, isFalse);
|
|
|
|
await tester.pumpWidget(
|
|
Focus(
|
|
autofocus: true,
|
|
focusNode: focusNode,
|
|
child: Container(),
|
|
),
|
|
);
|
|
|
|
await tester.pump();
|
|
expect(focusNode.hasPrimaryFocus, isTrue);
|
|
});
|
|
|
|
testWidgets("Won't autofocus a node if one is already focused.", (WidgetTester tester) async {
|
|
final FocusNode focusNodeA = FocusNode(debugLabel: 'Test Node A');
|
|
addTearDown(focusNodeA.dispose);
|
|
final FocusNode focusNodeB = FocusNode(debugLabel: 'Test Node B');
|
|
addTearDown(focusNodeB.dispose);
|
|
await tester.pumpWidget(
|
|
Column(
|
|
children: <Widget>[
|
|
Focus(
|
|
focusNode: focusNodeA,
|
|
autofocus: true,
|
|
child: Container(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
await tester.pump();
|
|
expect(focusNodeA.hasPrimaryFocus, isTrue);
|
|
|
|
await tester.pumpWidget(
|
|
Column(
|
|
children: <Widget>[
|
|
Focus(
|
|
focusNode: focusNodeA,
|
|
child: Container(),
|
|
),
|
|
Focus(
|
|
focusNode: focusNodeB,
|
|
autofocus: true,
|
|
child: Container(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
await tester.pump();
|
|
expect(focusNodeB.hasPrimaryFocus, isFalse);
|
|
expect(focusNodeA.hasPrimaryFocus, isTrue);
|
|
});
|
|
|
|
testWidgets("FocusScope doesn't update the focusNode attributes when the widget updates if withExternalFocusNode is used", (WidgetTester tester) async {
|
|
final GlobalKey key1 = GlobalKey(debugLabel: '1');
|
|
final FocusScopeNode focusScopeNode = FocusScopeNode();
|
|
addTearDown(focusScopeNode.dispose);
|
|
bool? keyEventHandled;
|
|
KeyEventResult handleCallback(FocusNode node, RawKeyEvent event) {
|
|
keyEventHandled = true;
|
|
return KeyEventResult.handled;
|
|
}
|
|
KeyEventResult handleEventCallback(FocusNode node, KeyEvent event) {
|
|
keyEventHandled = true;
|
|
return KeyEventResult.handled;
|
|
}
|
|
KeyEventResult ignoreCallback(FocusNode node, RawKeyEvent event) => KeyEventResult.ignored;
|
|
KeyEventResult ignoreEventCallback(FocusNode node, KeyEvent event) => KeyEventResult.ignored;
|
|
focusScopeNode.onKey = ignoreCallback;
|
|
focusScopeNode.onKeyEvent = ignoreEventCallback;
|
|
focusScopeNode.descendantsAreFocusable = false;
|
|
focusScopeNode.descendantsAreTraversable = false;
|
|
focusScopeNode.skipTraversal = false;
|
|
focusScopeNode.canRequestFocus = true;
|
|
FocusScope focusScopeWidget = FocusScope.withExternalFocusNode(
|
|
focusScopeNode: focusScopeNode,
|
|
child: Container(key: key1),
|
|
);
|
|
await tester.pumpWidget(focusScopeWidget);
|
|
expect(focusScopeNode.onKey, equals(ignoreCallback));
|
|
expect(focusScopeNode.onKeyEvent, equals(ignoreEventCallback));
|
|
expect(focusScopeNode.descendantsAreFocusable, isFalse);
|
|
expect(focusScopeNode.descendantsAreTraversable, isFalse);
|
|
expect(focusScopeNode.skipTraversal, isFalse);
|
|
expect(focusScopeNode.canRequestFocus, isTrue);
|
|
expect(focusScopeWidget.onKey, equals(focusScopeNode.onKey));
|
|
expect(focusScopeWidget.onKeyEvent, equals(focusScopeNode.onKeyEvent));
|
|
expect(focusScopeWidget.descendantsAreFocusable, equals(focusScopeNode.descendantsAreFocusable));
|
|
expect(focusScopeWidget.descendantsAreTraversable, equals(focusScopeNode.descendantsAreTraversable));
|
|
expect(focusScopeWidget.skipTraversal, equals(focusScopeNode.skipTraversal));
|
|
expect(focusScopeWidget.canRequestFocus, equals(focusScopeNode.canRequestFocus));
|
|
|
|
FocusScope.of(key1.currentContext!).requestFocus();
|
|
await tester.pump();
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
|
expect(keyEventHandled, isNull);
|
|
|
|
focusScopeNode.onKey = handleCallback;
|
|
focusScopeNode.onKeyEvent = handleEventCallback;
|
|
focusScopeNode.descendantsAreFocusable = true;
|
|
focusScopeNode.descendantsAreTraversable = true;
|
|
focusScopeWidget = FocusScope.withExternalFocusNode(
|
|
focusScopeNode: focusScopeNode,
|
|
child: Container(key: key1),
|
|
);
|
|
await tester.pumpWidget(focusScopeWidget);
|
|
expect(focusScopeNode.onKey, equals(handleCallback));
|
|
expect(focusScopeNode.onKeyEvent, equals(handleEventCallback));
|
|
expect(focusScopeNode.descendantsAreFocusable, isTrue);
|
|
expect(focusScopeNode.descendantsAreTraversable, isTrue);
|
|
expect(focusScopeNode.skipTraversal, isFalse);
|
|
expect(focusScopeNode.canRequestFocus, isTrue);
|
|
expect(focusScopeWidget.onKey, equals(focusScopeNode.onKey));
|
|
expect(focusScopeWidget.onKeyEvent, equals(focusScopeNode.onKeyEvent));
|
|
expect(focusScopeWidget.descendantsAreFocusable, equals(focusScopeNode.descendantsAreFocusable));
|
|
expect(focusScopeWidget.descendantsAreTraversable, equals(focusScopeNode.descendantsAreTraversable));
|
|
expect(focusScopeWidget.skipTraversal, equals(focusScopeNode.skipTraversal));
|
|
expect(focusScopeWidget.canRequestFocus, equals(focusScopeNode.canRequestFocus));
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
|
expect(keyEventHandled, isTrue);
|
|
});
|
|
});
|
|
|
|
group('Focus', () {
|
|
testWidgets('Focus.of stops at the nearest Focus widget.', (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');
|
|
final FocusScopeNode scopeNode = FocusScopeNode();
|
|
addTearDown(scopeNode.dispose);
|
|
await tester.pumpWidget(
|
|
FocusScope(
|
|
key: key1,
|
|
node: scopeNode,
|
|
debugLabel: 'Key 1',
|
|
child: Container(
|
|
key: key2,
|
|
child: Focus(
|
|
debugLabel: 'Key 3',
|
|
key: key3,
|
|
child: Container(
|
|
key: key4,
|
|
child: Focus(
|
|
debugLabel: 'Key 5',
|
|
key: key5,
|
|
child: Container(
|
|
key: key6,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
final Element element1 = tester.element(find.byKey(key1));
|
|
final Element element2 = tester.element(find.byKey(key2));
|
|
final Element element3 = tester.element(find.byKey(key3));
|
|
final Element element4 = tester.element(find.byKey(key4));
|
|
final Element element5 = tester.element(find.byKey(key5));
|
|
final Element element6 = tester.element(find.byKey(key6));
|
|
final FocusNode root = element1.owner!.focusManager.rootScope;
|
|
|
|
expect(Focus.maybeOf(element1), isNull);
|
|
expect(Focus.maybeOf(element2), isNull);
|
|
expect(Focus.maybeOf(element3), isNull);
|
|
expect(Focus.of(element4).parent!.parent!.parent!.parent, equals(root));
|
|
expect(Focus.of(element5).parent!.parent!.parent!.parent, equals(root));
|
|
expect(Focus.of(element6).parent!.parent!.parent!.parent!.parent, equals(root));
|
|
});
|
|
testWidgets('Can traverse Focus children.', (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');
|
|
final GlobalKey key7 = GlobalKey(debugLabel: '7');
|
|
final GlobalKey key8 = GlobalKey(debugLabel: '8');
|
|
await tester.pumpWidget(
|
|
Focus(
|
|
child: Column(
|
|
key: key1,
|
|
children: <Widget>[
|
|
Focus(
|
|
key: key2,
|
|
child: Focus(
|
|
key: key3,
|
|
child: Container(),
|
|
),
|
|
),
|
|
Focus(
|
|
key: key4,
|
|
child: Focus(
|
|
key: key5,
|
|
child: Container(),
|
|
),
|
|
),
|
|
Focus(
|
|
key: key6,
|
|
child: Column(
|
|
children: <Widget>[
|
|
Focus(
|
|
key: key7,
|
|
child: Container(),
|
|
),
|
|
Focus(
|
|
key: key8,
|
|
child: Container(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
final Element firstScope = tester.element(find.byKey(key1));
|
|
final List<FocusNode> nodes = <FocusNode>[];
|
|
final List<Key> keys = <Key>[];
|
|
bool visitor(FocusNode node) {
|
|
nodes.add(node);
|
|
keys.add(node.context!.widget.key!);
|
|
return true;
|
|
}
|
|
|
|
await tester.pump();
|
|
|
|
Focus.of(firstScope).descendants.forEach(visitor);
|
|
expect(nodes.length, equals(7));
|
|
expect(keys.length, equals(7));
|
|
// Depth first.
|
|
expect(keys, equals(<Key>[key3, key2, key5, key4, key7, key8, key6]));
|
|
|
|
// Just traverses a sub-tree.
|
|
final Element secondScope = tester.element(find.byKey(key7));
|
|
nodes.clear();
|
|
keys.clear();
|
|
Focus.of(secondScope).descendants.forEach(visitor);
|
|
expect(nodes.length, equals(2));
|
|
expect(keys, equals(<Key>[key7, key8]));
|
|
});
|
|
|
|
testWidgets('Can set focus.', (WidgetTester tester) async {
|
|
final GlobalKey key1 = GlobalKey(debugLabel: '1');
|
|
late bool gotFocus;
|
|
await tester.pumpWidget(
|
|
Focus(
|
|
onFocusChange: (bool focused) => gotFocus = focused,
|
|
child: Container(key: key1),
|
|
),
|
|
);
|
|
|
|
final Element firstNode = tester.element(find.byKey(key1));
|
|
final FocusNode node = Focus.of(firstNode);
|
|
node.requestFocus();
|
|
|
|
await tester.pump();
|
|
|
|
expect(gotFocus, isTrue);
|
|
expect(node.hasFocus, isTrue);
|
|
});
|
|
|
|
testWidgets('Focus is ignored when set to not focusable.', (WidgetTester tester) async {
|
|
final GlobalKey key1 = GlobalKey(debugLabel: '1');
|
|
bool? gotFocus;
|
|
await tester.pumpWidget(
|
|
Focus(
|
|
canRequestFocus: false,
|
|
onFocusChange: (bool focused) => gotFocus = focused,
|
|
child: Container(key: key1),
|
|
),
|
|
);
|
|
|
|
final Element firstNode = tester.element(find.byKey(key1));
|
|
final FocusNode node = Focus.of(firstNode);
|
|
node.requestFocus();
|
|
|
|
await tester.pump();
|
|
|
|
expect(gotFocus, isNull);
|
|
expect(node.hasFocus, isFalse);
|
|
});
|
|
|
|
testWidgets('Focus is lost when set to not focusable.', (WidgetTester tester) async {
|
|
final GlobalKey key1 = GlobalKey(debugLabel: '1');
|
|
bool? gotFocus;
|
|
await tester.pumpWidget(
|
|
Focus(
|
|
autofocus: true,
|
|
canRequestFocus: true,
|
|
onFocusChange: (bool focused) => gotFocus = focused,
|
|
child: Container(key: key1),
|
|
),
|
|
);
|
|
|
|
Element firstNode = tester.element(find.byKey(key1));
|
|
FocusNode node = Focus.of(firstNode);
|
|
node.requestFocus();
|
|
|
|
await tester.pump();
|
|
|
|
expect(gotFocus, isTrue);
|
|
expect(node.hasFocus, isTrue);
|
|
|
|
gotFocus = null;
|
|
await tester.pumpWidget(
|
|
Focus(
|
|
canRequestFocus: false,
|
|
onFocusChange: (bool focused) => gotFocus = focused,
|
|
child: Container(key: key1),
|
|
),
|
|
);
|
|
|
|
firstNode = tester.element(find.byKey(key1));
|
|
node = Focus.of(firstNode);
|
|
node.requestFocus();
|
|
|
|
await tester.pump();
|
|
|
|
expect(gotFocus, false);
|
|
expect(node.hasFocus, isFalse);
|
|
});
|
|
|
|
testWidgets('Child of unfocusable Focus can get focus.', (WidgetTester tester) async {
|
|
final GlobalKey key1 = GlobalKey(debugLabel: '1');
|
|
final GlobalKey key2 = GlobalKey(debugLabel: '2');
|
|
final FocusNode focusNode = FocusNode();
|
|
addTearDown(focusNode.dispose);
|
|
bool? gotFocus;
|
|
await tester.pumpWidget(
|
|
Focus(
|
|
canRequestFocus: false,
|
|
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);
|
|
unfocusableNode.requestFocus();
|
|
|
|
await tester.pump();
|
|
|
|
expect(gotFocus, isNull);
|
|
expect(unfocusableNode.hasFocus, isFalse);
|
|
|
|
final Element containerWidget = tester.element(find.byKey(key2));
|
|
final FocusNode focusableNode = Focus.of(containerWidget);
|
|
focusableNode.requestFocus();
|
|
|
|
await tester.pump();
|
|
|
|
expect(gotFocus, isTrue);
|
|
expect(unfocusableNode.hasFocus, isTrue);
|
|
});
|
|
|
|
testWidgets('Nodes are removed when all Focuses are removed.', (WidgetTester tester) async {
|
|
final GlobalKey key1 = GlobalKey(debugLabel: '1');
|
|
late bool gotFocus;
|
|
await tester.pumpWidget(
|
|
FocusScope(
|
|
child: Focus(
|
|
onFocusChange: (bool focused) => gotFocus = focused,
|
|
child: Container(key: key1),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Element firstNode = tester.element(find.byKey(key1));
|
|
final FocusNode node = Focus.of(firstNode);
|
|
node.requestFocus();
|
|
|
|
await tester.pump();
|
|
|
|
expect(gotFocus, isTrue);
|
|
expect(node.hasFocus, isTrue);
|
|
|
|
await tester.pumpWidget(Container());
|
|
// Even with no other focusable widgets, there will be the top level focus
|
|
// traversal and view focus nodes.
|
|
expect(FocusManager.instance.rootScope.descendants, hasLength(2));
|
|
});
|
|
|
|
testWidgets('Focus widgets set Semantics information about focus', (WidgetTester tester) async {
|
|
final GlobalKey<TestFocusState> key = GlobalKey();
|
|
|
|
await tester.pumpWidget(
|
|
TestFocus(key: key),
|
|
);
|
|
|
|
final SemanticsNode semantics = tester.getSemantics(find.byKey(key));
|
|
|
|
expect(key.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(semantics.hasFlag(SemanticsFlag.isFocused), isFalse);
|
|
expect(semantics.hasFlag(SemanticsFlag.isFocusable), isTrue);
|
|
|
|
FocusScope.of(key.currentContext!).requestFocus(key.currentState!.focusNode);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(key.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(semantics.hasFlag(SemanticsFlag.isFocused), isTrue);
|
|
expect(semantics.hasFlag(SemanticsFlag.isFocusable), isTrue);
|
|
|
|
key.currentState!.focusNode.canRequestFocus = false;
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(key.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(key.currentState!.focusNode.canRequestFocus, isFalse);
|
|
expect(semantics.hasFlag(SemanticsFlag.isFocused), isFalse);
|
|
expect(semantics.hasFlag(SemanticsFlag.isFocusable), isFalse);
|
|
});
|
|
|
|
testWidgets('Setting canRequestFocus on focus node causes update.', (WidgetTester tester) async {
|
|
final GlobalKey<TestFocusState> key = GlobalKey();
|
|
|
|
final TestFocus testFocus = TestFocus(key: key);
|
|
await tester.pumpWidget(
|
|
testFocus,
|
|
);
|
|
|
|
await tester.pumpAndSettle();
|
|
key.currentState!.built = false;
|
|
key.currentState!.focusNode.canRequestFocus = false;
|
|
await tester.pumpAndSettle();
|
|
key.currentState!.built = true;
|
|
|
|
expect(key.currentState!.focusNode.canRequestFocus, isFalse);
|
|
});
|
|
|
|
testWidgets('canRequestFocus causes descendants of scope to be skipped.', (WidgetTester tester) async {
|
|
final GlobalKey scope1 = GlobalKey(debugLabel: 'scope1');
|
|
final GlobalKey scope2 = GlobalKey(debugLabel: 'scope2');
|
|
final GlobalKey focus1 = GlobalKey(debugLabel: 'focus1');
|
|
final GlobalKey focus2 = GlobalKey(debugLabel: 'focus2');
|
|
final GlobalKey container1 = GlobalKey(debugLabel: 'container');
|
|
Future<void> pumpTest({
|
|
bool allowScope1 = true,
|
|
bool allowScope2 = true,
|
|
bool allowFocus1 = true,
|
|
bool allowFocus2 = true,
|
|
}) async {
|
|
await tester.pumpWidget(
|
|
FocusScope(
|
|
key: scope1,
|
|
canRequestFocus: allowScope1,
|
|
child: FocusScope(
|
|
key: scope2,
|
|
canRequestFocus: allowScope2,
|
|
child: Focus(
|
|
key: focus1,
|
|
canRequestFocus: allowFocus1,
|
|
child: Focus(
|
|
key: focus2,
|
|
canRequestFocus: allowFocus2,
|
|
child: Container(
|
|
key: container1,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
}
|
|
|
|
// Check childless node (focus2).
|
|
await pumpTest();
|
|
Focus.of(container1.currentContext!).requestFocus();
|
|
await tester.pump();
|
|
expect(Focus.of(container1.currentContext!).hasFocus, isTrue);
|
|
await pumpTest(allowFocus2: false);
|
|
expect(Focus.of(container1.currentContext!).hasFocus, isFalse);
|
|
Focus.of(container1.currentContext!).requestFocus();
|
|
await tester.pump();
|
|
expect(Focus.of(container1.currentContext!).hasFocus, isFalse);
|
|
await pumpTest();
|
|
Focus.of(container1.currentContext!).requestFocus();
|
|
await tester.pump();
|
|
expect(Focus.of(container1.currentContext!).hasFocus, isTrue);
|
|
|
|
// Check FocusNode with child (focus1). Shouldn't affect children.
|
|
await pumpTest(allowFocus1: false);
|
|
expect(Focus.of(container1.currentContext!).hasFocus, isTrue); // focus2 has focus.
|
|
Focus.of(focus2.currentContext!).requestFocus(); // Try to focus focus1
|
|
await tester.pump();
|
|
expect(Focus.of(container1.currentContext!).hasFocus, isTrue); // focus2 still has focus.
|
|
Focus.of(container1.currentContext!).requestFocus(); // Now try to focus focus2
|
|
await tester.pump();
|
|
expect(Focus.of(container1.currentContext!).hasFocus, isTrue);
|
|
await pumpTest();
|
|
// Try again, now that we've set focus1's canRequestFocus to true again.
|
|
Focus.of(container1.currentContext!).unfocus();
|
|
await tester.pump();
|
|
expect(Focus.of(container1.currentContext!).hasFocus, isFalse);
|
|
Focus.of(container1.currentContext!).requestFocus();
|
|
await tester.pump();
|
|
expect(Focus.of(container1.currentContext!).hasFocus, isTrue);
|
|
|
|
// Check FocusScopeNode with only FocusNode children (scope2). Should affect children.
|
|
await pumpTest(allowScope2: false);
|
|
expect(Focus.of(container1.currentContext!).hasFocus, isFalse);
|
|
FocusScope.of(focus1.currentContext!).requestFocus(); // Try to focus scope2
|
|
await tester.pump();
|
|
expect(Focus.of(container1.currentContext!).hasFocus, isFalse);
|
|
Focus.of(focus2.currentContext!).requestFocus(); // Try to focus focus1
|
|
await tester.pump();
|
|
expect(Focus.of(container1.currentContext!).hasFocus, isFalse);
|
|
Focus.of(container1.currentContext!).requestFocus(); // Try to focus focus2
|
|
await tester.pump();
|
|
expect(Focus.of(container1.currentContext!).hasFocus, isFalse);
|
|
await pumpTest();
|
|
// Try again, now that we've set scope2's canRequestFocus to true again.
|
|
Focus.of(container1.currentContext!).requestFocus();
|
|
await tester.pump();
|
|
expect(Focus.of(container1.currentContext!).hasFocus, isTrue);
|
|
|
|
// Check FocusScopeNode with both FocusNode children and FocusScope children (scope1). Should affect children.
|
|
await pumpTest(allowScope1: false);
|
|
expect(Focus.of(container1.currentContext!).hasFocus, isFalse);
|
|
FocusScope.of(scope2.currentContext!).requestFocus(); // Try to focus scope1
|
|
await tester.pump();
|
|
expect(Focus.of(container1.currentContext!).hasFocus, isFalse);
|
|
FocusScope.of(focus1.currentContext!).requestFocus(); // Try to focus scope2
|
|
await tester.pump();
|
|
expect(Focus.of(container1.currentContext!).hasFocus, isFalse);
|
|
Focus.of(focus2.currentContext!).requestFocus(); // Try to focus focus1
|
|
await tester.pump();
|
|
expect(Focus.of(container1.currentContext!).hasFocus, isFalse);
|
|
Focus.of(container1.currentContext!).requestFocus(); // Try to focus focus2
|
|
await tester.pump();
|
|
expect(Focus.of(container1.currentContext!).hasFocus, isFalse);
|
|
await pumpTest();
|
|
// Try again, now that we've set scope1's canRequestFocus to true again.
|
|
Focus.of(container1.currentContext!).requestFocus();
|
|
await tester.pump();
|
|
expect(Focus.of(container1.currentContext!).hasFocus, isTrue);
|
|
});
|
|
|
|
testWidgets('skipTraversal works as expected.', (WidgetTester tester) async {
|
|
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1');
|
|
addTearDown(scope1.dispose);
|
|
final FocusScopeNode scope2 = FocusScopeNode(debugLabel: 'scope2');
|
|
addTearDown(scope2.dispose);
|
|
final FocusNode focus1 = FocusNode(debugLabel: 'focus1');
|
|
addTearDown(focus1.dispose);
|
|
final FocusNode focus2 = FocusNode(debugLabel: 'focus2');
|
|
addTearDown(focus2.dispose);
|
|
|
|
Future<void> pumpTest({
|
|
bool traverseScope1 = false,
|
|
bool traverseScope2 = false,
|
|
bool traverseFocus1 = false,
|
|
bool traverseFocus2 = false,
|
|
}) async {
|
|
await tester.pumpWidget(
|
|
FocusScope(
|
|
node: scope1,
|
|
skipTraversal: traverseScope1,
|
|
child: FocusScope(
|
|
node: scope2,
|
|
skipTraversal: traverseScope2,
|
|
child: Focus(
|
|
focusNode: focus1,
|
|
skipTraversal: traverseFocus1,
|
|
child: Focus(
|
|
focusNode: focus2,
|
|
skipTraversal: traverseFocus2,
|
|
child: Container(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
}
|
|
|
|
await pumpTest();
|
|
expect(scope1.traversalDescendants, equals(<FocusNode>[focus2, focus1, scope2]));
|
|
|
|
// Check childless node (focus2).
|
|
await pumpTest(traverseFocus2: true);
|
|
expect(scope1.traversalDescendants, equals(<FocusNode>[focus1, scope2]));
|
|
|
|
// Check FocusNode with child (focus1). Shouldn't affect children.
|
|
await pumpTest(traverseFocus1: true);
|
|
expect(scope1.traversalDescendants, equals(<FocusNode>[focus2, scope2]));
|
|
|
|
// Check FocusScopeNode with only FocusNode children (scope2). Should affect children.
|
|
await pumpTest(traverseScope2: true);
|
|
expect(scope1.traversalDescendants, equals(<FocusNode>[focus2, focus1]));
|
|
|
|
// Check FocusScopeNode with both FocusNode children and FocusScope children (scope1). Should affect children.
|
|
await pumpTest(traverseScope1: true);
|
|
expect(scope1.traversalDescendants, equals(<FocusNode>[focus2, focus1, scope2]));
|
|
});
|
|
|
|
testWidgets('descendantsAreFocusable works as expected.', (WidgetTester tester) async {
|
|
final GlobalKey key1 = GlobalKey(debugLabel: '1');
|
|
final GlobalKey key2 = GlobalKey(debugLabel: '2');
|
|
final FocusNode focusNode = FocusNode();
|
|
addTearDown(focusNode.dispose);
|
|
bool? gotFocus;
|
|
await tester.pumpWidget(
|
|
Focus(
|
|
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('descendantsAreTraversable works as expected.', (WidgetTester tester) async {
|
|
final FocusScopeNode scopeNode = FocusScopeNode(debugLabel: 'scope');
|
|
addTearDown(scopeNode.dispose);
|
|
final FocusNode node1 = FocusNode(debugLabel: 'node 1');
|
|
addTearDown(node1.dispose);
|
|
final FocusNode node2 = FocusNode(debugLabel: 'node 2');
|
|
addTearDown(node2.dispose);
|
|
final FocusNode node3 = FocusNode(debugLabel: 'node 3');
|
|
addTearDown(node3.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
FocusScope(
|
|
node: scopeNode,
|
|
child: Column(
|
|
children: <Widget>[
|
|
Focus(
|
|
focusNode: node1,
|
|
child: Container(),
|
|
),
|
|
Focus(
|
|
focusNode: node2,
|
|
descendantsAreTraversable: false,
|
|
child: Focus(
|
|
focusNode: node3,
|
|
child: Container(),
|
|
)
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(scopeNode.traversalDescendants, equals(<FocusNode>[node1, node2]));
|
|
expect(node2.traversalDescendants, equals(<FocusNode>[]));
|
|
});
|
|
|
|
testWidgets("Focus doesn't introduce a Semantics node when includeSemantics is false", (WidgetTester tester) async {
|
|
final SemanticsTester semantics = SemanticsTester(tester);
|
|
await tester.pumpWidget(Focus(includeSemantics: false, child: Container()));
|
|
final TestSemantics expectedSemantics = TestSemantics.root();
|
|
expect(semantics, hasSemantics(expectedSemantics));
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('Focus updates the onKey handler when the widget updates', (WidgetTester tester) async {
|
|
final GlobalKey key1 = GlobalKey(debugLabel: '1');
|
|
final FocusNode focusNode = FocusNode();
|
|
addTearDown(focusNode.dispose);
|
|
bool? keyEventHandled;
|
|
KeyEventResult handleCallback(FocusNode node, RawKeyEvent event) {
|
|
keyEventHandled = true;
|
|
return KeyEventResult.handled;
|
|
}
|
|
KeyEventResult ignoreCallback(FocusNode node, RawKeyEvent event) => KeyEventResult.ignored;
|
|
Focus focusWidget = Focus(
|
|
onKey: ignoreCallback, // This one does nothing.
|
|
focusNode: focusNode,
|
|
skipTraversal: true,
|
|
canRequestFocus: true,
|
|
child: Container(key: key1),
|
|
);
|
|
focusNode.onKeyEvent = null;
|
|
await tester.pumpWidget(focusWidget);
|
|
expect(focusNode.onKey, equals(ignoreCallback));
|
|
expect(focusWidget.onKey, equals(focusNode.onKey));
|
|
expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent));
|
|
expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable));
|
|
expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal));
|
|
expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus));
|
|
|
|
Focus.of(key1.currentContext!).requestFocus();
|
|
await tester.pump();
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
|
expect(keyEventHandled, isNull);
|
|
|
|
focusWidget = Focus(
|
|
onKey: handleCallback,
|
|
focusNode: focusNode,
|
|
skipTraversal: true,
|
|
canRequestFocus: true,
|
|
child: Container(key: key1),
|
|
);
|
|
await tester.pumpWidget(focusWidget);
|
|
expect(focusNode.onKey, equals(handleCallback));
|
|
expect(focusWidget.onKey, equals(focusNode.onKey));
|
|
expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent));
|
|
expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable));
|
|
expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal));
|
|
expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus));
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
|
expect(keyEventHandled, isTrue);
|
|
});
|
|
|
|
testWidgets('Focus updates the onKeyEvent handler when the widget updates', (WidgetTester tester) async {
|
|
final GlobalKey key1 = GlobalKey(debugLabel: '1');
|
|
final FocusNode focusNode = FocusNode();
|
|
addTearDown(focusNode.dispose);
|
|
bool? keyEventHandled;
|
|
KeyEventResult handleEventCallback(FocusNode node, KeyEvent event) {
|
|
keyEventHandled = true;
|
|
return KeyEventResult.handled;
|
|
}
|
|
KeyEventResult ignoreEventCallback(FocusNode node, KeyEvent event) => KeyEventResult.ignored;
|
|
Focus focusWidget = Focus(
|
|
onKeyEvent: ignoreEventCallback, // This one does nothing.
|
|
focusNode: focusNode,
|
|
skipTraversal: true,
|
|
canRequestFocus: true,
|
|
child: Container(key: key1),
|
|
);
|
|
focusNode.onKeyEvent = null;
|
|
await tester.pumpWidget(focusWidget);
|
|
expect(focusNode.onKeyEvent, equals(ignoreEventCallback));
|
|
expect(focusWidget.onKey, equals(focusNode.onKey));
|
|
expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent));
|
|
expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable));
|
|
expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal));
|
|
expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus));
|
|
|
|
Focus.of(key1.currentContext!).requestFocus();
|
|
await tester.pump();
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
|
expect(keyEventHandled, isNull);
|
|
|
|
focusWidget = Focus(
|
|
onKeyEvent: handleEventCallback,
|
|
focusNode: focusNode,
|
|
skipTraversal: true,
|
|
canRequestFocus: true,
|
|
child: Container(key: key1),
|
|
);
|
|
await tester.pumpWidget(focusWidget);
|
|
expect(focusNode.onKeyEvent, equals(handleEventCallback));
|
|
expect(focusWidget.onKey, equals(focusNode.onKey));
|
|
expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent));
|
|
expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable));
|
|
expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal));
|
|
expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus));
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
|
expect(keyEventHandled, isTrue);
|
|
});
|
|
|
|
testWidgets("Focus doesn't update the focusNode attributes when the widget updates if withExternalFocusNode is used", (WidgetTester tester) async {
|
|
final GlobalKey key1 = GlobalKey(debugLabel: '1');
|
|
final FocusNode focusNode = FocusNode();
|
|
addTearDown(focusNode.dispose);
|
|
bool? keyEventHandled;
|
|
KeyEventResult handleCallback(FocusNode node, RawKeyEvent event) {
|
|
keyEventHandled = true;
|
|
return KeyEventResult.handled;
|
|
}
|
|
KeyEventResult handleEventCallback(FocusNode node, KeyEvent event) {
|
|
keyEventHandled = true;
|
|
return KeyEventResult.handled;
|
|
}
|
|
KeyEventResult ignoreCallback(FocusNode node, RawKeyEvent event) => KeyEventResult.ignored;
|
|
KeyEventResult ignoreEventCallback(FocusNode node, KeyEvent event) => KeyEventResult.ignored;
|
|
focusNode.onKey = ignoreCallback;
|
|
focusNode.onKeyEvent = ignoreEventCallback;
|
|
focusNode.descendantsAreFocusable = false;
|
|
focusNode.descendantsAreTraversable = false;
|
|
focusNode.skipTraversal = false;
|
|
focusNode.canRequestFocus = true;
|
|
Focus focusWidget = Focus.withExternalFocusNode(
|
|
focusNode: focusNode,
|
|
child: Container(key: key1),
|
|
);
|
|
await tester.pumpWidget(focusWidget);
|
|
expect(focusNode.onKey, equals(ignoreCallback));
|
|
expect(focusNode.onKeyEvent, equals(ignoreEventCallback));
|
|
expect(focusNode.descendantsAreFocusable, isFalse);
|
|
expect(focusNode.descendantsAreTraversable, isFalse);
|
|
expect(focusNode.skipTraversal, isFalse);
|
|
expect(focusNode.canRequestFocus, isTrue);
|
|
expect(focusWidget.onKey, equals(focusNode.onKey));
|
|
expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent));
|
|
expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable));
|
|
expect(focusWidget.descendantsAreTraversable, equals(focusNode.descendantsAreTraversable));
|
|
expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal));
|
|
expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus));
|
|
|
|
Focus.of(key1.currentContext!).requestFocus();
|
|
await tester.pump();
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
|
expect(keyEventHandled, isNull);
|
|
|
|
focusNode.onKey = handleCallback;
|
|
focusNode.onKeyEvent = handleEventCallback;
|
|
focusNode.descendantsAreFocusable = true;
|
|
focusNode.descendantsAreTraversable = true;
|
|
focusWidget = Focus.withExternalFocusNode(
|
|
focusNode: focusNode,
|
|
child: Container(key: key1),
|
|
);
|
|
await tester.pumpWidget(focusWidget);
|
|
expect(focusNode.onKey, equals(handleCallback));
|
|
expect(focusNode.onKeyEvent, equals(handleEventCallback));
|
|
expect(focusNode.descendantsAreFocusable, isTrue);
|
|
expect(focusNode.descendantsAreTraversable, isTrue);
|
|
expect(focusNode.skipTraversal, isFalse);
|
|
expect(focusNode.canRequestFocus, isTrue);
|
|
expect(focusWidget.onKey, equals(focusNode.onKey));
|
|
expect(focusWidget.onKeyEvent, equals(focusNode.onKeyEvent));
|
|
expect(focusWidget.descendantsAreFocusable, equals(focusNode.descendantsAreFocusable));
|
|
expect(focusWidget.descendantsAreTraversable, equals(focusNode.descendantsAreTraversable));
|
|
expect(focusWidget.skipTraversal, equals(focusNode.skipTraversal));
|
|
expect(focusWidget.canRequestFocus, equals(focusNode.canRequestFocus));
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
|
expect(keyEventHandled, isTrue);
|
|
});
|
|
|
|
testWidgets('Focus does not update the focusNode attributes when the widget updates if withExternalFocusNode is used 2', (WidgetTester tester) async {
|
|
final TestExternalFocusNode focusNode = TestExternalFocusNode();
|
|
assert(!focusNode.isModified);
|
|
addTearDown(focusNode.dispose);
|
|
|
|
final Focus focusWidget = Focus.withExternalFocusNode(
|
|
focusNode: focusNode,
|
|
child: Container(),
|
|
);
|
|
|
|
await tester.pumpWidget(focusWidget);
|
|
expect(focusNode.isModified, isFalse);
|
|
await tester.pumpWidget(const SizedBox());
|
|
});
|
|
|
|
testWidgets('Focus passes changes in attribute values to its focus node', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
Focus(
|
|
child: Container(),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('Focus widget gains input focus when it gains accessibility focus', (WidgetTester tester) async {
|
|
final SemanticsTester semantics = SemanticsTester(tester);
|
|
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
|
|
final FocusNode focusNode = FocusNode();
|
|
addTearDown(focusNode.dispose);
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.rtl,
|
|
child: Focus(
|
|
focusNode: focusNode,
|
|
child: const Text('Test'),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
semantics,
|
|
hasSemantics(
|
|
TestSemantics.root(
|
|
children: <TestSemantics>[
|
|
TestSemantics(
|
|
id: 1,
|
|
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
|
|
actions: <SemanticsAction>[SemanticsAction.focus],
|
|
label: 'Test',
|
|
textDirection: TextDirection.rtl,
|
|
),
|
|
],
|
|
),
|
|
ignoreRect: true,
|
|
ignoreTransform: true,
|
|
),
|
|
);
|
|
|
|
expect(focusNode.hasFocus, isFalse);
|
|
semanticsOwner.performAction(1, SemanticsAction.focus);
|
|
await tester.pumpAndSettle();
|
|
expect(focusNode.hasFocus, isTrue);
|
|
semantics.dispose();
|
|
});
|
|
});
|
|
|
|
group('ExcludeFocus', () {
|
|
testWidgets("Descendants of ExcludeFocus aren't focusable.", (WidgetTester tester) async {
|
|
final GlobalKey key1 = GlobalKey(debugLabel: '1');
|
|
final GlobalKey key2 = GlobalKey(debugLabel: '2');
|
|
final FocusNode focusNode = FocusNode();
|
|
addTearDown(focusNode.dispose);
|
|
bool? gotFocus;
|
|
await tester.pumpWidget(
|
|
ExcludeFocus(
|
|
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);
|
|
});
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/61700
|
|
testWidgets("ExcludeFocus doesn't transfer focus to another descendant.", (WidgetTester tester) async {
|
|
final FocusNode parentFocusNode = FocusNode(debugLabel: 'group');
|
|
addTearDown(parentFocusNode.dispose);
|
|
final FocusNode focusNode1 = FocusNode(debugLabel: 'node 1');
|
|
addTearDown(focusNode1.dispose);
|
|
final FocusNode focusNode2 = FocusNode(debugLabel: 'node 2');
|
|
addTearDown(focusNode2.dispose);
|
|
await tester.pumpWidget(
|
|
ExcludeFocus(
|
|
excluding: false,
|
|
child: Focus(
|
|
focusNode: parentFocusNode,
|
|
child: Column(
|
|
children: <Widget>[
|
|
Focus(
|
|
autofocus: true,
|
|
focusNode: focusNode1,
|
|
child: Container(),
|
|
),
|
|
Focus(
|
|
focusNode: focusNode2,
|
|
child: Container(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.pump();
|
|
|
|
expect(parentFocusNode.hasFocus, isTrue);
|
|
expect(focusNode1.hasPrimaryFocus, isTrue);
|
|
expect(focusNode2.hasFocus, isFalse);
|
|
|
|
// Move focus to the second node to create some focus history for the scope.
|
|
focusNode2.requestFocus();
|
|
await tester.pump();
|
|
|
|
expect(parentFocusNode.hasFocus, isTrue);
|
|
expect(focusNode1.hasFocus, isFalse);
|
|
expect(focusNode2.hasPrimaryFocus, isTrue);
|
|
|
|
// Now turn off the focus for the subtree.
|
|
await tester.pumpWidget(
|
|
ExcludeFocus(
|
|
child: Focus(
|
|
focusNode: parentFocusNode,
|
|
child: Column(
|
|
children: <Widget>[
|
|
Focus(
|
|
autofocus: true,
|
|
focusNode: focusNode1,
|
|
child: Container(),
|
|
),
|
|
Focus(
|
|
focusNode: focusNode2,
|
|
child: Container(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(focusNode1.hasFocus, isFalse);
|
|
expect(focusNode2.hasFocus, isFalse);
|
|
expect(parentFocusNode.hasFocus, isFalse);
|
|
expect(parentFocusNode.enclosingScope!.hasPrimaryFocus, isTrue);
|
|
});
|
|
|
|
testWidgets("ExcludeFocus doesn't introduce a Semantics node", (WidgetTester tester) async {
|
|
final SemanticsTester semantics = SemanticsTester(tester);
|
|
await tester.pumpWidget(ExcludeFocus(child: Container()));
|
|
final TestSemantics expectedSemantics = TestSemantics.root();
|
|
expect(semantics, hasSemantics(expectedSemantics));
|
|
semantics.dispose();
|
|
});
|
|
|
|
// Regression test for https://github.com/flutter/flutter/issues/92693
|
|
testWidgets('Setting parent FocusScope.canRequestFocus to false, does not set descendant Focus._internalNode._canRequestFocus to false', (WidgetTester tester) async {
|
|
final FocusNode childFocusNode = FocusNode(debugLabel: 'node 1');
|
|
addTearDown(childFocusNode.dispose);
|
|
|
|
Widget buildFocusTree({required bool parentCanRequestFocus}) {
|
|
return FocusScope(
|
|
canRequestFocus: parentCanRequestFocus,
|
|
child: Column(
|
|
children: <Widget>[
|
|
Focus(
|
|
focusNode: childFocusNode,
|
|
child: Container(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// childFocusNode.canRequestFocus is true when parent canRequestFocus is true
|
|
await tester.pumpWidget(buildFocusTree(parentCanRequestFocus: true));
|
|
expect(childFocusNode.canRequestFocus, isTrue);
|
|
|
|
// childFocusNode.canRequestFocus is false when parent canRequestFocus is false
|
|
await tester.pumpWidget(buildFocusTree(parentCanRequestFocus: false));
|
|
expect(childFocusNode.canRequestFocus, isFalse);
|
|
|
|
// childFocusNode.canRequestFocus is true again when parent canRequestFocus is changed back to true
|
|
await tester.pumpWidget(buildFocusTree(parentCanRequestFocus: true));
|
|
expect(childFocusNode.canRequestFocus, isTrue);
|
|
});
|
|
});
|
|
}
|
|
|
|
class TestFocus extends StatefulWidget {
|
|
const TestFocus({
|
|
super.key,
|
|
this.debugLabel,
|
|
this.name = 'a',
|
|
this.autofocus = false,
|
|
this.parentNode,
|
|
});
|
|
|
|
final String? debugLabel;
|
|
final String name;
|
|
final bool autofocus;
|
|
final FocusNode? parentNode;
|
|
|
|
@override
|
|
TestFocusState createState() => TestFocusState();
|
|
}
|
|
|
|
class TestFocusState extends State<TestFocus> {
|
|
late FocusNode focusNode;
|
|
late String _label;
|
|
bool built = false;
|
|
|
|
@override
|
|
void dispose() {
|
|
focusNode.removeListener(_updateLabel);
|
|
focusNode.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
String get label => focusNode.hasFocus ? '${widget.name.toUpperCase()} FOCUSED' : widget.name.toLowerCase();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
focusNode = FocusNode(debugLabel: widget.debugLabel);
|
|
_label = label;
|
|
focusNode.addListener(_updateLabel);
|
|
}
|
|
|
|
void _updateLabel() {
|
|
setState(() {
|
|
_label = label;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
built = true;
|
|
return GestureDetector(
|
|
onTap: () {
|
|
FocusScope.of(context).requestFocus(focusNode);
|
|
},
|
|
child: Focus(
|
|
autofocus: widget.autofocus,
|
|
focusNode: focusNode,
|
|
parentNode: widget.parentNode,
|
|
debugLabel: widget.debugLabel,
|
|
child: Text(
|
|
_label,
|
|
textDirection: TextDirection.ltr,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class TestExternalFocusNode extends FocusNode {
|
|
TestExternalFocusNode();
|
|
|
|
bool isModified = false;
|
|
|
|
@override
|
|
FocusOnKeyEventCallback? get onKeyEvent => _onKeyEvent;
|
|
FocusOnKeyEventCallback? _onKeyEvent;
|
|
@override
|
|
set onKeyEvent(FocusOnKeyEventCallback? newValue) {
|
|
if (newValue != _onKeyEvent) {
|
|
_onKeyEvent = newValue;
|
|
isModified = true;
|
|
}
|
|
}
|
|
|
|
@override
|
|
set descendantsAreFocusable(bool newValue) {
|
|
super.descendantsAreFocusable = newValue;
|
|
isModified = true;
|
|
}
|
|
|
|
@override
|
|
set descendantsAreTraversable(bool newValue) {
|
|
super.descendantsAreTraversable = newValue;
|
|
isModified = true;
|
|
}
|
|
|
|
@override
|
|
set skipTraversal(bool newValue) {
|
|
super.skipTraversal = newValue;
|
|
isModified = true;
|
|
}
|
|
|
|
@override
|
|
set canRequestFocus(bool newValue) {
|
|
super.canRequestFocus = newValue;
|
|
isModified = true;
|
|
}
|
|
}
|