-
Notifications
You must be signed in to change notification settings - Fork 30.1k
Closed
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work listf: material designflutter/packages/flutter/material repository.flutter/packages/flutter/material repository.frameworkflutter/packages/flutter repository. See also f: labels.flutter/packages/flutter repository. See also f: labels.team-frameworkOwned by Framework teamOwned by Framework teamtriaged-frameworkTriaged by Framework teamTriaged by Framework team
Description
Steps to reproduce
- Run example.
- Right click to open menu
Expected results
It should always begin opening at the pointer.
Actual results
It begins opening from the bottom of the padded area, since that is the anchor.
Code sample
Code sample
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// Flutter code sample for [MenuAnchor].
void main() => runApp(const ContextMenuApp());
/// An enhanced enum to define the available menus and their shortcuts.
///
/// Using an enum for menu definition is not required, but this illustrates how
/// they could be used for simple menu systems.
enum MenuEntry {
about('About'),
showMessage(
'Show Message',
SingleActivator(LogicalKeyboardKey.keyS, control: true),
),
hideMessage(
'Hide Message',
SingleActivator(LogicalKeyboardKey.keyS, control: true),
),
colorMenu('Color Menu'),
colorRed(
'Red Background',
SingleActivator(LogicalKeyboardKey.keyR, control: true),
),
colorGreen(
'Green Background',
SingleActivator(LogicalKeyboardKey.keyG, control: true),
),
colorBlue(
'Blue Background',
SingleActivator(LogicalKeyboardKey.keyB, control: true),
);
const MenuEntry(this.label, [this.shortcut]);
final String label;
final MenuSerializableShortcut? shortcut;
}
class MyContextMenu extends StatefulWidget {
const MyContextMenu({super.key, required this.message});
final String message;
@override
State<MyContextMenu> createState() => _MyContextMenuState();
}
class _MyContextMenuState extends State<MyContextMenu> {
MenuEntry? _lastSelection;
final FocusNode _buttonFocusNode = FocusNode(debugLabel: 'Menu Button');
final MenuController _menuController = MenuController();
ShortcutRegistryEntry? _shortcutsEntry;
bool _menuWasEnabled = false;
Color get backgroundColor => _backgroundColor;
Color _backgroundColor = Colors.red;
set backgroundColor(Color value) {
if (_backgroundColor != value) {
setState(() {
_backgroundColor = value;
});
}
}
bool get showingMessage => _showingMessage;
bool _showingMessage = false;
set showingMessage(bool value) {
if (_showingMessage != value) {
setState(() {
_showingMessage = value;
});
}
}
@override
void initState() {
super.initState();
_disableContextMenu();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Dispose of any previously registered shortcuts, since they are about to
// be replaced.
_shortcutsEntry?.dispose();
// Collect the shortcuts from the different menu selections so that they can
// be registered to apply to the entire app. Menus don't register their
// shortcuts, they only display the shortcut hint text.
final Map<ShortcutActivator, Intent> shortcuts =
<ShortcutActivator, Intent>{
for (final MenuEntry item in MenuEntry.values)
if (item.shortcut != null)
item.shortcut!: VoidCallbackIntent(() => _activate(item)),
};
// Register the shortcuts with the ShortcutRegistry so that they are
// available to the entire application.
_shortcutsEntry = ShortcutRegistry.of(context).addAll(shortcuts);
}
@override
void dispose() {
_shortcutsEntry?.dispose();
_buttonFocusNode.dispose();
_reenableContextMenu();
super.dispose();
}
Future<void> _disableContextMenu() async {
if (!kIsWeb) {
// Does nothing on non-web platforms.
return;
}
_menuWasEnabled = BrowserContextMenu.enabled;
if (_menuWasEnabled) {
await BrowserContextMenu.disableContextMenu();
}
}
void _reenableContextMenu() {
if (!kIsWeb) {
// Does nothing on non-web platforms.
return;
}
if (_menuWasEnabled && !BrowserContextMenu.enabled) {
BrowserContextMenu.enableContextMenu();
}
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(50),
child: GestureDetector(
onTapDown: _handleTapDown,
onSecondaryTapDown: _handleSecondaryTapDown,
child: MenuAnchor(
animated: true,
controller: _menuController,
menuChildren: <Widget>[
MenuItemButton(
child: Text(MenuEntry.about.label),
onPressed: () => _activate(MenuEntry.about),
),
if (_showingMessage)
MenuItemButton(
onPressed: () => _activate(MenuEntry.hideMessage),
shortcut: MenuEntry.hideMessage.shortcut,
child: Text(MenuEntry.hideMessage.label),
),
if (!_showingMessage)
MenuItemButton(
onPressed: () => _activate(MenuEntry.showMessage),
shortcut: MenuEntry.showMessage.shortcut,
child: Text(MenuEntry.showMessage.label),
),
SubmenuButton(
animated: true,
menuChildren: <Widget>[
MenuItemButton(
onPressed: () => _activate(MenuEntry.colorRed),
shortcut: MenuEntry.colorRed.shortcut,
child: Text(MenuEntry.colorRed.label),
),
MenuItemButton(
onPressed: () => _activate(MenuEntry.colorGreen),
shortcut: MenuEntry.colorGreen.shortcut,
child: Text(MenuEntry.colorGreen.label),
),
MenuItemButton(
onPressed: () => _activate(MenuEntry.colorBlue),
shortcut: MenuEntry.colorBlue.shortcut,
child: Text(MenuEntry.colorBlue.label),
),
],
child: const Text('Background Color'),
),
],
child: Container(
alignment: Alignment.center,
color: backgroundColor,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'Right-click anywhere on the background to show the menu.',
),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
showingMessage ? widget.message : '',
style: Theme.of(context).textTheme.headlineSmall,
),
),
Text(
_lastSelection != null
? 'Last Selected: ${_lastSelection!.label}'
: '',
),
],
),
),
),
),
);
}
void _activate(MenuEntry selection) {
setState(() {
_lastSelection = selection;
});
switch (selection) {
case MenuEntry.about:
showAboutDialog(
context: context,
applicationName: 'MenuBar Sample',
applicationVersion: '1.0.0',
);
case MenuEntry.showMessage:
case MenuEntry.hideMessage:
showingMessage = !showingMessage;
case MenuEntry.colorMenu:
break;
case MenuEntry.colorRed:
backgroundColor = Colors.red;
case MenuEntry.colorGreen:
backgroundColor = Colors.green;
case MenuEntry.colorBlue:
backgroundColor = Colors.blue;
}
}
void _handleSecondaryTapDown(TapDownDetails details) {
_menuController.open(position: details.localPosition);
}
void _handleTapDown(TapDownDetails details) {
if (_menuController.isOpen) {
_menuController.close();
return;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
// Don't open the menu on these platforms with a Ctrl-tap (or a
// tap).
break;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
// Only open the menu on these platforms if the control button is down
// when the tap occurs.
if (HardwareKeyboard.instance.logicalKeysPressed.contains(
LogicalKeyboardKey.controlLeft,
) ||
HardwareKeyboard.instance.logicalKeysPressed.contains(
LogicalKeyboardKey.controlRight,
)) {
_menuController.open(position: details.localPosition);
}
}
}
}
class ContextMenuApp extends StatelessWidget {
const ContextMenuApp({super.key});
static const String kMessage = '"Talk less. Smile more." - A. Burr';
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(body: MyContextMenu(message: kMessage)),
);
}
}Screenshots or Video
Screenshots / Video demonstration
Screen.Recording.2026-02-26.at.12.46.32.AM.mov
Logs
Logs
N/AFlutter Doctor output
Doctor output
[✓] Flutter (Channel master, 3.42.0-1.0.pre-18, on macOS 15.5 24F74 darwin-arm64, locale en-US) [1,724ms]
• Flutter version 3.42.0-1.0.pre-18 on channel master at /Users/davidhicks/fvm/versions/master
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 73a7978d56 (2 weeks ago), 2026-02-09 00:59:49 -0500
• Engine revision 73a7978d56
• Dart version 3.12.0 (build 3.12.0-127.0.dev)
• DevTools version 2.54.0
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets,
omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration, enable-riscv64
[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [3.1s]
• Android SDK at /Users/davidhicks/Library/Android/sdk
• Emulator version 36.3.10.0 (build_id 14472402) (CL:N/A)
• Platform android-36.1, build-tools 36.1.0
• ANDROID_HOME = /Users/davidhicks/Library/Android/sdk
• Java binary at: /Users/davidhicks/Library/Java/JavaVirtualMachines/jbr-17.0.14/Contents/Home/bin/java
This JDK is specified in your Flutter configuration.
To change the current JDK, run: `flutter config --jdk-dir="path/to/jdk"`.
• Java version OpenJDK Runtime Environment JBR-17.0.14+1-1367.22-nomod (build 17.0.14+1-b1367.22)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 16.4) [1,755ms]
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 16F6
• CocoaPods version 1.16.2
[✓] Chrome - develop for the web [6ms]
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Connected device (2 available) [11.2s]
• macOS (desktop) • macos • darwin-arm64 • macOS 15.5 24F74 darwin-arm64
• Chrome (web) • chrome • web-javascript • Google Chrome 145.0.7632.117
! Error: Browsing on the local area network for David’s iPhone. Ensure the device is unlocked and attached with a cable or associated with the same local area network
as this Mac.
The device must be opted into Developer Mode to connect wirelessly. (code -27)
[✓] Network resources [224ms]
• All expected network resources are available.
• No issues found!Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
P2Important issues not at the top of the work listImportant issues not at the top of the work listf: material designflutter/packages/flutter/material repository.flutter/packages/flutter/material repository.frameworkflutter/packages/flutter repository. See also f: labels.flutter/packages/flutter repository. See also f: labels.team-frameworkOwned by Framework teamOwned by Framework teamtriaged-frameworkTriaged by Framework teamTriaged by Framework team