Unverified Commit f1d88586 authored by Taha Tesser's avatar Taha Tesser Committed by GitHub

Make popup menu position configurable (#98979)

parent 03c92a99
...@@ -38,6 +38,14 @@ const double _kMenuVerticalPadding = 8.0; ...@@ -38,6 +38,14 @@ const double _kMenuVerticalPadding = 8.0;
const double _kMenuWidthStep = 56.0; const double _kMenuWidthStep = 56.0;
const double _kMenuScreenPadding = 8.0; const double _kMenuScreenPadding = 8.0;
/// Used to configure how the [PopupMenuButton] positions its popup menu.
enum PopupMenuPosition {
/// Menu is positioned over the anchor.
over,
/// Menu is positioned under the anchor.
under,
}
/// A base class for entries in a material design popup menu. /// A base class for entries in a material design popup menu.
/// ///
/// The popup menu widget uses this interface to interact with the menu items. /// The popup menu widget uses this interface to interact with the menu items.
...@@ -977,8 +985,8 @@ class PopupMenuButton<T> extends StatefulWidget { ...@@ -977,8 +985,8 @@ class PopupMenuButton<T> extends StatefulWidget {
this.color, this.color,
this.enableFeedback, this.enableFeedback,
this.constraints, this.constraints,
this.position = PopupMenuPosition.over,
}) : assert(itemBuilder != null), }) : assert(itemBuilder != null),
assert(offset != null),
assert(enabled != null), assert(enabled != null),
assert( assert(
!(child != null && icon != null), !(child != null && icon != null),
...@@ -1033,10 +1041,10 @@ class PopupMenuButton<T> extends StatefulWidget { ...@@ -1033,10 +1041,10 @@ class PopupMenuButton<T> extends StatefulWidget {
/// and the button will behave like an [IconButton]. /// and the button will behave like an [IconButton].
final Widget? icon; final Widget? icon;
/// The offset applied to the Popup Menu Button. /// The offset is applied relative to the initial position
/// set by the [position].
/// ///
/// When not set, the Popup Menu Button will be positioned directly next to /// When not set, the offset defaults to [Offset.zero].
/// the button that was used to create it.
final Offset offset; final Offset offset;
/// Whether this popup menu button is interactive. /// Whether this popup menu button is interactive.
...@@ -1099,6 +1107,15 @@ class PopupMenuButton<T> extends StatefulWidget { ...@@ -1099,6 +1107,15 @@ class PopupMenuButton<T> extends StatefulWidget {
/// the default maximum width. /// the default maximum width.
final BoxConstraints? constraints; final BoxConstraints? constraints;
/// Whether the popup menu is positioned over or under the popup menu button.
///
/// [offset] is used to change the position of the popup menu relative to the
/// position set by this parameter.
///
/// When not set, the position defaults to [PopupMenuPosition.over] which makes the
/// popup menu appear directly over the button that was used to create it.
final PopupMenuPosition position;
@override @override
PopupMenuButtonState<T> createState() => PopupMenuButtonState<T>(); PopupMenuButtonState<T> createState() => PopupMenuButtonState<T>();
} }
...@@ -1120,10 +1137,19 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> { ...@@ -1120,10 +1137,19 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
final RenderBox button = context.findRenderObject()! as RenderBox; final RenderBox button = context.findRenderObject()! as RenderBox;
final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox; final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;
final Offset offset;
switch (widget.position) {
case PopupMenuPosition.over:
offset = widget.offset;
break;
case PopupMenuPosition.under:
offset = Offset(0.0, button.size.height - (widget.padding.vertical / 2)) + widget.offset;
break;
}
final RelativeRect position = RelativeRect.fromRect( final RelativeRect position = RelativeRect.fromRect(
Rect.fromPoints( Rect.fromPoints(
button.localToGlobal(widget.offset, ancestor: overlay), button.localToGlobal(offset, ancestor: overlay),
button.localToGlobal(button.size.bottomRight(Offset.zero) + widget.offset, ancestor: overlay), button.localToGlobal(button.size.bottomRight(Offset.zero) + offset, ancestor: overlay),
), ),
Offset.zero & overlay.size, Offset.zero & overlay.size,
); );
......
...@@ -2569,6 +2569,135 @@ void main() { ...@@ -2569,6 +2569,135 @@ void main() {
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).height, 48); expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).height, 48);
expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).width, 500); expect(tester.getSize(find.widgetWithText(menuItemType, 'Item 0')).width, 500);
}); });
testWidgets('Can change menu position and offset', (WidgetTester tester) async {
PopupMenuButton<int> buildMenuButton({required PopupMenuPosition position}) {
return PopupMenuButton<int>(
position: position,
itemBuilder: (BuildContext context) {
return <PopupMenuItem<int>>[
PopupMenuItem<int>(
value: 1,
child: Builder(
builder: (BuildContext context) {
return const Text('AAA');
},
),
),
];
},
);
}
// Popup menu with `MenuPosition.over (default) with default offset`.
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Material(
child: buildMenuButton(position: PopupMenuPosition.over),
),
),
),
);
// Open the popup menu.
await tester.tap(find.byType(IconButton));
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_PopupMenu<int?>')), const Offset(8.0, 8.0));
// Close the popup menu.
await tester.tapAt(Offset.zero);
await tester.pump();
// Popup menu with `MenuPosition.under`(custom) with default offset`.
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Material(
child: buildMenuButton(position: PopupMenuPosition.under),
),
),
),
);
// Open the popup menu.
await tester.tap(find.byType(IconButton));
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_PopupMenu<int?>')), const Offset(8.0, 40.0));
// Close the popup menu.
await tester.tapAt(Offset.zero);
await tester.pump();
// Popup menu with `MenuPosition.over (default) with custom offset`.
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Material(
child: PopupMenuButton<int>(
offset: const Offset(0.0, 50),
itemBuilder: (BuildContext context) {
return <PopupMenuItem<int>>[
PopupMenuItem<int>(
value: 1,
child: Builder(
builder: (BuildContext context) {
return const Text('AAA');
},
),
),
];
},
),
),
),
),
);
// Open the popup menu.
await tester.tap(find.byType(IconButton));
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_PopupMenu<int?>')), const Offset(8.0, 50.0));
// Close the popup menu.
await tester.tapAt(Offset.zero);
await tester.pump();
// Popup menu with `MenuPosition.under (custom) with custom offset`.
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Material(
child: PopupMenuButton<int>(
offset: const Offset(0.0, 50),
position: PopupMenuPosition.under,
itemBuilder: (BuildContext context) {
return <PopupMenuItem<int>>[
PopupMenuItem<int>(
value: 1,
child: Builder(
builder: (BuildContext context) {
return const Text('AAA');
},
),
),
];
},
),
),
),
),
);
// Open the popup menu.
await tester.tap(find.byType(IconButton));
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_PopupMenu<int?>')), const Offset(8.0, 90.0));
});
} }
class TestApp extends StatefulWidget { class TestApp extends StatefulWidget {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment