Skip to content

Commit 27896a7

Browse files
Adds PopupMenuPosition position to the PopupMenuThemeData (#110268)
1 parent 22cef48 commit 27896a7

File tree

3 files changed

+86
-38
lines changed

3 files changed

+86
-38
lines changed

packages/flutter/lib/src/material/popup_menu.dart

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,6 @@ const double _kMenuVerticalPadding = 8.0;
3838
const double _kMenuWidthStep = 56.0;
3939
const double _kMenuScreenPadding = 8.0;
4040

41-
/// Used to configure how the [PopupMenuButton] positions its popup menu.
42-
enum PopupMenuPosition {
43-
/// Menu is positioned over the anchor.
44-
over,
45-
/// Menu is positioned under the anchor.
46-
under,
47-
}
48-
4941
/// A base class for entries in a Material Design popup menu.
5042
///
5143
/// The popup menu widget uses this interface to interact with the menu items.
@@ -1025,7 +1017,7 @@ class PopupMenuButton<T> extends StatefulWidget {
10251017
this.color,
10261018
this.enableFeedback,
10271019
this.constraints,
1028-
this.position = PopupMenuPosition.over,
1020+
this.position,
10291021
this.clipBehavior = Clip.none,
10301022
}) : assert(itemBuilder != null),
10311023
assert(enabled != null),
@@ -1157,9 +1149,11 @@ class PopupMenuButton<T> extends StatefulWidget {
11571149
/// [offset] is used to change the position of the popup menu relative to the
11581150
/// position set by this parameter.
11591151
///
1160-
/// When not set, the position defaults to [PopupMenuPosition.over] which makes the
1161-
/// popup menu appear directly over the button that was used to create it.
1162-
final PopupMenuPosition position;
1152+
/// If this property is `null`, then [PopupMenuThemeData.position] is used. If
1153+
/// [PopupMenuThemeData.position] is also `null`, then the position defaults
1154+
/// to [PopupMenuPosition.over] which makes the popup menu appear directly
1155+
/// over the button that was used to create it.
1156+
final PopupMenuPosition? position;
11631157

11641158
/// {@macro flutter.material.Material.clipBehavior}
11651159
///
@@ -1189,8 +1183,9 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
11891183
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
11901184
final RenderBox button = context.findRenderObject()! as RenderBox;
11911185
final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;
1186+
final PopupMenuPosition popupMenuPosition = widget.position ?? popupMenuTheme.position ?? PopupMenuPosition.over;
11921187
final Offset offset;
1193-
switch (widget.position) {
1188+
switch (popupMenuPosition) {
11941189
case PopupMenuPosition.over:
11951190
offset = widget.offset;
11961191
break;

packages/flutter/lib/src/material/popup_menu_theme.dart

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ import 'theme.dart';
1313
// Examples can assume:
1414
// late BuildContext context;
1515

16+
/// Used to configure how the [PopupMenuButton] positions its popup menu.
17+
enum PopupMenuPosition {
18+
/// Menu is positioned over the anchor.
19+
over,
20+
/// Menu is positioned under the anchor.
21+
under,
22+
}
23+
1624
/// Defines the visual properties of the routes used to display popup menus
1725
/// as well as [PopupMenuItem] and [PopupMenuDivider] widgets.
1826
///
@@ -43,6 +51,7 @@ class PopupMenuThemeData with Diagnosticable {
4351
this.textStyle,
4452
this.enableFeedback,
4553
this.mouseCursor,
54+
this.position,
4655
});
4756

4857
/// The background color of the popup menu.
@@ -67,6 +76,12 @@ class PopupMenuThemeData with Diagnosticable {
6776
/// If specified, overrides the default value of [PopupMenuItem.mouseCursor].
6877
final MaterialStateProperty<MouseCursor?>? mouseCursor;
6978

79+
/// Whether the popup menu is positioned over or under the popup menu button.
80+
///
81+
/// When not set, the position defaults to [PopupMenuPosition.over] which makes the
82+
/// popup menu appear directly over the button that was used to create it.
83+
final PopupMenuPosition? position;
84+
7085
/// Creates a copy of this object with the given fields replaced with the
7186
/// new values.
7287
PopupMenuThemeData copyWith({
@@ -76,6 +91,7 @@ class PopupMenuThemeData with Diagnosticable {
7691
TextStyle? textStyle,
7792
bool? enableFeedback,
7893
MaterialStateProperty<MouseCursor?>? mouseCursor,
94+
PopupMenuPosition? position,
7995
}) {
8096
return PopupMenuThemeData(
8197
color: color ?? this.color,
@@ -84,6 +100,7 @@ class PopupMenuThemeData with Diagnosticable {
84100
textStyle: textStyle ?? this.textStyle,
85101
enableFeedback: enableFeedback ?? this.enableFeedback,
86102
mouseCursor: mouseCursor ?? this.mouseCursor,
103+
position: position ?? this.position,
87104
);
88105
}
89106

@@ -104,6 +121,7 @@ class PopupMenuThemeData with Diagnosticable {
104121
textStyle: TextStyle.lerp(a?.textStyle, b?.textStyle, t),
105122
enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback,
106123
mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor,
124+
position: t < 0.5 ? a?.position : b?.position,
107125
);
108126
}
109127

@@ -115,6 +133,7 @@ class PopupMenuThemeData with Diagnosticable {
115133
textStyle,
116134
enableFeedback,
117135
mouseCursor,
136+
position,
118137
);
119138

120139
@override
@@ -131,7 +150,8 @@ class PopupMenuThemeData with Diagnosticable {
131150
&& other.shape == shape
132151
&& other.textStyle == textStyle
133152
&& other.enableFeedback == enableFeedback
134-
&& other.mouseCursor == mouseCursor;
153+
&& other.mouseCursor == mouseCursor
154+
&& other.position == position;
135155
}
136156

137157
@override
@@ -143,6 +163,7 @@ class PopupMenuThemeData with Diagnosticable {
143163
properties.add(DiagnosticsProperty<TextStyle>('text style', textStyle, defaultValue: null));
144164
properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null));
145165
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: null));
166+
properties.add(EnumProperty<PopupMenuPosition?>('position', position, defaultValue: null));
146167
}
147168
}
148169

