From b215737883e7cfe82623bbda9959e0d0b1d85d8d Mon Sep 17 00:00:00 2001 From: Benji Date: Thu, 10 Sep 2026 16:12:14 +1200 Subject: [PATCH] [material_ui] Keep ExpansionPanelList dividers when expanded with 0 materialGapSize (#183974) Fixes https://github.com/flutter/flutter/issues/183974 --- .../expansion_panel_list.2.dart | 96 +++++++++++++++++++ .../expansion_panel_list.2_test.dart | 26 +++++ .../material_ui/lib/src/expansion_panel.dart | 23 ++++- .../expansion_panel_retain_dividers.yaml | 4 + .../test/expansion_panel_test.dart | 59 ++++++++++-- 5 files changed, 199 insertions(+), 9 deletions(-) create mode 100644 packages/material_ui/example/lib/expansion_panel/expansion_panel_list.2.dart create mode 100644 packages/material_ui/example/test/expansion_panel/expansion_panel_list.2_test.dart create mode 100644 packages/material_ui/pending_changelogs/expansion_panel_retain_dividers.yaml diff --git a/packages/material_ui/example/lib/expansion_panel/expansion_panel_list.2.dart b/packages/material_ui/example/lib/expansion_panel/expansion_panel_list.2.dart new file mode 100644 index 000000000000..cb50a7f16ec8 --- /dev/null +++ b/packages/material_ui/example/lib/expansion_panel/expansion_panel_list.2.dart @@ -0,0 +1,96 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// #region body +import 'package:material_ui/material_ui.dart'; + +/// Flutter code sample for a flat [ExpansionPanelList] with divider separators. + +void main() => runApp(const FlatExpansionPanelListExampleApp()); + +class FlatExpansionPanelListExampleApp extends StatelessWidget { + const FlatExpansionPanelListExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar(title: const Text('Flat ExpansionPanelList Sample')), + body: const Center(child: FlatExpansionPanelListExample()), + ), + ); + } +} + +class FlatExpansionPanelListExample extends StatefulWidget { + const FlatExpansionPanelListExample({super.key}); + + @override + State createState() => + _FlatExpansionPanelListExampleState(); +} + +class _FlatExpansionPanelListExampleState + extends State { + final List _isExpanded = [false, false, false]; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Padding( + padding: const .all(16.0), + child: Column( + children: [ + const Padding( + padding: .only(bottom: 16.0), + child: Text( + 'With materialGapSize set to 0, dividers between expanded panels are ' + 'preserved, creating a flat list appearance.', + textAlign: .center, + ), + ), + Card( + child: ExpansionPanelList( + materialGapSize: 0, + expandedHeaderPadding: .zero, + expansionCallback: (int index, bool isExpanded) { + setState(() { + _isExpanded[index] = isExpanded; + }); + }, + children: [ + ExpansionPanel( + headerBuilder: (BuildContext context, bool isExpanded) { + return const ListTile(title: Text('Panel A')); + }, + body: const ListTile(title: Text('Content for Panel A')), + isExpanded: _isExpanded[0], + canTapOnHeader: true, + ), + ExpansionPanel( + headerBuilder: (BuildContext context, bool isExpanded) { + return const ListTile(title: Text('Panel B')); + }, + body: const ListTile(title: Text('Content for Panel B')), + isExpanded: _isExpanded[1], + canTapOnHeader: true, + ), + ExpansionPanel( + headerBuilder: (BuildContext context, bool isExpanded) { + return const ListTile(title: Text('Panel C')); + }, + body: const ListTile(title: Text('Content for Panel C')), + isExpanded: _isExpanded[2], + canTapOnHeader: true, + ), + ], + ), + ), + ], + ), + ), + ); + } +} +// #endregion body diff --git a/packages/material_ui/example/test/expansion_panel/expansion_panel_list.2_test.dart b/packages/material_ui/example/test/expansion_panel/expansion_panel_list.2_test.dart new file mode 100644 index 000000000000..ff032f97e18b --- /dev/null +++ b/packages/material_ui/example/test/expansion_panel/expansion_panel_list.2_test.dart @@ -0,0 +1,26 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:material_ui/material_ui.dart'; +import 'package:material_ui_examples/expansion_panel/expansion_panel_list.2.dart' + as example; + +void main() { + testWidgets( + 'Flat ExpansionPanelList preserves dividers with no MaterialGap', + (WidgetTester tester) async { + await tester.pumpWidget(const example.FlatExpansionPanelListExampleApp()); + + await tester.tap(find.text('Panel A')); + await tester.pumpAndSettle(); + + final MergeableMaterial mergeableMaterial = tester.widget( + find.byType(MergeableMaterial), + ); + expect(mergeableMaterial.children.whereType().length, 0); + expect(mergeableMaterial.hasDividers, true); + }, + ); +} diff --git a/packages/material_ui/lib/src/expansion_panel.dart b/packages/material_ui/lib/src/expansion_panel.dart index 1f94e8627f89..6361dba98ff6 100644 --- a/packages/material_ui/lib/src/expansion_panel.dart +++ b/packages/material_ui/lib/src/expansion_panel.dart @@ -292,6 +292,19 @@ class ExpansionPanelList extends StatefulWidget { /// Defines the [MaterialGap.size] of the [MaterialGap] which is placed /// between the [ExpansionPanelList.children] when they're expanded. /// + /// When set to zero, no [MaterialGap] is inserted between expanded panels + /// and dividers are preserved, allowing a flat, divider-separated appearance. + /// + /// + /// + /// Here is an example with [materialGapSize] set to zero. + /// + /// {@macro material_ui.dartpad_guide} + /// + /// {@example /example/lib/expansion_panel/expansion_panel_list.2.dart#body} + /// + /// + /// /// Defaults to `16.0`. final double materialGapSize; @@ -392,9 +405,13 @@ class _ExpansionPanelListState extends State { ); final items = []; + final bool shouldInsertMaterialGap = widget.materialGapSize > 0; for (var index = 0; index < widget.children.length; index += 1) { - if (_isChildExpanded(index) && index != 0 && !_isChildExpanded(index - 1)) { + if (_isChildExpanded(index) && + index != 0 && + !_isChildExpanded(index - 1) && + shouldInsertMaterialGap) { items.add( MaterialGap( key: _SaltedKey(context, index * 2 - 1), @@ -483,7 +500,9 @@ class _ExpansionPanelListState extends State { ), ); - if (_isChildExpanded(index) && index != widget.children.length - 1) { + if (_isChildExpanded(index) && + index != widget.children.length - 1 && + shouldInsertMaterialGap) { items.add( MaterialGap( key: _SaltedKey(context, index * 2 + 1), diff --git a/packages/material_ui/pending_changelogs/expansion_panel_retain_dividers.yaml b/packages/material_ui/pending_changelogs/expansion_panel_retain_dividers.yaml new file mode 100644 index 000000000000..8f9f240df9e2 --- /dev/null +++ b/packages/material_ui/pending_changelogs/expansion_panel_retain_dividers.yaml @@ -0,0 +1,4 @@ +changelog: | + - Keeps `ExpansionPanelList` dividers when expanded if `materialGapSize` is 0. +version: minor + diff --git a/packages/material_ui/test/expansion_panel_test.dart b/packages/material_ui/test/expansion_panel_test.dart index b5b51553f12e..09bd5506a66a 100644 --- a/packages/material_ui/test/expansion_panel_test.dart +++ b/packages/material_ui/test/expansion_panel_test.dart @@ -1938,14 +1938,9 @@ void main() { await tester.pumpWidget(buildWidgetForTest(materialGapSize: 0)); await tester.pumpAndSettle(); final MergeableMaterial mergeableMaterial = tester.widget(find.byType(MergeableMaterial)); - expect(mergeableMaterial.children.length, 3); - expect(mergeableMaterial.children.whereType().length, 1); + expect(mergeableMaterial.children.length, 2); + expect(mergeableMaterial.children.whereType().length, 0); expect(mergeableMaterial.children.whereType().length, 2); - for (final MergeableMaterialItem e in mergeableMaterial.children) { - if (e is MaterialGap) { - expect(e.size, 0); - } - } await tester.pumpWidget(buildWidgetForTest(materialGapSize: 20)); await tester.pumpAndSettle(); @@ -1972,6 +1967,56 @@ void main() { } }); + Future pumpExpansionPanelListAndVerifyGaps( + WidgetTester tester, { + required double materialGapSize, + required int expectedGapCount, + }) async { + await tester.pumpWidget( + MaterialApp( + home: SingleChildScrollView( + child: ExpansionPanelList( + materialGapSize: materialGapSize, + children: [ + ExpansionPanel( + isExpanded: true, + canTapOnHeader: true, + body: const SizedBox.shrink(), + headerBuilder: (BuildContext context, bool isExpanded) { + return const SizedBox.shrink(); + }, + ), + ExpansionPanel( + canTapOnHeader: true, + body: const SizedBox.shrink(), + headerBuilder: (BuildContext context, bool isExpanded) { + return const SizedBox.shrink(); + }, + ), + ], + ), + ), + ), + ); + await tester.pumpAndSettle(); + + final MergeableMaterial mergeableMaterial = tester.widget(find.byType(MergeableMaterial)); + expect(mergeableMaterial.children.whereType().length, expectedGapCount); + expect(mergeableMaterial.children.whereType().length, 2); + } + + testWidgets('ExpansionPanelList does not insert MaterialGap when materialGapSize is 0', ( + WidgetTester tester, + ) async { + await pumpExpansionPanelListAndVerifyGaps(tester, materialGapSize: 0, expectedGapCount: 0); + }); + + testWidgets('ExpansionPanelList inserts MaterialGap when materialGapSize is greater than 0', ( + WidgetTester tester, + ) async { + await pumpExpansionPanelListAndVerifyGaps(tester, materialGapSize: 16, expectedGapCount: 1); + }); + testWidgets( 'Ensure IconButton splashColor and highlightColor are correctly set when canTapOnHeader is false', (WidgetTester tester) async {