Skip to content

[web] Scope the mobile semantics placeholder to its view - #190856

Merged
auto-submit[bot] merged 11 commits into
flutter:masterfrom
flutter-zl:issue_152838_fix
Sep 21, 2026
Merged

auto-submit[bot] merged 11 commits into
flutter:masterfrom
flutter-zl:issue_152838_fix

Conversation

@flutter-zl

Copy link
Copy Markdown
Contributor

Fixes #152838

Problem

Flutter Web puts an invisible "Enable accessibility" button on the page for screen reader users. On mobile it covered the whole page instead of just the Flutter app, swallowing taps meant for the surrounding HTML until about 20 taps made the engine remove it.

Desktop was never affected: its button is one pixel and sits offscreen.

Fix

The button now lives inside <flutter-view> rather than <body>, one per view, so it can only cover the Flutter app. The CSS is unchanged, only the parent element, and it goes in as the first child so platform views keep their own events.

Demo

Before: https://flutter-demo-59-before.web.app
After: https://flutter-demo-59-after.web.app

Open either on a phone and tap the HTML links before touching the Flutter app.

  • Before: nothing responds. The links do not highlight and the log stays at "no HTML link clicked yet". They come alive only after tapping the Flutter app about 20 times.
  • After: the links respond on the first tap.

Desktop browsers cannot show the difference, since they use the 1x1 placeholder on both builds.

@github-actions github-actions Bot added engine flutter/engine related. See also e: labels. a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) platform-web Web applications specifically team-web Owned by Web platform team labels Aug 10, 2026
@flutter-zl
flutter-zl marked this pull request as ready for review August 10, 2026 19:03
@flutter-zl flutter-zl added r: duplicate Issue is closed as a duplicate of an existing issue CICD Run CI/CD labels Aug 10, 2026

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request refactors accessibility placeholder management in the Flutter Web engine to support multi-view applications. On desktop, a single page-level placeholder is shared across views and attached to the document body, while on mobile, individual placeholders are attached to each view root. Feedback identifies two issues: first, addPlaceholder() is evaluated in DesktopSemanticsEnabler even if domDocument.body is null, which incorrectly tracks a placeholder; second, semanticsActivationTimer in MobileSemanticsEnabler should be explicitly cancelled during disposal to prevent post-dispose callbacks.

@flutter-zl
flutter-zl requested review from chunhtai and mdebbar August 11, 2026 21:57
///
/// This is used when semantics is enabled programmatically and therefore the
/// placehodler is no longer needed.
/// placeholder is no longer needed.

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.

Suggested change
/// placeholder is no longer needed.
/// placeholders are no longer needed.

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.

Done.

@@ -131,12 +178,46 @@ abstract class SemanticsEnabler {
/// and enables semantics.
@visibleForTesting
class DesktopSemanticsEnabler extends SemanticsEnabler {

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.

not related to this pr, but none of the method in this class should call its own dispose(). Only the owner that initalize a instance of this class should call dispose.

A instance shouldn't self destroy as a side effect of calling other method.

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.

If the intention is to clear out placeholder list, refactor it to a separate clean up method.

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.

Agreed. Done.

///
/// See https://github.com/flutter/flutter/issues/152838
@override
void addPlaceholderForView(DomElement viewRoot) {

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.

is there a reason desktop and mobile can't share the same placeholder insert and remove logic?

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.

@flutter-zl so on desktop, we always have a single placeholder that covers the entire screen? That may not be ideal for multi-view mode where the screen contains other non-Flutter content.

Does it make sense to match the mobile behavior (have a placeholder for each view)?

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 suggestion and it's shared now.

@mdebbar on coverage: the desktop placeholder doesn't cover anything. The two form factors use opposite CSS.

 // mobile: fills its containing block
 ..left = '0'  ..top = '0'  ..right = '0'  ..bottom = '0';
 // desktop: 1x1, offscreen
 ..left = '-1px'  ..top = '-1px'  ..width = '1px'  ..height = '1px';

/// For Webkit browser engine the placeholders are removed upon the next
/// "touchend" event. This is to prevent Safari from swallowing the event
/// that happens on an element that's being removed. Blink doesn't have
/// this issue.

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.

does it work if we handle it the same way for Blink? that way we don't need to maintain two behavior

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.

This PR doesn't change that behavior. The only edits in this block are pluralizing "placeholder is removed" to "placeholders are removed".

void dispose() {
_semanticsPlaceholder?.remove();
_semanticsPlaceholder = null;
super.dispose();

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.

dispose should be the last,

dispose should be the reverse order of init

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.

Done.

/// Only touches [placeholders]. The caller attaches the element and keeps
/// its own bookkeeping in sync, so this is for [addPlaceholderForView]
/// implementations rather than outside callers.
DomElement addPlaceholder() {

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.

If this is only meant for internal use (as the comment says), let's make it private. If it's needed only by tests, then let's add @visibleForTesting.

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.

addPlaceholder() is gone.

///
/// See https://github.com/flutter/flutter/issues/152838
@override
void addPlaceholderForView(DomElement viewRoot) {

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.

@flutter-zl so on desktop, we always have a single placeholder that covers the entire screen? That may not be ideal for multi-view mode where the screen contains other non-Flutter content.

Does it make sense to match the mobile behavior (have a placeholder for each view)?

@mdebbar mdebbar 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.

LGTM

@flutter-zl flutter-zl added the autosubmit Merge PR when tree becomes green via auto submit App label Sep 21, 2026
@auto-submit auto-submit Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Sep 21, 2026
@auto-submit

auto-submit Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/flutter/190856, because The base commit of the PR is older than 7 days and can not be merged. Please merge the latest changes from the main into this branch and resubmit the PR.

@flutter-zl flutter-zl added the autosubmit Merge PR when tree becomes green via auto submit App label Sep 21, 2026
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. platform-web Web applications specifically r: duplicate Issue is closed as a duplicate of an existing issue team-web Owned by Web platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Web][Mobile browsers] HTML link is not clickable until interacting with the embedded Flutter app

3 participants