Unverified Commit 49f620d8 authored by Nitesh Sharma's avatar Nitesh Sharma Committed by GitHub

Fix dual focus issue in CheckboxListTile, RadioListTile and SwitchListTile (#143213)

These widgets can now only receive focus once when tabbing through the focus tree.
parent ace3e58f
...@@ -475,42 +475,46 @@ class CheckboxListTile extends StatelessWidget { ...@@ -475,42 +475,46 @@ class CheckboxListTile extends StatelessWidget {
switch (_checkboxType) { switch (_checkboxType) {
case _CheckboxType.material: case _CheckboxType.material:
control = Checkbox( control = ExcludeFocus(
value: value, child: Checkbox(
onChanged: enabled ?? true ? onChanged : null, value: value,
mouseCursor: mouseCursor, onChanged: enabled ?? true ? onChanged : null,
activeColor: activeColor, mouseCursor: mouseCursor,
fillColor: fillColor, activeColor: activeColor,
checkColor: checkColor, fillColor: fillColor,
hoverColor: hoverColor, checkColor: checkColor,
overlayColor: overlayColor, hoverColor: hoverColor,
splashRadius: splashRadius, overlayColor: overlayColor,
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap, splashRadius: splashRadius,
autofocus: autofocus, materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
tristate: tristate, autofocus: autofocus,
shape: checkboxShape, tristate: tristate,
side: side, shape: checkboxShape,
isError: isError, side: side,
semanticLabel: checkboxSemanticLabel, isError: isError,
semanticLabel: checkboxSemanticLabel,
),
); );
case _CheckboxType.adaptive: case _CheckboxType.adaptive:
control = Checkbox.adaptive( control = ExcludeFocus(
value: value, child: Checkbox.adaptive(
onChanged: enabled ?? true ? onChanged : null, value: value,
mouseCursor: mouseCursor, onChanged: enabled ?? true ? onChanged : null,
activeColor: activeColor, mouseCursor: mouseCursor,
fillColor: fillColor, activeColor: activeColor,
checkColor: checkColor, fillColor: fillColor,
hoverColor: hoverColor, checkColor: checkColor,
overlayColor: overlayColor, hoverColor: hoverColor,
splashRadius: splashRadius, overlayColor: overlayColor,
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap, splashRadius: splashRadius,
autofocus: autofocus, materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
tristate: tristate, autofocus: autofocus,
shape: checkboxShape, tristate: tristate,
side: side, shape: checkboxShape,
isError: isError, side: side,
semanticLabel: checkboxSemanticLabel, isError: isError,
semanticLabel: checkboxSemanticLabel,
),
); );
} }
......
...@@ -452,35 +452,39 @@ class RadioListTile<T> extends StatelessWidget { ...@@ -452,35 +452,39 @@ class RadioListTile<T> extends StatelessWidget {
final Widget control; final Widget control;
switch (_radioType) { switch (_radioType) {
case _RadioType.material: case _RadioType.material:
control = Radio<T>( control = ExcludeFocus(
value: value, child: Radio<T>(
groupValue: groupValue, value: value,
onChanged: onChanged, groupValue: groupValue,
toggleable: toggleable, onChanged: onChanged,
activeColor: activeColor, toggleable: toggleable,
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap, activeColor: activeColor,
autofocus: autofocus, materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
fillColor: fillColor, autofocus: autofocus,
mouseCursor: mouseCursor, fillColor: fillColor,
hoverColor: hoverColor, mouseCursor: mouseCursor,
overlayColor: overlayColor, hoverColor: hoverColor,
splashRadius: splashRadius, overlayColor: overlayColor,
splashRadius: splashRadius,
),
); );
case _RadioType.adaptive: case _RadioType.adaptive:
control = Radio<T>.adaptive( control = ExcludeFocus(
value: value, child: Radio<T>.adaptive(
groupValue: groupValue, value: value,
onChanged: onChanged, groupValue: groupValue,
toggleable: toggleable, onChanged: onChanged,
activeColor: activeColor, toggleable: toggleable,
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap, activeColor: activeColor,
autofocus: autofocus, materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
fillColor: fillColor, autofocus: autofocus,
mouseCursor: mouseCursor, fillColor: fillColor,
hoverColor: hoverColor, mouseCursor: mouseCursor,
overlayColor: overlayColor, hoverColor: hoverColor,
splashRadius: splashRadius, overlayColor: overlayColor,
useCupertinoCheckmarkStyle: useCupertinoCheckmarkStyle, splashRadius: splashRadius,
useCupertinoCheckmarkStyle: useCupertinoCheckmarkStyle,
),
); );
} }
......
...@@ -511,54 +511,58 @@ class SwitchListTile extends StatelessWidget { ...@@ -511,54 +511,58 @@ class SwitchListTile extends StatelessWidget {
final Widget control; final Widget control;
switch (_switchListTileType) { switch (_switchListTileType) {
case _SwitchListTileType.adaptive: case _SwitchListTileType.adaptive:
control = Switch.adaptive( control = ExcludeFocus(
value: value, child: Switch.adaptive(
onChanged: onChanged, value: value,
activeColor: activeColor, onChanged: onChanged,
activeThumbImage: activeThumbImage, activeColor: activeColor,
inactiveThumbImage: inactiveThumbImage, activeThumbImage: activeThumbImage,
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap, inactiveThumbImage: inactiveThumbImage,
activeTrackColor: activeTrackColor, materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
inactiveTrackColor: inactiveTrackColor, activeTrackColor: activeTrackColor,
inactiveThumbColor: inactiveThumbColor, inactiveTrackColor: inactiveTrackColor,
autofocus: autofocus, inactiveThumbColor: inactiveThumbColor,
onFocusChange: onFocusChange, autofocus: autofocus,
onActiveThumbImageError: onActiveThumbImageError, onFocusChange: onFocusChange,
onInactiveThumbImageError: onInactiveThumbImageError, onActiveThumbImageError: onActiveThumbImageError,
thumbColor: thumbColor, onInactiveThumbImageError: onInactiveThumbImageError,
trackColor: trackColor, thumbColor: thumbColor,
trackOutlineColor: trackOutlineColor, trackColor: trackColor,
thumbIcon: thumbIcon, trackOutlineColor: trackOutlineColor,
applyCupertinoTheme: applyCupertinoTheme, thumbIcon: thumbIcon,
dragStartBehavior: dragStartBehavior, applyCupertinoTheme: applyCupertinoTheme,
mouseCursor: mouseCursor, dragStartBehavior: dragStartBehavior,
splashRadius: splashRadius, mouseCursor: mouseCursor,
overlayColor: overlayColor, splashRadius: splashRadius,
overlayColor: overlayColor,
),
); );
case _SwitchListTileType.material: case _SwitchListTileType.material:
control = Switch( control = ExcludeFocus(
value: value, child: Switch(
onChanged: onChanged, value: value,
activeColor: activeColor, onChanged: onChanged,
activeThumbImage: activeThumbImage, activeColor: activeColor,
inactiveThumbImage: inactiveThumbImage, activeThumbImage: activeThumbImage,
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap, inactiveThumbImage: inactiveThumbImage,
activeTrackColor: activeTrackColor, materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
inactiveTrackColor: inactiveTrackColor, activeTrackColor: activeTrackColor,
inactiveThumbColor: inactiveThumbColor, inactiveTrackColor: inactiveTrackColor,
autofocus: autofocus, inactiveThumbColor: inactiveThumbColor,
onFocusChange: onFocusChange, autofocus: autofocus,
onActiveThumbImageError: onActiveThumbImageError, onFocusChange: onFocusChange,
onInactiveThumbImageError: onInactiveThumbImageError, onActiveThumbImageError: onActiveThumbImageError,
thumbColor: thumbColor, onInactiveThumbImageError: onInactiveThumbImageError,
trackColor: trackColor, thumbColor: thumbColor,
trackOutlineColor: trackOutlineColor, trackColor: trackColor,
thumbIcon: thumbIcon, trackOutlineColor: trackOutlineColor,
dragStartBehavior: dragStartBehavior, thumbIcon: thumbIcon,
mouseCursor: mouseCursor, dragStartBehavior: dragStartBehavior,
splashRadius: splashRadius, mouseCursor: mouseCursor,
overlayColor: overlayColor, splashRadius: splashRadius,
overlayColor: overlayColor,
),
); );
} }
......
...@@ -1194,6 +1194,41 @@ void main() { ...@@ -1194,6 +1194,41 @@ void main() {
handle.dispose(); handle.dispose();
}); });
testWidgets('CheckboxListTile.control widget should not request focus on traversal', (WidgetTester tester) async {
final GlobalKey firstChildKey = GlobalKey();
final GlobalKey secondChildKey = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
CheckboxListTile(
value: true,
onChanged: (bool? value) {},
title: Text('Hey', key: firstChildKey),
),
CheckboxListTile(
value: true,
onChanged: (bool? value) {},
title: Text('There', key: secondChildKey),
),
],
),
),
),
);
await tester.pump();
Focus.of(firstChildKey.currentContext!).requestFocus();
await tester.pump();
expect(Focus.of(firstChildKey.currentContext!).hasPrimaryFocus, isTrue);
Focus.of(firstChildKey.currentContext!).nextFocus();
await tester.pump();
expect(Focus.of(firstChildKey.currentContext!).hasPrimaryFocus, isFalse);
expect(Focus.of(secondChildKey.currentContext!).hasPrimaryFocus, isTrue);
});
} }
class _SelectedGrabMouseCursor extends MaterialStateMouseCursor { class _SelectedGrabMouseCursor extends MaterialStateMouseCursor {
......
...@@ -1260,6 +1260,43 @@ void main() { ...@@ -1260,6 +1260,43 @@ void main() {
expect(tester.getSize(find.byType(Radio<bool>)), const Size(48.0, 48.0)); expect(tester.getSize(find.byType(Radio<bool>)), const Size(48.0, 48.0));
}); });
testWidgets('RadioListTile.control widget should not request focus on traversal', (WidgetTester tester) async {
final GlobalKey firstChildKey = GlobalKey();
final GlobalKey secondChildKey = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
RadioListTile<bool>(
value: true,
groupValue: true,
onChanged: (bool? value) {},
title: Text('Hey', key: firstChildKey),
),
RadioListTile<bool>(
value: true,
groupValue: true,
onChanged: (bool? value) {},
title: Text('There', key: secondChildKey),
),
],
),
),
),
);
await tester.pump();
Focus.of(firstChildKey.currentContext!).requestFocus();
await tester.pump();
expect(Focus.of(firstChildKey.currentContext!).hasPrimaryFocus, isTrue);
Focus.of(firstChildKey.currentContext!).nextFocus();
await tester.pump();
expect(Focus.of(firstChildKey.currentContext!).hasPrimaryFocus, isFalse);
expect(Focus.of(secondChildKey.currentContext!).hasPrimaryFocus, isTrue);
});
testWidgets('RadioListTile.adaptive shows the correct radio platform widget', (WidgetTester tester) async { testWidgets('RadioListTile.adaptive shows the correct radio platform widget', (WidgetTester tester) async {
Widget buildApp(TargetPlatform platform) { Widget buildApp(TargetPlatform platform) {
return MaterialApp( return MaterialApp(
......
...@@ -359,7 +359,19 @@ void main() { ...@@ -359,7 +359,19 @@ void main() {
final ListTile listTile = tester.widget(find.byType(ListTile)); final ListTile listTile = tester.widget(find.byType(ListTile));
// When controlAffinity is ListTileControlAffinity.leading, the position of // When controlAffinity is ListTileControlAffinity.leading, the position of
// Switch is at leading edge and SwitchListTile.secondary at trailing edge. // Switch is at leading edge and SwitchListTile.secondary at trailing edge.
expect(listTile.leading.runtimeType, Switch);
// Find the ExcludeFocus widget within the ListTile's leading
final ExcludeFocus excludeFocusWidget = tester.widget(
find.byWidgetPredicate((Widget widget) => listTile.leading == widget && widget is ExcludeFocus),
);
// Assert that the ExcludeFocus widget is not null
expect(excludeFocusWidget, isNotNull);
// Assert that the child of ExcludeFocus is Switch
expect(excludeFocusWidget.child.runtimeType, Switch);
// Assert that the trailing is Icon
expect(listTile.trailing.runtimeType, Icon); expect(listTile.trailing.runtimeType, Icon);
}); });
...@@ -379,8 +391,20 @@ void main() { ...@@ -379,8 +391,20 @@ void main() {
// By default, value of controlAffinity is ListTileControlAffinity.platform, // By default, value of controlAffinity is ListTileControlAffinity.platform,
// where the position of SwitchListTile.secondary is at leading edge and Switch // where the position of SwitchListTile.secondary is at leading edge and Switch
// at trailing edge. This also covers test for ListTileControlAffinity.trailing. // at trailing edge. This also covers test for ListTileControlAffinity.trailing.
// Find the ExcludeFocus widget within the ListTile's trailing
final ExcludeFocus excludeFocusWidget = tester.widget(
find.byWidgetPredicate((Widget widget) => listTile.trailing == widget && widget is ExcludeFocus),
);
// Assert that the ExcludeFocus widget is not null
expect(excludeFocusWidget, isNotNull);
// Assert that the child of ExcludeFocus is Switch
expect(excludeFocusWidget.child.runtimeType, Switch);
// Assert that the leading is Icon
expect(listTile.leading.runtimeType, Icon); expect(listTile.leading.runtimeType, Icon);
expect(listTile.trailing.runtimeType, Switch);
}); });
testWidgets('SwitchListTile respects shape', (WidgetTester tester) async { testWidgets('SwitchListTile respects shape', (WidgetTester tester) async {
...@@ -1632,4 +1656,39 @@ void main() { ...@@ -1632,4 +1656,39 @@ void main() {
paints..rrect()..rrect(color: hoveredTrackColor, style: PaintingStyle.stroke) paints..rrect()..rrect(color: hoveredTrackColor, style: PaintingStyle.stroke)
); );
}); });
testWidgets('SwitchListTile.control widget should not request focus on traversal', (WidgetTester tester) async {
final GlobalKey firstChildKey = GlobalKey();
final GlobalKey secondChildKey = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
SwitchListTile(
value: true,
onChanged: (bool? value) {},
title: Text('Hey', key: firstChildKey),
),
SwitchListTile(
value: true,
onChanged: (bool? value) {},
title: Text('There', key: secondChildKey),
),
],
),
),
),
);
await tester.pump();
Focus.of(firstChildKey.currentContext!).requestFocus();
await tester.pump();
expect(Focus.of(firstChildKey.currentContext!).hasPrimaryFocus, isTrue);
Focus.of(firstChildKey.currentContext!).nextFocus();
await tester.pump();
expect(Focus.of(firstChildKey.currentContext!).hasPrimaryFocus, isFalse);
expect(Focus.of(secondChildKey.currentContext!).hasPrimaryFocus, isTrue);
});
} }
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