Skip to content

Implement RawMenuAnchor - #158255

Merged
auto-submit[bot] merged 100 commits into
flutter:masterfrom
davidhicks980:raw_menu_anchor
Feb 4, 2025
Merged

Implement RawMenuAnchor#158255
auto-submit[bot] merged 100 commits into
flutter:masterfrom
davidhicks980:raw_menu_anchor

Conversation

@davidhicks980

@davidhicks980davidhicks980 commented Nov 6, 2024

Copy link
Copy Markdown
Contributor

This PR adds a RawMenuAnchor() widget to widgets.dart. The purpose of this widget is to provide a menu primitive for the Material and Cupertino libraries (and others) to build upon. Additionally, this PR makes MenuController an inherited widget to simplify nested access to the menu (e.g. if you want to launch a context menu from a deeply-nested widget).

This PR:

  • Centralizes core menu logic to a private class, _RawMenuAnchor(),
    • Provides the internals for interacting with menus:
      • TapRegion interop
      • DismissMenuAction handler
      • Close on scroll/resize
      • Focus traversal information, if applicable
      • Subclasses override _open, _close, _isOpen, _buildAnchor, and _menuScopeNode
    • State is accessible by descendents via MenuController.maybeOf(context)._anchor
  • Adds 2 public constructors, backed by a _RawMenuAnchor() that contains shared logic.
    • RawMenuAnchor()
      • Users build the overlay from scratch.
      • Provides anchor/overlay position information and TapRegionGroupId to builder
      • Does not provide FocusScope management.
    • RawMenuAnchorGroup()
      • A primitive for menus that do not have overlays (menu bars).
      • This was previously called RawMenuAnchor.node(), but @dkwingsmt made a good case for splitting out the constructor.

Documentation examples have been added, and can be viewed at https://menu-anchor.web.app/

https://github.com/user-attachments/assets/25d35f23-2aad-4d07-9172-5c3fd65d53cf

@dkwingsmt

List which issues are fixed by this PR. #143712

Some issues that need to be addressed:

Semantics:
image
I'm basing the menu semantics off of the comment here, but I'm unsure whether the route should be given a name. There is no menubar/menu/menuitem role in Flutter, so I'm assuming the menu should be composed of nested dialogs

Unlike the menubar pattern from W3C, the RawMenuAnchor

  • does not close on tab/shift-tab. I left this behavior out of the menu so that users could customize tab behavior, but I'm not opinionated either way
  • does not open on ArrowUp/ArrowDown, because this could interfere with user focus behavior in unconventional menu setups (e.g. a vertical menu).
  • does not automatically focus the first item in a menu overlay when activated via enter/spacebar, but does focus the first item when horizontal traversal opens a submenu. Automatically focusing the first item whenever an overlay opens interferes with hover traversal, and I couldn't think of a good way to only focus the first item when an overlay is triggered via enter/spacebar.
  • doesn't focus disabled items (I wasn't sure how to address this without editing MenuItemButton)

While it is possible to nest menus -- for example, a dropdown anchor within a full-app context menu area -- nested menus behave as a single group. I was considering adding an additional parameter that separates nested root menus from their parents, and am interested to hear your feedback.

If you had to change anything in the flutter/tests repo, include a link to the migration guide as per the breaking change policy.

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@github-actionsgithub-actionsBot added the framework flutter/packages/flutter repository. See also f: labels. label Nov 6, 2024
@dkwingsmt
dkwingsmt self-requested a review November 6, 2024 19:34
@dkwingsmt

Copy link
Copy Markdown
Contributor

This PR only includes the raw_menu_anchor.dart and raw_menu_anchor_test.dart because the test suite is over 5000 LOC

Holy cow! That was quite some work! Thanks in advance (before I start to take a look)! :)

@victorsanni
victorsanni self-requested a review November 6, 2024 19:58
@github-actionsgithub-actionsBot added d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos p: material_ui material_ui package in flutter/packages labels Nov 11, 2024

@blerouxbleroux left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Impressive work! 🤩

I just reviewed the examples as a starting point.
Noticed some formatting nits and one file is missing a newline at the end of the file which makes the 'Linux analyze' CI check red.

Comment threadexamples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.0.dart Outdated
Comment threadexamples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.0.dart Outdated
Comment threadexamples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.0.dart Outdated
Comment threadexamples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.0.dart Outdated
Comment threadexamples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.3.dart Outdated
Comment threadexamples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.3.dart Outdated
Comment threadexamples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.0_test.dart Outdated
Comment threadexamples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.1_test.dart Outdated
Comment threadexamples/api/test/widgets/raw_menu_anchor/raw_menu_anchor.1_test.dart Outdated
@davidhicks980

