Unverified Commit cccfe96e authored by EricEnslen's avatar EricEnslen Committed by GitHub

Allow ListTiles to be autofocused (#54229)

Adds an "autofocus" param to ListTile and its Checkbox, Radio and Switch variants, and passes the given value through to the wrapped InkWell, Switch, etc.

This is important for scenarios like a settings screen, where the first interactable element on a page may be a SwitchListTile, for example.
parent 627cca79
...@@ -266,11 +266,13 @@ class CheckboxListTile extends StatelessWidget { ...@@ -266,11 +266,13 @@ class CheckboxListTile extends StatelessWidget {
this.secondary, this.secondary,
this.selected = false, this.selected = false,
this.controlAffinity = ListTileControlAffinity.platform, this.controlAffinity = ListTileControlAffinity.platform,
this.autofocus = false,
}) : assert(value != null), }) : assert(value != null),
assert(isThreeLine != null), assert(isThreeLine != null),
assert(!isThreeLine || subtitle != null), assert(!isThreeLine || subtitle != null),
assert(selected != null), assert(selected != null),
assert(controlAffinity != null), assert(controlAffinity != null),
assert(autofocus != null),
super(key: key); super(key: key);
/// Whether this checkbox is checked. /// Whether this checkbox is checked.
...@@ -351,6 +353,9 @@ class CheckboxListTile extends StatelessWidget { ...@@ -351,6 +353,9 @@ class CheckboxListTile extends StatelessWidget {
/// Where to place the control relative to the text. /// Where to place the control relative to the text.
final ListTileControlAffinity controlAffinity; final ListTileControlAffinity controlAffinity;
/// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Widget control = Checkbox( final Widget control = Checkbox(
...@@ -359,6 +364,7 @@ class CheckboxListTile extends StatelessWidget { ...@@ -359,6 +364,7 @@ class CheckboxListTile extends StatelessWidget {
activeColor: activeColor, activeColor: activeColor,
checkColor: checkColor, checkColor: checkColor,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
autofocus: autofocus,
); );
Widget leading, trailing; Widget leading, trailing;
switch (controlAffinity) { switch (controlAffinity) {
...@@ -385,6 +391,7 @@ class CheckboxListTile extends StatelessWidget { ...@@ -385,6 +391,7 @@ class CheckboxListTile extends StatelessWidget {
enabled: onChanged != null, enabled: onChanged != null,
onTap: onChanged != null ? () { onChanged(!value); } : null, onTap: onChanged != null ? () { onChanged(!value); } : null,
selected: selected, selected: selected,
autofocus: autofocus,
), ),
), ),
); );
......
...@@ -638,9 +638,11 @@ class ListTile extends StatelessWidget { ...@@ -638,9 +638,11 @@ class ListTile extends StatelessWidget {
this.onTap, this.onTap,
this.onLongPress, this.onLongPress,
this.selected = false, this.selected = false,
this.autofocus = false,
}) : assert(isThreeLine != null), }) : assert(isThreeLine != null),
assert(enabled != null), assert(enabled != null),
assert(selected != null), assert(selected != null),
assert(autofocus != null),
assert(!isThreeLine || subtitle != null), assert(!isThreeLine || subtitle != null),
super(key: key); super(key: key);
...@@ -724,6 +726,9 @@ class ListTile extends StatelessWidget { ...@@ -724,6 +726,9 @@ class ListTile extends StatelessWidget {
/// can be overridden with a [ListTileTheme]. /// can be overridden with a [ListTileTheme].
final bool selected; final bool selected;
/// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus;
/// Add a one pixel border in between each tile. If color isn't specified the /// Add a one pixel border in between each tile. If color isn't specified the
/// [ThemeData.dividerColor] of the context's [Theme] is used. /// [ThemeData.dividerColor] of the context's [Theme] is used.
/// ///
...@@ -883,6 +888,7 @@ class ListTile extends StatelessWidget { ...@@ -883,6 +888,7 @@ class ListTile extends StatelessWidget {
onTap: enabled ? onTap : null, onTap: enabled ? onTap : null,
onLongPress: enabled ? onLongPress : null, onLongPress: enabled ? onLongPress : null,
canRequestFocus: enabled, canRequestFocus: enabled,
autofocus: autofocus,
child: Semantics( child: Semantics(
selected: selected, selected: selected,
enabled: enabled, enabled: enabled,
......
...@@ -318,12 +318,14 @@ class RadioListTile<T> extends StatelessWidget { ...@@ -318,12 +318,14 @@ class RadioListTile<T> extends StatelessWidget {
this.secondary, this.secondary,
this.selected = false, this.selected = false,
this.controlAffinity = ListTileControlAffinity.platform, this.controlAffinity = ListTileControlAffinity.platform,
this.autofocus = false,
}) : assert(toggleable != null), }) : assert(toggleable != null),
assert(isThreeLine != null), assert(isThreeLine != null),
assert(!isThreeLine || subtitle != null), assert(!isThreeLine || subtitle != null),
assert(selected != null), assert(selected != null),
assert(controlAffinity != null), assert(controlAffinity != null),
assert(autofocus != null),
super(key: key); super(key: key);
/// The value represented by this radio button. /// The value represented by this radio button.
...@@ -464,6 +466,9 @@ class RadioListTile<T> extends StatelessWidget { ...@@ -464,6 +466,9 @@ class RadioListTile<T> extends StatelessWidget {
/// Where to place the control relative to the text. /// Where to place the control relative to the text.
final ListTileControlAffinity controlAffinity; final ListTileControlAffinity controlAffinity;
/// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus;
/// Whether this radio button is checked. /// Whether this radio button is checked.
/// ///
/// To control this value, set [value] and [groupValue] appropriately. /// To control this value, set [value] and [groupValue] appropriately.
...@@ -478,6 +483,7 @@ class RadioListTile<T> extends StatelessWidget { ...@@ -478,6 +483,7 @@ class RadioListTile<T> extends StatelessWidget {
toggleable: toggleable, toggleable: toggleable,
activeColor: activeColor, activeColor: activeColor,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
autofocus: autofocus,
); );
Widget leading, trailing; Widget leading, trailing;
switch (controlAffinity) { switch (controlAffinity) {
...@@ -512,6 +518,7 @@ class RadioListTile<T> extends StatelessWidget { ...@@ -512,6 +518,7 @@ class RadioListTile<T> extends StatelessWidget {
} }
} : null, } : null,
selected: selected, selected: selected,
autofocus: autofocus,
), ),
), ),
); );
......
...@@ -272,11 +272,13 @@ class SwitchListTile extends StatelessWidget { ...@@ -272,11 +272,13 @@ class SwitchListTile extends StatelessWidget {
this.contentPadding, this.contentPadding,
this.secondary, this.secondary,
this.selected = false, this.selected = false,
this.autofocus = false,
}) : _switchListTileType = _SwitchListTileType.material, }) : _switchListTileType = _SwitchListTileType.material,
assert(value != null), assert(value != null),
assert(isThreeLine != null), assert(isThreeLine != null),
assert(!isThreeLine || subtitle != null), assert(!isThreeLine || subtitle != null),
assert(selected != null), assert(selected != null),
assert(autofocus != null),
super(key: key); super(key: key);
/// Creates the wrapped switch with [Switch.adaptive]. /// Creates the wrapped switch with [Switch.adaptive].
...@@ -304,11 +306,13 @@ class SwitchListTile extends StatelessWidget { ...@@ -304,11 +306,13 @@ class SwitchListTile extends StatelessWidget {
this.contentPadding, this.contentPadding,
this.secondary, this.secondary,
this.selected = false, this.selected = false,
this.autofocus = false,
}) : _switchListTileType = _SwitchListTileType.adaptive, }) : _switchListTileType = _SwitchListTileType.adaptive,
assert(value != null), assert(value != null),
assert(isThreeLine != null), assert(isThreeLine != null),
assert(!isThreeLine || subtitle != null), assert(!isThreeLine || subtitle != null),
assert(selected != null), assert(selected != null),
assert(autofocus != null),
super(key: key); super(key: key);
/// Whether this switch is checked. /// Whether this switch is checked.
...@@ -419,6 +423,9 @@ class SwitchListTile extends StatelessWidget { ...@@ -419,6 +423,9 @@ class SwitchListTile extends StatelessWidget {
/// Normally, this property is left to its default value, false. /// Normally, this property is left to its default value, false.
final bool selected; final bool selected;
/// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus;
/// If adaptive, creates the switch with [Switch.adaptive]. /// If adaptive, creates the switch with [Switch.adaptive].
final _SwitchListTileType _switchListTileType; final _SwitchListTileType _switchListTileType;
...@@ -437,6 +444,7 @@ class SwitchListTile extends StatelessWidget { ...@@ -437,6 +444,7 @@ class SwitchListTile extends StatelessWidget {
activeTrackColor: activeTrackColor, activeTrackColor: activeTrackColor,
inactiveTrackColor: inactiveTrackColor, inactiveTrackColor: inactiveTrackColor,
inactiveThumbColor: inactiveThumbColor, inactiveThumbColor: inactiveThumbColor,
autofocus: autofocus,
); );
break; break;
...@@ -451,6 +459,7 @@ class SwitchListTile extends StatelessWidget { ...@@ -451,6 +459,7 @@ class SwitchListTile extends StatelessWidget {
activeTrackColor: activeTrackColor, activeTrackColor: activeTrackColor,
inactiveTrackColor: inactiveTrackColor, inactiveTrackColor: inactiveTrackColor,
inactiveThumbColor: inactiveThumbColor, inactiveThumbColor: inactiveThumbColor,
autofocus: autofocus,
); );
} }
return MergeSemantics( return MergeSemantics(
...@@ -467,6 +476,7 @@ class SwitchListTile extends StatelessWidget { ...@@ -467,6 +476,7 @@ class SwitchListTile extends StatelessWidget {
enabled: onChanged != null, enabled: onChanged != null,
onTap: onChanged != null ? () { onChanged(!value); } : null, onTap: onChanged != null ? () { onChanged(!value); } : null,
selected: selected, selected: selected,
autofocus: autofocus,
), ),
), ),
); );
......
...@@ -82,4 +82,37 @@ void main() { ...@@ -82,4 +82,37 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(getCheckboxListTileRenderer(), paints..rrect(color: const Color(0xFFFFFFFF))); // paints's color is 0xFFFFFFFF (params) expect(getCheckboxListTileRenderer(), paints..rrect(color: const Color(0xFFFFFFFF))); // paints's color is 0xFFFFFFFF (params)
}); });
testWidgets('CheckboxListTile can autofocus unless disabled.', (WidgetTester tester) async {
final GlobalKey childKey = GlobalKey();
await tester.pumpWidget(
wrap(
child: CheckboxListTile(
value: true,
onChanged: (_) {},
title: Text('Hello', key: childKey),
autofocus: true,
),
),
);
await tester.pump();
expect(Focus.of(childKey.currentContext, nullOk: true).hasPrimaryFocus, isTrue);
await tester.pumpWidget(
wrap(
child: CheckboxListTile(
value: true,
onChanged: null,
title: Text('Hello', key: childKey),
autofocus: true,
),
),
);
await tester.pump();
expect(Focus.of(childKey.currentContext, nullOk: true).hasPrimaryFocus, isFalse);
});
} }
...@@ -1137,6 +1137,7 @@ void main() { ...@@ -1137,6 +1137,7 @@ void main() {
expect(tester.getRect(find.byType(Placeholder).at(0)), const Rect.fromLTWH(800.0 - 16.0 - 24.0, 16.0, 24.0, 56.0)); expect(tester.getRect(find.byType(Placeholder).at(0)), const Rect.fromLTWH(800.0 - 16.0 - 24.0, 16.0, 24.0, 56.0));
expect(tester.getRect(find.byType(Placeholder).at(1)), const Rect.fromLTWH(800.0 - 16.0 - 24.0, 88.0 + 16.0, 24.0, 56.0)); expect(tester.getRect(find.byType(Placeholder).at(1)), const Rect.fromLTWH(800.0 - 16.0 - 24.0, 88.0 + 16.0, 24.0, 56.0));
}); });
testWidgets('ListTile only accepts focus when enabled', (WidgetTester tester) async { testWidgets('ListTile only accepts focus when enabled', (WidgetTester tester) async {
final GlobalKey childKey = GlobalKey(); final GlobalKey childKey = GlobalKey();
...@@ -1184,4 +1185,50 @@ void main() { ...@@ -1184,4 +1185,50 @@ void main() {
expect(tester.binding.focusManager.primaryFocus, isNot(equals(tileNode))); expect(tester.binding.focusManager.primaryFocus, isNot(equals(tileNode)));
expect(Focus.of(childKey.currentContext, nullOk: true).hasPrimaryFocus, isFalse); expect(Focus.of(childKey.currentContext, nullOk: true).hasPrimaryFocus, isFalse);
}); });
testWidgets('ListTile can autofocus unless disabled.', (WidgetTester tester) async {
final GlobalKey childKey = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ListView(
children: <Widget>[
ListTile(
title: Text('A', key: childKey),
dense: true,
enabled: true,
autofocus: true,
onTap: () {},
),
],
),
),
),
);
await tester.pump();
expect(Focus.of(childKey.currentContext, nullOk: true).hasPrimaryFocus, isTrue);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ListView(
children: <Widget>[
ListTile(
title: Text('A', key: childKey),
dense: true,
enabled: false,
autofocus: true,
onTap: () {},
),
],
),
),
),
);
await tester.pump();
expect(Focus.of(childKey.currentContext, nullOk: true).hasPrimaryFocus, isFalse);
});
} }
...@@ -572,4 +572,38 @@ void main() { ...@@ -572,4 +572,38 @@ void main() {
semantics.dispose(); semantics.dispose();
SystemChannels.accessibility.setMockMessageHandler(null); SystemChannels.accessibility.setMockMessageHandler(null);
}); });
testWidgets('RadioListTile can autofocus unless disabled.', (WidgetTester tester) async {
final GlobalKey childKey = GlobalKey();
await tester.pumpWidget(
wrap(
child: RadioListTile<int>(
value: 1,
groupValue: 2,
onChanged: (_) {},
title: Text('Title', key: childKey),
autofocus: true,
),
),
);
await tester.pump();
expect(Focus.of(childKey.currentContext, nullOk: true).hasPrimaryFocus, isTrue);
await tester.pumpWidget(
wrap(
child: RadioListTile<int>(
value: 1,
groupValue: 2,
onChanged: null,
title: Text('Title', key: childKey),
autofocus: true,
),
),
);
await tester.pump();
expect(Focus.of(childKey.currentContext, nullOk: true).hasPrimaryFocus, isFalse);
});
} }
...@@ -254,4 +254,48 @@ void main() { ...@@ -254,4 +254,48 @@ void main() {
expect(tester.getTopLeft(find.byType(Switch)).dx, 20.0); // contentPadding.end = 20 expect(tester.getTopLeft(find.byType(Switch)).dx, 20.0); // contentPadding.end = 20
expect(tester.getTopRight(find.text('L')).dx, 790.0); // 800 - contentPadding.start expect(tester.getTopRight(find.text('L')).dx, 790.0); // 800 - contentPadding.start
}); });
testWidgets('SwitchListTile can autofocus unless disabled.', (WidgetTester tester) async {
final GlobalKey childKey = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ListView(
children: <Widget>[
SwitchListTile(
value: true,
onChanged: (_) {},
title: Text('A', key: childKey),
autofocus: true,
),
],
),
),
),
);
await tester.pump();
expect(Focus.of(childKey.currentContext, nullOk: true).hasPrimaryFocus, isTrue);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ListView(
children: <Widget>[
SwitchListTile(
value: true,
onChanged: null,
title: Text('A', key: childKey),
autofocus: true,
),
],
),
),
),
);
await tester.pump();
expect(Focus.of(childKey.currentContext, nullOk: true).hasPrimaryFocus, isFalse);
});
} }
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