Skip to content

Commit 94b9fa4

Browse files
authored
Provide an option to update Focus's semantics under FocusableActionDetector` (#115833)
Update test Update comments
1 parent b9caef5 commit 94b9fa4

File tree

2 files changed

+126
-0
lines changed

2 files changed

+126
-0
lines changed

packages/flutter/lib/src/widgets/actions.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,6 +1077,7 @@ class FocusableActionDetector extends StatefulWidget {
10771077
this.onShowHoverHighlight,
10781078
this.onFocusChange,
10791079
this.mouseCursor = MouseCursor.defer,
1080+
this.includeFocusSemantics = true,
10801081
required this.child,
10811082
}) : assert(enabled != null),
10821083
assert(autofocus != null),
@@ -1133,6 +1134,11 @@ class FocusableActionDetector extends StatefulWidget {
11331134
/// cursor to the next region behind it in hit-test order.
11341135
final MouseCursor mouseCursor;
11351136

1137+
/// Whether to include semantics from [Focus].
1138+
///
1139+
/// Defaults to true.
1140+
final bool includeFocusSemantics;
1141+
11361142
/// The child widget for this [FocusableActionDetector] widget.
11371143
///
11381144
/// {@macro flutter.widgets.ProxyWidget.child}
@@ -1293,6 +1299,7 @@ class _FocusableActionDetectorState extends State<FocusableActionDetector> {
12931299
descendantsAreTraversable: widget.descendantsAreTraversable,
12941300
canRequestFocus: _canRequestFocus,
12951301
onFocusChange: _handleFocusChange,
1302+
includeSemantics: widget.includeFocusSemantics,
12961303
child: widget.child,
12971304
),
12981305
);

packages/flutter/test/widgets/actions_test.dart

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ void main() {
2828
expect(invoked, isTrue);
2929
});
3030
});
31+
3132
group(Actions, () {
3233
Intent? invokedIntent;
3334
Action<Intent>? invokedAction;
@@ -99,6 +100,7 @@ void main() {
99100
expect(result, isTrue);
100101
expect(invoked, isTrue);
101102
});
103+
102104
testWidgets('maybeInvoke returns null when no action is found', (WidgetTester tester) async {
103105
final GlobalKey containerKey = GlobalKey();
104106
bool invoked = false;
@@ -125,6 +127,7 @@ void main() {
125127
expect(result, isNull);
126128
expect(invoked, isFalse);
127129
});
130+
128131
testWidgets('invoke throws when no action is found', (WidgetTester tester) async {
129132
final GlobalKey containerKey = GlobalKey();
130133
bool invoked = false;
@@ -151,6 +154,7 @@ void main() {
151154
expect(result, isNull);
152155
expect(invoked, isFalse);
153156
});
157+
154158
testWidgets('Actions widget can invoke actions with custom dispatcher', (WidgetTester tester) async {
155159
final GlobalKey containerKey = GlobalKey();
156160
bool invoked = false;
@@ -181,6 +185,7 @@ void main() {
181185
expect(invoked, isTrue);
182186
expect(invokedIntent, equals(intent));
183187
});
188+
184189
testWidgets('Actions can invoke actions in ancestor dispatcher', (WidgetTester tester) async {
185190
final GlobalKey containerKey = GlobalKey();
186191
bool invoked = false;
@@ -217,6 +222,7 @@ void main() {
217222
expect(invokedAction, equals(testAction));
218223
expect(invokedDispatcher.runtimeType, equals(TestDispatcher1));
219224
});
225+
220226
testWidgets("Actions can invoke actions in ancestor dispatcher if a lower one isn't specified", (WidgetTester tester) async {
221227
final GlobalKey containerKey = GlobalKey();
222228
bool invoked = false;
@@ -252,6 +258,7 @@ void main() {
252258
expect(invokedAction, equals(testAction));
253259
expect(invokedDispatcher.runtimeType, equals(TestDispatcher1));
254260
});
261+
255262
testWidgets('Actions widget can be found with of', (WidgetTester tester) async {
256263
final GlobalKey containerKey = GlobalKey();
257264
final ActionDispatcher testDispatcher = TestDispatcher1(postInvoke: collect);
@@ -268,6 +275,7 @@ void main() {
268275
final ActionDispatcher dispatcher = Actions.of(containerKey.currentContext!);
269276
expect(dispatcher, equals(testDispatcher));
270277
});
278+
271279
testWidgets('Action can be found with find', (WidgetTester tester) async {
272280
final GlobalKey containerKey = GlobalKey();
273281
final ActionDispatcher testDispatcher = TestDispatcher1(postInvoke: collect);
@@ -314,6 +322,7 @@ void main() {
314322
expect(() => Actions.find<DoNothingIntent>(containerKey.currentContext!), throwsAssertionError);
315323
expect(Actions.maybeFind<DoNothingIntent>(containerKey.currentContext!), isNull);
316324
});
325+
317326
testWidgets('FocusableActionDetector keeps track of focus and hover even when disabled.', (WidgetTester tester) async {
318327
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
319328
final GlobalKey containerKey = GlobalKey();
@@ -383,6 +392,7 @@ void main() {
383392
expect(hovering, isFalse);
384393
expect(focusing, isFalse);
385394
});
395+
386396
testWidgets('FocusableActionDetector changes mouse cursor when hovered', (WidgetTester tester) async {
387397
await tester.pumpWidget(
388398
MouseRegion(
@@ -415,6 +425,7 @@ void main() {
415425

416426
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden);
417427
});
428+
418429
testWidgets('Actions.invoke returns the value of Action.invoke', (WidgetTester tester) async {
419430
final GlobalKey containerKey = GlobalKey();
420431
final Object sentinel = Object();
@@ -445,6 +456,7 @@ void main() {
445456
expect(identical(result, sentinel), isTrue);
446457
expect(invoked, isTrue);
447458
});
459+
448460
testWidgets('ContextAction can return null', (WidgetTester tester) async {
449461
final GlobalKey containerKey = GlobalKey();
450462
const TestIntent intent = TestIntent();
@@ -471,6 +483,7 @@ void main() {
471483
expect(invokedDispatcher.runtimeType, equals(TestDispatcher1));
472484
expect(testAction.capturedContexts.single, containerKey.currentContext);
473485
});
486+
474487
testWidgets('Disabled actions stop propagation to an ancestor', (WidgetTester tester) async {
475488
final GlobalKey containerKey = GlobalKey();
476489
bool invoked = false;
@@ -775,6 +788,7 @@ void main() {
775788
expect(hovering, isFalse);
776789
expect(focusing, isFalse);
777790
});
791+
778792
testWidgets('FocusableActionDetector shows focus highlight appropriately when focused and disabled', (WidgetTester tester) async {
779793
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
780794
final GlobalKey containerKey = GlobalKey();
@@ -805,6 +819,7 @@ void main() {
805819
await tester.pump();
806820
expect(focusing, isTrue);
807821
});
822+
808823
testWidgets('FocusableActionDetector can be used without callbacks', (WidgetTester tester) async {
809824
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
810825
final GlobalKey containerKey = GlobalKey();
@@ -951,6 +966,110 @@ void main() {
951966
expect(buttonNode2.hasFocus, isFalse);
952967
},
953968
);
969+
970+
testWidgets('FocusableActionDetector can exclude Focus semantics', (WidgetTester tester) async {
971+
await tester.pumpWidget(
972+
MaterialApp(
973+
home: FocusableActionDetector(
974+
child: Column(
975+
children: <Widget>[
976+
TextButton(
977+
onPressed: () {},
978+
child: const Text('Button 1'),
979+
),
980+
TextButton(
981+
onPressed: () {},
982+
child: const Text('Button 2'),
983+
),
984+
],
985+
),
986+
),
987+
),
988+
);
989+
990+
expect(
991+
tester.getSemantics(find.byType(FocusableActionDetector)),
992+
matchesSemantics(
993+
scopesRoute: true,
994+
children: <Matcher>[
995+
// This semantic is from `Focus` widget under `FocusableActionDetector`.
996+
matchesSemantics(
997+
isFocusable: true,
998+
children: <Matcher>[
999+
matchesSemantics(
1000+
hasTapAction: true,
1001+
isButton: true,
1002+
hasEnabledState: true,
1003+
isEnabled: true,
1004+
isFocusable: true,
1005+
label: 'Button 1',
1006+
textDirection: TextDirection.ltr,
1007+
),
1008+
matchesSemantics(
1009+
hasTapAction: true,
1010+
isButton: true,
1011+
hasEnabledState: true,
1012+
isEnabled: true,
1013+
isFocusable: true,
1014+
label: 'Button 2',
1015+
textDirection: TextDirection.ltr,
1016+
),
1017+
],
1018+
),
1019+
],
1020+
),
1021+
);
1022+
1023+
// Set `includeFocusSemantics` to false to exclude semantics
1024+
// from `Focus` widget under `FocusableActionDetector`.
1025+
await tester.pumpWidget(
1026+
MaterialApp(
1027+
home: FocusableActionDetector(
1028+
includeFocusSemantics: false,
1029+
child: Column(
1030+
children: <Widget>[
1031+
TextButton(
1032+
onPressed: () {},
1033+
child: const Text('Button 1'),
1034+
),
1035+
TextButton(
1036+
onPressed: () {},
1037+
child: const Text('Button 2'),
1038+
),
1039+
],
1040+
),
1041+
),
1042+
),
1043+
);
1044+
1045+
// Semantics from the `Focus` widget will be removed.
1046+
expect(
1047+
tester.getSemantics(find.byType(FocusableActionDetector)),
1048+
matchesSemantics(
1049+
scopesRoute: true,
1050+
children: <Matcher>[
1051+
matchesSemantics(
1052+
hasTapAction: true,
1053+
isButton: true,
1054+
hasEnabledState: true,
1055+
isEnabled: true,
1056+
isFocusable: true,
1057+
label: 'Button 1',
1058+
textDirection: TextDirection.ltr,
1059+
),
1060+
matchesSemantics(
1061+
hasTapAction: true,
1062+
isButton: true,
1063+
hasEnabledState: true,
1064+
isEnabled: true,
1065+
isFocusable: true,
1066+
label: 'Button 2',
1067+
textDirection: TextDirection.ltr,
1068+
),
1069+
],
1070+
),
1071+
);
1072+
});
9541073
});
9551074

9561075
group('Action subclasses', () {

0 commit comments

Comments
 (0)