Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 40df5ac

Browse files
authored
[web] - Fix autofill group input ordering (#42268)
Ordering of input elements inside of the DOM tree for autofill groups does not reflect the order of the form rendered on screen. This is causing some issues with password managers and autofill, specifically Bitwarden. We are currently always appending the currently focused input element to the end of the form. This leads to a tree that appears out of order: <img width="354" alt="Screenshot 2023-05-23 at 2 57 37 PM" src="https://github.com/flutter/engine/assets/110993981/7e90a93f-5522-4482-8fb6-a1607b403d10"> This fix is tracking the position of where the focused input node should be inserted and inserting it there, rather than always at the end of the form. Once the tree is ordered correctly, Bitwarden's autofill logic works in Flutter forms. Tree order after fix: <img width="502" alt="Screenshot 2023-05-23 at 6 01 05 PM" src="https://github.com/flutter/engine/assets/110993981/bd15a8a1-71f4-4f28-a86e-1903953bf030"> Fixes flutter/flutter#61301
1 parent 4f3566b commit 40df5ac

File tree

2 files changed

+62
-1
lines changed

2 files changed

+62
-1
lines changed

lib/web_ui/lib/src/engine/text_editing/text_editing.dart

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ class EngineAutofillForm {
145145
this.elements,
146146
this.items,
147147
this.formIdentifier = '',
148+
this.insertionReferenceNode,
148149
});
149150

150151
final DomHTMLFormElement formElement;
@@ -153,6 +154,7 @@ class EngineAutofillForm {
153154

154155
final Map<String, AutofillInfo>? items;
155156

157+
final DomHTMLElement? insertionReferenceNode;
156158
/// Identifier for the form.
157159
///
158160
/// It is constructed by concatenating unique ids of input elements on the
@@ -189,6 +191,7 @@ class EngineAutofillForm {
189191
final Map<String, DomHTMLElement> elements = <String, DomHTMLElement>{};
190192
final Map<String, AutofillInfo> items = <String, AutofillInfo>{};
191193
final DomHTMLFormElement formElement = createDomHTMLFormElement();
194+
DomHTMLElement? insertionReferenceNode;
192195

193196
// Validation is in the framework side.
194197
formElement.noValidate = true;
@@ -209,6 +212,7 @@ class EngineAutofillForm {
209212
AutofillInfo.fromFrameworkMessage(focusedElementAutofill);
210213

211214
if (fields != null) {
215+
bool fieldIsFocusedElement = false;
212216
for (final Map<String, dynamic> field in
213217
fields.cast<Map<String, dynamic>>()) {
214218
final Map<String, dynamic> autofillInfo = field.readJson('autofill');
@@ -234,6 +238,17 @@ class EngineAutofillForm {
234238
items[autofill.uniqueIdentifier] = autofill;
235239
elements[autofill.uniqueIdentifier] = htmlElement;
236240
formElement.append(htmlElement);
241+
242+
// We want to track the node in the position directly after our focused
243+
// element, so we can later insert that element in the correct position
244+
// right before this node.
245+
if(fieldIsFocusedElement){
246+
insertionReferenceNode = htmlElement;
247+
fieldIsFocusedElement = false;
248+
}
249+
} else {
250+
// current field is the focused element that we create elsewhere
251+
fieldIsFocusedElement = true;
237252
}
238253
}
239254
} else {
@@ -268,16 +283,21 @@ class EngineAutofillForm {
268283

269284
formElement.append(submitButton);
270285

286+
// If the focused node is at the end of the form, we'll default to inserting
287+
// it before the submit field.
288+
insertionReferenceNode ??= submitButton;
289+
271290
return EngineAutofillForm(
272291
formElement: formElement,
273292
elements: elements,
274293
items: items,
275294
formIdentifier: formIdentifier,
295+
insertionReferenceNode: insertionReferenceNode
276296
);
277297
}
278298

279299
void placeForm(DomHTMLElement mainTextEditingElement) {
280-
formElement.append(mainTextEditingElement);
300+
formElement.insertBefore(mainTextEditingElement, insertionReferenceNode);
281301
defaultTextEditingRoot.append(formElement);
282302
}
283303

lib/web_ui/test/engine/text_editing_test.dart

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2176,6 +2176,47 @@ Future<void> testMain() async {
21762176
expect(autofillForm, isNull);
21772177
});
21782178

2179+
test('placeForm() should place element in correct position', () {
2180+
final List<dynamic> fields = createFieldValues(<String>[
2181+
'email',
2182+
'username',
2183+
'password',
2184+
], <String>[
2185+
'field1',
2186+
'field2',
2187+
'field3'
2188+
]);
2189+
final EngineAutofillForm autofillForm =
2190+
EngineAutofillForm.fromFrameworkMessage(
2191+
createAutofillInfo('email', 'field1'), fields)!;
2192+
2193+
expect(autofillForm.elements, hasLength(2));
2194+
2195+
List<DomHTMLInputElement> formChildNodes =
2196+
autofillForm.formElement.childNodes.toList() as List<DomHTMLInputElement>;
2197+
2198+
// Only username, password, submit nodes are created
2199+
expect(formChildNodes, hasLength(3));
2200+
expect(formChildNodes[0].name, 'username');
2201+
expect(formChildNodes[1].name, 'current-password');
2202+
expect(formChildNodes[2].type, 'submit');
2203+
// insertion point for email should be before username
2204+
expect(autofillForm.insertionReferenceNode, formChildNodes[0]);
2205+
2206+
final DomHTMLInputElement testInputElement = createDomHTMLInputElement();
2207+
testInputElement.name = 'email';
2208+
autofillForm.placeForm(testInputElement);
2209+
2210+
formChildNodes = autofillForm.formElement.childNodes.toList()
2211+
as List<DomHTMLInputElement>;
2212+
// email node should be placed before username
2213+
expect(formChildNodes, hasLength(4));
2214+
expect(formChildNodes[0].name, 'email');
2215+
expect(formChildNodes[1].name, 'username');
2216+
expect(formChildNodes[2].name, 'current-password');
2217+
expect(formChildNodes[3].type, 'submit');
2218+
});
2219+
21792220
tearDown(() {
21802221
clearForms();
21812222
});

0 commit comments

Comments
 (0)