diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart index 3e9a3a17685..0159e40adb5 100644 --- a/packages/flutter/lib/src/material/popup_menu.dart +++ b/packages/flutter/lib/src/material/popup_menu.dart @@ -715,23 +715,12 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { @override Offset getPositionForChild(Size size, Size childSize) { + final double y = position.top; + + // Find the ideal horizontal position. // size: The size of the overlay. // childSize: The size of the menu, when fully open, as determined by // getConstraintsForChild. - - final double buttonHeight = size.height - position.top - position.bottom; - // Find the ideal vertical position. - double y = position.top; - if (selectedItemIndex != null) { - double selectedItemOffset = _kMenuVerticalPadding; - for (int index = 0; index < selectedItemIndex!; index += 1) { - selectedItemOffset += itemSizes[index]!.height; - } - selectedItemOffset += itemSizes[selectedItemIndex!]!.height / 2; - y = y + buttonHeight / 2.0 - selectedItemOffset; - } - - // Find the ideal horizontal position. double x; if (position.left > position.right) { // Menu button is closer to the right edge, so grow to the left, aligned to the right edge. diff --git a/packages/flutter/test/material/popup_menu_test.dart b/packages/flutter/test/material/popup_menu_test.dart index e462834c917..e8ed919b920 100644 --- a/packages/flutter/test/material/popup_menu_test.dart +++ b/packages/flutter/test/material/popup_menu_test.dart @@ -3955,6 +3955,80 @@ void main() { expect(tester.getSize(find.byType(Material).last), within(distance: 0.1, from: const Size(112.0, 160.0))); }); + + testWidgets('PopupMenuButton properly positions a constrained-size popup', (WidgetTester tester) async { + final Size windowSize = tester.view.physicalSize / tester.view.devicePixelRatio; + const int length = 50; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Padding( + padding: const EdgeInsets.all(50), + child: Align( + alignment: Alignment.bottomCenter, + child: PopupMenuButton( + itemBuilder: (BuildContext context) { + return List>.generate(length, (int index) { + return PopupMenuItem(value: index, child: Text('item #$index')); + }); + }, + constraints: BoxConstraints(maxHeight: windowSize.height / 3), + popUpAnimationStyle: AnimationStyle.noAnimation, + initialValue: length - 1, + child: const Text('click here'), + ), + ), + ), + ), + ), + ); + await tester.tap(find.text('click here')); + await tester.pump(); + + // Set up finders and verify basic widget structure + final Finder findButton = find.byType(PopupMenuButton); + final Finder findLastItem = find.text('item #49'); + final Finder findListBody = find.byType(ListBody); + final Finder findListViewport = find.ancestor( + of: findListBody, + matching: find.byType(SingleChildScrollView), + ); + expect(findButton, findsOne); + expect(findLastItem, findsOne); + expect(findListBody, findsOne); + expect(findListViewport, findsOne); + + // The button and the list viewport should overlap + final RenderBox button = tester.renderObject(findButton); + final Rect buttonBounds = button.localToGlobal(Offset.zero) & button.size; + final RenderBox listViewport = tester.renderObject(findListViewport); + final Rect listViewportBounds = listViewport.localToGlobal(Offset.zero) & listViewport.size; + expect(listViewportBounds.topLeft.dy, lessThanOrEqualTo(windowSize.height)); + expect(listViewportBounds.bottomRight.dy, lessThanOrEqualTo(windowSize.height)); + expect(listViewportBounds, overlaps(buttonBounds)); + }); +} + +Matcher overlaps(Rect other) => OverlapsMatcher(other); + +class OverlapsMatcher extends Matcher { + OverlapsMatcher(this.other); + + final Rect other; + + @override + Description describe(Description description) { + return description.add(''); + } + + @override + bool matches(Object? item, Map matchState) => item is Rect && item.overlaps(other); + + @override + Description describeMismatch(dynamic item, Description mismatchDescription, + Map matchState, bool verbose) { + return mismatchDescription.add('does not overlap'); + } } class TestApp extends StatelessWidget {