diff --git a/dev/tools/gen_defaults/lib/popup_menu_template.dart b/dev/tools/gen_defaults/lib/popup_menu_template.dart index 61076c3dd33..56eae16bbfd 100644 --- a/dev/tools/gen_defaults/lib/popup_menu_template.dart +++ b/dev/tools/gen_defaults/lib/popup_menu_template.dart @@ -44,8 +44,13 @@ class _${blockName}DefaultsM3 extends PopupMenuThemeData { @override ShapeBorder? get shape => ${shape("md.comp.menu.container")}; + // TODO(bleroux): This is taken from https://m3.material.io/components/menus/specs + // Update this when the token is available. + @override + EdgeInsets? get menuPadding => const EdgeInsets.symmetric(vertical: 8.0); + // TODO(tahatesser): This is taken from https://m3.material.io/components/menus/specs // Update this when the token is available. - static EdgeInsets menuHorizontalPadding = const EdgeInsets.symmetric(horizontal: 12.0); + static EdgeInsets menuItemPadding = const EdgeInsets.symmetric(horizontal: 12.0); }'''; } diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart index dc0d4d5c019..052b5f8b81d 100644 --- a/packages/flutter/lib/src/material/popup_menu.dart +++ b/packages/flutter/lib/src/material/popup_menu.dart @@ -39,7 +39,6 @@ const double _kMenuCloseIntervalEnd = 2.0 / 3.0; const double _kMenuDividerHeight = 16.0; const double _kMenuMaxWidth = 5.0 * _kMenuWidthStep; const double _kMenuMinWidth = 2.0 * _kMenuWidthStep; -const double _kMenuVerticalPadding = 8.0; const double _kMenuWidthStep = 56.0; const double _kMenuScreenPadding = 8.0; @@ -379,7 +378,7 @@ class PopupMenuItemState> extends State { child: Container( alignment: AlignmentDirectional.centerStart, constraints: BoxConstraints(minHeight: widget.height), - padding: widget.padding ?? (theme.useMaterial3 ? _PopupMenuDefaultsM3.menuHorizontalPadding : _PopupMenuDefaultsM2.menuHorizontalPadding), + padding: widget.padding ?? (theme.useMaterial3 ? _PopupMenuDefaultsM3.menuItemPadding : _PopupMenuDefaultsM2.menuItemPadding), child: buildChild(), ), ); @@ -610,7 +609,6 @@ class _PopupMenuState extends State<_PopupMenu> { ) { _setOpacities(); } - } void _setOpacities() { @@ -687,9 +685,7 @@ class _PopupMenuState extends State<_PopupMenu> { explicitChildNodes: true, label: widget.semanticLabel, child: SingleChildScrollView( - padding: const EdgeInsets.symmetric( - vertical: _kMenuVerticalPadding, - ), + padding: widget.route.menuPadding ?? popupMenuTheme.menuPadding ?? defaults.menuPadding, child: ListBody(children: children), ), ), @@ -853,6 +849,7 @@ class _PopupMenuRoute extends PopupRoute { required this.barrierLabel, this.semanticLabel, this.shape, + this.menuPadding, this.color, required this.capturedThemes, this.constraints, @@ -874,6 +871,7 @@ class _PopupMenuRoute extends PopupRoute { final Color? shadowColor; final String? semanticLabel; final ShapeBorder? shape; + final EdgeInsetsGeometry? menuPadding; final Color? color; final CapturedThemes capturedThemes; final BoxConstraints? constraints; @@ -1040,6 +1038,7 @@ Future showMenu({ Color? surfaceTintColor, String? semanticLabel, ShapeBorder? shape, + EdgeInsetsGeometry? menuPadding, Color? color, bool useRootNavigator = false, BoxConstraints? constraints, @@ -1074,6 +1073,7 @@ Future showMenu({ semanticLabel: semanticLabel, barrierLabel: MaterialLocalizations.of(context).menuDismissLabel, shape: shape, + menuPadding: menuPadding, color: color, capturedThemes: InheritedTheme.capture(from: context, to: navigator.context), constraints: constraints, @@ -1194,6 +1194,7 @@ class PopupMenuButton extends StatefulWidget { this.shadowColor, this.surfaceTintColor, this.padding = const EdgeInsets.all(8.0), + this.menuPadding, this.child, this.splashRadius, this.icon, @@ -1274,6 +1275,14 @@ class PopupMenuButton extends StatefulWidget { /// to set the padding to zero. final EdgeInsetsGeometry padding; + /// If provided, menu padding is used for empty space around the outside + /// of the popup menu. + /// + /// If this property is null, then [PopupMenuThemeData.menuPadding] is used. + /// If [PopupMenuThemeData.menuPadding] is also null, then vertical padding + /// of 8 pixels is used. + final EdgeInsetsGeometry? menuPadding; + /// The splash radius. /// /// If null, default splash radius of [InkWell] or [IconButton] is used. @@ -1475,6 +1484,7 @@ class PopupMenuButtonState extends State> { initialValue: widget.initialValue, position: position, shape: widget.shape ?? popupMenuTheme.shape, + menuPadding: widget.menuPadding ?? popupMenuTheme.menuPadding, color: widget.color ?? popupMenuTheme.color, constraints: widget.constraints, clipBehavior: widget.clipBehavior, @@ -1571,7 +1581,10 @@ class _PopupMenuDefaultsM2 extends PopupMenuThemeData { @override TextStyle? get textStyle => _textTheme.titleMedium; - static EdgeInsets menuHorizontalPadding = const EdgeInsets.symmetric(horizontal: 16.0); + @override + EdgeInsets? get menuPadding => const EdgeInsets.symmetric(vertical: 8.0); + + static EdgeInsets menuItemPadding = const EdgeInsets.symmetric(horizontal: 16.0); } // BEGIN GENERATED TOKEN PROPERTIES - PopupMenu @@ -1613,8 +1626,13 @@ class _PopupMenuDefaultsM3 extends PopupMenuThemeData { @override ShapeBorder? get shape => const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))); + // TODO(bleroux): This is taken from https://m3.material.io/components/menus/specs + // Update this when the token is available. + @override + EdgeInsets? get menuPadding => const EdgeInsets.symmetric(vertical: 8.0); + // TODO(tahatesser): This is taken from https://m3.material.io/components/menus/specs // Update this when the token is available. - static EdgeInsets menuHorizontalPadding = const EdgeInsets.symmetric(horizontal: 12.0); + static EdgeInsets menuItemPadding = const EdgeInsets.symmetric(horizontal: 12.0); } // END GENERATED TOKEN PROPERTIES - PopupMenu diff --git a/packages/flutter/lib/src/material/popup_menu_theme.dart b/packages/flutter/lib/src/material/popup_menu_theme.dart index 5d2c675398c..2dbb58bd5f9 100644 --- a/packages/flutter/lib/src/material/popup_menu_theme.dart +++ b/packages/flutter/lib/src/material/popup_menu_theme.dart @@ -47,6 +47,7 @@ class PopupMenuThemeData with Diagnosticable { const PopupMenuThemeData({ this.color, this.shape, + this.menuPadding, this.elevation, this.shadowColor, this.surfaceTintColor, @@ -65,6 +66,11 @@ class PopupMenuThemeData with Diagnosticable { /// The shape of the popup menu. final ShapeBorder? shape; + /// If specified, the padding of the popup menu. + /// + /// If [PopupMenuButton.menuPadding] is provided, [menuPadding] is ignored. + final EdgeInsetsGeometry? menuPadding; + /// The elevation of the popup menu. final double? elevation; @@ -108,6 +114,7 @@ class PopupMenuThemeData with Diagnosticable { PopupMenuThemeData copyWith({ Color? color, ShapeBorder? shape, + EdgeInsetsGeometry? menuPadding, double? elevation, Color? shadowColor, Color? surfaceTintColor, @@ -122,6 +129,7 @@ class PopupMenuThemeData with Diagnosticable { return PopupMenuThemeData( color: color ?? this.color, shape: shape ?? this.shape, + menuPadding: menuPadding ?? this.menuPadding, elevation: elevation ?? this.elevation, shadowColor: shadowColor ?? this.shadowColor, surfaceTintColor: surfaceTintColor ?? this.surfaceTintColor, @@ -147,6 +155,7 @@ class PopupMenuThemeData with Diagnosticable { return PopupMenuThemeData( color: Color.lerp(a?.color, b?.color, t), shape: ShapeBorder.lerp(a?.shape, b?.shape, t), + menuPadding: EdgeInsetsGeometry.lerp(a?.menuPadding, b?.menuPadding, t), elevation: lerpDouble(a?.elevation, b?.elevation, t), shadowColor: Color.lerp(a?.shadowColor, b?.shadowColor, t), surfaceTintColor: Color.lerp(a?.surfaceTintColor, b?.surfaceTintColor, t), @@ -164,6 +173,7 @@ class PopupMenuThemeData with Diagnosticable { int get hashCode => Object.hash( color, shape, + menuPadding, elevation, shadowColor, surfaceTintColor, @@ -187,6 +197,7 @@ class PopupMenuThemeData with Diagnosticable { return other is PopupMenuThemeData && other.color == color && other.shape == shape + && other.menuPadding == menuPadding && other.elevation == elevation && other.shadowColor == shadowColor && other.surfaceTintColor == surfaceTintColor @@ -204,6 +215,7 @@ class PopupMenuThemeData with Diagnosticable { super.debugFillProperties(properties); properties.add(ColorProperty('color', color, defaultValue: null)); properties.add(DiagnosticsProperty('shape', shape, defaultValue: null)); + properties.add(DiagnosticsProperty('menuPadding', menuPadding, defaultValue: null)); properties.add(DoubleProperty('elevation', elevation, defaultValue: null)); properties.add(ColorProperty('shadowColor', shadowColor, defaultValue: null)); properties.add(ColorProperty('surfaceTintColor', surfaceTintColor, defaultValue: null)); diff --git a/packages/flutter/test/material/popup_menu_test.dart b/packages/flutter/test/material/popup_menu_test.dart index 7c530194526..d6258b98c0f 100644 --- a/packages/flutter/test/material/popup_menu_test.dart +++ b/packages/flutter/test/material/popup_menu_test.dart @@ -1671,6 +1671,44 @@ void main() { expect(tester.widget(find.widgetWithText(Container, 'Item 1')).padding, const EdgeInsets.symmetric(horizontal: 12.0)); }); + testWidgets('PopupMenu default padding', (WidgetTester tester) async { + final Key popupMenuButtonKey = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: PopupMenuButton( + key: popupMenuButtonKey, + child: const Text('button'), + onSelected: (String result) { }, + itemBuilder: (BuildContext context) { + return >[ + const PopupMenuItem( + value: '0', + enabled: false, + child: Text('Item 0'), + ), + const PopupMenuItem( + value: '1', + child: Text('Item 1'), + ), + ]; + }, + ), + ), + ), + ), + ); + + // Show the menu. + await tester.tap(find.byKey(popupMenuButtonKey)); + await tester.pump(const Duration(milliseconds: 300)); + + // Check popup menu padding. + final SingleChildScrollView popupMenu = tester.widget(find.byType(SingleChildScrollView)); + expect(popupMenu.padding, const EdgeInsets.symmetric(vertical: 8.0)); + }); + testWidgets('Material2 - PopupMenuItem default padding', (WidgetTester tester) async { final Key popupMenuButtonKey = UniqueKey(); await tester.pumpWidget( @@ -1709,6 +1747,45 @@ void main() { expect(tester.widget(find.widgetWithText(Container, 'Item 1')).padding, const EdgeInsets.symmetric(horizontal: 16.0)); }); + testWidgets('Material2 - PopupMenuItem default padding', (WidgetTester tester) async { + final Key popupMenuButtonKey = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: false), + home: Scaffold( + body: Center( + child: PopupMenuButton( + key: popupMenuButtonKey, + child: const Text('button'), + onSelected: (String result) { }, + itemBuilder: (BuildContext context) { + return >[ + const PopupMenuItem( + value: '0', + enabled: false, + child: Text('Item 0'), + ), + const PopupMenuItem( + value: '1', + child: Text('Item 1'), + ), + ]; + }, + ), + ), + ), + ), + ); + + // Show the menu. + await tester.tap(find.byKey(popupMenuButtonKey)); + await tester.pump(const Duration(milliseconds: 300)); + + // Check popup menu padding. + final SingleChildScrollView popupMenu = tester.widget(find.byType(SingleChildScrollView)); + expect(popupMenu.padding, const EdgeInsets.symmetric(vertical: 8.0)); + }); + testWidgets('PopupMenuItem custom padding', (WidgetTester tester) async { final Key popupMenuButtonKey = UniqueKey(); final Type menuItemType = const PopupMenuItem(child: Text('item')).runtimeType; diff --git a/packages/flutter/test/material/popup_menu_theme_test.dart b/packages/flutter/test/material/popup_menu_theme_test.dart index 9069b6fd84f..3abc1e77df3 100644 --- a/packages/flutter/test/material/popup_menu_theme_test.dart +++ b/packages/flutter/test/material/popup_menu_theme_test.dart @@ -26,6 +26,7 @@ PopupMenuThemeData _popupMenuThemeM3() { return PopupMenuThemeData( color: Colors.orange, shape: const BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))), + menuPadding: const EdgeInsets.symmetric(vertical: 9.0), elevation: 12.0, shadowColor: const Color(0xff00ff00), surfaceTintColor: const Color(0xff00ff00), @@ -62,6 +63,7 @@ void main() { const PopupMenuThemeData popupMenuTheme = PopupMenuThemeData(); expect(popupMenuTheme.color, null); expect(popupMenuTheme.shape, null); + expect(popupMenuTheme.menuPadding, null); expect(popupMenuTheme.elevation, null); expect(popupMenuTheme.shadowColor, null); expect(popupMenuTheme.surfaceTintColor, null); @@ -88,6 +90,7 @@ void main() { PopupMenuThemeData( color: const Color(0xfffffff1), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))), + menuPadding: const EdgeInsets.symmetric(vertical: 12.0), elevation: 2.0, shadowColor: const Color(0xfffffff2), surfaceTintColor: const Color(0xfffffff3), @@ -113,6 +116,7 @@ void main() { expect(description, [ 'color: Color(0xfffffff1)', 'shape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(2.0))', + 'menuPadding: EdgeInsets(0.0, 12.0, 0.0, 12.0)', 'elevation: 2.0', 'shadowColor: Color(0xfffffff2)', 'surfaceTintColor: Color(0xfffffff3)', @@ -244,6 +248,10 @@ void main() { // Test checked CheckedPopupMenuItem label. listTile = tester.widget(find.byType(ListTile).last); expect(listTile.titleTextStyle?.color, theme.colorScheme.onSurface); + + // Check popup menu padding. + final SingleChildScrollView popupMenu = tester.widget(find.byType(SingleChildScrollView)); + expect(popupMenu.padding, const EdgeInsets.symmetric(vertical: 8.0)); }); testWidgets('Popup menu uses values from PopupMenuThemeData', (WidgetTester tester) async { @@ -360,6 +368,10 @@ void main() { // Test checked CheckedPopupMenuItem label. listTile = tester.widget(find.byType(ListTile).last); expect(listTile.titleTextStyle, popupMenuTheme.labelTextStyle?.resolve(enabled)); + + // Check popup menu padding. + final SingleChildScrollView popupMenu = tester.widget(find.byType(SingleChildScrollView)); + expect(popupMenu.padding, popupMenuTheme.menuPadding); }); testWidgets('Popup menu widget properties take priority over theme', (WidgetTester tester) async { @@ -374,6 +386,7 @@ void main() { const ShapeBorder shape = RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(9.0)), ); + const EdgeInsets menuPadding = EdgeInsets.zero; const double elevation = 7.0; const TextStyle textStyle = TextStyle(color: Color(0xfff14fff), fontSize: 19.0); const MouseCursor cursor = SystemMouseCursors.forbidden; @@ -393,6 +406,7 @@ void main() { surfaceTintColor: surfaceTintColor, color: color, shape: shape, + menuPadding: menuPadding, iconColor: iconColor, iconSize: iconSize, itemBuilder: (BuildContext context) { @@ -460,6 +474,10 @@ void main() { // Test CheckedPopupMenuItem label. final ListTile listTile = tester.widget(find.byType(ListTile).first); expect(listTile.titleTextStyle, textStyle); + + // Check popup menu padding. + final SingleChildScrollView popupMenu = tester.widget(find.byType(SingleChildScrollView)); + expect(popupMenu.padding, EdgeInsets.zero); }); group('Material 2', () { @@ -564,6 +582,10 @@ void main() { RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click, ); + + // Check popup menu padding. + final SingleChildScrollView popupMenu = tester.widget(find.byType(SingleChildScrollView)); + expect(popupMenu.padding, const EdgeInsets.symmetric(vertical: 8.0)); }); testWidgets('Popup menu uses values from PopupMenuThemeData', (WidgetTester tester) async {