Copy link
Copy Markdown
ContributorAuthor

Impressive work! 🤩

I just reviewed the examples as a starting point. Noticed some formatting nits and one file is missing a newline at the end of the file which makes the 'Linux analyze' CI check red.

Thanks for the fixes! I should have come back and fixed these a while ago... I appreciate you doing so.

bleroux

This comment was marked as resolved.

@bleroux

bleroux commented Nov 13, 2024

Copy link
Copy Markdown
Contributor

@davidhicks980
It seems the remaning CI failures are related to two failing tests from menu_anchor_test.dart (only two failing tests is impressive for such a huge PR).
Those failures look legit.
I extracted one of the test as a runnable code sample:

MenuBar with padding above MaterialApp
// 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';
/// Flutter code sample for a [RawMenuAnchor] that shows a simple menu with/// three items.voidmain() =>runApp(constSimpleMenuApp());
classSimpleMenuAppextendsStatelessWidget {
constSimpleMenuApp({super.key});
@overrideWidgetbuild(BuildContext context) {
returnPadding(
padding:constEdgeInsets.all(10.0),
child:MaterialApp(
theme:ThemeData(useMaterial3:false),
home:Material(
child:Column(
children:<Widget>[
Padding(
padding:constEdgeInsets.all(12.0),
child:Row(
children:<Widget>[
Expanded(
child:MenuBar(
children:createTestMenus(onPressed: (TestMenu entry) {
print('$entry pressed!');
}),
),
),
],
),
),
constExpanded(child:Placeholder()),
],
),
),
),
);
}
}
List<Widget> createTestMenus({
voidFunction(TestMenu)? onPressed,
voidFunction(TestMenu)? onOpen,
voidFunction(TestMenu)? onClose,
Map<TestMenu, MenuSerializableShortcut> shortcuts =const<TestMenu, MenuSerializableShortcut>{},
bool includeExtraGroups =false,
bool accelerators =false,
}) {
WidgetsubmenuButton(
TestMenu menu, {
requiredList<Widget> menuChildren,
}) {
returnSubmenuButton(
onOpen: onOpen !=null? () =>onOpen(menu) :null,
onClose: onClose !=null? () =>onClose(menu) :null,
menuChildren: menuChildren,
child: accelerators ?MenuAcceleratorLabel(menu.acceleratorLabel) :Text(menu.label),
);
}
WidgetmenuItemButton(
TestMenu menu, {
bool enabled =true,
Widget? leadingIcon,
Widget? trailingIcon,
Key? key,
}) {
returnMenuItemButton(
key: key,
onPressed: enabled && onPressed !=null? () =>onPressed(menu) :null,
shortcut: shortcuts[menu],
leadingIcon: leadingIcon,
trailingIcon: trailingIcon,
child: accelerators ?MenuAcceleratorLabel(menu.acceleratorLabel) :Text(menu.label),
);
}
finalList<Widget> result =<Widget>[
submenuButton(
TestMenu.mainMenu0,
menuChildren:<Widget>[
menuItemButton(TestMenu.subMenu00, leadingIcon:constIcon(Icons.add)),
menuItemButton(TestMenu.subMenu01),
menuItemButton(TestMenu.subMenu02),
],
),
submenuButton(
TestMenu.mainMenu1,
menuChildren:<Widget>[
menuItemButton(TestMenu.subMenu10),
submenuButton(
TestMenu.subMenu11,
menuChildren:<Widget>[
menuItemButton(TestMenu.subSubMenu110, key:UniqueKey()),
menuItemButton(TestMenu.subSubMenu111),
menuItemButton(TestMenu.subSubMenu112),
menuItemButton(TestMenu.subSubMenu113),
],
),
menuItemButton(TestMenu.subMenu12),
],
),
submenuButton(
TestMenu.mainMenu2,
menuChildren:<Widget>[
menuItemButton(
TestMenu.subMenu20,
leadingIcon:constIcon(Icons.ac_unit),
enabled:false,
),
],
),
if (includeExtraGroups)
submenuButton(
TestMenu.mainMenu3,
menuChildren:<Widget>[
menuItemButton(TestMenu.subMenu30, enabled:false),
],
),
if (includeExtraGroups)
submenuButton(
TestMenu.mainMenu4,
menuChildren:<Widget>[
menuItemButton(TestMenu.subMenu40, enabled:false),
menuItemButton(TestMenu.subMenu41, enabled:false),
menuItemButton(TestMenu.subMenu42, enabled:false),
],
),
submenuButton(TestMenu.mainMenu5, menuChildren:const<Widget>[]),
];
return result;
}
enumTestMenu {
mainMenu0('&Menu 0'),
mainMenu1('M&enu &1'),
mainMenu2('Me&nu 2'),
mainMenu3('Men&u 3'),
mainMenu4('Menu &4'),
mainMenu5('Menu &5 && &6 &'),
subMenu00('Sub &Menu 0&0'),
subMenu01('Sub Menu 0&1'),
subMenu02('Sub Menu 0&2'),
subMenu10('Sub Menu 1&0'),
subMenu11('Sub Menu 1&1'),
subMenu12('Sub Menu 1&2'),
subMenu20('Sub Menu 2&0'),
subMenu30('Sub Menu 3&0'),
subMenu40('Sub Menu 4&0'),
subMenu41('Sub Menu 4&1'),
subMenu42('Sub Menu 4&2'),
subSubMenu110('Sub Sub Menu 11&0'),
subSubMenu111('Sub Sub Menu 11&1'),
subSubMenu112('Sub Sub Menu 11&2'),
subSubMenu113('Sub Sub Menu 11&3'),
anchorButton('Press Me'),
outsideButton('Outside');
constTestMenu(this.acceleratorLabel);
finalString acceleratorLabel;
// Strip the accelerator markers.Stringget label =>MenuAcceleratorLabel.stripAcceleratorMarkers(acceleratorLabel);
}
BeforeAfter
imageimage

