From cccfe96e721fcc2c7985fbd8a652d53f7c2f7682 Mon Sep 17 00:00:00 2001 From: EricEnslen <61600502+EricEnslen@users.noreply.github.com> Date: Tue, 7 Apr 2020 15:39:44 -0700 Subject: [PATCH] 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. --- .../lib/src/material/checkbox_list_tile.dart | 7 +++ .../flutter/lib/src/material/list_tile.dart | 6 +++ .../lib/src/material/radio_list_tile.dart | 7 +++ .../lib/src/material/switch_list_tile.dart | 10 ++++ .../material/checkbox_list_tile_test.dart | 33 +++++++++++++ .../flutter/test/material/list_tile_test.dart | 47 +++++++++++++++++++ .../test/material/radio_list_tile_test.dart | 34 ++++++++++++++ .../test/material/switch_list_tile_test.dart | 44 +++++++++++++++++ 8 files changed, 188 insertions(+) diff --git a/packages/flutter/lib/src/material/checkbox_list_tile.dart b/packages/flutter/lib/src/material/checkbox_list_tile.dart index e684cfc182..410980423c 100644 --- a/packages/flutter/lib/src/material/checkbox_list_tile.dart +++ b/packages/flutter/lib/src/material/checkbox_list_tile.dart @@ -266,11 +266,13 @@ class CheckboxListTile extends StatelessWidget { this.secondary, this.selected = false, this.controlAffinity = ListTileControlAffinity.platform, + this.autofocus = false, }) : assert(value != null), assert(isThreeLine != null), assert(!isThreeLine || subtitle != null), assert(selected != null), assert(controlAffinity != null), + assert(autofocus != null), super(key: key); /// Whether this checkbox is checked. @@ -351,6 +353,9 @@ class CheckboxListTile extends StatelessWidget { /// Where to place the control relative to the text. final ListTileControlAffinity controlAffinity; + /// {@macro flutter.widgets.Focus.autofocus} + final bool autofocus; + @override Widget build(BuildContext context) { final Widget control = Checkbox( @@ -359,6 +364,7 @@ class CheckboxListTile extends StatelessWidget { activeColor: activeColor, checkColor: checkColor, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + autofocus: autofocus, ); Widget leading, trailing; switch (controlAffinity) { @@ -385,6 +391,7 @@ class CheckboxListTile extends StatelessWidget { enabled: onChanged != null, onTap: onChanged != null ? () { onChanged(!value); } : null, selected: selected, + autofocus: autofocus, ), ), ); diff --git a/packages/flutter/lib/src/material/list_tile.dart b/packages/flutter/lib/src/material/list_tile.dart index e5562924c7..9a636c5eaa 100644 --- a/packages/flutter/lib/src/material/list_tile.dart +++ b/packages/flutter/lib/src/material/list_tile.dart @@ -638,9 +638,11 @@ class ListTile extends StatelessWidget { this.onTap, this.onLongPress, this.selected = false, + this.autofocus = false, }) : assert(isThreeLine != null), assert(enabled != null), assert(selected != null), + assert(autofocus != null), assert(!isThreeLine || subtitle != null), super(key: key); @@ -724,6 +726,9 @@ class ListTile extends StatelessWidget { /// can be overridden with a [ListTileTheme]. 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 /// [ThemeData.dividerColor] of the context's [Theme] is used. /// @@ -883,6 +888,7 @@ class ListTile extends StatelessWidget { onTap: enabled ? onTap : null, onLongPress: enabled ? onLongPress : null, canRequestFocus: enabled, + autofocus: autofocus, child: Semantics( selected: selected, enabled: enabled, diff --git a/packages/flutter/lib/src/material/radio_list_tile.dart b/packages/flutter/lib/src/material/radio_list_tile.dart index 6a56ad28df..50274f643d 100644 --- a/packages/flutter/lib/src/material/radio_list_tile.dart +++ b/packages/flutter/lib/src/material/radio_list_tile.dart @@ -318,12 +318,14 @@ class RadioListTile extends StatelessWidget { this.secondary, this.selected = false, this.controlAffinity = ListTileControlAffinity.platform, + this.autofocus = false, }) : assert(toggleable != null), assert(isThreeLine != null), assert(!isThreeLine || subtitle != null), assert(selected != null), assert(controlAffinity != null), + assert(autofocus != null), super(key: key); /// The value represented by this radio button. @@ -464,6 +466,9 @@ class RadioListTile extends StatelessWidget { /// Where to place the control relative to the text. final ListTileControlAffinity controlAffinity; + /// {@macro flutter.widgets.Focus.autofocus} + final bool autofocus; + /// Whether this radio button is checked. /// /// To control this value, set [value] and [groupValue] appropriately. @@ -478,6 +483,7 @@ class RadioListTile extends StatelessWidget { toggleable: toggleable, activeColor: activeColor, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + autofocus: autofocus, ); Widget leading, trailing; switch (controlAffinity) { @@ -512,6 +518,7 @@ class RadioListTile extends StatelessWidget { } } : null, selected: selected, + autofocus: autofocus, ), ), ); diff --git a/packages/flutter/lib/src/material/switch_list_tile.dart b/packages/flutter/lib/src/material/switch_list_tile.dart index e3d8a136a6..62fae85df7 100644 --- a/packages/flutter/lib/src/material/switch_list_tile.dart +++ b/packages/flutter/lib/src/material/switch_list_tile.dart @@ -272,11 +272,13 @@ class SwitchListTile extends StatelessWidget { this.contentPadding, this.secondary, this.selected = false, + this.autofocus = false, }) : _switchListTileType = _SwitchListTileType.material, assert(value != null), assert(isThreeLine != null), assert(!isThreeLine || subtitle != null), assert(selected != null), + assert(autofocus != null), super(key: key); /// Creates the wrapped switch with [Switch.adaptive]. @@ -304,11 +306,13 @@ class SwitchListTile extends StatelessWidget { this.contentPadding, this.secondary, this.selected = false, + this.autofocus = false, }) : _switchListTileType = _SwitchListTileType.adaptive, assert(value != null), assert(isThreeLine != null), assert(!isThreeLine || subtitle != null), assert(selected != null), + assert(autofocus != null), super(key: key); /// Whether this switch is checked. @@ -419,6 +423,9 @@ class SwitchListTile extends StatelessWidget { /// Normally, this property is left to its default value, false. final bool selected; + /// {@macro flutter.widgets.Focus.autofocus} + final bool autofocus; + /// If adaptive, creates the switch with [Switch.adaptive]. final _SwitchListTileType _switchListTileType; @@ -437,6 +444,7 @@ class SwitchListTile extends StatelessWidget { activeTrackColor: activeTrackColor, inactiveTrackColor: inactiveTrackColor, inactiveThumbColor: inactiveThumbColor, + autofocus: autofocus, ); break; @@ -451,6 +459,7 @@ class SwitchListTile extends StatelessWidget { activeTrackColor: activeTrackColor, inactiveTrackColor: inactiveTrackColor, inactiveThumbColor: inactiveThumbColor, + autofocus: autofocus, ); } return MergeSemantics( @@ -467,6 +476,7 @@ class SwitchListTile extends StatelessWidget { enabled: onChanged != null, onTap: onChanged != null ? () { onChanged(!value); } : null, selected: selected, + autofocus: autofocus, ), ), ); diff --git a/packages/flutter/test/material/checkbox_list_tile_test.dart b/packages/flutter/test/material/checkbox_list_tile_test.dart index cde81ebd4c..18fa5746a3 100644 --- a/packages/flutter/test/material/checkbox_list_tile_test.dart +++ b/packages/flutter/test/material/checkbox_list_tile_test.dart @@ -82,4 +82,37 @@ void main() { await tester.pumpAndSettle(); 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); + }); + } diff --git a/packages/flutter/test/material/list_tile_test.dart b/packages/flutter/test/material/list_tile_test.dart index d737320345..c447c02fae 100644 --- a/packages/flutter/test/material/list_tile_test.dart +++ b/packages/flutter/test/material/list_tile_test.dart @@ -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(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 { final GlobalKey childKey = GlobalKey(); @@ -1184,4 +1185,50 @@ void main() { expect(tester.binding.focusManager.primaryFocus, isNot(equals(tileNode))); 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: [ + 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: [ + 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); + }); } diff --git a/packages/flutter/test/material/radio_list_tile_test.dart b/packages/flutter/test/material/radio_list_tile_test.dart index fd76a03fe1..3b03afac52 100644 --- a/packages/flutter/test/material/radio_list_tile_test.dart +++ b/packages/flutter/test/material/radio_list_tile_test.dart @@ -572,4 +572,38 @@ void main() { semantics.dispose(); SystemChannels.accessibility.setMockMessageHandler(null); }); + + testWidgets('RadioListTile can autofocus unless disabled.', (WidgetTester tester) async { + final GlobalKey childKey = GlobalKey(); + + await tester.pumpWidget( + wrap( + child: RadioListTile( + 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( + 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); + }); } diff --git a/packages/flutter/test/material/switch_list_tile_test.dart b/packages/flutter/test/material/switch_list_tile_test.dart index 4769d6b8a4..a4ad456bcd 100644 --- a/packages/flutter/test/material/switch_list_tile_test.dart +++ b/packages/flutter/test/material/switch_list_tile_test.dart @@ -254,4 +254,48 @@ void main() { expect(tester.getTopLeft(find.byType(Switch)).dx, 20.0); // contentPadding.end = 20 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: [ + 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: [ + SwitchListTile( + value: true, + onChanged: null, + title: Text('A', key: childKey), + autofocus: true, + ), + ], + ), + ), + ), + ); + + await tester.pump(); + expect(Focus.of(childKey.currentContext, nullOk: true).hasPrimaryFocus, isFalse); + }); }