Skip to content

[web] Fix MenuAnchor dismiss when semantics enabled#183093

Merged
auto-submit[bot] merged 23 commits into
flutter:masterfrom
flutter-zl:issue_167487
May 12, 2026
Merged

[web] Fix MenuAnchor dismiss when semantics enabled#183093
auto-submit[bot] merged 23 commits into
flutter:masterfrom
flutter-zl:issue_167487

Conversation

@flutter-zl
Copy link
Copy Markdown
Contributor

@flutter-zl flutter-zl commented Mar 1, 2026

Fixes #167487

Problem
When semantics are enabled, MenuAnchor does not close when clicking outside it. ClickDebouncer was converting all debounced clicks to SemanticsAction.tap, which bypasses the pointer event pipeline. TapRegionSurface never saw a PointerDownEvent, so onTapOutside was never called and the menu stayed open.

Fix
TapRegionSurface now registers a listener with SemanticsBinding.addSemanticsActionListener to intercept SemanticsAction.tap and SemanticsAction.longPress events. On each action, it hit-tests the render tree at the tapped node's center and calls onTapOutside/onTapInside directly. The inside/outside classification is extracted into a shared _classifyRegions method used by both the pointer and semantics paths.

ClickDebouncer.onClick also calls stopPropagation() before flushing pointer events for the isListening: false case, preventing a duplicate browser click.

Note: consumeOutsideTaps cannot suppress semantics action propagation today. A follow-up API change similar to WidgetsBindingObserver.handleStartBackGesture would be needed. See the TODO in _handleSemanticsAction.

Demo

Before change
https://flutter-demo-14-before.web.app

After change
https://flutter-demo-14-after.web.app

Steps: click Open Menu → click Button A outside the menu.
Before: button fires but menu stays open.
After: button fires and menu closes.

@github-actions github-actions Bot added framework flutter/packages/flutter repository. See also f: labels. engine flutter/engine related. See also e: labels. a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) platform-web Web applications specifically labels Mar 1, 2026
@flutter-zl flutter-zl marked this pull request as ready for review March 4, 2026 06:37
@flutter-zl flutter-zl requested review from chunhtai and mdebbar March 4, 2026 06:37
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request addresses an issue where MenuAnchor does not dismiss on an outside click when semantics are enabled. A new mergesDescendants flag is introduced and propagated through the semantics system. This flag is used in ClickDebouncer to conditionally flush pointer events for non-merged nodes, which allows TapRegionSurface to detect outside taps. For merged nodes, SemanticsAction.tap is still used to preserve existing behavior. The changes also include updates to tests to cover the new logic. My feedback is focused on improving documentation for the newly introduced parameters.

Comment thread engine/src/flutter/lib/ui/semantics.dart Outdated
Comment thread engine/src/flutter/lib/web_ui/lib/semantics.dart Outdated
Comment thread engine/src/flutter/lib/web_ui/lib/src/engine/pointer_binding.dart Outdated
Copy link
Copy Markdown
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

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

Can you explain a bit more about the exact repro setup?

is it because the there is an area that it should hit inside a big semantics node, but instead swallowed by the semantics tap? how does the rect looks like in this cases?

