Skip to content

Commit f0dffde

Browse files
committed
Fix CheckboxListTile missing properties pass through
1 parent 967286b commit f0dffde

File tree

2 files changed

+342
-1
lines changed

2 files changed

+342
-1
lines changed

packages/flutter/lib/src/material/checkbox_list_tile.dart

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,11 @@ class CheckboxListTile extends StatelessWidget {
205205
this.onFocusChange,
206206
this.enableFeedback,
207207
this.checkboxSemanticLabel,
208+
this.horizontalTitleGap,
209+
this.minVerticalPadding,
210+
this.minLeadingWidth,
211+
this.minTileHeight,
212+
this.titleAlignment,
208213
this.internalAddSemanticForOnTap = false,
209214
}) : _checkboxType = _CheckboxType.material,
210215
assert(tristate || value != null),
@@ -250,6 +255,11 @@ class CheckboxListTile extends StatelessWidget {
250255
this.onFocusChange,
251256
this.enableFeedback,
252257
this.checkboxSemanticLabel,
258+
this.horizontalTitleGap,
259+
this.minVerticalPadding,
260+
this.minLeadingWidth,
261+
this.minTileHeight,
262+
this.titleAlignment,
253263
this.internalAddSemanticForOnTap = false,
254264
}) : _checkboxType = _CheckboxType.adaptive,
255265
assert(tristate || value != null),
@@ -354,7 +364,6 @@ class CheckboxListTile extends StatelessWidget {
354364
/// {@macro flutter.material.themedata.visualDensity}
355365
final VisualDensity? visualDensity;
356366

357-
358367
/// {@macro flutter.widgets.Focus.focusNode}
359368
final FocusNode? focusNode;
360369

@@ -466,6 +475,48 @@ class CheckboxListTile extends StatelessWidget {
466475
/// inoperative.
467476
final bool? enabled;
468477

478+
/// The horizontal gap between the titles and the leading/trailing widgets.
479+
///
480+
/// If null, then the value of [ListTileTheme.horizontalTitleGap] is used. If
481+
/// that is also null, then a default value of 16 is used.
482+
final double? horizontalTitleGap;
483+
484+
/// The minimum padding on the top and bottom of the title and subtitle widgets.
485+
///
486+
/// If null, then the value of [ListTileTheme.minVerticalPadding] is used. If
487+
/// that is also null, then a default value of 4 is used.
488+
final double? minVerticalPadding;
489+
490+
/// The minimum width allocated for the [ListTile.leading] widget.
491+
///
492+
/// If null, then the value of [ListTileTheme.minLeadingWidth] is used. If
493+
/// that is also null, then a default value of 40 is used.
494+
final double? minLeadingWidth;
495+
496+
/// {@template flutter.material.ListTile.minTileHeight}
497+
/// The minimum height allocated for the [ListTile] widget.
498+
///
499+
/// If this is null, default tile heights are 56.0, 72.0, and 88.0 for one,
500+
/// two, and three lines of text respectively. If `isDense` is true, these
501+
/// defaults are changed to 48.0, 64.0, and 76.0. A visual density value or
502+
/// a large title will also adjust the default tile heights.
503+
/// {@endtemplate}
504+
final double? minTileHeight;
505+
506+
/// Defines how [ListTile.leading] and [ListTile.trailing] are
507+
/// vertically aligned relative to the [ListTile]'s titles
508+
/// ([ListTile.title] and [ListTile.subtitle]).
509+
///
510+
/// If this property is null then [ListTileThemeData.titleAlignment]
511+
/// is used. If that is also null then [ListTileTitleAlignment.threeLine]
512+
/// is used.
513+
///
514+
/// See also:
515+
///
516+
/// * [ListTileTheme.of], which returns the nearest [ListTileTheme]'s
517+
/// [ListTileThemeData].
518+
final ListTileTitleAlignment? titleAlignment;
519+
469520
/// Whether to add button:true to the semantics if onTap is provided.
470521
/// This is a temporary flag to help changing the behavior of ListTile onTap semantics.
471522
///
@@ -576,6 +627,11 @@ class CheckboxListTile extends StatelessWidget {
576627
focusNode: focusNode,
577628
onFocusChange: onFocusChange,
578629
enableFeedback: enableFeedback,
630+
horizontalTitleGap: horizontalTitleGap,
631+
minVerticalPadding: minVerticalPadding,
632+
minLeadingWidth: minLeadingWidth,
633+
minTileHeight: minTileHeight,
634+
titleAlignment: titleAlignment,
579635
internalAddSemanticForOnTap: internalAddSemanticForOnTap,
580636
),
581637
);

packages/flutter/test/material/checkbox_list_tile_test.dart

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1234,6 +1234,291 @@ void main() {
12341234
expect(Focus.of(secondChildKey.currentContext!).hasPrimaryFocus, isTrue);
12351235
});
12361236

