Implement Material MenuBar and MenuAnchor (#112239)

This implements a MenuBar widget that can render a Material menu bar, and a MenuAnchor widget used to create a cascading menu in a region. The menus are drawn in the overlay, while the menu bar itself is in the regular widget tree. Keyboard traversal works between the two.

This implementation of the MenuBar uses MenuAnchor to create a cascading menu that contains widgets representing the menu items. These menu items can be any kind of widget, but are typically SubmenuButtons that host submenus, or MenuItemButtons that have shortcut hints (but don't actually activate the shortcuts) and don't host submenus.

Cascading menus can be created outside of a MenuBar by using a MenuAnchor. They can be either given a specific location to appear (a coordinate), or they can be located by the MenuAnchor region that wraps the control that opens them.

The developer may also create a MenuController to pass to the various menu primitives (MenuBar or MenuAnchor) to associate menus so that they can be traversed together and closed together. Creating a controller is not required.
diff --git a/dev/manual_tests/lib/menu_anchor.dart b/dev/manual_tests/lib/menu_anchor.dart
new file mode 100644
index 0000000..5d0c8de
--- /dev/null
+++ b/dev/manual_tests/lib/menu_anchor.dart
@@ -0,0 +1,667 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+void main() {
+  runApp(
+    const MaterialApp(
+      title: 'Menu Tester',
+      home: Material(
+        child: Home(),
+      ),
+    ),
+  );
+}
+
+class Home extends StatefulWidget {
+  const Home({super.key});
+
+  @override
+  State<Home> createState() => _HomeState();
+}
+
+class _HomeState extends State<Home> {
+  final MenuController _controller = MenuController();
+  VisualDensity _density = VisualDensity.standard;
+  TextDirection _textDirection = TextDirection.ltr;
+  double _extraPadding = 0;
+  bool _addItem = false;
+  bool _transparent = false;
+  bool _funkyTheme = false;
+
+  @override
+  Widget build(BuildContext context) {
+    final ThemeData theme = Theme.of(context);
+    MenuThemeData menuTheme = MenuTheme.of(context);
+    MenuBarThemeData menuBarTheme = MenuBarTheme.of(context);
+    MenuButtonThemeData menuButtonTheme = MenuButtonTheme.of(context);
+    if (_funkyTheme) {
+      menuTheme = const MenuThemeData(
+        style: MenuStyle(
+          shape: MaterialStatePropertyAll<OutlinedBorder>(
+            RoundedRectangleBorder(
+              borderRadius: BorderRadius.all(
+                Radius.circular(10),
+              ),
+            ),
+          ),
+          backgroundColor: MaterialStatePropertyAll<Color?>(Colors.blue),
+          elevation: MaterialStatePropertyAll<double?>(10),
+          padding: MaterialStatePropertyAll<EdgeInsetsDirectional>(
+            EdgeInsetsDirectional.all(20),
+          ),
+        ),
+      );
+      menuButtonTheme = const MenuButtonThemeData(
+        style: ButtonStyle(
+          shape: MaterialStatePropertyAll<OutlinedBorder>(StadiumBorder()),
+          backgroundColor: MaterialStatePropertyAll<Color?>(Colors.green),
+          foregroundColor: MaterialStatePropertyAll<Color?>(Colors.white),
+        ),
+      );
+      menuBarTheme = const MenuBarThemeData(
+        style: MenuStyle(
+          shape: MaterialStatePropertyAll<OutlinedBorder>(RoundedRectangleBorder()),
+          backgroundColor: MaterialStatePropertyAll<Color?>(Colors.blue),
+          elevation: MaterialStatePropertyAll<double?>(10),
+          padding: MaterialStatePropertyAll<EdgeInsetsDirectional>(
+            EdgeInsetsDirectional.all(20),
+          ),
+        ),
+      );
+    }
+    return SafeArea(
+      child: Padding(
+        padding: EdgeInsets.all(_extraPadding),
+        child: Directionality(
+          textDirection: _textDirection,
+          child: Theme(
+            data: theme.copyWith(
+              visualDensity: _density,
+              menuTheme: _transparent
+                  ? MenuThemeData(
+                      style: MenuStyle(
+                        backgroundColor: MaterialStatePropertyAll<Color>(
+                          Colors.blue.withOpacity(0.12),
+                        ),
+                        elevation: const MaterialStatePropertyAll<double>(0),
+                      ),
+                    )
+                  : menuTheme,
+              menuBarTheme: menuBarTheme,
+              menuButtonTheme: menuButtonTheme,
+            ),
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: <Widget>[
+                _TestMenus(
+                  menuController: _controller,
+                  addItem: _addItem,
+                ),
+                Expanded(
+                  child: SingleChildScrollView(
+                    child: _Controls(
+                      menuController: _controller,
+                      density: _density,
+                      addItem: _addItem,
+                      transparent: _transparent,
+                      funkyTheme: _funkyTheme,
+                      extraPadding: _extraPadding,
+                      textDirection: _textDirection,
+                      onDensityChanged: (VisualDensity value) {
+                        setState(() {
+                          _density = value;
+                        });
+                      },
+                      onTextDirectionChanged: (TextDirection value) {
+                        setState(() {
+                          _textDirection = value;
+                        });
+                      },
+                      onExtraPaddingChanged: (double value) {
+                        setState(() {
+                          _extraPadding = value;
+                        });
+                      },
+                      onAddItemChanged: (bool value) {
+                        setState(() {
+                          _addItem = value;
+                        });
+                      },
+                      onTransparentChanged: (bool value) {
+                        setState(() {
+                          _transparent = value;
+                        });
+                      },
+                      onFunkyThemeChanged: (bool value) {
+                        setState(() {
+                          _funkyTheme = value;
+                        });
+                      },
+                    ),
+                  ),
+                ),
+              ],
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+class _Controls extends StatefulWidget {
+  const _Controls({
+    required this.density,
+    required this.textDirection,
+    required this.extraPadding,
+    this.addItem = false,
+    this.transparent = false,
+    this.funkyTheme = false,
+    required this.onDensityChanged,
+    required this.onTextDirectionChanged,
+    required this.onExtraPaddingChanged,
+    required this.onAddItemChanged,
+    required this.onTransparentChanged,
+    required this.onFunkyThemeChanged,
+    required this.menuController,
+  });
+
+  final VisualDensity density;
+  final TextDirection textDirection;
+  final double extraPadding;
+  final bool addItem;
+  final bool transparent;
+  final bool funkyTheme;
+  final ValueChanged<VisualDensity> onDensityChanged;
+  final ValueChanged<TextDirection> onTextDirectionChanged;
+  final ValueChanged<double> onExtraPaddingChanged;
+  final ValueChanged<bool> onAddItemChanged;
+  final ValueChanged<bool> onTransparentChanged;
+  final ValueChanged<bool> onFunkyThemeChanged;
+  final MenuController menuController;
+
+  @override
+  State<_Controls> createState() => _ControlsState();
+}
+
+class _ControlsState extends State<_Controls> {
+  final FocusNode _focusNode = FocusNode(debugLabel: 'Floating');
+
+  @override
+  void dispose() {
+    _focusNode.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      color: Colors.lightBlueAccent,
+      alignment: Alignment.center,
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: <Widget>[
+          MenuAnchor(
+            childFocusNode: _focusNode,
+            style: const MenuStyle(alignment: AlignmentDirectional.topEnd),
+            alignmentOffset: const Offset(100, -8),
+            menuChildren: <Widget>[
+              MenuItemButton(
+                shortcut: const SingleActivator(
+                  LogicalKeyboardKey.keyB,
+                  control: true,
+                ),
+                onPressed: () {
+                  _itemSelected(TestMenu.standaloneMenu1);
+                },
+                child: Text(TestMenu.standaloneMenu1.label),
+              ),
+              MenuItemButton(
+                leadingIcon: const Icon(Icons.send),
+                trailingIcon: const Icon(Icons.mail),
+                onPressed: () {
+                  _itemSelected(TestMenu.standaloneMenu2);
+                },
+                child: Text(TestMenu.standaloneMenu2.label),
+              ),
+            ],
+            builder: (BuildContext context, MenuController controller, Widget? child) {
+              return TextButton(
+                focusNode: _focusNode,
+                onPressed: () {
+                  if (controller.isOpen) {
+                    controller.close();
+                  } else {
+                    controller.open();
+                  }
+                },
+                child: child!,
+              );
+            },
+            child: const Text('Open Menu'),
+          ),
+          ConstrainedBox(
+            constraints: const BoxConstraints(maxWidth: 400),
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.stretch,
+              children: <Widget>[
+                _ControlSlider(
+                  label: 'Extra Padding: ${widget.extraPadding.toStringAsFixed(1)}',
+                  value: widget.extraPadding,
+                  max: 40,
+                  divisions: 20,
+                  onChanged: (double value) {
+                    widget.onExtraPaddingChanged(value);
+                  },
+                ),
+                _ControlSlider(
+                  label: 'Horizontal Density: ${widget.density.horizontal.toStringAsFixed(1)}',
+                  value: widget.density.horizontal,
+                  max: 4,
+                  min: -4,
+                  divisions: 12,
+                  onChanged: (double value) {
+                    widget.onDensityChanged(
+                      VisualDensity(
+                        horizontal: value,
+                        vertical: widget.density.vertical,
+                      ),
+                    );
+                  },
+                ),
+                _ControlSlider(
+                  label: 'Vertical Density: ${widget.density.vertical.toStringAsFixed(1)}',
+                  value: widget.density.vertical,
+                  max: 4,
+                  min: -4,
+                  divisions: 12,
+                  onChanged: (double value) {
+                    widget.onDensityChanged(
+                      VisualDensity(
+                        horizontal: widget.density.horizontal,
+                        vertical: value,
+                      ),
+                    );
+                  },
+                ),
+              ],
+            ),
+          ),
+          Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: <Widget>[
+              Row(
+                mainAxisSize: MainAxisSize.min,
+                children: <Widget>[
+                  Checkbox(
+                    value: widget.textDirection == TextDirection.rtl,
+                    onChanged: (bool? value) {
+                      if (value ?? false) {
+                        widget.onTextDirectionChanged(TextDirection.rtl);
+                      } else {
+                        widget.onTextDirectionChanged(TextDirection.ltr);
+                      }
+                    },
+                  ),
+                  const Text('RTL Text')
+                ],
+              ),
+              Row(
+                mainAxisSize: MainAxisSize.min,
+                children: <Widget>[
+                  Checkbox(
+                    value: widget.addItem,
+                    onChanged: (bool? value) {
+                      if (value ?? false) {
+                        widget.onAddItemChanged(true);
+                      } else {
+                        widget.onAddItemChanged(false);
+                      }
+                    },
+                  ),
+                  const Text('Add Item')
+                ],
+              ),
+              Row(
+                mainAxisSize: MainAxisSize.min,
+                children: <Widget>[
+                  Checkbox(
+                    value: widget.transparent,
+                    onChanged: (bool? value) {
+                      if (value ?? false) {
+                        widget.onTransparentChanged(true);
+                      } else {
+                        widget.onTransparentChanged(false);
+                      }
+                    },
+                  ),
+                  const Text('Transparent')
+                ],
+              ),
+              Row(
+                mainAxisSize: MainAxisSize.min,
+                children: <Widget>[
+                  Checkbox(
+                    value: widget.funkyTheme,
+                    onChanged: (bool? value) {
+                      if (value ?? false) {
+                        widget.onFunkyThemeChanged(true);
+                      } else {
+                        widget.onFunkyThemeChanged(false);
+                      }
+                    },
+                  ),
+                  const Text('Funky Theme')
+                ],
+              ),
+            ],
+          ),
+        ],
+      ),
+    );
+  }
+
+  void _itemSelected(TestMenu item) {
+    debugPrint('App: Selected item ${item.label}');
+  }
+}
+
+class _ControlSlider extends StatelessWidget {
+  const _ControlSlider({
+    required this.label,
+    required this.value,
+    required this.onChanged,
+    this.min = 0,
+    this.max = 1,
+    this.divisions,
+  });
+
+  final String label;
+  final double value;
+  final ValueChanged<double> onChanged;
+  final double min;
+  final double max;
+  final int? divisions;
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      mainAxisAlignment: MainAxisAlignment.end,
+      children: <Widget>[
+        Container(
+          alignment: AlignmentDirectional.centerEnd,
+          constraints: const BoxConstraints(minWidth: 150),
+          child: Text(label),
+        ),
+        Expanded(
+          child: Slider(
+            value: value,
+            min: min,
+            max: max,
+            divisions: divisions,
+            onChanged: onChanged,
+          ),
+        ),
+      ],
+    );
+  }
+}
+
+class _TestMenus extends StatefulWidget {
+  const _TestMenus({
+    required this.menuController,
+    this.addItem = false,
+  });
+
+  final MenuController menuController;
+  final bool addItem;
+
+  @override
+  State<_TestMenus> createState() => _TestMenusState();
+}
+
+class _TestMenusState extends State<_TestMenus> {
+  final TextEditingController textController = TextEditingController();
+
+  void _itemSelected(TestMenu item) {
+    debugPrint('App: Selected item ${item.label}');
+  }
+
+  void _openItem(TestMenu item) {
+    debugPrint('App: Opened item ${item.label}');
+  }
+
+  void _closeItem(TestMenu item) {
+    debugPrint('App: Closed item ${item.label}');
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      children: <Widget>[
+        Expanded(
+          child: MenuBar(
+            controller: widget.menuController,
+            children: <Widget>[
+              SubmenuButton(
+                onOpen: () {
+                  _openItem(TestMenu.mainMenu1);
+                },
+                onClose: () {
+                  _closeItem(TestMenu.mainMenu1);
+                },
+                menuChildren: <Widget>[
+                  MenuItemButton(
+                    shortcut: const SingleActivator(
+                      LogicalKeyboardKey.keyB,
+                      control: true,
+                    ),
+                    leadingIcon:
+                        widget.addItem ? const Icon(Icons.check_box) : const Icon(Icons.check_box_outline_blank),
+                    trailingIcon: const Icon(Icons.assessment),
+                    onPressed: () {
+                      _itemSelected(TestMenu.subMenu1);
+                    },
+                    child: Text(TestMenu.subMenu1.label),
+                  ),
+                  MenuItemButton(
+                    leadingIcon: const Icon(Icons.send),
+                    trailingIcon: const Icon(Icons.mail),
+                    onPressed: () {
+                      _itemSelected(TestMenu.subMenu2);
+                    },
+                    child: Text(TestMenu.subMenu2.label),
+                  ),
+                ],
+                child: Text(TestMenu.mainMenu1.label),
+              ),
+              SubmenuButton(
+                onOpen: () {
+                  _openItem(TestMenu.mainMenu2);
+                },
+                onClose: () {
+                  _closeItem(TestMenu.mainMenu2);
+                },
+                menuChildren: <Widget>[
+                  TextButton(
+                    child: const Text('TEST'),
+                    onPressed: () {
+                      _itemSelected(TestMenu.testButton);
+                      widget.menuController.close();
+                    },
+                  ),
+                  MenuItemButton(
+                    shortcut: const SingleActivator(
+                      LogicalKeyboardKey.enter,
+                      control: true,
+                    ),
+                    onPressed: () {
+                      _itemSelected(TestMenu.subMenu3);
+                    },
+                    child: Text(TestMenu.subMenu3.label),
+                  ),
+                ],
+                child: Text(TestMenu.mainMenu2.label),
+              ),
+              SubmenuButton(
+                onOpen: () {
+                  _openItem(TestMenu.mainMenu3);
+                },
+                onClose: () {
+                  _closeItem(TestMenu.mainMenu3);
+                },
+                menuChildren: <Widget>[
+                  MenuItemButton(
+                    child: Text(TestMenu.subMenu8.label),
+                    onPressed: () {
+                      _itemSelected(TestMenu.subMenu8);
+                    },
+                  ),
+                ],
+                child: Text(TestMenu.mainMenu3.label),
+              ),
+              SubmenuButton(
+                onOpen: () {
+                  _openItem(TestMenu.mainMenu4);
+                },
+                onClose: () {
+                  _closeItem(TestMenu.mainMenu4);
+                },
+                menuChildren: <Widget>[
+                  Actions(
+                    actions: <Type, Action<Intent>>{
+                      ActivateIntent: CallbackAction<ActivateIntent>(
+                        onInvoke: (ActivateIntent? intent) {
+                          debugPrint('Activated!');
+                          return;
+                        },
+                      )
+                    },
+                    child: MenuItemButton(
+                      shortcut: const SingleActivator(
+                        LogicalKeyboardKey.keyA,
+                        control: true,
+                      ),
+                      onPressed: () {
+                        debugPrint('Activated text input item with ${textController.text} as a value.');
+                      },
+                      child: SizedBox(
+                        width: 200,
+                        child: TextField(
+                          controller: textController,
+                          onSubmitted: (String value) {
+                            debugPrint('String $value submitted.');
+                          },
+                        ),
+                      ),
+                    ),
+                  ),
+                  SubmenuButton(
+                    onOpen: () {
+                      _openItem(TestMenu.subMenu5);
+                    },
+                    onClose: () {
+                      _closeItem(TestMenu.subMenu5);
+                    },
+                    menuChildren: <Widget>[
+                      MenuItemButton(
+                        shortcut: widget.addItem
+                            ? const SingleActivator(
+                                LogicalKeyboardKey.f11,
+                                control: true,
+                              )
+                            : const SingleActivator(
+                                LogicalKeyboardKey.f10,
+                                control: true,
+                              ),
+                        onPressed: () {
+                          _itemSelected(TestMenu.subSubMenu1);
+                        },
+                        child: Text(TestMenu.subSubMenu1.label),
+                      ),
+                      MenuItemButton(
+                        child: Text(TestMenu.subSubMenu2.label),
+                        onPressed: () {
+                          _itemSelected(TestMenu.subSubMenu2);
+                        },
+                      ),
+                      if (widget.addItem)
+                        SubmenuButton(
+                          menuChildren: <Widget>[
+                            MenuItemButton(
+                              child: Text(TestMenu.subSubSubMenu1.label),
+                              onPressed: () {
+                                _itemSelected(TestMenu.subSubSubMenu1);
+                              },
+                            ),
+                          ],
+                          child: Text(TestMenu.subSubMenu3.label),
+                        ),
+                    ],
+                    child: Text(TestMenu.subMenu5.label),
+                  ),
+                  MenuItemButton(
+                    // Disabled button
+                    shortcut: const SingleActivator(
+                      LogicalKeyboardKey.tab,
+                      control: true,
+                    ),
+                    child: Text(TestMenu.subMenu6.label),
+                  ),
+                  MenuItemButton(
+                    child: Text(TestMenu.subMenu7.label),
+                    onPressed: () {
+                      _itemSelected(TestMenu.subMenu7);
+                    },
+                  ),
+                  MenuItemButton(
+                    child: Text(TestMenu.subMenu7.label),
+                    onPressed: () {
+                      _itemSelected(TestMenu.subMenu7);
+                    },
+                  ),
+                  MenuItemButton(
+                    child: Text(TestMenu.subMenu8.label),
+                    onPressed: () {
+                      _itemSelected(TestMenu.subMenu8);
+                    },
+                  ),
+                ],
+                child: Text(TestMenu.mainMenu4.label),
+              ),
+            ],
+          ),
+        ),
+      ],
+    );
+  }
+}
+
+enum TestMenu {
+  mainMenu1('Menu 1'),
+  mainMenu2('Menu 2'),
+  mainMenu3('Menu 3'),
+  mainMenu4('Menu 4'),
+  subMenu1('Sub Menu 1'),
+  subMenu2('Sub Menu 2'),
+  subMenu3('Sub Menu 3'),
+  subMenu4('Sub Menu 4'),
+  subMenu5('Sub Menu 5'),
+  subMenu6('Sub Menu 6'),
+  subMenu7('Sub Menu 7'),
+  subMenu8('Sub Menu 8'),
+  subSubMenu1('Sub Sub Menu 1'),
+  subSubMenu2('Sub Sub Menu 2'),
+  subSubMenu3('Sub Sub Menu 3'),
+  subSubSubMenu1('Sub Sub Sub Menu 1'),
+  testButton('TEST button'),
+  standaloneMenu1('Standalone Menu 1'),
+  standaloneMenu2('Standalone Menu 2');
+
+  const TestMenu(this.label);
+  final String label;
+}
diff --git a/examples/api/lib/material/menu_anchor/menu_anchor.0.dart b/examples/api/lib/material/menu_anchor/menu_anchor.0.dart
new file mode 100644
index 0000000..b613e14
--- /dev/null
+++ b/examples/api/lib/material/menu_anchor/menu_anchor.0.dart
@@ -0,0 +1,212 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// Flutter code sample for [MenuAnchor].
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+void main() => runApp(const MenuApp());
+
+/// 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 MyCascadingMenu extends StatefulWidget {
+  const MyCascadingMenu({super.key, required this.message});
+
+  final String message;
+
+  @override
+  State<MyCascadingMenu> createState() => _MyCascadingMenuState();
+}
+
+class _MyCascadingMenuState extends State<MyCascadingMenu> {
+  MenuEntry? _lastSelection;
+  final FocusNode _buttonFocusNode = FocusNode(debugLabel: 'Menu Button');
+  ShortcutRegistryEntry? _shortcutsEntry;
+
+  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 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();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: <Widget>[
+        MenuAnchor(
+          childFocusNode: _buttonFocusNode,
+          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(
+              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'),
+            ),
+          ],
+          builder: (BuildContext context, MenuController controller, Widget? child) {
+            return TextButton(
+              focusNode: _buttonFocusNode,
+              onPressed: () {
+                if (controller.isOpen) {
+                  controller.close();
+                } else {
+                  controller.open();
+                }
+              },
+              child: const Text('OPEN MENU'),
+            );
+          },
+        ),
+        Expanded(
+          child: Container(
+            alignment: Alignment.center,
+            color: backgroundColor,
+            child: Column(
+              mainAxisAlignment: MainAxisAlignment.center,
+              children: <Widget>[
+                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',
+        );
+        break;
+      case MenuEntry.hideMessage:
+      case MenuEntry.showMessage:
+        showingMessage = !showingMessage;
+        break;
+      case MenuEntry.colorMenu:
+        break;
+      case MenuEntry.colorRed:
+        backgroundColor = Colors.red;
+        break;
+      case MenuEntry.colorGreen:
+        backgroundColor = Colors.green;
+        break;
+      case MenuEntry.colorBlue:
+        backgroundColor = Colors.blue;
+        break;
+    }
+  }
+}
+
+class MenuApp extends StatelessWidget {
+  const MenuApp({super.key});
+
+  static const String kMessage = '"Talk less. Smile more." - A. Burr';
+
+  @override
+  Widget build(BuildContext context) {
+    return const MaterialApp(
+      home: Scaffold(body: MyCascadingMenu(message: kMessage)),
+    );
+  }
+}
diff --git a/examples/api/lib/material/menu_anchor/menu_anchor.1.dart b/examples/api/lib/material/menu_anchor/menu_anchor.1.dart
new file mode 100644
index 0000000..da12e0b
--- /dev/null
+++ b/examples/api/lib/material/menu_anchor/menu_anchor.1.dart
@@ -0,0 +1,211 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// Flutter code sample for [MenuAnchor].
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+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;
+
+  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 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();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.all(50),
+      child: GestureDetector(
+        onTapDown: _handleTapDown,
+        child: MenuAnchor(
+          controller: _menuController,
+          anchorTapClosesMenu: true,
+          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(
+              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('Ctrl-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',
+        );
+        break;
+      case MenuEntry.showMessage:
+      case MenuEntry.hideMessage:
+        showingMessage = !showingMessage;
+        break;
+      case MenuEntry.colorMenu:
+        break;
+      case MenuEntry.colorRed:
+        backgroundColor = Colors.red;
+        break;
+      case MenuEntry.colorGreen:
+        backgroundColor = Colors.green;
+        break;
+      case MenuEntry.colorBlue:
+        backgroundColor = Colors.blue;
+        break;
+    }
+  }
+
+  void _handleTapDown(TapDownDetails details) {
+    if (!HardwareKeyboard.instance.logicalKeysPressed.contains(LogicalKeyboardKey.controlLeft) &&
+        !HardwareKeyboard.instance.logicalKeysPressed.contains(LogicalKeyboardKey.controlRight)) {
+      return;
+    }
+    _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)),
+    );
+  }
+}
diff --git a/examples/api/lib/material/menu_anchor/menu_bar.0.dart b/examples/api/lib/material/menu_anchor/menu_bar.0.dart
new file mode 100644
index 0000000..eb68121
--- /dev/null
+++ b/examples/api/lib/material/menu_anchor/menu_bar.0.dart
@@ -0,0 +1,236 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// Flutter code sample for [MenuBar]
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+void main() => runApp(const MenuBarApp());
+
+/// A class for consolidating the definition of menu entries.
+///
+/// This sort of class is not required, but illustrates one way that defining
+/// menus could be done.
+class MenuEntry {
+  const MenuEntry({required this.label, this.shortcut, this.onPressed, this.menuChildren})
+      : assert(menuChildren == null || onPressed == null, 'onPressed is ignored if menuChildren are provided');
+  final String label;
+
+  final MenuSerializableShortcut? shortcut;
+  final VoidCallback? onPressed;
+  final List<MenuEntry>? menuChildren;
+
+  static List<Widget> build(List<MenuEntry> selections) {
+    Widget buildSelection(MenuEntry selection) {
+      if (selection.menuChildren != null) {
+        return SubmenuButton(
+          menuChildren: MenuEntry.build(selection.menuChildren!),
+          child: Text(selection.label),
+        );
+      }
+      return MenuItemButton(
+        shortcut: selection.shortcut,
+        onPressed: selection.onPressed,
+        child: Text(selection.label),
+      );
+    }
+
+    return selections.map<Widget>(buildSelection).toList();
+  }
+
+  static Map<MenuSerializableShortcut, Intent> shortcuts(List<MenuEntry> selections) {
+    final Map<MenuSerializableShortcut, Intent> result = <MenuSerializableShortcut, Intent>{};
+    for (final MenuEntry selection in selections) {
+      if (selection.menuChildren != null) {
+        result.addAll(MenuEntry.shortcuts(selection.menuChildren!));
+      } else {
+        if (selection.shortcut != null && selection.onPressed != null) {
+          result[selection.shortcut!] = VoidCallbackIntent(selection.onPressed!);
+        }
+      }
+    }
+    return result;
+  }
+}
+
+class MyMenuBar extends StatefulWidget {
+  const MyMenuBar({
+    super.key,
+    required this.message,
+  });
+
+  final String message;
+
+  @override
+  State<MyMenuBar> createState() => _MyMenuBarState();
+}
+
+class _MyMenuBarState extends State<MyMenuBar> {
+  ShortcutRegistryEntry? _shortcutsEntry;
+  String? _lastSelection;
+
+  Color get backgroundColor => _backgroundColor;
+  Color _backgroundColor = Colors.red;
+  set backgroundColor(Color value) {
+    if (_backgroundColor != value) {
+      setState(() {
+        _backgroundColor = value;
+      });
+    }
+  }
+
+  bool get showingMessage => _showMessage;
+  bool _showMessage = false;
+  set showingMessage(bool value) {
+    if (_showMessage != value) {
+      setState(() {
+        _showMessage = value;
+      });
+    }
+  }
+
+  @override
+  void dispose() {
+    _shortcutsEntry?.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Column(
+      children: <Widget>[
+        Row(
+          mainAxisSize: MainAxisSize.min,
+          children: <Widget>[
+            Expanded(
+              child: MenuBar(
+                children: MenuEntry.build(_getMenus()),
+              ),
+            ),
+          ],
+        ),
+        Expanded(
+          child: Container(
+            alignment: Alignment.center,
+            color: backgroundColor,
+            child: Column(
+              mainAxisAlignment: MainAxisAlignment.center,
+              children: <Widget>[
+                Padding(
+                  padding: const EdgeInsets.all(12.0),
+                  child: Text(
+                    showingMessage ? widget.message : '',
+                    style: Theme.of(context).textTheme.headlineSmall,
+                  ),
+                ),
+                Text(_lastSelection != null ? 'Last Selected: $_lastSelection' : ''),
+              ],
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+
+  List<MenuEntry> _getMenus() {
+    final List<MenuEntry> result = <MenuEntry>[
+      MenuEntry(
+        label: 'Menu Demo',
+        menuChildren: <MenuEntry>[
+          MenuEntry(
+            label: 'About',
+            onPressed: () {
+              showAboutDialog(
+                context: context,
+                applicationName: 'MenuBar Sample',
+                applicationVersion: '1.0.0',
+              );
+              setState(() {
+                _lastSelection = 'About';
+              });
+            },
+          ),
+          MenuEntry(
+            label: showingMessage ? 'Hide Message' : 'Show Message',
+            onPressed: () {
+              setState(() {
+                _lastSelection = showingMessage ? 'Hide Message' : 'Show Message';
+                showingMessage = !showingMessage;
+              });
+            },
+            shortcut: const SingleActivator(LogicalKeyboardKey.keyS, control: true),
+          ),
+          // Hides the message, but is only enabled if the message isn't
+          // already hidden.
+          MenuEntry(
+            label: 'Reset Message',
+            onPressed: showingMessage
+                ? () {
+                    setState(() {
+                      _lastSelection = 'Reset Message';
+                      showingMessage = false;
+                    });
+                  }
+                : null,
+            shortcut: const SingleActivator(LogicalKeyboardKey.escape),
+          ),
+          MenuEntry(
+            label: 'Background Color',
+            menuChildren: <MenuEntry>[
+              MenuEntry(
+                label: 'Red Background',
+                onPressed: () {
+                  setState(() {
+                    _lastSelection = 'Red Background';
+                    backgroundColor = Colors.red;
+                  });
+                },
+                shortcut: const SingleActivator(LogicalKeyboardKey.keyR, control: true),
+              ),
+              MenuEntry(
+                label: 'Green Background',
+                onPressed: () {
+                  setState(() {
+                    _lastSelection = 'Green Background';
+                    backgroundColor = Colors.green;
+                  });
+                },
+                shortcut: const SingleActivator(LogicalKeyboardKey.keyG, control: true),
+              ),
+              MenuEntry(
+                label: 'Blue Background',
+                onPressed: () {
+                  setState(() {
+                    _lastSelection = 'Blue Background';
+                    backgroundColor = Colors.blue;
+                  });
+                },
+                shortcut: const SingleActivator(LogicalKeyboardKey.keyB, control: true),
+              ),
+            ],
+          ),
+        ],
+      ),
+    ];
+    // (Re-)register the shortcuts with the ShortcutRegistry so that they are
+    // available to the entire application, and update them if they've changed.
+    _shortcutsEntry?.dispose();
+    _shortcutsEntry = ShortcutRegistry.of(context).addAll(MenuEntry.shortcuts(result));
+    return result;
+  }
+}
+
+class MenuBarApp extends StatelessWidget {
+  const MenuBarApp({super.key});
+
+  static const String kMessage = '"Talk less. Smile more." - A. Burr';
+
+  @override
+  Widget build(BuildContext context) {
+    return const MaterialApp(
+      home: Scaffold(body: MyMenuBar(message: kMessage)),
+    );
+  }
+}
diff --git a/examples/api/test/material/menu_anchor/menu_anchor.0_test.dart b/examples/api/test/material/menu_anchor/menu_anchor.0_test.dart
new file mode 100644
index 0000000..54016dc
--- /dev/null
+++ b/examples/api/test/material/menu_anchor/menu_anchor.0_test.dart
@@ -0,0 +1,107 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_api_samples/material/menu_anchor/menu_anchor.0.dart' as example;
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  testWidgets('Can open menu', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      const example.MenuApp(),
+    );
+
+    await tester.tap(find.byType(TextButton));
+    await tester.pump();
+
+    expect(find.text(example.MenuEntry.about.label), findsOneWidget);
+    expect(find.text(example.MenuEntry.showMessage.label), findsOneWidget);
+    expect(find.text(example.MenuEntry.hideMessage.label), findsNothing);
+    expect(find.text('Background Color'), findsOneWidget);
+    expect(find.text(example.MenuEntry.colorRed.label), findsNothing);
+    expect(find.text(example.MenuEntry.colorGreen.label), findsNothing);
+    expect(find.text(example.MenuEntry.colorBlue.label), findsNothing);
+    expect(find.text(example.MenuApp.kMessage), findsNothing);
+
+    await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+    await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+    await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+    await tester.pump();
+
+    expect(find.text('Background Color'), findsOneWidget);
+
+    await tester.tap(find.text('Background Color'));
+    await tester.pump();
+
+    expect(find.text(example.MenuEntry.colorRed.label), findsOneWidget);
+    expect(find.text(example.MenuEntry.colorGreen.label), findsOneWidget);
+    expect(find.text(example.MenuEntry.colorBlue.label), findsOneWidget);
+
+    await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
+    await tester.sendKeyEvent(LogicalKeyboardKey.enter);
+    await tester.pump();
+
+    expect(find.text(example.MenuApp.kMessage), findsOneWidget);
+    expect(find.text('Last Selected: ${example.MenuEntry.showMessage.label}'), findsOneWidget);
+  });
+
+  testWidgets('Shortcuts work', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      const example.MenuApp(),
+    );
+
+    // Open the menu so we can watch state changes resulting from the shortcuts
+    // firing.
+    await tester.tap(find.byType(TextButton));
+    await tester.pump();
+
+    expect(find.text(example.MenuEntry.showMessage.label), findsOneWidget);
+    expect(find.text(example.MenuEntry.hideMessage.label), findsNothing);
+    expect(find.text(example.MenuApp.kMessage), findsNothing);
+
+    await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
+    await tester.sendKeyEvent(LogicalKeyboardKey.keyS);
+    await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
+    await tester.pump();
+    // Need to pump twice because of the one frame delay in the notification to
+    // update the overlay entry.
+    await tester.pump();
+
+    expect(find.text(example.MenuEntry.showMessage.label), findsNothing);
+    expect(find.text(example.MenuEntry.hideMessage.label), findsOneWidget);
+    expect(find.text(example.MenuApp.kMessage), findsOneWidget);
+
+    await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
+    await tester.sendKeyEvent(LogicalKeyboardKey.keyS);
+    await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
+    await tester.pump();
+    await tester.pump();
+
+    expect(find.text(example.MenuEntry.showMessage.label), findsOneWidget);
+    expect(find.text(example.MenuEntry.hideMessage.label), findsNothing);
+    expect(find.text(example.MenuApp.kMessage), findsNothing);
+
+    await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
+    await tester.sendKeyEvent(LogicalKeyboardKey.keyR);
+    await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
+    await tester.pump();
+
+    expect(find.text('Last Selected: ${example.MenuEntry.colorRed.label}'), findsOneWidget);
+
+    await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
+    await tester.sendKeyEvent(LogicalKeyboardKey.keyG);
+    await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
+    await tester.pump();
+
+    expect(find.text('Last Selected: ${example.MenuEntry.colorGreen.label}'), findsOneWidget);
+
+    await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
+    await tester.sendKeyEvent(LogicalKeyboardKey.keyB);
+    await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
+    await tester.pump();
+
+    expect(find.text('Last Selected: ${example.MenuEntry.colorBlue.label}'), findsOneWidget);
+  });
+}
diff --git a/examples/api/test/material/menu_anchor/menu_anchor.1_test.dart b/examples/api/test/material/menu_anchor/menu_anchor.1_test.dart
new file mode 100644
index 0000000..dbe3547
--- /dev/null
+++ b/examples/api/test/material/menu_anchor/menu_anchor.1_test.dart
@@ -0,0 +1,121 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_api_samples/material/menu_anchor/menu_anchor.1.dart' as example;
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  testWidgets('Can open menu', (WidgetTester tester) async {
+    Finder findMenu() {
+      return find.ancestor(
+        of: find.text(example.MenuEntry.about.label),
+        matching: find.byType(FocusScope),
+      ).first;
+    }
+
+    await tester.pumpWidget(const example.ContextMenuApp());
+
+    await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
+    await tester.tapAt(const Offset(100, 200));
+    await tester.pump();
+    expect(tester.getRect(findMenu()), equals(const Rect.fromLTRB(100.0, 200.0, 404.0, 352.0)));
+
+    // Make sure tapping in a different place causes the menu to move.
+    await tester.tapAt(const Offset(200, 100));
+    await tester.pump();
+    await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
+
+    expect(tester.getRect(findMenu()), equals(const Rect.fromLTRB(200.0, 100.0, 504.0, 252.0)));
+
+    expect(find.text(example.MenuEntry.about.label), findsOneWidget);
+    expect(find.text(example.MenuEntry.showMessage.label), findsOneWidget);
+    expect(find.text(example.MenuEntry.hideMessage.label), findsNothing);
+    expect(find.text('Background Color'), findsOneWidget);
+    expect(find.text(example.MenuEntry.colorRed.label), findsNothing);
+    expect(find.text(example.MenuEntry.colorGreen.label), findsNothing);
+    expect(find.text(example.MenuEntry.colorBlue.label), findsNothing);
+    expect(find.text(example.ContextMenuApp.kMessage), findsNothing);
+
+    await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+    await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+    await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+    await tester.pump();
+
+    expect(find.text('Background Color'), findsOneWidget);
+
+    await tester.tap(find.text('Background Color'));
+    await tester.pump();
+
+    expect(find.text(example.MenuEntry.colorRed.label), findsOneWidget);
+    expect(find.text(example.MenuEntry.colorGreen.label), findsOneWidget);
+    expect(find.text(example.MenuEntry.colorBlue.label), findsOneWidget);
+
+    await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
+    await tester.sendKeyEvent(LogicalKeyboardKey.enter);
+    await tester.pump();
+
+    expect(find.text(example.ContextMenuApp.kMessage), findsOneWidget);
+    expect(find.text('Last Selected: ${example.MenuEntry.showMessage.label}'), findsOneWidget);
+  });
+
+  testWidgets('Shortcuts work', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      const example.ContextMenuApp(),
+    );
+
+    // Open the menu so we can look for state changes reflected in the menu.
+    await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
+    await tester.tapAt(const Offset(100, 200));
+    await tester.pump();
+
+    expect(find.text(example.MenuEntry.showMessage.label), findsOneWidget);
+    expect(find.text(example.MenuEntry.hideMessage.label), findsNothing);
+    expect(find.text(example.ContextMenuApp.kMessage), findsNothing);
+
+    await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
+    await tester.sendKeyEvent(LogicalKeyboardKey.keyS);
+    await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
+    await tester.pump();
+    // Need to pump twice because of the one frame delay in the notification to
+    // update the overlay entry.
+    await tester.pump();
+
+    expect(find.text(example.MenuEntry.showMessage.label), findsNothing);
+    expect(find.text(example.MenuEntry.hideMessage.label), findsOneWidget);
+    expect(find.text(example.ContextMenuApp.kMessage), findsOneWidget);
+
+    await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
+    await tester.sendKeyEvent(LogicalKeyboardKey.keyS);
+    await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
+    await tester.pump();
+    await tester.pump();
+
+    expect(find.text(example.MenuEntry.showMessage.label), findsOneWidget);
+    expect(find.text(example.MenuEntry.hideMessage.label), findsNothing);
+    expect(find.text(example.ContextMenuApp.kMessage), findsNothing);
+
+    await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
+    await tester.sendKeyEvent(LogicalKeyboardKey.keyR);
+    await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
+    await tester.pump();
+
+    expect(find.text('Last Selected: ${example.MenuEntry.colorRed.label}'), findsOneWidget);
+
+    await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
+    await tester.sendKeyEvent(LogicalKeyboardKey.keyG);
+    await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
+    await tester.pump();
+
+    expect(find.text('Last Selected: ${example.MenuEntry.colorGreen.label}'), findsOneWidget);
+
+    await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
+    await tester.sendKeyEvent(LogicalKeyboardKey.keyB);
+    await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
+    await tester.pump();
+
+    expect(find.text('Last Selected: ${example.MenuEntry.colorBlue.label}'), findsOneWidget);
+  });
+}
diff --git a/examples/api/test/material/menu_anchor/menu_bar.0_test.dart b/examples/api/test/material/menu_anchor/menu_bar.0_test.dart
new file mode 100644
index 0000000..b508ba4
--- /dev/null
+++ b/examples/api/test/material/menu_anchor/menu_bar.0_test.dart
@@ -0,0 +1,94 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_api_samples/material/menu_anchor/menu_bar.0.dart' as example;
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  testWidgets('Can open menu', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      const example.MenuBarApp(),
+    );
+
+    final Finder menuBarFinder = find.byType(MenuBar);
+    final MenuBar menuBar = tester.widget<MenuBar>(menuBarFinder);
+    expect(menuBar.children, isNotEmpty);
+    expect(menuBar.children.length, equals(1));
+
+    final Finder menuButtonFinder = find.byType(SubmenuButton).first;
+    await tester.tap(menuButtonFinder);
+    await tester.pump();
+
+    expect(find.text('About'), findsOneWidget);
+    expect(find.text('Show Message'), findsOneWidget);
+    expect(find.text('Reset Message'), findsOneWidget);
+    expect(find.text('Background Color'), findsOneWidget);
+    expect(find.text('Red Background'), findsNothing);
+    expect(find.text('Green Background'), findsNothing);
+    expect(find.text('Blue Background'), findsNothing);
+    expect(find.text(example.MenuBarApp.kMessage), findsNothing);
+
+    await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+    await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+    await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+    await tester.pump();
+
+    expect(find.text('About'), findsOneWidget);
+    expect(find.text('Show Message'), findsOneWidget);
+    expect(find.text('Reset Message'), findsOneWidget);
+    expect(find.text('Background Color'), findsOneWidget);
+    expect(find.text('Red Background'), findsOneWidget);
+    expect(find.text('Green Background'), findsOneWidget);
+    expect(find.text('Blue Background'), findsOneWidget);
+
+    await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
+    await tester.sendKeyEvent(LogicalKeyboardKey.enter);
+    await tester.pump();
+
+    expect(find.text(example.MenuBarApp.kMessage), findsOneWidget);
+    expect(find.text('Last Selected: Show Message'), findsOneWidget);
+  });
+  testWidgets('Shortcuts work', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      const example.MenuBarApp(),
+    );
+
+    expect(find.text(example.MenuBarApp.kMessage), findsNothing);
+
+    await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
+    await tester.sendKeyEvent(LogicalKeyboardKey.keyS);
+    await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
+    await tester.pump();
+
+    expect(find.text(example.MenuBarApp.kMessage), findsOneWidget);
+    await tester.sendKeyEvent(LogicalKeyboardKey.escape);
+    await tester.pump();
+
+    expect(find.text(example.MenuBarApp.kMessage), findsNothing);
+    expect(find.text('Last Selected: Reset Message'), findsOneWidget);
+
+    await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
+    await tester.sendKeyEvent(LogicalKeyboardKey.keyR);
+    await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
+    await tester.pump();
+
+    expect(find.text('Last Selected: Red Background'), findsOneWidget);
+
+    await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
+    await tester.sendKeyEvent(LogicalKeyboardKey.keyG);
+    await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
+    await tester.pump();
+
+    expect(find.text('Last Selected: Green Background'), findsOneWidget);
+
+    await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
+    await tester.sendKeyEvent(LogicalKeyboardKey.keyB);
+    await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
+    await tester.pump();
+
+    expect(find.text('Last Selected: Blue Background'), findsOneWidget);
+  });
+}
diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart
index 5e84a60..33e77f8 100644
--- a/packages/flutter/lib/material.dart
+++ b/packages/flutter/lib/material.dart
@@ -111,6 +111,11 @@
 export 'src/material/material_localizations.dart';
 export 'src/material/material_state.dart';
 export 'src/material/material_state_mixin.dart';
+export 'src/material/menu_anchor.dart';
+export 'src/material/menu_bar_theme.dart';
+export 'src/material/menu_button_theme.dart';
+export 'src/material/menu_style.dart';
+export 'src/material/menu_theme.dart';
 export 'src/material/mergeable_material.dart';
 export 'src/material/navigation_bar.dart';
 export 'src/material/navigation_bar_theme.dart';
diff --git a/packages/flutter/lib/src/material/menu_anchor.dart b/packages/flutter/lib/src/material/menu_anchor.dart
new file mode 100644
index 0000000..3167117
--- /dev/null
+++ b/packages/flutter/lib/src/material/menu_anchor.dart
@@ -0,0 +1,2846 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:math' as math;
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter/scheduler.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+
+import 'button_style.dart';
+import 'button_style_button.dart';
+import 'color_scheme.dart';
+import 'colors.dart';
+import 'constants.dart';
+import 'icons.dart';
+import 'ink_well.dart';
+import 'material.dart';
+import 'material_localizations.dart';
+import 'material_state.dart';
+import 'menu_bar_theme.dart';
+import 'menu_button_theme.dart';
+import 'menu_style.dart';
+import 'menu_theme.dart';
+import 'text_button.dart';
+import 'theme.dart';
+import 'theme_data.dart';
+
+// Enable if you want verbose logging about menu changes.
+const bool _kDebugMenus = false;
+
+// The default size of the arrow in _MenuItemLabel that indicates that a menu
+// has a submenu.
+const double _kDefaultSubmenuIconSize = 24;
+
+// The default spacing between the the leading icon, label, trailing icon, and
+// shortcut label in a _MenuItemLabel.
+const double _kLabelItemDefaultSpacing = 18;
+
+// The minimum spacing between the the leading icon, label, trailing icon, and
+// shortcut label in a _MenuItemLabel.
+const double _kLabelItemMinSpacing = 4;
+
+// Navigation shortcuts that we need to make sure are active when menus are
+// open.
+const Map<ShortcutActivator, Intent> _kMenuTraversalShortcuts = <ShortcutActivator, Intent>{
+  SingleActivator(LogicalKeyboardKey.gameButtonA): ActivateIntent(),
+  SingleActivator(LogicalKeyboardKey.escape): DismissIntent(),
+  SingleActivator(LogicalKeyboardKey.tab): NextFocusIntent(),
+  SingleActivator(LogicalKeyboardKey.tab, shift: true): PreviousFocusIntent(),
+  SingleActivator(LogicalKeyboardKey.arrowDown): DirectionalFocusIntent(TraversalDirection.down),
+  SingleActivator(LogicalKeyboardKey.arrowUp): DirectionalFocusIntent(TraversalDirection.up),
+  SingleActivator(LogicalKeyboardKey.arrowLeft): DirectionalFocusIntent(TraversalDirection.left),
+  SingleActivator(LogicalKeyboardKey.arrowRight): DirectionalFocusIntent(TraversalDirection.right),
+};
+
+// The minimum vertical spacing on the outside of menus.
+const double _kMenuVerticalMinPadding = 4;
+
+// How close to the edge of the safe area the menu will be placed.
+const double _kMenuViewPadding = 8;
+
+// The minimum horizontal spacing on the outside of the top level menu.
+const double _kTopLevelMenuHorizontalMinPadding = 4;
+
+/// The type of builder function used by [MenuAnchor.builder] to build the
+/// widget that the [MenuAnchor] surrounds.
+///
+/// The `context` is the context that the widget is being built in.
+///
+/// The `controller` is the [MenuController] that can be used to open and close
+/// the menu with.
+///
+/// The `child` is an optional child supplied as the [MenuAnchor.child]
+/// attribute. The child is intended to be incorporated in the result of the
+/// function.
+typedef MenuAnchorChildBuilder = Widget Function(
+  BuildContext context,
+  MenuController controller,
+  Widget? child,
+);
+
+/// A widget used to mark the "anchor" for a set of submenus, defining the
+/// rectangle used to position the menu, which can be done either with an
+/// explicit location, or with an alignment.
+///
+/// When creating a menu with [MenuBar] or a [SubmenuButton], a [MenuAnchor] is
+/// not needed, since they provide their own internally.
+///
+/// The [MenuAnchor] is meant to be a slightly lower level interface than
+/// [MenuBar], used in situations where a [MenuBar] isn't appropriate, or to
+/// construct widgets or screen regions that have submenus.
+///
+/// {@tool dartpad}
+/// This example shows how to use a [MenuAnchor] to wrap a button and open a
+/// cascading menu from the button.
+///
+/// ** See code in examples/api/lib/material/menu_anchor/menu_anchor.0.dart **
+/// {@end-tool}
+///
+/// {@tool dartpad}
+/// This example shows how to use a [MenuAnchor] to create a cascading context
+/// menu in a region of the view, positioned where the user clicks the mouse
+/// with Ctrl pressed. The [anchorTapClosesMenu] attribute is set to true so
+/// that clicks on the [MenuAnchor] area will cause the menus to be closed.
+///
+/// ** See code in examples/api/lib/material/menu_anchor/menu_anchor.1.dart **
+/// {@end-tool}
+class MenuAnchor extends StatefulWidget {
+  /// Creates a const [MenuAnchor].
+  ///
+  /// The [menuChildren] argument is required.
+  const MenuAnchor({
+    super.key,
+    this.controller,
+    this.childFocusNode,
+    this.style,
+    this.alignmentOffset = Offset.zero,
+    this.clipBehavior = Clip.none,
+    this.anchorTapClosesMenu = false,
+    this.onOpen,
+    this.onClose,
+    required this.menuChildren,
+    this.builder,
+    this.child,
+  });
+
+  /// An optional controller that allows opening and closing of the menu from
+  /// other widgets.
+  final MenuController? controller;
+
+  /// The [childFocusNode] attribute is the optional [FocusNode] also associated
+  /// the [child] or [builder] widget that opens the menu.
+  ///
+  /// The focus node should be attached to the widget that should receive focus
+  /// if keyboard focus traversal moves the focus off of the submenu with the
+  /// arrow keys.
+  ///
+  /// If not supplied, then keyboard traversal from the menu back to the
+  /// controlling button when the menu is open is disabled.
+  final FocusNode? childFocusNode;
+
+  /// The [MenuStyle] that defines the visual attributes of the menu bar.
+  ///
+  /// Colors and sizing of the menus is controllable via the [MenuStyle].
+  ///
+  /// Defaults to the ambient [MenuThemeData.style].
+  final MenuStyle? style;
+
+  /// The offset of the menu relative to the alignment origin determined by
+  /// [MenuStyle.alignment] on the [style] attribute and the ambient
+  /// [Directionality].
+  ///
+  /// Use this for adjustments of the menu placement.
+  ///
+  /// Increasing [Offset.dy] values of [alignmentOffset] move the menu position
+  /// down.
+  ///
+  /// If the [MenuStyle.alignment] from [style] is not an [AlignmentDirectional]
+  /// (e.g. [Alignment]), then increasing [Offset.dx] values of
+  /// [alignmentOffset] move the menu position to the right.
+  ///
+  /// If the [MenuStyle.alignment] from [style] is an [AlignmentDirectional],
+  /// then in a [TextDirection.ltr] [Directionality], increasing [Offset.dx]
+  /// values of [alignmentOffset] move the menu position to the right. In a
+  /// [TextDirection.rtl] directionality, increasing [Offset.dx] values of
+  /// [alignmentOffset] move the menu position to the left.
+  ///
+  /// Defaults to [Offset.zero].
+  final Offset? alignmentOffset;
+
+  /// {@macro flutter.material.Material.clipBehavior}
+  ///
+  /// Defaults to [Clip.none].
+  final Clip clipBehavior;
+
+  /// Whether the menus will be closed if the anchor area is tapped.
+  ///
+  /// For menus opened by buttons that toggle the menu, if the button is tapped
+  /// when the menu is open, the button should close the menu. But if
+  /// [anchorTapClosesMenu] is true, then the menu will close, and
+  /// (surprisingly) immediately re-open. This is because tapping on the button
+  /// closes the menu before the `onPressed` or `onTap` handler is called
+  /// because of it being considered to be "outside" the menu system, and then
+  /// the button (seeing that the menu is closed) immediately reopens the menu.
+  /// The result is that the user thinks that tapping on the button does
+  /// nothing. So, for button-initiated menus, this value is typically false so
+  /// that the menu anchor area is considered "inside" of the menu system and
+  /// doesn't cause it to close unless [MenuController.close] is called.
+  ///
+  /// For menus that are positioned using [MenuController.open]'s `position`
+  /// parameter, it is often desirable that clicking on the anchor always closes
+  /// the menu since the anchor area isn't usually considered part of the menu
+  /// system by the user. In this case [anchorTapClosesMenu] should be true.
+  ///
+  /// Defaults to false.
+  final bool anchorTapClosesMenu;
+
+  /// A callback that is invoked when the menu is opened.
+  final VoidCallback? onOpen;
+
+  /// A callback that is invoked when the menu is closed.
+  final VoidCallback? onClose;
+
+  /// A list of children containing the menu items that are the contents of the
+  /// menu surrounded by this [MenuAnchor].
+  ///
+  /// {@macro flutter.material.menu_bar.shortcuts_note}
+  final List<Widget> menuChildren;
+
+  /// The widget that this [MenuAnchor] surrounds.
+  ///
+  /// Typically this is a button used to open the menu by calling
+  /// [MenuController.open] on the `controller` passed to the builder.
+  ///
+  /// If not supplied, then the [MenuAnchor] will be the size that its parent
+  /// allocates for it.
+  final MenuAnchorChildBuilder? builder;
+
+  /// The optional child to be passed to the [builder].
+  ///
+  /// Supply this child if there is a portion of the widget tree built in
+  /// [builder] that doesn't depend on the `controller` or `context` supplied to
+  /// the [builder]. It will be more efficient, since Flutter doesn't then need
+  /// to rebuild this child when those change.
+  final Widget? child;
+
+  @override
+  State<MenuAnchor> createState() => _MenuAnchorState();
+
+  @override
+  List<DiagnosticsNode> debugDescribeChildren() {
+    return menuChildren.map<DiagnosticsNode>((Widget child) => child.toDiagnosticsNode()).toList();
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(FlagProperty('anchorTapClosesMenu', value: anchorTapClosesMenu, ifTrue: 'AUTO-CLOSE'));
+    properties.add(DiagnosticsProperty<FocusNode?>('focusNode', childFocusNode));
+    properties.add(DiagnosticsProperty<MenuStyle?>('style', style));
+    properties.add(EnumProperty<Clip>('clipBehavior', clipBehavior));
+    properties.add(DiagnosticsProperty<Offset?>('alignmentOffset', alignmentOffset));
+    properties.add(StringProperty('child', child.toString()));
+  }
+}
+
+class _MenuAnchorState extends State<MenuAnchor> {
+  // This is the global key that is used later to determine the bounding rect
+  // for the anchor's region that the CustomSingleChildLayout's delegate
+  // uses to determine where to place the menu on the screen and to avoid the
+  // view's edges.
+  final GlobalKey _anchorKey = GlobalKey(debugLabel: kReleaseMode ? null : 'MenuAnchor');
+  _MenuAnchorState? _parent;
+  bool _childIsOpen = false;
+  final FocusScopeNode _menuScopeNode = FocusScopeNode(debugLabel: kReleaseMode ? null : 'MenuAnchor sub menu');
+  MenuController? _internalMenuController;
+  final List<_MenuAnchorState> _anchorChildren = <_MenuAnchorState>[];
+  ScrollPosition? _position;
+  Size? _viewSize;
+  OverlayEntry? _overlayEntry;
+  Axis get _orientation => Axis.vertical;
+  bool get _isOpen => _overlayEntry != null;
+  bool get _isRoot => _parent == null;
+  bool get _isTopLevel => _parent?._isRoot ?? false;
+  MenuController get _menuController => widget.controller ?? _internalMenuController!;
+
+  @override
+  void initState() {
+    super.initState();
+    if (widget.controller == null) {
+      _internalMenuController = MenuController();
+    }
+    _menuController._attach(this);
+  }
+
+  @override
+  void dispose() {
+    assert(_debugMenuInfo('Disposing of $this'));
+    if (_isOpen) {
+      _close(inDispose: true);
+      _parent?._removeChild(this);
+    }
+    _anchorChildren.clear();
+    _menuController._detach(this);
+    _internalMenuController = null;
+    super.dispose();
+  }
+
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+    _parent?._removeChild(this);
+    _parent = _MenuAnchorState._maybeOf(context);
+    _parent?._addChild(this);
+    _position?.isScrollingNotifier.removeListener(_handleScroll);
+    _position = Scrollable.of(context)?.position;
+    _position?.isScrollingNotifier.addListener(_handleScroll);
+    final Size newSize = MediaQuery.of(context).size;
+    if (_viewSize != null && newSize != _viewSize) {
+      // Close the menus if the view changes size.
+      _root._close();
+    }
+    _viewSize = newSize;
+  }
+
+  @override
+  void didUpdateWidget(MenuAnchor oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (oldWidget.controller != widget.controller) {
+      oldWidget.controller?._detach(this);
+      if (widget.controller != null) {
+        _internalMenuController?._detach(this);
+        _internalMenuController = null;
+        widget.controller?._attach(this);
+      } else {
+        assert(_internalMenuController == null);
+        _internalMenuController = MenuController().._attach(this);
+      }
+    }
+    assert(_menuController._anchor == this);
+    if (_overlayEntry != null) {
+      // Needs to update the overlay entry on the next frame, since it's in the
+      // overlay.
+      SchedulerBinding.instance.addPostFrameCallback((Duration _) {
+        _overlayEntry!.markNeedsBuild();
+      });
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    Widget child = _buildContents(context);
+
+    if (!widget.anchorTapClosesMenu) {
+      child = TapRegion(
+        groupId: _root,
+        onTapOutside: (PointerDownEvent event) {
+          assert(_debugMenuInfo('Tapped Outside ${widget.controller}'));
+          _closeChildren();
+        },
+        child: child,
+      );
+    }
+
+    return _MenuAnchorMarker(
+      anchorKey: _anchorKey,
+      anchor: this,
+      child: child,
+    );
+  }
+
+  Widget _buildContents(BuildContext context) {
+    return Builder(
+      key: _anchorKey,
+      builder: (BuildContext context) {
+        if (widget.builder == null) {
+          return widget.child ?? const SizedBox();
+        }
+        return widget.builder!(
+          context,
+          _menuController,
+          widget.child,
+        );
+      },
+    );
+  }
+
+  // Returns the first focusable item in the submenu, where "first" is
+  // determined by the focus traversal policy.
+  FocusNode? get _firstItemFocusNode {
+    if (_menuScopeNode.context == null) {
+      return null;
+    }
+    final FocusTraversalPolicy policy =
+        FocusTraversalGroup.maybeOf(_menuScopeNode.context!) ?? ReadingOrderTraversalPolicy();
+    return policy.findFirstFocus(_menuScopeNode, ignoreCurrentFocus: true);
+  }
+
+  void _addChild(_MenuAnchorState child) {
+    assert(_isRoot || _debugMenuInfo('Added root child: $child'));
+    assert(!_anchorChildren.contains(child));
+    _anchorChildren.add(child);
+    assert(_debugMenuInfo('Tree:\n${widget.toStringDeep()}'));
+  }
+
+  void _removeChild(_MenuAnchorState child) {
+    assert(_isRoot || _debugMenuInfo('Removed root child: $child'));
+    assert(_anchorChildren.contains(child));
+    _anchorChildren.remove(child);
+    assert(_debugMenuInfo('Tree:\n${widget.toStringDeep()}'));
+  }
+
+  _MenuAnchorState? get _nextSibling {
+    final int index = _parent!._anchorChildren.indexOf(this);
+    assert(index != -1, 'Unable to find this widget $this in parent $_parent');
+    if (index < _parent!._anchorChildren.length - 1) {
+      return _parent!._anchorChildren[index + 1];
+    }
+    return null;
+  }
+
+  _MenuAnchorState? get _previousSibling {
+    final int index = _parent!._anchorChildren.indexOf(this);
+    assert(index != -1, 'Unable to find this widget $this in parent $_parent');
+    if (index > 0) {
+      return _parent!._anchorChildren[index - 1];
+    }
+    return null;
+  }
+
+  _MenuAnchorState get _root {
+    _MenuAnchorState anchor = this;
+    while (anchor._parent != null) {
+      anchor = anchor._parent!;
+    }
+    return anchor;
+  }
+
+  _MenuAnchorState get _topLevel {
+    _MenuAnchorState handle = this;
+    while (handle._parent!._isTopLevel) {
+      handle = handle._parent!;
+    }
+    return handle;
+  }
+
+  void _childChangedOpenState(bool value) {
+    if (_childIsOpen != value) {
+      _parent?._childChangedOpenState(_childIsOpen || _isOpen);
+      if (mounted) {
+        setState(() {
+          _childIsOpen = value;
+        });
+      }
+    }
+  }
+
+  void _focusButton() {
+    if (widget.childFocusNode == null) {
+      return;
+    }
+    assert(_debugMenuInfo('Requesting focus for ${widget.childFocusNode}'));
+    widget.childFocusNode!.requestFocus();
+  }
+
+  void _handleScroll() {
+    // If an ancestor scrolls, and we're a top level or root anchor, then close
+    // the menus. Don't just close it on *any* scroll, since we want to be able
+    // to scroll menus themselves if they're too big for the view.
+    if (_isTopLevel || _isRoot) {
+      _root._close();
+    }
+  }
+
+  /// Open the menu, optionally at a position relative to the [MenuAnchor].
+  ///
+  /// Call this when the menu should be shown to the user.
+  ///
+  /// The optional `position` argument will specify the location of the menu in
+  /// the local coordinates of the [MenuAnchor], ignoring any
+  /// [MenuStyle.alignment] and/or [MenuAnchor.alignmentOffset] that were
+  /// specified.
+  void _open({Offset? position}) {
+    assert(_menuController._anchor == this);
+    if (_isOpen && position == null) {
+      assert(_debugMenuInfo("Not opening $this because it's already open"));
+      return;
+    }
+    if (_isOpen && position != null) {
+      // The menu is already open, but we need to move to another location, so
+      // close it first.
+      _close();
+    }
+    assert(_debugMenuInfo('Opening ${this} at ${position ?? Offset.zero} with alignment offset ${widget.alignmentOffset ?? Offset.zero}'));
+    _parent?._closeChildren(); // Close all siblings.
+    assert(_overlayEntry == null);
+
+    final BuildContext outerContext = context;
+    setState(() {
+      _parent?._childChangedOpenState(true);
+      _overlayEntry = OverlayEntry(
+        builder: (BuildContext context) {
+          final OverlayState overlay = Overlay.of(outerContext);
+          return Positioned.directional(
+            textDirection: Directionality.of(outerContext),
+            top: 0,
+            start: 0,
+            child: Directionality(
+              textDirection: Directionality.of(outerContext),
+              child: InheritedTheme.captureAll(
+                // Copy all the themes from the supplied outer context to the
+                // overlay.
+                outerContext,
+                _MenuAnchorMarker(
+                  // Re-advertize the anchor here in the overlay, since
+                  // otherwise a search for the anchor by descendants won't find
+                  // it.
+                  anchorKey: _anchorKey,
+                  anchor: this,
+                  child: _Submenu(
+                    anchor: this,
+                    menuStyle: widget.style,
+                    alignmentOffset: widget.alignmentOffset ?? Offset.zero,
+                    menuPosition: position,
+                    clipBehavior: widget.clipBehavior,
+                    menuChildren: widget.menuChildren,
+                  ),
+                ),
+                to: overlay.context,
+              ),
+            ),
+          );
+        },
+      );
+    });
+
+    Overlay.of(context).insert(_overlayEntry!);
+    widget.onOpen?.call();
+  }
+
+  /// Close the menu.
+  ///
+  /// Call this when the menu should be closed. Has no effect if the menu is
+  /// already closed.
+  void _close({bool inDispose = false}) {
+    assert(_debugMenuInfo('Closing $this'));
+    if (!_isOpen) {
+      return;
+    }
+    _closeChildren(inDispose: inDispose);
+    _overlayEntry?.remove();
+    _overlayEntry = null;
+    if (!inDispose && mounted) {
+      setState(() {
+        // Notify that _isOpen may have changed state, but only if not currently
+        // disposing or unmounted.
+        _parent?._childChangedOpenState(false);
+      });
+    }
+    widget.onClose?.call();
+  }
+
+  void _closeChildren({bool inDispose = false}) {
+    assert(_debugMenuInfo('Closing children of ${this}${inDispose ? ' (dispose)' : ''}'));
+    for (final _MenuAnchorState child in List<_MenuAnchorState>.from(_anchorChildren)) {
+      child._close(inDispose: inDispose);
+    }
+  }
+
+  // Returns the active anchor in the given context, if any, and creates a
+  // dependency relationship that will rebuild the context when the node
+  // changes.
+  static _MenuAnchorState? _maybeOf(BuildContext context) {
+    return context.dependOnInheritedWidgetOfExactType<_MenuAnchorMarker>()?.anchor;
+  }
+}
+
+/// A controller to manage a menu created by a [MenuBar] or [MenuAnchor].
+///
+/// A [MenuController] is used to control and interrogate a menu after it has
+/// been created, with methods such as [open] and [close], and state accessors
+/// like [isOpen].
+///
+/// See also:
+///
+/// * [MenuAnchor], a widget that defines a region that has submenu.
+/// * [MenuBar], a widget that creates a menu bar, that can take an optional
+///   [MenuController].
+/// * [SubmenuButton], a widget that has a button that manages a submenu.
+class MenuController {
+  /// The anchor that this controller controls.
+  ///
+  /// This is set automatically when a [MenuController] is given to the anchor
+  /// it controls.
+  _MenuAnchorState? _anchor;
+
+  /// Whether or not the associated menu is currently open.
+  bool get isOpen {
+    assert(_anchor != null);
+    return _anchor!._isOpen;
+  }
+
+  /// Close the menu that this menu controller is associated with.
+  ///
+  /// Associating with a menu is done by passing a [MenuController] to a
+  /// [MenuAnchor]. A [MenuController] is also be received by the
+  /// [MenuAnchor.builder] when invoked.
+  ///
+  /// If the menu's anchor point (either a [MenuBar] or a [MenuAnchor]) is
+  /// scrolled by an ancestor, or the view changes size, then any open menu will
+  /// automatically close.
+  void close() {
+    assert(_anchor != null);
+    _anchor!._close();
+  }
+
+  /// Opens the menu that this menu controller is associated with.
+  ///
+  /// If `position` is given, then the menu will open at the position given, in
+  /// the coordinate space of the [MenuAnchor] this controller is attached to.
+  ///
+  /// If given, the `position` will override the [MenuAnchor.alignmentOffset]
+  /// given to the [MenuAnchor].
+  ///
+  /// If the menu's anchor point (either a [MenuBar] or a [MenuAnchor]) is
+  /// scrolled by an ancestor, or the view changes size, then any open menu will
+  /// automatically close.
+  void open({Offset? position}) {
+    assert(_anchor != null);
+    _anchor!._open(position: position);
+  }
+
+  // ignore: use_setters_to_change_properties
+  void _attach(_MenuAnchorState anchor) {
+    _anchor = anchor;
+  }
+
+  void _detach(_MenuAnchorState anchor) {
+    if (_anchor == anchor) {
+      _anchor = null;
+    }
+  }
+}
+
+/// A menu bar that manages cascading child menus.
+///
+/// This is a Material Design menu bar that typically resides above the main
+/// body of an application (but can go anywhere) that defines a menu system for
+/// invoking callbacks in response to user selection of a menu item.
+///
+/// The menus can be opened with a click or tap. Once a menu is opened, it can
+/// be navigated by using the arrow and tab keys or via mouse hover. Selecting a
+/// menu item can be done by pressing enter, or by clicking or tapping on the
+/// menu item. Clicking or tapping on any part of the user interface that isn't
+/// part of the menu system controlled by the same controller will cause all of
+/// the menus controlled by that controller to close, as will pressing the
+/// escape key.
+///
+/// When a menu item with a submenu is clicked on, it toggles the visibility of
+/// the submenu. When the menu item is hovered over, the submenu will open, and
+/// hovering over other items will close the previous menu and open the newly
+/// hovered one. When those open/close transitions occur, [SubmenuButton.onOpen],
+/// and [SubmenuButton.onClose] are called on the corresponding [SubmenuButton] child
+/// of the menu bar.
+///
+/// {@template flutter.material.menu_bar.shortcuts_note}
+/// Menus using [MenuItemButton] can have a [SingleActivator] or
+/// [CharacterActivator] assigned to them as their [MenuItemButton.shortcut],
+/// which will display an appropriate shortcut hint. Even though the shortcut
+/// labels are displayed in the menu, shortcuts are not automatically handled.
+/// They must be available in whatever context they are appropriate, and handled
+/// via another mechanism.
+///
+/// If shortcuts should be generally enabled, but are not easily defined in a
+/// context surrounding the menu bar, consider registering them with a
+/// [ShortcutRegistry] (one is already included in the [WidgetsApp], and thus
+/// also [MaterialApp] and [CupertinoApp]), as shown in the example below. To be
+/// sure that selecting a menu item and triggering the shortcut do the same
+/// thing, it is recommended that they call the same callback.
+///
+/// {@tool dartpad}
+/// This example shows a [MenuBar] that contains a single top level menu,
+/// containing three items: "About", a checkbox menu item for showing a
+/// message, and "Quit". The items are identified with an enum value, and the
+/// shortcuts are registered globally with the [ShortcutRegistry].
+///
+/// ** See code in examples/api/lib/material/menu_anchor/menu_bar.0.dart **
+/// {@end-tool}
+/// {@endtemplate}
+///
+/// See also:
+///
+/// * [MenuAnchor], a widget that creates a region with a submenu and shows it
+///   when requested.
+/// * [SubmenuButton], a menu item which manages a submenu.
+/// * [MenuItemButton], a leaf menu item which displays the label, an optional
+///   shortcut label, and optional leading and trailing icons.
+/// * [PlatformMenuBar], which creates a menu bar that is rendered by the host
+///   platform instead of by Flutter (on macOS, for example).
+/// * [ShortcutRegistry], a registry of shortcuts that apply for the entire
+///   application.
+/// * [VoidCallbackIntent] to define intents that will call a [VoidCallback] and
+///   work with the [Actions] and [Shortcuts] system.
+/// * [CallbackShortcuts] to define shortcuts that simply call a callback and
+///   don't involve using [Actions].
+class MenuBar extends StatelessWidget {
+  /// Creates a const [MenuBar].
+  ///
+  /// The [children] argument is required.
+  const MenuBar({
+    super.key,
+    this.style,
+    this.clipBehavior = Clip.none,
+    this.controller,
+    required this.children,
+  });
+
+  /// The [MenuStyle] that defines the visual attributes of the menu bar.
+  ///
+  /// Colors and sizing of the menus is controllable via the [MenuStyle].
+  ///
+  /// Defaults to the ambient [MenuThemeData.style].
+  final MenuStyle? style;
+
+  /// {@macro flutter.material.Material.clipBehavior}
+  ///
+  /// Defaults to [Clip.none].
+  final Clip clipBehavior;
+
+  /// The [MenuController] to use for this menu bar.
+  final MenuController? controller;
+
+  /// The list of menu items that are the top level children of the [MenuBar].
+  ///
+  /// A Widget in Flutter is immutable, so directly modifying the [children]
+  /// with [List] APIs such as `someMenuBarWidget.menus.add(...)` will result in
+  /// incorrect behaviors. Whenever the menus list is modified, a new list
+  /// object must be provided.
+  ///
+  /// {@macro flutter.material.menu_bar.shortcuts_note}
+  final List<Widget> children;
+
+  @override
+  Widget build(BuildContext context) {
+    assert(debugCheckHasOverlay(context));
+    return _MenuBarAnchor(
+      controller: controller,
+      clipBehavior: clipBehavior,
+      style: style,
+      menuChildren: children,
+    );
+  }
+
+  @override
+  List<DiagnosticsNode> debugDescribeChildren() {
+    return <DiagnosticsNode>[
+      ...children.map<DiagnosticsNode>(
+            (Widget item) => item.toDiagnosticsNode(),
+      ),
+    ];
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<MenuStyle?>('style', style, defaultValue: null));
+    properties.add(DiagnosticsProperty<Clip>('clipBehavior', clipBehavior, defaultValue: null));
+  }
+}
+
+/// A button for use in a [MenuBar], in a menu created with [MenuAnchor], or on
+/// its own, that can be activated by click or keyboard navigation.
+///
+/// This widget represents a leaf entry in a menu hierarchy that is typically
+/// part of a [MenuBar], but may be used independently, or as part of a menu
+/// created with a [MenuAnchor].
+///
+/// {@macro flutter.material.menu_bar.shortcuts_note}
+///
+/// See also:
+///
+/// * [MenuBar], a class that creates a top level menu bar in a Material Design
+///   style.
+/// * [MenuAnchor], a widget that creates a region with a submenu and shows it
+///   when requested.
+/// * [SubmenuButton], a menu item similar to this one which manages a submenu.
+/// * [PlatformMenuBar], which creates a menu bar that is rendered by the host
+///   platform instead of by Flutter (on macOS, for example).
+/// * [ShortcutRegistry], a registry of shortcuts that apply for the entire
+///   application.
+/// * [VoidCallbackIntent] to define intents that will call a [VoidCallback] and
+///   work with the [Actions] and [Shortcuts] system.
+/// * [CallbackShortcuts] to define shortcuts that simply call a callback and
+///   don't involve using [Actions].
+class MenuItemButton extends StatefulWidget {
+  /// Creates a const [MenuItemButton].
+  ///
+  /// The [child] attribute is required.
+  const MenuItemButton({
+    super.key,
+    this.onPressed,
+    this.onHover,
+    this.onFocusChange,
+    this.focusNode,
+    this.shortcut,
+    this.style,
+    this.statesController,
+    this.clipBehavior = Clip.none,
+    this.leadingIcon,
+    this.trailingIcon,
+    required this.child,
+  });
+
+  /// Called when the button is tapped or otherwise activated.
+  ///
+  /// If this callback is null, then the button will be disabled.
+  ///
+  /// See also:
+  ///
+  ///  * [enabled], which is true if the button is enabled.
+  final VoidCallback? onPressed;
+
+  /// Called when a pointer enters or exits the button response area.
+  ///
+  /// The value passed to the callback is true if a pointer has entered button
+  /// area and false if a pointer has exited.
+  final ValueChanged<bool>? onHover;
+
+  /// Handler called when the focus changes.
+  ///
+  /// Called with true if this widget's node gains focus, and false if it loses
+  /// focus.
+  final ValueChanged<bool>? onFocusChange;
+
+  /// {@macro flutter.widgets.Focus.focusNode}
+  final FocusNode? focusNode;
+
+  /// The optional shortcut that selects this [MenuItemButton].
+  ///
+  /// {@macro flutter.material.menu_bar.shortcuts_note}
+  final MenuSerializableShortcut? shortcut;
+
+  /// Customizes this button's appearance.
+  ///
+  /// Non-null properties of this style override the corresponding properties in
+  /// [themeStyleOf] and [defaultStyleOf]. [MaterialStateProperty]s that resolve
+  /// to non-null values will similarly override the corresponding
+  /// [MaterialStateProperty]s in [themeStyleOf] and [defaultStyleOf].
+  ///
+  /// Null by default.
+  final ButtonStyle? style;
+
+  /// {@macro flutter.material.inkwell.statesController}
+  final MaterialStatesController? statesController;
+
+  /// {@macro flutter.material.Material.clipBehavior}
+  ///
+  /// Defaults to [Clip.none].
+  final Clip clipBehavior;
+
+  /// An optional icon to display before the [child] label.
+  final Widget? leadingIcon;
+
+  /// An optional icon to display after the [child] label.
+  final Widget? trailingIcon;
+
+  /// The widget displayed in the center of this button.
+  ///
+  /// Typically this is the button's label, using a [Text] widget.
+  ///
+  /// {@macro flutter.widgets.ProxyWidget.child}
+  final Widget? child;
+
+  /// Whether the button is enabled or disabled.
+  ///
+  /// To enable a button, set its [onPressed] property to a non-null value.
+  bool get enabled => onPressed != null;
+
+  @override
+  State<MenuItemButton> createState() => _MenuItemButtonState();
+
+  /// Defines the button's default appearance.
+  ///
+  /// {@macro flutter.material.text_button.default_style_of}
+  ///
+  /// {@macro flutter.material.text_button.material3_defaults}
+  ButtonStyle defaultStyleOf(BuildContext context) {
+    return _MenuButtonDefaultsM3(context);
+  }
+
+  /// Returns the [MenuButtonThemeData.style] of the closest
+  /// [MenuButtonTheme] ancestor.
+  ButtonStyle? themeStyleOf(BuildContext context) {
+    return MenuButtonTheme.of(context).style;
+  }
+
+  /// A static convenience method that constructs a [MenuItemButton]'s
+  /// [ButtonStyle] given simple values.
+  ///
+  /// The [foregroundColor] color is used to create a [MaterialStateProperty]
+  /// [ButtonStyle.foregroundColor] value. Specify a value for [foregroundColor]
+  /// to specify the color of the button's icons. Use [backgroundColor] for the
+  /// button's background fill color. Use [disabledForegroundColor] and
+  /// [disabledBackgroundColor] to specify the button's disabled icon and fill
+  /// color.
+  ///
+  /// All of the other parameters are either used directly or used to create a
+  /// [MaterialStateProperty] with a single value for all states.
+  ///
+  /// All parameters default to null, by default this method returns a
+  /// [ButtonStyle] that doesn't override anything.
+  ///
+  /// For example, to override the default foreground color for a
+  /// [MenuItemButton], as well as its overlay color, with all of the standard
+  /// opacity adjustments for the pressed, focused, and hovered states, one
+  /// could write:
+  ///
+  /// ```dart
+  /// MenuItemButton(
+  ///   leadingIcon: const Icon(Icons.pets),
+  ///   style: MenuItemButton.styleFrom(foregroundColor: Colors.green),
+  ///   onPressed: () {
+  ///     // ...
+  ///   },
+  ///   child: const Text('Button Label'),
+  /// ),
+  /// ```
+  static ButtonStyle styleFrom({
+    Color? foregroundColor,
+    Color? backgroundColor,
+    Color? disabledForegroundColor,
+    Color? disabledBackgroundColor,
+    Color? shadowColor,
+    Color? surfaceTintColor,
+    TextStyle? textStyle,
+    double? elevation,
+    EdgeInsetsGeometry? padding,
+    Size? minimumSize,
+    Size? fixedSize,
+    Size? maximumSize,
+    MouseCursor? enabledMouseCursor,
+    MouseCursor? disabledMouseCursor,
+    BorderSide? side,
+    OutlinedBorder? shape,
+    VisualDensity? visualDensity,
+    MaterialTapTargetSize? tapTargetSize,
+    Duration? animationDuration,
+    bool? enableFeedback,
+    AlignmentGeometry? alignment,
+    InteractiveInkFeatureFactory? splashFactory,
+  }) {
+    return TextButton.styleFrom(
+      foregroundColor: foregroundColor,
+      backgroundColor: backgroundColor,
+      disabledBackgroundColor: disabledBackgroundColor,
+      disabledForegroundColor: disabledForegroundColor,
+      shadowColor: shadowColor,
+      surfaceTintColor: surfaceTintColor,
+      textStyle: textStyle,
+      elevation: elevation,
+      padding: padding,
+      minimumSize: minimumSize,
+      fixedSize: fixedSize,
+      maximumSize: maximumSize,
+      enabledMouseCursor: enabledMouseCursor,
+      disabledMouseCursor: disabledMouseCursor,
+      side: side,
+      shape: shape,
+      visualDensity: visualDensity,
+      tapTargetSize: tapTargetSize,
+      animationDuration: animationDuration,
+      enableFeedback: enableFeedback,
+      alignment: alignment,
+      splashFactory: splashFactory,
+    );
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(FlagProperty('enabled', value: onPressed != null, ifFalse: 'DISABLED'));
+    properties.add(DiagnosticsProperty<String>('child', child.toString()));
+    properties.add(DiagnosticsProperty<ButtonStyle?>('style', style, defaultValue: null));
+    properties.add(DiagnosticsProperty<MenuSerializableShortcut?>('shortcut', shortcut, defaultValue: null));
+    properties.add(DiagnosticsProperty<Widget?>('leadingIcon', leadingIcon, defaultValue: null));
+    properties.add(DiagnosticsProperty<Widget?>('trailingIcon', trailingIcon, defaultValue: null));
+    properties.add(DiagnosticsProperty<FocusNode?>('focusNode', focusNode, defaultValue: null));
+    properties.add(EnumProperty<Clip>('clipBehavior', clipBehavior, defaultValue: Clip.none));
+    properties.add(DiagnosticsProperty<MaterialStatesController?>('statesController', statesController, defaultValue: null));
+  }
+}
+
+class _MenuItemButtonState extends State<MenuItemButton> {
+  // If a focus node isn't given to the widget, then we have to manage our own.
+  FocusNode? _internalFocusNode;
+  FocusNode get _focusNode => widget.focusNode ?? _internalFocusNode!;
+
+  @override
+  void initState() {
+    super.initState();
+    _createInternalFocusNodeIfNeeded();
+    _focusNode.addListener(_handleFocusChange);
+  }
+
+  @override
+  void dispose() {
+    _focusNode.removeListener(_handleFocusChange);
+    _internalFocusNode?.dispose();
+    _internalFocusNode = null;
+    super.dispose();
+  }
+
+  @override
+  void didUpdateWidget(MenuItemButton oldWidget) {
+    if (widget.focusNode != oldWidget.focusNode) {
+      _focusNode.removeListener(_handleFocusChange);
+      if (widget.focusNode != null) {
+        _internalFocusNode?.dispose();
+        _internalFocusNode = null;
+      }
+      _createInternalFocusNodeIfNeeded();
+      _focusNode.addListener(_handleFocusChange);
+    }
+    super.didUpdateWidget(oldWidget);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    // Since we don't want to use the theme style or default style from the
+    // TextButton, we merge the styles, merging them in the right order when
+    // each type of style exists. Each "*StyleOf" function is only called once.
+    final ButtonStyle mergedStyle =
+        widget.style?.merge(widget.themeStyleOf(context)?.merge(widget.defaultStyleOf(context))) ??
+            widget.themeStyleOf(context)?.merge(widget.defaultStyleOf(context)) ??
+            widget.defaultStyleOf(context);
+
+    return TextButton(
+      onPressed: widget.enabled ? _handleSelect : null,
+      onHover: widget.enabled ? _handleHover : null,
+      onFocusChange: widget.enabled ? widget.onFocusChange : null,
+      focusNode: _focusNode,
+      style: mergedStyle,
+      statesController: widget.statesController,
+      clipBehavior: widget.clipBehavior,
+      child: _MenuItemLabel(
+        leadingIcon: widget.leadingIcon,
+        shortcut: widget.shortcut,
+        trailingIcon: widget.trailingIcon,
+        hasSubmenu: false,
+        child: widget.child!,
+      ),
+    );
+  }
+
+  void _handleFocusChange() {
+    if (!_focusNode.hasPrimaryFocus) {
+      // Close any child menus of this button's menu.
+      _MenuAnchorState._maybeOf(context)?._closeChildren();
+    }
+  }
+
+  void _handleHover(bool hovering) {
+    widget.onHover?.call(hovering);
+    if (hovering) {
+      assert(_debugMenuInfo('Requesting focus for $_focusNode from hover'));
+      _focusNode.requestFocus();
+    }
+  }
+
+  void _handleSelect() {
+    assert(_debugMenuInfo('Selected ${widget.child} menu'));
+    widget.onPressed?.call();
+    _MenuAnchorState._maybeOf(context)?._root._close();
+  }
+
+  void _createInternalFocusNodeIfNeeded() {
+    if (widget.focusNode == null) {
+      _internalFocusNode = FocusNode();
+      assert(() {
+        if (_internalFocusNode != null) {
+          _internalFocusNode!.debugLabel = '$MenuItemButton(${widget.child})';
+        }
+        return true;
+      }());
+    }
+  }
+}
+
+/// A menu button that displays a cascading menu.
+///
+/// It can be used as part of a [MenuBar], or as a standalone widget.
+///
+/// This widget represents a menu item that has a submenu. Like the leaf
+/// [MenuItemButton], it shows a label with an optional leading or trailing
+/// icon, but additionally shows an arrow icon showing that it has a submenu.
+///
+/// By default the submenu will appear to the side of the controlling button.
+/// The alignment and offset of the submenu can be controlled by setting
+/// [MenuStyle.alignment] on the [style] and the [alignmentOffset] argument,
+/// respectively.
+///
+/// When activated (by being clicked, through keyboard navigation, or via
+/// hovering with a mouse), it will open a submenu containing the
+/// [menuChildren].
+///
+/// If [menuChildren] is empty, then this menu item will appear disabled.
+///
+/// See also:
+///
+/// * [MenuItemButton], a widget that represents a leaf menu item that does not
+///   host a submenu.
+/// * [MenuBar], a widget that renders menu items in a row in a Material Design
+///   style.
+/// * [MenuAnchor], a widget that creates a region with a submenu and shows it
+///   when requested.
+/// * [PlatformMenuBar], a widget that renders similar menu bar items from a
+///   [PlatformMenuItem] using platform-native APIs instead of Flutter.
+class SubmenuButton extends StatefulWidget {
+  /// Creates a const [SubmenuButton].
+  ///
+  /// The [child] and [menuChildren] attributes are required.
+  const SubmenuButton({
+    super.key,
+    this.onHover,
+    this.onFocusChange,
+    this.onOpen,
+    this.onClose,
+    this.style,
+    this.menuStyle,
+    this.alignmentOffset,
+    this.clipBehavior = Clip.none,
+    this.focusNode,
+    this.statesController,
+    this.leadingIcon,
+    this.trailingIcon,
+    required this.menuChildren,
+    required this.child,
+  });
+
+  /// Called when a pointer enters or exits the button response area.
+  ///
+  /// The value passed to the callback is true if a pointer has entered this
+  /// part of the button and false if a pointer has exited.
+  final ValueChanged<bool>? onHover;
+
+  /// Handler called when the focus changes.
+  ///
+  /// Called with true if this widget's [focusNode] gains focus, and false if it
+  /// loses focus.
+  final ValueChanged<bool>? onFocusChange;
+
+  /// A callback that is invoked when the menu is opened.
+  final VoidCallback? onOpen;
+
+  /// A callback that is invoked when the menu is closed.
+  final VoidCallback? onClose;
+
+  /// Customizes this button's appearance.
+  ///
+  /// Non-null properties of this style override the corresponding properties in
+  /// [themeStyleOf] and [defaultStyleOf]. [MaterialStateProperty]s that resolve
+  /// to non-null values will similarly override the corresponding
+  /// [MaterialStateProperty]s in [themeStyleOf] and [defaultStyleOf].
+  ///
+  /// Null by default.
+  final ButtonStyle? style;
+
+  /// The [MenuStyle] of the menu specified by [menuChildren].
+  ///
+  /// Defaults to the value of [MenuThemeData.style] of the ambient [MenuTheme].
+  final MenuStyle? menuStyle;
+
+  /// The offset of the menu relative to the alignment origin determined by
+  /// [MenuStyle.alignment] on the [style] attribute.
+  ///
+  /// Use this for fine adjustments of the menu placement.
+  ///
+  /// Defaults to an offset that takes into account the padding of the menu so
+  /// that the top starting corner of the first menu item is aligned with the
+  /// top of the [MenuAnchor] region.
+  final Offset? alignmentOffset;
+
+  /// {@macro flutter.material.Material.clipBehavior}
+  ///
+  /// Defaults to [Clip.none].
+  final Clip clipBehavior;
+
+  /// {@macro flutter.widgets.Focus.focusNode}
+  final FocusNode? focusNode;
+
+  /// {@macro flutter.material.inkwell.statesController}
+  final MaterialStatesController? statesController;
+
+  /// An optional icon to display before the [child].
+  final Widget? leadingIcon;
+
+  /// An optional icon to display after the [child].
+  final Widget? trailingIcon;
+
+  /// The list of widgets that appear in the menu when it is opened.
+  ///
+  /// These can be any widget, but are typically either [MenuItemButton] or
+  /// [SubmenuButton] widgets.
+  ///
+  /// If [menuChildren] is empty, then the button for this menu item will be
+  /// disabled.
+  final List<Widget> menuChildren;
+
+  /// The widget displayed in the middle portion of this button.
+  ///
+  /// Typically this is the button's label, using a [Text] widget.
+  ///
+  /// {@macro flutter.widgets.ProxyWidget.child}
+  final Widget? child;
+
+  @override
+  State<SubmenuButton> createState() => _SubmenuButtonState();
+
+  /// Defines the button's default appearance.
+  ///
+  /// {@macro flutter.material.text_button.default_style_of}
+  ///
+  /// {@macro flutter.material.text_button.material3_defaults}
+  ButtonStyle defaultStyleOf(BuildContext context) {
+    return _MenuButtonDefaultsM3(context);
+  }
+
+  /// Returns the [MenuButtonThemeData.style] of the closest [MenuButtonTheme]
+  /// ancestor.
+  ButtonStyle? themeStyleOf(BuildContext context) {
+    return MenuButtonTheme.of(context).style;
+  }
+
+  /// A static convenience method that constructs a [SubmenuButton]'s
+  /// [ButtonStyle] given simple values.
+  ///
+  /// The [foregroundColor] color is used to create a [MaterialStateProperty]
+  /// [ButtonStyle.foregroundColor] value. Specify a value for [foregroundColor]
+  /// to specify the color of the button's icons. Use [backgroundColor] for the
+  /// button's background fill color. Use [disabledForegroundColor] and
+  /// [disabledBackgroundColor] to specify the button's disabled icon and fill
+  /// color.
+  ///
+  /// All of the other parameters are either used directly or used to create a
+  /// [MaterialStateProperty] with a single value for all states.
+  ///
+  /// All parameters default to null, by default this method returns a
+  /// [ButtonStyle] that doesn't override anything.
+  ///
+  /// For example, to override the default foreground color for a
+  /// [SubmenuButton], as well as its overlay color, with all of the standard
+  /// opacity adjustments for the pressed, focused, and hovered states, one
+  /// could write:
+  ///
+  /// ```dart
+  /// SubmenuButton(
+  ///   leadingIcon: const Icon(Icons.pets),
+  ///   style: SubmenuButton.styleFrom(foregroundColor: Colors.green),
+  ///   menuChildren: const <Widget>[ /* ... */ ],
+  ///   child: const Text('Button Label'),
+  /// ),
+  /// ```
+  static ButtonStyle styleFrom({
+    Color? foregroundColor,
+    Color? backgroundColor,
+    Color? disabledForegroundColor,
+    Color? disabledBackgroundColor,
+    Color? shadowColor,
+    Color? surfaceTintColor,
+    TextStyle? textStyle,
+    double? elevation,
+    EdgeInsetsGeometry? padding,
+    Size? minimumSize,
+    Size? fixedSize,
+    Size? maximumSize,
+    MouseCursor? enabledMouseCursor,
+    MouseCursor? disabledMouseCursor,
+    BorderSide? side,
+    OutlinedBorder? shape,
+    VisualDensity? visualDensity,
+    MaterialTapTargetSize? tapTargetSize,
+    Duration? animationDuration,
+    bool? enableFeedback,
+    AlignmentGeometry? alignment,
+    InteractiveInkFeatureFactory? splashFactory,
+  }) {
+    return TextButton.styleFrom(
+      foregroundColor: foregroundColor,
+      backgroundColor: backgroundColor,
+      disabledBackgroundColor: disabledBackgroundColor,
+      disabledForegroundColor: disabledForegroundColor,
+      shadowColor: shadowColor,
+      surfaceTintColor: surfaceTintColor,
+      textStyle: textStyle,
+      elevation: elevation,
+      padding: padding,
+      minimumSize: minimumSize,
+      fixedSize: fixedSize,
+      maximumSize: maximumSize,
+      enabledMouseCursor: enabledMouseCursor,
+      disabledMouseCursor: disabledMouseCursor,
+      side: side,
+      shape: shape,
+      visualDensity: visualDensity,
+      tapTargetSize: tapTargetSize,
+      animationDuration: animationDuration,
+      enableFeedback: enableFeedback,
+      alignment: alignment,
+      splashFactory: splashFactory,
+    );
+  }
+
+  @override
+  List<DiagnosticsNode> debugDescribeChildren() {
+    return <DiagnosticsNode>[
+      ...menuChildren.map<DiagnosticsNode>((Widget child) {
+        return child.toDiagnosticsNode();
+      })
+    ];
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<Widget>('leadingIcon', leadingIcon, defaultValue: null));
+    properties.add(DiagnosticsProperty<String>('child', child.toString()));
+    properties.add(DiagnosticsProperty<Widget>('trailingIcon', trailingIcon, defaultValue: null));
+    properties.add(DiagnosticsProperty<FocusNode?>('focusNode', focusNode));
+    properties.add(DiagnosticsProperty<MenuStyle>('menuStyle', menuStyle, defaultValue: null));
+    properties.add(DiagnosticsProperty<Offset>('alignmentOffset', alignmentOffset));
+    properties.add(EnumProperty<Clip>('clipBehavior', clipBehavior));
+  }
+}
+
+class _SubmenuButtonState extends State<SubmenuButton> {
+  FocusNode? _internalFocusNode;
+  bool _waitingToFocusMenu = false;
+  final MenuController _menuController = MenuController();
+  _MenuAnchorState? get _anchor => _MenuAnchorState._maybeOf(context);
+  FocusNode get _buttonFocusNode => widget.focusNode ?? _internalFocusNode!;
+  bool get _enabled => widget.menuChildren.isNotEmpty;
+
+  @override
+  void initState() {
+    super.initState();
+    if (widget.focusNode == null) {
+      _internalFocusNode = FocusNode();
+      assert(() {
+        if (_internalFocusNode != null) {
+          _internalFocusNode!.debugLabel = '$SubmenuButton(${widget.child})';
+        }
+        return true;
+      }());
+    }
+    _buttonFocusNode.addListener(_handleFocusChange);
+  }
+
+  @override
+  void dispose() {
+    _internalFocusNode?.removeListener(_handleFocusChange);
+    _internalFocusNode?.dispose();
+    _internalFocusNode = null;
+    super.dispose();
+  }
+
+  @override
+  void didUpdateWidget(SubmenuButton oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (widget.focusNode != oldWidget.focusNode) {
+      if (oldWidget.focusNode == null) {
+        _internalFocusNode?.removeListener(_handleFocusChange);
+        _internalFocusNode?.dispose();
+        _internalFocusNode = null;
+      } else {
+        oldWidget.focusNode!.removeListener(_handleFocusChange);
+      }
+      if (widget.focusNode == null) {
+        _internalFocusNode ??= FocusNode();
+        assert(() {
+          if (_internalFocusNode != null) {
+            _internalFocusNode!.debugLabel = '$SubmenuButton(${widget.child})';
+          }
+          return true;
+        }());
+      }
+      _buttonFocusNode.addListener(_handleFocusChange);
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final Offset menuPaddingOffset;
+    final EdgeInsets menuPadding = _computeMenuPadding(context);
+    switch (_anchor?._root._orientation ?? Axis.vertical) {
+      case Axis.horizontal:
+        switch (Directionality.of(context)) {
+          case TextDirection.rtl:
+            menuPaddingOffset = widget.alignmentOffset ?? Offset(-menuPadding.right, 0);
+            break;
+          case TextDirection.ltr:
+            menuPaddingOffset = widget.alignmentOffset ?? Offset(-menuPadding.left, 0);
+            break;
+        }
+        break;
+      case Axis.vertical:
+        menuPaddingOffset = widget.alignmentOffset ?? Offset(0, -menuPadding.top);
+        break;
+    }
+
+    return MenuAnchor(
+      controller: _menuController,
+      childFocusNode: _buttonFocusNode,
+      alignmentOffset: menuPaddingOffset,
+      clipBehavior: widget.clipBehavior,
+      onClose: widget.onClose,
+      onOpen: widget.onOpen,
+      style: widget.menuStyle,
+      builder: (BuildContext context, MenuController controller, Widget? child) {
+        // Since we don't want to use the theme style or default style from the
+        // TextButton, we merge the styles, merging them in the right order when
+        // each type of style exists. Each "*StyleOf" function is only called
+        // once.
+        final ButtonStyle mergedStyle =
+            widget.style?.merge(widget.themeStyleOf(context)?.merge(widget.defaultStyleOf(context))) ??
+                widget.themeStyleOf(context)?.merge(widget.defaultStyleOf(context)) ??
+                widget.defaultStyleOf(context);
+
+        void toggleShowMenu(BuildContext context) {
+          if (controller.isOpen) {
+            controller.close();
+          } else {
+            controller.open();
+            if (!_waitingToFocusMenu) {
+              // Only schedule this if it's not already scheduled.
+              SchedulerBinding.instance.addPostFrameCallback((Duration _) {
+                // This has to happen in the next frame because the menu bar is
+                // not focusable until the first menu is open.
+                controller._anchor?._focusButton();
+                _waitingToFocusMenu = false;
+              });
+              _waitingToFocusMenu = true;
+            }
+          }
+        }
+
+        // Called when the pointer is hovering over the menu button.
+        void handleHover(bool hovering, BuildContext context) {
+          widget.onHover?.call(hovering);
+          // Don't open the root menu bar menus on hover unless something else
+          // is already open. This means that the user has to first click to
+          // open a menu on the menu bar before hovering allows them to traverse
+          // it.
+          if (controller._anchor!._root._orientation == Axis.horizontal && !controller._anchor!._root._childIsOpen) {
+            return;
+          }
+
+          if (hovering) {
+            controller.open();
+            controller._anchor!._focusButton();
+          }
+        }
+
+        return TextButton(
+          style: mergedStyle,
+          focusNode: _buttonFocusNode,
+          onHover: _enabled ? (bool hovering) => handleHover(hovering, context) : null,
+          onPressed: _enabled ? () => toggleShowMenu(context) : null,
+          child: _MenuItemLabel(
+            leadingIcon: widget.leadingIcon,
+            trailingIcon: widget.trailingIcon,
+            hasSubmenu: true,
+            showDecoration: (controller._anchor!._parent?._orientation ?? Axis.horizontal) == Axis.vertical,
+            child: child ?? const SizedBox(),
+          ),
+        );
+      },
+      menuChildren: widget.menuChildren,
+      child: widget.child,
+    );
+  }
+
+  EdgeInsets _computeMenuPadding(BuildContext context) {
+    final MenuStyle? themeStyle = MenuTheme.of(context).style;
+    final MenuStyle defaultStyle = _MenuDefaultsM3(context);
+
+    T? effectiveValue<T>(T? Function(MenuStyle? style) getProperty) {
+      return getProperty(widget.menuStyle) ?? getProperty(themeStyle) ?? getProperty(defaultStyle);
+    }
+
+    T? resolve<T>(MaterialStateProperty<T>? Function(MenuStyle? style) getProperty) {
+      return effectiveValue(
+            (MenuStyle? style) {
+          return getProperty(style)?.resolve(widget.statesController?.value ?? const <MaterialState>{});
+        },
+      );
+    }
+
+    return resolve<EdgeInsetsGeometry?>(
+          (MenuStyle? style) => style?.padding,
+    )?.resolve(
+      Directionality.of(context),
+    ) ??
+        EdgeInsets.zero;
+  }
+
+  void _handleFocusChange() {
+    if (_buttonFocusNode.hasPrimaryFocus) {
+      if (!_menuController.isOpen) {
+        _menuController.open();
+      }
+    } else {
+      if (!_menuController._anchor!._menuScopeNode.hasFocus && _menuController.isOpen) {
+        _menuController.close();
+      }
+    }
+  }
+}
+
+/// An action that closes all the menus associated with the given
+/// [MenuController].
+///
+/// See also:
+///
+///  * [MenuAnchor], a widget that hosts a cascading submenu.
+///  * [MenuBar], a widget that defines a menu bar with cascading submenus.
+class DismissMenuAction extends DismissAction {
+  /// Creates a [DismissMenuAction].
+  DismissMenuAction({required this.controller});
+
+  /// The [MenuController] associated with the menus that should be closed.
+  final MenuController controller;
+
+  @override
+  void invoke(DismissIntent intent) {
+    assert(_debugMenuInfo('$runtimeType: Dismissing all open menus.'));
+    controller._anchor!._root._close();
+  }
+
+  @override
+  bool isEnabled(DismissIntent intent) {
+    return controller.isOpen;
+  }
+}
+
+/// A helper class used to generate shortcut labels for a
+/// [MenuSerializableShortcut] (a subset of the subclasses of
+/// [ShortcutActivator]).
+///
+/// This helper class is typically used by the [MenuItemButton] and
+/// [SubmenuButton] classes to display a label for their assigned shortcuts.
+///
+/// Call [getShortcutLabel] with the [MenuSerializableShortcut] to get a label
+/// for it.
+///
+/// For instance, calling [getShortcutLabel] with `SingleActivator(trigger:
+/// LogicalKeyboardKey.keyA, control: true)` would return "⌃ A" on macOS, "Ctrl
+/// A" in an US English locale, and "Strg A" in a German locale.
+class _LocalizedShortcutLabeler {
+  _LocalizedShortcutLabeler._();
+
+  static _LocalizedShortcutLabeler? _instance;
+
+  static final Map<LogicalKeyboardKey, String> _shortcutGraphicEquivalents = <LogicalKeyboardKey, String>{
+    LogicalKeyboardKey.arrowLeft: '←',
+    LogicalKeyboardKey.arrowRight: '→',
+    LogicalKeyboardKey.arrowUp: '↑',
+    LogicalKeyboardKey.arrowDown: '↓',
+    LogicalKeyboardKey.enter: '↵',
+    LogicalKeyboardKey.shift: '⇧',
+    LogicalKeyboardKey.shiftLeft: '⇧',
+    LogicalKeyboardKey.shiftRight: '⇧',
+  };
+
+  static final Set<LogicalKeyboardKey> _modifiers = <LogicalKeyboardKey>{
+    LogicalKeyboardKey.alt,
+    LogicalKeyboardKey.control,
+    LogicalKeyboardKey.meta,
+    LogicalKeyboardKey.shift,
+    LogicalKeyboardKey.altLeft,
+    LogicalKeyboardKey.controlLeft,
+    LogicalKeyboardKey.metaLeft,
+    LogicalKeyboardKey.shiftLeft,
+    LogicalKeyboardKey.altRight,
+    LogicalKeyboardKey.controlRight,
+    LogicalKeyboardKey.metaRight,
+    LogicalKeyboardKey.shiftRight,
+  };
+
+  /// Return the instance for this singleton.
+  static _LocalizedShortcutLabeler get instance {
+    return _instance ??= _LocalizedShortcutLabeler._();
+  }
+
+  // Caches the created shortcut key maps so that creating one of these isn't
+  // expensive after the first time for each unique localizations object.
+  final Map<MaterialLocalizations, Map<LogicalKeyboardKey, String>> _cachedShortcutKeys =
+      <MaterialLocalizations, Map<LogicalKeyboardKey, String>>{};
+
+  /// Returns the label to be shown to the user in the UI when a
+  /// [MenuSerializableShortcut] is used as a keyboard shortcut.
+  ///
+  /// To keep the representation short, this will return graphical key
+  /// representations when it can. For instance, the default
+  /// [LogicalKeyboardKey.shift] will return '⇧', and the arrow keys will return
+  /// arrows. When [defaultTargetPlatform] is [TargetPlatform.macOS] or
+  /// [TargetPlatform.iOS], the key [LogicalKeyboardKey.meta] will show as '⌘',
+  /// [LogicalKeyboardKey.control] will show as '˄', and
+  /// [LogicalKeyboardKey.alt] will show as '⌥'.
+  String getShortcutLabel(MenuSerializableShortcut shortcut, MaterialLocalizations localizations) {
+    final ShortcutSerialization serialized = shortcut.serializeForMenu();
+    if (serialized.trigger != null) {
+      final List<String> modifiers = <String>[];
+      final LogicalKeyboardKey trigger = serialized.trigger!;
+      // These should be in this order, to match the LogicalKeySet version.
+      if (serialized.alt!) {
+        modifiers.add(_getModifierLabel(LogicalKeyboardKey.alt, localizations));
+      }
+      if (serialized.control!) {
+        modifiers.add(_getModifierLabel(LogicalKeyboardKey.control, localizations));
+      }
+      if (serialized.meta!) {
+        modifiers.add(_getModifierLabel(LogicalKeyboardKey.meta, localizations));
+      }
+      if (serialized.shift!) {
+        modifiers.add(_getModifierLabel(LogicalKeyboardKey.shift, localizations));
+      }
+      String? shortcutTrigger;
+      final int logicalKeyId = trigger.keyId;
+      if (_shortcutGraphicEquivalents.containsKey(trigger)) {
+        shortcutTrigger = _shortcutGraphicEquivalents[trigger];
+      } else {
+        // Otherwise, look it up, and if we don't have a translation for it,
+        // then fall back to the key label.
+        shortcutTrigger = _getLocalizedName(trigger, localizations);
+        if (shortcutTrigger == null && logicalKeyId & LogicalKeyboardKey.planeMask == 0x0) {
+          // If the trigger is a Unicode-character-producing key, then use the
+          // character.
+          shortcutTrigger = String.fromCharCode(logicalKeyId & LogicalKeyboardKey.valueMask).toUpperCase();
+        }
+        // Fall back to the key label if all else fails.
+        shortcutTrigger ??= trigger.keyLabel;
+      }
+      return <String>[
+        ...modifiers,
+        if (shortcutTrigger != null && shortcutTrigger.isNotEmpty) shortcutTrigger,
+      ].join(' ');
+    } else if (serialized.character != null) {
+      return serialized.character!;
+    }
+    throw UnimplementedError('Shortcut labels for ShortcutActivators that do not implement '
+        'MenuSerializableShortcut (e.g. ShortcutActivators other than SingleActivator or '
+        'CharacterActivator) are not supported.');
+  }
+
+  // Tries to look up the key in an internal table, and if it can't find it,
+  // then fall back to the key's keyLabel.
+  String? _getLocalizedName(LogicalKeyboardKey key, MaterialLocalizations localizations) {
+    // Since this is an expensive table to build, we cache it based on the
+    // localization object. There's currently no way to clear the cache, but
+    // it's unlikely that more than one or two will be cached for each run, and
+    // they're not huge.
+    _cachedShortcutKeys[localizations] ??= <LogicalKeyboardKey, String>{
+      LogicalKeyboardKey.altGraph: localizations.keyboardKeyAltGraph,
+      LogicalKeyboardKey.backspace: localizations.keyboardKeyBackspace,
+      LogicalKeyboardKey.capsLock: localizations.keyboardKeyCapsLock,
+      LogicalKeyboardKey.channelDown: localizations.keyboardKeyChannelDown,
+      LogicalKeyboardKey.channelUp: localizations.keyboardKeyChannelUp,
+      LogicalKeyboardKey.delete: localizations.keyboardKeyDelete,
+      LogicalKeyboardKey.eject: localizations.keyboardKeyEject,
+      LogicalKeyboardKey.end: localizations.keyboardKeyEnd,
+      LogicalKeyboardKey.escape: localizations.keyboardKeyEscape,
+      LogicalKeyboardKey.fn: localizations.keyboardKeyFn,
+      LogicalKeyboardKey.home: localizations.keyboardKeyHome,
+      LogicalKeyboardKey.insert: localizations.keyboardKeyInsert,
+      LogicalKeyboardKey.numLock: localizations.keyboardKeyNumLock,
+      LogicalKeyboardKey.numpad1: localizations.keyboardKeyNumpad1,
+      LogicalKeyboardKey.numpad2: localizations.keyboardKeyNumpad2,
+      LogicalKeyboardKey.numpad3: localizations.keyboardKeyNumpad3,
+      LogicalKeyboardKey.numpad4: localizations.keyboardKeyNumpad4,
+      LogicalKeyboardKey.numpad5: localizations.keyboardKeyNumpad5,
+      LogicalKeyboardKey.numpad6: localizations.keyboardKeyNumpad6,
+      LogicalKeyboardKey.numpad7: localizations.keyboardKeyNumpad7,
+      LogicalKeyboardKey.numpad8: localizations.keyboardKeyNumpad8,
+      LogicalKeyboardKey.numpad9: localizations.keyboardKeyNumpad9,
+      LogicalKeyboardKey.numpad0: localizations.keyboardKeyNumpad0,
+      LogicalKeyboardKey.numpadAdd: localizations.keyboardKeyNumpadAdd,
+      LogicalKeyboardKey.numpadComma: localizations.keyboardKeyNumpadComma,
+      LogicalKeyboardKey.numpadDecimal: localizations.keyboardKeyNumpadDecimal,
+      LogicalKeyboardKey.numpadDivide: localizations.keyboardKeyNumpadDivide,
+      LogicalKeyboardKey.numpadEnter: localizations.keyboardKeyNumpadEnter,
+      LogicalKeyboardKey.numpadEqual: localizations.keyboardKeyNumpadEqual,
+      LogicalKeyboardKey.numpadMultiply: localizations.keyboardKeyNumpadMultiply,
+      LogicalKeyboardKey.numpadParenLeft: localizations.keyboardKeyNumpadParenLeft,
+      LogicalKeyboardKey.numpadParenRight: localizations.keyboardKeyNumpadParenRight,
+      LogicalKeyboardKey.numpadSubtract: localizations.keyboardKeyNumpadSubtract,
+      LogicalKeyboardKey.pageDown: localizations.keyboardKeyPageDown,
+      LogicalKeyboardKey.pageUp: localizations.keyboardKeyPageUp,
+      LogicalKeyboardKey.power: localizations.keyboardKeyPower,
+      LogicalKeyboardKey.powerOff: localizations.keyboardKeyPowerOff,
+      LogicalKeyboardKey.printScreen: localizations.keyboardKeyPrintScreen,
+      LogicalKeyboardKey.scrollLock: localizations.keyboardKeyScrollLock,
+      LogicalKeyboardKey.select: localizations.keyboardKeySelect,
+      LogicalKeyboardKey.space: localizations.keyboardKeySpace,
+    };
+    return _cachedShortcutKeys[localizations]![key];
+  }
+
+  String _getModifierLabel(LogicalKeyboardKey modifier, MaterialLocalizations localizations) {
+    assert(_modifiers.contains(modifier), '${modifier.keyLabel} is not a modifier key');
+    if (modifier == LogicalKeyboardKey.meta ||
+        modifier == LogicalKeyboardKey.metaLeft ||
+        modifier == LogicalKeyboardKey.metaRight) {
+      switch (defaultTargetPlatform) {
+        case TargetPlatform.android:
+        case TargetPlatform.fuchsia:
+        case TargetPlatform.linux:
+          return localizations.keyboardKeyMeta;
+        case TargetPlatform.windows:
+          return localizations.keyboardKeyMetaWindows;
+        case TargetPlatform.iOS:
+        case TargetPlatform.macOS:
+          return '⌘';
+      }
+    }
+    if (modifier == LogicalKeyboardKey.alt ||
+        modifier == LogicalKeyboardKey.altLeft ||
+        modifier == LogicalKeyboardKey.altRight) {
+      switch (defaultTargetPlatform) {
+        case TargetPlatform.android:
+        case TargetPlatform.fuchsia:
+        case TargetPlatform.linux:
+        case TargetPlatform.windows:
+          return localizations.keyboardKeyAlt;
+        case TargetPlatform.iOS:
+        case TargetPlatform.macOS:
+          return '⌥';
+      }
+    }
+    if (modifier == LogicalKeyboardKey.control ||
+        modifier == LogicalKeyboardKey.controlLeft ||
+        modifier == LogicalKeyboardKey.controlRight) {
+      // '⎈' (a boat helm wheel, not an asterisk) is apparently the standard
+      // icon for "control", but only seems to appear on the French Canadian
+      // keyboard. A '✲' (an open center asterisk) appears on some Microsoft
+      // keyboards. For all but macOS (which has standardized on "⌃", it seems),
+      // we just return the local translation of "Ctrl".
+      switch (defaultTargetPlatform) {
+        case TargetPlatform.android:
+        case TargetPlatform.fuchsia:
+        case TargetPlatform.linux:
+        case TargetPlatform.windows:
+          return localizations.keyboardKeyControl;
+        case TargetPlatform.iOS:
+        case TargetPlatform.macOS:
+          return '⌃';
+      }
+    }
+    if (modifier == LogicalKeyboardKey.shift ||
+        modifier == LogicalKeyboardKey.shiftLeft ||
+        modifier == LogicalKeyboardKey.shiftRight) {
+      return _shortcutGraphicEquivalents[LogicalKeyboardKey.shift]!;
+    }
+    throw ArgumentError('Keyboard key ${modifier.keyLabel} is not a modifier.');
+  }
+}
+
+class _MenuAnchorMarker extends InheritedWidget {
+  const _MenuAnchorMarker({
+    required super.child,
+    required this.anchorKey,
+    required this.anchor,
+  });
+
+  final GlobalKey anchorKey;
+  final _MenuAnchorState anchor;
+
+  @override
+  bool updateShouldNotify(_MenuAnchorMarker oldWidget) {
+    return anchorKey != oldWidget.anchorKey || anchor != anchor;
+  }
+}
+
+/// MenuBar-specific private specialization of [MenuAnchor] so that it can act
+/// differently in regards to orientation, how open works, and what gets built.
+class _MenuBarAnchor extends MenuAnchor {
+  const _MenuBarAnchor({
+    required super.menuChildren,
+    super.controller,
+    super.clipBehavior,
+    super.style,
+  });
+
+  @override
+  State<MenuAnchor> createState() => _MenuBarAnchorState();
+}
+
+class _MenuBarAnchorState extends _MenuAnchorState {
+  @override
+  bool get _isOpen {
+    // If it's a bar, then it's "open" if any of its children are open.
+    return _childIsOpen;
+  }
+
+  @override
+  Axis get _orientation => Axis.horizontal;
+
+  @override
+  Widget _buildContents(BuildContext context) {
+    return FocusScope(
+      node: _menuScopeNode,
+      skipTraversal: !_isOpen,
+      canRequestFocus: _isOpen,
+      child: ExcludeFocus(
+        excluding: !_isOpen,
+        child: Shortcuts(
+          shortcuts: _kMenuTraversalShortcuts,
+          child: Actions(
+            actions: <Type, Action<Intent>>{
+              DirectionalFocusIntent: _MenuDirectionalFocusAction(),
+              DismissIntent: DismissMenuAction(controller: _menuController),
+            },
+            child: Builder(builder: (BuildContext context) {
+              return _MenuPanel(
+                menuStyle: widget.style,
+                clipBehavior: widget.clipBehavior,
+                orientation: Axis.horizontal,
+                children: widget.menuChildren,
+              );
+            }),
+          ),
+        ),
+      ),
+    );
+  }
+
+  @override
+  void _open({Offset? position}) {
+    assert(_menuController._anchor == this);
+    // Menu bars can't be opened, because they're already always open.
+    return;
+  }
+}
+
+class _MenuDirectionalFocusAction extends DirectionalFocusAction {
+  /// Creates a [DirectionalFocusAction].
+  _MenuDirectionalFocusAction();
+
+  @override
+  void invoke(DirectionalFocusIntent intent) {
+    assert(_debugMenuInfo('_MenuDirectionalFocusAction invoked with $intent'));
+    final BuildContext? context = FocusManager.instance.primaryFocus?.context;
+    if (context == null) {
+      super.invoke(intent);
+      return;
+    }
+    final _MenuAnchorState? anchor = _MenuAnchorState._maybeOf(context);
+    if (anchor == null || !anchor._root._isOpen) {
+      super.invoke(intent);
+      return;
+    }
+    final bool buttonIsFocused = anchor.widget.childFocusNode?.hasPrimaryFocus ?? false;
+    Axis orientation;
+    if (buttonIsFocused) {
+      orientation = anchor._parent!._orientation;
+    } else {
+      orientation = anchor._orientation;
+    }
+    final bool firstItemIsFocused = anchor._firstItemFocusNode?.hasPrimaryFocus ?? false;
+    assert(_debugMenuInfo('In _MenuDirectionalFocusAction, current node is ${anchor.widget.childFocusNode?.debugLabel}, '
+        'button is${buttonIsFocused ? '' : ' not'} focused. Assuming ${orientation.name} orientation.'));
+
+    switch (intent.direction) {
+      case TraversalDirection.up:
+        switch (orientation) {
+          case Axis.horizontal:
+            if (_moveToParent(anchor)) {
+              return;
+            }
+            break;
+          case Axis.vertical:
+            if (firstItemIsFocused) {
+              if (_moveToParent(anchor)) {
+                return;
+              }
+            }
+            if (_moveToPrevious(anchor)) {
+              return;
+            }
+            break;
+        }
+        break;
+      case TraversalDirection.down:
+        switch (orientation) {
+          case Axis.horizontal:
+            if (_moveToSubmenu(anchor)) {
+              return;
+            }
+            break;
+          case Axis.vertical:
+            if (_moveToNext(anchor)) {
+              return;
+            }
+            break;
+        }
+        break;
+      case TraversalDirection.left:
+        switch (orientation) {
+          case Axis.horizontal:
+            switch (Directionality.of(context)) {
+              case TextDirection.rtl:
+                if (_moveToNext(anchor)) {
+                  return;
+                }
+                break;
+              case TextDirection.ltr:
+                if (_moveToPrevious(anchor)) {
+                  return;
+                }
+                break;
+            }
+            break;
+          case Axis.vertical:
+            switch (Directionality.of(context)) {
+              case TextDirection.rtl:
+                if (buttonIsFocused) {
+                  if (_moveToSubmenu(anchor)) {
+                    return;
+                  }
+                } else {
+                  if (_moveToNextTopLevel(anchor)) {
+                    return;
+                  }
+                }
+                break;
+              case TextDirection.ltr:
+                switch (anchor._parent!._orientation) {
+                  case Axis.horizontal:
+                    if (_moveToPreviousTopLevel(anchor)) {
+                      return;
+                    }
+                    break;
+                  case Axis.vertical:
+                    if (buttonIsFocused) {
+                      if (_moveToPreviousTopLevel(anchor)) {
+                        return;
+                      }
+                    } else {
+                      if (_moveToParent(anchor)) {
+                        return;
+                      }
+                    }
+                    break;
+                }
+                break;
+            }
+            break;
+        }
+        break;
+      case TraversalDirection.right:
+        switch (orientation) {
+          case Axis.horizontal:
+            switch (Directionality.of(context)) {
+              case TextDirection.rtl:
+                if (_moveToPrevious(anchor)) {
+                  return;
+                }
+                break;
+              case TextDirection.ltr:
+                if (_moveToNext(anchor)) {
+                  return;
+                }
+                break;
+            }
+            break;
+          case Axis.vertical:
+            switch (Directionality.of(context)) {
+              case TextDirection.rtl:
+                switch (anchor._parent!._orientation) {
+                  case Axis.horizontal:
+                    if (_moveToPreviousTopLevel(anchor)) {
+                      return;
+                    }
+                    break;
+                  case Axis.vertical:
+                    if (_moveToParent(anchor)) {
+                      return;
+                    }
+                    break;
+                }
+                break;
+              case TextDirection.ltr:
+                if (buttonIsFocused) {
+                  if (_moveToSubmenu(anchor)) {
+                    return;
+                  }
+                } else {
+                  if (_moveToNextTopLevel(anchor)) {
+                    return;
+                  }
+                }
+                break;
+            }
+            break;
+        }
+        break;
+    }
+    super.invoke(intent);
+  }
+
+  bool _moveToNext(_MenuAnchorState currentMenu) {
+    assert(_debugMenuInfo('Moving focus to next item in menu'));
+    // Need to invalidate the scope data because we're switching scopes, and
+    // otherwise the anti-hysteresis code will interfere with moving to the
+    // correct node.
+    if (currentMenu.widget.childFocusNode != null) {
+      final FocusTraversalPolicy? policy = FocusTraversalGroup.maybeOf(primaryFocus!.context!);
+      policy?.invalidateScopeData(currentMenu.widget.childFocusNode!.nearestScope!);
+    }
+    return false;
+  }
+
+  bool _moveToNextTopLevel(_MenuAnchorState currentMenu) {
+    final _MenuAnchorState? sibling = currentMenu._topLevel._nextSibling;
+    if (sibling == null) {
+      // Wrap around to the first top level.
+      currentMenu._topLevel._parent!._anchorChildren.first._focusButton();
+    } else {
+      sibling._focusButton();
+    }
+    return true;
+  }
+
+  bool _moveToParent(_MenuAnchorState currentMenu) {
+    assert(_debugMenuInfo('Moving focus to parent menu button'));
+    if (!(currentMenu.widget.childFocusNode?.hasPrimaryFocus ?? true)) {
+      currentMenu._focusButton();
+    }
+    return true;
+  }
+
+  bool _moveToPrevious(_MenuAnchorState currentMenu) {
+    assert(_debugMenuInfo('Moving focus to previous item in menu'));
+    // Need to invalidate the scope data because we're switching scopes, and
+    // otherwise the anti-hysteresis code will interfere with moving to the
+    // correct node.
+    if (currentMenu.widget.childFocusNode != null) {
+      final FocusTraversalPolicy? policy = FocusTraversalGroup.maybeOf(primaryFocus!.context!);
+      policy?.invalidateScopeData(currentMenu.widget.childFocusNode!.nearestScope!);
+    }
+    return false;
+  }
+
+  bool _moveToPreviousTopLevel(_MenuAnchorState currentMenu) {
+    final _MenuAnchorState? sibling = currentMenu._topLevel._previousSibling;
+    if (sibling == null) {
+      // Already on the first one, wrap around to the last one.
+      currentMenu._topLevel._parent!._anchorChildren.last._focusButton();
+    } else {
+      sibling._focusButton();
+    }
+    return true;
+  }
+
+  bool _moveToSubmenu(_MenuAnchorState currentMenu) {
+    assert(_debugMenuInfo('Opening submenu'));
+    if (!currentMenu._isOpen) {
+      // If no submenu is open, then an arrow opens the submenu.
+      currentMenu._open();
+      return true;
+    } else {
+      final FocusNode? firstNode = currentMenu._firstItemFocusNode;
+      if (firstNode != null && firstNode.nearestScope != firstNode) {
+        // Don't request focus if the "first" found node is a focus scope, since
+        // that means that nothing else in the submenu is focusable.
+        firstNode.requestFocus();
+      }
+      return true;
+    }
+  }
+}
+
+/// A label widget that is used as the label for a [MenuItemButton] or
+/// [SubmenuButton].
+///
+/// It not only shows the [SubmenuButton.child] or [MenuItemButton.child], but if
+/// there is a shortcut associated with the [MenuItemButton], it will display a
+/// mnemonic for the shortcut. For [SubmenuButton]s, it will display a visual
+/// indicator that there is a submenu.
+class _MenuItemLabel extends StatelessWidget {
+  /// Creates a const [_MenuItemLabel].
+  ///
+  /// The [child] and [hasSubmenu] arguments are required.
+  const _MenuItemLabel({
+    required this.hasSubmenu,
+    this.showDecoration = true,
+    this.leadingIcon,
+    this.trailingIcon,
+    this.shortcut,
+    required this.child,
+  });
+
+  /// Whether or not this menu has a submenu.
+  ///
+  /// Determines whether the submenu arrow is shown or not.
+  final bool hasSubmenu;
+
+  /// Whether or not this item should show decorations like shortcut labels or
+  /// submenu arrows. Items in a [MenuBar] don't show these decorations when
+  /// they are laid out horizontally.
+  final bool showDecoration;
+
+  /// The optional icon that comes before the [child].
+  final Widget? leadingIcon;
+
+  /// The optional icon that comes after the [child].
+  final Widget? trailingIcon;
+
+  /// The shortcut for this label, so that it can generate a string describing
+  /// the shortcut.
+  final MenuSerializableShortcut? shortcut;
+
+  /// The required label child widget.
+  final Widget child;
+
+  @override
+  Widget build(BuildContext context) {
+    final VisualDensity density = Theme.of(context).visualDensity;
+    final double horizontalPadding = math.max(
+      _kLabelItemMinSpacing,
+      _kLabelItemDefaultSpacing + density.horizontal * 2,
+    );
+    return Row(
+      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+      children: <Widget>[
+        Row(
+          mainAxisSize: MainAxisSize.min,
+          children: <Widget>[
+            if (leadingIcon != null) leadingIcon!,
+            Padding(
+              padding: leadingIcon != null ? EdgeInsetsDirectional.only(start: horizontalPadding) : EdgeInsets.zero,
+              child: child,
+            ),
+            if (trailingIcon != null)
+              Padding(
+                padding: EdgeInsetsDirectional.only(start: horizontalPadding),
+                child: trailingIcon,
+              ),
+          ],
+        ),
+        if (showDecoration && (shortcut != null || hasSubmenu)) SizedBox(width: horizontalPadding),
+        if (showDecoration && shortcut != null)
+          Padding(
+            padding: EdgeInsetsDirectional.only(start: horizontalPadding),
+            child: Text(
+              _LocalizedShortcutLabeler.instance.getShortcutLabel(
+                shortcut!,
+                MaterialLocalizations.of(context),
+              ),
+            ),
+          ),
+        if (showDecoration && hasSubmenu)
+          Padding(
+            padding: EdgeInsetsDirectional.only(start: horizontalPadding),
+            child: const Icon(
+              Icons.arrow_right, // Automatically switches with text direction.
+              size: _kDefaultSubmenuIconSize,
+            ),
+          ),
+      ],
+    );
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<String>('child', child.toString()));
+    properties.add(DiagnosticsProperty<MenuSerializableShortcut>('shortcut', shortcut, defaultValue: null));
+    properties.add(DiagnosticsProperty<bool>('hasSubmenu', hasSubmenu));
+    properties.add(DiagnosticsProperty<bool>('showDecoration', showDecoration));
+  }
+}
+
+// Positions the menu in the view while trying to keep as much as possible
+// visible in the view.
+class _MenuLayout extends SingleChildLayoutDelegate {
+  const _MenuLayout({
+    required this.anchorRect,
+    required this.textDirection,
+    required this.alignment,
+    required this.alignmentOffset,
+    required this.menuPosition,
+    required this.menuPadding,
+    required this.avoidBounds,
+    required this.orientation,
+    required this.parentOrientation,
+  });
+
+  // Rectangle of underlying button, relative to the overlay's dimensions.
+  final Rect anchorRect;
+
+  // Whether to prefer going to the left or to the right.
+  final TextDirection textDirection;
+
+  // The alignment to use when finding the ideal location for the menu.
+  final AlignmentGeometry alignment;
+
+  // The offset from the alignment position to find the ideal location for the
+  // menu.
+  final Offset alignmentOffset;
+
+  // The position passed to the open method, if any.
+  final Offset? menuPosition;
+
+  // The padding on the inside of the menu, so it can be accounted for when
+  // positioning.
+  final EdgeInsetsGeometry menuPadding;
+
+  // List of rectangles that we should avoid overlapping. Unusable screen area.
+  final Set<Rect> avoidBounds;
+
+  // The orientation of this menu
+  final Axis orientation;
+
+  // The orientation of this menu's parent.
+  final Axis parentOrientation;
+
+  @override
+  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
+    // The menu can be at most the size of the overlay minus _kMenuViewPadding
+    // pixels in each direction.
+    return BoxConstraints.loose(constraints.biggest).deflate(
+      const EdgeInsets.all(_kMenuViewPadding),
+    );
+  }
+
+  @override
+  Offset getPositionForChild(Size size, Size childSize) {
+    // 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;
+    double x;
+    double y;
+    if (menuPosition == null) {
+      Offset desiredPosition = alignment.resolve(textDirection).withinRect(anchorRect);
+      final Offset directionalOffset;
+      if (alignment is AlignmentDirectional) {
+        switch (textDirection) {
+          case TextDirection.rtl:
+            directionalOffset = Offset(-alignmentOffset.dx, alignmentOffset.dy);
+            break;
+          case TextDirection.ltr:
+            directionalOffset = alignmentOffset;
+            break;
+        }
+      } else {
+        directionalOffset = alignmentOffset;
+      }
+      desiredPosition += directionalOffset;
+      x = desiredPosition.dx;
+      y = desiredPosition.dy;
+      switch (textDirection) {
+        case TextDirection.rtl:
+          x -= childSize.width;
+          break;
+        case TextDirection.ltr:
+          break;
+      }
+    } else {
+      final Offset adjustedPosition = menuPosition! + anchorRect.topLeft;
+      x = adjustedPosition.dx;
+      y = adjustedPosition.dy;
+    }
+
+    final Iterable<Rect> subScreens = DisplayFeatureSubScreen.subScreensInBounds(overlayRect, avoidBounds);
+    final Rect allowedRect = _closestScreen(subScreens, anchorRect.center);
+    bool offLeftSide(double x) => x < allowedRect.left;
+    bool offRightSide(double x) => x + childSize.width > allowedRect.right;
+    bool offTop(double y) => y < allowedRect.top;
+    bool offBottom(double y) => y + childSize.height > allowedRect.bottom;
+    // Avoid going outside an area defined as the rectangle offset from the
+    // edge of the screen by the button padding. If the menu is off of the screen,
+    // move the menu to the other side of the button first, and then if it
+    // doesn't fit there, then just move it over as much as needed to make it
+    // fit.
+    if (childSize.width >= allowedRect.width) {
+      // It just doesn't fit, so put as much on the screen as possible.
+      x = allowedRect.left;
+    } else {
+      if (offLeftSide(x)) {
+        // If the parent is a different orientation than the current one, then
+        // just push it over instead of trying the other side.
+        if (parentOrientation != orientation) {
+          x = allowedRect.left;
+        } else {
+          final double newX = anchorRect.right;
+          if (!offRightSide(newX)) {
+            x = newX;
+          } else {
+            x = allowedRect.left;
+          }
+        }
+      } else if (offRightSide(x)) {
+        if (parentOrientation != orientation) {
+          x = allowedRect.right - childSize.width;
+        } else {
+          final double newX = anchorRect.left - childSize.width;
+          if (!offLeftSide(newX)) {
+            x = newX;
+          } else {
+            x = allowedRect.right - childSize.width;
+          }
+        }
+      }
+    }
+    if (childSize.height >= allowedRect.height) {
+      // Too tall to fit, fit as much on as possible.
+      y = allowedRect.top;
+    } else {
+      if (offTop(y)) {
+        final double newY = anchorRect.bottom;
+        if (!offBottom(newY)) {
+          y = newY;
+        } else {
+          y = allowedRect.top;
+        }
+      } else if (offBottom(y)) {
+        final double newY = anchorRect.top - childSize.height;
+        if (!offTop(newY)) {
+          y = newY;
+        } else {
+          y = allowedRect.bottom - childSize.height;
+        }
+      }
+    }
+    return Offset(x, y);
+  }
+
+  @override
+  bool shouldRelayout(_MenuLayout oldDelegate) {
+    return anchorRect != oldDelegate.anchorRect ||
+        textDirection != oldDelegate.textDirection ||
+        alignment != oldDelegate.alignment ||
+        alignmentOffset != oldDelegate.alignmentOffset ||
+        menuPosition != oldDelegate.menuPosition ||
+        orientation != oldDelegate.orientation ||
+        parentOrientation != oldDelegate.parentOrientation ||
+        !setEquals(avoidBounds, oldDelegate.avoidBounds);
+  }
+
+  Rect _closestScreen(Iterable<Rect> screens, Offset point) {
+    Rect closest = screens.first;
+    for (final Rect screen in screens) {
+      if ((screen.center - point).distance < (closest.center - point).distance) {
+        closest = screen;
+      }
+    }
+    return closest;
+  }
+}
+
+/// A widget that manages a list of menu buttons in a menu.
+///
+/// It sizes itself to the widest/tallest item it contains, and then sizes all
+/// the other entries to match.
+class _MenuPanel extends StatefulWidget {
+  const _MenuPanel({
+    required this.menuStyle,
+    this.clipBehavior = Clip.none,
+    required this.orientation,
+    required this.children,
+  });
+
+  /// The menu style that has all the attributes for this menu panel.
+  final MenuStyle? menuStyle;
+
+  /// {@macro flutter.material.Material.clipBehavior}
+  ///
+  /// Defaults to [Clip.none].
+  final Clip clipBehavior;
+
+  /// The layout orientation of this panel.
+  final Axis orientation;
+
+  /// The list of widgets to use as children of this menu bar.
+  ///
+  /// These are the top level [SubmenuButton]s.
+  final List<Widget> children;
+
+  @override
+  State<_MenuPanel> createState() => _MenuPanelState();
+}
+
+class _MenuPanelState extends State<_MenuPanel> {
+  @override
+  Widget build(BuildContext context) {
+    final MenuStyle? themeStyle;
+    final MenuStyle defaultStyle;
+    switch (widget.orientation) {
+      case Axis.horizontal:
+        themeStyle = MenuBarTheme.of(context).style;
+        defaultStyle = _MenuBarDefaultsM3(context);
+        break;
+      case Axis.vertical:
+        themeStyle = MenuTheme.of(context).style;
+        defaultStyle = _MenuDefaultsM3(context);
+        break;
+    }
+    final MenuStyle? widgetStyle = widget.menuStyle;
+
+    T? effectiveValue<T>(T? Function(MenuStyle? style) getProperty) {
+      return getProperty(widgetStyle) ?? getProperty(themeStyle) ?? getProperty(defaultStyle);
+    }
+
+    T? resolve<T>(MaterialStateProperty<T>? Function(MenuStyle? style) getProperty) {
+      return effectiveValue(
+        (MenuStyle? style) {
+          return getProperty(style)?.resolve(<MaterialState>{});
+        },
+      );
+    }
+
+    final Color? backgroundColor = resolve<Color?>((MenuStyle? style) => style?.backgroundColor);
+    final Color? shadowColor = resolve<Color?>((MenuStyle? style) => style?.shadowColor);
+    final Color? surfaceTintColor = resolve<Color?>((MenuStyle? style) => style?.surfaceTintColor);
+    final double elevation = resolve<double?>((MenuStyle? style) => style?.elevation) ?? 0;
+    final Size? minimumSize = resolve<Size?>((MenuStyle? style) => style?.minimumSize);
+    final Size? fixedSize = resolve<Size?>((MenuStyle? style) => style?.fixedSize);
+    final Size? maximumSize = resolve<Size?>((MenuStyle? style) => style?.maximumSize);
+    final BorderSide? side = resolve<BorderSide?>((MenuStyle? style) => style?.side);
+    final OutlinedBorder shape = resolve<OutlinedBorder?>((MenuStyle? style) => style?.shape)!.copyWith(side: side);
+    final VisualDensity visualDensity =
+        effectiveValue((MenuStyle? style) => style?.visualDensity) ?? VisualDensity.standard;
+    final EdgeInsetsGeometry padding =
+        resolve<EdgeInsetsGeometry?>((MenuStyle? style) => style?.padding) ?? EdgeInsets.zero;
+    final Offset densityAdjustment = visualDensity.baseSizeAdjustment;
+    // Per the Material Design team: don't allow the VisualDensity
+    // adjustment to reduce the width of the left/right padding. If we
+    // did, VisualDensity.compact, the default for desktop/web, would
+    // reduce the horizontal padding to zero.
+    final double dy = densityAdjustment.dy;
+    final double dx = math.max(0, densityAdjustment.dx);
+    final EdgeInsetsGeometry resolvedPadding = padding
+        .add(EdgeInsets.fromLTRB(dx, dy, dx, dy))
+        .clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity); // ignore_clamp_double_lint
+
+    BoxConstraints effectiveConstraints = visualDensity.effectiveConstraints(
+      BoxConstraints(
+        minWidth: minimumSize?.width ?? 0,
+        minHeight: minimumSize?.height ?? 0,
+        maxWidth: maximumSize?.width ?? double.infinity,
+        maxHeight: maximumSize?.height ?? double.infinity,
+      ),
+    );
+    if (fixedSize != null) {
+      final Size size = effectiveConstraints.constrain(fixedSize);
+      if (size.width.isFinite) {
+        effectiveConstraints = effectiveConstraints.copyWith(
+          minWidth: size.width,
+          maxWidth: size.width,
+        );
+      }
+      if (size.height.isFinite) {
+        effectiveConstraints = effectiveConstraints.copyWith(
+          minHeight: size.height,
+          maxHeight: size.height,
+        );
+      }
+    }
+    return ConstrainedBox(
+      constraints: effectiveConstraints,
+      child: UnconstrainedBox(
+        constrainedAxis: widget.orientation,
+        clipBehavior: Clip.hardEdge,
+        alignment: AlignmentDirectional.centerStart,
+      child: _intrinsicCrossSize(
+        child: Material(
+          elevation: elevation,
+          shape: shape,
+          color: backgroundColor,
+          shadowColor: shadowColor,
+          surfaceTintColor: surfaceTintColor,
+          type: backgroundColor == null ? MaterialType.transparency : MaterialType.canvas,
+            clipBehavior: Clip.hardEdge,
+          child: Padding(
+            padding: resolvedPadding,
+            child: SingleChildScrollView(
+              scrollDirection: widget.orientation,
+              child: Flex(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                textDirection: Directionality.of(context),
+                direction: widget.orientation,
+                mainAxisSize: MainAxisSize.min,
+                children: widget.children,
+                ),
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  Widget _intrinsicCrossSize({required Widget child}) {
+    switch (widget.orientation) {
+      case Axis.horizontal:
+        return IntrinsicHeight(child: child);
+      case Axis.vertical:
+        return IntrinsicWidth(child: child);
+    }
+  }
+}
+
+// A widget that defines the menu drawn inside of the overlay entry.
+class _Submenu extends StatelessWidget {
+  const _Submenu({
+    required this.anchor,
+    required this.menuStyle,
+    required this.menuPosition,
+    required this.alignmentOffset,
+    required this.clipBehavior,
+    required this.menuChildren,
+  });
+
+  final _MenuAnchorState anchor;
+  final MenuStyle? menuStyle;
+  final Offset? menuPosition;
+  final Offset alignmentOffset;
+  final Clip clipBehavior;
+  final List<Widget> menuChildren;
+
+  @override
+  Widget build(BuildContext context) {
+    // Use the text direction of the context where the button is.
+    final TextDirection textDirection = Directionality.of(context);
+    final MenuStyle? themeStyle;
+    final MenuStyle defaultStyle;
+    switch (anchor._parent?._orientation ?? Axis.horizontal) {
+      case Axis.horizontal:
+        themeStyle = MenuBarTheme.of(context).style;
+        defaultStyle = _MenuBarDefaultsM3(context);
+        break;
+      case Axis.vertical:
+        themeStyle = MenuTheme.of(context).style;
+        defaultStyle = _MenuDefaultsM3(context);
+        break;
+    }
+    T? effectiveValue<T>(T? Function(MenuStyle? style) getProperty) {
+      return getProperty(menuStyle) ?? getProperty(themeStyle) ?? getProperty(defaultStyle);
+    }
+    T? resolve<T>(MaterialStateProperty<T>? Function(MenuStyle? style) getProperty) {
+      return effectiveValue(
+        (MenuStyle? style) {
+          return getProperty(style)?.resolve(<MaterialState>{});
+        },
+      );
+    }
+
+    final MaterialStateMouseCursor mouseCursor = _MouseCursor(
+      (Set<MaterialState> states) => effectiveValue((MenuStyle? style) => style?.mouseCursor?.resolve(states)),
+    );
+
+    final VisualDensity visualDensity =
+        effectiveValue((MenuStyle? style) => style?.visualDensity) ?? VisualDensity.standard;
+    final AlignmentGeometry alignment = effectiveValue((MenuStyle? style) => style?.alignment)!;
+    final BuildContext anchorContext = anchor._anchorKey.currentContext!;
+    final RenderBox overlay = Overlay.of(anchorContext).context.findRenderObject()! as RenderBox;
+    final RenderBox anchorBox = anchorContext.findRenderObject()! as RenderBox;
+    final Offset upperLeft = anchorBox.localToGlobal(Offset.zero, ancestor: overlay);
+    final Offset bottomRight = anchorBox.localToGlobal(anchorBox.paintBounds.bottomRight, ancestor: overlay);
+    final Rect anchorRect = Rect.fromPoints(upperLeft, bottomRight);
+    final EdgeInsetsGeometry padding =
+        resolve<EdgeInsetsGeometry?>((MenuStyle? style) => style?.padding) ?? EdgeInsets.zero;
+    final Offset densityAdjustment = visualDensity.baseSizeAdjustment;
+    // Per the Material Design team: don't allow the VisualDensity
+    // adjustment to reduce the width of the left/right padding. If we
+    // did, VisualDensity.compact, the default for desktop/web, would
+    // reduce the horizontal padding to zero.
+    final double dy = densityAdjustment.dy;
+    final double dx = math.max(0, densityAdjustment.dx);
+    final EdgeInsetsGeometry resolvedPadding = padding
+        .add(EdgeInsets.fromLTRB(dx, dy, dx, dy))
+        .clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity); // ignore_clamp_double_lint
+
+    return Theme(
+      data: Theme.of(context).copyWith(
+        visualDensity: visualDensity,
+      ),
+      child: ConstrainedBox(
+        constraints: BoxConstraints.loose(overlay.paintBounds.size),
+        child: CustomSingleChildLayout(
+          delegate: _MenuLayout(
+            anchorRect: anchorRect,
+            textDirection: textDirection,
+            avoidBounds: DisplayFeatureSubScreen.avoidBounds(MediaQuery.of(context)).toSet(),
+            menuPadding: resolvedPadding,
+            alignment: alignment,
+            alignmentOffset: alignmentOffset,
+            menuPosition: menuPosition,
+            orientation: anchor._orientation,
+            parentOrientation: anchor._parent?._orientation ?? Axis.horizontal,
+          ),
+          child: TapRegion(
+            groupId: anchor._root,
+            onTapOutside: (PointerDownEvent event) {
+              anchor._close();
+            },
+            child: MouseRegion(
+              cursor: mouseCursor,
+              hitTestBehavior: HitTestBehavior.deferToChild,
+              child: FocusScope(
+                node: anchor._menuScopeNode,
+                child: Actions(
+                  actions: <Type, Action<Intent>>{
+                    DirectionalFocusIntent: _MenuDirectionalFocusAction(),
+                    DismissIntent: DismissMenuAction(controller: anchor._menuController),
+                  },
+                  child: Shortcuts(
+                    shortcuts: _kMenuTraversalShortcuts,
+                    child: Directionality(
+                      // Copy the directionality from the button into the overlay.
+                      textDirection: textDirection,
+                      child: _MenuPanel(
+                        menuStyle: menuStyle,
+                        clipBehavior: clipBehavior,
+                        orientation: anchor._orientation,
+                        children: menuChildren,
+                      ),
+                    ),
+                  ),
+                ),
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+/// Wraps the [MaterialStateMouseCursor] so that it can default to
+/// [MouseCursor.uncontrolled] if none is set.
+class _MouseCursor extends MaterialStateMouseCursor {
+  const _MouseCursor(this.resolveCallback);
+
+  final MaterialPropertyResolver<MouseCursor?> resolveCallback;
+
+  @override
+  MouseCursor resolve(Set<MaterialState> states) => resolveCallback(states) ?? MouseCursor.uncontrolled;
+
+  @override
+  String get debugDescription => 'Menu_MouseCursor';
+}
+
+/// A debug print function, which should only be called within an assert, like
+/// so:
+///
+///   assert(_debugMenuInfo('Debug Message'));
+///
+/// so that the call is entirely removed in release builds.
+///
+/// Enable debug printing by setting [_kDebugMenus] to true at the top of the
+/// file.
+bool _debugMenuInfo(String message, [Iterable<String>? details]) {
+  assert(() {
+    if (_kDebugMenus) {
+      debugPrint('MENU: $message');
+      if (details != null && details.isNotEmpty) {
+        for (final String detail in details) {
+          debugPrint('    $detail');
+        }
+      }
+    }
+    return true;
+  }());
+  // Return true so that it can be easily used inside of an assert.
+  return true;
+}
+
+// This class will eventually be auto-generated, so it should remain at the end
+// of the file.
+class _MenuBarDefaultsM3 extends MenuStyle {
+  _MenuBarDefaultsM3(this.context)
+      : super(
+          elevation: const MaterialStatePropertyAll<double?>(4),
+          shape: const MaterialStatePropertyAll<OutlinedBorder>(_defaultMenuBorder),
+          alignment: AlignmentDirectional.bottomStart,
+        );
+  static const RoundedRectangleBorder _defaultMenuBorder =
+      RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.elliptical(2, 3)));
+
+  final BuildContext context;
+
+  late final ColorScheme _colors = Theme.of(context).colorScheme;
+
+  @override
+  MaterialStateProperty<Color?> get backgroundColor {
+    return MaterialStatePropertyAll<Color?>(_colors.surface);
+  }
+
+  @override
+  MaterialStateProperty<EdgeInsetsGeometry?>? get padding {
+    return MaterialStatePropertyAll<EdgeInsetsGeometry>(
+      EdgeInsetsDirectional.symmetric(
+        horizontal: math.max(
+          _kTopLevelMenuHorizontalMinPadding,
+          2 + Theme.of(context).visualDensity.baseSizeAdjustment.dx,
+        ),
+      ),
+    );
+  }
+}
+
+// This class will eventually be auto-generated, so it should remain at the end
+// of the file.
+class _MenuButtonDefaultsM3 extends ButtonStyle {
+  _MenuButtonDefaultsM3(this.context)
+      : super(
+          animationDuration: kThemeChangeDuration,
+          enableFeedback: true,
+          alignment: AlignmentDirectional.centerStart,
+        );
+  final BuildContext context;
+
+  late final ColorScheme _colors = Theme.of(context).colorScheme;
+
+  @override
+  MaterialStateProperty<Color?>? get backgroundColor {
+    return ButtonStyleButton.allOrNull<Color>(Colors.transparent);
+  }
+
+  // No default shadow color
+
+  // No default surface tint color
+
+  @override
+  MaterialStateProperty<double>? get elevation {
+    return ButtonStyleButton.allOrNull<double>(0);
+  }
+
+  @override
+  MaterialStateProperty<Color?>? get foregroundColor {
+    return MaterialStateProperty.resolveWith(
+      (Set<MaterialState> states) {
+        if (states.contains(MaterialState.disabled)) {
+          return _colors.onSurface.withOpacity(0.38);
+        }
+        return _colors.primary;
+      },
+    );
+  }
+
+  // No default fixedSize
+
+  @override
+  MaterialStateProperty<Size>? get maximumSize {
+    return ButtonStyleButton.allOrNull<Size>(Size.infinite);
+  }
+
+  @override
+  MaterialStateProperty<Size>? get minimumSize {
+    return ButtonStyleButton.allOrNull<Size>(const Size(64, 40));
+  }
+
+  @override
+  MaterialStateProperty<MouseCursor?>? get mouseCursor {
+    return MaterialStateProperty.resolveWith(
+      (Set<MaterialState> states) {
+        if (states.contains(MaterialState.disabled)) {
+          return SystemMouseCursors.basic;
+        }
+        return SystemMouseCursors.click;
+      },
+    );
+  }
+
+  @override
+  MaterialStateProperty<Color?>? get overlayColor {
+    return MaterialStateProperty.resolveWith(
+      (Set<MaterialState> states) {
+        if (states.contains(MaterialState.hovered)) {
+          return _colors.primary.withOpacity(0.08);
+        }
+        if (states.contains(MaterialState.focused)) {
+          return _colors.primary.withOpacity(0.12);
+        }
+        if (states.contains(MaterialState.pressed)) {
+          return _colors.primary.withOpacity(0.12);
+        }
+        return null;
+      },
+    );
+  }
+
+  @override
+  MaterialStateProperty<EdgeInsetsGeometry>? get padding {
+    return ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(_scaledPadding(context));
+  }
+
+  // No default side
+
+  @override
+  MaterialStateProperty<OutlinedBorder>? get shape {
+    return ButtonStyleButton.allOrNull<OutlinedBorder>(const RoundedRectangleBorder());
+  }
+
+  @override
+  InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
+
+  @override
+  MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize;
+
+  @override
+  MaterialStateProperty<TextStyle?> get textStyle {
+    return MaterialStatePropertyAll<TextStyle?>(Theme.of(context).textTheme.labelLarge);
+  }
+
+  @override
+  VisualDensity? get visualDensity => Theme.of(context).visualDensity;
+
+  EdgeInsetsGeometry _scaledPadding(BuildContext context) {
+    return ButtonStyleButton.scaledPadding(
+      const EdgeInsets.all(8),
+      const EdgeInsets.symmetric(horizontal: 8),
+      const EdgeInsets.symmetric(horizontal: 4),
+      MediaQuery.maybeOf(context)?.textScaleFactor ?? 1,
+    );
+  }
+}
+
+// This class will eventually be auto-generated, so it should remain at the end
+// of the file.
+class _MenuDefaultsM3 extends MenuStyle {
+  _MenuDefaultsM3(this.context)
+      : super(
+          elevation: const MaterialStatePropertyAll<double?>(4),
+          shape: const MaterialStatePropertyAll<OutlinedBorder>(_defaultMenuBorder),
+          alignment: AlignmentDirectional.topEnd,
+        );
+  static const RoundedRectangleBorder _defaultMenuBorder =
+      RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.elliptical(2, 3)));
+
+  final BuildContext context;
+
+  late final ColorScheme _colors = Theme.of(context).colorScheme;
+
+  @override
+  MaterialStateProperty<Color?> get backgroundColor {
+    return MaterialStatePropertyAll<Color?>(_colors.surface);
+  }
+
+  @override
+  MaterialStateProperty<EdgeInsetsGeometry?>? get padding {
+    return MaterialStatePropertyAll<EdgeInsetsGeometry>(
+      EdgeInsetsDirectional.symmetric(
+        vertical: math.max(
+          _kMenuVerticalMinPadding,
+          2 + Theme.of(context).visualDensity.baseSizeAdjustment.dy,
+        ),
+      ),
+    );
+  }
+}
diff --git a/packages/flutter/lib/src/material/menu_bar_theme.dart b/packages/flutter/lib/src/material/menu_bar_theme.dart
new file mode 100644
index 0000000..8a8299f
--- /dev/null
+++ b/packages/flutter/lib/src/material/menu_bar_theme.dart
@@ -0,0 +1,112 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/widgets.dart';
+
+import 'menu_anchor.dart';
+import 'menu_style.dart';
+import 'menu_theme.dart';
+import 'theme.dart';
+
+// Examples can assume:
+// late Widget child;
+
+/// A data class that [MenuBarTheme] uses to define the visual properties of
+/// [MenuBar] widgets.
+///
+/// This class defines the visual properties of [MenuBar] widgets themselves,
+/// but not their submenus. Those properties are defined by [MenuThemeData] or
+/// [MenuButtonThemeData] instead.
+///
+/// Descendant widgets obtain the current [MenuBarThemeData] object using
+/// `MenuBarTheme.of(context)`.
+///
+/// Typically, a [MenuBarThemeData] is specified as part of the overall [Theme]
+/// with [ThemeData.menuBarTheme]. Otherwise, [MenuTheme] can be used to
+/// configure its own widget subtree.
+///
+/// All [MenuBarThemeData] properties are `null` by default. If any of these
+/// properties are null, the menu bar will provide its own defaults.
+///
+/// See also:
+///
+/// * [MenuThemeData], which describes the theme for the submenus of a
+///   [MenuBar].
+/// * [MenuButtonThemeData], which describes the theme for the [MenuItemButton]s
+///   in a menu.
+/// * [ThemeData], which describes the overall theme for the application.
+@immutable
+class MenuBarThemeData extends MenuThemeData {
+  /// Creates a const set of properties used to configure [MenuTheme].
+  const MenuBarThemeData({super.style});
+
+  /// Linearly interpolate between two text button themes.
+  static MenuBarThemeData? lerp(MenuBarThemeData? a, MenuBarThemeData? b, double t) {
+    return MenuBarThemeData(style: MenuStyle.lerp(a?.style, b?.style, t));
+  }
+}
+
+/// An inherited widget that defines the configuration for the [MenuBar] widgets
+/// in this widget's descendants.
+///
+/// This class defines the visual properties of [MenuBar] widgets themselves,
+/// but not their submenus. Those properties are defined by [MenuTheme] or
+/// [MenuButtonTheme] instead.
+///
+/// Values specified here are used for [MenuBar]'s properties that are not given
+/// an explicit non-null value.
+///
+/// See also:
+/// * [MenuStyle], a configuration object that holds attributes of a menu, and
+///   is used by this theme to define those attributes.
+/// * [MenuTheme], which does the same thing for the menus created by a
+///   [SubmenuButton] or [MenuAnchor].
+/// * [MenuButtonTheme], which does the same thing for the [MenuItemButton]s
+///   inside of the menus.
+/// * [SubmenuButton], a button that manages a submenu that uses these
+///   properties.
+/// * [MenuBar], a widget that creates a menu bar that can use [SubmenuButton]s.
+class MenuBarTheme extends InheritedTheme {
+  /// Creates a theme that controls the configurations for [MenuBar] and
+  /// [MenuItemButton] in its widget subtree.
+  const MenuBarTheme({
+    super.key,
+    required this.data,
+    required super.child,
+  }) : assert(data != null);
+
+  /// The properties to set for [MenuBar] in this widget's descendants.
+  final MenuBarThemeData data;
+
+  /// Returns the closest instance of this class's [data] value that encloses
+  /// the given context. If there is no ancestor, it returns
+  /// [ThemeData.menuBarTheme].
+  ///
+  /// Typical usage is as follows:
+  ///
+  /// ```dart
+  /// Widget build(BuildContext context) {
+  ///   return MenuTheme(
+  ///     data: const MenuThemeData(
+  ///       style: MenuStyle(
+  ///         backgroundColor: MaterialStatePropertyAll<Color?>(Colors.red),
+  ///       ),
+  ///     ),
+  ///     child: child,
+  ///   );
+  /// }
+  /// ```
+  static MenuBarThemeData of(BuildContext context) {
+    final MenuBarTheme? menuBarTheme = context.dependOnInheritedWidgetOfExactType<MenuBarTheme>();
+    return menuBarTheme?.data ?? Theme.of(context).menuBarTheme;
+  }
+
+  @override
+  Widget wrap(BuildContext context, Widget child) {
+    return MenuBarTheme(data: data, child: child);
+  }
+
+  @override
+  bool updateShouldNotify(MenuBarTheme oldWidget) => data != oldWidget.data;
+}
diff --git a/packages/flutter/lib/src/material/menu_button_theme.dart b/packages/flutter/lib/src/material/menu_button_theme.dart
new file mode 100644
index 0000000..f4462e1
--- /dev/null
+++ b/packages/flutter/lib/src/material/menu_button_theme.dart
@@ -0,0 +1,136 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/widgets.dart';
+
+import 'button_style.dart';
+import 'material_state.dart';
+import 'menu_anchor.dart';
+import 'theme.dart';
+
+// Examples can assume:
+// late BuildContext context;
+
+/// A [ButtonStyle] theme that overrides the default appearance of
+/// [SubmenuButton]s and [MenuItemButton]s when it's used with a
+/// [MenuButtonTheme] or with the overall [Theme]'s [ThemeData.menuTheme].
+///
+/// The [style]'s properties override [MenuItemButton]'s and [SubmenuButton]'s
+/// default style, i.e. the [ButtonStyle] returned by
+/// [MenuItemButton.defaultStyleOf] and [SubmenuButton.defaultStyleOf]. Only the
+/// style's non-null property values or resolved non-null
+/// [MaterialStateProperty] values are used.
+///
+/// See also:
+///
+/// * [MenuButtonTheme], the theme which is configured with this class.
+/// * [MenuTheme], the theme used to configure the look of the menus these
+///   buttons reside in.
+/// * [MenuItemButton.defaultStyleOf] and [SubmenuButton.defaultStyleOf] which
+///   return the default [ButtonStyle]s for menu buttons.
+/// * [MenuItemButton.styleFrom] and [SubmenuButton.styleFrom], which converts
+///   simple values into a [ButtonStyle] that's consistent with their respective
+///   defaults.
+/// * [MaterialStateProperty.resolve], "resolve" a material state property to a
+///   simple value based on a set of [MaterialState]s.
+/// * [ThemeData.menuButtonTheme], which can be used to override the default
+///   [ButtonStyle] for [MenuItemButton]s and [SubmenuButton]s below the overall
+///   [Theme].
+/// * [MenuAnchor], a widget which hosts cascading menus.
+/// * [MenuBar], a widget which defines a menu bar of buttons hosting cascading
+///   menus.
+@immutable
+class MenuButtonThemeData with Diagnosticable {
+  /// Creates a [MenuButtonThemeData].
+  ///
+  /// The [style] may be null.
+  const MenuButtonThemeData({this.style});
+
+  /// Overrides for [SubmenuButton] and [MenuItemButton]'s default style.
+  ///
+  /// Non-null properties or non-null resolved [MaterialStateProperty] values
+  /// override the [ButtonStyle] returned by [SubmenuButton.defaultStyleOf] or
+  /// [MenuItemButton.defaultStyleOf].
+  ///
+  /// If [style] is null, then this theme doesn't override anything.
+  final ButtonStyle? style;
+
+  /// Linearly interpolate between two menu button themes.
+  static MenuButtonThemeData? lerp(MenuButtonThemeData? a, MenuButtonThemeData? b, double t) {
+    return MenuButtonThemeData(style: ButtonStyle.lerp(a?.style, b?.style, t));
+  }
+
+  @override
+  int get hashCode => style.hashCode;
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) {
+      return true;
+    }
+    if (other.runtimeType != runtimeType) {
+      return false;
+    }
+    return other is MenuButtonThemeData && other.style == style;
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<ButtonStyle>('style', style, defaultValue: null));
+  }
+}
+
+/// Overrides the default [ButtonStyle] of its [MenuItemButton] and
+/// [SubmenuButton] descendants.
+///
+/// See also:
+///
+/// * [MenuButtonThemeData], which is used to configure this theme.
+/// * [MenuTheme], the theme used to configure the look of the menus themselves.
+/// * [MenuItemButton.defaultStyleOf] and [SubmenuButton.defaultStyleOf] which
+///   return the default [ButtonStyle]s for menu buttons.
+/// * [MenuItemButton.styleFrom] and [SubmenuButton.styleFrom], which converts
+///   simple values into a [ButtonStyle] that's consistent with their respective
+///   defaults.
+/// * [ThemeData.menuButtonTheme], which can be used to override the default
+///   [ButtonStyle] for [MenuItemButton]s and [SubmenuButton]s below the overall
+///   [Theme].
+class MenuButtonTheme extends InheritedTheme {
+  /// Create a [MenuButtonTheme].
+  ///
+  /// The [data] parameter must not be null.
+  const MenuButtonTheme({
+    super.key,
+    required this.data,
+    required super.child,
+  }) : assert(data != null);
+
+  /// The configuration of this theme.
+  final MenuButtonThemeData data;
+
+  /// The closest instance of this class that encloses the given context.
+  ///
+  /// If there is no enclosing [MenuButtonTheme] widget, then
+  /// [ThemeData.menuButtonTheme] is used.
+  ///
+  /// Typical usage is as follows:
+  ///
+  /// ```dart
+  /// MenuButtonThemeData theme = MenuButtonTheme.of(context);
+  /// ```
+  static MenuButtonThemeData of(BuildContext context) {
+    final MenuButtonTheme? buttonTheme = context.dependOnInheritedWidgetOfExactType<MenuButtonTheme>();
+    return buttonTheme?.data ?? Theme.of(context).menuButtonTheme;
+  }
+
+  @override
+  Widget wrap(BuildContext context, Widget child) {
+    return MenuButtonTheme(data: data, child: child);
+  }
+
+  @override
+  bool updateShouldNotify(MenuButtonTheme oldWidget) => data != oldWidget.data;
+}
diff --git a/packages/flutter/lib/src/material/menu_style.dart b/packages/flutter/lib/src/material/menu_style.dart
new file mode 100644
index 0000000..a335eee
--- /dev/null
+++ b/packages/flutter/lib/src/material/menu_style.dart
@@ -0,0 +1,371 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:ui' show lerpDouble;
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/widgets.dart';
+
+import 'button_style.dart';
+import 'material_state.dart';
+import 'menu_anchor.dart';
+import 'theme.dart';
+import 'theme_data.dart';
+
+// Examples can assume:
+// late Widget child;
+// late BuildContext context;
+// late MenuStyle style;
+// @immutable
+// class MyAppHome extends StatelessWidget {
+//   const MyAppHome({super.key});
+//   @override
+//   Widget build(BuildContext context) => const SizedBox();
+// }
+
+/// The visual properties that menus have in common.
+///
+/// Menus created by [MenuBar] and [MenuAnchor] and their themes have a
+/// [MenuStyle] property which defines the visual properties whose default
+/// values are to be overridden. The default values are defined by the
+/// individual menu widgets and are typically based on overall theme's
+/// [ThemeData.colorScheme] and [ThemeData.textTheme].
+///
+/// All of the [MenuStyle] properties are null by default.
+///
+/// Many of the [MenuStyle] properties are [MaterialStateProperty] objects which
+/// resolve to different values depending on the menu's state. For example the
+/// [Color] properties are defined with `MaterialStateProperty<Color>` and can
+/// resolve to different colors depending on if the menu is pressed, hovered,
+/// focused, disabled, etc.
+///
+/// These properties can override the default value for just one state or all of
+/// them. For example to create a [SubmenuButton] whose background color is the
+/// color scheme’s primary color with 50% opacity, but only when the menu is
+/// pressed, one could write:
+///
+/// ```dart
+/// SubmenuButton(
+///   menuStyle: MenuStyle(
+///     backgroundColor: MaterialStateProperty.resolveWith<Color?>(
+///       (Set<MaterialState> states) {
+///         if (states.contains(MaterialState.focused)) {
+///           return Theme.of(context).colorScheme.primary.withOpacity(0.5);
+///         }
+///         return null; // Use the component's default.
+///       },
+///     ),
+///   ),
+///   menuChildren: const <Widget>[ /* ... */ ],
+///   child: const Text('Fly me to the moon'),
+/// ),
+/// ```
+///
+/// In this case the background color for all other menu states would fall back
+/// to the [SubmenuButton]'s default values. To unconditionally set the menu's
+/// [backgroundColor] for all states one could write:
+///
+/// ```dart
+/// const SubmenuButton(
+///   menuStyle: MenuStyle(
+///     backgroundColor: MaterialStatePropertyAll<Color>(Colors.green),
+///   ),
+///   menuChildren: <Widget>[ /* ... */ ],
+///   child: Text('Let me play among the stars'),
+/// ),
+/// ```
+///
+/// To configure all of the application's menus in the same way, specify the
+/// overall theme's `menuTheme`:
+///
+/// ```dart
+/// MaterialApp(
+///   theme: ThemeData(
+///     menuTheme: const MenuThemeData(
+///       style: MenuStyle(backgroundColor: MaterialStatePropertyAll<Color>(Colors.red)),
+///     ),
+///   ),
+///   home: const MyAppHome(),
+/// ),
+/// ```
+///
+/// See also:
+///
+/// * [MenuAnchor], a widget which hosts cascading menus.
+/// * [MenuBar], a widget which defines a menu bar of buttons hosting cascading
+///   menus.
+/// * [MenuButtonTheme], the theme for [SubmenuButton]s and [MenuItemButton]s.
+/// * [ButtonStyle], a similar configuration object for button styles.
+@immutable
+class MenuStyle with Diagnosticable {
+  /// Create a [MenuStyle].
+  const MenuStyle({
+    this.backgroundColor,
+    this.shadowColor,
+    this.surfaceTintColor,
+    this.elevation,
+    this.padding,
+    this.minimumSize,
+    this.fixedSize,
+    this.maximumSize,
+    this.side,
+    this.shape,
+    this.mouseCursor,
+    this.visualDensity,
+    this.alignment,
+  });
+
+  /// The menu's background fill color.
+  final MaterialStateProperty<Color?>? backgroundColor;
+
+  /// The shadow color of the menu's [Material].
+  ///
+  /// The material's elevation shadow can be difficult to see for dark themes,
+  /// so by default the menu classes add a semi-transparent overlay to indicate
+  /// elevation. See [ThemeData.applyElevationOverlayColor].
+  final MaterialStateProperty<Color?>? shadowColor;
+
+  /// The surface tint color of the menu's [Material].
+  ///
+  /// See [Material.surfaceTintColor] for more details.
+  final MaterialStateProperty<Color?>? surfaceTintColor;
+
+  /// The elevation of the menu's [Material].
+  final MaterialStateProperty<double?>? elevation;
+
+  /// The padding between the menu's boundary and its child.
+  final MaterialStateProperty<EdgeInsetsGeometry?>? padding;
+
+  /// The minimum size of the menu itself.
+  ///
+  /// This value must be less than or equal to [maximumSize].
+  final MaterialStateProperty<Size?>? minimumSize;
+
+  /// The menu's size.
+  ///
+  /// This size is still constrained by the style's [minimumSize] and
+  /// [maximumSize]. Fixed size dimensions whose value is [double.infinity] are
+  /// ignored.
+  ///
+  /// To specify menus with a fixed width and the default height use `fixedSize:
+  /// Size.fromWidth(320)`. Similarly, to specify a fixed height and the default
+  /// width use `fixedSize: Size.fromHeight(100)`.
+  final MaterialStateProperty<Size?>? fixedSize;
+
+  /// The maximum size of the menu itself.
+  ///
+  /// A [Size.infinite] or null value for this property means that the menu's
+  /// maximum size is not constrained.
+  ///
+  /// This value must be greater than or equal to [minimumSize].
+  final MaterialStateProperty<Size?>? maximumSize;
+
+  /// The color and weight of the menu's outline.
+  ///
+  /// This value is combined with [shape] to create a shape decorated with an
+  /// outline.
+  final MaterialStateProperty<BorderSide?>? side;
+
+  /// The shape of the menu's underlying [Material].
+  ///
+  /// This shape is combined with [side] to create a shape decorated with an
+  /// outline.
+  final MaterialStateProperty<OutlinedBorder?>? shape;
+
+  /// The cursor for a mouse pointer when it enters or is hovering over this
+  /// menu's [InkWell].
+  final MaterialStateProperty<MouseCursor?>? mouseCursor;
+
+  /// Defines how compact the menu's layout will be.
+  ///
+  /// {@macro flutter.material.themedata.visualDensity}
+  ///
+  /// See also:
+  ///
+  ///  * [ThemeData.visualDensity], which specifies the [visualDensity] for all
+  ///    widgets within a [Theme].
+  final VisualDensity? visualDensity;
+
+  /// Determines the desired alignment of the submenu when opened relative to
+  /// the button that opens it.
+  ///
+  /// If there isn't sufficient space to open the menu with the given alignment,
+  /// and there's space on the other side of the button, then the alignment is
+  /// swapped to it's opposite (1 becomes -1, etc.), and the menu will try to
+  /// appear on the other side of the button. If there isn't enough space there
+  /// either, then the menu will be pushed as far over as necessary to display
+  /// as much of itself as possible, possibly overlapping the parent button.
+  final AlignmentGeometry? alignment;
+
+  @override
+  int get hashCode {
+    final List<Object?> values = <Object?>[
+      backgroundColor,
+      shadowColor,
+      surfaceTintColor,
+      elevation,
+      padding,
+      minimumSize,
+      fixedSize,
+      maximumSize,
+      side,
+      shape,
+      mouseCursor,
+      visualDensity,
+      alignment,
+    ];
+    return Object.hashAll(values);
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) {
+      return true;
+    }
+    if (other.runtimeType != runtimeType) {
+      return false;
+    }
+    return other is MenuStyle
+        && other.backgroundColor == backgroundColor
+        && other.shadowColor == shadowColor
+        && other.surfaceTintColor == surfaceTintColor
+        && other.elevation == elevation
+        && other.padding == padding
+        && other.minimumSize == minimumSize
+        && other.fixedSize == fixedSize
+        && other.maximumSize == maximumSize
+        && other.side == side
+        && other.shape == shape
+        && other.mouseCursor == mouseCursor
+        && other.visualDensity == visualDensity
+        && other.alignment == alignment;
+  }
+
+  /// Returns a copy of this MenuStyle with the given fields replaced with
+  /// the new values.
+  MenuStyle copyWith({
+    MaterialStateProperty<Color?>? backgroundColor,
+    MaterialStateProperty<Color?>? shadowColor,
+    MaterialStateProperty<Color?>? surfaceTintColor,
+    MaterialStateProperty<double?>? elevation,
+    MaterialStateProperty<EdgeInsetsGeometry?>? padding,
+    MaterialStateProperty<Size?>? minimumSize,
+    MaterialStateProperty<Size?>? fixedSize,
+    MaterialStateProperty<Size?>? maximumSize,
+    MaterialStateProperty<BorderSide?>? side,
+    MaterialStateProperty<OutlinedBorder?>? shape,
+    MaterialStateProperty<MouseCursor?>? mouseCursor,
+    VisualDensity? visualDensity,
+    AlignmentGeometry? alignment,
+  }) {
+    return MenuStyle(
+      backgroundColor: backgroundColor ?? this.backgroundColor,
+      shadowColor: shadowColor ?? this.shadowColor,
+      surfaceTintColor: surfaceTintColor ?? this.surfaceTintColor,
+      elevation: elevation ?? this.elevation,
+      padding: padding ?? this.padding,
+      minimumSize: minimumSize ?? this.minimumSize,
+      fixedSize: fixedSize ?? this.fixedSize,
+      maximumSize: maximumSize ?? this.maximumSize,
+      side: side ?? this.side,
+      shape: shape ?? this.shape,
+      mouseCursor: mouseCursor ?? this.mouseCursor,
+      visualDensity: visualDensity ?? this.visualDensity,
+      alignment: alignment ?? this.alignment,
+    );
+  }
+
+  /// Returns a copy of this MenuStyle where the non-null fields in [style]
+  /// have replaced the corresponding null fields in this MenuStyle.
+  ///
+  /// In other words, [style] is used to fill in unspecified (null) fields
+  /// this MenuStyle.
+  MenuStyle merge(MenuStyle? style) {
+    if (style == null) {
+      return this;
+    }
+    return copyWith(
+      backgroundColor: backgroundColor ?? style.backgroundColor,
+      shadowColor: shadowColor ?? style.shadowColor,
+      surfaceTintColor: surfaceTintColor ?? style.surfaceTintColor,
+      elevation: elevation ?? style.elevation,
+      padding: padding ?? style.padding,
+      minimumSize: minimumSize ?? style.minimumSize,
+      fixedSize: fixedSize ?? style.fixedSize,
+      maximumSize: maximumSize ?? style.maximumSize,
+      side: side ?? style.side,
+      shape: shape ?? style.shape,
+      mouseCursor: mouseCursor ?? style.mouseCursor,
+      visualDensity: visualDensity ?? style.visualDensity,
+      alignment: alignment ?? style.alignment,
+    );
+  }
+
+  /// Linearly interpolate between two [MenuStyle]s.
+  static MenuStyle? lerp(MenuStyle? a, MenuStyle? b, double t) {
+    assert (t != null);
+    if (a == null && b == null) {
+      return null;
+    }
+    return MenuStyle(
+      backgroundColor: MaterialStateProperty.lerp<Color?>(a?.backgroundColor, b?.backgroundColor, t, Color.lerp),
+      shadowColor: MaterialStateProperty.lerp<Color?>(a?.shadowColor, b?.shadowColor, t, Color.lerp),
+      surfaceTintColor: MaterialStateProperty.lerp<Color?>(a?.surfaceTintColor, b?.surfaceTintColor, t, Color.lerp),
+      elevation: MaterialStateProperty.lerp<double?>(a?.elevation, b?.elevation, t, lerpDouble),
+      padding:  MaterialStateProperty.lerp<EdgeInsetsGeometry?>(a?.padding, b?.padding, t, EdgeInsetsGeometry.lerp),
+      minimumSize: MaterialStateProperty.lerp<Size?>(a?.minimumSize, b?.minimumSize, t, Size.lerp),
+      fixedSize: MaterialStateProperty.lerp<Size?>(a?.fixedSize, b?.fixedSize, t, Size.lerp),
+      maximumSize: MaterialStateProperty.lerp<Size?>(a?.maximumSize, b?.maximumSize, t, Size.lerp),
+      side: _LerpSides(a?.side, b?.side, t),
+      shape: MaterialStateProperty.lerp<OutlinedBorder?>(a?.shape, b?.shape, t, OutlinedBorder.lerp),
+      mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor,
+      visualDensity: t < 0.5 ? a?.visualDensity : b?.visualDensity,
+      alignment: AlignmentGeometry.lerp(a?.alignment, b?.alignment, t),
+    );
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('backgroundColor', backgroundColor, defaultValue: null));
+    properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('shadowColor', shadowColor, defaultValue: null));
+    properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('surfaceTintColor', surfaceTintColor, defaultValue: null));
+    properties.add(DiagnosticsProperty<MaterialStateProperty<double?>>('elevation', elevation, defaultValue: null));
+    properties.add(DiagnosticsProperty<MaterialStateProperty<EdgeInsetsGeometry?>>('padding', padding, defaultValue: null));
+    properties.add(DiagnosticsProperty<MaterialStateProperty<Size?>>('minimumSize', minimumSize, defaultValue: null));
+    properties.add(DiagnosticsProperty<MaterialStateProperty<Size?>>('fixedSize', fixedSize, defaultValue: null));
+    properties.add(DiagnosticsProperty<MaterialStateProperty<Size?>>('maximumSize', maximumSize, defaultValue: null));
+    properties.add(DiagnosticsProperty<MaterialStateProperty<BorderSide?>>('side', side, defaultValue: null));
+    properties.add(DiagnosticsProperty<MaterialStateProperty<OutlinedBorder?>>('shape', shape, defaultValue: null));
+    properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: null));
+    properties.add(DiagnosticsProperty<VisualDensity>('visualDensity', visualDensity, defaultValue: null));
+    properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment, defaultValue: null));
+  }
+}
+
+/// A required helper class because [BorderSide.lerp] doesn't support passing or
+/// returning null values.
+class _LerpSides implements MaterialStateProperty<BorderSide?> {
+  const _LerpSides(this.a, this.b, this.t);
+
+  final MaterialStateProperty<BorderSide?>? a;
+  final MaterialStateProperty<BorderSide?>? b;
+  final double t;
+
+  @override
+  BorderSide? resolve(Set<MaterialState> states) {
+    final BorderSide? resolvedA = a?.resolve(states);
+    final BorderSide? resolvedB = b?.resolve(states);
+    if (resolvedA == null && resolvedB == null) {
+      return null;
+    }
+    if (resolvedA == null) {
+      return BorderSide.lerp(BorderSide(width: 0, color: resolvedB!.color.withAlpha(0)), resolvedB, t);
+    }
+    if (resolvedB == null) {
+      return BorderSide.lerp(resolvedA, BorderSide(width: 0, color: resolvedA.color.withAlpha(0)), t);
+    }
+    return BorderSide.lerp(resolvedA, resolvedB, t);
+  }
+}
diff --git a/packages/flutter/lib/src/material/menu_theme.dart b/packages/flutter/lib/src/material/menu_theme.dart
new file mode 100644
index 0000000..b1c098b
--- /dev/null
+++ b/packages/flutter/lib/src/material/menu_theme.dart
@@ -0,0 +1,130 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/widgets.dart';
+
+import 'menu_anchor.dart';
+import 'menu_style.dart';
+import 'theme.dart';
+
+// Examples can assume:
+// late Widget child;
+
+/// Defines the configuration of the submenus created by the [SubmenuButton],
+/// [MenuBar], or [MenuAnchor] widgets.
+///
+/// Descendant widgets obtain the current [MenuThemeData] object using
+/// `MenuTheme.of(context)`.
+///
+/// Typically, a [MenuThemeData] is specified as part of the overall [Theme]
+/// with [ThemeData.menuTheme]. Otherwise, [MenuTheme] can be used to configure
+/// its own widget subtree.
+///
+/// All [MenuThemeData] properties are `null` by default. If any of these
+/// properties are null, the menu bar will provide its own defaults.
+///
+/// See also:
+///
+/// * [ThemeData], which describes the overall theme for the application.
+/// * [MenuBarThemeData], which describes the theme for the menu bar itself in a
+///   [MenuBar] widget.
+@immutable
+class MenuThemeData with Diagnosticable {
+  /// Creates a const set of properties used to configure [MenuTheme].
+  const MenuThemeData({this.style});
+
+  /// The [MenuStyle] of a [SubmenuButton] menu.
+  ///
+  /// Any values not set in the [MenuStyle] will use the menu default for that
+  /// property.
+  final MenuStyle? style;
+
+  /// Linearly interpolate between two menu button themes.
+  static MenuThemeData? lerp(MenuThemeData? a, MenuThemeData? b, double t) {
+    return MenuThemeData(style: MenuStyle.lerp(a?.style, b?.style, t));
+  }
+
+  @override
+  int get hashCode => style.hashCode;
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) {
+      return true;
+    }
+    if (other.runtimeType != runtimeType) {
+      return false;
+    }
+    return other is MenuThemeData && other.style == style;
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(DiagnosticsProperty<MenuStyle>('style', style, defaultValue: null));
+  }
+}
+
+/// An inherited widget that defines the configuration in this widget's
+/// descendants for menus created by the [SubmenuButton], [MenuBar], or
+/// [MenuAnchor] widgets.
+///
+/// Values specified here are used for [SubmenuButton]'s menu properties that
+/// are not given an explicit non-null value.
+///
+/// See also:
+///
+/// * [MenuThemeData], a configuration object that holds attributes of a menu
+///   used by this theme.
+/// * [MenuBarTheme], which does the same thing for the [MenuBar] widget.
+/// * [MenuBar], a widget that manages [MenuItemButton]s.
+/// * [MenuAnchor], a widget that creates a region that has a submenu.
+/// * [MenuItemButton], a widget that is a selectable item in a menu bar menu.
+/// * [SubmenuButton], a widget that specifies an item with a cascading submenu
+///   in a [MenuBar] menu.
+class MenuTheme extends InheritedTheme {
+  /// Creates a const theme that controls the configurations for the menus
+  /// created by the [SubmenuButton] or [MenuAnchor] widgets.
+  const MenuTheme({
+    super.key,
+    required this.data,
+    required super.child,
+  }) : assert(data != null);
+
+  /// The properties for [MenuBar] and [MenuItemButton] in this widget's
+  /// descendants.
+  final MenuThemeData data;
+
+  /// Returns the closest instance of this class's [data] value that encloses
+  /// the given context. If there is no ancestor, it returns
+  /// [ThemeData.menuTheme].
+  ///
+  /// Typical usage is as follows:
+  ///
+  /// ```dart
+  /// Widget build(BuildContext context) {
+  ///   return MenuTheme(
+  ///     data: const MenuThemeData(
+  ///       style: MenuStyle(
+  ///         backgroundColor: MaterialStatePropertyAll<Color>(Colors.red),
+  ///       ),
+  ///     ),
+  ///     child: child,
+  ///   );
+  /// }
+  /// ```
+  static MenuThemeData of(BuildContext context) {
+    final MenuTheme? menuTheme = context.dependOnInheritedWidgetOfExactType<MenuTheme>();
+    return menuTheme?.data ?? Theme.of(context).menuTheme;
+  }
+
+  @override
+  Widget wrap(BuildContext context, Widget child) {
+    return MenuTheme(data: data, child: child);
+  }
+
+  @override
+  bool updateShouldNotify(MenuTheme oldWidget) => data != oldWidget.data;
+}
diff --git a/packages/flutter/lib/src/material/text_button.dart b/packages/flutter/lib/src/material/text_button.dart
index 9be4e53..b3dc1b5 100644
--- a/packages/flutter/lib/src/material/text_button.dart
+++ b/packages/flutter/lib/src/material/text_button.dart
@@ -222,13 +222,14 @@
 
   /// Defines the button's default appearance.
   ///