packages/flutter/test/material/popup_menu_theme_test.dart

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ PopupMenuThemeData _popupMenuTheme() {
1313
shape: BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
1414
elevation: 12.0,
1515
textStyle: TextStyle(color: Color(0xffffffff), textBaseline: TextBaseline.alphabetic),
16+
position: PopupMenuPosition.under,
1617
);
1718
}
1819

@@ -29,6 +30,7 @@ void main() {
2930
expect(popupMenuTheme.elevation, null);
3031
expect(popupMenuTheme.textStyle, null);
3132
expect(popupMenuTheme.mouseCursor, null);
33+
expect(popupMenuTheme.position, null);
3234
});
3335

3436
testWidgets('Default PopupMenuThemeData debugFillProperties', (WidgetTester tester) async {
@@ -51,6 +53,7 @@ void main() {
5153
elevation: 2.0,
5254
textStyle: TextStyle(color: Color(0xffffffff)),
5355
mouseCursor: MaterialStateMouseCursor.clickable,
56+
position: PopupMenuPosition.over,
5457
).debugFillProperties(builder);
5558

5659
final List<String> description = builder.properties
@@ -64,6 +67,7 @@ void main() {
6467
'elevation: 2.0',
6568
'text style: TextStyle(inherit: true, color: Color(0xffffffff))',
6669
'mouseCursor: MaterialStateMouseCursor(clickable)',
70+
'position: over'
6771
]);
6872
});
6973

