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

[TextInput] enroll in autofill by default #28333

Merged
Merged
126 changes: 78 additions & 48 deletions lib/web_ui/lib/src/engine/text_editing/text_editing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,16 @@ void _hideAutofillElements(html.HtmlElement domElement,

/// Form that contains all the fields in the same AutofillGroup.
///
/// These values are to be used when autofill is enabled and there is a group of
/// text fields with more than one text field.
/// An [EngineAutofillForm] will only be constructed when autofill is enabled
/// (the default) on the current input field. See the [fromFrameworkMessage]
/// static method.
class EngineAutofillForm {
EngineAutofillForm(
{required this.formElement,
this.elements,
this.items,
this.formIdentifier = ''});
EngineAutofillForm({
required this.formElement,
this.elements,
this.items,
this.formIdentifier = '',
});

final html.FormElement formElement;

Expand All @@ -153,12 +155,23 @@ class EngineAutofillForm {
/// See [formsOnTheDom].
final String formIdentifier;

/// Creates an [EngineAutofillFrom] from the JSON representation of a Flutter
/// framework `TextInputConfiguration` object.
///
/// The `focusedElementAutofill` argument corresponds to the "autofill" field
/// in a `TextInputConfiguration`. Not having this field indicates autofill
/// is explicitly disabled on the text field by the developer.
///
/// The `fields` argument corresponds to the "fields" field in a
/// `TextInputConfiguration`.
///
/// Returns null if autofill is disabled for the input field.
static EngineAutofillForm? fromFrameworkMessage(
Map<String, dynamic>? focusedElementAutofill,
List<dynamic>? fields,
) {
// Autofill value can be null if focused text element does not have an
// autofill hint set.
// Autofill value will be null if the developer explicitly disables it on
// the input field.
if (focusedElementAutofill == null) {
return null;
}
Expand Down Expand Up @@ -287,7 +300,7 @@ class EngineAutofillForm {
element.onInput.listen((html.Event e) {
if (items![key] == null) {
throw StateError(
'Autofill would not work withuot Autofill value set');
'AutofillInfo must have a valid uniqueIdentifier.');
} else {
final AutofillInfo autofillInfo = items![key]!;
handleChange(element, autofillInfo);
Expand Down Expand Up @@ -330,11 +343,13 @@ class EngineAutofillForm {
/// These values are to be used when a text field have autofill enabled.
@visibleForTesting
class AutofillInfo {
AutofillInfo(
{required this.editingState,
required this.uniqueIdentifier,
required this.hint,
required this.textCapitalization});
AutofillInfo({
required this.editingState,
required this.uniqueIdentifier,
required this.autofillHint,
required this.textCapitalization,
this.placeholder,
});

/// The current text and selection state of a text field.
final EditingState editingState;
Expand All @@ -359,47 +374,67 @@ class AutofillInfo {
/// other the focused field, we need to use this information.
final TextCapitalizationConfig textCapitalization;

/// Attribute used for autofill.
/// The type of information expected in the field, specified by the developer.
///
/// Used as a guidance to the browser as to the type of information expected
/// in the field.
/// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
final String hint;
final String? autofillHint;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: is the first sentence in the dartdoc still useful? Seems obvious now that this is used for autofill.


/// The optional hint text placed on the view that typically suggests what
/// sort of input the field accepts, for example "enter your password here".
///
/// If the developer does not specify any [autofillHints], the [placeholder]
/// can be a useful indication to the platform autofill service as to what
/// information is expected in this field.
final String? placeholder;

factory AutofillInfo.fromFrameworkMessage(Map<String, dynamic> autofill,
{TextCapitalizationConfig textCapitalization =
const TextCapitalizationConfig.defaultCapitalization()}) {
assert(autofill != null); // ignore: unnecessary_null_comparison
final String uniqueIdentifier = autofill.readString('uniqueIdentifier');
final List<dynamic> hintsList = autofill.readList('hints');
final List<dynamic>? hintsList = autofill.tryList('hints');
final String? firstHint = (hintsList == null || hintsList.isEmpty) ? null : hintsList.first as String;
final EditingState editingState =
EditingState.fromFrameworkMessage(autofill.readJson('editingValue'));
return AutofillInfo(
uniqueIdentifier: uniqueIdentifier,
hint: BrowserAutofillHints.instance.flutterToEngine(hintsList[0] as String),
autofillHint: (firstHint != null) ? BrowserAutofillHints.instance.flutterToEngine(firstHint) : null,
editingState: editingState,
placeholder: autofill.tryString('hintText'),
textCapitalization: textCapitalization,
);
}

void applyToDomElement(html.HtmlElement domElement,
{bool focusedElement = false}) {
domElement.id = hint;
final String? autofillHint = this.autofillHint;
final String? placeholder = this.placeholder;
if (domElement is html.InputElement) {
final html.InputElement element = domElement;
element.name = hint;
element.id = hint;
element.autocomplete = hint;
if (hint.contains('password')) {
element.type = 'password';
} else {
element.type = 'text';
if (placeholder != null) {
element.placeholder = placeholder;
}
if (autofillHint != null) {
element.name = autofillHint;
element.id = autofillHint;
if (autofillHint.contains('password')) {
element.type = 'password';
} else {
element.type = 'text';
}
}
element.autocomplete = autofillHint ?? 'on';
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want to enable autocomplete when autofill hint is null?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah that's what the pull request trying to do: flutter/flutter#85554. If we want to do it on Android then the default behavior should probably be consistent across platforms (or does it make sense on the web?).

https://github.com/flutter/flutter/pull/86312/files tries to make so that even if the developer leaves the hint list empty (the default), EditableTextState will still generate an autofill JSON object for the text field when it's focused, and the autofill service can try to determine the type of the text field (or whether the field should be autofilled at all) using heuristics.
if the developer explicitly sets EditableText.autofillHints to null, then autofill will be disabled.

} else if (domElement is html.TextAreaElement) {
final html.TextAreaElement element = domElement;
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure why this variable was created in the first place, good idea to remove it.

element.name = hint;
element.id = hint;
element.setAttribute('autocomplete', hint);
if (placeholder != null) {
domElement.placeholder = placeholder;
}
if (autofillHint != null) {
domElement.name = autofillHint;
domElement.id = autofillHint;
}
domElement.setAttribute('autocomplete', autofillHint ?? 'on');
}
}
}
Expand Down Expand Up @@ -691,15 +726,13 @@ class GloballyPositionedTextEditingStrategy extends DefaultTextEditingStrategy {

@override
void placeElement() {
geometry?.applyToDomElement(activeDomElement);
if (hasAutofillGroup) {
geometry?.applyToDomElement(focusedFormElement!);
placeForm();
// Set the last editing state if it exists, this is critical for a
// users ongoing work to continue uninterrupted when there is an update to
// the transform.
if (lastEditingState != null) {
lastEditingState!.applyToDomElement(domElement);
}
lastEditingState?.applyToDomElement(domElement);
// On Chrome, when a form is focused, it opens an autofill menu
// immediately.
// Flutter framework sends `setEditableSizeAndTransform` for informing
Expand All @@ -712,8 +745,6 @@ class GloballyPositionedTextEditingStrategy extends DefaultTextEditingStrategy {
// Refocus on the elements after applying the geometry.
focusedFormElement!.focus();
activeDomElement.focus();
} else {
geometry?.applyToDomElement(activeDomElement);
}
}
}
Expand Down Expand Up @@ -762,9 +793,7 @@ class SafariDesktopTextEditingStrategy extends DefaultTextEditingStrategy {
// the transform.
// If domElement is not focused cursor location will not be correct.
activeDomElement.focus();
if (lastEditingState != null) {
lastEditingState!.applyToDomElement(activeDomElement);
}
lastEditingState?.applyToDomElement(activeDomElement);
}
}

Expand Down Expand Up @@ -888,7 +917,12 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {
activeDomElement.setAttribute('inputmode', 'none');
}

config.autofill?.applyToDomElement(activeDomElement, focusedElement: true);
final AutofillInfo? autofill = config.autofill;
if (autofill != null) {
autofill.applyToDomElement(activeDomElement, focusedElement: true);
} else {
activeDomElement.setAttribute('autocomplete', 'off');
}

final String autocorrectValue = config.autocorrect ? 'on' : 'off';
activeDomElement.setAttribute('autocorrect', autocorrectValue);
Expand Down Expand Up @@ -1366,9 +1400,7 @@ class FirefoxTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
// Set the last editing state if it exists, this is critical for a
// users ongoing work to continue uninterrupted when there is an update to
// the transform.
if (lastEditingState != null) {
lastEditingState!.applyToDomElement(activeDomElement);
}
lastEditingState?.applyToDomElement(activeDomElement);
}
}

Expand Down Expand Up @@ -1753,11 +1785,9 @@ class HybridTextEditing {
///
/// The constructor also decides which text editing strategy to use depending
/// on the operating system and browser engine.
HybridTextEditing() {
channel = TextEditingChannel(this);
}
HybridTextEditing();

late TextEditingChannel channel;
late final TextEditingChannel channel = TextEditingChannel(this);
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this still need late?

I feel like I'm wrong and I got tripped up by this recently...

Copy link
Contributor

Choose a reason for hiding this comment

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

I think late is still required because the right-hand-side expression refers to this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. This needs lazy initialization because this has to be initialized first, to be passed to the TextEditingChannel constructor, otherwise we'll have a circular dependency.

Copy link
Contributor

Choose a reason for hiding this comment

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

Does this still need to be late?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think late is still required because the right-hand-side expression refers to this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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


/// A CSS class name used to identify all elements used for text editing.
@visibleForTesting
Expand Down
Loading