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.
This commit is contained in:
parent
627cca791b
commit
cccfe96e72
@ -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 {
|
|||||||
/// 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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
} : null,
|
} : null,
|
||||||
selected: selected,
|
selected: selected,
|
||||||
|
autofocus: autofocus,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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 {
|
|||||||
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 {
|
|||||||
/// 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 {
|
|||||||
activeTrackColor: activeTrackColor,
|
activeTrackColor: activeTrackColor,
|
||||||
inactiveTrackColor: inactiveTrackColor,
|
inactiveTrackColor: inactiveTrackColor,
|
||||||
inactiveThumbColor: inactiveThumbColor,
|
inactiveThumbColor: inactiveThumbColor,
|
||||||
|
autofocus: autofocus,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -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 {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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() {
|
|||||||
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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user