Repository navigation
Description
What package does this bug report belong to?
material_ui
What target platforms are you seeing this bug on?
Android, iOS, macOS, Linux, Windows
Have you already upgraded your packages?
Yes
Dependency versions
pubspec.lock
material_ui:
dependency: "direct main"
description:
name: material_ui
sha256: e4031746f96c8316864d9b92bce375f1320a36ed56e166307e1e1bda9d90b4ab
url: "https://pub.dev"
source: hosted
version: "1.8.0"
Steps to reproduce
- Run the code sample with a hardware keyboard.
- Click or tap More.
- Press Escape.
Expected results
The menu closes, as it does when it was opened from the keyboard, and as PopupMenuButton's menu does after a tap.
Actual results
The menu stays open. A tap moves focus nowhere, so Escape never reaches the anchor's DismissMenuAction. Opening from the keyboard works because Tab has already focused the button.
Same on every platform, and the framework's own MenuAnchor (package:flutter/material.dart, Flutter 3.47.7) behaves identically.
What we found
RawMenuAnchor's open path moves focus exactly once:
if (_isRootOverlayAnchor) {
widget.childFocusNode?.requestFocus();
}That is the only thing putting focus under the anchor's DismissMenuAction. childFocusNode is optional, and its doc only mentions arrow-key traversal.
Escape is not unhandled. It resolves to _DismissModalAction, which only pops routes, so it ends as a silent no-op. A menu inside a pushed route leaves that route alone too.
| focus after tap | Escape | |
|---|---|---|
MenuAnchor |
the route's scope | stays open |
PopupMenuButton |
the route's scope | closes |
Same focus, different outcome. PopupMenuButton pushes a real route, so _DismissModalAction has something to pop. Focus location is not the difference.
Workaround: pass childFocusNode, wired to the anchor's own button.
Code sample
Code sample
import 'package:flutter/widgets.dart';
import 'package:material_ui/material_ui.dart';
void main() => runApp(const MaterialApp(home: MenuPage()));
class MenuPage extends StatelessWidget {
const MenuPage({super.key});
@override
Widget build(BuildContext context) => Scaffold(
body: Center(
child: MenuAnchor(
menuChildren: [MenuItemButton(onPressed: () {}, child: const Text('Share'))],
builder: (context, controller, _) => TextButton(
onPressed: () => controller.isOpen ? controller.close() : controller.open(),
child: const Text('More'),
),
),
),
);
}Widget test
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:material_ui/material_ui.dart';
// MenuPage from the code sample above.
bool isOpen() => find.text('Share').evaluate().isNotEmpty;
void main() {
testWidgets('Escape closes a menu opened by a tap', (tester) async {
await tester.pumpWidget(const MaterialApp(home: MenuPage()));
await tester.tap(find.text('More'));
await tester.pumpAndSettle();
expect(isOpen(), isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
expect(isOpen(), isFalse); // Fails: the menu is still open.
}, variant: TargetPlatformVariant.all());
testWidgets('Escape closes a menu opened from the keyboard', (tester) async {
await tester.pumpWidget(const MaterialApp(home: MenuPage()));
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();
expect(isOpen(), isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pumpAndSettle();
expect(isOpen(), isFalse); // Passes.
}, variant: TargetPlatformVariant.all());
}Screenshots or Videos
None, the widget test shows it.
Logs
Logs
$ flutter test
Escape closes a menu opened by a tap (variant: TargetPlatform.android)
Expected: false
Actual: <true>The same failure on every other variant. The keyboard test passes on all of them.
Flutter Doctor output
Doctor output
[✓] Flutter (Channel stable, 3.47.7, on macOS 15.7.9 24G830 darwin-arm64, locale en-US)
• Framework revision abaf9c5237 (2 weeks ago), 2026-10-08 09:51:21 -0700
• Engine revision deb287481e
• Dart version 3.13.5
Activity
mbcorona commentedon Oct 22, 2026
Hi @LahaLuhem, thanks for the widget test and the precise focus analysis.
Escape maps to DismissIntent in the menu's traversal shortcuts, wired through Shortcuts and a FocusScope that only takes focus while the menu is open (menu_anchor.dart). Shortcuts only fire when focus is inside that subtree, so a tap or click that never moves focus there leaves Escape with nothing to handle it, exactly as you found.
Routing to team-design.
LahaLuhem commentedon Oct 23, 2026
Refined the body with what the probes showed.
RawMenuAnchormoves focus on open only throughchildFocusNode?.requestFocus(), so the whole mechanism is opt-in. And focus does not need to be inside the menu: the keyboard path works with focus on the button. AMenuAnchorwhose child is aTextFieldalready works for the same reason.That leaves two directions:
Is 2. already in progress under team-design? I have some ideas otherwise
[material_ui] Fix Escape key not closing MenuAnchor opened by tap or …
github-actions commentedon Nov 13, 2026
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.