Unverified Commit 12016e41 authored by xubaolin's avatar xubaolin Committed by GitHub

Reland "Update PopupMenuButton to match Material Design spec" (#74620)

parent 76be5581
...@@ -409,8 +409,14 @@ void main() { ...@@ -409,8 +409,14 @@ void main() {
}); });
test('Popup Menu has correct Android semantics', () async { test('Popup Menu has correct Android semantics', () async {
final SerializableFinder button = find.descendant(
of: find.byValueKey(popupButtonKeyValue),
matching: find.byType('Semantics'),
firstMatchOnly: true,
);
expect( expect(
await getSemantics(find.byValueKey(popupButtonKeyValue)), await getSemantics(button),
hasAndroidSemantics( hasAndroidSemantics(
className: AndroidClassName.button, className: AndroidClassName.button,
isChecked: false, isChecked: false,
...@@ -432,20 +438,21 @@ void main() { ...@@ -432,20 +438,21 @@ void main() {
for (final String item in popupItems) { for (final String item in popupItems) {
expect( expect(
await getSemantics(find.byValueKey('$popupKeyValue.$item')), await getSemantics(find.byValueKey('$popupKeyValue.$item')),
hasAndroidSemantics( hasAndroidSemantics(
className: AndroidClassName.button, className: AndroidClassName.button,
isChecked: false, isChecked: false,
isCheckable: false, isCheckable: false,
isEnabled: true, isEnabled: true,
isFocusable: true, isFocusable: true,
actions: <AndroidSemanticsAction>[ actions: <AndroidSemanticsAction>[
if (item == popupItems.first) AndroidSemanticsAction.clearAccessibilityFocus, if (item == popupItems.first) AndroidSemanticsAction.clearAccessibilityFocus,
if (item != popupItems.first) AndroidSemanticsAction.accessibilityFocus, if (item != popupItems.first) AndroidSemanticsAction.accessibilityFocus,
AndroidSemanticsAction.click, AndroidSemanticsAction.click,
], ],
), ),
reason: "Popup $item doesn't have the right semantics"); reason: "Popup $item doesn't have the right semantics",
);
} }
await driver.tap(find.byValueKey('$popupKeyValue.${popupItems.first}')); await driver.tap(find.byValueKey('$popupKeyValue.${popupItems.first}'));
......
...@@ -599,7 +599,15 @@ class _PopupMenu<T> extends StatelessWidget { ...@@ -599,7 +599,15 @@ class _PopupMenu<T> extends StatelessWidget {
// Positioning of the menu on the screen. // Positioning of the menu on the screen.
class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
_PopupMenuRouteLayout(this.position, this.itemSizes, this.selectedItemIndex, this.textDirection); _PopupMenuRouteLayout(
this.position,
this.itemSizes,
this.selectedItemIndex,
this.textDirection,
this.topPadding,
this.bottomPadding,
this.placement,
);
// Rectangle of underlying button, relative to the overlay's dimensions. // Rectangle of underlying button, relative to the overlay's dimensions.
final RelativeRect position; final RelativeRect position;
...@@ -615,6 +623,15 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { ...@@ -615,6 +623,15 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
// Whether to prefer going to the left or to the right. // Whether to prefer going to the left or to the right.
final TextDirection textDirection; final TextDirection textDirection;
// Top padding of unsafe area.
final double topPadding;
// Bottom padding of unsafe area.
final double bottomPadding;
// The placement of the menu.
final PopupMenuPlacement placement;
// We put the child wherever position specifies, so long as it will fit within // We put the child wherever position specifies, so long as it will fit within
// the specified parent size padded (inset) by 8. If necessary, we adjust the // the specified parent size padded (inset) by 8. If necessary, we adjust the
// child's position so that it fits. // child's position so that it fits.
...@@ -624,7 +641,8 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { ...@@ -624,7 +641,8 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
// The menu can be at most the size of the overlay minus 8.0 pixels in each // The menu can be at most the size of the overlay minus 8.0 pixels in each
// direction. // direction.
return BoxConstraints.loose( return BoxConstraints.loose(
constraints.biggest - const Offset(_kMenuScreenPadding * 2.0, _kMenuScreenPadding * 2.0) as Size, constraints.biggest - Offset(_kMenuScreenPadding * 2.0,
_kMenuScreenPadding * 2.0 + topPadding + bottomPadding) as Size,
); );
} }
...@@ -634,14 +652,23 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { ...@@ -634,14 +652,23 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
// childSize: The size of the menu, when fully open, as determined by // childSize: The size of the menu, when fully open, as determined by
// getConstraintsForChild. // getConstraintsForChild.
final double buttonHeight = size.height - position.top - position.bottom;
// Find the ideal vertical position. // Find the ideal vertical position.
double y = position.top; // Default vertical position is below the element that generates it.
double y = placement == PopupMenuPlacement.belowButton
? position.top + buttonHeight
: position.top;
if (selectedItemIndex != null && itemSizes != null) { if (selectedItemIndex != null && itemSizes != null) {
double selectedItemOffset = _kMenuVerticalPadding; double selectedItemOffset = _kMenuVerticalPadding;
for (int index = 0; index < selectedItemIndex!; index += 1) for (int index = 0; index < selectedItemIndex!; index += 1)
selectedItemOffset += itemSizes[index]!.height; selectedItemOffset += itemSizes[index]!.height;
selectedItemOffset += itemSizes[selectedItemIndex!]!.height / 2; selectedItemOffset += itemSizes[selectedItemIndex!]!.height / 2;
y = position.top + (size.height - position.top - position.bottom) / 2.0 - selectedItemOffset; if (placement == PopupMenuPlacement.belowButton) {
y = y - buttonHeight / 2.0 - selectedItemOffset;
} else {
y = y + buttonHeight / 2.0 - selectedItemOffset;
}
} }
// Find the ideal horizontal position. // Find the ideal horizontal position.
...@@ -671,10 +698,10 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { ...@@ -671,10 +698,10 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
x = _kMenuScreenPadding; x = _kMenuScreenPadding;
else if (x + childSize.width > size.width - _kMenuScreenPadding) else if (x + childSize.width > size.width - _kMenuScreenPadding)
x = size.width - childSize.width - _kMenuScreenPadding; x = size.width - childSize.width - _kMenuScreenPadding;
if (y < _kMenuScreenPadding) if (y < _kMenuScreenPadding + topPadding)
y = _kMenuScreenPadding; y = _kMenuScreenPadding + topPadding;
else if (y + childSize.height > size.height - _kMenuScreenPadding) else if (y + childSize.height > size.height - _kMenuScreenPadding)
y = size.height - childSize.height - _kMenuScreenPadding; y = size.height - bottomPadding - _kMenuScreenPadding - childSize.height ;
return Offset(x, y); return Offset(x, y);
} }
...@@ -686,9 +713,9 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { ...@@ -686,9 +713,9 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
assert(itemSizes.length == oldDelegate.itemSizes.length); assert(itemSizes.length == oldDelegate.itemSizes.length);
return position != oldDelegate.position return position != oldDelegate.position
|| selectedItemIndex != oldDelegate.selectedItemIndex || selectedItemIndex != oldDelegate.selectedItemIndex
|| textDirection != oldDelegate.textDirection || textDirection != oldDelegate.textDirection
|| !listEquals(itemSizes, oldDelegate.itemSizes); || !listEquals(itemSizes, oldDelegate.itemSizes);
} }
} }
...@@ -703,6 +730,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> { ...@@ -703,6 +730,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
this.shape, this.shape,
this.color, this.color,
required this.capturedThemes, required this.capturedThemes,
required this.placement,
}) : itemSizes = List<Size?>.filled(items.length, null); }) : itemSizes = List<Size?>.filled(items.length, null);
final RelativeRect position; final RelativeRect position;
...@@ -714,6 +742,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> { ...@@ -714,6 +742,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
final ShapeBorder? shape; final ShapeBorder? shape;
final Color? color; final Color? color;
final CapturedThemes capturedThemes; final CapturedThemes capturedThemes;
final PopupMenuPlacement placement;
@override @override
Animation<double> createAnimation() { Animation<double> createAnimation() {
...@@ -749,20 +778,22 @@ class _PopupMenuRoute<T> extends PopupRoute<T> { ...@@ -749,20 +778,22 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
final Widget menu = _PopupMenu<T>(route: this, semanticLabel: semanticLabel); final Widget menu = _PopupMenu<T>(route: this, semanticLabel: semanticLabel);
return SafeArea( return Builder(
child: Builder( builder: (BuildContext context) {
builder: (BuildContext context) { final MediaQueryData mediaQuery = MediaQuery.of(context);
return CustomSingleChildLayout( return CustomSingleChildLayout(
delegate: _PopupMenuRouteLayout( delegate: _PopupMenuRouteLayout(
position, position,
itemSizes, itemSizes,
selectedItemIndex, selectedItemIndex,
Directionality.of(context), Directionality.of(context),
), mediaQuery.padding.top,
child: capturedThemes.wrap(menu), mediaQuery.padding.bottom,
); placement,
}, ),
), child: capturedThemes.wrap(menu),
);
},
); );
} }
} }
...@@ -832,6 +863,7 @@ Future<T?> showMenu<T>({ ...@@ -832,6 +863,7 @@ Future<T?> showMenu<T>({
ShapeBorder? shape, ShapeBorder? shape,
Color? color, Color? color,
bool useRootNavigator = false, bool useRootNavigator = false,
PopupMenuPlacement placement = PopupMenuPlacement.aboveButton,
}) { }) {
assert(context != null); assert(context != null);
assert(position != null); assert(position != null);
...@@ -861,6 +893,7 @@ Future<T?> showMenu<T>({ ...@@ -861,6 +893,7 @@ Future<T?> showMenu<T>({
shape: shape, shape: shape,
color: color, color: color,
capturedThemes: InheritedTheme.capture(from: context, to: navigator.context), capturedThemes: InheritedTheme.capture(from: context, to: navigator.context),
placement: placement,
)); ));
} }
...@@ -883,6 +916,17 @@ typedef PopupMenuCanceled = void Function(); ...@@ -883,6 +916,17 @@ typedef PopupMenuCanceled = void Function();
/// Used by [PopupMenuButton.itemBuilder]. /// Used by [PopupMenuButton.itemBuilder].
typedef PopupMenuItemBuilder<T> = List<PopupMenuEntry<T>> Function(BuildContext context); typedef PopupMenuItemBuilder<T> = List<PopupMenuEntry<T>> Function(BuildContext context);
/// The placement of the menu popped up by press the [PopupMenuButton].
///
/// Used by [PopupMenuButton.placement].
enum PopupMenuPlacement {
/// The popup menu is positioned above the button that generates it.
aboveButton,
/// The popup menu is positioned below the button that generates it.
belowButton,
}
/// Displays a menu when pressed and calls [onSelected] when the menu is dismissed /// Displays a menu when pressed and calls [onSelected] when the menu is dismissed
/// because an item was selected. The value passed to [onSelected] is the value of /// because an item was selected. The value passed to [onSelected] is the value of
/// the selected menu item. /// the selected menu item.
...@@ -955,6 +999,7 @@ class PopupMenuButton<T> extends StatefulWidget { ...@@ -955,6 +999,7 @@ class PopupMenuButton<T> extends StatefulWidget {
this.shape, this.shape,
this.color, this.color,
this.enableFeedback, this.enableFeedback,
this.placement = PopupMenuPlacement.aboveButton,
}) : assert(itemBuilder != null), }) : assert(itemBuilder != null),
assert(offset != null), assert(offset != null),
assert(enabled != null), assert(enabled != null),
...@@ -1006,7 +1051,7 @@ class PopupMenuButton<T> extends StatefulWidget { ...@@ -1006,7 +1051,7 @@ class PopupMenuButton<T> extends StatefulWidget {
/// The offset applied to the Popup Menu Button. /// The offset applied to the Popup Menu Button.
/// ///
/// When not set, the Popup Menu Button will be positioned directly next to /// When not set, the Popup Menu Button will be positioned directly below
/// the button that was used to create it. /// the button that was used to create it.
final Offset offset; final Offset offset;
...@@ -1049,6 +1094,11 @@ class PopupMenuButton<T> extends StatefulWidget { ...@@ -1049,6 +1094,11 @@ class PopupMenuButton<T> extends StatefulWidget {
/// * [Feedback] for providing platform-specific feedback to certain actions. /// * [Feedback] for providing platform-specific feedback to certain actions.
final bool? enableFeedback; final bool? enableFeedback;
/// The placement of the menu popped up by press the [PopupMenuButton].
///
/// Default to [PopupMenuPlacement.aboveButton].
final PopupMenuPlacement placement;
/// If provided, the size of the [Icon]. /// If provided, the size of the [Icon].
/// ///
/// If this property is null, the default size is 24.0 pixels. /// If this property is null, the default size is 24.0 pixels.
...@@ -1063,8 +1113,9 @@ class PopupMenuButton<T> extends StatefulWidget { ...@@ -1063,8 +1113,9 @@ class PopupMenuButton<T> extends StatefulWidget {
/// See [showButtonMenu] for a way to programmatically open the popup menu /// See [showButtonMenu] for a way to programmatically open the popup menu
/// of your button state. /// of your button state.
class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> { class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
final GlobalKey _menuButtonKey = GlobalKey();
/// A method to show a popup menu with the items supplied to /// A method to show a popup menu with the items supplied to
/// [PopupMenuButton.itemBuilder] at the position of your [PopupMenuButton]. /// [PopupMenuButton.itemBuilder] at the position below your [PopupMenuButton].
/// ///
/// By default, it is called when the user taps the button and [PopupMenuButton.enabled] /// By default, it is called when the user taps the button and [PopupMenuButton.enabled]
/// is set to `true`. Moreover, you can open the button by calling the method manually. /// is set to `true`. Moreover, you can open the button by calling the method manually.
...@@ -1073,8 +1124,14 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> { ...@@ -1073,8 +1124,14 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
/// show the menu of the button with `globalKey.currentState.showButtonMenu`. /// show the menu of the button with `globalKey.currentState.showButtonMenu`.
void showButtonMenu() { void showButtonMenu() {
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
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 RenderBox button;
if (widget.placement == PopupMenuPlacement.belowButton) {
button = _menuButtonKey.currentContext!.findRenderObject()! as RenderBox;
} else {
// Backward compatible.
button = context.findRenderObject()! as RenderBox;
}
final RelativeRect position = RelativeRect.fromRect( final RelativeRect position = RelativeRect.fromRect(
Rect.fromPoints( Rect.fromPoints(
button.localToGlobal(widget.offset, ancestor: overlay), button.localToGlobal(widget.offset, ancestor: overlay),
...@@ -1093,6 +1150,7 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> { ...@@ -1093,6 +1150,7 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
position: position, position: position,
shape: widget.shape ?? popupMenuTheme.shape, shape: widget.shape ?? popupMenuTheme.shape,
color: widget.color ?? popupMenuTheme.color, color: widget.color ?? popupMenuTheme.color,
placement: widget.placement,
) )
.then<void>((T? newValue) { .then<void>((T? newValue) {
if (!mounted) if (!mounted)
...@@ -1132,18 +1190,40 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> { ...@@ -1132,18 +1190,40 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
child: InkWell( child: InkWell(
onTap: widget.enabled ? showButtonMenu : null, onTap: widget.enabled ? showButtonMenu : null,
canRequestFocus: _canRequestFocus, canRequestFocus: _canRequestFocus,
child: widget.child,
enableFeedback: enableFeedback, enableFeedback: enableFeedback,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return UnconstrainedBox(
constrainedAxis: Axis.horizontal,
child: LimitedBox(
key: _menuButtonKey,
maxHeight: constraints.maxHeight,
child: widget.child,
),
);
},
),
), ),
); );
return IconButton( return LayoutBuilder(
icon: widget.icon ?? Icon(Icons.adaptive.more), builder: (BuildContext context, BoxConstraints constraints) {
padding: widget.padding, return UnconstrainedBox(
iconSize: widget.iconSize ?? 24.0, constrainedAxis: Axis.horizontal,
tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, child: LimitedBox(
onPressed: widget.enabled ? showButtonMenu : null, maxHeight: constraints.maxHeight,
enableFeedback: enableFeedback, child: IconButton(
key: _menuButtonKey,
icon: widget.icon ?? Icon(Icons.adaptive.more),
padding: widget.padding,
iconSize: widget.iconSize ?? 24.0,
tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip,
onPressed: widget.enabled ? showButtonMenu : null,
enableFeedback: enableFeedback,
),
),
);
},
); );
} }
} }
...@@ -453,6 +453,7 @@ void main() { ...@@ -453,6 +453,7 @@ void main() {
testWidgets('PopupMenu positioning', (WidgetTester tester) async { testWidgets('PopupMenu positioning', (WidgetTester tester) async {
final Widget testButton = PopupMenuButton<int>( final Widget testButton = PopupMenuButton<int>(
placement: PopupMenuPlacement.belowButton,
itemBuilder: (BuildContext context) { itemBuilder: (BuildContext context) {
return <PopupMenuItem<int>>[ return <PopupMenuItem<int>>[
const PopupMenuItem<int>(value: 1, child: Text('AAA')), const PopupMenuItem<int>(value: 1, child: Text('AAA')),
...@@ -618,24 +619,24 @@ void main() { ...@@ -618,24 +619,24 @@ void main() {
}); });
} }
await testPositioningDown(tester, TextDirection.ltr, Alignment.topRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 8.0, 0.0, 0.0)); await testPositioningDown(tester, TextDirection.ltr, Alignment.topRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 100.0, 0.0, 0.0));
await testPositioningDown(tester, TextDirection.rtl, Alignment.topRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 8.0, 0.0, 0.0)); await testPositioningDown(tester, TextDirection.rtl, Alignment.topRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 100.0, 0.0, 0.0));
await testPositioningDown(tester, TextDirection.ltr, Alignment.topLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 8.0, 0.0, 0.0)); await testPositioningDown(tester, TextDirection.ltr, Alignment.topLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 100.0, 0.0, 0.0));
await testPositioningDown(tester, TextDirection.rtl, Alignment.topLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 8.0, 0.0, 0.0)); await testPositioningDown(tester, TextDirection.rtl, Alignment.topLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 100.0, 0.0, 0.0));
await testPositioningDown(tester, TextDirection.ltr, Alignment.topCenter, TextDirection.ltr, const Rect.fromLTWH(350.0, 8.0, 0.0, 0.0)); await testPositioningDown(tester, TextDirection.ltr, Alignment.topCenter, TextDirection.ltr, const Rect.fromLTWH(350.0, 100.0, 0.0, 0.0));
await testPositioningDown(tester, TextDirection.rtl, Alignment.topCenter, TextDirection.rtl, const Rect.fromLTWH(450.0, 8.0, 0.0, 0.0)); await testPositioningDown(tester, TextDirection.rtl, Alignment.topCenter, TextDirection.rtl, const Rect.fromLTWH(450.0, 100.0, 0.0, 0.0));
await testPositioningDown(tester, TextDirection.ltr, Alignment.centerRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 250.0, 0.0, 0.0)); await testPositioningDown(tester, TextDirection.ltr, Alignment.centerRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 350.0, 0.0, 0.0));
await testPositioningDown(tester, TextDirection.rtl, Alignment.centerRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 250.0, 0.0, 0.0)); await testPositioningDown(tester, TextDirection.rtl, Alignment.centerRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 350.0, 0.0, 0.0));
await testPositioningDown(tester, TextDirection.ltr, Alignment.centerLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 250.0, 0.0, 0.0)); await testPositioningDown(tester, TextDirection.ltr, Alignment.centerLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 350.0, 0.0, 0.0));
await testPositioningDown(tester, TextDirection.rtl, Alignment.centerLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 250.0, 0.0, 0.0)); await testPositioningDown(tester, TextDirection.rtl, Alignment.centerLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 350.0, 0.0, 0.0));
await testPositioningDown(tester, TextDirection.ltr, Alignment.center, TextDirection.ltr, const Rect.fromLTWH(350.0, 250.0, 0.0, 0.0)); await testPositioningDown(tester, TextDirection.ltr, Alignment.center, TextDirection.ltr, const Rect.fromLTWH(350.0, 350.0, 0.0, 0.0));
await testPositioningDown(tester, TextDirection.rtl, Alignment.center, TextDirection.rtl, const Rect.fromLTWH(450.0, 250.0, 0.0, 0.0)); await testPositioningDown(tester, TextDirection.rtl, Alignment.center, TextDirection.rtl, const Rect.fromLTWH(450.0, 350.0, 0.0, 0.0));
await testPositioningDownThenUp(tester, TextDirection.ltr, Alignment.bottomRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 500.0, 0.0, 0.0)); await testPositioningDownThenUp(tester, TextDirection.ltr, Alignment.bottomRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 592.0, 0.0, 0.0));
await testPositioningDownThenUp(tester, TextDirection.rtl, Alignment.bottomRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 500.0, 0.0, 0.0)); await testPositioningDownThenUp(tester, TextDirection.rtl, Alignment.bottomRight, TextDirection.rtl, const Rect.fromLTWH(792.0, 592.0, 0.0, 0.0));
await testPositioningDownThenUp(tester, TextDirection.ltr, Alignment.bottomLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 500.0, 0.0, 0.0)); await testPositioningDownThenUp(tester, TextDirection.ltr, Alignment.bottomLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 592.0, 0.0, 0.0));
await testPositioningDownThenUp(tester, TextDirection.rtl, Alignment.bottomLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 500.0, 0.0, 0.0)); await testPositioningDownThenUp(tester, TextDirection.rtl, Alignment.bottomLeft, TextDirection.ltr, const Rect.fromLTWH(8.0, 592.0, 0.0, 0.0));
await testPositioningDownThenUp(tester, TextDirection.ltr, Alignment.bottomCenter, TextDirection.ltr, const Rect.fromLTWH(350.0, 500.0, 0.0, 0.0)); await testPositioningDownThenUp(tester, TextDirection.ltr, Alignment.bottomCenter, TextDirection.ltr, const Rect.fromLTWH(350.0, 592.0, 0.0, 0.0));
await testPositioningDownThenUp(tester, TextDirection.rtl, Alignment.bottomCenter, TextDirection.rtl, const Rect.fromLTWH(450.0, 500.0, 0.0, 0.0)); await testPositioningDownThenUp(tester, TextDirection.rtl, Alignment.bottomCenter, TextDirection.rtl, const Rect.fromLTWH(450.0, 592.0, 0.0, 0.0));
}); });
testWidgets('PopupMenu positioning inside nested Overlay', (WidgetTester tester) async { testWidgets('PopupMenu positioning inside nested Overlay', (WidgetTester tester) async {
...@@ -653,6 +654,7 @@ void main() { ...@@ -653,6 +654,7 @@ void main() {
builder: (_) => Center( builder: (_) => Center(
child: PopupMenuButton<int>( child: PopupMenuButton<int>(
key: buttonKey, key: buttonKey,
placement: PopupMenuPlacement.belowButton,
itemBuilder: (_) => <PopupMenuItem<int>>[ itemBuilder: (_) => <PopupMenuItem<int>>[
const PopupMenuItem<int>(value: 1, child: Text('Item 1')), const PopupMenuItem<int>(value: 1, child: Text('Item 1')),
const PopupMenuItem<int>(value: 2, child: Text('Item 2')), const PopupMenuItem<int>(value: 2, child: Text('Item 2')),
...@@ -673,8 +675,8 @@ void main() { ...@@ -673,8 +675,8 @@ void main() {
await tester.tap(buttonFinder); await tester.tap(buttonFinder);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final Offset buttonTopLeft = tester.getTopLeft(buttonFinder); final Offset buttonBottomLeft = tester.getBottomLeft(buttonFinder);
expect(tester.getTopLeft(popupFinder), buttonTopLeft); expect(tester.getTopLeft(popupFinder), buttonBottomLeft);
}); });
testWidgets('PopupMenu positioning inside nested Navigator', (WidgetTester tester) async { testWidgets('PopupMenu positioning inside nested Navigator', (WidgetTester tester) async {
...@@ -695,6 +697,7 @@ void main() { ...@@ -695,6 +697,7 @@ void main() {
child: Center( child: Center(
child: PopupMenuButton<int>( child: PopupMenuButton<int>(
key: buttonKey, key: buttonKey,
placement: PopupMenuPlacement.belowButton,
itemBuilder: (_) => <PopupMenuItem<int>>[ itemBuilder: (_) => <PopupMenuItem<int>>[
const PopupMenuItem<int>(value: 1, child: Text('Item 1')), const PopupMenuItem<int>(value: 1, child: Text('Item 1')),
const PopupMenuItem<int>(value: 2, child: Text('Item 2')), const PopupMenuItem<int>(value: 2, child: Text('Item 2')),
...@@ -717,8 +720,8 @@ void main() { ...@@ -717,8 +720,8 @@ void main() {
await tester.tap(buttonFinder); await tester.tap(buttonFinder);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final Offset buttonTopLeft = tester.getTopLeft(buttonFinder); final Offset buttonBottomLeft = tester.getBottomLeft(buttonFinder);
expect(tester.getTopLeft(popupFinder), buttonTopLeft); expect(tester.getTopLeft(popupFinder), buttonBottomLeft);
}); });
testWidgets('PopupMenu removes MediaQuery padding', (WidgetTester tester) async { testWidgets('PopupMenu removes MediaQuery padding', (WidgetTester tester) async {
...@@ -731,6 +734,7 @@ void main() { ...@@ -731,6 +734,7 @@ void main() {
), ),
child: Material( child: Material(
child: PopupMenuButton<int>( child: PopupMenuButton<int>(
placement: PopupMenuPlacement.belowButton,
itemBuilder: (BuildContext context) { itemBuilder: (BuildContext context) {
popupContext = context; popupContext = context;
return <PopupMenuItem<int>>[ return <PopupMenuItem<int>>[
...@@ -765,6 +769,7 @@ void main() { ...@@ -765,6 +769,7 @@ void main() {
testWidgets('Popup Menu Offset Test', (WidgetTester tester) async { testWidgets('Popup Menu Offset Test', (WidgetTester tester) async {
PopupMenuButton<int> buildMenuButton({Offset offset = Offset.zero}) { PopupMenuButton<int> buildMenuButton({Offset offset = Offset.zero}) {
return PopupMenuButton<int>( return PopupMenuButton<int>(
placement: PopupMenuPlacement.belowButton,
offset: offset, offset: offset,
itemBuilder: (BuildContext context) { itemBuilder: (BuildContext context) {
return <PopupMenuItem<int>>[ return <PopupMenuItem<int>>[
...@@ -796,8 +801,10 @@ void main() { ...@@ -796,8 +801,10 @@ void main() {
await tester.tap(find.byType(IconButton)); await tester.tap(find.byType(IconButton));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Initial state, the menu start at Offset(8.0, 8.0), the 8 pixels is edge padding when offset.dx < 8.0. final Size iconSize = tester.getSize(find.byType(IconButton));
expect(tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_PopupMenu<int?>')), const Offset(8.0, 8.0));
// Initial state, the menu start at bottomLeft of the icon, the 8 pixels is edge padding when offset.dx < 8.0.
expect(tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_PopupMenu<int?>')), Offset(8.0, iconSize.height));
// Collapse the menu. // Collapse the menu.
await tester.tap(find.byType(IconButton)); await tester.tap(find.byType(IconButton));
...@@ -817,8 +824,8 @@ void main() { ...@@ -817,8 +824,8 @@ void main() {
await tester.tap(find.byType(IconButton)); await tester.tap(find.byType(IconButton));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// This time the menu should start at Offset(50.0, 50.0), the padding only added when offset.dx < 8.0. // This time the menu should start at Offset(50.0, 50.0 + iconSize.height), the padding only added when offset.dx < 8.0.
expect(tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_PopupMenu<int?>')), const Offset(50.0, 50.0)); expect(tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_PopupMenu<int?>')), Offset(50.0, 50.0 + iconSize.height));
}); });
testWidgets('open PopupMenu has correct semantics', (WidgetTester tester) async { testWidgets('open PopupMenu has correct semantics', (WidgetTester tester) async {
...@@ -1804,6 +1811,124 @@ void main() { ...@@ -1804,6 +1811,124 @@ void main() {
); );
}); });
testWidgets('PopupMenu button can render at its "natural" size in AppBar', (WidgetTester tester) async {
final GlobalKey buttonKey = GlobalKey();
Widget buildFrame(double width, double height) {
return MaterialApp(
builder: (BuildContext context, Widget? child) {
return MediaQuery(
data: const MediaQueryData(
padding: EdgeInsets.only(
top: 32.0,
bottom: 32.0,
),
),
child: child!,
);
},
home: Scaffold(
appBar: AppBar(
toolbarHeight: 100.0,
title: const Text('PopupMenu Test'),
actions: <Widget>[PopupMenuButton<int>(
child: SizedBox(
key: buttonKey,
height: height,
width: width,
child: const ColoredBox(
color: Colors.pink,
),
),
itemBuilder: (BuildContext context) => <PopupMenuEntry<int>>[
const PopupMenuItem<int>(child: Text('-1-'), value: 1),
const PopupMenuItem<int>(child: Text('-2-'), value: 2),
const PopupMenuItem<int>(child: Text('-3-'), value: 3),
const PopupMenuItem<int>(child: Text('-4-'), value: 4),
const PopupMenuItem<int>(child: Text('-5-'), value: 5),
],
)],
),
body: Container(),
),
);
}
await tester.pumpWidget(buildFrame(10.0, 10.0));
Size buttonSize = tester.getSize(find.byKey(buttonKey));
expect(buttonSize, const Size(10.0, 10.0));
await tester.pumpWidget(buildFrame(20.5, 30.5));
buttonSize = tester.getSize(find.byKey(buttonKey));
expect(buttonSize, const Size(20.5, 30.5));
await tester.pumpWidget(buildFrame(20.0, 100.0));
buttonSize = tester.getSize(find.byKey(buttonKey));
expect(buttonSize, const Size(20.0, 100.0));
await tester.pumpWidget(buildFrame(20.0, 100.1));
buttonSize = tester.getSize(find.byKey(buttonKey));
expect(buttonSize, const Size(20.0, 100.0)); // Do not overflow the AppBar.
});
testWidgets('PopupMenu position test when have unsafe area', (WidgetTester tester) async {
final GlobalKey buttonKey = GlobalKey();
final GlobalKey firstItemKey = GlobalKey();
Widget buildFrame(double width, double height) {
return MaterialApp(
builder: (BuildContext context, Widget? child) {
return MediaQuery(
data: const MediaQueryData(
padding: EdgeInsets.only(
top: 32.0,
bottom: 32.0,
),
),
child: child!,
);
},
home: Scaffold(
appBar: AppBar(
title: const Text('PopupMenu Test'),
actions: <Widget>[PopupMenuButton<int>(
placement: PopupMenuPlacement.belowButton,
child: SizedBox(
key: buttonKey,
height: height,
width: width,
child: const ColoredBox(
color: Colors.pink,
),
),
itemBuilder: (BuildContext context) => <PopupMenuEntry<int>>[
PopupMenuItem<int>(
key: firstItemKey,
child: const Text('-1-'),
value: 1,
),
const PopupMenuItem<int>(child: Text('-2-'), value: 2,),
],
)],
),
body: Container(),
),
);
}
await tester.pumpWidget(buildFrame(20.0, 20.0));
await tester.tap(find.byKey(buttonKey));
await tester.pumpAndSettle();
final Offset button = tester.getBottomRight(find.byKey(buttonKey));
final Offset popupMenu = tester.getTopRight(find.byKey(firstItemKey));
// The menu should popup below the button.
// The 8.0 pixels are [_kMenuScreenPadding] and [_kMenuVerticalPadding].
expect(popupMenu, Offset(button.dx - 8.0, button.dy + 8.0));
});
group('feedback', () { group('feedback', () {
late FeedbackTester feedback; late FeedbackTester feedback;
......
...@@ -38,6 +38,7 @@ void main() { ...@@ -38,6 +38,7 @@ void main() {
key: targetKey, key: targetKey,
builder: (BuildContext context) { builder: (BuildContext context) {
return PopupMenuButton<int>( return PopupMenuButton<int>(
placement: PopupMenuPlacement.belowButton,
onSelected: (int value) { onSelected: (int value) {
Navigator.pushNamed(context, '/next'); Navigator.pushNamed(context, '/next');
}, },
...@@ -78,20 +79,20 @@ void main() { ...@@ -78,20 +79,20 @@ void main() {
Offset bottomLeft = tester.getBottomLeft(find.text('hello, world')); Offset bottomLeft = tester.getBottomLeft(find.text('hello, world'));
Offset bottomRight = tester.getBottomRight(find.text('hello, world')); Offset bottomRight = tester.getBottomRight(find.text('hello, world'));
expect(topLeft, const Offset(392.0, 299.5)); expect(topLeft, const Offset(392.0, 347.5));
expect(topRight, const Offset(596.0, 299.5)); expect(topRight, const Offset(596.0, 347.5));
expect(bottomLeft, const Offset(392.0, 316.5)); expect(bottomLeft, const Offset(392.0, 364.5));
expect(bottomRight, const Offset(596.0, 316.5)); expect(bottomRight, const Offset(596.0, 364.5));
topLeft = tester.getTopLeft(find.text('你好,世界')); topLeft = tester.getTopLeft(find.text('你好,世界'));
topRight = tester.getTopRight(find.text('你好,世界')); topRight = tester.getTopRight(find.text('你好,世界'));
bottomLeft = tester.getBottomLeft(find.text('你好,世界')); bottomLeft = tester.getBottomLeft(find.text('你好,世界'));
bottomRight = tester.getBottomRight(find.text('你好,世界')); bottomRight = tester.getBottomRight(find.text('你好,世界'));
expect(topLeft, const Offset(392.0, 347.5)); expect(topLeft, const Offset(392.0, 395.5));
expect(topRight, const Offset(477.0, 347.5)); expect(topRight, const Offset(477.0, 395.5));
expect(bottomLeft, const Offset(392.0, 364.5)); expect(bottomLeft, const Offset(392.0, 412.5));
expect(bottomRight, const Offset(477.0, 364.5)); expect(bottomRight, const Offset(477.0, 412.5));
}); });
testWidgets('Text baseline with EN locale', (WidgetTester tester) async { testWidgets('Text baseline with EN locale', (WidgetTester tester) async {
...@@ -124,6 +125,7 @@ void main() { ...@@ -124,6 +125,7 @@ void main() {
key: targetKey, key: targetKey,
builder: (BuildContext context) { builder: (BuildContext context) {
return PopupMenuButton<int>( return PopupMenuButton<int>(
placement: PopupMenuPlacement.belowButton,
onSelected: (int value) { onSelected: (int value) {
Navigator.pushNamed(context, '/next'); Navigator.pushNamed(context, '/next');
}, },
...@@ -164,19 +166,19 @@ void main() { ...@@ -164,19 +166,19 @@ void main() {
Offset bottomLeft = tester.getBottomLeft(find.text('hello, world')); Offset bottomLeft = tester.getBottomLeft(find.text('hello, world'));
Offset bottomRight = tester.getBottomRight(find.text('hello, world')); Offset bottomRight = tester.getBottomRight(find.text('hello, world'));
expect(topLeft, const Offset(392.0, 300.0)); expect(topLeft, const Offset(392.0, 348.0));
expect(topRight, const Offset(584.0, 300.0)); expect(topRight, const Offset(584.0, 348.0));
expect(bottomLeft, const Offset(392.0, 316)); expect(bottomLeft, const Offset(392.0, 364));
expect(bottomRight, const Offset(584.0, 316)); expect(bottomRight, const Offset(584.0, 364));
topLeft = tester.getTopLeft(find.text('你好,世界')); topLeft = tester.getTopLeft(find.text('你好,世界'));
topRight = tester.getTopRight(find.text('你好,世界')); topRight = tester.getTopRight(find.text('你好,世界'));
bottomLeft = tester.getBottomLeft(find.text('你好,世界')); bottomLeft = tester.getBottomLeft(find.text('你好,世界'));
bottomRight = tester.getBottomRight(find.text('你好,世界')); bottomRight = tester.getBottomRight(find.text('你好,世界'));
expect(topLeft, const Offset(392.0, 348.0)); expect(topLeft, const Offset(392.0, 396.0));
expect(topRight, const Offset(472.0, 348.0)); expect(topRight, const Offset(472.0, 396.0));
expect(bottomLeft, const Offset(392.0, 364.0)); expect(bottomLeft, const Offset(392.0, 412.0));
expect(bottomRight, const Offset(472.0, 364.0)); expect(bottomRight, const Offset(472.0, 412.0));
}); });
} }
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