// There's a pending queue of pointer events. Prefer sending the tap action
// instead of pointer events, because the pointer events may not land on the
// combined semantic node and miss the click/tap.
if (isMergedNode && isListening) {
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.

merge node may not be the only cases this can be true,

you can have a gesture detector under a Semantics(container: true) which has smaller rect but merge up to the bigger container

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. I broadened the flag from mergesDescendants to absorbedChildSemantics. Instead of only checking _mergeAllDescendantsIntoThisNode, the framework now sets absorbedChildSemantics = true whenever absorbAll encounters a child SemanticsConfiguration that hasBeenAnnotated and has an onTap handler. This covers both MergeSemantics and Semantics(container: true) cases where a smaller tappable child's tap action gets absorbed into a larger container node, causing a rect mismatch.

In the engine, ClickDebouncer.onClick now checks absorbedChildSemantics instead of isMergedNode. When true, it sends SemanticsAction.tap to avoid coordinate miss-hits. When false, it flushes pointer events so TapRegionSurface can detect outside taps and close MenuAnchor.

@flutter-zl flutter-zl added the CICD Run CI/CD label Mar 13, 2026
@flutter-zl
Copy link
Copy Markdown
Contributor Author

Can you explain a bit more about the exact repro setup?

is it because the there is an area that it should hit inside a big semantics node, but instead swallowed by the semantics tap? how does the rect looks like in this cases?

The repro setup is the demo app linked in the PR description. A MenuAnchor opens a dropdown menu, and below it are standalone ElevatedButtons. With semantics enabled, each button gets a semantics DOM element overlaid on top of the canvas. When the user clicks a button while the menu is open, the browser fires a click event on the semantics element. ClickDebouncer intercepts it. Previously, it always converted this into SemanticsAction.tap, which goes directly to the button's tap handler, bypassing the pointer event pipeline entirely. Since TapRegionSurface never sees a PointerDownEvent, onTapOutside is never called, and the menu stays open.

@flutter-zl flutter-zl requested a review from chunhtai March 14, 2026 01:02
@mdebbar
Copy link
Copy Markdown
Contributor

mdebbar commented Mar 16, 2026

@chunhtai should this be considered a bug in TapRegionSurface if it's not detecting semantics taps outside a region? I think it should detect normal taps and semantics taps.

@chunhtai
Copy link
Copy Markdown
Contributor

Thanks for the info, yeah I agree this is something that probably should be fixed in TapRegionSurface. This will be tricky to implement though since semantics action and gesture rect are two separate channel. there isn't a good way to hook TapRegionSurface to the semantics action without actually participate in the semantics tree.

@github-actions github-actions Bot removed the CICD Run CI/CD label Mar 17, 2026
@flutter-zl
Copy link
Copy Markdown
Contributor Author

flutter-zl commented Mar 24, 2026

Thanks for the info, yeah I agree this is something that probably should be fixed in TapRegionSurface. This will be tricky to implement though since semantics action and gesture rect are two separate channel. there isn't a good way to hook TapRegionSurface to the semantics action without actually participate in the semantics tree.

TapRegionSurface only knows about PointerDownEvent and PointerUpEvent. It does not know about SemanticsAction, SemanticsActionEvent, or anything from the semantics system. There is no import, no callback, no listener for semantics actions anywhere in this file. @mdebbar what do you think?

@mdebbar
Copy link
Copy Markdown
Contributor

mdebbar commented Apr 2, 2026

TapRegionSurface only knows about PointerDownEvent and PointerUpEvent. It does not know about SemanticsAction, SemanticsActionEvent, or anything from the semantics system. There is no import, no callback, no listener for semantics actions anywhere in this file.

Yeah, that's the bug 🙂 TapRegion should know about semantics and should take SemanticsAction.tap into account.

As a first step, TapRegion can listen to all semantics actions using:

SemanticsBinding.instance.addSemanticsActionListener(...);

Then, it needs to decide if the received tap was inside or outside. This is the part I'm not sure how to implement. Any ideas @chunhtai ?

I'm assuming we can't use Semantics(onTap: ...) since that would absorb the tap; and TapRegion is designed to passively listen to taps without affecting the rest of the system.

@chunhtai
Copy link
Copy Markdown
Contributor

chunhtai commented Apr 2, 2026

The only way i can think of is to use SemanticsBinding.instance.addSemanticsActionListener(...) and then see if the semanticnode id is a node under the tapRegion's semantics subtree. We will need to do this in RenderTapRegionSurface

@github-actions github-actions Bot removed the a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) label Apr 6, 2026
@flutter-zl flutter-zl requested a review from chunhtai May 11, 2026 20:56
chunhtai
chunhtai previously approved these changes May 11, 2026
Copy link
Copy Markdown
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

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

LGTM

/// the view has no semantics owner, or the node cannot be found. Callers
/// should only invoke this in response to a semantics action, in which case
/// all three lookups are expected to succeed.
ui.Rect? semanticsNodeGlobalRect(int viewId, int nodeId) => null;
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.

nit: rename this getRectOfSemanticsNodeInViewCoordinates.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated.

@github-actions github-actions Bot removed the CICD Run CI/CD label May 11, 2026
@flutter-zl flutter-zl added CICD Run CI/CD autosubmit Merge PR when tree becomes green via auto submit App labels May 11, 2026
@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label May 11, 2026
@auto-submit
Copy link
Copy Markdown
Contributor

auto-submit Bot commented May 11, 2026

autosubmit label was removed for flutter/flutter/183093, because This PR has not met approval requirements for merging. Changes were requested by {mdebbar}, please make the needed changes and resubmit this PR.
The PR author is a member of flutter-hackers and needs 1 more review(s) in order to merge this PR.

  • Merge guidelines: A PR needs at least one approved review if the author is already part of flutter-hackers or two member reviews if the author is not a member of flutter-hackers before re-applying the autosubmit label. Reviewers: If you left a comment approving, please use the "approve" review action instead.

@github-actions github-actions Bot removed the CICD Run CI/CD label May 12, 2026
@flutter-zl flutter-zl removed the request for review from mdebbar May 12, 2026 04:47
@flutter-zl flutter-zl added the CICD Run CI/CD label May 12, 2026
@flutter-zl flutter-zl dismissed mdebbar’s stale review May 12, 2026 04:51

Previous code change is outdated.

@flutter-zl flutter-zl requested a review from chunhtai May 12, 2026 16:35
Copy link
Copy Markdown
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

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

LGTM

@flutter-zl flutter-zl added the autosubmit Merge PR when tree becomes green via auto submit App label May 12, 2026
@auto-submit auto-submit Bot added this pull request to the merge queue May 12, 2026
Merged via the queue into flutter:master with commit f77fa02 May 12, 2026
199 of 200 checks passed
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label May 12, 2026
auto-submit Bot pushed a commit to flutter/packages that referenced this pull request May 15, 2026
…11713)