+  /// {@template flutter.material.text_button.default_style_of}
   /// The button [child]'s [Text] and [Icon] widgets are rendered with
   /// the [ButtonStyle]'s foreground color. The button's [InkWell] adds
   /// the style's overlay color when the button is focused, hovered
   /// or pressed. The button's background color becomes its [Material]
   /// color and is transparent by default.
   ///
-  /// All of the ButtonStyle's defaults appear below.
+  /// All of the [ButtonStyle]'s defaults appear below.
   ///
   /// In this list "Theme.foo" is shorthand for
   /// `Theme.of(context).foo`. Color scheme values like
@@ -245,6 +246,7 @@
   ///
   /// The color of the [ButtonStyle.textStyle] is not used, the
   /// [ButtonStyle.foregroundColor] color is used instead.
+  /// {@endtemplate}
   ///
   /// ## Material 2 defaults
   ///
@@ -295,6 +297,7 @@
   /// If [ThemeData.useMaterial3] is set to true the following defaults will
   /// be used:
   ///
+  /// {@template flutter.material.text_button.material3_defaults}
   /// * `textStyle` - Theme.textTheme.labelLarge
   /// * `backgroundColor` - transparent
   /// * `foregroundColor`
@@ -326,6 +329,7 @@
   /// * `enableFeedback` - true
   /// * `alignment` - Alignment.center
   /// * `splashFactory` - Theme.splashFactory
