Updates MenuAnchor to respect software keyboard (#180975)
<!--
Thanks for filing a pull request!
Reviewers are typically assigned within a week of filing a request.
To learn more about code review, see our documentation on Tree Hygiene:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
-->
This pull request addresses the issue of MenuAnchor widgets being
obscured by the software keyboard. It updates the Overlay to respect
MediaQuery constraints that are otherwise stripped by Scaffold widgets.
Fixes https://github.com/flutter/flutter/issues/142921
Fixes https://github.com/flutter/flutter/issues/171066
<details open>
<summary>Problem</summary>
https://github.com/user-attachments/assets/b7429479-bba6-4c93-9ab7-804addf070c4
</details>
<details open>
<summary>Fix</summary>
https://github.com/user-attachments/assets/732bb071-e368-4c6a-89ba-ba4183ea601a
</details>
## Example Application
```dart
import 'package:flutter/material.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatefulWidget {
const MainApp({super.key});
@override
State<MainApp> createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
final MenuController _controller = MenuController();
final List<String> words = ['This', 'is', 'a', 'test', 'This', 'is', 'a', 'test'];
void _toggleMenu() {
if (_controller.isOpen) {
_controller.close();
} else {
_controller.open();
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
children: [
Spacer(),
TextField(),
Spacer(),
MenuAnchor(
controller: _controller,
menuChildren: List.generate(words.length, (index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Text(words[index]),
);
}),
child: FilledButton(
onPressed: _toggleMenu,
child: Text('Tap me!'),
),
),
Spacer(),
],
),
),
);
}
}
```
*If you had to change anything in the [flutter/tests] repo, include a
link to the migration guide as per the [breaking change policy].*
## Pre-launch Checklist
- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.
If you need help, consider asking for advice on the #hackers-new channel
on [Discord].
**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.
<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
---------
Co-authored-by: flutter-pub-roller-bot <137456488+flutter-pub-roller-bot@users.noreply.github.com>
Co-authored-by: b-luk <97480502+b-luk@users.noreply.github.com>
Co-authored-by: engine-flutter-autoroll <engine-flutter-autoroll@skia.org>
Co-authored-by: walley892 <evanwall@buffalo.edu>
Co-authored-by: Ben Konyi <bkonyi@google.com>
Co-authored-by: David Iglesias <ditman@gmail.com>
Co-authored-by: Victor Sanni <victorsanniay@gmail.com>
Co-authored-by: Tong Mu <dkwingsmt@users.noreply.github.com>diff --git a/AUTHORS b/AUTHORS
index 89e904a..1f53093 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -137,3 +137,4 @@
Ricardo Dalarme <ricardodalarme@outlook.com>
yiiim <ybz975218925@live.com>
letrungdo <letrdo@gmail.com>
+Patrick Billingsley <prbillingsley89@gmail.com>
diff --git a/packages/flutter/lib/src/material/menu_anchor.dart b/packages/flutter/lib/src/material/menu_anchor.dart
index 8f15b29..db91019 100644
--- a/packages/flutter/lib/src/material/menu_anchor.dart
+++ b/packages/flutter/lib/src/material/menu_anchor.dart
@@ -3353,6 +3353,7 @@
required this.parentOrientation,
required this.reservedPadding,
required this.heightFactor,
+ required this.mediaQueryData,
});
// Rectangle of underlying button, relative to the overlay's dimensions.
@@ -3390,6 +3391,10 @@
// The factor by which the height of the menu is scaled.
final double heightFactor;
+ // Used to ensure the menu is positioned within the safe area and respects
+ // view insets such as the software keyboard.
+ final MediaQueryData mediaQueryData;
+
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
// The menu can be at most the size of the overlay minus the view padding
@@ -3402,7 +3407,9 @@
// size: The size of the overlay.
// childSize: The size of the menu, when fully open, as determined by
// getConstraintsForChild.
- final Rect overlayRect = Offset.zero & size;
+ final Rect overlayRect = mediaQueryData.padding.deflateRect(
+ mediaQueryData.viewInsets.deflateRect(Offset.zero & size),
+ );
final double unconstrainedHeight = heightFactor > 0.01 ? childSize.height / heightFactor : 0;
final double childHeightEstimate = math.min(unconstrainedHeight, size.height);
final childSizeEstimate = Size(childSize.width, childHeightEstimate);
@@ -3900,6 +3907,7 @@
parentOrientation: anchor._parent?._orientation ?? Axis.horizontal,
reservedPadding: reservedPadding,
heightFactor: heightAnimation.value,
+ mediaQueryData: mediaQuery,
),
child: menuPanel,
);
diff --git a/packages/flutter/test/material/menu_anchor_test.dart b/packages/flutter/test/material/menu_anchor_test.dart
index 2ed71ed..20c3a7b 100644
--- a/packages/flutter/test/material/menu_anchor_test.dart
+++ b/packages/flutter/test/material/menu_anchor_test.dart
@@ -4057,6 +4057,64 @@
expect(tester.getRect(findMenuPanels()).top, tester.getRect(find.byKey(contentKey)).bottom);
});
+ testWidgets('menu is positioned to avoid the software keyboard', (WidgetTester tester) async {
+ // Regression test for https://github.com/flutter/flutter/issues/142921
+ // The menu should not extend into the area occupied by the software keyboard.
+ const screenSize = Size(600, 800);
+ await changeSurfaceSize(tester, screenSize);
+ const keyboardHeight = 200.0;
+ final controller = MenuController();
+
+ await tester.pumpWidget(
+ MaterialApp(
+ builder: (BuildContext context, Widget? child) {
+ return MediaQuery(
+ data: MediaQuery.of(
+ context,
+ ).copyWith(viewInsets: const EdgeInsets.only(bottom: keyboardHeight)),
+ child: child!,
+ );
+ },
+ home: Material(
+ child: Stack(
+ children: <Widget>[
+ // Position anchor just above the keyboard, so menu would extend
+ // into keyboard area if opened below.
+ Positioned(
+ left: 100,
+ bottom: keyboardHeight + 100, // 100px above keyboard
+ child: MenuAnchor(
+ controller: controller,
+ menuChildren: List<Widget>.generate(5, (index) {
+ return MenuItemButton(onPressed: () {}, child: Text('Item $index'));
+ }),
+ builder: (BuildContext context, MenuController controller, Widget? child) {
+ return FilledButton(
+ onPressed: controller.open,
+ child: const Text('Open Menu'),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+
+ controller.open();
+ await tester.pumpAndSettle();
+
+ final Rect menuRect = tester.getRect(findMenuPanels());
+ // Menu should not extend into the keyboard area (bottom 200px of screen).
+ expect(
+ menuRect.bottom,
+ lessThanOrEqualTo(screenSize.height - keyboardHeight),
+ reason:
+ 'Menu bottom (${menuRect.bottom}) should not extend into keyboard area (below ${screenSize.height - keyboardHeight})',
+ );
+ });
+
testWidgets(
'Menu is correctly offset when a LayerLink is provided and alignmentOffset is set',
(WidgetTester tester) async {