From 0e92ff7b586ec715c1cb95f20ef2108e9eb1576f Mon Sep 17 00:00:00 2001 From: puneetkukreja98 Date: Mon, 29 Jun 2026 18:32:50 +0530 Subject: [PATCH 1/6] Add sortIconWidget parameter to DataTable. --- packages/material_ui/lib/src/data_table.dart | 29 +++++++- .../material_ui/test/data_table_test.dart | 72 +++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/packages/material_ui/lib/src/data_table.dart b/packages/material_ui/lib/src/data_table.dart index d16cc15ef77f..45150e39ad65 100644 --- a/packages/material_ui/lib/src/data_table.dart +++ b/packages/material_ui/lib/src/data_table.dart @@ -508,6 +508,7 @@ class DataTable extends StatelessWidget { this.checkboxHorizontalMargin, this.border, this.clipBehavior = Clip.none, + this.sortIconWidget, }) : assert(columns.isNotEmpty), assert( sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length), @@ -562,6 +563,15 @@ class DataTable extends StatelessWidget { /// Ascending order is represented by an upwards-facing arrow. final bool sortAscending; + /// A widget to use as the sorting indicator icon for the table's header cells. + /// + /// If null, the default Material design arrow icon ([Icons.arrow_upward]) is + /// used with a framework default size of 16.0. + /// + /// Any custom widget provided here will be rendered exactly as-is without + /// layout size modifications from the framework. + final Widget? sortIconWidget; + /// Invoked when the user selects or unselects every row, using the /// checkbox in the heading row. /// @@ -917,6 +927,7 @@ class DataTable extends StatelessWidget { visible: sorted, up: sorted ? ascending : null, duration: _sortArrowAnimationDuration, + sortIconWidget: sortIconWidget, ), const SizedBox(width: _sortArrowPadding), ], @@ -1339,7 +1350,12 @@ class TableRowInkWell extends InkResponse { } class _SortArrow extends StatefulWidget { - const _SortArrow({required this.visible, required this.up, required this.duration}); + const _SortArrow({ + required this.visible, + required this.up, + required this.duration, + this.sortIconWidget, + }); final bool visible; @@ -1347,6 +1363,8 @@ class _SortArrow extends StatefulWidget { final Duration duration; + final Widget? sortIconWidget; + @override _SortArrowState createState() => _SortArrowState(); } @@ -1439,13 +1457,20 @@ class _SortArrowState extends State<_SortArrow> with TickerProviderStateMixin { @override Widget build(BuildContext context) { + // If the user provided a custom widget, use it. + // Otherwise, instantly fall back to the standard Material arrow icon. + final Widget iconWidget = widget.sortIconWidget ?? const Icon(Icons.arrow_upward); + return FadeTransition( opacity: _opacityAnimation, child: Transform( transform: Matrix4.rotationZ(_orientationOffset + _orientationAnimation.value) ..setTranslationRaw(0.0, _arrowIconBaselineOffset, 0.0), alignment: Alignment.center, - child: const Icon(Icons.arrow_upward, size: _arrowIconSize), + child: IconTheme.merge( + data: const IconThemeData(size: _arrowIconSize), + child: iconWidget, + ), ), ); } diff --git a/packages/material_ui/test/data_table_test.dart b/packages/material_ui/test/data_table_test.dart index e3d9a7b7ac14..80aea302c4fe 100644 --- a/packages/material_ui/test/data_table_test.dart +++ b/packages/material_ui/test/data_table_test.dart @@ -432,6 +432,78 @@ void main() { expect(transformOfArrow.transform.getRotation(), equals(Matrix3.rotationZ(math.pi))); }); + testWidgets('DataTable custom sortIconWidget test', (WidgetTester tester) async { + Widget buildTable({Widget? sortIconWidget}) { + return DataTable( + sortColumnIndex: 0, + sortIconWidget: sortIconWidget, + columns: [ + DataColumn( + label: const Text('Name'), + tooltip: 'Name', + onSort: (int columnIndex, bool ascending) {}, + ), + ], + rows: kDesserts.map((Dessert dessert) { + return DataRow(cells: [DataCell(Text(dessert.name))]); + }).toList(), + ); + } + + // By default (null sortIconWidget), the sort indicator is Icons.arrow_upward. + await tester.pumpWidget(MaterialApp(home: Material(child: buildTable()))); + final Finder defaultIconFinder = find.descendant( + of: find.byType(DataTable), + matching: find.byIcon(Icons.arrow_upward), + ); + expect(defaultIconFinder, findsOneWidget); + expect(tester.widget(defaultIconFinder).size, isNull); + expect(IconTheme.of(tester.element(defaultIconFinder)).size, 16.0); + expect(tester.getSize(defaultIconFinder), const Size(16.0, 16.0)); + + // Using a custom sortIconWidget (an Icon). + await tester.pumpWidget( + MaterialApp( + home: Material(child: buildTable(sortIconWidget: const Icon(Icons.arrow_downward))), + ), + ); + final Finder customIconFinder = find.descendant( + of: find.byType(DataTable), + matching: find.byIcon(Icons.arrow_downward), + ); + expect(customIconFinder, findsOneWidget); + expect(tester.widget(customIconFinder).size, isNull); + expect(IconTheme.of(tester.element(customIconFinder)).size, 16.0); + expect(tester.getSize(customIconFinder), const Size(16.0, 16.0)); + + // Using a custom sortIconWidget with explicit size. + await tester.pumpWidget( + MaterialApp( + home: Material( + child: buildTable(sortIconWidget: const Icon(Icons.arrow_downward, size: 24.0)), + ), + ), + ); + final Finder customSizeIconFinder = find.descendant( + of: find.byType(DataTable), + matching: find.byIcon(Icons.arrow_downward), + ); + expect(customSizeIconFinder, findsOneWidget); + expect(tester.widget(customSizeIconFinder).size, 24.0); + expect(tester.getSize(customSizeIconFinder), const Size(24.0, 24.0)); + + // Using a custom non-Icon sortIconWidget. + await tester.pumpWidget( + MaterialApp( + home: Material(child: buildTable(sortIconWidget: const Text('Sort'))), + ), + ); + expect( + find.descendant(of: find.byType(DataTable), matching: find.text('Sort')), + findsOneWidget, + ); + }); + testWidgets('DataTable sort indicator orientation does not change on state update', ( WidgetTester tester, ) async { From 95a68e446d4e82b0946953b3c35b599e4fcdc032 Mon Sep 17 00:00:00 2001 From: puneetkukreja98 Date: Mon, 29 Jun 2026 18:44:35 +0530 Subject: [PATCH 2/6] Update sortIconWidget documentation to reflect IconTheme sizing behavior --- packages/material_ui/lib/src/data_table.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/material_ui/lib/src/data_table.dart b/packages/material_ui/lib/src/data_table.dart index 45150e39ad65..3700db013bac 100644 --- a/packages/material_ui/lib/src/data_table.dart +++ b/packages/material_ui/lib/src/data_table.dart @@ -568,8 +568,9 @@ class DataTable extends StatelessWidget { /// If null, the default Material design arrow icon ([Icons.arrow_upward]) is /// used with a framework default size of 16.0. /// - /// Any custom widget provided here will be rendered exactly as-is without - /// layout size modifications from the framework. + /// If this widget is an [Icon], the framework will automatically apply the + /// default size of 16.0 unless a custom size is explicitly specified on the + /// icon itself. final Widget? sortIconWidget; /// Invoked when the user selects or unselects every row, using the From ab9414f73f57fd102c67d9564360a2e9afa17bc2 Mon Sep 17 00:00:00 2001 From: puneetkukreja98 Date: Mon, 3 Aug 2026 17:38:35 +0530 Subject: [PATCH 3/6] Adding sortIconBuilder parameter. --- packages/material_ui/lib/src/data_table.dart | 48 ++++--- .../material_ui/lib/src/data_table_theme.dart | 42 +++++- .../lib/src/paginated_data_table.dart | 6 + .../material_ui/test/data_table_test.dart | 122 ++++++++++++------ .../test/data_table_theme_test.dart | 4 + .../test/paginated_data_table_test.dart | 31 +++++ 6 files changed, 180 insertions(+), 73 deletions(-) diff --git a/packages/material_ui/lib/src/data_table.dart b/packages/material_ui/lib/src/data_table.dart index 3700db013bac..d7c727808fc6 100644 --- a/packages/material_ui/lib/src/data_table.dart +++ b/packages/material_ui/lib/src/data_table.dart @@ -508,7 +508,7 @@ class DataTable extends StatelessWidget { this.checkboxHorizontalMargin, this.border, this.clipBehavior = Clip.none, - this.sortIconWidget, + this.sortIconBuilder, }) : assert(columns.isNotEmpty), assert( sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length), @@ -563,15 +563,14 @@ class DataTable extends StatelessWidget { /// Ascending order is represented by an upwards-facing arrow. final bool sortAscending; - /// A widget to use as the sorting indicator icon for the table's header cells. + /// {@template flutter.material.dataTable.sortIconBuilder} + /// A builder function that returns a widget to use as the sorting indicator + /// icon for the table's header cells. /// - /// If null, the default Material design arrow icon ([Icons.arrow_upward]) is - /// used with a framework default size of 16.0. - /// - /// If this widget is an [Icon], the framework will automatically apply the - /// default size of 16.0 unless a custom size is explicitly specified on the - /// icon itself. - final Widget? sortIconWidget; + /// If null, [DataTableThemeData.sortIconBuilder] is used. If that is also null, + /// the default Material design sort arrow animation is used. + /// {@endtemplate} + final DataTableSortIconBuilder? sortIconBuilder; /// Invoked when the user selects or unselects every row, using the /// checkbox in the heading row. @@ -914,6 +913,11 @@ class DataTable extends StatelessWidget { }) { final ThemeData themeData = Theme.of(context); final DataTableThemeData dataTableTheme = DataTableTheme.of(context); + final DataTableSortIconBuilder? effectiveSortIconBuilder = + sortIconBuilder ?? + dataTableTheme.sortIconBuilder ?? + themeData.dataTableTheme.sortIconBuilder; + label = Semantics( role: SemanticsRole.columnHeader, child: Row( @@ -924,12 +928,14 @@ class DataTable extends StatelessWidget { const SizedBox(width: _SortArrowState._arrowIconSize + _sortArrowPadding), label, if (onSort != null) ...[ - _SortArrow( - visible: sorted, - up: sorted ? ascending : null, - duration: _sortArrowAnimationDuration, - sortIconWidget: sortIconWidget, - ), + if (effectiveSortIconBuilder != null) + effectiveSortIconBuilder(context, sorted, ascending) + else + _SortArrow( + visible: sorted, + up: sorted ? ascending : null, + duration: _sortArrowAnimationDuration, + ), const SizedBox(width: _sortArrowPadding), ], ], @@ -1355,7 +1361,6 @@ class _SortArrow extends StatefulWidget { required this.visible, required this.up, required this.duration, - this.sortIconWidget, }); final bool visible; @@ -1364,8 +1369,6 @@ class _SortArrow extends StatefulWidget { final Duration duration; - final Widget? sortIconWidget; - @override _SortArrowState createState() => _SortArrowState(); } @@ -1458,20 +1461,13 @@ class _SortArrowState extends State<_SortArrow> with TickerProviderStateMixin { @override Widget build(BuildContext context) { - // If the user provided a custom widget, use it. - // Otherwise, instantly fall back to the standard Material arrow icon. - final Widget iconWidget = widget.sortIconWidget ?? const Icon(Icons.arrow_upward); - return FadeTransition( opacity: _opacityAnimation, child: Transform( transform: Matrix4.rotationZ(_orientationOffset + _orientationAnimation.value) ..setTranslationRaw(0.0, _arrowIconBaselineOffset, 0.0), alignment: Alignment.center, - child: IconTheme.merge( - data: const IconThemeData(size: _arrowIconSize), - child: iconWidget, - ), + child: const Icon(Icons.arrow_upward, size: _arrowIconSize), ), ); } diff --git a/packages/material_ui/lib/src/data_table_theme.dart b/packages/material_ui/lib/src/data_table_theme.dart index af00ee9be04b..ff1298c6f888 100644 --- a/packages/material_ui/lib/src/data_table_theme.dart +++ b/packages/material_ui/lib/src/data_table_theme.dart @@ -15,6 +15,19 @@ import 'theme.dart'; // Examples can assume: // late BuildContext context; +///// Signature for a builder function that returns a widget to use as a sorting +/// indicator icon in a [DataTable] header cell. +/// +/// The [visible] parameter indicates whether the sort icon should be visible +/// (i.e. whether the column is the active sort column). +/// +/// The [ascending] parameter indicates whether the sort order is ascending. +typedef DataTableSortIconBuilder = Widget Function( + BuildContext context, + bool visible, + bool ascending, +); + /// Defines default property values for descendant [DataTable] /// widgets. /// @@ -59,17 +72,18 @@ class DataTableThemeData with Diagnosticable { this.headingCellCursor, this.dataRowCursor, this.headingRowAlignment, + this.sortIconBuilder, }) : assert( dataRowMinHeight == null || dataRowMaxHeight == null || dataRowMaxHeight >= dataRowMinHeight, ), - assert( - dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null), - 'dataRowHeight ($dataRowHeight) must not be set if dataRowMinHeight ($dataRowMinHeight) or dataRowMaxHeight ($dataRowMaxHeight) are set.', - ), - dataRowMinHeight = dataRowHeight ?? dataRowMinHeight, - dataRowMaxHeight = dataRowHeight ?? dataRowMaxHeight; + assert( + dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null), + 'dataRowHeight ($dataRowHeight) must not be set if dataRowMinHeight ($dataRowMinHeight) or dataRowMaxHeight ($dataRowMaxHeight) are set.', + ), + dataRowMinHeight = dataRowHeight ?? dataRowMinHeight, + dataRowMaxHeight = dataRowHeight ?? dataRowMaxHeight; /// {@macro material_ui.dataTable.decoration} final Decoration? decoration; @@ -125,6 +139,9 @@ class DataTableThemeData with Diagnosticable { /// If specified, overrides the default value of [DataColumn.headingRowAlignment]. final MainAxisAlignment? headingRowAlignment; + /// {@macro flutter.material.dataTable.sortIconBuilder} + final DataTableSortIconBuilder? sortIconBuilder; + /// Creates a copy of this object but with the given fields replaced with the /// new values. DataTableThemeData copyWith({ @@ -148,6 +165,7 @@ class DataTableThemeData with Diagnosticable { WidgetStateProperty? headingCellCursor, WidgetStateProperty? dataRowCursor, MainAxisAlignment? headingRowAlignment, + DataTableSortIconBuilder? sortIconBuilder, }) { assert( dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null), @@ -172,6 +190,7 @@ class DataTableThemeData with Diagnosticable { headingCellCursor: headingCellCursor ?? this.headingCellCursor, dataRowCursor: dataRowCursor ?? this.dataRowCursor, headingRowAlignment: headingRowAlignment ?? this.headingRowAlignment, + sortIconBuilder: sortIconBuilder ?? this.sortIconBuilder, ); } @@ -207,6 +226,7 @@ class DataTableThemeData with Diagnosticable { headingCellCursor: t < 0.5 ? a.headingCellCursor : b.headingCellCursor, dataRowCursor: t < 0.5 ? a.dataRowCursor : b.dataRowCursor, headingRowAlignment: t < 0.5 ? a.headingRowAlignment : b.headingRowAlignment, + sortIconBuilder: t < 0.5 ? a.sortIconBuilder : b.sortIconBuilder, ); } @@ -227,6 +247,7 @@ class DataTableThemeData with Diagnosticable { headingCellCursor, dataRowCursor, headingRowAlignment, + sortIconBuilder, ); @override @@ -252,7 +273,8 @@ class DataTableThemeData with Diagnosticable { other.checkboxHorizontalMargin == checkboxHorizontalMargin && other.headingCellCursor == headingCellCursor && other.dataRowCursor == dataRowCursor && - other.headingRowAlignment == headingRowAlignment; + other.headingRowAlignment == headingRowAlignment && + other.sortIconBuilder == sortIconBuilder; } @override @@ -309,6 +331,12 @@ class DataTableThemeData with Diagnosticable { defaultValue: null, ), ); + properties.add( + ObjectFlagProperty.has( + 'sortIconBuilder', + sortIconBuilder, + ), + ); } } diff --git a/packages/material_ui/lib/src/paginated_data_table.dart b/packages/material_ui/lib/src/paginated_data_table.dart index e014153125cd..851efa9e62e2 100644 --- a/packages/material_ui/lib/src/paginated_data_table.dart +++ b/packages/material_ui/lib/src/paginated_data_table.dart @@ -16,6 +16,7 @@ import 'card.dart'; import 'constants.dart'; import 'data_table.dart'; import 'data_table_source.dart'; +import 'data_table_theme.dart'; import 'debug.dart'; import 'dropdown.dart'; import 'icon_button.dart'; @@ -140,6 +141,7 @@ class PaginatedDataTable extends StatefulWidget { this.headingRowColor, this.dividerThickness, this.showEmptyRows = true, + this.sortIconBuilder, }) : assert(actions == null || (header != null)), assert(columns.isNotEmpty), assert( @@ -372,6 +374,9 @@ class PaginatedDataTable extends StatefulWidget { /// When set to `false`, empty rows will not be created. final bool showEmptyRows; + /// {@macro flutter.material.dataTable.sortIconBuilder} + final DataTableSortIconBuilder? sortIconBuilder; + @override PaginatedDataTableState createState() => PaginatedDataTableState(); } @@ -692,6 +697,7 @@ class PaginatedDataTableState extends State { showBottomBorder: true, rows: _getRows(_firstRowIndex, widget.rowsPerPage), headingRowColor: widget.headingRowColor, + sortIconBuilder: widget.sortIconBuilder, ), ), ), diff --git a/packages/material_ui/test/data_table_test.dart b/packages/material_ui/test/data_table_test.dart index 80aea302c4fe..8b40dee11540 100644 --- a/packages/material_ui/test/data_table_test.dart +++ b/packages/material_ui/test/data_table_test.dart @@ -432,76 +432,118 @@ void main() { expect(transformOfArrow.transform.getRotation(), equals(Matrix3.rotationZ(math.pi))); }); - testWidgets('DataTable custom sortIconWidget test', (WidgetTester tester) async { - Widget buildTable({Widget? sortIconWidget}) { - return DataTable( - sortColumnIndex: 0, - sortIconWidget: sortIconWidget, + testWidgets('DataTable custom sortIconBuilder test', (WidgetTester tester) async { + bool? capturedVisibleColumn0; + bool? capturedAscendingColumn0; + bool? capturedVisibleColumn1; + bool? capturedAscendingColumn1; + + Widget buildTable({ + DataTableSortIconBuilder? sortIconBuilder, + DataTableThemeData? themeData, + int? sortColumnIndex = 0, + bool sortAscending = true, + }) { + final Widget table = DataTable( + sortColumnIndex: sortColumnIndex, + sortAscending: sortAscending, + sortIconBuilder: sortIconBuilder, columns: [ DataColumn( label: const Text('Name'), - tooltip: 'Name', + onSort: (int columnIndex, bool ascending) {}, + ), + DataColumn( + label: const Text('Calories'), onSort: (int columnIndex, bool ascending) {}, ), ], rows: kDesserts.map((Dessert dessert) { - return DataRow(cells: [DataCell(Text(dessert.name))]); + return DataRow(cells: [ + DataCell(Text(dessert.name)), + DataCell(Text('${dessert.calories}')), + ]); }).toList(), ); + + return MaterialApp( + home: Material( + child: themeData != null ? DataTableTheme(data: themeData, child: table) : table, + ), + ); } - // By default (null sortIconWidget), the sort indicator is Icons.arrow_upward. - await tester.pumpWidget(MaterialApp(home: Material(child: buildTable()))); + // Default (null sortIconBuilder): uses standard Icon(Icons.arrow_upward). + await tester.pumpWidget(buildTable()); final Finder defaultIconFinder = find.descendant( of: find.byType(DataTable), matching: find.byIcon(Icons.arrow_upward), ); expect(defaultIconFinder, findsOneWidget); - expect(tester.widget(defaultIconFinder).size, isNull); - expect(IconTheme.of(tester.element(defaultIconFinder)).size, 16.0); - expect(tester.getSize(defaultIconFinder), const Size(16.0, 16.0)); - // Using a custom sortIconWidget (an Icon). + // Custom sortIconBuilder on DataTable. await tester.pumpWidget( - MaterialApp( - home: Material(child: buildTable(sortIconWidget: const Icon(Icons.arrow_downward))), + buildTable( + sortIconBuilder: (BuildContext context, bool visible, bool ascending) { + return Text(visible ? (ascending ? 'ASC' : 'DESC') : 'INACTIVE'); + }, ), ); - final Finder customIconFinder = find.descendant( - of: find.byType(DataTable), - matching: find.byIcon(Icons.arrow_downward), - ); - expect(customIconFinder, findsOneWidget); - expect(tester.widget(customIconFinder).size, isNull); - expect(IconTheme.of(tester.element(customIconFinder)).size, 16.0); - expect(tester.getSize(customIconFinder), const Size(16.0, 16.0)); + expect(find.text('ASC'), findsOneWidget); + expect(find.text('INACTIVE'), findsOneWidget); - // Using a custom sortIconWidget with explicit size. + // Verify sortAscending = false passes ascending = false. await tester.pumpWidget( - MaterialApp( - home: Material( - child: buildTable(sortIconWidget: const Icon(Icons.arrow_downward, size: 24.0)), - ), + buildTable( + sortAscending: false, + sortIconBuilder: (BuildContext context, bool visible, bool ascending) { + return Text(visible ? (ascending ? 'ASC' : 'DESC') : 'INACTIVE'); + }, ), ); - final Finder customSizeIconFinder = find.descendant( - of: find.byType(DataTable), - matching: find.byIcon(Icons.arrow_downward), + expect(find.text('DESC'), findsOneWidget); + expect(find.text('INACTIVE'), findsOneWidget); + + // Verify sorted column index change passes updated visible flags. + await tester.pumpWidget( + buildTable( + sortColumnIndex: 1, + sortAscending: true, + sortIconBuilder: (BuildContext context, bool visible, bool ascending) { + return Text(visible ? 'SORTED_COL1' : 'UNSORTED_COL0'); + }, + ), ); - expect(customSizeIconFinder, findsOneWidget); - expect(tester.widget(customSizeIconFinder).size, 24.0); - expect(tester.getSize(customSizeIconFinder), const Size(24.0, 24.0)); + expect(find.text('SORTED_COL1'), findsOneWidget); + expect(find.text('UNSORTED_COL0'), findsOneWidget); - // Using a custom non-Icon sortIconWidget. + // Verify DataTableThemeData.sortIconBuilder theme resolution. await tester.pumpWidget( - MaterialApp( - home: Material(child: buildTable(sortIconWidget: const Text('Sort'))), + buildTable( + themeData: DataTableThemeData( + sortIconBuilder: (BuildContext context, bool visible, bool ascending) { + return const Text('THEME_BUILDER'); + }, + ), ), ); - expect( - find.descendant(of: find.byType(DataTable), matching: find.text('Sort')), - findsOneWidget, + expect(find.text('THEME_BUILDER'), findsNWidgets(2)); + + // Verify DataTable.sortIconBuilder overrides DataTableThemeData.sortIconBuilder. + await tester.pumpWidget( + buildTable( + sortIconBuilder: (BuildContext context, bool visible, bool ascending) { + return const Text('WIDGET_BUILDER'); + }, + themeData: DataTableThemeData( + sortIconBuilder: (BuildContext context, bool visible, bool ascending) { + return const Text('THEME_BUILDER'); + }, + ), + ), ); + expect(find.text('WIDGET_BUILDER'), findsNWidgets(2)); + expect(find.text('THEME_BUILDER'), findsNothing); }); testWidgets('DataTable sort indicator orientation does not change on state update', ( diff --git a/packages/material_ui/test/data_table_theme_test.dart b/packages/material_ui/test/data_table_theme_test.dart index 6a59af053371..6227155769f4 100644 --- a/packages/material_ui/test/data_table_theme_test.dart +++ b/packages/material_ui/test/data_table_theme_test.dart @@ -45,6 +45,7 @@ void main() { expect(themeData.headingCellCursor, null); expect(themeData.dataRowCursor, null); expect(themeData.headingRowAlignment, null); + expect(themeData.sortIconBuilder, null); const theme = DataTableTheme(data: DataTableThemeData(), child: SizedBox()); expect(theme.data.decoration, null); @@ -63,6 +64,7 @@ void main() { expect(theme.data.headingCellCursor, null); expect(theme.data.dataRowCursor, null); expect(theme.data.headingRowAlignment, null); + expect(theme.data.sortIconBuilder, null); }); testWidgets('Default DataTableThemeData debugFillProperties', (WidgetTester tester) async { @@ -99,6 +101,7 @@ void main() { headingCellCursor: const MaterialStatePropertyAll(SystemMouseCursors.grab), dataRowCursor: const MaterialStatePropertyAll(SystemMouseCursors.forbidden), headingRowAlignment: MainAxisAlignment.center, + sortIconBuilder: (BuildContext context, bool visible, bool ascending) => const SizedBox(), ).debugFillProperties(builder); final List description = builder.properties @@ -121,6 +124,7 @@ void main() { expect(description[12], 'headingCellCursor: WidgetStatePropertyAll(SystemMouseCursor(grab))'); expect(description[13], 'dataRowCursor: WidgetStatePropertyAll(SystemMouseCursor(forbidden))'); expect(description[14], 'headingRowAlignment: center'); + expect(description[15], 'has sortIconBuilder'); }); testWidgets('DataTable is themeable', (WidgetTester tester) async { diff --git a/packages/material_ui/test/paginated_data_table_test.dart b/packages/material_ui/test/paginated_data_table_test.dart index c716991d072b..a12b9bfd87a8 100644 --- a/packages/material_ui/test/paginated_data_table_test.dart +++ b/packages/material_ui/test/paginated_data_table_test.dart @@ -1534,4 +1534,35 @@ void main() { ); expect(tester.getSize(find.byType(PaginatedDataTable)), Size.zero); }); + + testWidgets('PaginatedDataTable custom sortIconBuilder test', (WidgetTester tester) async { + final TestDataSource source = TestDataSource(); + addTearDown(source.dispose); + + await tester.pumpWidget( + MaterialApp( + home: PaginatedDataTable( + sortColumnIndex: 0, + sortAscending: true, + sortIconBuilder: (BuildContext context, bool visible, bool ascending) { + return Text( + visible + ? (ascending ? 'PAGINATED_ASC' : 'PAGINATED_DESC') + : 'PAGINATED_INACTIVE', + ); + }, + header: const Text('Test Table'), + columns: [ + DataColumn( + label: const Text('Name'), + onSort: (int columnIndex, bool ascending) {}, + ), + ], + source: source, + ), + ), + ); + + expect(find.text('PAGINATED_ASC'), findsOneWidget); + }); } From 39d4261502f8bfde727c652ab001a257df799918 Mon Sep 17 00:00:00 2001 From: puneetkukreja98 Date: Tue, 4 Aug 2026 17:10:45 +0530 Subject: [PATCH 4/6] Fix breaking unit test and format code for sortIconBuilder --- packages/material_ui/lib/src/data_table.dart | 6 +---- .../material_ui/lib/src/data_table_theme.dart | 24 +++++++----------- .../material_ui/test/data_table_test.dart | 25 +++++-------------- .../test/paginated_data_table_test.dart | 16 ++++-------- 4 files changed, 21 insertions(+), 50 deletions(-) diff --git a/packages/material_ui/lib/src/data_table.dart b/packages/material_ui/lib/src/data_table.dart index d7c727808fc6..ddd00b664693 100644 --- a/packages/material_ui/lib/src/data_table.dart +++ b/packages/material_ui/lib/src/data_table.dart @@ -1357,11 +1357,7 @@ class TableRowInkWell extends InkResponse { } class _SortArrow extends StatefulWidget { - const _SortArrow({ - required this.visible, - required this.up, - required this.duration, - }); + const _SortArrow({required this.visible, required this.up, required this.duration}); final bool visible; diff --git a/packages/material_ui/lib/src/data_table_theme.dart b/packages/material_ui/lib/src/data_table_theme.dart index ff1298c6f888..5777e2929560 100644 --- a/packages/material_ui/lib/src/data_table_theme.dart +++ b/packages/material_ui/lib/src/data_table_theme.dart @@ -22,11 +22,8 @@ import 'theme.dart'; /// (i.e. whether the column is the active sort column). /// /// The [ascending] parameter indicates whether the sort order is ascending. -typedef DataTableSortIconBuilder = Widget Function( - BuildContext context, - bool visible, - bool ascending, -); +typedef DataTableSortIconBuilder = + Widget Function(BuildContext context, bool visible, bool ascending); /// Defines default property values for descendant [DataTable] /// widgets. @@ -78,12 +75,12 @@ class DataTableThemeData with Diagnosticable { dataRowMaxHeight == null || dataRowMaxHeight >= dataRowMinHeight, ), - assert( - dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null), - 'dataRowHeight ($dataRowHeight) must not be set if dataRowMinHeight ($dataRowMinHeight) or dataRowMaxHeight ($dataRowMaxHeight) are set.', - ), - dataRowMinHeight = dataRowHeight ?? dataRowMinHeight, - dataRowMaxHeight = dataRowHeight ?? dataRowMaxHeight; + assert( + dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null), + 'dataRowHeight ($dataRowHeight) must not be set if dataRowMinHeight ($dataRowMinHeight) or dataRowMaxHeight ($dataRowMaxHeight) are set.', + ), + dataRowMinHeight = dataRowHeight ?? dataRowMinHeight, + dataRowMaxHeight = dataRowHeight ?? dataRowMaxHeight; /// {@macro material_ui.dataTable.decoration} final Decoration? decoration; @@ -332,10 +329,7 @@ class DataTableThemeData with Diagnosticable { ), ); properties.add( - ObjectFlagProperty.has( - 'sortIconBuilder', - sortIconBuilder, - ), + ObjectFlagProperty.has('sortIconBuilder', sortIconBuilder), ); } } diff --git a/packages/material_ui/test/data_table_test.dart b/packages/material_ui/test/data_table_test.dart index 8b40dee11540..4783584326dd 100644 --- a/packages/material_ui/test/data_table_test.dart +++ b/packages/material_ui/test/data_table_test.dart @@ -433,11 +433,6 @@ void main() { }); testWidgets('DataTable custom sortIconBuilder test', (WidgetTester tester) async { - bool? capturedVisibleColumn0; - bool? capturedAscendingColumn0; - bool? capturedVisibleColumn1; - bool? capturedAscendingColumn1; - Widget buildTable({ DataTableSortIconBuilder? sortIconBuilder, DataTableThemeData? themeData, @@ -449,20 +444,13 @@ void main() { sortAscending: sortAscending, sortIconBuilder: sortIconBuilder, columns: [ - DataColumn( - label: const Text('Name'), - onSort: (int columnIndex, bool ascending) {}, - ), - DataColumn( - label: const Text('Calories'), - onSort: (int columnIndex, bool ascending) {}, - ), + DataColumn(label: const Text('Name'), onSort: (int columnIndex, bool ascending) {}), + DataColumn(label: const Text('Calories'), onSort: (int columnIndex, bool ascending) {}), ], rows: kDesserts.map((Dessert dessert) { - return DataRow(cells: [ - DataCell(Text(dessert.name)), - DataCell(Text('${dessert.calories}')), - ]); + return DataRow( + cells: [DataCell(Text(dessert.name)), DataCell(Text('${dessert.calories}'))], + ); }).toList(), ); @@ -479,7 +467,7 @@ void main() { of: find.byType(DataTable), matching: find.byIcon(Icons.arrow_upward), ); - expect(defaultIconFinder, findsOneWidget); + expect(defaultIconFinder, findsNWidgets(2)); // Custom sortIconBuilder on DataTable. await tester.pumpWidget( @@ -508,7 +496,6 @@ void main() { await tester.pumpWidget( buildTable( sortColumnIndex: 1, - sortAscending: true, sortIconBuilder: (BuildContext context, bool visible, bool ascending) { return Text(visible ? 'SORTED_COL1' : 'UNSORTED_COL0'); }, diff --git a/packages/material_ui/test/paginated_data_table_test.dart b/packages/material_ui/test/paginated_data_table_test.dart index a12b9bfd87a8..b8b86a6e07eb 100644 --- a/packages/material_ui/test/paginated_data_table_test.dart +++ b/packages/material_ui/test/paginated_data_table_test.dart @@ -1536,27 +1536,21 @@ void main() { }); testWidgets('PaginatedDataTable custom sortIconBuilder test', (WidgetTester tester) async { - final TestDataSource source = TestDataSource(); - addTearDown(source.dispose); - await tester.pumpWidget( MaterialApp( home: PaginatedDataTable( sortColumnIndex: 0, - sortAscending: true, sortIconBuilder: (BuildContext context, bool visible, bool ascending) { return Text( - visible - ? (ascending ? 'PAGINATED_ASC' : 'PAGINATED_DESC') - : 'PAGINATED_INACTIVE', + visible ? (ascending ? 'PAGINATED_ASC' : 'PAGINATED_DESC') : 'PAGINATED_INACTIVE', ); }, header: const Text('Test Table'), + rowsPerPage: 2, columns: [ - DataColumn( - label: const Text('Name'), - onSort: (int columnIndex, bool ascending) {}, - ), + DataColumn(label: const Text('Name'), onSort: (int columnIndex, bool ascending) {}), + const DataColumn(label: Text('Calories'), numeric: true), + const DataColumn(label: Text('Generation')), ], source: source, ), From 1047cbfc9a58fd9c5f66e090b412f2fad73017b3 Mon Sep 17 00:00:00 2001 From: puneetkukreja98 Date: Mon, 10 Aug 2026 14:25:30 +0530 Subject: [PATCH 5/6] Update sortIconBuilder docs with transition and sizing details --- packages/material_ui/lib/src/data_table.dart | 12 ++++++++++++ packages/material_ui/lib/src/data_table_theme.dart | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/material_ui/lib/src/data_table.dart b/packages/material_ui/lib/src/data_table.dart index ddd00b664693..5773008eb119 100644 --- a/packages/material_ui/lib/src/data_table.dart +++ b/packages/material_ui/lib/src/data_table.dart @@ -567,6 +567,18 @@ class DataTable extends StatelessWidget { /// A builder function that returns a widget to use as the sorting indicator /// icon for the table's header cells. /// + /// When providing a custom sort icon via this builder, the default sort arrow's + /// automatic rotation and opacity transitions are omitted. To achieve animated + /// transitions for custom icons, wrap the returned widget in explicit + /// transition widgets such as [AnimatedRotation] or [AnimatedOpacity]. + /// + /// Custom icons should target a size of approximately 18.0 logical pixels or + /// be wrapped in a fixed-size container to maintain visually balanced header + /// labels. For columns where [DataColumn.numeric] is true, [DataTable] adds + /// a leading 20.0 logical pixel spacer to mirror the space occupied by the + /// sort icon and its padding, ensuring centered column labels remain properly + /// aligned. + /// /// If null, [DataTableThemeData.sortIconBuilder] is used. If that is also null, /// the default Material design sort arrow animation is used. /// {@endtemplate} diff --git a/packages/material_ui/lib/src/data_table_theme.dart b/packages/material_ui/lib/src/data_table_theme.dart index 5777e2929560..f6296281d1a8 100644 --- a/packages/material_ui/lib/src/data_table_theme.dart +++ b/packages/material_ui/lib/src/data_table_theme.dart @@ -15,7 +15,7 @@ import 'theme.dart'; // Examples can assume: // late BuildContext context; -///// Signature for a builder function that returns a widget to use as a sorting +/// Signature for a builder function that returns a widget to use as a sorting /// indicator icon in a [DataTable] header cell. /// /// The [visible] parameter indicates whether the sort icon should be visible From ec07199bf1475c83424f48e9fbe1325c4cccfee6 Mon Sep 17 00:00:00 2001 From: puneetkukreja98 Date: Thu, 27 Aug 2026 10:25:20 +0530 Subject: [PATCH 6/6] Add changelog and version bump --- packages/material_ui/CHANGELOG.md | 4 ++++ packages/material_ui/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/material_ui/CHANGELOG.md b/packages/material_ui/CHANGELOG.md index 1f05474e3080..7f04bb044c50 100644 --- a/packages/material_ui/CHANGELOG.md +++ b/packages/material_ui/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.2.0 + +* Adds `sortIconBuilder` parameter to `DataTable`, `PaginatedDataTable`, and `DataTableThemeData` for custom sorting indicators. + ## 1.1.0 - Add missing Widget of the Week videos to widget API docs. diff --git a/packages/material_ui/pubspec.yaml b/packages/material_ui/pubspec.yaml index 6ee348a404c6..858529f465ff 100644 --- a/packages/material_ui/pubspec.yaml +++ b/packages/material_ui/pubspec.yaml @@ -1,6 +1,6 @@ name: material_ui description: The official Flutter Material UI Library, implementing Google's Material Design design system. -version: 1.1.0 +version: 1.2.0 repository: https://github.com/flutter/packages/tree/main/packages/material_ui issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A%20material%20design%22