+  /// {@endtemplate}
   @override
   ButtonStyle defaultStyleOf(BuildContext context) {
     final ThemeData theme = Theme.of(context);
diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart
index 30cd867..3b1e90c 100644
--- a/packages/flutter/lib/src/material/theme_data.dart
+++ b/packages/flutter/lib/src/material/theme_data.dart
@@ -36,6 +36,9 @@
 import 'input_decorator.dart';
 import 'list_tile.dart';
 import 'list_tile_theme.dart';
+import 'menu_bar_theme.dart';
+import 'menu_button_theme.dart';
+import 'menu_theme.dart';
 import 'navigation_bar_theme.dart';
 import 'navigation_rail_theme.dart';
 import 'outlined_button_theme.dart';
@@ -345,6 +348,9 @@
     FloatingActionButtonThemeData? floatingActionButtonTheme,
     IconButtonThemeData? iconButtonTheme,
     ListTileThemeData? listTileTheme,
+    MenuBarThemeData? menuBarTheme,
+    MenuButtonThemeData? menuButtonTheme,
+    MenuThemeData? menuTheme,
     NavigationBarThemeData? navigationBarTheme,
     NavigationRailThemeData? navigationRailTheme,
     OutlinedButtonThemeData? outlinedButtonTheme,
@@ -568,17 +574,21 @@
     bottomSheetTheme ??= const BottomSheetThemeData();
     buttonBarTheme ??= const ButtonBarThemeData();
     cardTheme ??= const CardTheme();
-    chipTheme ??= const ChipThemeData();
     checkboxTheme ??= const CheckboxThemeData();
+    chipTheme ??= const ChipThemeData();
     dataTableTheme ??= const DataTableThemeData();
     dialogTheme ??= const DialogTheme();
     dividerTheme ??= const DividerThemeData();
     drawerTheme ??= const DrawerThemeData();
     elevatedButtonTheme ??= const ElevatedButtonThemeData();
+    expansionTileTheme ??= const ExpansionTileThemeData();
     filledButtonTheme ??= const FilledButtonThemeData();
     floatingActionButtonTheme ??= const FloatingActionButtonThemeData();
     iconButtonTheme ??= const IconButtonThemeData();
     listTileTheme ??= const ListTileThemeData();
+    menuBarTheme ??= const MenuBarThemeData();
+    menuButtonTheme ??= const MenuButtonThemeData();
+    menuTheme ??= const MenuThemeData();
     navigationBarTheme ??= const NavigationBarThemeData();
     navigationRailTheme ??= const NavigationRailThemeData();
     outlinedButtonTheme ??= const OutlinedButtonThemeData();
@@ -594,7 +604,6 @@
     timePickerTheme ??= const TimePickerThemeData();
     toggleButtonsTheme ??= const ToggleButtonsThemeData();
     tooltipTheme ??= const TooltipThemeData();
-    expansionTileTheme ??= const ExpansionTileThemeData();
 
     // DEPRECATED (newest deprecations at the bottom)
     accentTextTheme = defaultAccentTextTheme.merge(accentTextTheme);
@@ -671,6 +680,9 @@
       floatingActionButtonTheme: floatingActionButtonTheme,
       iconButtonTheme: iconButtonTheme,
       listTileTheme: listTileTheme,
+      menuBarTheme: menuBarTheme,
+      menuButtonTheme: menuButtonTheme,
+      menuTheme: menuTheme,
       navigationBarTheme: navigationBarTheme,
       navigationRailTheme: navigationRailTheme,
       outlinedButtonTheme: outlinedButtonTheme,
@@ -778,6 +790,9 @@
     required this.floatingActionButtonTheme,
     required this.iconButtonTheme,
     required this.listTileTheme,
+    required this.menuBarTheme,
+    required this.menuButtonTheme,
+    required this.menuTheme,
     required this.navigationBarTheme,
     required this.navigationRailTheme,
     required this.outlinedButtonTheme,
@@ -943,6 +958,9 @@
        assert(floatingActionButtonTheme != null),
        assert(iconButtonTheme != null),
        assert(listTileTheme != null),
+       assert(menuBarTheme != null),
+       assert(menuButtonTheme != null),
+       assert(menuTheme != null),
        assert(navigationBarTheme != null),
        assert(navigationRailTheme != null),
        assert(outlinedButtonTheme != null),
@@ -1520,6 +1538,18 @@
   /// A theme for customizing the appearance of [ListTile] widgets.
   final ListTileThemeData listTileTheme;
 
+  /// A theme for customizing the color, shape, elevation, and other [MenuStyle]
+  /// aspects of the menu bar created by the [MenuBar] widget.
+  final MenuBarThemeData menuBarTheme;
+
+  /// A theme for customizing the color, shape, elevation, and text style of
+  /// cascading menu buttons created by [SubmenuButton] or [MenuItemButton].
+  final MenuButtonThemeData menuButtonTheme;
+
+  /// A theme for customizing the color, shape, elevation, and other [MenuStyle]
+  /// attributes of menus created by the [SubmenuButton] widget.
+  final MenuThemeData menuTheme;
+
   /// A theme for customizing the background color, text style, and icon themes
   /// of a [NavigationBar].
   final NavigationBarThemeData navigationBarTheme;
@@ -1814,6 +1844,9 @@
     FloatingActionButtonThemeData? floatingActionButtonTheme,
     IconButtonThemeData? iconButtonTheme,
     ListTileThemeData? listTileTheme,
+    MenuBarThemeData? menuBarTheme,
+    MenuButtonThemeData? menuButtonTheme,
+    MenuThemeData? menuTheme,
     NavigationBarThemeData? navigationBarTheme,
     NavigationRailThemeData? navigationRailTheme,
     OutlinedButtonThemeData? outlinedButtonTheme,
@@ -1972,6 +2005,9 @@
       floatingActionButtonTheme: floatingActionButtonTheme ?? this.floatingActionButtonTheme,
       iconButtonTheme: iconButtonTheme ?? this.iconButtonTheme,
       listTileTheme: listTileTheme ?? this.listTileTheme,
+      menuBarTheme: menuBarTheme ?? this.menuBarTheme,
+      menuButtonTheme: menuButtonTheme ?? this.menuButtonTheme,
+      menuTheme: menuTheme ?? this.menuTheme,
       navigationBarTheme: navigationBarTheme ?? this.navigationBarTheme,
       navigationRailTheme: navigationRailTheme ?? this.navigationRailTheme,
       outlinedButtonTheme: outlinedButtonTheme ?? this.outlinedButtonTheme,
@@ -2172,6 +2208,9 @@
       floatingActionButtonTheme: FloatingActionButtonThemeData.lerp(a.floatingActionButtonTheme, b.floatingActionButtonTheme, t)!,
       iconButtonTheme: IconButtonThemeData.lerp(a.iconButtonTheme, b.iconButtonTheme, t)!,
       listTileTheme: ListTileThemeData.lerp(a.listTileTheme, b.listTileTheme, t)!,
+      menuBarTheme: MenuBarThemeData.lerp(a.menuBarTheme, b.menuBarTheme, t)!,
+      menuButtonTheme: MenuButtonThemeData.lerp(a.menuButtonTheme, b.menuButtonTheme, t)!,
+      menuTheme: MenuThemeData.lerp(a.menuTheme, b.menuTheme, t)!,
       navigationBarTheme: NavigationBarThemeData.lerp(a.navigationBarTheme, b.navigationBarTheme, t)!,
       navigationRailTheme: NavigationRailThemeData.lerp(a.navigationRailTheme, b.navigationRailTheme, t)!,
       outlinedButtonTheme: OutlinedButtonThemeData.lerp(a.outlinedButtonTheme, b.outlinedButtonTheme, t)!,
@@ -2274,6 +2313,9 @@
         other.floatingActionButtonTheme == floatingActionButtonTheme &&
         other.iconButtonTheme == iconButtonTheme &&
         other.listTileTheme == listTileTheme &&
+        other.menuBarTheme == menuBarTheme &&
+        other.menuButtonTheme == menuButtonTheme &&
+        other.menuTheme == menuTheme &&
         other.navigationBarTheme == navigationBarTheme &&
         other.navigationRailTheme == navigationRailTheme &&
         other.outlinedButtonTheme == outlinedButtonTheme &&
@@ -2373,6 +2415,9 @@
       floatingActionButtonTheme,
       iconButtonTheme,
       listTileTheme,
+      menuBarTheme,
+      menuButtonTheme,
+      menuTheme,
       navigationBarTheme,
       navigationRailTheme,
       outlinedButtonTheme,
@@ -2474,6 +2519,9 @@
     properties.add(DiagnosticsProperty<FloatingActionButtonThemeData>('floatingActionButtonTheme', floatingActionButtonTheme, defaultValue: defaultData.floatingActionButtonTheme, level: DiagnosticLevel.debug));
     properties.add(DiagnosticsProperty<IconButtonThemeData>('iconButtonTheme', iconButtonTheme, defaultValue: defaultData.iconButtonTheme, level: DiagnosticLevel.debug));
     properties.add(DiagnosticsProperty<ListTileThemeData>('listTileTheme', listTileTheme, defaultValue: defaultData.listTileTheme, level: DiagnosticLevel.debug));
+    properties.add(DiagnosticsProperty<MenuBarThemeData>('menuBarTheme', menuBarTheme, defaultValue: defaultData.menuBarTheme, level: DiagnosticLevel.debug));
+    properties.add(DiagnosticsProperty<MenuButtonThemeData>('menuButtonTheme', menuButtonTheme, defaultValue: defaultData.menuButtonTheme, level: DiagnosticLevel.debug));
+    properties.add(DiagnosticsProperty<MenuThemeData>('menuTheme', menuTheme, defaultValue: defaultData.menuTheme, level: DiagnosticLevel.debug));
     properties.add(DiagnosticsProperty<NavigationBarThemeData>('navigationBarTheme', navigationBarTheme, defaultValue: defaultData.navigationBarTheme, level: DiagnosticLevel.debug));
     properties.add(DiagnosticsProperty<NavigationRailThemeData>('navigationRailTheme', navigationRailTheme, defaultValue: defaultData.navigationRailTheme, level: DiagnosticLevel.debug));
     properties.add(DiagnosticsProperty<OutlinedButtonThemeData>('outlinedButtonTheme', outlinedButtonTheme, defaultValue: defaultData.outlinedButtonTheme, level: DiagnosticLevel.debug));
@@ -2839,7 +2887,7 @@
   Offset get baseSizeAdjustment {
     // The number of logical pixels represented by an increase or decrease in
     // density by one. The Material Design guidelines say to increment/decrement
-    // sized in terms of four pixel increments.
+    // sizes in terms of four pixel increments.
     const double interval = 4.0;
 
     return Offset(horizontal, vertical) * interval;
diff --git a/packages/flutter/lib/src/rendering/layer.dart b/packages/flutter/lib/src/rendering/layer.dart
index b1cf4ee..a298d50 100644
--- a/packages/flutter/lib/src/rendering/layer.dart
+++ b/packages/flutter/lib/src/rendering/layer.dart
@@ -2419,7 +2419,9 @@
   Size? leaderSize;
 
   @override
-  String toString() => '${describeIdentity(this)}(${ _leader != null ? "<linked>" : "<dangling>" })';
+  String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
+    return '${describeIdentity(this)}(${ _leader != null ? "<linked>" : "<dangling>" })';
+  }
 }
 
 /// A composited layer that can be followed by a [FollowerLayer].
diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart
index 6465d88..074711c 100644
--- a/packages/flutter/lib/src/rendering/proxy_box.dart
+++ b/packages/flutter/lib/src/rendering/proxy_box.dart
@@ -4996,7 +4996,7 @@
   void paint(PaintingContext context, Offset offset) {
     final Size? leaderSize = link.leaderSize;
     assert(
-      link.leaderSize != null || (link.leader == null || leaderAnchor == Alignment.topLeft),
+      link.leaderSize != null || link.leader == null || leaderAnchor == Alignment.topLeft,
       '$link: layer is linked to ${link.leader} but a valid leaderSize is not set. '
       'leaderSize is required when leaderAnchor is not Alignment.topLeft '
       '(current value is $leaderAnchor).',
diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart
index 6fe55c0..8c86b27 100644
--- a/packages/flutter/lib/src/widgets/basic.dart
+++ b/packages/flutter/lib/src/widgets/basic.dart
@@ -85,7 +85,7 @@
 /// infrequently change. This provides a performance tradeoff where building
 /// the [Widget]s is faster but performing updates is slower.
 ///
-/// |                     | _UbiquitiousInheritedElement | InheritedElement |
+/// |                     | _UbiquitousInheritedElement | InheritedElement |
 /// |---------------------|------------------------------|------------------|
 /// | insert (best case)  | O(1)                         | O(1)             |
 /// | insert (worst case) | O(1)                         | O(n)             |
diff --git a/packages/flutter/test/material/menu_anchor_test.dart b/packages/flutter/test/material/menu_anchor_test.dart
new file mode 100644
index 0000000..c8249b0
--- /dev/null
+++ b/packages/flutter/test/material/menu_anchor_test.dart
@@ -0,0 +1,1793 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  late MenuController controller;
+  String? focusedMenu;
+  final List<TestMenu> selected = <TestMenu>[];
+  final List<TestMenu> opened = <TestMenu>[];
+  final List<TestMenu> closed = <TestMenu>[];
+  final GlobalKey menuItemKey = GlobalKey();
+
+  void onPressed(TestMenu item) {
+    selected.add(item);
+  }
+
+  void onOpen(TestMenu item) {
+    opened.add(item);
+  }
+
+  void onClose(TestMenu item) {
+    closed.add(item);
+  }
+
+  void handleFocusChange() {
+    focusedMenu = primaryFocus?.debugLabel ?? primaryFocus?.toString();
+  }
+
+  setUp(() {
+    focusedMenu = null;
+    selected.clear();
+    opened.clear();
+    closed.clear();
+    controller = MenuController();
+    focusedMenu = null;
+  });
+
+  void listenForFocusChanges() {
+    FocusManager.instance.addListener(handleFocusChange);
+    addTearDown(() => FocusManager.instance.removeListener(handleFocusChange));
+  }
+
+  Finder findMenuPanels() {
+    return find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_MenuPanel');
+  }
+
+  Finder findMenuBarItemLabels() {
+    return find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_MenuItemLabel');
+  }
+
+  // Finds the mnemonic associated with the menu item that has the given label.
+  Finder findMnemonic(String label) {
+    return find
+        .descendant(
+          of: find.ancestor(of: find.text(label), matching: findMenuBarItemLabels()),
+          matching: find.byType(Text),
+        )
+        .last;
+  }
+
+  Widget buildTestApp({
+    AlignmentGeometry? alignment,
+    Offset alignmentOffset = Offset.zero,
+    TextDirection textDirection = TextDirection.ltr,
+  }) {
+    final FocusNode focusNode = FocusNode();
+    return MaterialApp(
+      home: Material(
+        child: Directionality(
+          textDirection: textDirection,
+          child: Center(
+            child: MenuAnchor(
+              childFocusNode: focusNode,
+              controller: controller,
+              alignmentOffset: alignmentOffset,
+              style: MenuStyle(alignment: alignment),
+              menuChildren: <Widget>[
+                MenuItemButton(
+                  key: menuItemKey,
+                  shortcut: const SingleActivator(
+                    LogicalKeyboardKey.keyB,
+                    control: true,
+                  ),
+                  onPressed: () {},
+                  child: Text(TestMenu.subMenu00.label),
+                ),
+                MenuItemButton(
+                  leadingIcon: const Icon(Icons.send),
+                  trailingIcon: const Icon(Icons.mail),
+                  onPressed: () {},
+                  child: Text(TestMenu.subMenu01.label),
+                ),
+              ],
+              builder: (BuildContext context, MenuController controller, Widget? child) {
+                return ElevatedButton(
+                  focusNode: focusNode,
+                  onPressed: () {
+                    if (controller.isOpen) {
+                      controller.close();
+                    } else {
+                      controller.open();
+                    }
+                  },
+                  child: child,
+                );
+              },
+              child: const Text('Press Me'),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  Future<TestGesture> hoverOver(WidgetTester tester, Finder finder) async {
+    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
+    await gesture.moveTo(tester.getCenter(finder));
+    await tester.pumpAndSettle();
+    return gesture;
+  }
+
+  Material getMenuBarMaterial(WidgetTester tester) {
+    return tester.widget<Material>(
+      find.descendant(of: findMenuPanels(), matching: find.byType(Material)).first,
+    );
+  }
+
+  group('Menu functions', () {
+    testWidgets('basic menu structure', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: MenuBar(
+              controller: controller,
+              children: createTestMenus(
+                onPressed: onPressed,
+                onOpen: onOpen,
+                onClose: onClose,
+              ),
+            ),
+          ),
+        ),
+      );
+
+      expect(find.text(TestMenu.mainMenu0.label), findsOneWidget);
+      expect(find.text(TestMenu.mainMenu1.label), findsOneWidget);
+      expect(find.text(TestMenu.mainMenu2.label), findsOneWidget);
+      expect(find.text(TestMenu.subMenu10.label), findsNothing);
+      expect(find.text(TestMenu.subSubMenu110.label), findsNothing);
+      expect(opened, isEmpty);
+
+      await tester.tap(find.text(TestMenu.mainMenu1.label));
+      await tester.pump();
+
+      expect(find.text(TestMenu.mainMenu0.label), findsOneWidget);
+      expect(find.text(TestMenu.mainMenu1.label), findsOneWidget);
+      expect(find.text(TestMenu.mainMenu2.label), findsOneWidget);
+      expect(find.text(TestMenu.subMenu10.label), findsOneWidget);
+      expect(find.text(TestMenu.subMenu11.label), findsOneWidget);
+      expect(find.text(TestMenu.subMenu12.label), findsOneWidget);
+      expect(find.text(TestMenu.subSubMenu110.label), findsNothing);
+      expect(find.text(TestMenu.subSubMenu111.label), findsNothing);
+      expect(find.text(TestMenu.subSubMenu112.label), findsNothing);
+      expect(opened.last, equals(TestMenu.mainMenu1));
+      opened.clear();
+
+      await tester.tap(find.text(TestMenu.subMenu11.label));
+      await tester.pump();
+
+      expect(find.text(TestMenu.mainMenu0.label), findsOneWidget);
+      expect(find.text(TestMenu.mainMenu1.label), findsOneWidget);
+      expect(find.text(TestMenu.mainMenu2.label), findsOneWidget);
+      expect(find.text(TestMenu.subMenu10.label), findsOneWidget);
+      expect(find.text(TestMenu.subMenu11.label), findsOneWidget);
+      expect(find.text(TestMenu.subMenu12.label), findsOneWidget);
+      expect(find.text(TestMenu.subSubMenu110.label), findsOneWidget);
+      expect(find.text(TestMenu.subSubMenu111.label), findsOneWidget);
+      expect(find.text(TestMenu.subSubMenu112.label), findsOneWidget);
+      expect(opened.last, equals(TestMenu.subMenu11));
+    });
+
+    testWidgets('geometry', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: Column(
+              children: <Widget>[
+                Row(
+                  children: <Widget>[
+                    Expanded(
+                      child: MenuBar(
+                        children: createTestMenus(onPressed: onPressed),
+                      ),
+                    ),
+                  ],
+                ),
+                const Expanded(child: Placeholder()),
+              ],
+            ),
+          ),
+        ),
+      );
+      await tester.pump();
+
+      expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(0, 0, 800, 48)));
+
+      // Open and make sure things are the right size.
+      await tester.tap(find.text(TestMenu.mainMenu1.label));
+      await tester.pump();
+
+      expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(0, 0, 800, 48)));
+      expect(
+        tester.getRect(find.text(TestMenu.subMenu10.label)),
+        equals(const Rect.fromLTRB(112.0, 69.0, 266.0, 83.0)),
+      );
+      expect(
+        tester.getRect(
+          find.ancestor(of: find.text(TestMenu.subMenu10.label), matching: find.byType(Material)).at(1),
+        ),
+        equals(const Rect.fromLTRB(104.0, 48.0, 334.0, 200.0)),
+      );
+    });
+
+    testWidgets('geometry with RTL direction', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: Directionality(
+              textDirection: TextDirection.rtl,
+              child: Column(
+                children: <Widget>[
+                  Row(
+                    children: <Widget>[
+                      Expanded(
+                        child: MenuBar(
+                          children: createTestMenus(onPressed: onPressed),
+                        ),
+                      ),
+                    ],
+                  ),
+                  const Expanded(child: Placeholder()),
+                ],
+              ),
+            ),
+          ),
+        ),
+      );
+      await tester.pump();
+
+      expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(0, 0, 800, 48)));
+
+      // Open and make sure things are the right size.
+      await tester.tap(find.text(TestMenu.mainMenu1.label));
+      await tester.pump();
+
+      expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(0, 0, 800, 48)));
+      expect(
+        tester.getRect(find.text(TestMenu.subMenu10.label)),
+        equals(const Rect.fromLTRB(534.0, 69.0, 688.0, 83.0)),
+      );
+      expect(
+        tester.getRect(
+          find.ancestor(of: find.text(TestMenu.subMenu10.label), matching: find.byType(Material)).at(1),
+        ),
+        equals(const Rect.fromLTRB(466.0, 48.0, 696.0, 200.0)),
+      );
+
+      // Close and make sure it goes back where it was.
+      await tester.tap(find.text(TestMenu.mainMenu1.label));
+      await tester.pump();
+
+      expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(0, 0, 800, 48)));
+
+      // Test menu bar size when not expanded.
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: Column(
+              children: <Widget>[
+                MenuBar(
+                  children: createTestMenus(onPressed: onPressed),
+                ),
+                const Expanded(child: Placeholder()),
+              ],
+            ),
+          ),
+        ),
+      );
+      await tester.pump();
+
+      expect(
+        tester.getRect(find.byType(MenuBar)),
+        equals(const Rect.fromLTRB(246.0, 0.0, 554.0, 48.0)),
+      );
+    });
+
+    testWidgets('menu alignment and offset in LTR', (WidgetTester tester) async {
+      await tester.pumpWidget(buildTestApp());
+
+      final Rect buttonRect = tester.getRect(find.byType(ElevatedButton));
+      expect(buttonRect, equals(const Rect.fromLTRB(328.0, 276.0, 472.0, 324.0)));
+
+      final Finder findMenuScope = find.ancestor(of: find.byKey(menuItemKey), matching: find.byType(FocusScope)).first;
+
+      // Open the menu and make sure things are the right size, in the right place.
+      await tester.tap(find.text('Press Me'));
+      await tester.pump();
+      expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(328.0, 324.0, 618.0, 428.0)));
+
+      await tester.pumpWidget(buildTestApp(alignment: AlignmentDirectional.topStart));
+      await tester.pump();
+      expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(328.0, 276.0, 618.0, 380.0)));
+
+      await tester.pumpWidget(buildTestApp(alignment: AlignmentDirectional.center));
+      await tester.pump();
+      expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(400.0, 300.0, 690.0, 404.0)));
+
+      await tester.pumpWidget(buildTestApp(alignment: AlignmentDirectional.bottomEnd));
+      await tester.pump();
+      expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(472.0, 324.0, 762.0, 428.0)));
+
+      await tester.pumpWidget(buildTestApp(alignment: AlignmentDirectional.topStart));
+      await tester.pump();
+
+      final Rect menuRect = tester.getRect(findMenuScope);
+      await tester.pumpWidget(
+        buildTestApp(
+          alignment: AlignmentDirectional.topStart,
+          alignmentOffset: const Offset(10, 20),
+        ),
+      );
+      await tester.pump();
+      final Rect offsetMenuRect = tester.getRect(findMenuScope);
+      expect(
+        offsetMenuRect.topLeft - menuRect.topLeft,
+        equals(const Offset(10, 20)),
+      );
+    });
+
+    testWidgets('menu alignment and offset in RTL', (WidgetTester tester) async {
+      await tester.pumpWidget(buildTestApp(textDirection: TextDirection.rtl));
+
+      final Rect buttonRect = tester.getRect(find.byType(ElevatedButton));
+      expect(buttonRect, equals(const Rect.fromLTRB(328.0, 276.0, 472.0, 324.0)));
+
+      final Finder findMenuScope =
+          find.ancestor(of: find.text(TestMenu.subMenu00.label), matching: find.byType(FocusScope)).first;
+
+      // Open the menu and make sure things are the right size, in the right place.
+      await tester.tap(find.text('Press Me'));
+      await tester.pump();
+      expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(182.0, 324.0, 472.0, 428.0)));
+
+      await tester.pumpWidget(buildTestApp(textDirection: TextDirection.rtl, alignment: AlignmentDirectional.topStart));
+      await tester.pump();
+      expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(182.0, 276.0, 472.0, 380.0)));
+
+      await tester.pumpWidget(buildTestApp(textDirection: TextDirection.rtl, alignment: AlignmentDirectional.center));
+      await tester.pump();
+      expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(110.0, 300.0, 400.0, 404.0)));
+
+      await tester
+          .pumpWidget(buildTestApp(textDirection: TextDirection.rtl, alignment: AlignmentDirectional.bottomEnd));
+      await tester.pump();
+      expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(38.0, 324.0, 328.0, 428.0)));
+
+      await tester.pumpWidget(buildTestApp(textDirection: TextDirection.rtl, alignment: AlignmentDirectional.topStart));
+      await tester.pump();
+
+      final Rect menuRect = tester.getRect(findMenuScope);
+      await tester.pumpWidget(
+        buildTestApp(
+          textDirection: TextDirection.rtl,
+          alignment: AlignmentDirectional.topStart,
+          alignmentOffset: const Offset(10, 20),
+        ),
+      );
+      await tester.pump();
+      expect(tester.getRect(findMenuScope).topLeft - menuRect.topLeft, equals(const Offset(-10, 20)));
+    });
+
+    testWidgets('menu position in LTR', (WidgetTester tester) async {
+      await tester.pumpWidget(buildTestApp(alignmentOffset: const Offset(100, 50)));
+
+      final Rect buttonRect = tester.getRect(find.byType(ElevatedButton));
+      expect(buttonRect, equals(const Rect.fromLTRB(328.0, 276.0, 472.0, 324.0)));
+
+      final Finder findMenuScope =
+          find.ancestor(of: find.text(TestMenu.subMenu00.label), matching: find.byType(FocusScope)).first;
+
+      // Open the menu and make sure things are the right size, in the right place.
+      await tester.tap(find.text('Press Me'));
+      await tester.pump();
+      expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(428.0, 374.0, 718.0, 478.0)));
+
+      // Now move the menu by calling open() again with a local position on the
+      // anchor.
+      controller.open(position: const Offset(200, 200));
+      await tester.pump();
+      expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(510.0, 476.0, 800.0, 580.0)));
+    });
+
+    testWidgets('menu position in RTL', (WidgetTester tester) async {
+      await tester.pumpWidget(buildTestApp(
+        alignmentOffset: const Offset(100, 50),
+        textDirection: TextDirection.rtl,
+      ));
+
+      final Rect buttonRect = tester.getRect(find.byType(ElevatedButton));
+      expect(buttonRect, equals(const Rect.fromLTRB(328.0, 276.0, 472.0, 324.0)));
+      expect(buttonRect, equals(const Rect.fromLTRB(328.0, 276.0, 472.0, 324.0)));
+
+      final Finder findMenuScope =
+          find.ancestor(of: find.text(TestMenu.subMenu00.label), matching: find.byType(FocusScope)).first;
+
+      // Open the menu and make sure things are the right size, in the right place.
+      await tester.tap(find.text('Press Me'));
+      await tester.pump();
+      expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(82.0, 374.0, 372.0, 478.0)));
+
+      // Now move the menu by calling open() again with a local position on the
+      // anchor.
+      controller.open(position: const Offset(400, 200));
+      await tester.pump();
+      expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(510.0, 476.0, 800.0, 580.0)));
+    });
+
+    testWidgets('works with Padding around menu and overlay', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        Padding(
+          padding: const EdgeInsets.all(10.0),
+          child: MaterialApp(
+            home: Material(
+              child: Column(
+                children: <Widget>[
+                  Padding(
+                    padding: const EdgeInsets.all(12.0),
+                    child: Row(
+                      children: <Widget>[
+                        Expanded(
+                          child: MenuBar(
+                            children: createTestMenus(onPressed: onPressed),
+                          ),
+                        ),
+                      ],
+                    ),
+                  ),
+                  const Expanded(child: Placeholder()),
+                ],
+              ),
+            ),
+          ),
+        ),
+      );
+      await tester.pump();
+
+      expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(22.0, 22.0, 778.0, 70.0)));
+
+      // Open and make sure things are the right size.
+      await tester.tap(find.text(TestMenu.mainMenu1.label));
+      await tester.pump();
+
+      expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(22.0, 22.0, 778.0, 70.0)));
+      expect(
+        tester.getRect(find.text(TestMenu.subMenu10.label)),
+        equals(const Rect.fromLTRB(134.0, 91.0, 288.0, 105.0)),
+      );
+      expect(
+        tester.getRect(find.ancestor(of: find.text(TestMenu.subMenu10.label), matching: find.byType(Material)).at(1)),
+        equals(const Rect.fromLTRB(126.0, 70.0, 356.0, 222.0)),
+      );
+
+      // Close and make sure it goes back where it was.
+      await tester.tap(find.text(TestMenu.mainMenu1.label));
+      await tester.pump();
+
+      expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(22.0, 22.0, 778.0, 70.0)));
+    });
+
+    testWidgets('works with Padding around menu and overlay with RTL direction', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        Padding(
+          padding: const EdgeInsets.all(10.0),
+          child: MaterialApp(
+            home: Material(
+              child: Directionality(
+                textDirection: TextDirection.rtl,
+                child: Column(
+                  children: <Widget>[
+                    Padding(
+                      padding: const EdgeInsets.all(12.0),
+                      child: Row(
+                        children: <Widget>[
+                          Expanded(
+                            child: MenuBar(
+                              children: createTestMenus(onPressed: onPressed),
+                            ),
+                          ),
+                        ],
+                      ),
+                    ),
+                    const Expanded(child: Placeholder()),
+                  ],
+                ),
+              ),
+            ),
+          ),
+        ),
+      );
+      await tester.pump();
+
+      expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(22.0, 22.0, 778.0, 70.0)));
+
+      // Open and make sure things are the right size.
+      await tester.tap(find.text(TestMenu.mainMenu1.label));
+      await tester.pump();
+
+      expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(22.0, 22.0, 778.0, 70.0)));
+      expect(
+        tester.getRect(find.text(TestMenu.subMenu10.label)),
+        equals(const Rect.fromLTRB(512.0, 91.0, 666.0, 105.0)),
+      );
+      expect(
+        tester.getRect(find.ancestor(of: find.text(TestMenu.subMenu10.label), matching: find.byType(Material)).at(1)),
+        equals(const Rect.fromLTRB(444.0, 70.0, 674.0, 222.0)),
+      );
+
+      // Close and make sure it goes back where it was.
+      await tester.tap(find.text(TestMenu.mainMenu1.label));
+      await tester.pump();
+
+      expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(22.0, 22.0, 778.0, 70.0)));
+    });
+
+    testWidgets('visual attributes can be set', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: Column(
+              children: <Widget>[
+                Row(
+                  children: <Widget>[
+                    Expanded(
+                      child: MenuBar(
+                        style: MenuStyle(
+                          elevation: MaterialStateProperty.all<double?>(10),
+                          backgroundColor: const MaterialStatePropertyAll<Color>(Colors.red),
+                        ),
+                        children: createTestMenus(onPressed: onPressed),
+                      ),
+                    ),
+                  ],
+                ),
+                const Expanded(child: Placeholder()),
+              ],
+            ),
+          ),
+        ),
+      );
+      expect(tester.getRect(findMenuPanels()), equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 48.0)));
+      final Material material = getMenuBarMaterial(tester);
+      expect(material.elevation, equals(10));
+      expect(material.color, equals(Colors.red));
+    });
+
+    testWidgets('open and close works', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: MenuBar(
+              controller: controller,
+              children: createTestMenus(
+                onPressed: onPressed,
+                onOpen: onOpen,
+                onClose: onClose,
+              ),
+            ),
+          ),
+        ),
+      );
+
+      expect(opened, isEmpty);
+      expect(closed, isEmpty);
+
+      await tester.tap(find.text(TestMenu.mainMenu1.label));
+      await tester.pump();
+      expect(opened, equals(<TestMenu>[TestMenu.mainMenu1]));
+      expect(closed, isEmpty);
+      opened.clear();
+      closed.clear();
+
+      await tester.tap(find.text(TestMenu.subMenu11.label));
+      await tester.pump();
+
+      expect(opened, equals(<TestMenu>[TestMenu.subMenu11]));
+      expect(closed, isEmpty);
+      opened.clear();
+      closed.clear();
+
+      await tester.tap(find.text(TestMenu.subMenu11.label));
+      await tester.pump();
+
+      expect(opened, isEmpty);
+      expect(closed, equals(<TestMenu>[TestMenu.subMenu11]));
+      opened.clear();
+      closed.clear();
+
+      await tester.tap(find.text(TestMenu.mainMenu0.label));
+      await tester.pump();
+
+      expect(opened, equals(<TestMenu>[TestMenu.mainMenu0]));
+      expect(closed, equals(<TestMenu>[TestMenu.mainMenu1]));
+    });
+
+    testWidgets('select works', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: MenuBar(
+              controller: controller,
+              children: createTestMenus(
+                onPressed: onPressed,
+                onOpen: onOpen,
+                onClose: onClose,
+              ),
+            ),
+          ),
+        ),
+      );
+
+      await tester.tap(find.text(TestMenu.mainMenu1.label));
+      await tester.pump();
+
+      await tester.tap(find.text(TestMenu.subMenu11.label));
+      await tester.pump();
+
+      expect(opened, equals(<TestMenu>[TestMenu.mainMenu1, TestMenu.subMenu11]));
+      opened.clear();
+      await tester.tap(find.text(TestMenu.subSubMenu110.label));
+      await tester.pump();
+
+      expect(selected, equals(<TestMenu>[TestMenu.subSubMenu110]));
+
+      // Selecting a non-submenu item should close all the menus.
+      expect(opened, isEmpty);
+      expect(find.text(TestMenu.subSubMenu110.label), findsNothing);
+      expect(find.text(TestMenu.subMenu11.label), findsNothing);
+    });
+
+    testWidgets('diagnostics', (WidgetTester tester) async {
+      const MenuItemButton item = MenuItemButton(
+        shortcut: SingleActivator(LogicalKeyboardKey.keyA),
+        child: Text('label2'),
+      );
+      final MenuBar menuBar = MenuBar(
+        controller: controller,
+        style: const MenuStyle(
+          backgroundColor: MaterialStatePropertyAll<Color>(Colors.red),
+          elevation: MaterialStatePropertyAll<double?>(10.0),
+        ),
+        children: const <Widget>[item],
+      );
+
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: menuBar,
+          ),
+        ),
+      );
+      await tester.pump();
+
+      final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
+      menuBar.debugFillProperties(builder);
+
+      final List<String> description = builder.properties
+          .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
+          .map((DiagnosticsNode node) => node.toString())
+          .toList();
+
+      expect(
+        description.join('\n'),
+        equalsIgnoringHashCodes(
+            'style: MenuStyle#00000(backgroundColor: MaterialStatePropertyAll(MaterialColor(primary value: Color(0xfff44336))), elevation: MaterialStatePropertyAll(10.0))\n'
+            'clipBehavior: Clip.none'),
+      );
+    });
+
+    testWidgets('keyboard tab traversal works', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: Column(
+              children: <Widget>[
+                MenuBar(
+                  controller: controller,
+                  children: createTestMenus(
+                    onPressed: onPressed,
+                    onOpen: onOpen,
+                    onClose: onClose,
+                  ),
+                ),
+                const Expanded(child: Placeholder()),
+              ],
+            ),
+          ),
+        ),
+      );
+
+      listenForFocusChanges();
+
+      // Have to open a menu initially to start things going.
+      await tester.tap(find.text(TestMenu.mainMenu0.label));
+      await tester.pumpAndSettle();
+
+      expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
+      await tester.sendKeyEvent(LogicalKeyboardKey.tab);
+      await tester.pump();
+      expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
+      await tester.sendKeyEvent(LogicalKeyboardKey.tab);
+      await tester.pump();
+      expect(focusedMenu, equals('SubmenuButton(Text("Menu 2"))'));
+      await tester.sendKeyEvent(LogicalKeyboardKey.tab);
+      await tester.pump();
+      expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
+
+      await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
+      await tester.sendKeyEvent(LogicalKeyboardKey.tab);
+      await tester.pump();
+      expect(focusedMenu, equals('SubmenuButton(Text("Menu 2"))'));
+      await tester.sendKeyEvent(LogicalKeyboardKey.tab);
+      await tester.pump();
+      expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
+      await tester.sendKeyEvent(LogicalKeyboardKey.tab);
+      await tester.pump();
+      expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
+      await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
+      opened.clear();
+      closed.clear();
+
+      // Test closing a menu with enter.
+      await tester.sendKeyEvent(LogicalKeyboardKey.enter);
+      await tester.pump();
+      expect(opened, isEmpty);
+      expect(closed, <TestMenu>[TestMenu.mainMenu0]);
+    });
+
+    testWidgets('keyboard directional traversal works', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: MenuBar(
+              controller: controller,
+              children: createTestMenus(
+                onPressed: onPressed,
+                onOpen: onOpen,
+                onClose: onClose,
+              ),
+            ),
+          ),
+        ),
+      );
+
+      listenForFocusChanges();
+
+      // Have to open a menu initially to start things going.
+      await tester.tap(find.text(TestMenu.mainMenu0.label));
+      await tester.pumpAndSettle();
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
+      await tester.pump();
+      expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+      expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 10"))'));
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+      expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+      expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 12"))'));
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+      expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 12"))'));
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
+      await tester.pump();
+      expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
+
+      // Open the next submenu
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
+      await tester.pump();
+      expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 110"))'));
+
+      // Go back, close the submenu.
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
+      await tester.pump();
+      expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
+
+      // Move up, should close the submenu.
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
+      await tester.pump();
+      expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 10"))'));
+
+      // Move down, should reopen the submenu.
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+      await tester.pump();
+      expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
+
+      // Open the next submenu again.
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
+      await tester.pump();
+      expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 110"))'));
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+      expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 111"))'));
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+      expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 112"))'));
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+      expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 113"))'));
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+      expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 113"))'));
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
+      await tester.pump();
+      expect(focusedMenu, equals('SubmenuButton(Text("Menu 2"))'));
+    });
+
+    testWidgets('keyboard directional traversal works in RTL mode', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Directionality(
+            textDirection: TextDirection.rtl,
+            child: Material(
+              child: MenuBar(
+                controller: controller,
+                children: createTestMenus(
+                  onPressed: onPressed,
+                  onOpen: onOpen,
+                  onClose: onClose,
+                ),
+              ),
+            ),
+          ),
+        ),
+      );
+
+      listenForFocusChanges();
+
+      // Have to open a menu initially to start things going.
+      await tester.tap(find.text(TestMenu.mainMenu0.label));
+      await tester.pump();
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
+      expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+      await tester.pump();
+      expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+      expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 10"))'));
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+      expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+      expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 12"))'));
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+      expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 12"))'));
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
+      await tester.pump();
+      expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
+
+      // Open the next submenu
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
+      await tester.pump();
+      expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 110"))'));
+
+      // Go back, close the submenu.
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
+      await tester.pump();
+      expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
+
+      // Move up, should close the submenu.
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
+      await tester.pump();
+      expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 10"))'));
+
+      // Move down, should reopen the submenu.
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+      await tester.pump();
+      expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
+
+      // Open the next submenu again.
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
+      await tester.pump();
+      expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 110"))'));
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+      expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 111"))'));
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+      expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 112"))'));
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+      expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 113"))'));
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
+      expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 113"))'));
+
+      await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
+      await tester.pump();
+      expect(focusedMenu, equals('SubmenuButton(Text("Menu 2"))'));
+    });
+
+    testWidgets('hover traversal works', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: MenuBar(
+              controller: controller,
+              children: createTestMenus(
+                onPressed: onPressed,
+                onOpen: onOpen,
+                onClose: onClose,
+              ),
+            ),
+          ),
+        ),
+      );
+
+      listenForFocusChanges();
+
+      // Hovering when the menu is not yet open does nothing.
+      await hoverOver(tester, find.text(TestMenu.mainMenu0.label));
+      await tester.pump();
+      expect(focusedMenu, isNull);
+
+      // Have to open a menu initially to start things going.
+      await tester.tap(find.text(TestMenu.mainMenu0.label));
+      await tester.pump();
+      expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
+
+      // Hovering when the menu is already  open does nothing.
+      await hoverOver(tester, find.text(TestMenu.mainMenu0.label));
+      await tester.pump();
+      expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
+
+      // Hovering over the other main menu items opens them now.
+      await hoverOver(tester, find.text(TestMenu.mainMenu2.label));
+      await tester.pump();
+      expect(focusedMenu, equals('SubmenuButton(Text("Menu 2"))'));
+
+      await hoverOver(tester, find.text(TestMenu.mainMenu1.label));
+      await tester.pump();
+      expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
+
+      // Hovering over the menu items focuses them.
+      await hoverOver(tester, find.text(TestMenu.subMenu10.label));
+      await tester.pump();
+      expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 10"))'));
+
+      await hoverOver(tester, find.text(TestMenu.subMenu11.label));
+      await tester.pump();
+      expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
+
+      await hoverOver(tester, find.text(TestMenu.subSubMenu110.label));
+      await tester.pump();
+      expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 110"))'));
+    });
+
+    testWidgets('menus close on ancestor scroll', (WidgetTester tester) async {
+      final ScrollController scrollController = ScrollController();
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: SingleChildScrollView(
+              controller: scrollController,
+              child: Container(
+                height: 1000,
+                alignment: Alignment.center,
+                child: MenuBar(
+                  controller: controller,
+                  children: createTestMenus(
+                    onPressed: onPressed,
+                    onOpen: onOpen,
+                    onClose: onClose,
+                  ),
+                ),
+              ),
+            ),
+          ),
+        ),
+      );
+
+      await tester.tap(find.text(TestMenu.mainMenu0.label));
+      await tester.pump();
+
+      expect(opened, isNotEmpty);
+      expect(closed, isEmpty);
+      opened.clear();
+
+      scrollController.jumpTo(1000);
+      await tester.pump();
+
+      expect(opened, isEmpty);
+      expect(closed, isNotEmpty);
+    });
+
+    testWidgets('menus close on view size change', (WidgetTester tester) async {
+      final ScrollController scrollController = ScrollController();
+      final MediaQueryData mediaQueryData = MediaQueryData.fromWindow(tester.binding.window);
+
+      Widget build(Size size) {
+        return MaterialApp(
+          home: Material(
+            child: MediaQuery(
+              data: mediaQueryData.copyWith(size: size),
+              child: SingleChildScrollView(
+                controller: scrollController,
+                child: Container(
+                  height: 1000,
+                  alignment: Alignment.center,
+                  child: MenuBar(
+                    controller: controller,
+                    children: createTestMenus(
+                      onPressed: onPressed,
+                      onOpen: onOpen,
+                      onClose: onClose,
+                    ),
+                  ),
+                ),
+              ),
+            ),
+          ),
+        );
+      }
+
+      await tester.pumpWidget(build(mediaQueryData.size));
+
+      await tester.tap(find.text(TestMenu.mainMenu0.label));
+      await tester.pump();
+
+      expect(opened, isNotEmpty);
+      expect(closed, isEmpty);
+      opened.clear();
+
+      const Size smallSize = Size(200, 200);
+      await tester.binding.setSurfaceSize(smallSize);
+
+      await tester.pumpWidget(build(smallSize));
+      await tester.pump();
+
+      expect(opened, isEmpty);
+      expect(closed, isNotEmpty);
+
+      // Reset binding when done.
+      await tester.binding.setSurfaceSize(mediaQueryData.size);
+    });
+  });
+
+  group('MenuController', () {
+    testWidgets('Moving a controller to a new instance works', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: MenuBar(
+              key: UniqueKey(),
+              controller: controller,
+              children: createTestMenus(),
+            ),
+          ),
+        ),
+      );
+
+      // Open a menu initially.
+      await tester.tap(find.text(TestMenu.mainMenu1.label));
+      await tester.pump();
+
+      await tester.tap(find.text(TestMenu.subMenu11.label));
+      await tester.pump();
+
+      // Now pump a new menu with a different UniqueKey to dispose of the opened
+      // menu's node, but keep the existing controller.
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: MenuBar(
+              key: UniqueKey(),
+              controller: controller,
+              children: createTestMenus(
+                includeExtraGroups: true,
+              ),
+            ),
+          ),
+        ),
+      );
+      await tester.pumpAndSettle();
+    });
+
+    testWidgets('closing via controller works', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: MenuBar(
+              controller: controller,
+              children: createTestMenus(
+                onPressed: onPressed,
+                onOpen: onOpen,
+                onClose: onClose,
+                shortcuts: <TestMenu, MenuSerializableShortcut>{
+                  TestMenu.subSubMenu110: const SingleActivator(
+                    LogicalKeyboardKey.keyA,
+                    control: true,
+                  )
+                },
+              ),
+            ),
+          ),
+        ),
+      );
+
+      // Open a menu initially.
+      await tester.tap(find.text(TestMenu.mainMenu1.label));
+      await tester.pump();
+
+      await tester.tap(find.text(TestMenu.subMenu11.label));
+      await tester.pump();
+      expect(opened, unorderedEquals(<TestMenu>[TestMenu.mainMenu1, TestMenu.subMenu11]));
+      opened.clear();
+      closed.clear();
+
+      // Close menus using the controller
+      controller.close();
+      await tester.pump();
+
+      // The menu should go away,
+      expect(closed, unorderedEquals(<TestMenu>[TestMenu.mainMenu1, TestMenu.subMenu11]));
+      expect(opened, isEmpty);
+    });
+  });
+
+  group('MenuItemButton', () {
+    testWidgets('Shortcut mnemonics are displayed', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: MenuBar(
+              controller: controller,
+              children: createTestMenus(
+                shortcuts: <TestMenu, MenuSerializableShortcut>{
+                  TestMenu.subSubMenu110: const SingleActivator(LogicalKeyboardKey.keyA, control: true),
+                  TestMenu.subSubMenu111: const SingleActivator(LogicalKeyboardKey.keyB, shift: true),
+                  TestMenu.subSubMenu112: const SingleActivator(LogicalKeyboardKey.keyC, alt: true),
+                  TestMenu.subSubMenu113: const SingleActivator(LogicalKeyboardKey.keyD, meta: true),
+                },
+              ),
+            ),
+          ),
+        ),
+      );
+
+      // Open a menu initially.
+      await tester.tap(find.text(TestMenu.mainMenu1.label));
+      await tester.pump();
+
+      await tester.tap(find.text(TestMenu.subMenu11.label));
+      await tester.pump();
+
+      Text mnemonic0;
+      Text mnemonic1;
+      Text mnemonic2;
+      Text mnemonic3;
+
+      switch (defaultTargetPlatform) {
+        case TargetPlatform.android:
+        case TargetPlatform.fuchsia:
+        case TargetPlatform.linux:
+          mnemonic0 = tester.widget(findMnemonic(TestMenu.subSubMenu110.label));
+          expect(mnemonic0.data, equals('Ctrl A'));
+          mnemonic1 = tester.widget(findMnemonic(TestMenu.subSubMenu111.label));
+          expect(mnemonic1.data, equals('⇧ B'));
+          mnemonic2 = tester.widget(findMnemonic(TestMenu.subSubMenu112.label));
+          expect(mnemonic2.data, equals('Alt C'));
+          mnemonic3 = tester.widget(findMnemonic(TestMenu.subSubMenu113.label));
+          expect(mnemonic3.data, equals('Meta D'));
+          break;
+        case TargetPlatform.windows:
+          mnemonic0 = tester.widget(findMnemonic(TestMenu.subSubMenu110.label));
+          expect(mnemonic0.data, equals('Ctrl A'));
+          mnemonic1 = tester.widget(findMnemonic(TestMenu.subSubMenu111.label));
+          expect(mnemonic1.data, equals('⇧ B'));
+          mnemonic2 = tester.widget(findMnemonic(TestMenu.subSubMenu112.label));
+          expect(mnemonic2.data, equals('Alt C'));
+          mnemonic3 = tester.widget(findMnemonic(TestMenu.subSubMenu113.label));
+          expect(mnemonic3.data, equals('Win D'));
+          break;
+        case TargetPlatform.iOS:
+        case TargetPlatform.macOS:
+          mnemonic0 = tester.widget(findMnemonic(TestMenu.subSubMenu110.label));
+          expect(mnemonic0.data, equals('⌃ A'));
+          mnemonic1 = tester.widget(findMnemonic(TestMenu.subSubMenu111.label));
+          expect(mnemonic1.data, equals('⇧ B'));
+          mnemonic2 = tester.widget(findMnemonic(TestMenu.subSubMenu112.label));
+          expect(mnemonic2.data, equals('⌥ C'));
+          mnemonic3 = tester.widget(findMnemonic(TestMenu.subSubMenu113.label));
+          expect(mnemonic3.data, equals('⌘ D'));
+          break;
+      }
+
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: MenuBar(
+              controller: controller,
+              children: createTestMenus(
+                includeExtraGroups: true,
+                shortcuts: <TestMenu, MenuSerializableShortcut>{
+                  TestMenu.subSubMenu110: const SingleActivator(LogicalKeyboardKey.arrowRight),
+                  TestMenu.subSubMenu111: const SingleActivator(LogicalKeyboardKey.arrowLeft),
+                  TestMenu.subSubMenu112: const SingleActivator(LogicalKeyboardKey.arrowUp),
+                  TestMenu.subSubMenu113: const SingleActivator(LogicalKeyboardKey.arrowDown),
+                },
+              ),
+            ),
+          ),
+        ),
+      );
+      await tester.pumpAndSettle();
+
+      mnemonic0 = tester.widget(findMnemonic(TestMenu.subSubMenu110.label));
+      expect(mnemonic0.data, equals('→'));
+      mnemonic1 = tester.widget(findMnemonic(TestMenu.subSubMenu111.label));
+      expect(mnemonic1.data, equals('←'));
+      mnemonic2 = tester.widget(findMnemonic(TestMenu.subSubMenu112.label));
+      expect(mnemonic2.data, equals('↑'));
+      mnemonic3 = tester.widget(findMnemonic(TestMenu.subSubMenu113.label));
+      expect(mnemonic3.data, equals('↓'));
+
+      // Try some weirder ones.
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: MenuBar(
+              controller: controller,
+              children: createTestMenus(
+                shortcuts: <TestMenu, MenuSerializableShortcut>{
+                  TestMenu.subSubMenu110: const SingleActivator(LogicalKeyboardKey.escape),
+                  TestMenu.subSubMenu111: const SingleActivator(LogicalKeyboardKey.fn),
+                  TestMenu.subSubMenu112: const SingleActivator(LogicalKeyboardKey.enter),
+                },
+              ),
+            ),
+          ),
+        ),
+      );
+      await tester.pumpAndSettle();
+
+      mnemonic0 = tester.widget(findMnemonic(TestMenu.subSubMenu110.label));
+      expect(mnemonic0.data, equals('Esc'));
+      mnemonic1 = tester.widget(findMnemonic(TestMenu.subSubMenu111.label));
+      expect(mnemonic1.data, equals('Fn'));
+      mnemonic2 = tester.widget(findMnemonic(TestMenu.subSubMenu112.label));
+      expect(mnemonic2.data, equals('↵'));
+    }, variant: TargetPlatformVariant.all());
+
+    testWidgets('leadingIcon is used when set', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: MenuBar(
+              controller: controller,
+              children: <Widget>[
+                SubmenuButton(
+                  menuChildren: <Widget>[
+                    MenuItemButton(
+                      leadingIcon: const Text('leadingIcon'),
+                      child: Text(TestMenu.subMenu00.label),
+                    ),
+                  ],
+                  child: Text(TestMenu.mainMenu0.label),
+                ),
+              ],
+            ),
+          ),
+        ),
+      );
+
+      await tester.tap(find.text(TestMenu.mainMenu0.label));
+      await tester.pump();
+
+      expect(find.text('leadingIcon'), findsOneWidget);
+    });
+
+    testWidgets('trailingIcon is used when set', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: MenuBar(
+              controller: controller,
+              children: <Widget>[
+                SubmenuButton(
+                  menuChildren: <Widget>[
+                    MenuItemButton(
+                      trailingIcon: const Text('trailingIcon'),
+                      child: Text(TestMenu.subMenu00.label),
+                    ),
+                  ],
+                  child: Text(TestMenu.mainMenu0.label),
+                ),
+              ],
+            ),
+          ),
+        ),
+      );
+
+      await tester.tap(find.text(TestMenu.mainMenu0.label));
+      await tester.pump();
+
+      expect(find.text('trailingIcon'), findsOneWidget);
+    });
+
+    testWidgets('diagnostics', (WidgetTester tester) async {
+      final ButtonStyle style = ButtonStyle(
+        shape: MaterialStateProperty.all<OutlinedBorder?>(const StadiumBorder()),
+        elevation: MaterialStateProperty.all<double?>(10.0),
+        backgroundColor: const MaterialStatePropertyAll<Color>(Colors.red),
+      );
+      final MenuStyle menuStyle = MenuStyle(
+        shape: MaterialStateProperty.all<OutlinedBorder?>(const RoundedRectangleBorder()),
+        elevation: MaterialStateProperty.all<double?>(20.0),
+        backgroundColor: const MaterialStatePropertyAll<Color>(Colors.green),
+      );
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: MenuBar(
+              controller: controller,
+              children: <Widget>[
+                SubmenuButton(
+                  style: style,
+                  menuStyle: menuStyle,
+                  menuChildren: <Widget>[
+                    MenuItemButton(
+                      style: style,
+                      child: Text(TestMenu.subMenu00.label),
+                    ),
+                  ],
+                  child: Text(TestMenu.mainMenu0.label),
+                ),
+              ],
+            ),
+          ),
+        ),
+      );
+
+      await tester.tap(find.text(TestMenu.mainMenu0.label));
+      await tester.pump();
+
+      final SubmenuButton submenu = tester.widget(find.byType(SubmenuButton));
+      final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
+      submenu.debugFillProperties(builder);
+
+      final List<String> description = builder.properties
+          .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
+          .map((DiagnosticsNode node) => node.toString())
+          .toList();
+
+      expect(
+        description,
+        equalsIgnoringHashCodes(
+          <String>[
+            'child: Text("Menu 0")',
+            'focusNode: null',
+            'menuStyle: MenuStyle#00000(backgroundColor: MaterialStatePropertyAll(MaterialColor(primary value: Color(0xff4caf50))), elevation: MaterialStatePropertyAll(20.0), shape: MaterialStatePropertyAll(RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.zero)))',
+            'alignmentOffset: null',
+            'clipBehavior: none',
+          ],
+        ),
+      );
+    });
+  });
+
+  group('Layout', () {
+    List<Rect> collectMenuRects() {
+      final List<Rect> menuRects = <Rect>[];
+      final List<Element> candidates = find.byType(SubmenuButton).evaluate().toList();
+      for (final Element candidate in candidates) {
+        final RenderBox box = candidate.renderObject! as RenderBox;
+        final Offset topLeft = box.localToGlobal(box.size.topLeft(Offset.zero));
+        final Offset bottomRight = box.localToGlobal(box.size.bottomRight(Offset.zero));
+        menuRects.add(Rect.fromPoints(topLeft, bottomRight));
+      }
+      return menuRects;
+    }
+
+    testWidgets('unconstrained menus show up in the right place in LTR', (WidgetTester tester) async {
+      await tester.binding.setSurfaceSize(const Size(800, 600));
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: Column(
+              children: <Widget>[
+                Row(
+                  children: <Widget>[
+                    Expanded(
+                      child: MenuBar(
+                        children: createTestMenus(onPressed: onPressed),
+                      ),
+                    ),
+                  ],
+                ),
+                const Expanded(child: Placeholder()),
+              ],
+            ),
+          ),
+        ),
+      );
+      await tester.pump();
+
+      await tester.tap(find.text(TestMenu.mainMenu1.label));
+      await tester.pump();
+      await tester.tap(find.text(TestMenu.subMenu11.label));
+      await tester.pump();
+
+      expect(find.byType(MenuItemButton), findsNWidgets(6));
+      expect(find.byType(SubmenuButton), findsNWidgets(4));
+      final List<Rect> menuRects = collectMenuRects();
+      expect(menuRects[0], equals(const Rect.fromLTRB(4.0, 0.0, 104.0, 48.0)));
+      expect(menuRects[1], equals(const Rect.fromLTRB(104.0, 0.0, 204.0, 48.0)));
+      expect(menuRects[2], equals(const Rect.fromLTRB(204.0, 0.0, 304.0, 48.0)));
+      expect(menuRects[3], equals(const Rect.fromLTRB(104.0, 100.0, 334.0, 148.0)));
+    });
+
+    testWidgets('unconstrained menus show up in the right place in RTL', (WidgetTester tester) async {
+      await tester.binding.setSurfaceSize(const Size(800, 600));
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Directionality(
+            textDirection: TextDirection.rtl,
+            child: Material(
+              child: Column(
+                children: <Widget>[
+                  Row(
+                    children: <Widget>[
+                      Expanded(
+                        child: MenuBar(
+                          children: createTestMenus(onPressed: onPressed),
+                        ),
+                      ),
+                    ],
+                  ),
+                  const Expanded(child: Placeholder()),
+                ],
+              ),
+            ),
+          ),
+        ),
+      );
+      await tester.pump();
+
+      await tester.tap(find.text(TestMenu.mainMenu1.label));
+      await tester.pump();
+      await tester.tap(find.text(TestMenu.subMenu11.label));
+      await tester.pump();
+
+      expect(find.byType(MenuItemButton), findsNWidgets(6));
+      expect(find.byType(SubmenuButton), findsNWidgets(4));
+      final List<Rect> menuRects = collectMenuRects();
+      expect(menuRects[0], equals(const Rect.fromLTRB(696.0, 0.0, 796.0, 48.0)));
+      expect(menuRects[1], equals(const Rect.fromLTRB(596.0, 0.0, 696.0, 48.0)));
+      expect(menuRects[2], equals(const Rect.fromLTRB(496.0, 0.0, 596.0, 48.0)));
+      expect(menuRects[3], equals(const Rect.fromLTRB(466.0, 100.0, 696.0, 148.0)));
+    });
+
+    testWidgets('constrained menus show up in the right place in LTR', (WidgetTester tester) async {
+      await tester.binding.setSurfaceSize(const Size(300, 300));
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Builder(
+            builder: (BuildContext context) {
+              return Directionality(
+                textDirection: TextDirection.ltr,
+                child: Material(
+                  child: Column(
+                    children: <Widget>[
+                      MenuBar(
+                        children: createTestMenus(onPressed: onPressed),
+                      ),
+                      const Expanded(child: Placeholder()),
+                    ],
+                  ),
+                ),
+              );
+            },
+          ),
+        ),
+      );
+      await tester.pump();
+
+      await tester.tap(find.text(TestMenu.mainMenu1.label));
+      await tester.pump();
+      await tester.tap(find.text(TestMenu.subMenu11.label));
+      await tester.pump();
+
+      expect(find.byType(MenuItemButton), findsNWidgets(6));
+      expect(find.byType(SubmenuButton), findsNWidgets(4));
+      final List<Rect> menuRects = collectMenuRects();
+      expect(menuRects[0], equals(const Rect.fromLTRB(4.0, 0.0, 104.0, 48.0)));
+      expect(menuRects[1], equals(const Rect.fromLTRB(104.0, 0.0, 204.0, 48.0)));
+      expect(menuRects[2], equals(const Rect.fromLTRB(204.0, 0.0, 304.0, 48.0)));
+      expect(menuRects[3], equals(const Rect.fromLTRB(70.0, 100.0, 300.0, 148.0)));
+    });
+
+    testWidgets('constrained menus show up in the right place in RTL', (WidgetTester tester) async {
+      await tester.binding.setSurfaceSize(const Size(300, 300));
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Builder(
+            builder: (BuildContext context) {
+              return Directionality(
+                textDirection: TextDirection.rtl,
+                child: Material(
+                  child: Column(
+                    children: <Widget>[
+                      MenuBar(
+                        children: createTestMenus(onPressed: onPressed),
+                      ),
+                      const Expanded(child: Placeholder()),
+                    ],
+                  ),
+                ),
+              );
+            },
+          ),
+        ),
+      );
+      await tester.pump();
+
+      await tester.tap(find.text(TestMenu.mainMenu1.label));
+      await tester.pump();
+      await tester.tap(find.text(TestMenu.subMenu11.label));
+      await tester.pump();
+
+      expect(find.byType(MenuItemButton), findsNWidgets(6));
+      expect(find.byType(SubmenuButton), findsNWidgets(4));
+      final List<Rect> menuRects = collectMenuRects();
+      expect(menuRects[0], equals(const Rect.fromLTRB(196.0, 0.0, 296.0, 48.0)));
+      expect(menuRects[1], equals(const Rect.fromLTRB(96.0, 0.0, 196.0, 48.0)));
+      expect(menuRects[2], equals(const Rect.fromLTRB(-4.0, 0.0, 96.0, 48.0)));
+      expect(menuRects[3], equals(const Rect.fromLTRB(0.0, 100.0, 230.0, 148.0)));
+    });
+  });
+
+  group('LocalizedShortcutLabeler', () {
+    testWidgets('getShortcutLabel returns the right labels', (WidgetTester tester) async {
+      String expectedMeta;
+      String expectedCtrl;
+      String expectedAlt;
+      switch (defaultTargetPlatform) {
+        case TargetPlatform.android:
+        case TargetPlatform.fuchsia:
+        case TargetPlatform.linux:
+          expectedCtrl = 'Ctrl';
+          expectedMeta = 'Meta';
+          expectedAlt = 'Alt';
+          break;
+        case TargetPlatform.windows:
+          expectedCtrl = 'Ctrl';
+          expectedMeta = 'Win';
+          expectedAlt = 'Alt';
+          break;
+        case TargetPlatform.iOS:
+        case TargetPlatform.macOS:
+          expectedCtrl = '⌃';
+          expectedMeta = '⌘';
+          expectedAlt = '⌥';
+          break;
+      }
+
+      const SingleActivator allModifiers = SingleActivator(
+        LogicalKeyboardKey.keyA,
+        control: true,
+        meta: true,
+        shift: true,
+        alt: true,
+      );
+      final String allExpected = '$expectedAlt $expectedCtrl $expectedMeta ⇧ A';
+      const CharacterActivator charShortcuts = CharacterActivator('ñ');
+      const String charExpected = 'ñ';
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: MenuBar(
+              controller: controller,
+              children: <Widget>[
+                SubmenuButton(
+                  menuChildren: <Widget>[
+                    MenuItemButton(
+                      shortcut: allModifiers,
+                      child: Text(TestMenu.subMenu10.label),
+                    ),
+                    MenuItemButton(
+                      shortcut: charShortcuts,
+                      child: Text(TestMenu.subMenu11.label),
+                    ),
+                  ],
+                  child: Text(TestMenu.mainMenu0.label),
+                ),
+              ],
+            ),
+          ),
+        ),
+      );
+      await tester.tap(find.text(TestMenu.mainMenu0.label));
+      await tester.pump();
+
+      expect(find.text(allExpected), findsOneWidget);
+      expect(find.text(charExpected), findsOneWidget);
+    }, variant: TargetPlatformVariant.all());
+  });
+}
+
+List<Widget> createTestMenus({
+  void Function(TestMenu)? onPressed,
+  void Function(TestMenu)? onOpen,
+  void Function(TestMenu)? onClose,
+  Map<TestMenu, MenuSerializableShortcut> shortcuts = const <TestMenu, MenuSerializableShortcut>{},
+  bool includeStandard = false,
+  bool includeExtraGroups = false,
+}) {
+  final List<Widget> result = <Widget>[
+    SubmenuButton(
+      onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu0) : null,
+      onClose: onClose != null ? () => onClose(TestMenu.mainMenu0) : null,
+      menuChildren: <Widget>[
+        MenuItemButton(
+          onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu00) : null,
+          shortcut: shortcuts[TestMenu.subMenu00],
+          child: Text(TestMenu.subMenu00.label),
+        ),
+        MenuItemButton(
+          onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu01) : null,
+          shortcut: shortcuts[TestMenu.subMenu01],
+          child: Text(TestMenu.subMenu01.label),
+        ),
+        MenuItemButton(
+          onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu02) : null,
+          shortcut: shortcuts[TestMenu.subMenu02],
+          child: Text(TestMenu.subMenu02.label),
+        ),
+      ],
+      child: Text(TestMenu.mainMenu0.label),
+    ),
+    SubmenuButton(
+      onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu1) : null,
+      onClose: onClose != null ? () => onClose(TestMenu.mainMenu1) : null,
+      menuChildren: <Widget>[
+        MenuItemButton(
+          onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu10) : null,
+          shortcut: shortcuts[TestMenu.subMenu10],
+          child: Text(TestMenu.subMenu10.label),
+        ),
+        SubmenuButton(
+          onOpen: onOpen != null ? () => onOpen(TestMenu.subMenu11) : null,
+          onClose: onClose != null ? () => onClose(TestMenu.subMenu11) : null,
+          menuChildren: <Widget>[
+            MenuItemButton(
+              key: UniqueKey(),
+              onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu110) : null,
+              shortcut: shortcuts[TestMenu.subSubMenu110],
+              child: Text(TestMenu.subSubMenu110.label),
+            ),
+            MenuItemButton(
+              onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu111) : null,
+              shortcut: shortcuts[TestMenu.subSubMenu111],
+              child: Text(TestMenu.subSubMenu111.label),
+            ),
+            MenuItemButton(
+              onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu112) : null,
+              shortcut: shortcuts[TestMenu.subSubMenu112],
+              child: Text(TestMenu.subSubMenu112.label),
+            ),
+            MenuItemButton(
+              onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu113) : null,
+              shortcut: shortcuts[TestMenu.subSubMenu113],
+              child: Text(TestMenu.subSubMenu113.label),
+            ),
+          ],
+          child: Text(TestMenu.subMenu11.label),
+        ),
+        MenuItemButton(
+          onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu12) : null,
+          shortcut: shortcuts[TestMenu.subMenu12],
+          child: Text(TestMenu.subMenu12.label),
+        ),
+      ],
+      child: Text(TestMenu.mainMenu1.label),
+    ),
+    SubmenuButton(
+      onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu2) : null,
+      onClose: onClose != null ? () => onClose(TestMenu.mainMenu2) : null,
+      menuChildren: <Widget>[
+        MenuItemButton(
+          // Always disabled.
+          shortcut: shortcuts[TestMenu.subMenu20],
+          child: Text(TestMenu.subMenu20.label),
+        ),
+      ],
+      child: Text(TestMenu.mainMenu2.label),
+    ),
+    if (includeExtraGroups)
+      SubmenuButton(
+        onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu3) : null,
+        onClose: onClose != null ? () => onClose(TestMenu.mainMenu3) : null,
+        menuChildren: <Widget>[
+          MenuItemButton(
+            // Always disabled.
+            shortcut: shortcuts[TestMenu.subMenu30],
+            // Always disabled.
+            child: Text(TestMenu.subMenu30.label),
+          ),
+        ],
+        child: Text(TestMenu.mainMenu3.label),
+      ),
+    if (includeExtraGroups)
+      SubmenuButton(
+        onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu4) : null,
+        onClose: onClose != null ? () => onClose(TestMenu.mainMenu4) : null,
+        menuChildren: <Widget>[
+          MenuItemButton(
+            // Always disabled.
+            shortcut: shortcuts[TestMenu.subMenu40],
+            // Always disabled.
+            child: Text(TestMenu.subMenu40.label),
+          ),
+          MenuItemButton(
+            // Always disabled.
+            shortcut: shortcuts[TestMenu.subMenu41],
+            // Always disabled.
+            child: Text(TestMenu.subMenu41.label),
+          ),
+          MenuItemButton(
+            // Always disabled.
+            shortcut: shortcuts[TestMenu.subMenu42],
+            // Always disabled.
+            child: Text(TestMenu.subMenu42.label),
+          ),
+        ],
+        child: Text(TestMenu.mainMenu4.label),
+      ),
+  ];
+  return result;
+}
+
+enum TestMenu {
+  mainMenu0('Menu 0'),
+  mainMenu1('Menu 1'),
+  mainMenu2('Menu 2'),
+  mainMenu3('Menu 3'),
+  mainMenu4('Menu 4'),
+  subMenu00('Sub Menu 00'),
+  subMenu01('Sub Menu 01'),
+  subMenu02('Sub Menu 02'),
+  subMenu10('Sub Menu 10'),
+  subMenu11('Sub Menu 11'),
+  subMenu12('Sub Menu 12'),
+  subMenu20('Sub Menu 20'),
+  subMenu30('Sub Menu 30'),
+  subMenu40('Sub Menu 40'),
+  subMenu41('Sub Menu 41'),
+  subMenu42('Sub Menu 42'),
+  subSubMenu110('Sub Sub Menu 110'),
+  subSubMenu111('Sub Sub Menu 111'),
+  subSubMenu112('Sub Sub Menu 112'),
+  subSubMenu113('Sub Sub Menu 113');
+
+  const TestMenu(this.label);
+  final String label;
+}
diff --git a/packages/flutter/test/material/menu_bar_theme_test.dart b/packages/flutter/test/material/menu_bar_theme_test.dart
new file mode 100644
index 0000000..b705d10
--- /dev/null
+++ b/packages/flutter/test/material/menu_bar_theme_test.dart
@@ -0,0 +1,315 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  void onPressed(TestMenu item) {}
+
+  Finder findMenuPanels(Axis orientation) {
+    return find.byWidgetPredicate((Widget widget) {
+      // ignore: avoid_dynamic_calls
+      return widget.runtimeType.toString() == '_MenuPanel' && (widget as dynamic).orientation == orientation;
+    });
+  }
+
+  Finder findMenuBarPanel() {
+    return findMenuPanels(Axis.horizontal);
+  }
+
+  Finder findSubmenuPanel() {
+    return findMenuPanels(Axis.vertical);
+  }
+
+  Finder findSubMenuItem() {
+    return find.descendant(of: findSubmenuPanel().last, matching: find.byType(MenuItemButton));
+  }
+
+  Material getMenuBarPanelMaterial(WidgetTester tester) {
+    return tester.widget<Material>(find.descendant(of: findMenuBarPanel(), matching: find.byType(Material)).first);
+  }
+
+  Material getSubmenuPanelMaterial(WidgetTester tester) {
+    return tester.widget<Material>(find.descendant(of: findSubmenuPanel(), matching: find.byType(Material)).first);
+  }
+
+  DefaultTextStyle getLabelStyle(WidgetTester tester, String labelText) {
+    return tester.widget<DefaultTextStyle>(
+      find
+          .ancestor(
+            of: find.text(labelText),
+            matching: find.byType(DefaultTextStyle),
+          )
+          .first,
+    );
+  }
+
+  testWidgets('theme is honored', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      MaterialApp(
+        home: Material(
+          child: Builder(builder: (BuildContext context) {
+            return MenuTheme(
+              data: const MenuThemeData(
+                style: MenuStyle(
+                  backgroundColor: MaterialStatePropertyAll<Color?>(Colors.green),
+                  elevation: MaterialStatePropertyAll<double?>(20.0),
+                ),
+              ),
+              child: MenuBarTheme(
+                data: const MenuBarThemeData(
+                  style: MenuStyle(
+                    backgroundColor: MaterialStatePropertyAll<Color?>(Colors.red),
+                    elevation: MaterialStatePropertyAll<double?>(15.0),
+                    shape: MaterialStatePropertyAll<OutlinedBorder?>(StadiumBorder()),
+                    padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(
+                      EdgeInsetsDirectional.all(10.0),
+                    ),
+                  ),
+                ),
+                child: Column(
+                  children: <Widget>[
+                    MenuBar(
+                      children: createTestMenus(onPressed: onPressed),
+                    ),
+                    const Expanded(child: Placeholder()),
+                  ],
+                ),
+              ),
+            );
+          }),
+        ),
+      ),
+    );
+
+    // Open a test menu.
+    await tester.tap(find.text(TestMenu.mainMenu1.label));
+    await tester.pump();
+    expect(tester.getRect(findMenuBarPanel().first), equals(const Rect.fromLTRB(240.0, 0.0, 560.0, 68.0)));
+    final Material menuBarMaterial = getMenuBarPanelMaterial(tester);
+    expect(menuBarMaterial.elevation, equals(15));
+    expect(menuBarMaterial.color, equals(Colors.red));
+
+    final Material subMenuMaterial = getSubmenuPanelMaterial(tester);
+    expect(tester.getRect(findSubmenuPanel()), equals(const Rect.fromLTRB(350.0, 58.0, 580.0, 210.0)));
+    expect(subMenuMaterial.elevation, equals(20));
+    expect(subMenuMaterial.color, equals(Colors.green));
+  });
+
+  testWidgets('Constructor parameters override theme parameters', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      MaterialApp(
+        home: Material(
+          child: Builder(
+            builder: (BuildContext context) {
+              return MenuTheme(
+                data: const MenuThemeData(
+                  style: MenuStyle(
+                    backgroundColor: MaterialStatePropertyAll<Color?>(Colors.green),
+                    elevation: MaterialStatePropertyAll<double?>(20.0),
+                  ),
+                ),
+                child: MenuBarTheme(
+                  data: const MenuBarThemeData(
+                    style: MenuStyle(
+                      backgroundColor: MaterialStatePropertyAll<Color?>(Colors.red),
+                      elevation: MaterialStatePropertyAll<double?>(15.0),
+                      shape: MaterialStatePropertyAll<OutlinedBorder?>(StadiumBorder()),
+                      padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(
+                        EdgeInsetsDirectional.all(10.0),
+                      ),
+                    ),
+                  ),
+                  child: Column(
+                    children: <Widget>[
+                      MenuBar(
+                        style: const MenuStyle(
+                          backgroundColor: MaterialStatePropertyAll<Color?>(Colors.blue),
+                          elevation: MaterialStatePropertyAll<double?>(10.0),
+                          padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(
+                            EdgeInsetsDirectional.all(12.0),
+                          ),
+                        ),
+                        children: createTestMenus(
+                          onPressed: onPressed,
+                          menuBackground: Colors.cyan,
+                          menuElevation: 18.0,
+                          menuPadding: const EdgeInsetsDirectional.all(14.0),
+                          menuShape: const BeveledRectangleBorder(),
+                          itemBackground: Colors.amber,
+                          itemForeground: Colors.grey,
+                          itemOverlay: Colors.blueGrey,
+                          itemPadding: const EdgeInsetsDirectional.all(11.0),
+                          itemShape: const StadiumBorder(),
+                        ),
+                      ),
+                      const Expanded(child: Placeholder()),
+                    ],
+                  ),
+                ),
+              );
+            },
+          ),
+        ),
+      ),
+    );
+
+    // Open a test menu.
+    await tester.tap(find.text(TestMenu.mainMenu1.label));
+    await tester.pump();
+
+    expect(tester.getRect(findMenuBarPanel().first), equals(const Rect.fromLTRB(238.0, 0.0, 562.0, 72.0)));
+    final Material menuBarMaterial = getMenuBarPanelMaterial(tester);
+    expect(menuBarMaterial.elevation, equals(10.0));
+    expect(menuBarMaterial.color, equals(Colors.blue));
+
+    final Material subMenuMaterial = getSubmenuPanelMaterial(tester);
+    expect(tester.getRect(findSubmenuPanel()), equals(const Rect.fromLTRB(336.0, 60.0, 594.0, 232.0)));
+    expect(subMenuMaterial.elevation, equals(18));
+    expect(subMenuMaterial.color, equals(Colors.cyan));
+    expect(subMenuMaterial.shape, equals(const BeveledRectangleBorder()));
+
+    final Finder menuItem = findSubMenuItem();
+    expect(tester.getRect(menuItem.first), equals(const Rect.fromLTRB(350.0, 74.0, 580.0, 122.0)));
+    final Material menuItemMaterial = tester.widget<Material>(
+        find.ancestor(of: find.text(TestMenu.subMenu10.label), matching: find.byType(Material)).first);
+    expect(menuItemMaterial.color, equals(Colors.amber));
+    expect(menuItemMaterial.elevation, equals(0.0));
+    expect(menuItemMaterial.shape, equals(const StadiumBorder()));
+    expect(getLabelStyle(tester, TestMenu.subMenu10.label).style.color, equals(Colors.grey));
+    final ButtonStyle? textButtonStyle = tester
+        .widget<TextButton>(find
+            .ancestor(
+              of: find.text(TestMenu.subMenu10.label),
+              matching: find.byType(TextButton),
+            )
+            .first)
+        .style;
+    expect(textButtonStyle?.overlayColor?.resolve(<MaterialState>{MaterialState.hovered}), equals(Colors.blueGrey));
+  });
+}
+
+List<Widget> createTestMenus({
+  void Function(TestMenu)? onPressed,
+  void Function(TestMenu)? onOpen,
+  void Function(TestMenu)? onClose,
+  Map<TestMenu, MenuSerializableShortcut> shortcuts = const <TestMenu, MenuSerializableShortcut>{},
+  bool includeStandard = false,
+  Color? itemOverlay,
+  Color? itemBackground,
+  Color? itemForeground,
+  EdgeInsetsDirectional? itemPadding,
+  Color? menuBackground,
+  EdgeInsetsDirectional? menuPadding,
+  OutlinedBorder? menuShape,
+  double? menuElevation,
+  OutlinedBorder? itemShape,
+}) {
+  final MenuStyle menuStyle = MenuStyle(
+    padding: menuPadding != null ? MaterialStatePropertyAll<EdgeInsetsGeometry>(menuPadding) : null,
+    backgroundColor: menuBackground != null ? MaterialStatePropertyAll<Color>(menuBackground) : null,
+    elevation: menuElevation != null ? MaterialStatePropertyAll<double>(menuElevation) : null,
+    shape: menuShape != null ? MaterialStatePropertyAll<OutlinedBorder>(menuShape) : null,
+  );
+  final ButtonStyle itemStyle = ButtonStyle(
+    padding: itemPadding != null ? MaterialStatePropertyAll<EdgeInsetsGeometry>(itemPadding) : null,
+    shape: itemShape != null ? MaterialStatePropertyAll<OutlinedBorder>(itemShape) : null,
+    foregroundColor: itemForeground != null ? MaterialStatePropertyAll<Color>(itemForeground) : null,
+    backgroundColor: itemBackground != null ? MaterialStatePropertyAll<Color>(itemBackground) : null,
+    overlayColor: itemOverlay != null ? MaterialStatePropertyAll<Color>(itemOverlay) : null,
+  );
+  final List<Widget> result = <Widget>[
+    SubmenuButton(
+      onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu0) : null,
+      onClose: onClose != null ? () => onClose(TestMenu.mainMenu0) : null,
+      menuChildren: <Widget>[
+        MenuItemButton(
+          onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu00) : null,
+          shortcut: shortcuts[TestMenu.subMenu00],
+          child: Text(TestMenu.subMenu00.label),
+        ),
+      ],
+      child: Text(TestMenu.mainMenu0.label),
+    ),
+    SubmenuButton(
+      onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu1) : null,
+      onClose: onClose != null ? () => onClose(TestMenu.mainMenu1) : null,
+      menuStyle: menuStyle,
+      menuChildren: <Widget>[
+        MenuItemButton(
+          onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu10) : null,
+          shortcut: shortcuts[TestMenu.subMenu10],
+          style: itemStyle,
+          child: Text(TestMenu.subMenu10.label),
+        ),
+        SubmenuButton(
+          onOpen: onOpen != null ? () => onOpen(TestMenu.subMenu11) : null,
+          onClose: onClose != null ? () => onClose(TestMenu.subMenu11) : null,
+          menuChildren: <Widget>[
+            MenuItemButton(
+              onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu110) : null,
+              shortcut: shortcuts[TestMenu.subSubMenu110],
+              child: Text(TestMenu.subSubMenu110.label),
+            ),
+            MenuItemButton(
+              onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu111) : null,
+              shortcut: shortcuts[TestMenu.subSubMenu111],
+              child: Text(TestMenu.subSubMenu111.label),
+            ),
+            MenuItemButton(
+              onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu112) : null,
+              shortcut: shortcuts[TestMenu.subSubMenu112],
+              child: Text(TestMenu.subSubMenu112.label),
+            ),
+            MenuItemButton(
+              onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu113) : null,
+              shortcut: shortcuts[TestMenu.subSubMenu113],
+              child: Text(TestMenu.subSubMenu113.label),
+            ),
+          ],
+          child: Text(TestMenu.subMenu11.label),
+        ),
+        MenuItemButton(
+          onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu12) : null,
+          shortcut: shortcuts[TestMenu.subMenu12],
+          child: Text(TestMenu.subMenu12.label),
+        ),
+      ],
+      child: Text(TestMenu.mainMenu1.label),
+    ),
+    SubmenuButton(
+      onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu2) : null,
+      onClose: onClose != null ? () => onClose(TestMenu.mainMenu2) : null,
+      menuChildren: <Widget>[
+        MenuItemButton(
+          // Always disabled.
+          shortcut: shortcuts[TestMenu.subMenu20],
+          // Always disabled.
+          child: Text(TestMenu.subMenu20.label),
+        ),
+      ],
+      child: Text(TestMenu.mainMenu2.label),
+    ),
+  ];
+  return result;
+}
+
+enum TestMenu {
+  mainMenu0('Menu 0'),
+  mainMenu1('Menu 1'),
+  mainMenu2('Menu 2'),
+  subMenu00('Sub Menu 00'),
+  subMenu10('Sub Menu 10'),
+  subMenu11('Sub Menu 11'),
+  subMenu12('Sub Menu 12'),
+  subMenu20('Sub Menu 20'),
+  subSubMenu110('Sub Sub Menu 110'),
+  subSubMenu111('Sub Sub Menu 111'),
+  subSubMenu112('Sub Sub Menu 112'),
+  subSubMenu113('Sub Sub Menu 113');
+
+  const TestMenu(this.label);
+  final String label;
+}
diff --git a/packages/flutter/test/material/menu_style_test.dart b/packages/flutter/test/material/menu_style_test.dart
new file mode 100644
index 0000000..66412d0
--- /dev/null
+++ b/packages/flutter/test/material/menu_style_test.dart
@@ -0,0 +1,439 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  Finder findMenuPanels() {
+    return find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_MenuPanel');
+  }
+
+  Material getMenuBarMaterial(WidgetTester tester) {
+    return tester.widget<Material>(
+      find.descendant(of: findMenuPanels(), matching: find.byType(Material)).first,
+    );
+  }
+
+  Padding getMenuBarPadding(WidgetTester tester) {
+    return tester.widget<Padding>(
+      find.descendant(of: findMenuPanels(), matching: find.byType(Padding)).first,
+    );
+  }
+
+  Material getMenuMaterial(WidgetTester tester) {
+    return tester.widget<Material>(
+      find.descendant(of: findMenuPanels().at(1), matching: find.byType(Material)).first,
+    );
+  }
+
+  Padding getMenuPadding(WidgetTester tester) {
+    return tester.widget<Padding>(
+      find.descendant(of: findMenuPanels().at(1), matching: find.byType(Padding)).first,
+    );
+  }
+
+  group('MenuStyle', () {
+    testWidgets('fixedSize affects geometry', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: Column(
+              children: <Widget>[
+                MenuBarTheme(
+                  data: const MenuBarThemeData(
+                    style: MenuStyle(
+                      fixedSize: MaterialStatePropertyAll<Size>(Size(600, 60)),
+                    ),
+                  ),
+                  child: MenuTheme(
+                    data: const MenuThemeData(
+                      style: MenuStyle(
+                        fixedSize: MaterialStatePropertyAll<Size>(Size(100, 100)),
+                      ),
+                    ),
+                    child: MenuBar(
+                      children: createTestMenus(onPressed: (TestMenu menu) {}),
+                    ),
+                  ),
+                ),
+                const Expanded(child: Placeholder()),
+              ],
+            ),
+          ),
+        ),
+      );
+
+      // Have to open a menu initially to start things going.
+      await tester.tap(find.text(TestMenu.mainMenu0.label));
+      await tester.pump();
+
+      // MenuBarTheme affects MenuBar.
+      expect(tester.getRect(findMenuPanels().first), equals(const Rect.fromLTRB(100.0, 0.0, 700.0, 60.0)));
+      expect(tester.getRect(findMenuPanels().first).size, equals(const Size(600.0, 60.0)));
+
+      // MenuTheme affects menus.
+      expect(tester.getRect(findMenuPanels().at(1)), equals(const Rect.fromLTRB(104.0, 54.0, 204.0, 154.0)));
+      expect(tester.getRect(findMenuPanels().at(1)).size, equals(const Size(100.0, 100.0)));
+    });
+
+    testWidgets('maximumSize affects geometry', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: Column(
+              children: <Widget>[
+                MenuBarTheme(
+                  data: const MenuBarThemeData(
+                    style: MenuStyle(
+                      maximumSize: MaterialStatePropertyAll<Size>(Size(250, 40)),
+                    ),
+                  ),
+                  child: MenuTheme(
+                    data: const MenuThemeData(
+                      style: MenuStyle(
+                        maximumSize: MaterialStatePropertyAll<Size>(Size(100, 100)),
+                      ),
+                    ),
+                    child: MenuBar(
+                      children: createTestMenus(onPressed: (TestMenu menu) {}),
+                    ),
+                  ),
+                ),
+                const Expanded(child: Placeholder()),
+              ],
+            ),
+          ),
+        ),
+      );
+
+      // Have to open a menu initially to start things going.
+      await tester.tap(find.text(TestMenu.mainMenu0.label));
+      await tester.pump();
+
+      // MenuBarTheme affects MenuBar.
+      expect(tester.getRect(findMenuPanels().first), equals(const Rect.fromLTRB(275.0, 0.0, 525.0, 40.0)));
+      expect(tester.getRect(findMenuPanels().first).size, equals(const Size(250.0, 40.0)));
+
+      // MenuTheme affects menus.
+      expect(tester.getRect(findMenuPanels().at(1)), equals(const Rect.fromLTRB(279.0, 44.0, 379.0, 144.0)));
+      expect(tester.getRect(findMenuPanels().at(1)).size, equals(const Size(100.0, 100.0)));
+    });
+
+    testWidgets('minimumSize affects geometry', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: Column(
+              children: <Widget>[
+                MenuBarTheme(
+                  data: const MenuBarThemeData(
+                    style: MenuStyle(
+                      minimumSize: MaterialStatePropertyAll<Size>(Size(400, 60)),
+                    ),
+                  ),
+                  child: MenuTheme(
+                    data: const MenuThemeData(
+                      style: MenuStyle(
+                        minimumSize: MaterialStatePropertyAll<Size>(Size(300, 300)),
+                      ),
+                    ),
+                    child: MenuBar(
+                      children: createTestMenus(onPressed: (TestMenu menu) {}),
+                    ),
+                  ),
+                ),
+                const Expanded(child: Placeholder()),
+              ],
+            ),
+          ),
+        ),
+      );
+
+      // Have to open a menu initially to start things going.
+      await tester.tap(find.text(TestMenu.mainMenu0.label));
+      await tester.pump();
+
+      // MenuBarTheme affects MenuBar.
+      expect(tester.getRect(findMenuPanels().first), equals(const Rect.fromLTRB(200.0, 0.0, 600.0, 60.0)));
+      expect(tester.getRect(findMenuPanels().first).size, equals(const Size(400.0, 60.0)));
+
+      // MenuTheme affects menus.
+      expect(tester.getRect(findMenuPanels().at(1)), equals(const Rect.fromLTRB(204.0, 54.0, 504.0, 354.0)));
+      expect(tester.getRect(findMenuPanels().at(1)).size, equals(const Size(300.0, 300.0)));
+    });
+
+    testWidgets('Material parameters are honored', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: Column(
+              children: <Widget>[
+                MenuBarTheme(
+                  data: const MenuBarThemeData(
+                    style: MenuStyle(
+                      backgroundColor: MaterialStatePropertyAll<Color>(Colors.red),
+                      shadowColor: MaterialStatePropertyAll<Color>(Colors.green),
+                      surfaceTintColor: MaterialStatePropertyAll<Color>(Colors.blue),
+                      padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(EdgeInsets.all(10)),
+                      elevation: MaterialStatePropertyAll<double>(10),
+                      side: MaterialStatePropertyAll<BorderSide>(BorderSide(color: Colors.redAccent)),
+                      shape: MaterialStatePropertyAll<OutlinedBorder>(StadiumBorder()),
+                    ),
+                  ),
+                  child: MenuTheme(
+                    data: const MenuThemeData(
+                      style: MenuStyle(
+                        backgroundColor: MaterialStatePropertyAll<Color>(Colors.cyan),
+                        shadowColor: MaterialStatePropertyAll<Color>(Colors.purple),
+                        surfaceTintColor: MaterialStatePropertyAll<Color>(Colors.yellow),
+                        padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(EdgeInsets.all(20)),
+                        elevation: MaterialStatePropertyAll<double>(20),
+                        side: MaterialStatePropertyAll<BorderSide>(BorderSide(color: Colors.cyanAccent)),
+                        shape: MaterialStatePropertyAll<OutlinedBorder>(StarBorder()),
+                      ),
+                    ),
+                    child: MenuBar(
+                      children: createTestMenus(onPressed: (TestMenu menu) {}),
+                    ),
+                  ),
+                ),
+                const Expanded(child: Placeholder()),
+              ],
+            ),
+          ),
+        ),
+      );
+
+      // Have to open a menu initially to start things going.
+      await tester.tap(find.text(TestMenu.mainMenu0.label));
+      await tester.pump();
+
+      final Material menuBarMaterial = getMenuBarMaterial(tester);
+      final Padding menuBarPadding = getMenuBarPadding(tester);
+      final Material panelMaterial = getMenuMaterial(tester);
+      final Padding panelPadding = getMenuPadding(tester);
+
+      // MenuBarTheme affects MenuBar.
+      expect(menuBarMaterial.color, equals(Colors.red));
+      expect(menuBarMaterial.shadowColor, equals(Colors.green));
+      expect(menuBarMaterial.surfaceTintColor, equals(Colors.blue));
+      expect(menuBarMaterial.shape, equals(const StadiumBorder(side: BorderSide(color: Colors.redAccent))));
+      expect(menuBarPadding.padding, equals(const EdgeInsets.all(10)));
+
+      // MenuBarTheme affects menus.
+      expect(panelMaterial.color, equals(Colors.cyan));
+      expect(panelMaterial.shadowColor, equals(Colors.purple));
+      expect(panelMaterial.surfaceTintColor, equals(Colors.yellow));
+      expect(panelMaterial.shape, equals(const StarBorder(side: BorderSide(color: Colors.cyanAccent))));
+      expect(panelPadding.padding, equals(const EdgeInsets.all(20)));
+    });
+
+    testWidgets('visual density', (WidgetTester tester) async {
+      await tester.pumpWidget(
+        MaterialApp(
+          home: Material(
+            child: Column(
+              children: <Widget>[
+                MenuBarTheme(
+                  data: const MenuBarThemeData(
+                    style: MenuStyle(
+                      visualDensity: VisualDensity(horizontal: 1.5, vertical: -1.5),
+                    ),
+                  ),
+                  child: MenuTheme(
+                    data: const MenuThemeData(
+                      style: MenuStyle(
+                        visualDensity: VisualDensity(horizontal: 0.5, vertical: -0.5),
+                      ),
+                    ),
+                    child: MenuBar(
+                      children: createTestMenus(onPressed: (TestMenu menu) {}),
+                    ),
+                  ),
+                ),
+                const Expanded(child: Placeholder()),
+              ],
+            ),
+          ),
+        ),
+      );
+      await tester.pump();
+
+      expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(240.0, 0.0, 560.0, 48.0)));
+
+      // Open and make sure things are the right size.
+      await tester.tap(find.text(TestMenu.mainMenu1.label));
+      await tester.pump();
+
+      expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(240.0, 0.0, 560.0, 48.0)));
+      expect(
+        tester.getRect(find.text(TestMenu.subMenu10.label)),
+        equals(const Rect.fromLTRB(366.0, 64.0, 520.0, 78.0)),
+      );
+      expect(
+        tester.getRect(find.ancestor(of: find.text(TestMenu.subMenu10.label), matching: find.byType(Material)).at(1)),
+        equals(const Rect.fromLTRB(350.0, 48.0, 602.0, 178.0)),
+      );
+    });
+  });
+}
+
+List<Widget> createTestMenus({
+  void Function(TestMenu)? onPressed,
+  void Function(TestMenu)? onOpen,
+  void Function(TestMenu)? onClose,
+  Map<TestMenu, MenuSerializableShortcut> shortcuts = const <TestMenu, MenuSerializableShortcut>{},
+  bool includeStandard = false,
+  bool includeExtraGroups = false,
+}) {
+  final List<Widget> result = <Widget>[
+    SubmenuButton(
+      onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu0) : null,
+      onClose: onClose != null ? () => onClose(TestMenu.mainMenu0) : null,
+      menuChildren: <Widget>[
+        MenuItemButton(
+          onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu00) : null,
+          shortcut: shortcuts[TestMenu.subMenu00],
+          child: Text(TestMenu.subMenu00.label),
+        ),
+        MenuItemButton(
+          onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu01) : null,
+          shortcut: shortcuts[TestMenu.subMenu01],
+          child: Text(TestMenu.subMenu01.label),
+        ),
+        MenuItemButton(
+          onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu02) : null,
+          shortcut: shortcuts[TestMenu.subMenu02],
+          child: Text(TestMenu.subMenu02.label),
+        ),
+      ],
+      child: Text(TestMenu.mainMenu0.label),
+    ),
+    SubmenuButton(
+      onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu1) : null,
+      onClose: onClose != null ? () => onClose(TestMenu.mainMenu1) : null,
+      menuChildren: <Widget>[
+        MenuItemButton(
+          onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu10) : null,
+          shortcut: shortcuts[TestMenu.subMenu10],
+          child: Text(TestMenu.subMenu10.label),
+        ),
+        SubmenuButton(
+          onOpen: onOpen != null ? () => onOpen(TestMenu.subMenu11) : null,
+          onClose: onClose != null ? () => onClose(TestMenu.subMenu11) : null,
+          menuChildren: <Widget>[
+            MenuItemButton(
+              key: UniqueKey(),
+              onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu110) : null,
+              shortcut: shortcuts[TestMenu.subSubMenu110],
+              child: Text(TestMenu.subSubMenu110.label),
+            ),
+            MenuItemButton(
+              onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu111) : null,
+              shortcut: shortcuts[TestMenu.subSubMenu111],
+              child: Text(TestMenu.subSubMenu111.label),
+            ),
+            MenuItemButton(
+              onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu112) : null,
+              shortcut: shortcuts[TestMenu.subSubMenu112],
+              child: Text(TestMenu.subSubMenu112.label),
+            ),
+            MenuItemButton(
+              onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu113) : null,
+              shortcut: shortcuts[TestMenu.subSubMenu113],
+              child: Text(TestMenu.subSubMenu113.label),
+            ),
+          ],
+          child: Text(TestMenu.subMenu11.label),
+        ),
+        MenuItemButton(
+          onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu12) : null,
+          shortcut: shortcuts[TestMenu.subMenu12],
+          child: Text(TestMenu.subMenu12.label),
+        ),
+      ],
+      child: Text(TestMenu.mainMenu1.label),
+    ),
+    SubmenuButton(
+      onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu2) : null,
+      onClose: onClose != null ? () => onClose(TestMenu.mainMenu2) : null,
+      menuChildren: <Widget>[
+        MenuItemButton(
+          // Always disabled.
+          shortcut: shortcuts[TestMenu.subMenu20],
+          child: Text(TestMenu.subMenu20.label),
+        ),
+      ],
+      child: Text(TestMenu.mainMenu2.label),
+    ),
+    if (includeExtraGroups)
+      SubmenuButton(
+        onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu3) : null,
+        onClose: onClose != null ? () => onClose(TestMenu.mainMenu3) : null,
+        menuChildren: <Widget>[
+          MenuItemButton(
+            // Always disabled.
+            shortcut: shortcuts[TestMenu.subMenu30],
+            // Always disabled.
+            child: Text(TestMenu.subMenu30.label),
+          ),
+        ],
+        child: Text(TestMenu.mainMenu3.label),
+      ),
+    if (includeExtraGroups)
+      SubmenuButton(
+        onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu4) : null,
+        onClose: onClose != null ? () => onClose(TestMenu.mainMenu4) : null,
+        menuChildren: <Widget>[
+          MenuItemButton(
+            // Always disabled.
+            shortcut: shortcuts[TestMenu.subMenu40],
+            // Always disabled.
+            child: Text(TestMenu.subMenu40.label),
+          ),
+          MenuItemButton(
+            // Always disabled.
+            shortcut: shortcuts[TestMenu.subMenu41],
+            // Always disabled.
+            child: Text(TestMenu.subMenu41.label),
+          ),
+          MenuItemButton(
+            // Always disabled.
+            shortcut: shortcuts[TestMenu.subMenu42],
+            // Always disabled.
+            child: Text(TestMenu.subMenu42.label),
+          ),
+        ],
+        child: Text(TestMenu.mainMenu4.label),
+      ),
+  ];
+  return result;
+}
+
+enum TestMenu {
+  mainMenu0('Menu 0'),
+  mainMenu1('Menu 1'),
+  mainMenu2('Menu 2'),
+  mainMenu3('Menu 3'),
+  mainMenu4('Menu 4'),
+  subMenu00('Sub Menu 00'),
+  subMenu01('Sub Menu 01'),
+  subMenu02('Sub Menu 02'),
+  subMenu10('Sub Menu 10'),
+  subMenu11('Sub Menu 11'),
+  subMenu12('Sub Menu 12'),
+  subMenu20('Sub Menu 20'),
+  subMenu30('Sub Menu 30'),
+  subMenu40('Sub Menu 40'),
+  subMenu41('Sub Menu 41'),
+  subMenu42('Sub Menu 42'),
+  subSubMenu110('Sub Sub Menu 110'),
+  subSubMenu111('Sub Sub Menu 111'),
+  subSubMenu112('Sub Sub Menu 112'),
+  subSubMenu113('Sub Sub Menu 113');
+
+  const TestMenu(this.label);
+  final String label;
+}
diff --git a/packages/flutter/test/material/menu_theme_test.dart b/packages/flutter/test/material/menu_theme_test.dart
new file mode 100644
index 0000000..d0459bc
--- /dev/null
+++ b/packages/flutter/test/material/menu_theme_test.dart
@@ -0,0 +1,315 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  void onPressed(TestMenu item) {}
+
+  Finder findMenuPanels(Axis orientation) {
+    return find.byWidgetPredicate((Widget widget) {
+      // ignore: avoid_dynamic_calls
+      return widget.runtimeType.toString() == '_MenuPanel' && (widget as dynamic).orientation == orientation;
+    });
+  }
+
+  Finder findMenuBarPanel() {
+    return findMenuPanels(Axis.horizontal);
+  }
+
+  Finder findSubmenuPanel() {
+    return findMenuPanels(Axis.vertical);
+  }
+
+  Finder findSubMenuItem() {
+    return find.descendant(of: findSubmenuPanel().last, matching: find.byType(MenuItemButton));
+  }
+
+  Material getMenuBarPanelMaterial(WidgetTester tester) {
+    return tester.widget<Material>(find.descendant(of: findMenuBarPanel(), matching: find.byType(Material)).first);
+  }
+
+  Material getSubmenuPanelMaterial(WidgetTester tester) {
+    return tester.widget<Material>(find.descendant(of: findSubmenuPanel(), matching: find.byType(Material)).first);
+  }
+
+  DefaultTextStyle getLabelStyle(WidgetTester tester, String labelText) {
+    return tester.widget<DefaultTextStyle>(
+      find
+          .ancestor(
+            of: find.text(labelText),
+            matching: find.byType(DefaultTextStyle),
+          )
+          .first,
+    );
+  }
+
+  testWidgets('theme is honored', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      MaterialApp(
+        home: Material(
+          child: Builder(builder: (BuildContext context) {
+            return MenuBarTheme(
+              data: const MenuBarThemeData(
+                style: MenuStyle(
+                  backgroundColor: MaterialStatePropertyAll<Color?>(Colors.green),
+                  elevation: MaterialStatePropertyAll<double?>(20.0),
+                ),
+              ),
+              child: MenuTheme(
+                data: const MenuThemeData(
+                  style: MenuStyle(
+                    backgroundColor: MaterialStatePropertyAll<Color?>(Colors.red),
+                    elevation: MaterialStatePropertyAll<double?>(15.0),
+                    shape: MaterialStatePropertyAll<OutlinedBorder?>(StadiumBorder()),
+                    padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(
+                      EdgeInsetsDirectional.all(10.0),
+                    ),
+                  ),
+                ),
+                child: Column(
+                  children: <Widget>[
+                    MenuBar(
+                      children: createTestMenus(onPressed: onPressed),
+                    ),
+                    const Expanded(child: Placeholder()),
+                  ],
+                ),
+              ),
+            );
+          }),
+        ),
+      ),
+    );
+
+    // Open a test menu.
+    await tester.tap(find.text(TestMenu.mainMenu1.label));
+    await tester.pump();
+    expect(tester.getRect(findMenuBarPanel().first), equals(const Rect.fromLTRB(246.0, 0.0, 554.0, 48.0)));
+    final Material menuBarMaterial = getMenuBarPanelMaterial(tester);
+    expect(menuBarMaterial.elevation, equals(20));
+    expect(menuBarMaterial.color, equals(Colors.green));
+
+    final Material subMenuMaterial = getSubmenuPanelMaterial(tester);
+    expect(tester.getRect(findSubmenuPanel()), equals(const Rect.fromLTRB(340.0, 48.0, 590.0, 212.0)));
+    expect(subMenuMaterial.elevation, equals(15));
+    expect(subMenuMaterial.color, equals(Colors.red));
+  });
+
+  testWidgets('Constructor parameters override theme parameters', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      MaterialApp(
+        home: Material(
+          child: Builder(
+            builder: (BuildContext context) {
+              return MenuBarTheme(
+                data: const MenuBarThemeData(
+                  style: MenuStyle(
+                    backgroundColor: MaterialStatePropertyAll<Color?>(Colors.green),
+                    elevation: MaterialStatePropertyAll<double?>(20.0),
+                  ),
+                ),
+                child: MenuTheme(
+                  data: const MenuThemeData(
+                    style: MenuStyle(
+                      backgroundColor: MaterialStatePropertyAll<Color?>(Colors.red),
+                      elevation: MaterialStatePropertyAll<double?>(15.0),
+                      shape: MaterialStatePropertyAll<OutlinedBorder?>(StadiumBorder()),
+                      padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(
+                        EdgeInsetsDirectional.all(10.0),
+                      ),
+                    ),
+                  ),
+                  child: Column(
+                    children: <Widget>[
+                      MenuBar(
+                        style: const MenuStyle(
+                          backgroundColor: MaterialStatePropertyAll<Color?>(Colors.blue),
+                          elevation: MaterialStatePropertyAll<double?>(10.0),
+                          padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(
+                            EdgeInsetsDirectional.all(12.0),
+                          ),
+                        ),
+                        children: createTestMenus(
+                          onPressed: onPressed,
+                          menuBackground: Colors.cyan,
+                          menuElevation: 18.0,
+                          menuPadding: const EdgeInsetsDirectional.all(14.0),
+                          menuShape: const BeveledRectangleBorder(),
+                          itemBackground: Colors.amber,
+                          itemForeground: Colors.grey,
+                          itemOverlay: Colors.blueGrey,
+                          itemPadding: const EdgeInsetsDirectional.all(11.0),
+                          itemShape: const StadiumBorder(),
+                        ),
+                      ),
+                      const Expanded(child: Placeholder()),
+                    ],
+                  ),
+                ),
+              );
+            },
+          ),
+        ),
+      ),
+    );
+
+    // Open a test menu.
+    await tester.tap(find.text(TestMenu.mainMenu1.label));
+    await tester.pump();
+
+    expect(tester.getRect(findMenuBarPanel().first), equals(const Rect.fromLTRB(238.0, 0.0, 562.0, 72.0)));
+    final Material menuBarMaterial = getMenuBarPanelMaterial(tester);
+    expect(menuBarMaterial.elevation, equals(10.0));
+    expect(menuBarMaterial.color, equals(Colors.blue));
+
+    final Material subMenuMaterial = getSubmenuPanelMaterial(tester);
+    expect(tester.getRect(findSubmenuPanel()), equals(const Rect.fromLTRB(336.0, 60.0, 594.0, 232.0)));
+    expect(subMenuMaterial.elevation, equals(18));
+    expect(subMenuMaterial.color, equals(Colors.cyan));
+    expect(subMenuMaterial.shape, equals(const BeveledRectangleBorder()));
+
+    final Finder menuItem = findSubMenuItem();
+    expect(tester.getRect(menuItem.first), equals(const Rect.fromLTRB(350.0, 74.0, 580.0, 122.0)));
+    final Material menuItemMaterial = tester.widget<Material>(
+        find.ancestor(of: find.text(TestMenu.subMenu10.label), matching: find.byType(Material)).first);
+    expect(menuItemMaterial.color, equals(Colors.amber));
+    expect(menuItemMaterial.elevation, equals(0.0));
+    expect(menuItemMaterial.shape, equals(const StadiumBorder()));
+    expect(getLabelStyle(tester, TestMenu.subMenu10.label).style.color, equals(Colors.grey));
+    final ButtonStyle? textButtonStyle = tester
+        .widget<TextButton>(find
+            .ancestor(
+              of: find.text(TestMenu.subMenu10.label),
+              matching: find.byType(TextButton),
+            )
+            .first)
+        .style;
+    expect(textButtonStyle?.overlayColor?.resolve(<MaterialState>{MaterialState.hovered}), equals(Colors.blueGrey));
+  });
+}
+
+List<Widget> createTestMenus({
+  void Function(TestMenu)? onPressed,
+  void Function(TestMenu)? onOpen,
+  void Function(TestMenu)? onClose,
+  Map<TestMenu, MenuSerializableShortcut> shortcuts = const <TestMenu, MenuSerializableShortcut>{},
+  bool includeStandard = false,
+  Color? itemOverlay,
+  Color? itemBackground,
+  Color? itemForeground,
+  EdgeInsetsDirectional? itemPadding,
+  Color? menuBackground,
+  EdgeInsetsDirectional? menuPadding,
+  OutlinedBorder? menuShape,
+  double? menuElevation,
+  OutlinedBorder? itemShape,
+}) {
+  final MenuStyle menuStyle = MenuStyle(
+    padding: menuPadding != null ? MaterialStatePropertyAll<EdgeInsetsGeometry>(menuPadding) : null,
+    backgroundColor: menuBackground != null ? MaterialStatePropertyAll<Color>(menuBackground) : null,
+    elevation: menuElevation != null ? MaterialStatePropertyAll<double>(menuElevation) : null,
+    shape: menuShape != null ? MaterialStatePropertyAll<OutlinedBorder>(menuShape) : null,
+  );
+  final ButtonStyle itemStyle = ButtonStyle(
+    padding: itemPadding != null ? MaterialStatePropertyAll<EdgeInsetsGeometry>(itemPadding) : null,
+    shape: itemShape != null ? MaterialStatePropertyAll<OutlinedBorder>(itemShape) : null,
+    foregroundColor: itemForeground != null ? MaterialStatePropertyAll<Color>(itemForeground) : null,
+    backgroundColor: itemBackground != null ? MaterialStatePropertyAll<Color>(itemBackground) : null,
+    overlayColor: itemOverlay != null ? MaterialStatePropertyAll<Color>(itemOverlay) : null,
+  );
+  final List<Widget> result = <Widget>[
+    SubmenuButton(
+      onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu0) : null,
+      onClose: onClose != null ? () => onClose(TestMenu.mainMenu0) : null,
+      menuChildren: <Widget>[
+        MenuItemButton(
+          onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu00) : null,
+          shortcut: shortcuts[TestMenu.subMenu00],
+          child: Text(TestMenu.subMenu00.label),
+        ),
+      ],
+      child: Text(TestMenu.mainMenu0.label),
+    ),
+    SubmenuButton(
+      onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu1) : null,
+      onClose: onClose != null ? () => onClose(TestMenu.mainMenu1) : null,
+      menuStyle: menuStyle,
+      menuChildren: <Widget>[
+        MenuItemButton(
+          onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu10) : null,
+          shortcut: shortcuts[TestMenu.subMenu10],
+          style: itemStyle,
+          child: Text(TestMenu.subMenu10.label),
+        ),
+        SubmenuButton(
+          onOpen: onOpen != null ? () => onOpen(TestMenu.subMenu11) : null,
+          onClose: onClose != null ? () => onClose(TestMenu.subMenu11) : null,
+          menuChildren: <Widget>[
+            MenuItemButton(
+              onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu110) : null,
+              shortcut: shortcuts[TestMenu.subSubMenu110],
+              child: Text(TestMenu.subSubMenu110.label),
+            ),
+            MenuItemButton(
+              onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu111) : null,
+              shortcut: shortcuts[TestMenu.subSubMenu111],
+              child: Text(TestMenu.subSubMenu111.label),
+            ),
+            MenuItemButton(
+              onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu112) : null,
+              shortcut: shortcuts[TestMenu.subSubMenu112],
+              child: Text(TestMenu.subSubMenu112.label),
+            ),
+            MenuItemButton(
+              onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu113) : null,
+              shortcut: shortcuts[TestMenu.subSubMenu113],
+              child: Text(TestMenu.subSubMenu113.label),
+            ),
+          ],
+          child: Text(TestMenu.subMenu11.label),
+        ),
+        MenuItemButton(
+          onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu12) : null,
+          shortcut: shortcuts[TestMenu.subMenu12],
+          child: Text(TestMenu.subMenu12.label),
+        ),
+      ],
+      child: Text(TestMenu.mainMenu1.label),
+    ),
+    SubmenuButton(
+      onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu2) : null,
+      onClose: onClose != null ? () => onClose(TestMenu.mainMenu2) : null,
+      menuChildren: <Widget>[
+        MenuItemButton(
+          // Always disabled.
+          shortcut: shortcuts[TestMenu.subMenu20],
+          // Always disabled.
+          child: Text(TestMenu.subMenu20.label),
+        ),
+      ],
+      child: Text(TestMenu.mainMenu2.label),
+    ),
+  ];
+  return result;
+}
+
+enum TestMenu {
+  mainMenu0('Menu 0'),
+  mainMenu1('Menu 1'),
+  mainMenu2('Menu 2'),
+  subMenu00('Sub Menu 00'),
+  subMenu10('Sub Menu 10'),
+  subMenu11('Sub Menu 11'),
+  subMenu12('Sub Menu 12'),
+  subMenu20('Sub Menu 20'),
+  subSubMenu110('Sub Sub Menu 110'),
+  subSubMenu111('Sub Sub Menu 111'),
+  subSubMenu112('Sub Sub Menu 112'),
+  subSubMenu113('Sub Sub Menu 113');
+
+  const TestMenu(this.label);
+  final String label;
+}
diff --git a/packages/flutter/test/material/theme_data_test.dart b/packages/flutter/test/material/theme_data_test.dart
index 5e2e0fb..e0e4a71 100644
--- a/packages/flutter/test/material/theme_data_test.dart
+++ b/packages/flutter/test/material/theme_data_test.dart
@@ -6,62 +6,6 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
 
