Fix dual focus issue in CheckboxListTile, RadioListTile and SwitchListTile (#143213)
These widgets can now only receive focus once when tabbing through the focus tree.
This commit is contained in:
parent
ace3e58f0a
commit
49f620d8ea
@ -475,42 +475,46 @@ class CheckboxListTile extends StatelessWidget {
|
||||
|
||||
switch (_checkboxType) {
|
||||
case _CheckboxType.material:
|
||||
control = Checkbox(
|
||||
value: value,
|
||||
onChanged: enabled ?? true ? onChanged : null,
|
||||
mouseCursor: mouseCursor,
|
||||
activeColor: activeColor,
|
||||
fillColor: fillColor,
|
||||
checkColor: checkColor,
|
||||
hoverColor: hoverColor,
|
||||
overlayColor: overlayColor,
|
||||
splashRadius: splashRadius,
|
||||
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
|
||||
autofocus: autofocus,
|
||||
tristate: tristate,
|
||||
shape: checkboxShape,
|
||||
side: side,
|
||||
isError: isError,
|
||||
semanticLabel: checkboxSemanticLabel,
|
||||
control = ExcludeFocus(
|
||||
child: Checkbox(
|
||||
value: value,
|
||||
onChanged: enabled ?? true ? onChanged : null,
|
||||
mouseCursor: mouseCursor,
|
||||
activeColor: activeColor,
|
||||
fillColor: fillColor,
|
||||
checkColor: checkColor,
|
||||
hoverColor: hoverColor,
|
||||
overlayColor: overlayColor,
|
||||
splashRadius: splashRadius,
|
||||
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
|
||||
autofocus: autofocus,
|
||||
tristate: tristate,
|
||||
shape: checkboxShape,
|
||||
side: side,
|
||||
isError: isError,
|
||||
semanticLabel: checkboxSemanticLabel,
|
||||
),
|
||||
);
|
||||
case _CheckboxType.adaptive:
|
||||
control = Checkbox.adaptive(
|
||||
value: value,
|
||||
onChanged: enabled ?? true ? onChanged : null,
|
||||
mouseCursor: mouseCursor,
|
||||
activeColor: activeColor,
|
||||
fillColor: fillColor,
|
||||
checkColor: checkColor,
|
||||
hoverColor: hoverColor,
|
||||
overlayColor: overlayColor,
|
||||
splashRadius: splashRadius,
|
||||
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
|
||||
autofocus: autofocus,
|
||||
tristate: tristate,
|
||||
shape: checkboxShape,
|
||||
side: side,
|
||||
isError: isError,
|
||||
semanticLabel: checkboxSemanticLabel,
|
||||
control = ExcludeFocus(
|
||||
child: Checkbox.adaptive(
|
||||
value: value,
|
||||
onChanged: enabled ?? true ? onChanged : null,
|
||||
mouseCursor: mouseCursor,
|
||||
activeColor: activeColor,
|
||||
fillColor: fillColor,
|
||||
checkColor: checkColor,
|
||||
hoverColor: hoverColor,
|
||||
overlayColor: overlayColor,
|
||||
splashRadius: splashRadius,
|
||||
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
|
||||
autofocus: autofocus,
|
||||
tristate: tristate,
|
||||
shape: checkboxShape,
|
||||
side: side,
|
||||
isError: isError,
|
||||
semanticLabel: checkboxSemanticLabel,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -452,35 +452,39 @@ class RadioListTile<T> extends StatelessWidget {
|
||||
final Widget control;
|
||||
switch (_radioType) {
|
||||
case _RadioType.material:
|
||||
control = Radio<T>(
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged,
|
||||
toggleable: toggleable,
|
||||
activeColor: activeColor,
|
||||
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
|
||||
autofocus: autofocus,
|
||||
fillColor: fillColor,
|
||||
mouseCursor: mouseCursor,
|
||||
hoverColor: hoverColor,
|
||||
overlayColor: overlayColor,
|
||||
splashRadius: splashRadius,
|
||||
control = ExcludeFocus(
|
||||
child: Radio<T>(
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged,
|
||||
toggleable: toggleable,
|
||||
activeColor: activeColor,
|
||||
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
|
||||
autofocus: autofocus,
|
||||
fillColor: fillColor,
|
||||
mouseCursor: mouseCursor,
|
||||
hoverColor: hoverColor,
|
||||
overlayColor: overlayColor,
|
||||
splashRadius: splashRadius,
|
||||
),
|
||||
);
|
||||
case _RadioType.adaptive:
|
||||
control = Radio<T>.adaptive(
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged,
|
||||
toggleable: toggleable,
|
||||
activeColor: activeColor,
|
||||
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
|
||||
autofocus: autofocus,
|
||||
fillColor: fillColor,
|
||||
mouseCursor: mouseCursor,
|
||||
hoverColor: hoverColor,
|
||||
overlayColor: overlayColor,
|
||||
splashRadius: splashRadius,
|
||||
useCupertinoCheckmarkStyle: useCupertinoCheckmarkStyle,
|
||||
control = ExcludeFocus(
|
||||
child: Radio<T>.adaptive(
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged,
|
||||
toggleable: toggleable,
|
||||
activeColor: activeColor,
|
||||
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
|
||||
autofocus: autofocus,
|
||||
fillColor: fillColor,
|
||||
mouseCursor: mouseCursor,
|
||||
hoverColor: hoverColor,
|
||||
overlayColor: overlayColor,
|
||||
splashRadius: splashRadius,
|
||||
useCupertinoCheckmarkStyle: useCupertinoCheckmarkStyle,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -511,54 +511,58 @@ class SwitchListTile extends StatelessWidget {
|
||||
final Widget control;
|
||||
switch (_switchListTileType) {
|
||||
case _SwitchListTileType.adaptive:
|
||||
control = Switch.adaptive(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
activeColor: activeColor,
|
||||
activeThumbImage: activeThumbImage,
|
||||
inactiveThumbImage: inactiveThumbImage,
|
||||
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
|
||||
activeTrackColor: activeTrackColor,
|
||||
inactiveTrackColor: inactiveTrackColor,
|
||||
inactiveThumbColor: inactiveThumbColor,
|
||||
autofocus: autofocus,
|
||||
onFocusChange: onFocusChange,
|
||||
onActiveThumbImageError: onActiveThumbImageError,
|
||||
onInactiveThumbImageError: onInactiveThumbImageError,
|
||||
thumbColor: thumbColor,
|
||||
trackColor: trackColor,
|
||||
trackOutlineColor: trackOutlineColor,
|
||||
thumbIcon: thumbIcon,
|
||||
applyCupertinoTheme: applyCupertinoTheme,
|
||||
dragStartBehavior: dragStartBehavior,
|
||||
mouseCursor: mouseCursor,
|
||||
splashRadius: splashRadius,
|
||||
overlayColor: overlayColor,
|
||||
control = ExcludeFocus(
|
||||
child: Switch.adaptive(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
activeColor: activeColor,
|
||||
activeThumbImage: activeThumbImage,
|
||||
inactiveThumbImage: inactiveThumbImage,
|
||||
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
|
||||
activeTrackColor: activeTrackColor,
|
||||
inactiveTrackColor: inactiveTrackColor,
|
||||
inactiveThumbColor: inactiveThumbColor,
|
||||
autofocus: autofocus,
|
||||
onFocusChange: onFocusChange,
|
||||
onActiveThumbImageError: onActiveThumbImageError,
|
||||
onInactiveThumbImageError: onInactiveThumbImageError,
|
||||
thumbColor: thumbColor,
|
||||
trackColor: trackColor,
|
||||
trackOutlineColor: trackOutlineColor,
|
||||
thumbIcon: thumbIcon,
|
||||
applyCupertinoTheme: applyCupertinoTheme,
|
||||
dragStartBehavior: dragStartBehavior,
|
||||
mouseCursor: mouseCursor,
|
||||
splashRadius: splashRadius,
|
||||
overlayColor: overlayColor,
|
||||
),
|
||||
);
|
||||
|
||||
case _SwitchListTileType.material:
|
||||
control = Switch(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
activeColor: activeColor,
|
||||
activeThumbImage: activeThumbImage,
|
||||
inactiveThumbImage: inactiveThumbImage,
|
||||
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
|
||||
activeTrackColor: activeTrackColor,
|
||||
inactiveTrackColor: inactiveTrackColor,
|
||||
inactiveThumbColor: inactiveThumbColor,
|
||||
autofocus: autofocus,
|
||||
onFocusChange: onFocusChange,
|
||||
onActiveThumbImageError: onActiveThumbImageError,
|
||||
onInactiveThumbImageError: onInactiveThumbImageError,
|
||||
thumbColor: thumbColor,
|
||||
trackColor: trackColor,
|
||||
trackOutlineColor: trackOutlineColor,
|
||||
thumbIcon: thumbIcon,
|
||||
dragStartBehavior: dragStartBehavior,
|
||||
mouseCursor: mouseCursor,
|
||||
splashRadius: splashRadius,
|
||||
overlayColor: overlayColor,
|
||||
control = ExcludeFocus(
|
||||
child: Switch(
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
activeColor: activeColor,
|
||||
activeThumbImage: activeThumbImage,
|
||||
inactiveThumbImage: inactiveThumbImage,
|
||||
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
|
||||
activeTrackColor: activeTrackColor,
|
||||
inactiveTrackColor: inactiveTrackColor,
|
||||
inactiveThumbColor: inactiveThumbColor,
|
||||
autofocus: autofocus,
|
||||
onFocusChange: onFocusChange,
|
||||
onActiveThumbImageError: onActiveThumbImageError,
|
||||
onInactiveThumbImageError: onInactiveThumbImageError,
|
||||
thumbColor: thumbColor,
|
||||
trackColor: trackColor,
|
||||
trackOutlineColor: trackOutlineColor,
|
||||
thumbIcon: thumbIcon,
|
||||
dragStartBehavior: dragStartBehavior,
|
||||
mouseCursor: mouseCursor,
|
||||
splashRadius: splashRadius,
|
||||
overlayColor: overlayColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1194,6 +1194,41 @@ void main() {
|
||||
|
||||
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 {
|
||||
|
@ -1260,6 +1260,43 @@ void main() {
|
||||
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 {
|
||||
Widget buildApp(TargetPlatform platform) {
|
||||
return MaterialApp(
|
||||
|
@ -359,7 +359,19 @@ void main() {
|
||||
final ListTile listTile = tester.widget(find.byType(ListTile));
|
||||
// When controlAffinity is ListTileControlAffinity.leading, the position of
|
||||
// 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);
|
||||
});
|
||||
|
||||
@ -379,8 +391,20 @@ void main() {
|
||||
// By default, value of controlAffinity is ListTileControlAffinity.platform,
|
||||
// where the position of SwitchListTile.secondary is at leading edge and Switch
|
||||
// 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.trailing.runtimeType, Switch);
|
||||
});
|
||||
|
||||
testWidgets('SwitchListTile respects shape', (WidgetTester tester) async {
|
||||
@ -1632,4 +1656,39 @@ void main() {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user