If it can save you time, I can investigate this particular issue.

A more general question, are they some features you want to implement or issues you want to fix before marking this PR as ready for review?

Comment threadpackages/flutter/lib/src/widgets/raw_menu_anchor.dart Outdated
@davidhicks980

davidhicks980 commented Nov 13, 2024 via email

Copy link
Copy Markdown
ContributorAuthor

@davidhicks980

davidhicks980 commented Nov 13, 2024 via email

Copy link
Copy Markdown
ContributorAuthor

auto-submitBot pushed a commit that referenced this pull request Nov 14, 2024
This PR adds a `MenuAnchor` test to check that `MenuAnchor.alignmentOffset` is correctly applied when `MenuAnchor.layerlink` is provided.
While reviewing #158255, I found that this new test would be useful.
@davidhicks980

davidhicks980 commented Nov 14, 2024

Copy link
Copy Markdown
ContributorAuthor

@bleroux Sorry I see that you're making changes and some of the tests I added are getting in the way. The reason why I removed the ancestor overlay parameter is because of a previous PR that moved from using the default OverlayPortal() constructor to OverlayPortal.targetsRootOverlay. Because we are targeting the root overlay, I don't think it makes sense to use the closest overlay to transform coordinates. I'm not sure if that's a correct assumption though -- should we either be transforming against the root overlay?

Also, I should note that I experienced the issue with this when creating the demo website. The sidebar shrunk the nearest overlay by 300px, so the menu was misaligned with its anchor.

@bleroux

bleroux commented Nov 14, 2024

Copy link
Copy Markdown
Contributor

@davidhicks980

Sorry I see that you're making changes and some of the tests I added are getting in the way.

No worry at all, I pushed my change because I thought that all checks would be green but later figured out that this change was intended (I have yet to understand the whole RawMenuAnchor structure).
Thanks for your message explaining the reasoning.
I will try to understand the positioning logic more deeply. Feel free to push more commits, I don't want to slow you down, just want to help this effort.
I just added a new MenuAnchor tests that might fail with this PR for the moment, see #158915.

@davidhicks980

Copy link
Copy Markdown
ContributorAuthor

@bleroux No worries! You've helped a lot, and the test you added is important. I'm still not sure how this should be handled.

@davidhicks980

davidhicks980 commented Nov 14, 2024

Copy link
Copy Markdown
ContributorAuthor

@bleroux thank you so much for the test you added. I migrated the _MenuPanel incorrectly, and it would've completely broken any menus that used LayerLink() had the test not caught the issue.

Otherwise, it looks like everything is fixed... assuming the tree-status changes. Any thoughts before submitting for review?

Edit: One potential bug I came across. A menu using LayerLink can overflow the screen without flipping. I'm not sure if this is something to address in the future, though.

@bleroux

Copy link
Copy Markdown
Contributor

Otherwise, it looks like everything is fixed... assuming the tree-status changes. Any thoughts before submitting for review?

Great! All checks are green, I think you can mark this PR as 'Ready for review'