-@immutable
-class MyThemeExtensionA extends ThemeExtension<MyThemeExtensionA> {
-  const MyThemeExtensionA({
-    required this.color1,
-    required this.color2,
-  });
-
-  final Color? color1;
-  final Color? color2;
-
-  @override
-  MyThemeExtensionA copyWith({Color? color1, Color? color2}) {
-    return MyThemeExtensionA(
-      color1: color1 ?? this.color1,
-      color2: color2 ?? this.color2,
-    );
-  }
-
-  @override
-  MyThemeExtensionA lerp(MyThemeExtensionA? other, double t) {
-    if (other is! MyThemeExtensionA) {
-      return this;
-    }
-    return MyThemeExtensionA(
-      color1: Color.lerp(color1, other.color1, t),
-      color2: Color.lerp(color2, other.color2, t),
-    );
-  }
-}
-
-@immutable
-class MyThemeExtensionB extends ThemeExtension<MyThemeExtensionB> {
-  const MyThemeExtensionB({
-    required this.textStyle,
-  });
-
-  final TextStyle? textStyle;
-
-  @override
-  MyThemeExtensionB copyWith({Color? color, TextStyle? textStyle}) {
-    return MyThemeExtensionB(
-      textStyle: textStyle ?? this.textStyle,
-    );
-  }
-
-  @override
-  MyThemeExtensionB lerp(MyThemeExtensionB? other, double t) {
-    if (other is! MyThemeExtensionB) {
-      return this;
-    }
-    return MyThemeExtensionB(
-      textStyle: TextStyle.lerp(textStyle, other.textStyle, t),
-    );
-  }
-}
-
 void main() {
   test('Theme data control test', () {
     final ThemeData dark = ThemeData.dark();
@@ -696,6 +640,9 @@
       floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.black),
       iconButtonTheme: IconButtonThemeData(style: IconButton.styleFrom(foregroundColor: Colors.pink)),
       listTileTheme: const ListTileThemeData(),
+      menuBarTheme: const MenuBarThemeData(style: MenuStyle(backgroundColor: MaterialStatePropertyAll<Color>(Colors.black))),
+      menuButtonTheme: MenuButtonThemeData(style: MenuItemButton.styleFrom(backgroundColor: Colors.black)),
+      menuTheme: const MenuThemeData(style: MenuStyle(backgroundColor: MaterialStatePropertyAll<Color>(Colors.black))),
       navigationBarTheme: const NavigationBarThemeData(backgroundColor: Colors.black),
       navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.black),
       outlinedButtonTheme: OutlinedButtonThemeData(style: OutlinedButton.styleFrom(foregroundColor: Colors.blue)),
@@ -810,6 +757,9 @@
       floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.white),
       iconButtonTheme: const IconButtonThemeData(),
       listTileTheme: const ListTileThemeData(),
