Unverified Commit 0e2eeb5a authored by Shi-Hao Hong's avatar Shi-Hao Hong Committed by GitHub

Set Max Height for ListTile trailing and leading widgets (#29771)

parent ffbb335e
...@@ -164,6 +164,13 @@ enum ListTileControlAffinity { ...@@ -164,6 +164,13 @@ enum ListTileControlAffinity {
/// and to ensure that [subtitle] doesn't wrap (if [isThreeLine] is false) or /// and to ensure that [subtitle] doesn't wrap (if [isThreeLine] is false) or
/// wraps to two lines (if it is true). /// wraps to two lines (if it is true).
/// ///
/// The heights of the [leading] and [trailing] widgets are constrained
/// according to the [Material spec]
/// (https://material.io/design/components/lists.html).
/// An exception is made for one-line ListTiles for accessibility. Please
/// see the example below to see how to adhere to both Material spec and
/// accessibility requirements.
///
/// List tiles are typically used in [ListView]s, or arranged in [Column]s in /// List tiles are typically used in [ListView]s, or arranged in [Column]s in
/// [Drawer]s and [Card]s. /// [Drawer]s and [Card]s.
/// ///
...@@ -199,6 +206,44 @@ enum ListTileControlAffinity { ...@@ -199,6 +206,44 @@ enum ListTileControlAffinity {
/// ``` /// ```
/// {@end-tool} /// {@end-tool}
/// ///
/// To be accessible, tappable [leading] and [trailing] widgets have to
/// be at least 48x48 in size. However, to adhere to the Material spec,
/// [trailing] and [leading] widgets in one-line ListTiles should visually be
/// at most 32 ([dense]: true) or 40 ([dense]: false) in height, which may
/// conflict with the accessibility requirement.
///
/// For this reason, a one-line ListTile allows the height of [leading]
/// and [trailing] widgets to be constrained by the height of the ListTile.
/// This allows for the creation of tappable [leading] and [trailing] widgets
/// that are large enough, but it is up to the developer to ensure that
/// their widgets follow the Material spec.
///
/// The following is an example of a one-line, non-[dense] ListTile with a
/// tappable leading widget that adheres to accessibility requirements and
/// the Material spec. To adjust the use case below for a one-line, [dense]
/// ListTile, adjust the vertical padding to 8.0.
///
/// {@tool sample}
///
/// ```dart
/// ListTile(
/// leading: GestureDetector(
/// behavior: HitTestBehavior.translucent,
/// onTap: () {},
/// child: Container(
/// width: 48,
/// height: 48,
/// padding: EdgeInsets.symmetric(vertical: 4.0),
/// alignment: Alignment.center,
/// child: CircleAvatar(),
/// ),
/// ),
/// title: Text('title'),
/// dense: false,
/// ),
/// ```
/// {@end-tool}
///
/// See also: /// See also:
/// ///
/// * [ListTileTheme], which defines visual properties for [ListTile]s. /// * [ListTileTheme], which defines visual properties for [ListTile]s.
...@@ -924,11 +969,21 @@ class _RenderListTile extends RenderBox { ...@@ -924,11 +969,21 @@ class _RenderListTile extends RenderBox {
final bool hasTrailing = trailing != null; final bool hasTrailing = trailing != null;
final bool isTwoLine = !isThreeLine && hasSubtitle; final bool isTwoLine = !isThreeLine && hasSubtitle;
final bool isOneLine = !isThreeLine && !hasSubtitle; final bool isOneLine = !isThreeLine && !hasSubtitle;
final BoxConstraints maxIconHeightConstraint = BoxConstraints(
// One-line trailing and leading widget heights do not follow
// Material specifications, but this sizing is required to adhere
// to accessibility requirements for smallest tappable widget.
// Two- and three-line trailing widget heights are constrained
// properly according to the Material spec.
maxHeight: isDense ? 48.0 : 56.0,
);
final BoxConstraints looseConstraints = constraints.loosen(); final BoxConstraints looseConstraints = constraints.loosen();
final BoxConstraints iconConstraints = looseConstraints.enforce(maxIconHeightConstraint);
final double tileWidth = looseConstraints.maxWidth; final double tileWidth = looseConstraints.maxWidth;
final Size leadingSize = _layoutBox(leading, looseConstraints); final Size leadingSize = _layoutBox(leading, iconConstraints);
final Size trailingSize = _layoutBox(trailing, looseConstraints); final Size trailingSize = _layoutBox(trailing, iconConstraints);
final double titleStart = hasLeading final double titleStart = hasLeading
? math.max(_minLeadingWidth, leadingSize.width) + _horizontalTitleGap ? math.max(_minLeadingWidth, leadingSize.width) + _horizontalTitleGap
......
...@@ -785,4 +785,338 @@ void main() { ...@@ -785,4 +785,338 @@ void main() {
expect(tester.getRect(find.byType(Placeholder).at(2)), Rect.fromLTWH( 16.0, 216.0 + 16.0, 24.0, 12.0)); expect(tester.getRect(find.byType(Placeholder).at(2)), Rect.fromLTWH( 16.0, 216.0 + 16.0, 24.0, 12.0));
expect(tester.getRect(find.byType(Placeholder).at(3)), Rect.fromLTWH(800.0 - 24.0 - 16.0, 216.0 + 16.0, 24.0, 24.0)); expect(tester.getRect(find.byType(Placeholder).at(3)), Rect.fromLTWH(800.0 - 24.0 - 16.0, 216.0 + 16.0, 24.0, 24.0));
}); });
testWidgets('ListTile leading icon height does not exceed ListTile height', (WidgetTester tester) async {
// regression test for https://github.com/flutter/flutter/issues/28765
const SizedBox oversizedWidget = SizedBox(height: 80.0, width: 24.0, child: Placeholder());
// Dense One line
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ListView(
children: const <Widget>[
ListTile(
leading: oversizedWidget,
title: Text('A'),
dense: true,
),
ListTile(
leading: oversizedWidget,
title: Text('B'),
dense: true,
),
],
),
),
),
);
expect(tester.getRect(find.byType(Placeholder).at(0)), Rect.fromLTWH(16.0, 0.0, 24.0, 48.0));
expect(tester.getRect(find.byType(Placeholder).at(1)), Rect.fromLTWH(16.0, 48.0, 24.0, 48.0));
// Non-dense One line
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ListView(
children: const <Widget>[
ListTile(
leading: oversizedWidget,
title: Text('A'),
dense: false,
),
ListTile(
leading: oversizedWidget,
title: Text('B'),
dense: false,
),
],
),
),
),
);
expect(tester.getRect(find.byType(Placeholder).at(0)), Rect.fromLTWH(16.0, 0.0, 24.0, 56.0));
expect(tester.getRect(find.byType(Placeholder).at(1)), Rect.fromLTWH(16.0, 56.0, 24.0, 56.0));
// Dense Two line
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ListView(
children: const <Widget>[
ListTile(
leading: oversizedWidget,
title: Text('A'),
subtitle: Text('A'),
dense: true,
),
ListTile(
leading: oversizedWidget,
title: Text('B'),
subtitle: Text('B'),
dense: true,
),
],
),
),
),
);
expect(tester.getRect(find.byType(Placeholder).at(0)), Rect.fromLTWH(16.0, 8.0, 24.0, 48.0));
expect(tester.getRect(find.byType(Placeholder).at(1)), Rect.fromLTWH(16.0, 64.0 + 8.0, 24.0, 48.0));
// Non-dense Two line
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ListView(
children: const <Widget>[
ListTile(
leading: oversizedWidget,
title: Text('A'),
subtitle: Text('A'),
dense: false,
),
ListTile(
leading: oversizedWidget,
title: Text('B'),
subtitle: Text('B'),
dense: false,
),
],
),
),
),
);
expect(tester.getRect(find.byType(Placeholder).at(0)), Rect.fromLTWH(16.0, 8.0, 24.0, 56.0));
expect(tester.getRect(find.byType(Placeholder).at(1)), Rect.fromLTWH(16.0, 72.0 + 8.0, 24.0, 56.0));
// Dense Three line
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ListView(
children: const <Widget>[
ListTile(
leading: oversizedWidget,
title: Text('A'),
subtitle: Text('A'),
isThreeLine: true,
dense: true,
),
ListTile(
leading: oversizedWidget,
title: Text('B'),
subtitle: Text('B'),
isThreeLine: true,
dense: true,
),
],
),
),
),
);
expect(tester.getRect(find.byType(Placeholder).at(0)), Rect.fromLTWH(16.0, 16.0, 24.0, 48.0));
expect(tester.getRect(find.byType(Placeholder).at(1)), Rect.fromLTWH(16.0, 76.0 + 16.0, 24.0, 48.0));
// Non-dense Three line
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ListView(
children: const <Widget>[
ListTile(
leading: oversizedWidget,
title: Text('A'),
subtitle: Text('A'),
isThreeLine: true,
dense: false,
),
ListTile(
leading: oversizedWidget,
title: Text('B'),
subtitle: Text('B'),
isThreeLine: true,
dense: false,
),
],
),
),
),
);
expect(tester.getRect(find.byType(Placeholder).at(0)), Rect.fromLTWH(16.0, 16.0, 24.0, 56.0));
expect(tester.getRect(find.byType(Placeholder).at(1)), Rect.fromLTWH(16.0, 88.0 + 16.0, 24.0, 56.0));
});
testWidgets('ListTile trailing icon height does not exceed ListTile height', (WidgetTester tester) async {
// regression test for https://github.com/flutter/flutter/issues/28765
const SizedBox oversizedWidget = SizedBox(height: 80.0, width: 24.0, child: Placeholder());
// Dense One line
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ListView(
children: const <Widget>[
ListTile(
trailing: oversizedWidget,
title: Text('A'),
dense: true,
),
ListTile(
trailing: oversizedWidget,
title: Text('B'),
dense: true,
),
],
),
),
),
);
expect(tester.getRect(find.byType(Placeholder).at(0)), Rect.fromLTWH(800.0 - 16.0 - 24.0, 0, 24.0, 48.0));
expect(tester.getRect(find.byType(Placeholder).at(1)), Rect.fromLTWH(800.0 - 16.0 - 24.0, 48.0, 24.0, 48.0));
// Non-dense One line
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ListView(
children: const <Widget>[
ListTile(
trailing: oversizedWidget,
title: Text('A'),
dense: false,
),
ListTile(
trailing: oversizedWidget,
title: Text('B'),
dense: false,
),
],
),
),
),
);
expect(tester.getRect(find.byType(Placeholder).at(0)), Rect.fromLTWH(800.0 - 16.0 - 24.0, 0.0, 24.0, 56.0));
expect(tester.getRect(find.byType(Placeholder).at(1)), Rect.fromLTWH(800.0 - 16.0 - 24.0, 56.0, 24.0, 56.0));
// Dense Two line
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ListView(
children: const <Widget>[
ListTile(
trailing: oversizedWidget,
title: Text('A'),
subtitle: Text('A'),
dense: true,
),
ListTile(
trailing: oversizedWidget,
title: Text('B'),
subtitle: Text('B'),
dense: true,
),
],
),
),
),
);
expect(tester.getRect(find.byType(Placeholder).at(0)), Rect.fromLTWH(800.0 - 16.0 - 24.0, 8.0, 24.0, 48.0));
expect(tester.getRect(find.byType(Placeholder).at(1)), Rect.fromLTWH(800.0 - 16.0 - 24.0, 64.0 + 8.0, 24.0, 48.0));
// Non-dense Two line
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ListView(
children: const <Widget>[
ListTile(
trailing: oversizedWidget,
title: Text('A'),
subtitle: Text('A'),
dense: false,
),
ListTile(
trailing: oversizedWidget,
title: Text('B'),
subtitle: Text('B'),
dense: false,
),
],
),
),
),
);
expect(tester.getRect(find.byType(Placeholder).at(0)), Rect.fromLTWH(800.0 - 16.0 - 24.0, 8.0, 24.0, 56.0));
expect(tester.getRect(find.byType(Placeholder).at(1)), Rect.fromLTWH(800.0 - 16.0 - 24.0, 72.0 + 8.0, 24.0, 56.0));
// Dense Three line
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ListView(
children: const <Widget>[
ListTile(
trailing: oversizedWidget,
title: Text('A'),
subtitle: Text('A'),
isThreeLine: true,
dense: true,
),
ListTile(
trailing: oversizedWidget,
title: Text('B'),
subtitle: Text('B'),
isThreeLine: true,
dense: true,
),
],
),
),
),
);
expect(tester.getRect(find.byType(Placeholder).at(0)), Rect.fromLTWH(800.0 - 16.0 - 24.0, 16.0, 24.0, 48.0));
expect(tester.getRect(find.byType(Placeholder).at(1)), Rect.fromLTWH(800.0 - 16.0 - 24.0, 76.0 + 16.0, 24.0, 48.0));
// Non-dense Three line
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ListView(
children: const <Widget>[
ListTile(
trailing: oversizedWidget,
title: Text('A'),
subtitle: Text('A'),
isThreeLine: true,
dense: false,
),
ListTile(
trailing: oversizedWidget,
title: Text('B'),
subtitle: Text('B'),
isThreeLine: true,
dense: false,
),
],
),
),
),
);
expect(tester.getRect(find.byType(Placeholder).at(0)), Rect.fromLTWH(800.0 - 16.0 - 24.0, 16.0, 24.0, 56.0));
expect(tester.getRect(find.byType(Placeholder).at(1)), Rect.fromLTWH(800.0 - 16.0 - 24.0, 88.0 + 16.0, 24.0, 56.0));
});
} }
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