Manual roll requested by bmparr@google.com

flutter/flutter@23f6f58...0541913

2026-05-14 98614782+auto-submit[bot]@users.noreply.github.com Reverts "[Windows] Propagate the enabled accessibility state (#184501)" (flutter/flutter#186492)
2026-05-13 srawlins@google.com [dev] Use super parameters in missed spots (flutter/flutter#186193)
2026-05-13 loic.peron@inetum.com [Windows] Propagate the enabled accessibility state (flutter/flutter#184501)
2026-05-13 matt.boetger@gmail.com [flutter_tool] filter out MotionEvent-JNI warning spam from logcat (#174783) (flutter/flutter#186079)
2026-05-13 engine-flutter-autoroll@skia.org Roll Packages from 93cbed6 to 2ec2236 (1 revision) (flutter/flutter#186464)
2026-05-13 mdebbar@google.com [web] Fix untriaged issues link label (flutter/flutter#186465)
2026-05-13 bdero@google.com [Impeller] Namespace user-supplied shaders to prevent entrypoint collisions (flutter/flutter#186332)
2026-05-13 1063596+reidbaker@users.noreply.github.com [flutter_tools] Migrate detectLowCompileSdkVersionOrNdkVersion to AGP task (flutter/flutter#184731)
2026-05-13 jason-simmons@users.noreply.github.com Update the Flutter Gallery web app template files to support running with Wasm (flutter/flutter#186268)
2026-05-13 jason-simmons@users.noreply.github.com [web] Use heap allocation for buffers that would consume too much space on the Wasm stack (flutter/flutter#186228)
2026-05-13 engine-flutter-autoroll@skia.org Roll Skia from 56ca5896c0d9 to 27f7bba22600 (3 revisions) (flutter/flutter#186444)
2026-05-13 engine-flutter-autoroll@skia.org Roll Fuchsia Linux SDK from z7ICmPtn4hspu02zk... to y6uQHA5xUN83IF395... (flutter/flutter#186442)
2026-05-13 engine-flutter-autoroll@skia.org Roll Skia from 6385958d2feb to 56ca5896c0d9 (1 revision) (flutter/flutter#186441)
2026-05-13 engine-flutter-autoroll@skia.org Roll Dart SDK from 9576691c37d8 to 8e30b88e4d5a (1 revision) (flutter/flutter#186429)
2026-05-13 engine-flutter-autoroll@skia.org Roll Skia from 77a21bc723dc to 6385958d2feb (9 revisions) (flutter/flutter#186428)
2026-05-13 164032450+AlexEduV@users.noreply.github.com Docs/improving docs for semantics UI lib (flutter/flutter#186125)
2026-05-12 jason-simmons@users.noreply.github.com [Tool] Support glob patterns when parsing workspaces in FlutterProject (flutter/flutter#185715)
2026-05-12 nico.reiab@gmail.com docs: fix overriden -> overridden in MediaQueryData dartdoc (flutter/flutter#186323)
2026-05-12 brackenavaron@gmail.com [Test cross imports] No material in `test/foundation`, `test/gestures`, `test/semantics`, `test/services` (flutter/flutter#186144)
2026-05-12 nico.reiab@gmail.com docs: fix "tha" -> "that" typo in widget_inspector_test comment (flutter/flutter#186322)
2026-05-12 nico.reiab@gmail.com docs: Fix doubled-word typos in framework dartdoc (flutter/flutter#186319)
2026-05-12 137456488+flutter-pub-roller-bot@users.noreply.github.com Roll pub packages (flutter/flutter#186418)
2026-05-12 30870216+gaaclarke@users.noreply.github.com Bumped required mediatek vender sdk version. (flutter/flutter#186405)
2026-05-12 magder@google.com Make DeepLinkJsonFromManifestTask Gradle task build cacheable (flutter/flutter#185903)
2026-05-12 66727653+ishaq2321@users.noreply.github.com Harden dev tooling scripts against command injection and log leaks (flutter/flutter#186076)
2026-05-12 137456488+flutter-pub-roller-bot@users.noreply.github.com Roll pub packages (flutter/flutter#186274)
2026-05-12 bdero@google.com [Flutter GPU] Allow allocating multi-mip textures and overwriting specific (mip, slice) levels (flutter/flutter#185890)
2026-05-12 zhongliu88889@gmail.com [web] Fix MenuAnchor dismiss when semantics enabled (flutter/flutter#183093)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC bmparr@google.com,stuartmorgan@google.com on the revert to ensure that a human
is aware of the problem.

To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) CICD Run CI/CD engine flutter/engine related. See also e: labels. framework flutter/packages/flutter repository. See also f: labels. platform-web Web applications specifically

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MenuAnchor doesn’t close when clicking on tappable widgets when SemanticsBinding.instance.ensureSemantics(); is enabled

3 participants