+      menuBarTheme: const MenuBarThemeData(style: MenuStyle(backgroundColor: MaterialStatePropertyAll<Color>(Colors.white))),
+      menuButtonTheme: MenuButtonThemeData(style: MenuItemButton.styleFrom(backgroundColor: Colors.black)),
+      menuTheme: const MenuThemeData(style: MenuStyle(backgroundColor: MaterialStatePropertyAll<Color>(Colors.white))),
       navigationBarTheme: const NavigationBarThemeData(backgroundColor: Colors.white),
       navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.white),
       outlinedButtonTheme: const OutlinedButtonThemeData(),
@@ -910,6 +860,9 @@
       floatingActionButtonTheme: otherTheme.floatingActionButtonTheme,
       iconButtonTheme: otherTheme.iconButtonTheme,
       listTileTheme: otherTheme.listTileTheme,
+      menuBarTheme: otherTheme.menuBarTheme,
+      menuButtonTheme: otherTheme.menuButtonTheme,
+      menuTheme: otherTheme.menuTheme,
       navigationBarTheme: otherTheme.navigationBarTheme,
       navigationRailTheme: otherTheme.navigationRailTheme,
       outlinedButtonTheme: otherTheme.outlinedButtonTheme,
@@ -1009,6 +962,9 @@
     expect(themeDataCopy.floatingActionButtonTheme, equals(otherTheme.floatingActionButtonTheme));
     expect(themeDataCopy.iconButtonTheme, equals(otherTheme.iconButtonTheme));
     expect(themeDataCopy.listTileTheme, equals(otherTheme.listTileTheme));
