import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'dart:async';
/// Flutter code sample for a [RawMenuAnchor] that animates a nested menu using
/// [RawMenuAnchor.onOpenRequested] and [RawMenuAnchor.onCloseRequested].
void main() {
runApp(const RawMenuAnchorSubmenuAnimationApp());
}
/// Signature for the function that builds a [Menu]'s contents.
typedef MenuPanelBuilder =
Widget Function(BuildContext context);
/// Signature for the function that builds a [Menu]'s anchor button.
///
/// The [MenuController] can be used to open and close the menu.
typedef MenuButtonBuilder =
Widget Function(
BuildContext context,
MenuController controller,
);
class RawMenuAnchorSubmenuAnimationExample extends StatelessWidget {
const RawMenuAnchorSubmenuAnimationExample({super.key});
@override
Widget build(BuildContext context) {
return Menu(
panelBuilder: (BuildContext context) {
final MenuController rootMenuController = MenuController.maybeOf(
context,
)!;
return Align(
alignment: .topRight,
child: Column(
children: <Widget>[
for (int i = 0; i < 4; i++)
Menu(
panelBuilder: (BuildContext context) {
return SizedBox.square(
dimension: 120,
child: Center(
child: Text(
'Panel',
textAlign: .center,
),
),
);
},
buttonBuilder:
(
BuildContext context,
MenuController controller,
) {
return MenuItemButton(
requestFocusOnHover: true,
onFocusChange: (bool focused) {
if (focused) {
controller.open();
}
},
onPressed: () {
if (!controller.isOpen) {
rootMenuController.closeChildren();
controller.open();
} else {
controller.close();
}
},
trailingIcon: const Icon(Icons.arrow_forward),
child: Text('Submenu $i'),
);
},
),
],
),
);
},
buttonBuilder:
(
BuildContext context,
MenuController controller,
) {
return FilledButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Text('Menu'),
);
},
);
}
}
class Menu extends StatefulWidget {
const Menu({
super.key,
required this.panelBuilder,
required this.buttonBuilder,
});
final MenuPanelBuilder panelBuilder;
final MenuButtonBuilder buttonBuilder;
@override
State<Menu> createState() => MenuState();
}
class MenuState extends State<Menu> with SingleTickerProviderStateMixin {
final MenuController menuController = MenuController();
bool get isSubmenu => MenuController.maybeOf(context) != null;
Timer? openTimer;
void _handleMenuOpenRequest(Offset? position, VoidCallback showOverlay) {
MenuController.maybeOf(context)?.closeChildren();
openTimer?.cancel();
openTimer = Timer(Duration(milliseconds: 500), () {
openTimer = null;
showOverlay();
});
}
void _handleMenuCloseRequest(VoidCallback hideOverlay) {
openTimer?.cancel();
openTimer = null;
hideOverlay();
}
@override
Widget build(BuildContext context) {
return Semantics(
role: .menu,
child: RawMenuAnchor(
controller: menuController,
onOpenRequested: _handleMenuOpenRequest,
onCloseRequested: _handleMenuCloseRequest,
overlayBuilder: (BuildContext context, RawMenuOverlayInfo info) {
final ui.Offset position = isSubmenu
? info.anchorRect.topRight
: info.anchorRect.bottomLeft;
return Positioned(
top: position.dy,
left: position.dx,
child: Semantics(
explicitChildNodes: true,
scopesRoute: true,
// Remove focus while the menu is closing.
child: TapRegion(
groupId: info.tapRegionGroupId,
onTapOutside: (PointerDownEvent event) {
menuController.close();
},
child: Container(
color: Colors.white,
padding: EdgeInsets.all(10),
child: widget.panelBuilder(context)),
),
),
);
},
builder:
(BuildContext context, MenuController controller, Widget? child) {
return widget.buttonBuilder(context, controller);
},
),
);
}
}
class RawMenuAnchorSubmenuAnimationApp extends StatelessWidget {
const RawMenuAnchorSubmenuAnimationApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const Scaffold(
body: Center(child: RawMenuAnchorSubmenuAnimationExample()),
),
);
}
}
Steps to reproduce
Expected results
Moving between menu items should reset the delay.
Actual results
Moving between menu items triggers a new delay regardless of whether the previous delay has finished.
Code sample
Code sample
Screenshots or Video
Screenshots / Video demonstration
Broken version (Main branch)
D539CF51-7049-4D74-A6E2-C5E6A0894702.mov
Correct version (Stable branch)
Screen.Recording.2026-05-11.at.6.24.59.PM.mov
Logs
Logs
N/AFlutter Doctor output
Doctor output
Dart SDK 3.11.5 and Flutter SDK 3.41.9 (Dartpad)