Reland "Adds a parent scope TraversalEdgeBehavior and fixes modal rou… (#134554)
…… (#134550)" fixes https://github.com/flutter/flutter/issues/112567 This reverts commit 5900c4baa751aff8f05e820287a02b60cdd62dfa. The internal test needs migration. cl/564746935 This is the same of original pr, no new change ## 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]. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [Features we expect every widget to implement]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#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/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat
This commit is contained in:
parent
51f1a464f5
commit
ff73448f33
@ -1683,6 +1683,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
|||||||
},
|
},
|
||||||
onUnknownRoute: _onUnknownRoute,
|
onUnknownRoute: _onUnknownRoute,
|
||||||
observers: widget.navigatorObservers!,
|
observers: widget.navigatorObservers!,
|
||||||
|
routeTraversalEdgeBehavior: kIsWeb ? TraversalEdgeBehavior.leaveFlutterView : TraversalEdgeBehavior.parentScope,
|
||||||
reportsRouteUpdateToEngine: true,
|
reportsRouteUpdateToEngine: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -120,6 +120,16 @@ enum TraversalEdgeBehavior {
|
|||||||
/// address bar, escape an `iframe`, or focus on HTML elements other than
|
/// address bar, escape an `iframe`, or focus on HTML elements other than
|
||||||
/// those managed by Flutter.
|
/// those managed by Flutter.
|
||||||
leaveFlutterView,
|
leaveFlutterView,
|
||||||
|
|
||||||
|
/// Allows focus to traverse up to parent scope.
|
||||||
|
///
|
||||||
|
/// When reaching the edge of the current scope, requesting the next focus
|
||||||
|
/// will look up to the parent scope of the current scope and focus the focus
|
||||||
|
/// node next to the current scope.
|
||||||
|
///
|
||||||
|
/// If there is no parent scope above the current scope, fallback to
|
||||||
|
/// [closedLoop] behavior.
|
||||||
|
parentScope,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determines how focusable widgets are traversed within a [FocusTraversalGroup].
|
/// Determines how focusable widgets are traversed within a [FocusTraversalGroup].
|
||||||
@ -186,6 +196,60 @@ abstract class FocusTraversalPolicy with Diagnosticable {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Request focus on a focus node as a result of a tab traversal.
|
||||||
|
///
|
||||||
|
/// If the `node` is a [FocusScopeNode], this method will recursively find
|
||||||
|
/// the next focus from its descendants until it find a regular [FocusNode].
|
||||||
|
///
|
||||||
|
/// Returns true if this method focused a new focus node.
|
||||||
|
bool _requestTabTraversalFocus(
|
||||||
|
FocusNode node, {
|
||||||
|
ScrollPositionAlignmentPolicy? alignmentPolicy,
|
||||||
|
double? alignment,
|
||||||
|
Duration? duration,
|
||||||
|
Curve? curve,
|
||||||
|
required bool forward,
|
||||||
|
}) {
|
||||||
|
if (node is FocusScopeNode) {
|
||||||
|
if (node.focusedChild != null) {
|
||||||
|
// Can't stop here as the `focusedChild` may be a focus scope node
|
||||||
|
// without a first focus. The first focus will be picked in the
|
||||||
|
// next iteration.
|
||||||
|
return _requestTabTraversalFocus(
|
||||||
|
node.focusedChild!,
|
||||||
|
alignmentPolicy: alignmentPolicy,
|
||||||
|
alignment: alignment,
|
||||||
|
duration: duration,
|
||||||
|
curve: curve,
|
||||||
|
forward: forward,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final List<FocusNode> sortedChildren = _sortAllDescendants(node, node);
|
||||||
|
if (sortedChildren.isNotEmpty) {
|
||||||
|
_requestTabTraversalFocus(
|
||||||
|
forward ? sortedChildren.first : sortedChildren.last,
|
||||||
|
alignmentPolicy: alignmentPolicy,
|
||||||
|
alignment: alignment,
|
||||||
|
duration: duration,
|
||||||
|
curve: curve,
|
||||||
|
forward: forward,
|
||||||
|
);
|
||||||
|
// Regardless if _requestTabTraversalFocus return true or false, a first
|
||||||
|
// focus has been picked.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final bool nodeHadPrimaryFocus = node.hasPrimaryFocus;
|
||||||
|
requestFocusCallback(
|
||||||
|
node,
|
||||||
|
alignmentPolicy: alignmentPolicy,
|
||||||
|
alignment: alignment,
|
||||||
|
duration: duration,
|
||||||
|
curve: curve,
|
||||||
|
);
|
||||||
|
return !nodeHadPrimaryFocus;
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the node that should receive focus if focus is traversing
|
/// Returns the node that should receive focus if focus is traversing
|
||||||
/// forwards, and there is no current focus.
|
/// forwards, and there is no current focus.
|
||||||
///
|
///
|
||||||
@ -340,10 +404,21 @@ abstract class FocusTraversalPolicy with Diagnosticable {
|
|||||||
@protected
|
@protected
|
||||||
Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode);
|
Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode);
|
||||||
|
|
||||||
Map<FocusNode?, _FocusTraversalGroupInfo> _findGroups(FocusScopeNode scope, _FocusTraversalGroupNode? scopeGroupNode, FocusNode currentNode) {
|
static Iterable<FocusNode> _getDescendantsWithoutExpandingScope(FocusNode node) {
|
||||||
|
final List<FocusNode> result = <FocusNode>[];
|
||||||
|
for (final FocusNode child in node.children) {
|
||||||
|
if (child is! FocusScopeNode) {
|
||||||
|
result.addAll(_getDescendantsWithoutExpandingScope(child));
|
||||||
|
}
|
||||||
|
result.add(child);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<FocusNode?, _FocusTraversalGroupInfo> _findGroups(FocusScopeNode scope, _FocusTraversalGroupNode? scopeGroupNode, FocusNode currentNode) {
|
||||||
final FocusTraversalPolicy defaultPolicy = scopeGroupNode?.policy ?? ReadingOrderTraversalPolicy();
|
final FocusTraversalPolicy defaultPolicy = scopeGroupNode?.policy ?? ReadingOrderTraversalPolicy();
|
||||||
final Map<FocusNode?, _FocusTraversalGroupInfo> groups = <FocusNode?, _FocusTraversalGroupInfo>{};
|
final Map<FocusNode?, _FocusTraversalGroupInfo> groups = <FocusNode?, _FocusTraversalGroupInfo>{};
|
||||||
for (final FocusNode node in scope.descendants) {
|
for (final FocusNode node in _getDescendantsWithoutExpandingScope(scope)) {
|
||||||
final _FocusTraversalGroupNode? groupNode = FocusTraversalGroup._getGroupNode(node);
|
final _FocusTraversalGroupNode? groupNode = FocusTraversalGroup._getGroupNode(node);
|
||||||
// Group nodes need to be added to their parent's node, or to the "null"
|
// Group nodes need to be added to their parent's node, or to the "null"
|
||||||
// node if no parent is found. This creates the hierarchy of group nodes
|
// node if no parent is found. This creates the hierarchy of group nodes
|
||||||
@ -376,7 +451,7 @@ abstract class FocusTraversalPolicy with Diagnosticable {
|
|||||||
|
|
||||||
// Sort all descendants, taking into account the FocusTraversalGroup
|
// Sort all descendants, taking into account the FocusTraversalGroup
|
||||||
// that they are each in, and filtering out non-traversable/focusable nodes.
|
// that they are each in, and filtering out non-traversable/focusable nodes.
|
||||||
List<FocusNode> _sortAllDescendants(FocusScopeNode scope, FocusNode currentNode) {
|
static List<FocusNode> _sortAllDescendants(FocusScopeNode scope, FocusNode currentNode) {
|
||||||
final _FocusTraversalGroupNode? scopeGroupNode = FocusTraversalGroup._getGroupNode(scope);
|
final _FocusTraversalGroupNode? scopeGroupNode = FocusTraversalGroup._getGroupNode(scope);
|
||||||
// Build the sorting data structure, separating descendants into groups.
|
// Build the sorting data structure, separating descendants into groups.
|
||||||
final Map<FocusNode?, _FocusTraversalGroupInfo> groups = _findGroups(scope, scopeGroupNode, currentNode);
|
final Map<FocusNode?, _FocusTraversalGroupInfo> groups = _findGroups(scope, scopeGroupNode, currentNode);
|
||||||
@ -463,30 +538,42 @@ abstract class FocusTraversalPolicy with Diagnosticable {
|
|||||||
if (focusedChild == null) {
|
if (focusedChild == null) {
|
||||||
final FocusNode? firstFocus = forward ? findFirstFocus(currentNode) : findLastFocus(currentNode);
|
final FocusNode? firstFocus = forward ? findFirstFocus(currentNode) : findLastFocus(currentNode);
|
||||||
if (firstFocus != null) {
|
if (firstFocus != null) {
|
||||||
requestFocusCallback(
|
return _requestTabTraversalFocus(
|
||||||
firstFocus,
|
firstFocus,
|
||||||
alignmentPolicy: forward ? ScrollPositionAlignmentPolicy.keepVisibleAtEnd : ScrollPositionAlignmentPolicy.keepVisibleAtStart,
|
alignmentPolicy: forward ? ScrollPositionAlignmentPolicy.keepVisibleAtEnd : ScrollPositionAlignmentPolicy.keepVisibleAtStart,
|
||||||
|
forward: forward,
|
||||||
);
|
);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
focusedChild ??= nearestScope;
|
focusedChild ??= nearestScope;
|
||||||
final List<FocusNode> sortedNodes = _sortAllDescendants(nearestScope, focusedChild);
|
final List<FocusNode> sortedNodes = _sortAllDescendants(nearestScope, focusedChild);
|
||||||
|
|
||||||
assert(sortedNodes.contains(focusedChild));
|
assert(sortedNodes.contains(focusedChild));
|
||||||
if (sortedNodes.length < 2) {
|
|
||||||
// If there are no nodes to traverse to, like when descendantsAreTraversable
|
|
||||||
// is false or skipTraversal for all the nodes is true.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (forward && focusedChild == sortedNodes.last) {
|
if (forward && focusedChild == sortedNodes.last) {
|
||||||
switch (nearestScope.traversalEdgeBehavior) {
|
switch (nearestScope.traversalEdgeBehavior) {
|
||||||
case TraversalEdgeBehavior.leaveFlutterView:
|
case TraversalEdgeBehavior.leaveFlutterView:
|
||||||
focusedChild.unfocus();
|
focusedChild.unfocus();
|
||||||
return false;
|
return false;
|
||||||
|
case TraversalEdgeBehavior.parentScope:
|
||||||
|
final FocusScopeNode? parentScope = nearestScope.enclosingScope;
|
||||||
|
if (parentScope != null && parentScope != FocusManager.instance.rootScope) {
|
||||||
|
focusedChild.unfocus();
|
||||||
|
parentScope.nextFocus();
|
||||||
|
// Verify the focus really has changed.
|
||||||
|
return focusedChild.enclosingScope?.focusedChild != focusedChild;
|
||||||
|
}
|
||||||
|
// No valid parent scope. Fallback to closed loop behavior.
|
||||||
|
return _requestTabTraversalFocus(
|
||||||
|
sortedNodes.first,
|
||||||
|
alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
|
||||||
|
forward: forward,
|
||||||
|
);
|
||||||
case TraversalEdgeBehavior.closedLoop:
|
case TraversalEdgeBehavior.closedLoop:
|
||||||
requestFocusCallback(sortedNodes.first, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd);
|
return _requestTabTraversalFocus(
|
||||||
return true;
|
sortedNodes.first,
|
||||||
|
alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
|
||||||
|
forward: forward,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!forward && focusedChild == sortedNodes.first) {
|
if (!forward && focusedChild == sortedNodes.first) {
|
||||||
@ -494,9 +581,26 @@ abstract class FocusTraversalPolicy with Diagnosticable {
|
|||||||
case TraversalEdgeBehavior.leaveFlutterView:
|
case TraversalEdgeBehavior.leaveFlutterView:
|
||||||
focusedChild.unfocus();
|
focusedChild.unfocus();
|
||||||
return false;
|
return false;
|
||||||
|
case TraversalEdgeBehavior.parentScope:
|
||||||
|
final FocusScopeNode? parentScope = nearestScope.enclosingScope;
|
||||||
|
if (parentScope != null && parentScope != FocusManager.instance.rootScope) {
|
||||||
|
focusedChild.unfocus();
|
||||||
|
parentScope.previousFocus();
|
||||||
|
// Verify the focus really has changed.
|
||||||
|
return focusedChild.enclosingScope?.focusedChild != focusedChild;
|
||||||
|
}
|
||||||
|
// No valid parent scope. Fallback to closed loop behavior.
|
||||||
|
return _requestTabTraversalFocus(
|
||||||
|
sortedNodes.last,
|
||||||
|
alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart,
|
||||||
|
forward: forward,
|
||||||
|
);
|
||||||
case TraversalEdgeBehavior.closedLoop:
|
case TraversalEdgeBehavior.closedLoop:
|
||||||
requestFocusCallback(sortedNodes.last, alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart);
|
return _requestTabTraversalFocus(
|
||||||
return true;
|
sortedNodes.last,
|
||||||
|
alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart,
|
||||||
|
forward: forward,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -504,11 +608,11 @@ abstract class FocusTraversalPolicy with Diagnosticable {
|
|||||||
FocusNode? previousNode;
|
FocusNode? previousNode;
|
||||||
for (final FocusNode node in maybeFlipped) {
|
for (final FocusNode node in maybeFlipped) {
|
||||||
if (previousNode == focusedChild) {
|
if (previousNode == focusedChild) {
|
||||||
requestFocusCallback(
|
return _requestTabTraversalFocus(
|
||||||
node,
|
node,
|
||||||
alignmentPolicy: forward ? ScrollPositionAlignmentPolicy.keepVisibleAtEnd : ScrollPositionAlignmentPolicy.keepVisibleAtStart,
|
alignmentPolicy: forward ? ScrollPositionAlignmentPolicy.keepVisibleAtEnd : ScrollPositionAlignmentPolicy.keepVisibleAtStart,
|
||||||
|
forward: forward,
|
||||||
);
|
);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
previousNode = node;
|
previousNode = node;
|
||||||
}
|
}
|
||||||
|
@ -1144,9 +1144,7 @@ class DefaultTransitionDelegate<T> extends TransitionDelegate<T> {
|
|||||||
/// The default value of [Navigator.routeTraversalEdgeBehavior].
|
/// The default value of [Navigator.routeTraversalEdgeBehavior].
|
||||||
///
|
///
|
||||||
/// {@macro flutter.widgets.navigator.routeTraversalEdgeBehavior}
|
/// {@macro flutter.widgets.navigator.routeTraversalEdgeBehavior}
|
||||||
const TraversalEdgeBehavior kDefaultRouteTraversalEdgeBehavior = kIsWeb
|
const TraversalEdgeBehavior kDefaultRouteTraversalEdgeBehavior = TraversalEdgeBehavior.parentScope;
|
||||||
? TraversalEdgeBehavior.leaveFlutterView
|
|
||||||
: TraversalEdgeBehavior.closedLoop;
|
|
||||||
|
|
||||||
/// A widget that manages a set of child widgets with a stack discipline.
|
/// A widget that manages a set of child widgets with a stack discipline.
|
||||||
///
|
///
|
||||||
|
@ -834,7 +834,9 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
|
|||||||
late Listenable _listenable;
|
late Listenable _listenable;
|
||||||
|
|
||||||
/// The node this scope will use for its root [FocusScope] widget.
|
/// The node this scope will use for its root [FocusScope] widget.
|
||||||
final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: '$_ModalScopeState Focus Scope');
|
final FocusScopeNode focusScopeNode = FocusScopeNode(
|
||||||
|
debugLabel: '$_ModalScopeState Focus Scope',
|
||||||
|
);
|
||||||
final ScrollController primaryScrollController = ScrollController();
|
final ScrollController primaryScrollController = ScrollController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -936,6 +938,8 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
|
|||||||
controller: primaryScrollController,
|
controller: primaryScrollController,
|
||||||
child: FocusScope(
|
child: FocusScope(
|
||||||
node: focusScopeNode, // immutable
|
node: focusScopeNode, // immutable
|
||||||
|
// Only top most route can participate in focus traversal.
|
||||||
|
skipTraversal: !widget.route.isCurrent,
|
||||||
child: RepaintBoundary(
|
child: RepaintBoundary(
|
||||||
child: AnimatedBuilder(
|
child: AnimatedBuilder(
|
||||||
animation: _listenable, // immutable
|
animation: _listenable, // immutable
|
||||||
@ -1704,11 +1708,26 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
|||||||
changedInternalState();
|
changedInternalState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeNext(Route<dynamic>? nextRoute) {
|
||||||
|
super.didChangeNext(nextRoute);
|
||||||
|
changedInternalState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didPopNext(Route<dynamic> nextRoute) {
|
||||||
|
super.didPopNext(nextRoute);
|
||||||
|
changedInternalState();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void changedInternalState() {
|
void changedInternalState() {
|
||||||
super.changedInternalState();
|
super.changedInternalState();
|
||||||
|
// No need to mark dirty if this method is called during build phase.
|
||||||
|
if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks) {
|
||||||
setState(() { /* internal state already changed */ });
|
setState(() { /* internal state already changed */ });
|
||||||
_modalBarrier.markNeedsBuild();
|
_modalBarrier.markNeedsBuild();
|
||||||
|
}
|
||||||
_modalScope.maintainState = maintainState;
|
_modalScope.maintainState = maintainState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@ -792,6 +793,10 @@ void main() {
|
|||||||
testWidgetsWithLeakTracking("Disabled IconButton can't be traversed to when disabled.", (WidgetTester tester) async {
|
testWidgetsWithLeakTracking("Disabled IconButton can't be traversed to when disabled.", (WidgetTester tester) async {
|
||||||
final FocusNode focusNode1 = FocusNode(debugLabel: 'IconButton 1');
|
final FocusNode focusNode1 = FocusNode(debugLabel: 'IconButton 1');
|
||||||
final FocusNode focusNode2 = FocusNode(debugLabel: 'IconButton 2');
|
final FocusNode focusNode2 = FocusNode(debugLabel: 'IconButton 2');
|
||||||
|
addTearDown(() {
|
||||||
|
focusNode1.dispose();
|
||||||
|
focusNode2.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
wrap(
|
wrap(
|
||||||
@ -821,11 +826,8 @@ void main() {
|
|||||||
expect(focusNode1.nextFocus(), isFalse);
|
expect(focusNode1.nextFocus(), isFalse);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(focusNode1.hasPrimaryFocus, isTrue);
|
expect(focusNode1.hasPrimaryFocus, !kIsWeb);
|
||||||
expect(focusNode2.hasPrimaryFocus, isFalse);
|
expect(focusNode2.hasPrimaryFocus, isFalse);
|
||||||
|
|
||||||
focusNode1.dispose();
|
|
||||||
focusNode2.dispose();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
group('feedback', () {
|
group('feedback', () {
|
||||||
|
@ -981,7 +981,7 @@ void main() {
|
|||||||
expect(buttonNode2.hasFocus, isFalse);
|
expect(buttonNode2.hasFocus, isFalse);
|
||||||
primaryFocus!.nextFocus();
|
primaryFocus!.nextFocus();
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(buttonNode1.hasFocus, isTrue);
|
expect(buttonNode1.hasFocus, isFalse);
|
||||||
expect(buttonNode2.hasFocus, isFalse);
|
expect(buttonNode2.hasFocus, isFalse);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -441,6 +441,96 @@ void main() {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgetsWithLeakTracking('Nested navigator does not trap focus', (WidgetTester tester) async {
|
||||||
|
final FocusNode node1 = FocusNode();
|
||||||
|
addTearDown(node1.dispose);
|
||||||
|
final FocusNode node2 = FocusNode();
|
||||||
|
addTearDown(node2.dispose);
|
||||||
|
final FocusNode node3 = FocusNode();
|
||||||
|
addTearDown(node3.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: FocusTraversalGroup(
|
||||||
|
policy: ReadingOrderTraversalPolicy(),
|
||||||
|
child: FocusScope(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Focus(
|
||||||
|
focusNode: node1,
|
||||||
|
child: const SizedBox(width: 100, height: 100),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
child: Navigator(
|
||||||
|
pages: <Page<void>>[
|
||||||
|
MaterialPage<void>(
|
||||||
|
child: Focus(
|
||||||
|
focusNode: node2,
|
||||||
|
child: const SizedBox(width: 100, height: 100),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onPopPage: (_, __) => false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Focus(
|
||||||
|
focusNode: node3,
|
||||||
|
child: const SizedBox(width: 100, height: 100),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
node1.requestFocus();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(node1.hasFocus, isTrue);
|
||||||
|
expect(node2.hasFocus, isFalse);
|
||||||
|
expect(node3.hasFocus, isFalse);
|
||||||
|
|
||||||
|
node1.nextFocus();
|
||||||
|
await tester.pump();
|
||||||
|
expect(node1.hasFocus, isFalse);
|
||||||
|
expect(node2.hasFocus, isTrue);
|
||||||
|
expect(node3.hasFocus, isFalse);
|
||||||
|
|
||||||
|
node2.nextFocus();
|
||||||
|
await tester.pump();
|
||||||
|
expect(node1.hasFocus, isFalse);
|
||||||
|
expect(node2.hasFocus, isFalse);
|
||||||
|
expect(node3.hasFocus, isTrue);
|
||||||
|
|
||||||
|
node3.nextFocus();
|
||||||
|
await tester.pump();
|
||||||
|
expect(node1.hasFocus, isTrue);
|
||||||
|
expect(node2.hasFocus, isFalse);
|
||||||
|
expect(node3.hasFocus, isFalse);
|
||||||
|
|
||||||
|
node1.previousFocus();
|
||||||
|
await tester.pump();
|
||||||
|
expect(node1.hasFocus, isFalse);
|
||||||
|
expect(node2.hasFocus, isFalse);
|
||||||
|
expect(node3.hasFocus, isTrue);
|
||||||
|
|
||||||
|
node3.previousFocus();
|
||||||
|
await tester.pump();
|
||||||
|
expect(node1.hasFocus, isFalse);
|
||||||
|
expect(node2.hasFocus, isTrue);
|
||||||
|
expect(node3.hasFocus, isFalse);
|
||||||
|
|
||||||
|
node2.previousFocus();
|
||||||
|
await tester.pump();
|
||||||
|
expect(node1.hasFocus, isTrue);
|
||||||
|
expect(node2.hasFocus, isFalse);
|
||||||
|
expect(node3.hasFocus, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
group(ReadingOrderTraversalPolicy, () {
|
group(ReadingOrderTraversalPolicy, () {
|
||||||
testWidgetsWithLeakTracking('Find the initial focus if there is none yet.', (WidgetTester tester) async {
|
testWidgetsWithLeakTracking('Find the initial focus if there is none yet.', (WidgetTester tester) async {
|
||||||
final GlobalKey key1 = GlobalKey(debugLabel: '1');
|
final GlobalKey key1 = GlobalKey(debugLabel: '1');
|
||||||
|
@ -1489,29 +1489,34 @@ void main() {
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
expect(log, <String>['building page 1 - false']);
|
final List<String> expected = <String>['building page 1 - false'];
|
||||||
|
expect(log, expected);
|
||||||
key.currentState!.pushReplacement(PageRouteBuilder<int>(
|
key.currentState!.pushReplacement(PageRouteBuilder<int>(
|
||||||
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
|
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
|
||||||
log.add('building page 2 - ${ModalRoute.of(context)!.canPop}');
|
log.add('building page 2 - ${ModalRoute.of(context)!.canPop}');
|
||||||
return const Placeholder();
|
return const Placeholder();
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
expect(log, <String>['building page 1 - false']);
|
expect(log, expected);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(log, <String>['building page 1 - false', 'building page 2 - false']);
|
expected.add('building page 2 - false');
|
||||||
|
expected.add('building page 1 - false'); // page 1 is rebuilt again because isCurrent changed.
|
||||||
|
expect(log, expected);
|
||||||
await tester.pump(const Duration(milliseconds: 150));
|
await tester.pump(const Duration(milliseconds: 150));
|
||||||
expect(log, <String>['building page 1 - false', 'building page 2 - false']);
|
expect(log, expected);
|
||||||
key.currentState!.pushReplacement(PageRouteBuilder<int>(
|
key.currentState!.pushReplacement(PageRouteBuilder<int>(
|
||||||
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
|
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
|
||||||
log.add('building page 3 - ${ModalRoute.of(context)!.canPop}');
|
log.add('building page 3 - ${ModalRoute.of(context)!.canPop}');
|
||||||
return const Placeholder();
|
return const Placeholder();
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
expect(log, <String>['building page 1 - false', 'building page 2 - false']);
|
expect(log, expected);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(log, <String>['building page 1 - false', 'building page 2 - false', 'building page 3 - false']);
|
expected.add('building page 3 - false');
|
||||||
|
expected.add('building page 2 - false'); // page 2 is rebuilt again because isCurrent changed.
|
||||||
|
expect(log, expected);
|
||||||
await tester.pump(const Duration(milliseconds: 200));
|
await tester.pump(const Duration(milliseconds: 200));
|
||||||
expect(log, <String>['building page 1 - false', 'building page 2 - false', 'building page 3 - false']);
|
expect(log, expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgetsWithLeakTracking('route semantics', (WidgetTester tester) async {
|
testWidgetsWithLeakTracking('route semantics', (WidgetTester tester) async {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user