1237+
testWidgets('CheckboxListTile minVerticalPadding = 80.0', (WidgetTester tester) async {
1238+
Widget buildFrame(TextDirection textDirection, { double? themeMinVerticalPadding, double? widgetMinVerticalPadding }) {
1239+
return MaterialApp(
1240+
theme: ThemeData(useMaterial3: true),
1241+
home: Directionality(
1242+
textDirection: textDirection,
1243+
child: Material(
1244+
child: ListTileTheme(
1245+
data: ListTileThemeData(minVerticalPadding: themeMinVerticalPadding),
1246+
child: Container(
1247+
alignment: Alignment.topLeft,
1248+
child: CheckboxListTile(
1249+
minVerticalPadding: widgetMinVerticalPadding,
1250+
title: const Text('title'),
1251+
value: false,
1252+
onChanged: (bool? value) {},
1253+
),
1254+
),
1255+
),
1256+
),
1257+
),
1258+
);
1259+
}
1260+
1261+
1262+
await tester.pumpWidget(buildFrame(TextDirection.ltr, widgetMinVerticalPadding: 80));
1263+
// 80 + 80 + 16(Title) = 176
1264+
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 184.0));
1265+
1266+
await tester.pumpWidget(buildFrame(TextDirection.ltr, themeMinVerticalPadding: 80));
1267+
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 184.0));
1268+
1269+
await tester.pumpWidget(buildFrame(TextDirection.ltr, themeMinVerticalPadding: 0, widgetMinVerticalPadding: 80));
1270+
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 184.0));
1271+
1272+
await tester.pumpWidget(buildFrame(TextDirection.rtl, widgetMinVerticalPadding: 80));
1273+
// 80 + 80 + 16(Title) = 176
1274+
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 184.0));
1275+
1276+
await tester.pumpWidget(buildFrame(TextDirection.rtl, themeMinVerticalPadding: 80));
1277+
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 184.0));
1278+
1279+
await tester.pumpWidget(buildFrame(TextDirection.rtl, themeMinVerticalPadding: 0, widgetMinVerticalPadding: 80));
1280+
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 184.0));
1281+
});
1282+
1283+
testWidgets('CheckboxListTile minLeadingWidth = 60.0', (WidgetTester tester) async {
1284+
Widget buildFrame(TextDirection textDirection, { double? themeMinLeadingWidth, double? widgetMinLeadingWidth }) {
1285+
return MediaQuery(
1286+
data: const MediaQueryData(),
1287+
child: Directionality(
1288+
textDirection: textDirection,
1289+
child: Material(
1290+
child: ListTileTheme(
1291+
data: ListTileThemeData(minLeadingWidth: themeMinLeadingWidth),
1292+
child: Container(
1293+
alignment: Alignment.topLeft,
1294+
child: CheckboxListTile(
1295+
minLeadingWidth: widgetMinLeadingWidth,
1296+
title: const Text('title'),
1297+
value: false,
1298+
onChanged: (bool? value) {},
1299+
),
1300+
),
1301+
),
1302+
),
1303+
),
1304+
);
1305+
}
1306+
1307+
double left(String text) => tester.getTopLeft(find.text(text)).dx;
1308+
double right(String text) => tester.getTopRight(find.text(text)).dx;
1309+
1310+
await tester.pumpWidget(buildFrame(TextDirection.ltr, widgetMinLeadingWidth: 60));
1311+
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 56.0));
1312+
// 92.0 = 16.0(Default contentPadding) + 16.0(Default horizontalTitleGap) + 60.0
1313+
expect(left('title'), 92.0);
1314+
1315+
await tester.pumpWidget(buildFrame(TextDirection.ltr, themeMinLeadingWidth: 60));
1316+
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 56.0));
1317+
expect(left('title'), 92.0);
1318+
1319+
await tester.pumpWidget(buildFrame(TextDirection.ltr, themeMinLeadingWidth: 0, widgetMinLeadingWidth: 60));
1320+
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 56.0));
1321+
expect(left('title'), 92.0);
1322+
1323+
1324+
await tester.pumpWidget(buildFrame(TextDirection.rtl, widgetMinLeadingWidth: 60));
1325+
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 56.0));
1326+
// 708.0 = 800.0 - (16.0(Default contentPadding) + 16.0(Default horizontalTitleGap) + 60.0)
1327+
expect(right('title'), 708.0);
1328+
1329+
await tester.pumpWidget(buildFrame(TextDirection.rtl, themeMinLeadingWidth: 60));
1330+
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 56.0));
1331+
expect(right('title'), 708.0);
1332+
1333+
await tester.pumpWidget(buildFrame(TextDirection.rtl, themeMinLeadingWidth: 0, widgetMinLeadingWidth: 60));
1334+
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 56.0));
1335+
expect(right('title'), 708.0);
1336+
});
1337+
1338+
testWidgets('CheckboxListTile minTileHeight', (WidgetTester tester) async {
1339+
Widget buildFrame(TextDirection textDirection, { double? minTileHeight, }) {
1340+
return MediaQuery(
1341+
data: const MediaQueryData(),
1342+
child: Directionality(
1343+
textDirection: textDirection,
1344+
child: Material(
1345+
child: Container(
1346+
alignment: Alignment.topLeft,
1347+
child: CheckboxListTile(
1348+
minTileHeight: minTileHeight,
1349+
value: false,
1350+
onChanged: (bool? value) {},
1351+
),
1352+
),
1353+
),
1354+
),
1355+
);
1356+
}
1357+
1358+
// Default list tile with height = 56.0
1359+
await tester.pumpWidget(buildFrame(TextDirection.ltr));
1360+
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 56.0));
1361+
1362+
// Set list tile height = 30.0
1363+
await tester.pumpWidget(buildFrame(TextDirection.ltr, minTileHeight: 30));
1364+
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 30.0));
1365+
1366+
// Set list tile height = 60.0
1367+
await tester.pumpWidget(buildFrame(TextDirection.ltr, minTileHeight: 60));
1368+
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 60.0));
1369+
});
1370+
1371+
testWidgets('CheckboxListTile titleAlignment position with title widget', (WidgetTester tester) async {
1372+
final Key leadingKey = GlobalKey();
1373+
final Key trailingKey = GlobalKey();
1374+
const double leadingHeight = 24.0;
1375+
const double titleHeight = 50.0;
1376+
const double trailingHeight = 24.0;
1377+
const double minVerticalPadding = 10.0;
1378+
const double tileHeight = minVerticalPadding * 2 + titleHeight;
1379+
1380+
Widget buildFrame({ ListTileTitleAlignment? titleAlignment }) {
1381+
return MaterialApp(
1382+
theme: ThemeData(useMaterial3: true),
1383+
home: Material(
1384+
child: Center(
1385+
child: CheckboxListTile(
1386+
titleAlignment: titleAlignment,
1387+
minVerticalPadding: minVerticalPadding,
1388+
title: const SizedBox(width: 20.0, height: titleHeight),
1389+
value: false,
1390+
onChanged: (bool? value) {},
1391+
),
1392+
),
1393+
),
1394+
);
1395+
}
1396+
1397+
// If [ThemeData.useMaterial3] is true, the default title alignment is
1398+
// [ListTileTitleAlignment.threeLine], which positions the leading and
1399+
// trailing widgets center vertically in the tile if the [ListTile.isThreeLine]
1400+
// property is false.
1401+
await tester.pumpWidget(buildFrame());
1402+
Offset tileOffset = tester.getTopLeft(find.byType(ListTile));
1403+
Offset leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
1404+
Offset trailingOffset = tester.getTopRight(find.byKey(trailingKey));
1405+
1406+
// Leading and trailing widgets are centered vertically in the tile.
1407+
const double centerPosition = (tileHeight / 2) - (leadingHeight / 2);
1408+
expect(leadingOffset.dy - tileOffset.dy, centerPosition);
1409+
expect(trailingOffset.dy - tileOffset.dy, centerPosition);
1410+
1411+
// Test [ListTileTitleAlignment.threeLine] alignment.
1412+
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.threeLine));
1413+
tileOffset = tester.getTopLeft(find.byType(ListTile));
1414+
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
1415+
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
1416+
1417+
// Leading and trailing widgets are centered vertically in the tile,
1418+
// If the [ListTile.isThreeLine] property is false.
1419+
expect(leadingOffset.dy - tileOffset.dy, centerPosition);
1420+
expect(trailingOffset.dy - tileOffset.dy, centerPosition);
1421+
1422+
// Test [ListTileTitleAlignment.titleHeight] alignment.
1423+
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.titleHeight));
1424+
tileOffset = tester.getTopLeft(find.byType(ListTile));
1425+
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
1426+
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
1427+
1428+
// If the tile height is less than 72.0 pixels, the leading widget is placed
1429+
// 16.0 pixels below the top of the title widget, and the trailing is centered
1430+
// vertically in the tile.
1431+
const double titlePosition = 16.0;
1432+
expect(leadingOffset.dy - tileOffset.dy, titlePosition);
1433+
expect(trailingOffset.dy - tileOffset.dy, centerPosition);
1434+
1435+
// Test [ListTileTitleAlignment.top] alignment.
1436+
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.top));
1437+
tileOffset = tester.getTopLeft(find.byType(ListTile));
1438+
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
1439+
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
1440+
1441+
// Leading and trailing widgets are placed minVerticalPadding below
1442+
// the top of the title widget.
1443+
const double topPosition = minVerticalPadding;
1444+
expect(leadingOffset.dy - tileOffset.dy, topPosition);
1445+
expect(trailingOffset.dy - tileOffset.dy, topPosition);
1446+
1447+
// Test [ListTileTitleAlignment.center] alignment.
1448+
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.center));
1449+
tileOffset = tester.getTopLeft(find.byType(ListTile));
1450+
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
1451+
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
1452+
1453+
// Leading and trailing widgets are centered vertically in the tile.
1454+
expect(leadingOffset.dy - tileOffset.dy, centerPosition);
1455+
expect(trailingOffset.dy - tileOffset.dy, centerPosition);
1456+
1457+
// Test [ListTileTitleAlignment.bottom] alignment.
1458+
await tester.pumpWidget(buildFrame(titleAlignment: ListTileTitleAlignment.bottom));
1459+
tileOffset = tester.getTopLeft(find.byType(ListTile));
1460+
leadingOffset = tester.getTopLeft(find.byKey(leadingKey));
1461+
trailingOffset = tester.getTopRight(find.byKey(trailingKey));
1462+
1463+
// Leading and trailing widgets are placed minVerticalPadding above
1464+
// the bottom of the subtitle widget.
1465+
const double bottomPosition = tileHeight - minVerticalPadding - leadingHeight;
1466+
expect(leadingOffset.dy - tileOffset.dy, bottomPosition);
1467+
expect(trailingOffset.dy - tileOffset.dy, bottomPosition);
1468+
});
1469+
1470+
testWidgets('CheckboxListTile horizontalTitleGap = 0.0', (WidgetTester tester) async {
1471+
Widget buildFrame(TextDirection textDirection, { double? themeHorizontalTitleGap, double? widgetHorizontalTitleGap }) {
1472+
return MaterialApp(
1473+
theme: ThemeData(useMaterial3: false),
1474+
home: Directionality(
1475+
textDirection: textDirection,
1476+
child: Material(
1477+
child: ListTileTheme(
1478+
data: ListTileThemeData(horizontalTitleGap: themeHorizontalTitleGap),
1479+
child: Container(
1480+
alignment: Alignment.topLeft,
1481+
child: CheckboxListTile(
1482+
horizontalTitleGap: widgetHorizontalTitleGap,
1483+
title: const Text('title'),
1484+
value: false,
1485+
onChanged: (bool? value) {},
1486+
),
1487+
),
1488+
),
1489+
),
1490+
),
1491+
);
1492+
}
1493+
1494+
double left(String text) => tester.getTopLeft(find.text(text)).dx;
1495+
double right(String text) => tester.getTopRight(find.text(text)).dx;
1496+
1497+
await tester.pumpWidget(buildFrame(TextDirection.ltr, widgetHorizontalTitleGap: 0));
1498+
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 56.0));
1499+
expect(left('title'), 56.0);
1500+
1501+
await tester.pumpWidget(buildFrame(TextDirection.ltr, themeHorizontalTitleGap: 0));
1502+
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 56.0));
1503+
expect(left('title'), 56.0);
1504+
1505+
await tester.pumpWidget(buildFrame(TextDirection.ltr, themeHorizontalTitleGap: 10, widgetHorizontalTitleGap: 0));
1506+
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 56.0));
1507+
expect(left('title'), 56.0);
1508+
1509+
await tester.pumpWidget(buildFrame(TextDirection.rtl, widgetHorizontalTitleGap: 0));
1510+
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 56.0));
1511+
expect(right('title'), 744.0);
1512+
1513+
await tester.pumpWidget(buildFrame(TextDirection.rtl, themeHorizontalTitleGap: 0));
1514+
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 56.0));
1515+
expect(right('title'), 744.0);
1516+
1517+
await tester.pumpWidget(buildFrame(TextDirection.rtl, themeHorizontalTitleGap: 10, widgetHorizontalTitleGap: 0));
1518+
expect(tester.getSize(find.byType(ListTile)), const Size(800.0, 56.0));
1519+
expect(right('title'), 744.0);
1520+
});
1521+
12371522
testWidgets('CheckboxListTile uses ListTileTheme controlAffinity', (WidgetTester tester) async {
12381523
Widget buildListTile(ListTileControlAffinity controlAffinity) {
12391524
return MaterialApp(

0 commit comments

Comments
 (0)