From 63502879a51413e2bec7a745782d39be1e99aaa4 Mon Sep 17 00:00:00 2001 From: Roothex200 Date: Sat, 12 Sep 2026 05:01:46 +0600 Subject: [PATCH] [material_ui] Stop MenuAnchor from covering its anchor A vertical menu is placed either above or below its anchor, but its height was only capped to the size of the whole overlay. When a menu was too tall to fit above an anchor near the bottom of the screen, the positioning logic fell through to pinning the menu to the bottom edge, which drew it straight over the anchor and hid it completely. Cap the height of a vertical menu to the larger of the space above and the space below the anchor, using the same bounds the positioning logic uses. A menu that no longer fits is shortened and scrolls instead of growing past the anchor. Addresses https://github.com/flutter/flutter/issues/161474 --- packages/material_ui/lib/src/menu_anchor.dart | 27 +++++++++- ...change_2026_09_12_menu_anchor_overlap.yaml | 3 ++ .../material_ui/test/menu_anchor_test.dart | 49 +++++++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 packages/material_ui/pending_changelogs/change_2026_09_12_menu_anchor_overlap.yaml 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 {