diff --git a/packages/material_ui/lib/src/menu_anchor.dart b/packages/material_ui/lib/src/menu_anchor.dart index 78d69aa7fb0..a163b3eebe5 100644 --- a/packages/material_ui/lib/src/menu_anchor.dart +++ b/packages/material_ui/lib/src/menu_anchor.dart @@ -3466,7 +3466,32 @@ class _MenuLayout extends SingleChildLayoutDelegate { BoxConstraints getConstraintsForChild(BoxConstraints constraints) { // The menu can be at most the size of the overlay minus the view padding // in each direction. - return BoxConstraints.loose(constraints.biggest).deflate(reservedPadding); + final BoxConstraints result = BoxConstraints.loose( + constraints.biggest, + ).deflate(reservedPadding); + if (menuPosition != null || orientation == Axis.horizontal) { + return result; + } + // A vertical menu is placed either above or below the anchor, so it can + // only ever occupy the space on one of those two sides. Capping its height + // to the larger of the two keeps a tall menu from growing past the anchor + // and covering it; the menu scrolls instead. + // + // This uses the same bounds as the positioning logic below, so that the cap + // matches the space the menu is actually allowed to be placed in. + final Rect overlayRect = mediaQueryData.padding.deflateRect( + mediaQueryData.viewInsets.deflateRect(Offset.zero & constraints.biggest), + ); + final double availableHeight = math.max( + anchorRect.top - overlayRect.top, + overlayRect.bottom - anchorRect.bottom, + ); + if (availableHeight <= 0.0) { + // The anchor leaves no room on either side, so there is nothing to cap + // the menu to. Leave it to the positioning logic to fit what it can. + return result; + } + return result.copyWith(maxHeight: math.min(availableHeight, result.maxHeight)); } @override diff --git a/packages/material_ui/pending_changelogs/change_2026_09_12_menu_anchor_overlap.yaml b/packages/material_ui/pending_changelogs/change_2026_09_12_menu_anchor_overlap.yaml new file mode 100644 index 00000000000..d152b17191b --- /dev/null +++ b/packages/material_ui/pending_changelogs/change_2026_09_12_menu_anchor_overlap.yaml @@ -0,0 +1,3 @@ +changelog: | + - Fixes `MenuAnchor` covering its anchor when the menu is too tall to fit above it. +version: patch diff --git a/packages/material_ui/test/menu_anchor_test.dart b/packages/material_ui/test/menu_anchor_test.dart index db9c3b4c696..b500ded0683 100644 --- a/packages/material_ui/test/menu_anchor_test.dart +++ b/packages/material_ui/test/menu_anchor_test.dart @@ -4114,6 +4114,55 @@ void main() { ); }); + // Regression test for https://github.com/flutter/flutter/issues/161474 + testWidgets('Menu does not cover the anchor when it does not fit above it', ( + WidgetTester tester, + ) async { + tester.view.physicalSize = const Size(400, 200); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.reset); + + final controller = MenuController(); + final anchorKey = UniqueKey(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + MenuAnchor( + controller: controller, + menuChildren: List.generate( + 3, + (int index) => + MenuItemButton(onPressed: () {}, child: Text('Item ${index + 1}')), + ), + builder: (BuildContext context, MenuController controller, Widget? child) { + return SizedBox(key: anchorKey, width: 56, height: 56); + }, + ), + ], + ), + ), + ), + ); + + controller.open(); + await tester.pumpAndSettle(); + + final Rect anchorRect = tester.getRect(find.byKey(anchorKey)); + final Rect menuRect = tester.getRect(findMenuPanels()); + // There is not enough room above the anchor to show the whole menu, so + // the menu is shortened rather than being pushed down over the anchor. + expect( + menuRect.bottom, + lessThanOrEqualTo(anchorRect.top), + reason: + 'Menu bottom (${menuRect.bottom}) should not cover the anchor top (${anchorRect.top})', + ); + }); + testWidgets( 'Menu is correctly offset when a LayerLink is provided and alignmentOffset is set', (WidgetTester tester) async {