diff --git a/lib/widgets/settings_group.dart b/lib/widgets/settings_group.dart index 39e7455a..55daf646 100644 --- a/lib/widgets/settings_group.dart +++ b/lib/widgets/settings_group.dart @@ -193,6 +193,23 @@ class _SettingsSearchTargetState extends State { } } +/// Whether rows sit in an expressive connected [SettingsGroup], where the gaps +/// between row surfaces replace dividers. +bool _rowsConnected(BuildContext context) => + context.dependOnInheritedWidgetOfExactType<_ConnectedRowsScope>() != null; + +BorderRadius _connectedRowRadius({ + required Radius top, + required Radius bottom, +}) => BorderRadius.vertical(top: top, bottom: bottom); + +class _ConnectedRowsScope extends InheritedWidget { + const _ConnectedRowsScope({required super.child}); + + @override + bool updateShouldNotify(_ConnectedRowsScope oldWidget) => false; +} + class SettingsGroup extends StatelessWidget { final List children; final EdgeInsetsGeometry? margin; @@ -244,7 +261,59 @@ class SettingsGroup extends StatelessWidget { ); } - if (materialExpressiveEnabled(context)) { + final expressive = materialExpressiveEnabled(context); + // Only groups of standard rows split into connected tiles. Forms and + // custom content keep one card, so spacers never become empty tiles. + final standardRows = + children.isNotEmpty && + children.every( + (child) => child is SettingsItem || child is SettingsSwitchItem, + ); + if (expressive && standardRows) { + // Expressive connected list: every row is its own surface separated by + // a 2dp gap. The group's ends keep the card radius, inner corners stay + // tight, and the gaps replace row dividers. + final outer = Radius.circular(context.tokens.radiusCard); + const inner = Radius.circular(4); + final last = children.length - 1; + return LayoutBuilder( + builder: (context, constraints) => Padding( + padding: + margin ?? + EdgeInsets.symmetric( + horizontal: + 16 + + (constraints.hasBoundedWidth + ? wideInsetForWidth(constraints.maxWidth) + : 0), + vertical: 4, + ), + child: _ConnectedRowsScope( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + for (final (index, row) in children.indexed) ...[ + if (index > 0) const SizedBox(height: 2), + Material( + color: cardColor, + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: _connectedRowRadius( + top: index == 0 ? outer : inner, + bottom: index == last ? outer : inner, + ), + ), + child: row, + ), + ], + ], + ), + ), + ), + ); + } + + if (expressive) { return LayoutBuilder( builder: (context, constraints) => Padding( padding: @@ -417,7 +486,7 @@ class SettingsItem extends StatelessWidget { ), ), ), - if (showDivider) + if (showDivider && !_rowsConnected(context)) Divider( height: 1, indent: showIcon @@ -538,7 +607,7 @@ class SettingsSwitchItem extends StatelessWidget { ), ), ), - if (showDivider) + if (showDivider && !_rowsConnected(context)) Divider( height: 1, indent: showIcon diff --git a/test/material_expressive_test.dart b/test/material_expressive_test.dart index 8601c055..4e1c8616 100644 --- a/test/material_expressive_test.dart +++ b/test/material_expressive_test.dart @@ -454,4 +454,46 @@ void main() { expect(find.byType(CupertinoButton), findsAtLeast(2)); expect(tester.takeException(), isNull); }); + + testWidgets('standard settings rows connect; custom content keeps a card', ( + tester, + ) async { + await tester.pumpWidget( + _host( + SettingsGroup( + children: [ + SettingsItem(title: 'First', onTap: () {}), + SettingsSwitchItem(title: 'Second', value: true, onChanged: (_) {}), + ], + ), + ), + ); + await tester.pumpAndSettle(); + final group = find.byType(SettingsGroup); + expect( + find.descendant(of: group, matching: find.byType(M3ECard)), + findsNothing, + ); + expect( + find.descendant(of: group, matching: find.byType(Divider)), + findsNothing, + ); + final first = tester.getRect(find.text('First')); + final second = tester.getRect(find.text('Second')); + expect(second.top, greaterThan(first.bottom)); + + await tester.pumpWidget( + _host( + SettingsGroup( + children: [ + SettingsItem(title: 'Field', onTap: () {}), + const SizedBox(height: 12), + ], + ), + ), + ); + await tester.pumpAndSettle(); + expect(find.byType(M3ECard), findsOneWidget); + expect(tester.takeException(), isNull); + }); }