diff --git a/packages/flutter/lib/src/material/data_table.dart b/packages/flutter/lib/src/material/data_table.dart index 3a62bb8499..e45a1fe6d9 100644 --- a/packages/flutter/lib/src/material/data_table.dart +++ b/packages/flutter/lib/src/material/data_table.dart @@ -469,6 +469,7 @@ class DataTable extends StatelessWidget { this.showBottomBorder = false, this.dividerThickness, required this.rows, + this.checkboxHorizontalMargin, }) : assert(columns != null), assert(columns.isNotEmpty), assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length)), @@ -636,6 +637,10 @@ class DataTable extends StatelessWidget { /// /// If null, [DataTableThemeData.horizontalMargin] is used. This value /// defaults to 24.0 to adhere to the Material Design specifications. + /// + /// If [checkboxHorizontalMargin] is null, then [horizontalMargin] is also the + /// margin between the edge of the table and the checkbox, as well as the + /// margin between the checkbox and the content in the first data column. final double? horizontalMargin; /// {@template flutter.material.dataTable.columnSpacing} @@ -679,6 +684,16 @@ class DataTable extends StatelessWidget { /// around the table defined by [decoration]. final bool showBottomBorder; + /// {@template flutter.material.dataTable.checkboxHorizontalMargin} + /// Horizontal margin around the checkbox, if it is displayed. + /// {@endtemplate} + /// + /// If null, [DataTableThemeData.checkboxHorizontalMargin] is used. If that is + /// also null, then [horizontalMargin] is used as the margin between the edge + /// of the table and the checkbox, as well as the margin between the checkbox + /// and the content in the first data column. This value defaults to 24.0. + final double? checkboxHorizontalMargin; + // Set by the constructor to the index of the only Column that is // non-numeric, if there is exactly one, otherwise null. final int? _onlyTextColumn; @@ -746,12 +761,18 @@ class DataTable extends StatelessWidget { final double effectiveHorizontalMargin = horizontalMargin ?? themeData.dataTableTheme.horizontalMargin ?? _horizontalMargin; + final double effectiveCheckboxHorizontalMarginStart = checkboxHorizontalMargin + ?? themeData.dataTableTheme.checkboxHorizontalMargin + ?? effectiveHorizontalMargin; + final double effectiveCheckboxHorizontalMarginEnd = checkboxHorizontalMargin + ?? themeData.dataTableTheme.checkboxHorizontalMargin + ?? effectiveHorizontalMargin / 2.0; Widget contents = Semantics( container: true, child: Padding( padding: EdgeInsetsDirectional.only( - start: effectiveHorizontalMargin, - end: effectiveHorizontalMargin / 2.0, + start: effectiveCheckboxHorizontalMarginStart, + end: effectiveCheckboxHorizontalMarginEnd, ), child: Center( child: Checkbox( @@ -933,6 +954,12 @@ class DataTable extends StatelessWidget { final double effectiveHorizontalMargin = horizontalMargin ?? theme.dataTableTheme.horizontalMargin ?? _horizontalMargin; + final double effectiveCheckboxHorizontalMarginStart = checkboxHorizontalMargin + ?? theme.dataTableTheme.checkboxHorizontalMargin + ?? effectiveHorizontalMargin; + final double effectiveCheckboxHorizontalMarginEnd = checkboxHorizontalMargin + ?? theme.dataTableTheme.checkboxHorizontalMargin + ?? effectiveHorizontalMargin / 2.0; final double effectiveColumnSpacing = columnSpacing ?? theme.dataTableTheme.columnSpacing ?? _columnSpacing; @@ -976,7 +1003,7 @@ class DataTable extends StatelessWidget { int displayColumnIndex = 0; if (displayCheckboxColumn) { - tableColumns[0] = FixedColumnWidth(effectiveHorizontalMargin + Checkbox.width + effectiveHorizontalMargin / 2.0); + tableColumns[0] = FixedColumnWidth(effectiveCheckboxHorizontalMarginStart + Checkbox.width + effectiveCheckboxHorizontalMarginEnd); tableRows[0].children![0] = _buildCheckbox( context: context, checked: someChecked ? null : allChecked, @@ -1004,7 +1031,9 @@ class DataTable extends StatelessWidget { final DataColumn column = columns[dataColumnIndex]; final double paddingStart; - if (dataColumnIndex == 0 && displayCheckboxColumn) { + if (dataColumnIndex == 0 && displayCheckboxColumn && checkboxHorizontalMargin != null) { + paddingStart = effectiveHorizontalMargin; + } else if (dataColumnIndex == 0 && displayCheckboxColumn) { paddingStart = effectiveHorizontalMargin / 2.0; } else if (dataColumnIndex == 0 && !displayCheckboxColumn) { paddingStart = effectiveHorizontalMargin; diff --git a/packages/flutter/lib/src/material/data_table_theme.dart b/packages/flutter/lib/src/material/data_table_theme.dart index 1d294d6807..0f4bd95adf 100644 --- a/packages/flutter/lib/src/material/data_table_theme.dart +++ b/packages/flutter/lib/src/material/data_table_theme.dart @@ -45,6 +45,7 @@ class DataTableThemeData with Diagnosticable { this.horizontalMargin, this.columnSpacing, this.dividerThickness, + this.checkboxHorizontalMargin, }); /// {@macro flutter.material.dataTable.decoration} @@ -79,6 +80,9 @@ class DataTableThemeData with Diagnosticable { /// {@macro flutter.material.dataTable.dividerThickness} final double? dividerThickness; + /// {@macro flutter.material.dataTable.checkboxHorizontalMargin} + final double? checkboxHorizontalMargin; + /// Creates a copy of this object but with the given fields replaced with the /// new values. DataTableThemeData copyWith({ @@ -92,6 +96,7 @@ class DataTableThemeData with Diagnosticable { double? horizontalMargin, double? columnSpacing, double? dividerThickness, + double? checkboxHorizontalMargin, }) { return DataTableThemeData( decoration: decoration ?? this.decoration, @@ -104,6 +109,7 @@ class DataTableThemeData with Diagnosticable { horizontalMargin: horizontalMargin ?? this.horizontalMargin, columnSpacing: columnSpacing ?? this.columnSpacing, dividerThickness: dividerThickness ?? this.dividerThickness, + checkboxHorizontalMargin: checkboxHorizontalMargin ?? this.checkboxHorizontalMargin, ); } @@ -124,7 +130,8 @@ class DataTableThemeData with Diagnosticable { headingTextStyle: TextStyle.lerp(a.headingTextStyle, b.headingTextStyle, t), horizontalMargin: lerpDouble(a.horizontalMargin, b.horizontalMargin, t), columnSpacing: lerpDouble(a.columnSpacing, b.columnSpacing, t), - dividerThickness: lerpDouble(a.dividerThickness, b.dividerThickness, t) + dividerThickness: lerpDouble(a.dividerThickness, b.dividerThickness, t), + checkboxHorizontalMargin: lerpDouble(a.checkboxHorizontalMargin, b.checkboxHorizontalMargin, t) ); } @@ -141,6 +148,7 @@ class DataTableThemeData with Diagnosticable { horizontalMargin, columnSpacing, dividerThickness, + checkboxHorizontalMargin, ); } @@ -160,7 +168,8 @@ class DataTableThemeData with Diagnosticable { && other.headingTextStyle == headingTextStyle && other.horizontalMargin == horizontalMargin && other.columnSpacing == columnSpacing - && other.dividerThickness == dividerThickness; + && other.dividerThickness == dividerThickness + && other.checkboxHorizontalMargin == checkboxHorizontalMargin; } @override @@ -176,6 +185,7 @@ class DataTableThemeData with Diagnosticable { properties.add(DoubleProperty('horizontalMargin', horizontalMargin, defaultValue: null)); properties.add(DoubleProperty('columnSpacing', columnSpacing, defaultValue: null)); properties.add(DoubleProperty('dividerThickness', dividerThickness, defaultValue: null)); + properties.add(DoubleProperty('checkboxHorizontalMargin', checkboxHorizontalMargin, defaultValue: null)); } static MaterialStateProperty? _lerpProperties(MaterialStateProperty? a, MaterialStateProperty? b, double t, T Function(T?, T?, double) lerpFunction ) { diff --git a/packages/flutter/lib/src/material/paginated_data_table.dart b/packages/flutter/lib/src/material/paginated_data_table.dart index 8eb1e0d7e9..80e14da7b1 100644 --- a/packages/flutter/lib/src/material/paginated_data_table.dart +++ b/packages/flutter/lib/src/material/paginated_data_table.dart @@ -86,6 +86,7 @@ class PaginatedDataTable extends StatefulWidget { this.onRowsPerPageChanged, this.dragStartBehavior = DragStartBehavior.start, required this.source, + this.checkboxHorizontalMargin, }) : assert(actions == null || (actions != null && header != null)), assert(columns != null), assert(dragStartBehavior != null), @@ -166,6 +167,10 @@ class PaginatedDataTable extends StatefulWidget { /// the content in the first data column. /// /// This value defaults to 24.0 to adhere to the Material Design specifications. + /// + /// If [checkboxHorizontalMargin] is null, then [horizontalMargin] is also the + /// margin between the edge of the table and the checkbox, as well as the + /// margin between the checkbox and the content in the first data column. final double horizontalMargin; /// The horizontal margin between the contents of each data column. @@ -224,6 +229,13 @@ class PaginatedDataTable extends StatefulWidget { /// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior; + /// Horizontal margin around the checkbox, if it is displayed. + /// + /// If null, then [horizontalMargin] is used as the margin between the edge + /// of the table and the checkbox, as well as the margin between the checkbox + /// and the content in the first data column. This value defaults to 24.0. + final double? checkboxHorizontalMargin; + @override PaginatedDataTableState createState() => PaginatedDataTableState(); } @@ -513,6 +525,7 @@ class PaginatedDataTableState extends State { dataRowHeight: widget.dataRowHeight, headingRowHeight: widget.headingRowHeight, horizontalMargin: widget.horizontalMargin, + checkboxHorizontalMargin: widget.checkboxHorizontalMargin, columnSpacing: widget.columnSpacing, showCheckboxColumn: widget.showCheckboxColumn, showBottomBorder: true, diff --git a/packages/flutter/test/material/data_table_test.dart b/packages/flutter/test/material/data_table_test.dart index 961f5bc92e..15f8ecdfdc 100644 --- a/packages/flutter/test/material/data_table_test.dart +++ b/packages/flutter/test/material/data_table_test.dart @@ -1540,4 +1540,93 @@ void main() { const Offset(width - borderVertical, height - borderHorizontal), ); }); + + testWidgets('checkboxHorizontalMargin properly applied', (WidgetTester tester) async { + const double _customCheckboxHorizontalMargin = 15.0; + const double _customHorizontalMargin = 10.0; + Finder cellContent; + Finder checkbox; + Finder padding; + + Widget buildCustomTable({ + int? sortColumnIndex, + bool sortAscending = true, + double? horizontalMargin, + double? checkboxHorizontalMargin, + }) { + return DataTable( + sortColumnIndex: sortColumnIndex, + sortAscending: sortAscending, + onSelectAll: (bool? value) {}, + horizontalMargin: horizontalMargin, + checkboxHorizontalMargin: checkboxHorizontalMargin, + columns: [ + const DataColumn( + label: Text('Name'), + tooltip: 'Name', + ), + DataColumn( + label: const Text('Calories'), + tooltip: 'Calories', + numeric: true, + onSort: (int columnIndex, bool ascending) {}, + ), + DataColumn( + label: const Text('Fat'), + tooltip: 'Fat', + numeric: true, + onSort: (int columnIndex, bool ascending) {}, + ), + ], + rows: kDesserts.map((Dessert dessert) { + return DataRow( + key: ValueKey(dessert.name), + onSelectChanged: (bool? selected) {}, + cells: [ + DataCell( + Text(dessert.name), + ), + DataCell( + Text('${dessert.calories}'), + showEditIcon: true, + onTap: () {}, + ), + DataCell( + Text('${dessert.fat}'), + showEditIcon: true, + onTap: () {}, + ), + ], + ); + }).toList(), + ); + } + + await tester.pumpWidget(MaterialApp( + home: Material(child: buildCustomTable( + checkboxHorizontalMargin: _customCheckboxHorizontalMargin, + horizontalMargin: _customHorizontalMargin, + )), + )); + + // Custom checkbox padding. + checkbox = find.byType(Checkbox).first; + padding = find.ancestor(of: checkbox, matching: find.byType(Padding)); + expect( + tester.getRect(checkbox).left - tester.getRect(padding).left, + _customCheckboxHorizontalMargin, + ); + expect( + tester.getRect(padding).right - tester.getRect(checkbox).right, + _customCheckboxHorizontalMargin, + ); + + // First column padding. + padding = find.widgetWithText(Padding, 'Frozen yogurt').first; + cellContent = find.widgetWithText(Align, 'Frozen yogurt'); // DataTable wraps its DataCells in an Align widget. + expect( + tester.getRect(cellContent).left - tester.getRect(padding).left, + _customHorizontalMargin, + ); + }); } diff --git a/packages/flutter/test/material/data_table_theme_test.dart b/packages/flutter/test/material/data_table_theme_test.dart index 69d5933a37..b93dba170c 100644 --- a/packages/flutter/test/material/data_table_theme_test.dart +++ b/packages/flutter/test/material/data_table_theme_test.dart @@ -24,6 +24,7 @@ void main() { expect(themeData.horizontalMargin, null); expect(themeData.columnSpacing, null); expect(themeData.dividerThickness, null); + expect(themeData.checkboxHorizontalMargin, null); const DataTableTheme theme = DataTableTheme(data: DataTableThemeData(), child: SizedBox()); expect(theme.data.decoration, null); @@ -36,6 +37,7 @@ void main() { expect(theme.data.horizontalMargin, null); expect(theme.data.columnSpacing, null); expect(theme.data.dividerThickness, null); + expect(theme.data.checkboxHorizontalMargin, null); }); testWidgets('Default DataTableThemeData debugFillProperties', (WidgetTester tester) async { @@ -67,6 +69,7 @@ void main() { horizontalMargin: 3.0, columnSpacing: 4.0, dividerThickness: 5.0, + checkboxHorizontalMargin: 6.0, ).debugFillProperties(builder); final List description = builder.properties @@ -84,6 +87,7 @@ void main() { expect(description[7], 'horizontalMargin: 3.0'); expect(description[8], 'columnSpacing: 4.0'); expect(description[9], 'dividerThickness: 5.0'); + expect(description[10], 'checkboxHorizontalMargin: 6.0'); }); testWidgets('DataTable is themeable', (WidgetTester tester) async { diff --git a/packages/flutter/test/material/paginated_data_table_test.dart b/packages/flutter/test/material/paginated_data_table_test.dart index 1ba4c4dbfe..729bbc5bc9 100644 --- a/packages/flutter/test/material/paginated_data_table_test.dart +++ b/packages/flutter/test/material/paginated_data_table_test.dart @@ -855,4 +855,72 @@ void main() { // Reset the surface size. await binding.setSurfaceSize(originalSize); }); + + testWidgets('PaginatedDataTable custom checkboxHorizontalMargin properly applied', (WidgetTester tester) async { + const double _customCheckboxHorizontalMargin = 15.0; + const double _customHorizontalMargin = 10.0; + + const double _width = 400; + const double _height = 400; + + final Size originalSize = binding.renderView.size; + + // Ensure the containing Card is small enough that we don't expand too + // much, resulting in our custom margin being ignored. + await binding.setSurfaceSize(const Size(_width, _height)); + + final TestDataSource source = TestDataSource( + onSelectChanged: (bool? value) {}, + ); + Finder cellContent; + Finder checkbox; + Finder padding; + + // CUSTOM VALUES + await tester.pumpWidget(MaterialApp( + home: Material( + child: PaginatedDataTable( + header: const Text('Test table'), + source: source, + rowsPerPage: 2, + availableRowsPerPage: const [ + 2, 4, + ], + onRowsPerPageChanged: (int? rowsPerPage) {}, + onPageChanged: (int rowIndex) {}, + onSelectAll: (bool? value) {}, + columns: const [ + DataColumn(label: Text('Name')), + DataColumn(label: Text('Calories'), numeric: true), + DataColumn(label: Text('Generation')), + ], + horizontalMargin: _customHorizontalMargin, + checkboxHorizontalMargin: _customCheckboxHorizontalMargin, + ), + ), + )); + + // Custom checkbox padding. + checkbox = find.byType(Checkbox).first; + padding = find.ancestor(of: checkbox, matching: find.byType(Padding)).first; + expect( + tester.getRect(checkbox).left - tester.getRect(padding).left, + _customCheckboxHorizontalMargin, + ); + expect( + tester.getRect(padding).right - tester.getRect(checkbox).right, + _customCheckboxHorizontalMargin, + ); + + // Custom first column padding. + padding = find.widgetWithText(Padding, 'Frozen yogurt (0)').first; + cellContent = find.widgetWithText(Align, 'Frozen yogurt (0)'); // DataTable wraps its DataCells in an Align widget. + expect( + tester.getRect(cellContent).left - tester.getRect(padding).left, + _customHorizontalMargin, + ); + + // Reset the surface size. + await binding.setSurfaceSize(originalSize); + }); }