Unverified Commit 27896a7b authored by Valentin Vignal's avatar Valentin Vignal Committed by GitHub

Adds `PopupMenuPosition position` to the `PopupMenuThemeData` (#110268)

parent 22cef489
...@@ -38,14 +38,6 @@ const double _kMenuVerticalPadding = 8.0; ...@@ -38,14 +38,6 @@ 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.
...@@ -1025,7 +1017,7 @@ class PopupMenuButton<T> extends StatefulWidget { ...@@ -1025,7 +1017,7 @@ class PopupMenuButton<T> extends StatefulWidget {
this.color, this.color,
this.enableFeedback, this.enableFeedback,
this.constraints, this.constraints,
this.position = PopupMenuPosition.over, this.position,
this.clipBehavior = Clip.none, this.clipBehavior = Clip.none,
}) : assert(itemBuilder != null), }) : assert(itemBuilder != null),
assert(enabled != null), assert(enabled != null),
...@@ -1157,9 +1149,11 @@ class PopupMenuButton<T> extends StatefulWidget { ...@@ -1157,9 +1149,11 @@ class PopupMenuButton<T> extends StatefulWidget {
/// [offset] is used to change the position of the popup menu relative to the /// [offset] is used to change the position of the popup menu relative to the
/// position set by this parameter. /// position set by this parameter.
/// ///
/// When not set, the position defaults to [PopupMenuPosition.over] which makes the /// If this property is `null`, then [PopupMenuThemeData.position] is used. If
/// popup menu appear directly over the button that was used to create it. /// [PopupMenuThemeData.position] is also `null`, then the position defaults
final PopupMenuPosition position; /// to [PopupMenuPosition.over] which makes the popup menu appear directly
/// over the button that was used to create it.
final PopupMenuPosition? position;
/// {@macro flutter.material.Material.clipBehavior} /// {@macro flutter.material.Material.clipBehavior}
/// ///
...@@ -1189,8 +1183,9 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> { ...@@ -1189,8 +1183,9 @@ 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 PopupMenuPosition popupMenuPosition = widget.position ?? popupMenuTheme.position ?? PopupMenuPosition.over;
final Offset offset; final Offset offset;
switch (widget.position) { switch (popupMenuPosition) {
case PopupMenuPosition.over: case PopupMenuPosition.over:
offset = widget.offset; offset = widget.offset;
break; break;
......
...@@ -13,6 +13,14 @@ import 'theme.dart'; ...@@ -13,6 +13,14 @@ import 'theme.dart';
// Examples can assume: // Examples can assume:
// late BuildContext context; // late BuildContext context;
/// 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,
}
/// Defines the visual properties of the routes used to display popup menus /// Defines the visual properties of the routes used to display popup menus
/// as well as [PopupMenuItem] and [PopupMenuDivider] widgets. /// as well as [PopupMenuItem] and [PopupMenuDivider] widgets.
/// ///
...@@ -43,6 +51,7 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -43,6 +51,7 @@ class PopupMenuThemeData with Diagnosticable {
this.textStyle, this.textStyle,
this.enableFeedback, this.enableFeedback,
this.mouseCursor, this.mouseCursor,
this.position,
}); });
/// The background color of the popup menu. /// The background color of the popup menu.
...@@ -67,6 +76,12 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -67,6 +76,12 @@ class PopupMenuThemeData with Diagnosticable {
/// If specified, overrides the default value of [PopupMenuItem.mouseCursor]. /// If specified, overrides the default value of [PopupMenuItem.mouseCursor].
final MaterialStateProperty<MouseCursor?>? mouseCursor; final MaterialStateProperty<MouseCursor?>? mouseCursor;
/// Whether the popup menu is positioned over or under the popup menu button.
///
/// 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;
/// Creates a copy of this object with the given fields replaced with the /// Creates a copy of this object with the given fields replaced with the
/// new values. /// new values.
PopupMenuThemeData copyWith({ PopupMenuThemeData copyWith({
...@@ -76,6 +91,7 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -76,6 +91,7 @@ class PopupMenuThemeData with Diagnosticable {
TextStyle? textStyle, TextStyle? textStyle,
bool? enableFeedback, bool? enableFeedback,
MaterialStateProperty<MouseCursor?>? mouseCursor, MaterialStateProperty<MouseCursor?>? mouseCursor,
PopupMenuPosition? position,
}) { }) {
return PopupMenuThemeData( return PopupMenuThemeData(
color: color ?? this.color, color: color ?? this.color,
...@@ -84,6 +100,7 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -84,6 +100,7 @@ class PopupMenuThemeData with Diagnosticable {
textStyle: textStyle ?? this.textStyle, textStyle: textStyle ?? this.textStyle,
enableFeedback: enableFeedback ?? this.enableFeedback, enableFeedback: enableFeedback ?? this.enableFeedback,
mouseCursor: mouseCursor ?? this.mouseCursor, mouseCursor: mouseCursor ?? this.mouseCursor,
position: position ?? this.position,
); );
} }
...@@ -104,6 +121,7 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -104,6 +121,7 @@ class PopupMenuThemeData with Diagnosticable {
textStyle: TextStyle.lerp(a?.textStyle, b?.textStyle, t), textStyle: TextStyle.lerp(a?.textStyle, b?.textStyle, t),
enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback, enableFeedback: t < 0.5 ? a?.enableFeedback : b?.enableFeedback,
mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor, mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor,
position: t < 0.5 ? a?.position : b?.position,
); );
} }
...@@ -115,6 +133,7 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -115,6 +133,7 @@ class PopupMenuThemeData with Diagnosticable {
textStyle, textStyle,
enableFeedback, enableFeedback,
mouseCursor, mouseCursor,
position,
); );
@override @override
...@@ -131,7 +150,8 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -131,7 +150,8 @@ class PopupMenuThemeData with Diagnosticable {
&& other.shape == shape && other.shape == shape
&& other.textStyle == textStyle && other.textStyle == textStyle
&& other.enableFeedback == enableFeedback && other.enableFeedback == enableFeedback
&& other.mouseCursor == mouseCursor; && other.mouseCursor == mouseCursor
&& other.position == position;
} }
@override @override
...@@ -143,6 +163,7 @@ class PopupMenuThemeData with Diagnosticable { ...@@ -143,6 +163,7 @@ class PopupMenuThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<TextStyle>('text style', textStyle, defaultValue: null)); properties.add(DiagnosticsProperty<TextStyle>('text style', textStyle, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null)); properties.add(DiagnosticsProperty<bool>('enableFeedback', enableFeedback, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: null)); properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: null));
properties.add(EnumProperty<PopupMenuPosition?>('position', position, defaultValue: null));
} }
} }
......
...@@ -13,6 +13,7 @@ PopupMenuThemeData _popupMenuTheme() { ...@@ -13,6 +13,7 @@ PopupMenuThemeData _popupMenuTheme() {
shape: BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))), shape: BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
elevation: 12.0, elevation: 12.0,
textStyle: TextStyle(color: Color(0xffffffff), textBaseline: TextBaseline.alphabetic), textStyle: TextStyle(color: Color(0xffffffff), textBaseline: TextBaseline.alphabetic),
position: PopupMenuPosition.under,
); );
} }
...@@ -29,6 +30,7 @@ void main() { ...@@ -29,6 +30,7 @@ void main() {
expect(popupMenuTheme.elevation, null); expect(popupMenuTheme.elevation, null);
expect(popupMenuTheme.textStyle, null); expect(popupMenuTheme.textStyle, null);
expect(popupMenuTheme.mouseCursor, null); expect(popupMenuTheme.mouseCursor, null);
expect(popupMenuTheme.position, null);
}); });
testWidgets('Default PopupMenuThemeData debugFillProperties', (WidgetTester tester) async { testWidgets('Default PopupMenuThemeData debugFillProperties', (WidgetTester tester) async {
...@@ -51,6 +53,7 @@ void main() { ...@@ -51,6 +53,7 @@ void main() {
elevation: 2.0, elevation: 2.0,
textStyle: TextStyle(color: Color(0xffffffff)), textStyle: TextStyle(color: Color(0xffffffff)),
mouseCursor: MaterialStateMouseCursor.clickable, mouseCursor: MaterialStateMouseCursor.clickable,
position: PopupMenuPosition.over,
).debugFillProperties(builder); ).debugFillProperties(builder);
final List<String> description = builder.properties final List<String> description = builder.properties
...@@ -64,6 +67,7 @@ void main() { ...@@ -64,6 +67,7 @@ void main() {
'elevation: 2.0', 'elevation: 2.0',
'text style: TextStyle(inherit: true, color: Color(0xffffffff))', 'text style: TextStyle(inherit: true, color: Color(0xffffffff))',
'mouseCursor: MaterialStateMouseCursor(clickable)', 'mouseCursor: MaterialStateMouseCursor(clickable)',
'position: over'
]); ]);
}); });
...@@ -78,16 +82,21 @@ void main() { ...@@ -78,16 +82,21 @@ void main() {
home: Material( home: Material(
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
PopupMenuButton<void>( Padding(
key: popupButtonKey, // The padding makes sure the menu as enough space to around to
itemBuilder: (BuildContext context) { // get properly aligned when displayed (`_kMenuScreenPadding`).
return <PopupMenuEntry<void>>[ padding: const EdgeInsets.all(8.0),
PopupMenuItem<void>( child: PopupMenuButton<void>(
key: popupItemKey, key: popupButtonKey,
child: const Text('Example'), itemBuilder: (BuildContext context) {
), return <PopupMenuEntry<void>>[
]; PopupMenuItem<void>(
}, key: popupItemKey,
child: const Text('Example'),
),
];
},
),
), ),
], ],
), ),
...@@ -123,6 +132,11 @@ void main() { ...@@ -123,6 +132,11 @@ void main() {
); );
expect(text.style.fontFamily, 'Roboto'); expect(text.style.fontFamily, 'Roboto');
expect(text.style.color, const Color(0xdd000000)); expect(text.style.color, const Color(0xdd000000));
expect(text.style.color, const Color(0xdd000000));
final Offset topLeftButton = tester.getTopLeft(find.byType(PopupMenuButton<void>));
final Offset topLeftMenu = tester.getTopLeft(find.byWidget(button));
expect(topLeftMenu, topLeftButton);
}); });
testWidgets('Popup menu uses values from PopupMenuThemeData', (WidgetTester tester) async { testWidgets('Popup menu uses values from PopupMenuThemeData', (WidgetTester tester) async {
...@@ -138,6 +152,10 @@ void main() { ...@@ -138,6 +152,10 @@ void main() {
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
PopupMenuButton<void>( PopupMenuButton<void>(
// The padding is used in the positioning of the menu when the
// position is `PopupMenuPosition.under`. Setting it to zero makes
// it easier to test.
padding: EdgeInsets.zero,
key: popupButtonKey, key: popupButtonKey,
itemBuilder: (BuildContext context) { itemBuilder: (BuildContext context) {
return <PopupMenuEntry<Object>>[ return <PopupMenuEntry<Object>>[
...@@ -181,6 +199,10 @@ void main() { ...@@ -181,6 +199,10 @@ void main() {
).last, ).last,
); );
expect(text.style, popupMenuTheme.textStyle); expect(text.style, popupMenuTheme.textStyle);
final Offset bottomLeftButton = tester.getBottomLeft(find.byType(PopupMenuButton<void>));
final Offset topLeftMenu = tester.getTopLeft(find.byWidget(button));
expect(topLeftMenu, bottomLeftButton);
}); });
testWidgets('Popup menu widget properties take priority over theme', (WidgetTester tester) async { testWidgets('Popup menu widget properties take priority over theme', (WidgetTester tester) async {
...@@ -202,20 +224,26 @@ void main() { ...@@ -202,20 +224,26 @@ void main() {
home: Material( home: Material(
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
PopupMenuButton<void>( Padding(
key: popupButtonKey, // The padding makes sure the menu as enough space to around to
elevation: elevation, // get properly aligned when displayed (`_kMenuScreenPadding`).
color: color, padding: const EdgeInsets.all(8.0),
shape: shape, child: PopupMenuButton<void>(
itemBuilder: (BuildContext context) { key: popupButtonKey,
return <PopupMenuEntry<void>>[ elevation: elevation,
PopupMenuItem<void>( color: color,
key: popupItemKey, shape: shape,
textStyle: textStyle, position: PopupMenuPosition.over,
child: const Text('Example'), itemBuilder: (BuildContext context) {
), return <PopupMenuEntry<void>>[
]; PopupMenuItem<void>(
}, key: popupItemKey,
textStyle: textStyle,
child: const Text('Example'),
),
];
},
),
), ),
], ],
), ),
...@@ -250,6 +278,10 @@ void main() { ...@@ -250,6 +278,10 @@ void main() {
).last, ).last,
); );
expect(text.style, textStyle); expect(text.style, textStyle);
final Offset topLeftButton = tester.getTopLeft(find.byType(PopupMenuButton<void>));
final Offset topLeftMenu = tester.getTopLeft(find.byWidget(button));
expect(topLeftMenu, topLeftButton);
}); });
testWidgets('ThemeData.popupMenuTheme properties are utilized', (WidgetTester tester) async { testWidgets('ThemeData.popupMenuTheme properties are utilized', (WidgetTester tester) async {
......
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