Edit: One potential bug I came across. A menu using LayerLink can overflow the screen without flipping. I'm not sure if this is something to address in the future, though.

yes, there are issues to address with the LayerLink. DropdownMenu is the only widget to rely on it and I proposed to revert this usage in #158930.
So we can decide on either to remove it from (probably after this PR is merged) and come back to it later or keep it and try to fix it (not part of this PR, I will come back to #157921, or another solution, when RawMenuAnchor will have landed).

@davidhicks980
davidhicks980 marked this pull request as ready for review November 14, 2024 16:14
@flutter-dashboard

Copy link
Copy Markdown

Golden file changes have been found for this pull request. Click here to view and triage (e.g. because this is an intentional change).

If you are still iterating on this change and are not ready to resolve the images on the Flutter Gold dashboard, consider marking this PR as a draft pull request above. You will still be able to view image results on the dashboard, commenting will be silenced, and the check will not try to resolve itself until marked ready for review.

For more guidance, visit Writing a golden file test for package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

Changes reported for pull request #158255 at sha e7f31dd

@flutter-dashboardflutter-dashboardBot added the will affect goldens Changes to golden files label Nov 14, 2024
@davidhicks980

davidhicks980 commented Nov 14, 2024

Copy link
Copy Markdown
ContributorAuthor

Otherwise, it looks like everything is fixed... assuming the tree-status changes. Any thoughts before submitting for review?

Great! All checks are green, I think you can mark this PR as 'Ready for review'

Edit: One potential bug I came across. A menu using LayerLink can overflow the screen without flipping. I'm not sure if this is something to address in the future, though.

yes, there are issues to address with the LayerLink. DropdownMenu is the only widget to rely on it and I proposed to revert this usage in #158930. So we can decide on either to remove it from (probably after this PR is merged) and come back to it later or keep it and try to fix it (not part of this PR, I will come back to #157921, or another solution, when RawMenuAnchor will have landed).

Very interesting -- I'm going to follow your work on this. I've been meaning to dig into the follower/target because it avoids the single-frame lag that MenuAnchor experiences when moved around the screen, and may be a better candidate for implementing nested animated menus (see #137936 for an example -- unfortunately this implementation uses Flow instead of overlays). Anyways, I appreciate the help!

@flutter-dashboard

This comment was marked as outdated.

@flutter-dashboard

Copy link
Copy Markdown

Golden file changes are available for triage from new commit, Click here to view.

For more guidance, visit Writing a golden file test for package:flutter.

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

Changes reported for pull request #158255 at sha 87c9392

engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 6, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 6, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 6, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 6, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 7, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 7, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 7, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 8, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 8, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 9, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 9, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 10, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 10, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 10, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 10, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 10, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 11, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 11, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 11, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 11, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Feb 11, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request May 20, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request May 20, 2025
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request May 21, 2025
chunhtai pushed a commit to chunhtai/packages that referenced this pull request Jun 12, 2026
This PR adds a `MenuAnchor` test to check that `MenuAnchor.alignmentOffset` is correctly applied when `MenuAnchor.layerlink` is provided.
While reviewing flutter/flutter#158255, I found that this new test would be useful.
chunhtai pushed a commit to chunhtai/packages that referenced this pull request Jun 12, 2026
## Description
This PR removes `DropdownMenu` usage of Leader/Follower.
Leader/Follower positioning was introduced in
flutter/flutter#154667 which uses
Leader/Follower approach to fix some weird positioning issues (such as
flutter/flutter#149037).
Unfortunately, it also introduces some regressions, see:
- flutter/flutter#157916
- flutter/flutter#158924
Because flutter/flutter#154667 is already
included in the beta channel, cherry-picking this PR should be
considered.
## Context
This PR is not a full revert and keeps Leader/Follower usage in
`MenuAnchor` because this usage is optional and doesn't cause any
regression.
There are some ongoing work which might fix or mitigate this problem:
- flutter/flutter#157921
- flutter/flutter#158255
## Related Issue
Fixesflutter/flutter#157916Fixesflutter/flutter#158924
Reopens flutter/flutter#123395
Reopens flutter/flutter#149037
Reopens flutter/flutter#151856
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

d: api docsIssues with https://api.flutter.dev/d: examplesSample code and demosframeworkflutter/packages/flutter repository. See also f: labels.p: material_uimaterial_ui package in flutter/packageswill affect goldensChanges to golden files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@davidhicks980@dkwingsmt@bleroux@justinmc@gspencergoog@nate-thegrate@chunhtai