@@ -78,16 +82,21 @@ void main() {
7882
home: Material(
7983
child: Column(
8084
children: <Widget>[
81-
PopupMenuButton<void>(
82-
key: popupButtonKey,
83-
itemBuilder: (BuildContext context) {
84-
return <PopupMenuEntry<void>>[
85-
PopupMenuItem<void>(
86-
key: popupItemKey,
87-
child: const Text('Example'),
88-
),
89-
];
90-
},
85+
Padding(
86+
// The padding makes sure the menu as enough space to around to
87+
// get properly aligned when displayed (`_kMenuScreenPadding`).
88+
padding: const EdgeInsets.all(8.0),
89+
child: PopupMenuButton<void>(
90+
key: popupButtonKey,
91+
itemBuilder: (BuildContext context) {
92+
return <PopupMenuEntry<void>>[
93+
PopupMenuItem<void>(
94+
key: popupItemKey,
95+
child: const Text('Example'),
96+
),
97+
];
98+
},
99+
),
91100
),
92101
],
93102
),
@@ -123,6 +132,11 @@ void main() {
123132
);
124133
expect(text.style.fontFamily, 'Roboto');
125134
expect(text.style.color, const Color(0xdd000000));
135+
expect(text.style.color, const Color(0xdd000000));
136+
137+
final Offset topLeftButton = tester.getTopLeft(find.byType(PopupMenuButton<void>));
138+
final Offset topLeftMenu = tester.getTopLeft(find.byWidget(button));
139+
expect(topLeftMenu, topLeftButton);
126140
});
127141

128142
testWidgets('Popup menu uses values from PopupMenuThemeData', (WidgetTester tester) async {
@@ -138,6 +152,10 @@ void main() {
138152
child: Column(
139153
children: <Widget>[
140154
PopupMenuButton<void>(
155+
// The padding is used in the positioning of the menu when the
156+
// position is `PopupMenuPosition.under`. Setting it to zero makes
157+
// it easier to test.
158+
padding: EdgeInsets.zero,
141159
key: popupButtonKey,
142160
itemBuilder: (BuildContext context) {
143161
return <PopupMenuEntry<Object>>[
@@ -181,6 +199,10 @@ void main() {
181199
).last,
182200
);
183201
expect(text.style, popupMenuTheme.textStyle);
202+
203+
final Offset bottomLeftButton = tester.getBottomLeft(find.byType(PopupMenuButton<void>));
204+
final Offset topLeftMenu = tester.getTopLeft(find.byWidget(button));
205+
expect(topLeftMenu, bottomLeftButton);
184206
});
185207

186208
testWidgets('Popup menu widget properties take priority over theme', (WidgetTester tester) async {
@@ -202,20 +224,26 @@ void main() {
202224
home: Material(
203225
child: Column(
204226
children: <Widget>[
205-
PopupMenuButton<void>(
206-
key: popupButtonKey,
207-
elevation: elevation,
208-
color: color,
209-
shape: shape,
210-
itemBuilder: (BuildContext context) {
211-
return <PopupMenuEntry<void>>[
212-
PopupMenuItem<void>(
213-
key: popupItemKey,
214-
textStyle: textStyle,
215-
child: const Text('Example'),
216-
),
217-
];
218-
},
227+
Padding(
228+
// The padding makes sure the menu as enough space to around to
229+
// get properly aligned when displayed (`_kMenuScreenPadding`).
230+
padding: const EdgeInsets.all(8.0),
231+
child: PopupMenuButton<void>(
232+
key: popupButtonKey,
233+
elevation: elevation,
234+
color: color,
235+
shape: shape,
236+
position: PopupMenuPosition.over,
237+
itemBuilder: (BuildContext context) {
238+
return <PopupMenuEntry<void>>[
239+
PopupMenuItem<void>(
240+
key: popupItemKey,
241+
textStyle: textStyle,
242+
child: const Text('Example'),
243+
),
244+
];
245+
},
246+
),
219247
),
220248
],
221249
),
@@ -250,6 +278,10 @@ void main() {
250278
).last,
251279
);
252280
expect(text.style, textStyle);
281+
282+
final Offset topLeftButton = tester.getTopLeft(find.byType(PopupMenuButton<void>));
283+
final Offset topLeftMenu = tester.getTopLeft(find.byWidget(button));
284+
expect(topLeftMenu, topLeftButton);
253285
});
254286

255287
testWidgets('ThemeData.popupMenuTheme properties are utilized', (WidgetTester tester) async {

0 commit comments

Comments
 (0)