Repository navigation
[web] Autofill all AutofillGroup fields in semantics mode - #187108
auto-submit[bot] merged 26 commits into
Conversation
56b1674 to
85930ea
Compare
There was a problem hiding this comment.
Code Review
This pull request updates the web text editing semantics to support grouped autofill without regressing accessibility tab traversal. It links the focused input element to the autofill form using the HTML form attribute instead of moving it into the form, and demotes it to a synthetic placeholder on blur. Review feedback suggests using WebKit engine checks instead of Safari-only checks to support other WebKit-based browsers on iOS, avoiding force-unwraps on potentially null configurations, and retrieving the element's name attribute dynamically to prevent runtime casting errors with multiline text areas.
| /// the Safari/iOS strategy), so the strategy type alone misses Safari there. | ||
| /// Fall back to a browser check so synthetic fields are not zero-sized on | ||
| /// Safari/WebKit, which would defeat autofill. | ||
| bool get _treatAsSafariForAutofill => _isSafariStrategy || ui_web.browser.isSafari; |
There was a problem hiding this comment.
Similar to the WebKit check in text_field.dart, using ui_web.browser.isSafari here misses other WebKit-based browsers (like Chrome/Firefox on iOS) when semantics mode is active. Consider checking if the browser engine is WebKit to ensure synthetic fields are not zero-sized on any WebKit-based browser, which would defeat autofill.
| bool get _treatAsSafariForAutofill => _isSafariStrategy || ui_web.browser.isSafari; | |
| bool get _treatAsSafariForAutofill => | |
| _isSafariStrategy || ui_web.browser.browserEngine == ui_web.BrowserEngine.webkit; |
| /// reliable, order-independent signal that [_updateInputType] must not | ||
| /// overwrite `autocomplete`, otherwise grouped autofill silently breaks on | ||
| /// the next semantics update (flutter/flutter#180652). | ||
| bool get _isAutofillOwned => (editableElement as DomHTMLInputElement).name?.isNotEmpty ?? false; |
There was a problem hiding this comment.
Casting editableElement directly to DomHTMLInputElement can lead to runtime type errors if the field is a multiline text field (which uses DomHTMLTextAreaElement). Since both input and textarea elements support the name attribute, you can safely use editableElement.getAttribute('name') to avoid any unsafe casting.
| bool get _isAutofillOwned => (editableElement as DomHTMLInputElement).name?.isNotEmpty ?? false; | |
| bool get _isAutofillOwned => editableElement.getAttribute('name')?.isNotEmpty ?? false; |
|
We already have a semantics form: https://github.com/flutter/flutter/blob/master/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/form.dart I'm curious why that's not working as expected? |
That's the a11y form, only created for the framework's Form widget for screen-reader announcement; AutofillGroup doesn't emit it and its inputs have no name/autocomplete, so the password manager won't fill it. Browser autofill goes through EngineAutofillForm in the text-editing layer (the shared path that also works with semantics off), and since the semantics-mode case was never implemented (per yjbanov's comment), this PR wires it up by linking the focused semantics to that form via the form= attribute. |
That might be exactly where the problem lies. The autofill setup in I’d much prefer we make the simpler semantics forms work rather than dragging the text-editing autofill complexity into semantics mode. @chunhtai, is it reasonable to have |
I felt this is a web only thing that it needs to use a form to enable the autofill. so I am more toward keeping the logic private to web engine. |
mdebbar
left a comment
There was a problem hiding this comment.
@chunhtai do you think it makes sense (in the future, not now) to have all autofill information available in the semantics tree? I'm thinking about a world where semantics is enabled by default, having autofill info in the semantics tree will simplify a lot of things.
| formElement = _createFormElementAndFields( | ||
| focusedElement, | ||
| focusedAutofill, | ||
| associateFocusedByAttribute: associateFocusedByAttribute, |
There was a problem hiding this comment.
I want to check if my understanding is correct: in semantics mode, we create the <form> and all text fields inside it, and then we delete the "focused" one because we want to use the semantics field.
If my understanding is correct, imagine a simple form of username and password. In the semantics tree, we create a username field and a password field. Then the user focuses on the username field, so we create the form, and we create an extra password field inside it. Now we have one username field and two password fields. Is this the case?
I think in semantics mode, the _createFormElementAndFields() method should only create the form, no fields, because the fields are already created in the semantics tree.
There was a problem hiding this comment.
- It's "skip," not "create then delete", the focused field is never synthesized:
if (field... == focusedAutofill.uniqueIdentifier) {
if (associateFocusedByAttribute) {
continue; // focused field stays in <flt-semantics>, linked via form=
}
htmlElement = focusedElement;
} else {
// synthetic for non-focused fields
}
-
So with username focused: username = one element (the real semantics input, linked by form=, not
duplicated); password = the idle semantics input + one synthetic in the form, but only the synthetic is in
the form. -
The form can't be empty, though: the semantics password isn't inside the form, so the browser won't group it. The synthetic is its in-form stand-in and value relay (browser fills it → listener forwards to the framework → AutofillScope writes it to the real field). Same path semantics-off uses. And the focused field stays in its semantics node because moving it into the form broke a11y tab order (Fix a11y tab traversal flutter-team-archive/engine#25797).
There was a problem hiding this comment.
It's "skip," not "create then delete", the focused field is never synthesized
Ah you are right. I was confused by the synthetic.remove(); line. Can we delete it? Maybe replace it with an assert to make sure no synthetic element was created for the focused ID.
The form can't be empty, though
The form is never empty. There's always at least a submitBtn in it.
the semantics password isn't inside the form, so the browser won't group it.
Can we do the same form= attribute for non-focused fields too?
There was a problem hiding this comment.
-
synthetic.remove()is only for waking a dormant form where a previous blur left a placeholder, not for deleting something created during the initial form build. The first-build case is already enforced by thecontinuein_createFormElementAndFields: the focused field is not synthesized when the form is first created in semantics mode. -
Agreed that “empty form” was imprecise. The form always has
submitBtn. -
For non-focused fields, this code path cannot use
form=today.EngineAutofillFormonly receives the focused semantics DOM element. For the other fields it only hasAutofillInfo, not the realSemanticTextField.editableElement, so it cannot attachform=to them. The synthetic controls are also the relay path for browser autofill values viaTextInputClient.updateEditingStateWithTag.
There was a problem hiding this comment.
Non-focused fields can use SemanticsTextEditingStrategy.instance.inputConfiguration.autofillGroup!.formDomId to get the ID for form=. I don't know what's the best place to do this, but maybe in the update() method?
There was a problem hiding this comment.
Right.formDomId is reachable from update().
The blocker is that form= alone only associates the element with the form. It does not tell the browser or Flutter which autofill field this semantics input represents.
For example, if we only add form= to the password input, we get:
<input form="login-form">But for browser autofill and framework value propagation, the real field would also need the matching AutofillInfo metadata:
<input
form="login-form"
name="current-password"
autocomplete="current-password">Today that DOM-element-to-AutofillInfo mapping only exists for the focused text input client. For non-focused fields, the synthetic inputs are created directly from the group's AutofillInfo, so they have the correct name, autocomplete, and tag for TextInputClient.updateEditingStateWithTag.
So I think keeping the synthetic non-focused fields is safer for this PR.
If I understand correctly, the pr grabs the autofill info from textinput client and use that to add form to the semantics dom. You suggest that we should have this autofill info in the semantics tree directly right? yeah i think that makes sense. It also inline with my suggestion to separate out coupling of textfield semantics and textinputclient so that it makes things easier to implement a custom textfield. cc @Renzo-Olivares |
| elements.addAll(existingForm.elements); | ||
| } else { | ||
| formElement = _createFormElementAndFields(focusedElement, focusedAutofill); | ||
| formElement = _createFormElementAndFields( |
There was a problem hiding this comment.
What if someone call wakeup with associateFocusedByAttribute: false and then call it with associateFocusedByAttribute: true?
It feels like associateFocusedByAttribute should be a constructor parameter of EngineAutofillForm so that we don't need to worry about the mismatch
There was a problem hiding this comment.
Great suggestion. Updated.
|
autosubmit label was removed for flutter/flutter/187108, because - The status or check suite Dashboard Checks has failed. Please fix the issues identified (or deflake) before re-applying this label. |
|
autosubmit label was removed for flutter/flutter/187108, because - The status or check suite Dashboard Checks has failed. Please fix the issues identified (or deflake) before re-applying this label. |
|
autosubmit label was removed for flutter/flutter/187108, because - The status or check suite Dashboard Checks has failed. Please fix the issues identified (or deflake) before re-applying this label. |
…12401) Manual roll Flutter from 2757a77a73df to e52f01c920ad (47 revisions) Manual roll requested by [email protected] flutter/flutter@2757a77...e52f01c 2026-07-31 [email protected] [Impeller] Execute PipelineCompileQueue jobs in the order that they were added (flutter/flutter#190262) 2026-07-31 [email protected] Roll Packages from 7d17fc8 to 5351d8c (5 revisions) (flutter/flutter#190351) 2026-07-31 [email protected] Roll Skia from dcbd015c9646 to 7ef86a5b0eb9 (4 revisions) (flutter/flutter#190347) 2026-07-31 [email protected] [Impeller] Do not acquire an unneeded image in KHRSwapchainVK if the swapchain must be recreated due to a size change (flutter/flutter#190116) 2026-07-31 [email protected] Remove the --enable-serial-gc engine flag (flutter/flutter#190310) 2026-07-31 [email protected] Migrate platform_channel_swift example to material_ui (flutter/flutter#190308) 2026-07-31 [email protected] macOS: Set static text accessibility role for read-only text fields (flutter/flutter#190330) 2026-07-31 [email protected] Roll Dart SDK from 96f49fc43f59 to c3acfc2479f6 (1 revision) (flutter/flutter#190336) 2026-07-31 [email protected] Roll Skia from 07f08414dd66 to dcbd015c9646 (1 revision) (flutter/flutter#190333) 2026-07-31 [email protected] [web] Autofill all AutofillGroup fields in semantics mode (flutter/flutter#187108) 2026-07-31 [email protected] iOS: Configure test task runner thread at display QoS (flutter/flutter#190283) 2026-07-31 [email protected] Roll Skia from a1109355de78 to 07f08414dd66 (5 revisions) (flutter/flutter#190328) 2026-07-31 [email protected] Support reduceMotion accessibility feature in MediaQuery (flutter/flutter#190287) 2026-07-30 [email protected] Roll Dart SDK from 43f7de5f4977 to 96f49fc43f59 (4 revisions) (flutter/flutter#190312) 2026-07-30 [email protected] fix: text selection two handles directionality. (flutter/flutter#179928) 2026-07-30 [email protected] Add java/gradle check when gradle crashes (flutter/flutter#189997) 2026-07-30 [email protected] [Impeller] Add filterQuality support to ImageFilter.shader (flutter/flutter#188544) 2026-07-30 [email protected] [flutter/flutter] Migrate multiple_windows example to material_ui (flutter/flutter#190313) 2026-07-30 [email protected] Roll Skia from 50cf67910462 to a1109355de78 (6 revisions) (flutter/flutter#190306) 2026-07-30 [email protected] Migrate swift xctests to Swift testing tests (flutter/flutter#187801) 2026-07-30 [email protected] Migrates the platform_channel example to material_ui (flutter/flutter#190253) 2026-07-30 [email protected] iOS: Drop unnecessary @mainactor from VSyncClientTests (flutter/flutter#190285) 2026-07-30 [email protected] Add nullptr check to isOpaque (flutter/flutter#189796) 2026-07-30 [email protected] [Flutter GPU] Bind uniforms and textures by cached reflection index (flutter/flutter#189820) 2026-07-30 [email protected] Migrate layers example to material_ui (flutter/flutter#190237) 2026-07-30 [email protected] Add `ShapeBorder` hit test fast paths (flutter/flutter#187962) 2026-07-30 [email protected] ci: mac-must-wait part duex (flutter/flutter#190295) 2026-07-30 [email protected] Roll Skia from 491301f393d1 to 50cf67910462 (3 revisions) (flutter/flutter#190294) 2026-07-30 [email protected] Roll Packages from dd11626 to 7d17fc8 (1 revision) (flutter/flutter#190293) 2026-07-30 [email protected] Roll Skia from 177de1940bf2 to 491301f393d1 (1 revision) (flutter/flutter#190279) 2026-07-30 [email protected] Roll Skia from b8bda91f02db to 177de1940bf2 (5 revisions) (flutter/flutter#190272) 2026-07-30 [email protected] Roll Fuchsia Linux SDK from J8rVTlBnjnUpuHYk3... to _J8wM3kyQpLN9wvRD... (flutter/flutter#190273) 2026-07-30 [email protected] Roll Skia from 78afc18c9ba0 to b8bda91f02db (3 revisions) (flutter/flutter#190268) 2026-07-30 [email protected] Revert: Split the platform and cpuArch part of TargetPlatform (flutter/flutter#190258) 2026-07-30 [email protected] Return null from RenderProxyBoxMixin.computeDryBaseline when the child has no baseline (flutter/flutter#189723) 2026-07-30 [email protected] Roll Skia from 6fea713d32ec to 78afc18c9ba0 (4 revisions) (flutter/flutter#190257) 2026-07-30 [email protected] Roll pub packages (flutter/flutter#190259) 2026-07-29 [email protected] [cross imports] Check examples cross imports (flutter/flutter#187662) 2026-07-29 [email protected] ci: mac verify binaries still has to wait on linux_host_engine (flutter/flutter#190239) 2026-07-29 [email protected] Adds analytics to macos impeller opt in/out (flutter/flutter#190220) 2026-07-29 [email protected] Roll Skia from 0f35bba4945c to 6fea713d32ec (6 revisions) (flutter/flutter#190230) 2026-07-29 [email protected] iOS: Fix Swift warning in TracingTests (flutter/flutter#190203) 2026-07-29 [email protected] Convert all gradle-wrapper.properties in /dev to use distribution 'bin' (flutter/flutter#190226) 2026-07-29 [email protected] Roll Packages from 3e63635 to dd11626 (11 revisions) (flutter/flutter#190216) ...
Fixes #180652
Problem
With a screen reader on, picking a saved login only filled the field you tapped. The other fields stayed empty.
Browsers autofill a login by putting all its fields in one hidden
<form>. But in screen-reader mode each field has to live in its own spot for accessibility, so it never went into that form. The browser saw a half-built login and filled only one field.Fix
Still build the hidden form, but instead of moving the field into it, just link them with the HTML
form="<id>"attribute. The field stays where the screen reader needs it, and the browser now sees a complete login and fills every field.Demo
Semantics ON:
Semantics OFF (regression check, both behave the same):
Known limitation
Chrome on iOS still fills only the username, not the password. That is a pre-existing WebKit limitation unrelated to this change (it also happens without a screen reader) and is tracked separately in #185327.