Unverified Commit 560873af authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Wire up canRequestFocus and skipTraversal in FocusScopeNode (#43013)

This adds a canRequestFocus and skipTraversal argument to FocusScope and FocusScopeNode, so that a scope can prevent being traversed.

This allows a fix for a problem in the gallery where the focus while traversing the list of items would sometimes appear to disappear, since it would be focusing things that were in the backdrop that were part of the tree, but were not visible.

Related Issues
Fixes #42955
parent df763544
...@@ -22,6 +22,65 @@ const Cubic _kDecelerateCurve = Cubic(0.23, 0.94, 0.41, 1.0); ...@@ -22,6 +22,65 @@ const Cubic _kDecelerateCurve = Cubic(0.23, 0.94, 0.41, 1.0);
const double _kPeakVelocityTime = 0.248210; const double _kPeakVelocityTime = 0.248210;
const double _kPeakVelocityProgress = 0.379146; const double _kPeakVelocityProgress = 0.379146;
class _TappableWhileStatusIs extends StatefulWidget {
const _TappableWhileStatusIs(
this.status, {
Key key,
this.controller,
this.child,
}) : super(key: key);
final AnimationController controller;
final AnimationStatus status;
final Widget child;
@override
_TappableWhileStatusIsState createState() => _TappableWhileStatusIsState();
}
class _TappableWhileStatusIsState extends State<_TappableWhileStatusIs> {
bool _active;
@override
void initState() {
super.initState();
widget.controller.addStatusListener(_handleStatusChange);
_active = widget.controller.status == widget.status;
}
@override
void dispose() {
widget.controller.removeStatusListener(_handleStatusChange);
super.dispose();
}
void _handleStatusChange(AnimationStatus status) {
final bool value = widget.controller.status == widget.status;
if (_active != value) {
setState(() {
_active = value;
});
}
}
@override
Widget build(BuildContext context) {
Widget child = AbsorbPointer(
absorbing: !_active,
child: widget.child,
);
if (!_active) {
child = FocusScope(
canRequestFocus: false,
debugLabel: '$_TappableWhileStatusIs',
child: child,
);
}
return child;
}
}
class _FrontLayer extends StatelessWidget { class _FrontLayer extends StatelessWidget {
const _FrontLayer({ const _FrontLayer({
Key key, Key key,
...@@ -275,14 +334,22 @@ class _BackdropState extends State<Backdrop> with SingleTickerProviderStateMixin ...@@ -275,14 +334,22 @@ class _BackdropState extends State<Backdrop> with SingleTickerProviderStateMixin
return Stack( return Stack(
key: _backdropKey, key: _backdropKey,
children: <Widget>[ children: <Widget>[
widget.backLayer, _TappableWhileStatusIs(
AnimationStatus.dismissed,
controller: _controller,
child: widget.backLayer,
),
PositionedTransition( PositionedTransition(
rect: _layerAnimation, rect: _layerAnimation,
child: _FrontLayer( child: _FrontLayer(
onTap: _toggleBackdropLayerVisibility, onTap: _toggleBackdropLayerVisibility,
child: _TappableWhileStatusIs(
AnimationStatus.completed,
controller: _controller,
child: widget.frontLayer, child: widget.frontLayer,
), ),
), ),
),
], ],
); );
} }
......
...@@ -66,10 +66,19 @@ class _TappableWhileStatusIsState extends State<_TappableWhileStatusIs> { ...@@ -66,10 +66,19 @@ class _TappableWhileStatusIsState extends State<_TappableWhileStatusIs> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AbsorbPointer( Widget child = AbsorbPointer(
absorbing: !_active, absorbing: !_active,
child: widget.child, child: widget.child,
); );
if (!_active) {
child = FocusScope(
canRequestFocus: false,
debugLabel: '$_TappableWhileStatusIs',
child: child,
);
}
return child;
} }
} }
...@@ -275,12 +284,16 @@ class _BackdropState extends State<Backdrop> with SingleTickerProviderStateMixin ...@@ -275,12 +284,16 @@ class _BackdropState extends State<Backdrop> with SingleTickerProviderStateMixin
), ),
), ),
Expanded( Expanded(
child: _TappableWhileStatusIs(
AnimationStatus.dismissed,
controller: _controller,
child: Visibility( child: Visibility(
child: widget.backLayer, child: widget.backLayer,
visible: _controller.status != AnimationStatus.completed, visible: _controller.status != AnimationStatus.completed,
maintainState: true, maintainState: true,
), ),
), ),
),
], ],
), ),
// Front layer // Front layer
......
...@@ -952,7 +952,16 @@ class FocusScopeNode extends FocusNode { ...@@ -952,7 +952,16 @@ class FocusScopeNode extends FocusNode {
FocusScopeNode({ FocusScopeNode({
String debugLabel, String debugLabel,
FocusOnKeyCallback onKey, FocusOnKeyCallback onKey,
}) : super(debugLabel: debugLabel, onKey: onKey); bool skipTraversal = false,
bool canRequestFocus = true,
}) : assert(skipTraversal != null),
assert(canRequestFocus != null),
super(
debugLabel: debugLabel,
onKey: onKey,
canRequestFocus: canRequestFocus,
skipTraversal: skipTraversal,
);
@override @override
FocusScopeNode get nearestScope => this; FocusScopeNode get nearestScope => this;
......
...@@ -516,6 +516,8 @@ class FocusScope extends Focus { ...@@ -516,6 +516,8 @@ class FocusScope extends Focus {
@required Widget child, @required Widget child,
bool autofocus = false, bool autofocus = false,
ValueChanged<bool> onFocusChange, ValueChanged<bool> onFocusChange,
bool canRequestFocus,
bool skipTraversal,
FocusOnKeyCallback onKey, FocusOnKeyCallback onKey,
String debugLabel, String debugLabel,
}) : assert(child != null), }) : assert(child != null),
...@@ -526,6 +528,8 @@ class FocusScope extends Focus { ...@@ -526,6 +528,8 @@ class FocusScope extends Focus {
focusNode: node, focusNode: node,
autofocus: autofocus, autofocus: autofocus,
onFocusChange: onFocusChange, onFocusChange: onFocusChange,
canRequestFocus: canRequestFocus,
skipTraversal: skipTraversal,
onKey: onKey, onKey: onKey,
debugLabel: debugLabel, debugLabel: debugLabel,
); );
...@@ -552,6 +556,8 @@ class _FocusScopeState extends _FocusState { ...@@ -552,6 +556,8 @@ class _FocusScopeState extends _FocusState {
FocusScopeNode _createNode() { FocusScopeNode _createNode() {
return FocusScopeNode( return FocusScopeNode(
debugLabel: widget.debugLabel, debugLabel: widget.debugLabel,
canRequestFocus: widget.canRequestFocus ?? true,
skipTraversal: widget.skipTraversal ?? false,
); );
} }
......
...@@ -263,6 +263,75 @@ void main() { ...@@ -263,6 +263,75 @@ void main() {
expect(parent1.children.first, equals(child2)); expect(parent1.children.first, equals(child2));
expect(parent2.children.first, equals(child1)); expect(parent2.children.first, equals(child1));
}); });
testWidgets('canRequestFocus affects children.', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope', canRequestFocus: true);
final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope);
parent2Attachment.reparent(parent: scope);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent1);
child1.requestFocus();
await tester.pump();
expect(tester.binding.focusManager.primaryFocus, equals(child1));
expect(scope.focusedChild, equals(child1));
expect(scope.traversalDescendants.contains(child1), isTrue);
expect(scope.traversalDescendants.contains(child2), isTrue);
scope.canRequestFocus = false;
await tester.pump();
child2.requestFocus();
await tester.pump();
expect(tester.binding.focusManager.primaryFocus, isNot(equals(child2)));
expect(tester.binding.focusManager.primaryFocus, isNot(equals(child1)));
expect(scope.focusedChild, isNull);
expect(scope.traversalDescendants.contains(child1), isFalse);
expect(scope.traversalDescendants.contains(child2), isFalse);
});
testWidgets("skipTraversal doesn't affect children.", (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope = FocusScopeNode(debugLabel: 'Scope', skipTraversal: false);
final FocusAttachment scopeAttachment = scope.attach(context);
final FocusNode parent1 = FocusNode(debugLabel: 'Parent 1');
final FocusAttachment parent1Attachment = parent1.attach(context);
final FocusNode parent2 = FocusNode(debugLabel: 'Parent 2');
final FocusAttachment parent2Attachment = parent2.attach(context);
final FocusNode child1 = FocusNode(debugLabel: 'Child 1');
final FocusAttachment child1Attachment = child1.attach(context);
final FocusNode child2 = FocusNode(debugLabel: 'Child 2');
final FocusAttachment child2Attachment = child2.attach(context);
scopeAttachment.reparent(parent: tester.binding.focusManager.rootScope);
parent1Attachment.reparent(parent: scope);
parent2Attachment.reparent(parent: scope);
child1Attachment.reparent(parent: parent1);
child2Attachment.reparent(parent: parent1);
child1.requestFocus();
await tester.pump();
expect(tester.binding.focusManager.primaryFocus, equals(child1));
expect(scope.focusedChild, equals(child1));
expect(tester.binding.focusManager.rootScope.traversalDescendants.contains(scope), isTrue);
expect(scope.traversalDescendants.contains(child1), isTrue);
expect(scope.traversalDescendants.contains(child2), isTrue);
scope.skipTraversal = true;
await tester.pump();
expect(tester.binding.focusManager.primaryFocus, equals(child1));
expect(scope.focusedChild, equals(child1));
expect(tester.binding.focusManager.rootScope.traversalDescendants.contains(scope), isFalse);
expect(scope.traversalDescendants.contains(child1), isTrue);
expect(scope.traversalDescendants.contains(child2), isTrue);
});
testWidgets('Can move node between scopes and lose scope focus', (WidgetTester tester) async { testWidgets('Can move node between scopes and lose scope focus', (WidgetTester tester) async {
final BuildContext context = await setupWidget(tester); final BuildContext context = await setupWidget(tester);
final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context); final FocusScopeNode scope1 = FocusScopeNode(debugLabel: 'scope1')..attach(context);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment