Skip to content

Commit 49f620d

Browse files
author
Nitesh Sharma
authored
Fix dual focus issue in CheckboxListTile, RadioListTile and SwitchListTile (#143213)
These widgets can now only receive focus once when tabbing through the focus tree.
1 parent ace3e58 commit 49f620d

File tree

6 files changed

+251
-108
lines changed

6 files changed

+251
-108
lines changed

packages/flutter/lib/src/material/checkbox_list_tile.dart

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -475,42 +475,46 @@ class CheckboxListTile extends StatelessWidget {
475475

476476
switch (_checkboxType) {
477477
case _CheckboxType.material:
478-
control = Checkbox(
479-
value: value,
480-
onChanged: enabled ?? true ? onChanged : null,
481-
mouseCursor: mouseCursor,
482-
activeColor: activeColor,
483-
fillColor: fillColor,
484-
checkColor: checkColor,
485-
hoverColor: hoverColor,
486-
overlayColor: overlayColor,
487-
splashRadius: splashRadius,
488-
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
489-
autofocus: autofocus,
490-
tristate: tristate,
491-
shape: checkboxShape,
492-
side: side,
493-
isError: isError,
494-
semanticLabel: checkboxSemanticLabel,
478+
control = ExcludeFocus(
479+
child: Checkbox(
480+
value: value,
481+
onChanged: enabled ?? true ? onChanged : null,
482+
mouseCursor: mouseCursor,
483+
activeColor: activeColor,
484+
fillColor: fillColor,
485+
checkColor: checkColor,
486+
hoverColor: hoverColor,
487+
overlayColor: overlayColor,
488+
splashRadius: splashRadius,
489+
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
490+
autofocus: autofocus,
491+
tristate: tristate,
492+
shape: checkboxShape,
493+
side: side,
494+
isError: isError,
495+
semanticLabel: checkboxSemanticLabel,
496+
),
495497
);
496498
case _CheckboxType.adaptive:
497-
control = Checkbox.adaptive(
498-
value: value,
499-
onChanged: enabled ?? true ? onChanged : null,
500-
mouseCursor: mouseCursor,
501-
activeColor: activeColor,
502-
fillColor: fillColor,
503-
checkColor: checkColor,
504-
hoverColor: hoverColor,
505-
overlayColor: overlayColor,
506-
splashRadius: splashRadius,
507-
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
508-
autofocus: autofocus,
509-
tristate: tristate,
510-
shape: checkboxShape,
511-
side: side,
512-
isError: isError,
513-
semanticLabel: checkboxSemanticLabel,
499+
control = ExcludeFocus(
500+
child: Checkbox.adaptive(
501+
value: value,
502+
onChanged: enabled ?? true ? onChanged : null,
503+
mouseCursor: mouseCursor,
504+
activeColor: activeColor,
505+
fillColor: fillColor,
506+
checkColor: checkColor,
507+
hoverColor: hoverColor,
508+
overlayColor: overlayColor,
509+
splashRadius: splashRadius,
510+
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
511+
autofocus: autofocus,
512+
tristate: tristate,
513+
shape: checkboxShape,
514+
side: side,
515+
isError: isError,
516+
semanticLabel: checkboxSemanticLabel,
517+
),
514518
);
515519
}
516520

packages/flutter/lib/src/material/radio_list_tile.dart

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -452,35 +452,39 @@ class RadioListTile<T> extends StatelessWidget {
452452
final Widget control;
453453
switch (_radioType) {
454454
case _RadioType.material:
455-
control = Radio<T>(
456-
value: value,
457-
groupValue: groupValue,
458-
onChanged: onChanged,
459-
toggleable: toggleable,
460-
activeColor: activeColor,
461-
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
462-
autofocus: autofocus,
463-
fillColor: fillColor,
464-
mouseCursor: mouseCursor,
465-
hoverColor: hoverColor,
466-
overlayColor: overlayColor,
467-
splashRadius: splashRadius,
455+
control = ExcludeFocus(
456+
child: Radio<T>(
457+
value: value,
458+
groupValue: groupValue,
459+
onChanged: onChanged,
460+
toggleable: toggleable,
461+
activeColor: activeColor,
462+
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
463+
autofocus: autofocus,
464+
fillColor: fillColor,
465+
mouseCursor: mouseCursor,
466+
hoverColor: hoverColor,
467+
overlayColor: overlayColor,
468+
splashRadius: splashRadius,
469+
),
468470
);
469471
case _RadioType.adaptive:
470-
control = Radio<T>.adaptive(
471-
value: value,
472-
groupValue: groupValue,
473-
onChanged: onChanged,
474-
toggleable: toggleable,
475-
activeColor: activeColor,
476-
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
477-
autofocus: autofocus,
478-
fillColor: fillColor,
479-
mouseCursor: mouseCursor,
480-
hoverColor: hoverColor,
481-
overlayColor: overlayColor,
482-
splashRadius: splashRadius,
483-
useCupertinoCheckmarkStyle: useCupertinoCheckmarkStyle,
472+
control = ExcludeFocus(
473+
child: Radio<T>.adaptive(
474+
value: value,
475+
groupValue: groupValue,
476+
onChanged: onChanged,
477+
toggleable: toggleable,
478+
activeColor: activeColor,
479+
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
480+
autofocus: autofocus,
481+
fillColor: fillColor,
482+
mouseCursor: mouseCursor,
483+
hoverColor: hoverColor,
484+
overlayColor: overlayColor,
485+
splashRadius: splashRadius,
486+
useCupertinoCheckmarkStyle: useCupertinoCheckmarkStyle,
487+
),
484488
);
485489
}
486490

packages/flutter/lib/src/material/switch_list_tile.dart

Lines changed: 49 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -511,54 +511,58 @@ class SwitchListTile extends StatelessWidget {
511511
final Widget control;
512512
switch (_switchListTileType) {
513513
case _SwitchListTileType.adaptive:
514-
control = Switch.adaptive(
515-
value: value,
516-
onChanged: onChanged,
517-
activeColor: activeColor,
518-
activeThumbImage: activeThumbImage,
519-
inactiveThumbImage: inactiveThumbImage,
520-
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
521-
activeTrackColor: activeTrackColor,
522-
inactiveTrackColor: inactiveTrackColor,
523-
inactiveThumbColor: inactiveThumbColor,
524-
autofocus: autofocus,
525-
onFocusChange: onFocusChange,
526-
onActiveThumbImageError: onActiveThumbImageError,
527-
onInactiveThumbImageError: onInactiveThumbImageError,
528-
thumbColor: thumbColor,
529-
trackColor: trackColor,
530-
trackOutlineColor: trackOutlineColor,
531-
thumbIcon: thumbIcon,
532-
applyCupertinoTheme: applyCupertinoTheme,
533-
dragStartBehavior: dragStartBehavior,
534-
mouseCursor: mouseCursor,
535-
splashRadius: splashRadius,
536-
overlayColor: overlayColor,
514+
control = ExcludeFocus(
515+
child: Switch.adaptive(
516+
value: value,
517+
onChanged: onChanged,
518+
activeColor: activeColor,
519+
activeThumbImage: activeThumbImage,
520+
inactiveThumbImage: inactiveThumbImage,
521+
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
522+
activeTrackColor: activeTrackColor,
523+
inactiveTrackColor: inactiveTrackColor,
524+
inactiveThumbColor: inactiveThumbColor,
525+
autofocus: autofocus,
526+
onFocusChange: onFocusChange,
527+
onActiveThumbImageError: onActiveThumbImageError,
528+
onInactiveThumbImageError: onInactiveThumbImageError,
529+
thumbColor: thumbColor,
530+
trackColor: trackColor,
531+
trackOutlineColor: trackOutlineColor,
532+
thumbIcon: thumbIcon,
533+
applyCupertinoTheme: applyCupertinoTheme,
534+
dragStartBehavior: dragStartBehavior,
535+
mouseCursor: mouseCursor,
536+
splashRadius: splashRadius,
537+
overlayColor: overlayColor,
538+
),
537539
);
538540

539541
case _SwitchListTileType.material:
540-
control = Switch(
541-
value: value,
542-
onChanged: onChanged,
543-
activeColor: activeColor,
544-
activeThumbImage: activeThumbImage,
545-
inactiveThumbImage: inactiveThumbImage,
546-
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
547-
activeTrackColor: activeTrackColor,
548-
inactiveTrackColor: inactiveTrackColor,
549-
inactiveThumbColor: inactiveThumbColor,
550-
autofocus: autofocus,
551-
onFocusChange: onFocusChange,
552-
onActiveThumbImageError: onActiveThumbImageError,
553-
onInactiveThumbImageError: onInactiveThumbImageError,
554-
thumbColor: thumbColor,
555-
trackColor: trackColor,
556-
trackOutlineColor: trackOutlineColor,
557-
thumbIcon: thumbIcon,
558-
dragStartBehavior: dragStartBehavior,
559-
mouseCursor: mouseCursor,
560-
splashRadius: splashRadius,
561-
overlayColor: overlayColor,
542+
control = ExcludeFocus(
543+
child: Switch(
544+
value: value,
545+
onChanged: onChanged,
546+
activeColor: activeColor,
547+
activeThumbImage: activeThumbImage,
548+
inactiveThumbImage: inactiveThumbImage,
549+
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
550+
activeTrackColor: activeTrackColor,
551+
inactiveTrackColor: inactiveTrackColor,
552+
inactiveThumbColor: inactiveThumbColor,
553+
autofocus: autofocus,
554+
onFocusChange: onFocusChange,
555+
onActiveThumbImageError: onActiveThumbImageError,
556+
onInactiveThumbImageError: onInactiveThumbImageError,
557+
thumbColor: thumbColor,
558+
trackColor: trackColor,
559+
trackOutlineColor: trackOutlineColor,
560+
thumbIcon: thumbIcon,
561+
dragStartBehavior: dragStartBehavior,
562+
mouseCursor: mouseCursor,
563+
splashRadius: splashRadius,
564+
overlayColor: overlayColor,
565+
),
562566
);
563567
}
564568

packages/flutter/test/material/checkbox_list_tile_test.dart

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,6 +1194,41 @@ void main() {
11941194

11951195
handle.dispose();
11961196
});
1197+
1198+
testWidgets('CheckboxListTile.control widget should not request focus on traversal', (WidgetTester tester) async {
1199+
final GlobalKey firstChildKey = GlobalKey();
1200+
final GlobalKey secondChildKey = GlobalKey();
1201+
1202+
await tester.pumpWidget(
1203+
MaterialApp(
1204+
home: Material(
1205+
child: Column(
1206+
children: <Widget>[
1207+
CheckboxListTile(
1208+
value: true,
1209+
onChanged: (bool? value) {},
1210+
title: Text('Hey', key: firstChildKey),
1211+
),
1212+
CheckboxListTile(
1213+
value: true,
1214+
onChanged: (bool? value) {},
1215+
title: Text('There', key: secondChildKey),
1216+
),
1217+
],
1218+
),
1219+
),
1220+
),
1221+
);
1222+
1223+
await tester.pump();
1224+
Focus.of(firstChildKey.currentContext!).requestFocus();
1225+
await tester.pump();
1226+
expect(Focus.of(firstChildKey.currentContext!).hasPrimaryFocus, isTrue);
1227+
Focus.of(firstChildKey.currentContext!).nextFocus();
1228+
await tester.pump();
1229+
expect(Focus.of(firstChildKey.currentContext!).hasPrimaryFocus, isFalse);
1230+
expect(Focus.of(secondChildKey.currentContext!).hasPrimaryFocus, isTrue);
1231+
});
11971232
}
11981233

11991234
class _SelectedGrabMouseCursor extends MaterialStateMouseCursor {

packages/flutter/test/material/radio_list_tile_test.dart

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,6 +1260,43 @@ void main() {
12601260
expect(tester.getSize(find.byType(Radio<bool>)), const Size(48.0, 48.0));
12611261
});
12621262

1263+
testWidgets('RadioListTile.control widget should not request focus on traversal', (WidgetTester tester) async {
1264+
final GlobalKey firstChildKey = GlobalKey();
1265+
final GlobalKey secondChildKey = GlobalKey();
1266+
1267+
await tester.pumpWidget(
1268+
MaterialApp(
1269+
home: Material(
1270+
child: Column(
1271+
children: <Widget>[
1272+
RadioListTile<bool>(
1273+
value: true,
1274+
groupValue: true,
1275+
onChanged: (bool? value) {},
1276+
title: Text('Hey', key: firstChildKey),
1277+
),
1278+
RadioListTile<bool>(
1279+
value: true,
1280+
groupValue: true,
1281+
onChanged: (bool? value) {},
1282+
title: Text('There', key: secondChildKey),
1283+
),
1284+
],
1285+
),
1286+
),
1287+
),
1288+
);
1289+
1290+
await tester.pump();
1291+
Focus.of(firstChildKey.currentContext!).requestFocus();
1292+
await tester.pump();
1293+
expect(Focus.of(firstChildKey.currentContext!).hasPrimaryFocus, isTrue);
1294+
Focus.of(firstChildKey.currentContext!).nextFocus();
1295+
await tester.pump();
1296+
expect(Focus.of(firstChildKey.currentContext!).hasPrimaryFocus, isFalse);
1297+
expect(Focus.of(secondChildKey.currentContext!).hasPrimaryFocus, isTrue);
1298+
});
1299+
12631300
testWidgets('RadioListTile.adaptive shows the correct radio platform widget', (WidgetTester tester) async {
12641301
Widget buildApp(TargetPlatform platform) {
12651302
return MaterialApp(

0 commit comments

Comments
 (0)