[web] - Fix autofill group input ordering (flutter/engine#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 https://github.com/flutter/flutter/issues/61301
This commit is contained in:
Hassan 2023-05-26 11:17:18 -05:00 committed by GitHub
parent b6e8bd6234
commit f0e2596c5c
2 changed files with 62 additions and 1 deletions

View File

@ -145,6 +145,7 @@ class EngineAutofillForm {
this.elements,
this.items,
this.formIdentifier = '',
this.insertionReferenceNode,
});
final DomHTMLFormElement formElement;
@ -153,6 +154,7 @@ class EngineAutofillForm {
final Map<String, AutofillInfo>? items;
final DomHTMLElement? insertionReferenceNode;
/// Identifier for the form.
///
/// It is constructed by concatenating unique ids of input elements on the
@ -189,6 +191,7 @@ class EngineAutofillForm {
final Map<String, DomHTMLElement> elements = <String, DomHTMLElement>{};
final Map<String, AutofillInfo> items = <String, AutofillInfo>{};
final DomHTMLFormElement formElement = createDomHTMLFormElement();
DomHTMLElement? insertionReferenceNode;
// Validation is in the framework side.
formElement.noValidate = true;
@ -209,6 +212,7 @@ class EngineAutofillForm {
AutofillInfo.fromFrameworkMessage(focusedElementAutofill);
if (fields != null) {
bool fieldIsFocusedElement = false;
for (final Map<String, dynamic> field in
fields.cast<Map<String, dynamic>>()) {
final Map<String, dynamic> autofillInfo = field.readJson('autofill');
@ -234,6 +238,17 @@ class EngineAutofillForm {
items[autofill.uniqueIdentifier] = autofill;
elements[autofill.uniqueIdentifier] = htmlElement;
formElement.append(htmlElement);
// We want to track the node in the position directly after our focused
// element, so we can later insert that element in the correct position
// right before this node.
if(fieldIsFocusedElement){
insertionReferenceNode = htmlElement;
fieldIsFocusedElement = false;
}
} else {
// current field is the focused element that we create elsewhere
fieldIsFocusedElement = true;
}
}
} else {
@ -268,16 +283,21 @@ class EngineAutofillForm {
formElement.append(submitButton);
// If the focused node is at the end of the form, we'll default to inserting
// it before the submit field.
insertionReferenceNode ??= submitButton;
return EngineAutofillForm(
formElement: formElement,
elements: elements,
items: items,
formIdentifier: formIdentifier,
insertionReferenceNode: insertionReferenceNode
);
}
void placeForm(DomHTMLElement mainTextEditingElement) {
formElement.append(mainTextEditingElement);
formElement.insertBefore(mainTextEditingElement, insertionReferenceNode);
defaultTextEditingRoot.append(formElement);
}

View File

@ -2176,6 +2176,47 @@ Future<void> testMain() async {
expect(autofillForm, isNull);
});
test('placeForm() should place element in correct position', () {
final List<dynamic> fields = createFieldValues(<String>[
'email',
'username',
'password',
], <String>[
'field1',
'field2',
'field3'
]);
final EngineAutofillForm autofillForm =
EngineAutofillForm.fromFrameworkMessage(
createAutofillInfo('email', 'field1'), fields)!;
expect(autofillForm.elements, hasLength(2));
List<DomHTMLInputElement> formChildNodes =
autofillForm.formElement.childNodes.toList() as List<DomHTMLInputElement>;
// Only username, password, submit nodes are created
expect(formChildNodes, hasLength(3));
expect(formChildNodes[0].name, 'username');
expect(formChildNodes[1].name, 'current-password');
expect(formChildNodes[2].type, 'submit');
// insertion point for email should be before username
expect(autofillForm.insertionReferenceNode, formChildNodes[0]);
final DomHTMLInputElement testInputElement = createDomHTMLInputElement();
testInputElement.name = 'email';
autofillForm.placeForm(testInputElement);
formChildNodes = autofillForm.formElement.childNodes.toList()
as List<DomHTMLInputElement>;
// email node should be placed before username
expect(formChildNodes, hasLength(4));
expect(formChildNodes[0].name, 'email');
expect(formChildNodes[1].name, 'username');
expect(formChildNodes[2].name, 'current-password');
expect(formChildNodes[3].type, 'submit');
});
tearDown(() {
clearForms();
});