diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart index 47b504f53a..2ad823e272 100644 --- a/packages/flutter/lib/src/material/popup_menu.dart +++ b/packages/flutter/lib/src/material/popup_menu.dart @@ -221,6 +221,7 @@ class PopupMenuItem extends PopupMenuEntry { this.value, this.enabled = true, this.height = kMinInteractiveDimension, + this.padding, this.textStyle, this.mouseCursor, required this.child, @@ -243,6 +244,15 @@ class PopupMenuItem extends PopupMenuEntry { @override final double height; + /// The padding of the menu item. + /// + /// Note that [height] may interact with the applied padding. For example, + /// If a [height] greater than the height of the sum of the padding and [child] + /// is provided, then the padding's effect will not be visible. + /// + /// When null, the horizontal padding defaults to 16.0 on both sides. + final EdgeInsets? padding; + /// The text style of the popup menu item. /// /// If this property is null, then [PopupMenuThemeData.textStyle] is used. @@ -327,7 +337,7 @@ class PopupMenuItemState> extends State { child: Container( alignment: AlignmentDirectional.centerStart, constraints: BoxConstraints(minHeight: widget.height), - padding: const EdgeInsets.symmetric(horizontal: _kMenuHorizontalPadding), + padding: widget.padding ?? const EdgeInsets.symmetric(horizontal: _kMenuHorizontalPadding), child: buildChild(), ), ); @@ -435,14 +445,18 @@ class CheckedPopupMenuItem extends PopupMenuItem { T? value, this.checked = false, bool enabled = true, + EdgeInsets? padding, + double height = kMinInteractiveDimension, Widget? child, }) : assert(checked != null), super( - key: key, - value: value, - enabled: enabled, - child: child, - ); + key: key, + value: value, + enabled: enabled, + padding: padding, + height: height, + child: child, + ); /// Whether to display a checkmark next to the menu item. /// diff --git a/packages/flutter/test/material/popup_menu_test.dart b/packages/flutter/test/material/popup_menu_test.dart index 78c3cc4190..584e132d28 100644 --- a/packages/flutter/test/material/popup_menu_test.dart +++ b/packages/flutter/test/material/popup_menu_test.dart @@ -1278,6 +1278,219 @@ void main() { ); }); + testWidgets('PopupMenuItem custom padding', (WidgetTester tester) async { + final Key popupMenuButtonKey = UniqueKey(); + final Type menuItemType = const PopupMenuItem(child: Text('item')).runtimeType; + + 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( + padding: EdgeInsets.zero, + value: '0', + child: Text('Item 0'), + ), + const PopupMenuItem( + padding: EdgeInsets.zero, + height: 0, + value: '0', + child: Text('Item 1'), + ), + const PopupMenuItem( + padding: EdgeInsets.all(20), + value: '0', + child: Text('Item 2'), + ), + const PopupMenuItem( + padding: EdgeInsets.all(20), + height: 100, + value: '0', + child: Text('Item 3'), + ), + ]; + }, + ), + ), + ), + ), + ); + + // Show the menu + await tester.tap(find.byKey(popupMenuButtonKey)); + await tester.pumpAndSettle(); + + // The menu items and their InkWells should have the expected vertical size + // given the interactions between heights and padding. + expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).height, 48); // Minimum interactive height (48) + expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 1')).height, 16); // Height of text (16) + expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 2')).height, 56); // Padding (20.0 + 20.0) + Height of text (16) = 56 + expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 3')).height, 100); // Height value of 100, since child (16) + padding (40) < 100 + + expect(tester.widget(find.widgetWithText(Container, 'Item 0')).padding, EdgeInsets.zero); + expect(tester.widget(find.widgetWithText(Container, 'Item 1')).padding, EdgeInsets.zero); + expect(tester.widget(find.widgetWithText(Container, 'Item 2')).padding, const EdgeInsets.all(20)); + expect(tester.widget(find.widgetWithText(Container, 'Item 3')).padding, const EdgeInsets.all(20)); + }); + + testWidgets('CheckedPopupMenuItem child height is a minimum, child is vertically centered', (WidgetTester tester) async { + final Key popupMenuButtonKey = UniqueKey(); + final Type menuItemType = const CheckedPopupMenuItem(child: Text('item')).runtimeType; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: PopupMenuButton( + key: popupMenuButtonKey, + child: const Text('button'), + onSelected: (String result) { }, + itemBuilder: (BuildContext context) { + return >[ + // This menu item's height will be 56.0 because the default minimum height + // is 48, but the contents of PopupMenuItem are 56.0 tall. + const CheckedPopupMenuItem( + checked: true, + value: '0', + child: Text('Item 0'), + ), + // This menu item's height parameter specifies its minimum height. The + // overall height of the menu item will be 60 because the child's + // height 56, is less than 60. + const CheckedPopupMenuItem( + checked: true, + height: 60, + value: '1', + child: SizedBox( + height: 40, + child: Text('Item 1'), + ), + ), + // This menu item's height parameter specifies its minimum height, so the + // overall height of the menu item will be 75. + const CheckedPopupMenuItem( + checked: true, + height: 75, + value: '2', + child: SizedBox( + child: Text('Item 2'), + ), + ), + // This menu item's height will be 100. + const CheckedPopupMenuItem( + checked: true, + height: 100, + value: '3', + child: SizedBox( + child: Text('Item 3'), + ), + ), + ]; + }, + ), + ), + ), + ), + ); + + // Show the menu + await tester.tap(find.byKey(popupMenuButtonKey)); + await tester.pumpAndSettle(); + + // The menu items and their InkWells should have the expected vertical size + expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).height, 56); + expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 1')).height, 60); + expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 2')).height, 75); + expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 3')).height, 100); + // We evaluate the InkWell at the first index because that is the ListTile's + // InkWell, which wins in the gesture arena over the child's InkWell and + // is the one of interest. + expect(tester.getSize(find.widgetWithText(InkWell, 'Item 0').at(1)).height, 56); + expect(tester.getSize(find.widgetWithText(InkWell, 'Item 1').at(1)).height, 60); + expect(tester.getSize(find.widgetWithText(InkWell, 'Item 2').at(1)).height, 75); + expect(tester.getSize(find.widgetWithText(InkWell, 'Item 3').at(1)).height, 100); + + // Menu item children which whose height is less than the PopupMenuItem + // are vertically centered. + expect( + tester.getRect(find.widgetWithText(menuItemType, 'Item 0')).center.dy, + tester.getRect(find.text('Item 0')).center.dy, + ); + expect( + tester.getRect(find.widgetWithText(menuItemType, 'Item 2')).center.dy, + tester.getRect(find.text('Item 2')).center.dy, + ); + }); + + + testWidgets('CheckedPopupMenuItem custom padding', (WidgetTester tester) async { + final Key popupMenuButtonKey = UniqueKey(); + final Type menuItemType = const CheckedPopupMenuItem(child: Text('item')).runtimeType; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: PopupMenuButton( + key: popupMenuButtonKey, + child: const Text('button'), + onSelected: (String result) { }, + itemBuilder: (BuildContext context) { + return >[ + const CheckedPopupMenuItem( + padding: EdgeInsets.zero, + value: '0', + child: Text('Item 0'), + ), + const CheckedPopupMenuItem( + padding: EdgeInsets.zero, + height: 0, + value: '0', + child: Text('Item 1'), + ), + const CheckedPopupMenuItem( + padding: EdgeInsets.all(20), + value: '0', + child: Text('Item 2'), + ), + const CheckedPopupMenuItem( + padding: EdgeInsets.all(20), + height: 100, + value: '0', + child: Text('Item 3'), + ), + ]; + }, + ), + ), + ), + ), + ); + + // Show the menu + await tester.tap(find.byKey(popupMenuButtonKey)); + await tester.pumpAndSettle(); + + // The menu items and their InkWells should have the expected vertical size + // given the interactions between heights and padding. + expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).height, 56); // Minimum ListTile height (56) + expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 1')).height, 56); // Minimum ListTile height (56) + expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 2')).height, 96); // Padding (20.0 + 20.0) + Height of ListTile (56) = 96 + expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 3')).height, 100); // Height value of 100, since child (56) + padding (40) < 100 + + expect(tester.widget(find.widgetWithText(Container, 'Item 0')).padding, EdgeInsets.zero); + expect(tester.widget(find.widgetWithText(Container, 'Item 1')).padding, EdgeInsets.zero); + expect(tester.widget(find.widgetWithText(Container, 'Item 2')).padding, const EdgeInsets.all(20)); + expect(tester.widget(find.widgetWithText(Container, 'Item 3')).padding, const EdgeInsets.all(20)); + }); + + testWidgets('Update PopupMenuItem layout while the menu is visible', (WidgetTester tester) async { final Key popupMenuButtonKey = UniqueKey(); final Type menuItemType = const PopupMenuItem(child: Text('item')).runtimeType;