mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
WIP Commits separated as follows: - Update lints in analysis_options files - Run `dart fix --apply` - Clean up leftover analysis issues - Run `dart format .` in the right places. Local analysis and testing passes. Checking CI now. Part of https://github.com/flutter/flutter/issues/178827 - Adoption of flutter_lints in examples/api coming in a separate change (cc @loic-sharma) ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2115 lines
76 KiB
Dart
2115 lines
76 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 'dart:ui' show Tristate;
|
|
|
|
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 parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node');
|
|
addTearDown(parentFocusScope.dispose);
|
|
final 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 parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node');
|
|
addTearDown(parentFocusScope.dispose);
|
|
final childFocusScope1 = FocusScopeNode(debugLabel: 'Child Scope Node 1');
|
|
addTearDown(childFocusScope1.dispose);
|
|
final 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 parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node');
|
|
addTearDown(parentFocusScope.dispose);
|
|
final 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 topNode = FocusNode(debugLabel: 'Top');
|
|
addTearDown(topNode.dispose);
|
|
final parentNode = FocusNode(debugLabel: 'Parent');
|
|
addTearDown(parentNode.dispose);
|
|
final childNode = FocusNode(debugLabel: 'Child');
|
|
addTearDown(childNode.dispose);
|
|
final 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 topNode = FocusScopeNode(debugLabel: 'Top');
|
|
addTearDown(topNode.dispose);
|
|
final parentNode = FocusScopeNode(debugLabel: 'Parent');
|
|
addTearDown(parentNode.dispose);
|
|
final childNode = FocusScopeNode(debugLabel: 'Child');
|
|
addTearDown(childNode.dispose);
|
|
final 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 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 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 parentFocusScope1 = FocusScopeNode(debugLabel: 'Parent Scope 1');
|
|
addTearDown(parentFocusScope1.dispose);
|
|
final 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 parentFocusScope1 = FocusScopeNode(debugLabel: 'Parent Scope 1');
|
|
addTearDown(parentFocusScope1.dispose);
|
|
final 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 parentFocusScope1 = FocusScopeNode();
|
|
addTearDown(parentFocusScope1.dispose);
|
|
final 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 parentFocusScope1 = FocusScopeNode(debugLabel: 'Scope 1');
|
|
addTearDown(parentFocusScope1.dispose);
|
|
final 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(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 focusNodeA = FocusNode(debugLabel: 'Test Node A');
|
|
addTearDown(focusNodeA.dispose);
|
|
final 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();
|
|
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;
|
|
var 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 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 nodes = <FocusNode>[];
|
|
final 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();
|
|
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.flagsCollection.isFocused, Tristate.isFalse);
|
|
|
|
FocusScope.of(key.currentContext!).requestFocus(key.currentState!.focusNode);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(key.currentState!.focusNode.hasFocus, isTrue);
|
|
expect(semantics.flagsCollection.isFocused, Tristate.isTrue);
|
|
|
|
key.currentState!.focusNode.canRequestFocus = false;
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(key.currentState!.focusNode.hasFocus, isFalse);
|
|
expect(key.currentState!.focusNode.canRequestFocus, isFalse);
|
|
expect(semantics.flagsCollection.isFocused, Tristate.none);
|
|
});
|
|
|
|
testWidgets('Setting canRequestFocus on focus node causes update.', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final GlobalKey<TestFocusState> key = GlobalKey();
|
|
|
|
final 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 scope1 = FocusScopeNode(debugLabel: 'scope1');
|
|
addTearDown(scope1.dispose);
|
|
final scope2 = FocusScopeNode(debugLabel: 'scope2');
|
|
addTearDown(scope2.dispose);
|
|
final focus1 = FocusNode(debugLabel: 'focus1');
|
|
addTearDown(focus1.dispose);
|
|
final 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();
|
|
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 scopeNode = FocusScopeNode(debugLabel: 'scope');
|
|
addTearDown(scopeNode.dispose);
|
|
final node1 = FocusNode(debugLabel: 'node 1');
|
|
addTearDown(node1.dispose);
|
|
final node2 = FocusNode(debugLabel: 'node 2');
|
|
addTearDown(node2.dispose);
|
|
final 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 semantics = SemanticsTester(tester);
|
|
await tester.pumpWidget(Focus(includeSemantics: false, child: Container()));
|
|
final 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();
|
|
addTearDown(focusNode.dispose);
|
|
bool? keyEventHandled;
|
|
KeyEventResult handleCallback(FocusNode node, RawKeyEvent event) {
|
|
keyEventHandled = true;
|
|
return KeyEventResult.handled;
|
|
}
|
|
|
|
KeyEventResult ignoreCallback(FocusNode node, RawKeyEvent event) => KeyEventResult.ignored;
|
|
var 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();
|
|
addTearDown(focusNode.dispose);
|
|
bool? keyEventHandled;
|
|
KeyEventResult handleEventCallback(FocusNode node, KeyEvent event) {
|
|
keyEventHandled = true;
|
|
return KeyEventResult.handled;
|
|
}
|
|
|
|
KeyEventResult ignoreEventCallback(FocusNode node, KeyEvent event) => KeyEventResult.ignored;
|
|
var 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();
|
|
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;
|
|
var 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 focusNode = TestExternalFocusNode();
|
|
assert(!focusNode.isModified);
|
|
addTearDown(focusNode.dispose);
|
|
|
|
final 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 semantics = SemanticsTester(tester);
|
|
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
|
|
final 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();
|
|
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 parentFocusNode = FocusNode(debugLabel: 'group');
|
|
addTearDown(parentFocusNode.dispose);
|
|
final focusNode1 = FocusNode(debugLabel: 'node 1');
|
|
addTearDown(focusNode1.dispose);
|
|
final 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 semantics = SemanticsTester(tester);
|
|
await tester.pumpWidget(ExcludeFocus(child: Container()));
|
|
final 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 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;
|
|
}
|
|
}
|