+    expect(themeDataCopy.menuBarTheme, equals(otherTheme.menuBarTheme));
+    expect(themeDataCopy.menuButtonTheme, equals(otherTheme.menuButtonTheme));
+    expect(themeDataCopy.menuTheme, equals(otherTheme.menuTheme));
     expect(themeDataCopy.navigationBarTheme, equals(otherTheme.navigationBarTheme));
     expect(themeDataCopy.navigationRailTheme, equals(otherTheme.navigationRailTheme));
     expect(themeDataCopy.outlinedButtonTheme, equals(otherTheme.outlinedButtonTheme));
@@ -1023,8 +979,6 @@
     expect(themeDataCopy.textSelectionTheme, equals(otherTheme.textSelectionTheme));
     expect(themeDataCopy.textSelectionTheme.selectionColor, equals(otherTheme.textSelectionTheme.selectionColor));
     expect(themeDataCopy.textSelectionTheme.cursorColor, equals(otherTheme.textSelectionTheme.cursorColor));
-    expect(themeDataCopy.textSelectionTheme.selectionColor, equals(otherTheme.textSelectionTheme.selectionColor));
-    expect(themeDataCopy.textSelectionTheme.cursorColor, equals(otherTheme.textSelectionTheme.cursorColor));
     expect(themeDataCopy.textSelectionTheme.selectionHandleColor, equals(otherTheme.textSelectionTheme.selectionHandleColor));
     expect(themeDataCopy.timePickerTheme, equals(otherTheme.timePickerTheme));
     expect(themeDataCopy.toggleButtonsTheme, equals(otherTheme.toggleButtonsTheme));
@@ -1141,10 +1095,14 @@
       'dividerTheme',
       'drawerTheme',
       'elevatedButtonTheme',
+      'expansionTileTheme',
       'filledButtonTheme',
       'floatingActionButtonTheme',
       'iconButtonTheme',
       'listTileTheme',
+      'menuBarTheme',
+      'menuButtonTheme',
+      'menuTheme',
       'navigationBarTheme',
       'navigationRailTheme',
       'outlinedButtonTheme',
@@ -1160,7 +1118,6 @@
       'timePickerTheme',
       'toggleButtonsTheme',
       'tooltipTheme',
-      'expansionTileTheme',
       // DEPRECATED (newest deprecations at the bottom)
       'accentColor',
       'accentColorBrightness',
@@ -1192,3 +1149,59 @@
     expect(propertyNames, expectedPropertyNames);
   });
 }
+
+@immutable
+class MyThemeExtensionA extends ThemeExtension<MyThemeExtensionA> {
+  const MyThemeExtensionA({
+    required this.color1,
+    required this.color2,
+  });
+
+  final Color? color1;
+  final Color? color2;
+
+  @override
+  MyThemeExtensionA copyWith({Color? color1, Color? color2}) {
+    return MyThemeExtensionA(
+      color1: color1 ?? this.color1,
+      color2: color2 ?? this.color2,
+    );
+  }
+
+  @override
+  MyThemeExtensionA lerp(MyThemeExtensionA? other, double t) {
+    if (other is! MyThemeExtensionA) {
+      return this;
+    }
+    return MyThemeExtensionA(
+      color1: Color.lerp(color1, other.color1, t),
+      color2: Color.lerp(color2, other.color2, t),
+    );
+  }
+}
+
+@immutable
+class MyThemeExtensionB extends ThemeExtension<MyThemeExtensionB> {
+  const MyThemeExtensionB({
+    required this.textStyle,
+  });
+
+  final TextStyle? textStyle;
+
+  @override
+  MyThemeExtensionB copyWith({Color? color, TextStyle? textStyle}) {
+    return MyThemeExtensionB(
+      textStyle: textStyle ?? this.textStyle,
+    );
+  }
+
+  @override
+  MyThemeExtensionB lerp(MyThemeExtensionB? other, double t) {
+    if (other is! MyThemeExtensionB) {
+      return this;
+    }
+    return MyThemeExtensionB(
+      textStyle: TextStyle.lerp(textStyle, other.textStyle, t),
+    );
+  }
+}