Merge 8a6b584671efa92e3a65eb0480d5b847a092e37a into 06df71c51446e96939c6a615b7c34ce9123806ba

This commit is contained in:
Mouad Debbar 2026-02-19 14:16:17 -03:00 committed by GitHub
commit 0fc2499dec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 315 additions and 207 deletions

View File

@ -188,20 +188,16 @@ void _insertEditingElementInView(DomElement element, int viewId) {
class EngineAutofillForm {
EngineAutofillForm({
required this.viewId,
required this.formElement,
this.elements,
this.items,
this.formIdentifier = '',
this.insertionReferenceNode,
required this.items,
required this.formIdentifier,
required this.focusedElementId,
});
final DomHTMLFormElement formElement;
DomHTMLFormElement? formElement;
final Map<String, DomHTMLElement>? elements;
final elements = <String, DomHTMLElement>{};
final Map<String, AutofillInfo>? items;
final DomHTMLElement? insertionReferenceNode;
final Map<String, FieldItem> items;
/// Identifier for the form.
///
@ -209,13 +205,19 @@ class EngineAutofillForm {
/// form.
///
/// It is used for storing the form until submission.
/// See [formsOnTheDom].
/// See [dormantForms].
final String formIdentifier;
/// The ID of the view that this form is rendered into.
final int viewId;
/// Creates an [EngineAutofillFrom] from the JSON representation of a Flutter
final String focusedElementId;
bool get _isSafariStrategy =>
textEditing.strategy is SafariDesktopTextEditingStrategy ||
textEditing.strategy is IOSTextEditingStrategy;
/// Creates an [EngineAutofillForm] from the JSON representation of a Flutter
/// framework `TextInputConfiguration` object.
///
/// The `focusedElementAutofill` argument corresponds to the "autofill" field
@ -237,15 +239,130 @@ class EngineAutofillForm {
return null;
}
final items = <String, FieldItem>{};
// If there is only one text field in the autofill model, `fields` will be
// null. `focusedElementAutofill` contains the information about the one
// text field.
final elements = <String, DomHTMLElement>{};
final items = <String, AutofillInfo>{};
final DomHTMLFormElement formElement = createDomHTMLFormElement();
final isSafariDesktopStrategy = textEditing.strategy is SafariDesktopTextEditingStrategy;
DomHTMLElement? insertionReferenceNode;
if (fields != null) {
for (final Map<String, dynamic> field in fields.cast<Map<String, dynamic>>()) {
final autofillInfo = AutofillInfo.fromFrameworkMessage(
field.readJson('autofill'),
textCapitalization: TextCapitalizationConfig.fromInputConfiguration(
field.readString('textCapitalization'),
),
);
final EngineInputType inputType = EngineInputType.fromName(
field.readJson('inputType').readString('name'),
);
items[autofillInfo.uniqueIdentifier] = FieldItem(
inputType: inputType,
autofillInfo: autofillInfo,
);
}
} else {
final autofillInfo = AutofillInfo.fromFrameworkMessage(
focusedElementAutofill,
textCapitalization: TextCapitalizationConfig.fromInputConfiguration(
focusedElementAutofill.readString('textCapitalization'),
),
);
// Any `inputType` is okay here since this will not be used to create the focused element
// here. The focused element is a special case that is created outside of the
// `EngineAutofillForm`.
const EngineInputType inputType = EngineInputType.none;
items[autofillInfo.uniqueIdentifier] = FieldItem(
inputType: inputType,
autofillInfo: autofillInfo,
);
}
return EngineAutofillForm(
viewId: viewId,
items: items,
formIdentifier: _getFormIdentifier(items),
focusedElementId: focusedElementAutofill.readString('uniqueIdentifier'),
);
}
static String _getFormIdentifier(Map<String, FieldItem> items) {
final ids = <String>[];
for (final FieldItem item in items.values) {
ids.add(item.autofillInfo.uniqueIdentifier);
}
ids.sort();
return ids.join('*');
}
/// Wakes up the form with the given focused element.
///
/// The [focusedElement] is inserted into the form, replacing the old focused element.
void wakeUp(DomHTMLElement focusedElement, AutofillInfo focusedAutofill) {
// Since we're disabling pointer events on the form to fix Safari autofill,
// we need to explicitly set pointer events on the active input element in
// order to calculate the correct pointer event offsets.
// See: https://github.com/flutter/flutter/issues/136006
if (_isSafariStrategy) {
focusedElement.style.pointerEvents = 'all';
}
final EngineAutofillForm? existingForm = dormantForms[formIdentifier];
if (!identical(this, existingForm)) {
assert(formElement == null);
assert(elements.isEmpty);
if (existingForm != null) {
// If the form already has a dormant DOM element, let's use it instead of creating a new one.
formElement = existingForm.formElement;
elements.addAll(existingForm.elements);
// There's a new focused element that needs to be inserted into the existing form.
//
// Do not cause DOM disturbance unless necessary. Doing superfluous DOM operations may seem
// harmless, but it actually causes focus changes that could break things.
if (!formElement!.contains(focusedElement)) {
// Find the matching element and replace it with the new focused element.
final DomElement oldFocusedElement = elements[focusedAutofill.uniqueIdentifier]!;
elements[focusedAutofill.uniqueIdentifier] = focusedElement;
oldFocusedElement.replaceWith(focusedElement);
}
} else {
formElement = _createFormElementAndFields(focusedElement, focusedAutofill);
_insertEditingElementInView(formElement!, viewId);
}
}
_updateFieldValues();
}
/// Makes the form dormant.
///
/// A dormant form stays in the DOM and does not interact with the framework until it's woken up.
///
/// The form is kept in the DOM to:
/// 1. Allow the browser to autofill it.
/// 2. Allow submitting the form later.
void goDormant() {
assert(formElement != null);
dormantForms[formIdentifier] = this;
_styleAutofillElements(formElement!, isOffScreen: true);
}
DomHTMLFormElement _createFormElementAndFields(
DomHTMLElement focusedElement,
AutofillInfo focusedAutofill,
) {
assert(this.formElement == null);
assert(elements.isEmpty);
final DomHTMLFormElement formElement = createDomHTMLFormElement();
// Validation is in the framework side.
formElement.noValidate = true;
formElement.method = 'post';
@ -255,88 +372,34 @@ class EngineAutofillForm {
// We need to explicitly disable pointer events on the form in Safari Desktop,
// so that we don't have pointer event collisions if users hover over or click
// into the invisible autofill elements within the form.
_styleAutofillElements(formElement, shouldDisablePointerEvents: isSafariDesktopStrategy);
_styleAutofillElements(formElement, shouldDisablePointerEvents: _isSafariStrategy);
// We keep the ids in a list then sort them later, in case the text fields'
// locations are re-ordered on the framework side.
final ids = List<String>.empty(growable: true);
for (final FieldItem field in items.values) {
final DomHTMLElement htmlElement;
if (field.autofillInfo.uniqueIdentifier == focusedAutofill.uniqueIdentifier) {
// Do not create the focused element here since it is created already. Use the provided one.
htmlElement = focusedElement;
} else {
htmlElement = field.inputType.createDomElement();
field.autofillInfo.applyToDomElement(htmlElement);
// The focused text editing element will not be created here.
final focusedElement = AutofillInfo.fromFrameworkMessage(focusedElementAutofill);
if (fields != null) {
var fieldIsFocusedElement = false;
for (final Map<String, dynamic> field in fields.cast<Map<String, dynamic>>()) {
final Map<String, dynamic> autofillInfo = field.readJson('autofill');
final autofill = AutofillInfo.fromFrameworkMessage(
autofillInfo,
textCapitalization: TextCapitalizationConfig.fromInputConfiguration(
field.readString('textCapitalization'),
),
// Safari does not respect elements that are invisible (or
// have no size) and that leads to issues with autofill only partially
// working (ref: https://github.com/flutter/flutter/issues/71275).
// Thus, we have to make sure that the elements remain invisible to users,
// but not to Safari for autofill to work. Since these elements are
// sized and placed on the DOM, we also have to disable pointer events.
_styleAutofillElements(
htmlElement,
shouldHideElement: !_isSafariStrategy,
shouldDisablePointerEvents: _isSafariStrategy,
);
ids.add(autofill.uniqueIdentifier);
if (autofill.uniqueIdentifier != focusedElement.uniqueIdentifier) {
final EngineInputType engineInputType = EngineInputType.fromName(
field.readJson('inputType').readString('name'),
);
final DomHTMLElement htmlElement = engineInputType.createDomElement();
autofill.editingState.applyToDomElement(htmlElement);
autofill.applyToDomElement(htmlElement);
// Safari Desktop does not respect elements that are invisible (or
// have no size) and that leads to issues with autofill only partially
// working (ref: https://github.com/flutter/flutter/issues/71275).
// Thus, we have to make sure that the elements remain invisible to users,
// but not to Safari for autofill to work. Since these elements are
// sized and placed on the DOM, we also have to disable pointer events.
_styleAutofillElements(
htmlElement,
shouldHideElement: !isSafariDesktopStrategy,
shouldDisablePointerEvents: isSafariDesktopStrategy,
);
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 {
// There is one input element in the form.
ids.add(focusedElement.uniqueIdentifier);
elements[field.autofillInfo.uniqueIdentifier] = htmlElement;
formElement.append(htmlElement);
}
ids.sort();
final idBuffer = StringBuffer();
// Add a separator between element identifiers.
for (final id in ids) {
if (idBuffer.length > 0) {
idBuffer.write('*');
}
idBuffer.write(id);
}
final formIdentifier = idBuffer.toString();
// If a form with the same Autofill elements is already on the dom, remove
// it from DOM.
final DomHTMLFormElement? form = formsOnTheDom[formIdentifier];
form?.remove();
// In order to submit the form when Framework sends a `TextInput.commit`
// message, we add a submit button to the form.
// The -1 tab index value makes this element not reachable by keyboard.
@ -344,43 +407,19 @@ class EngineAutofillForm {
_styleAutofillElements(submitButton, isOffScreen: true);
submitButton.className = 'submitBtn';
submitButton.type = 'submit';
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(
viewId: viewId,
formElement: formElement,
elements: elements,
items: items,
formIdentifier: formIdentifier,
insertionReferenceNode: insertionReferenceNode,
);
return formElement;
}
void placeForm(DomHTMLElement mainTextEditingElement) {
// Since we're disabling pointer events on the form to fix Safari autofill,
// we need to explicitly set pointer events on the active input element in
// order to calculate the correct pointer event offsets.
// See: https://github.com/flutter/flutter/issues/136006
if (textEditing.strategy is SafariDesktopTextEditingStrategy) {
mainTextEditingElement.style.pointerEvents = 'all';
/// Updates the field values in this form.
void _updateFieldValues() {
for (final String key in elements.keys) {
final DomHTMLElement element = elements[key]!;
final AutofillInfo autofill = items[key]!.autofillInfo;
// Maybe only apply text but not selection?
autofill.editingState.applyToDomElement(element);
}
// Do not cause DOM disturbance unless necessary. Doing superfluous DOM operations may seem
// harmless, but it actually causes focus changes that could break things.
if (!formElement.contains(mainTextEditingElement)) {
formElement.insertBefore(mainTextEditingElement, insertionReferenceNode);
}
_insertEditingElementInView(formElement, viewId);
}
void storeForm() {
formsOnTheDom[formIdentifier] = formElement;
_styleAutofillElements(formElement, isOffScreen: true);
}
/// Listens to `onInput` event on the form fields.
@ -394,21 +433,22 @@ class EngineAutofillForm {
/// [TextEditingStrategy.addEventHandlers] method call and all
/// listeners are removed during [TextEditingStrategy.disable] method call.
List<DomSubscription> addInputEventListeners() {
final Iterable<String> keys = elements!.keys;
final Iterable<String> keys = elements.keys;
final subscriptions = <DomSubscription>[];
void addSubscriptionForKey(String key) {
final DomElement element = elements![key]!;
final DomHTMLElement element = elements[key]!;
subscriptions.add(
DomSubscription(
element,
'input',
createDomEventListener((DomEvent e) {
if (items![key] == null) {
if (items[key] == null) {
throw StateError('AutofillInfo must have a valid uniqueIdentifier.');
} else {
final AutofillInfo autofillInfo = items![key]!;
handleChange(element, autofillInfo);
} else if (key != focusedElementId) {
// `input` events on the focused element are handled elsewhere.
final AutofillInfo autofillInfo = items[key]!.autofillInfo;
_handleChange(element, autofillInfo);
}
}),
),
@ -419,20 +459,19 @@ class EngineAutofillForm {
return subscriptions;
}
void handleChange(DomElement domElement, AutofillInfo autofillInfo) {
final newEditingState = EditingState.fromDomElement(domElement as DomHTMLElement);
void _handleChange(DomHTMLElement domElement, AutofillInfo autofillInfo) {
final newEditingState = EditingState.fromDomElement(domElement);
_sendAutofillEditingState(autofillInfo.uniqueIdentifier, newEditingState);
}
/// Sends the 'TextInputClient.updateEditingStateWithTag' message to the framework.
void _sendAutofillEditingState(String? tag, EditingState editingState) {
void _sendAutofillEditingState(String tag, EditingState editingState) {
EnginePlatformDispatcher.instance.invokeOnPlatformMessage(
'flutter/textinput',
const JSONMethodCodec().encodeMethodCall(
MethodCall('TextInputClient.updateEditingStateWithTag', <dynamic>[
0,
<String?, dynamic>{tag: editingState.toFlutter()},
<String, dynamic>{tag: editingState.toFlutter()},
]),
),
_emptyCallback,
@ -440,6 +479,13 @@ class EngineAutofillForm {
}
}
class FieldItem {
FieldItem({required this.inputType, required this.autofillInfo});
final EngineInputType inputType;
final AutofillInfo autofillInfo;
}
/// Autofill related values.
///
/// These values are to be used when a text field have autofill enabled.
@ -969,10 +1015,6 @@ class EditingState {
/// This should only be used by focused elements only, because only focused
/// elements can have their text selection range set. Attempting to set
/// selection range on a non-focused element will cause it to request focus.
///
/// See also:
///
/// * [applyTextToDomElement], which is used for non-focused elements.
void applyToDomElement(DomHTMLElement? domElement) {
if (domElement != null && domElement.isA<DomHTMLInputElement>()) {
final element = domElement as DomHTMLInputElement;
@ -988,25 +1030,6 @@ class EditingState {
);
}
}
/// Applies the [text] to the [domElement].
///
/// This is used by non-focused elements.
///
/// See also:
///
/// * [applyToDomElement], which is used for focused elements.
void applyTextToDomElement(DomHTMLElement? domElement) {
if (domElement != null && domElement.isA<DomHTMLInputElement>()) {
final element = domElement as DomHTMLInputElement;
element.value = text;
} else if (domElement != null && domElement.isA<DomHTMLTextAreaElement>()) {
final element = domElement as DomHTMLTextAreaElement;
element.value = text;
} else {
throw UnsupportedError('Unsupported DOM element type');
}
}
}
/// Controls the appearance of the input control being edited.
@ -1085,6 +1108,7 @@ class InputConfiguration {
final bool enableDeltaModel;
/// Autofill information for the focused text field.
final AutofillInfo? autofill;
final EngineAutofillForm? autofillGroup;
@ -1482,7 +1506,7 @@ abstract class DefaultTextEditingStrategy
// More details on `TextInput.finishAutofillContext` call.
if (_appendedToForm && inputConfiguration.autofillGroup?.formElement != null) {
_styleAutofillElements(activeDomElement, isOffScreen: true);
inputConfiguration.autofillGroup?.storeForm();
inputConfiguration.autofillGroup?.goDormant();
EnginePlatformDispatcher.instance.viewManager.safeBlur(activeDomElement);
} else {
EnginePlatformDispatcher.instance.viewManager.safeRemove(activeDomElement);
@ -1504,7 +1528,11 @@ abstract class DefaultTextEditingStrategy
}
void placeForm() {
inputConfiguration.autofillGroup!.placeForm(activeDomElement);
inputConfiguration.autofillGroup!.wakeUp(activeDomElement, inputConfiguration.autofill!);
// When the form woke up, it should've placed the correct editing state on all fields, including
// the focused one. We update `lastEditingState` to reflect that.
lastEditingState = EditingState.fromDomElement(activeDomElement);
_appendedToForm = true;
}
@ -2261,8 +2289,8 @@ class TextInputFinishAutofillContext extends TextInputCommand {
/// Called when the form is finalized with save option `true`.
/// See: https://github.com/flutter/flutter/blob/bf9f3a3dcfea3022f9cf2dfc3ab10b120b48b19d/packages/flutter/lib/src/services/text_input.dart#L1277
void saveForms() {
formsOnTheDom.forEach((String identifier, DomHTMLFormElement form) {
final submitBtn = form.getElementsByClassName('submitBtn').first as DomHTMLInputElement;
dormantForms.forEach((String identifier, EngineAutofillForm form) {
final submitBtn = form.formElement!.getElementsByClassName('submitBtn').first as DomElement;
submitBtn.click();
});
}
@ -2271,10 +2299,10 @@ void saveForms() {
///
/// Called when the form is finalized.
void cleanForms() {
for (final DomHTMLFormElement form in formsOnTheDom.values) {
form.remove();
for (final EngineAutofillForm form in dormantForms.values) {
form.formElement?.remove();
}
formsOnTheDom.clear();
dormantForms.clear();
}
/// Translates the message-based communication between the framework and the
@ -2302,7 +2330,7 @@ class TextEditingChannel {
case 'TextInput.updateConfig':
// Set configuration eagerly because it contains data about the text
// field used to flush the command queue. However, delaye applying the
// field used to flush the command queue. However, delay applying the
// configuration because the strategy may not be available yet.
implementation.configuration = InputConfiguration.fromFrameworkMessage(
call.arguments as Map<String, dynamic>,
@ -2415,6 +2443,17 @@ class TextEditingChannel {
_emptyCallback,
);
}
/// Sends the 'TextInput.refocus' message to the framework.
void refocus(int? clientId) {
EnginePlatformDispatcher.instance.invokeOnPlatformMessage(
'flutter/textinput',
const JSONMethodCodec().encodeMethodCall(
MethodCall('TextInput.refocus', <dynamic>[clientId]),
),
_emptyCallback,
);
}
}
/// Text editing singleton.
@ -2426,7 +2465,7 @@ final HybridTextEditing textEditing = HybridTextEditing();
/// save or cancel them.
///
/// See: https://github.com/flutter/flutter/blob/bf9f3a3dcfea3022f9cf2dfc3ab10b120b48b19d/packages/flutter/lib/src/services/text_input.dart#L1277
final Map<String, DomHTMLFormElement> formsOnTheDom = <String, DomHTMLFormElement>{};
final dormantForms = <String, EngineAutofillForm>{};
/// Should be used as a singleton to provide support for text editing in
/// Flutter Web.
@ -2441,7 +2480,17 @@ class HybridTextEditing {
///
/// The constructor also decides which text editing strategy to use depending
/// on the operating system and browser engine.
HybridTextEditing();
HybridTextEditing() {
if (ui_web.browser.operatingSystem == ui_web.OperatingSystem.iOs) {
// In iOS 26, the text field is blurred right before autofill occurs. We need to keep listening
// for focus events in order to re-establish the connection with the framework when the text
// field is focused again for autofill.
for (final EngineFlutterView view in EnginePlatformDispatcher.instance.views) {
_addRefocusListenerToView(view.viewId);
}
EnginePlatformDispatcher.instance.viewManager.onViewCreated.listen(_addRefocusListenerToView);
}
}
late final TextEditingChannel channel = TextEditingChannel(this);
@ -2505,6 +2554,24 @@ class HybridTextEditing {
channel.onConnectionClosed(_clientId);
}
}
void _addRefocusListenerToView(int viewId) {
final EngineFlutterView? view = EnginePlatformDispatcher.instance.viewManager[viewId];
view!.dom.textEditingHost.addEventListener('focusin', createDomEventListener(_handleRefocus));
}
void _handleRefocus(DomEvent event) {
if (isEditing) {
return;
}
final target = event.target as DomElement?;
if (target == null) {
return;
}
if (target.classList.contains(HybridTextEditing.textEditingClass)) {
channel.refocus(_clientId);
}
}
}
/// Information on the font and alignment of a text editing element.

View File

@ -1007,7 +1007,7 @@ Future<void> testMain() async {
// Form elements
{
final DomHTMLFormElement formElement =
textEditing!.configuration!.autofillGroup!.formElement;
textEditing!.configuration!.autofillGroup!.formElement!;
expect(formElement.style.alignContent, isEmpty);
// Should contain one <input type="text"> and one <input type="submit">
@ -1220,14 +1220,14 @@ Future<void> testMain() async {
expect(spy.messages, isEmpty);
// Form stays on the DOM until autofill context is finalized.
expect(defaultTextEditingRoot.querySelectorAll('form'), isNotEmpty);
expect(formsOnTheDom, hasLength(1));
expect(dormantForms, hasLength(1));
const finishAutofillContext = MethodCall('TextInput.finishAutofillContext', false);
sendFrameworkMessage(codec.encodeMethodCall(finishAutofillContext));
// Form element is removed from DOM.
expect(defaultTextEditingRoot.querySelectorAll('form'), isEmpty);
expect(formsOnTheDom, hasLength(0));
expect(dormantForms, hasLength(0));
});
test('finishAutofillContext with save submits forms', () async {
@ -1339,7 +1339,7 @@ Future<void> testMain() async {
await expectLater(await submittedForm.future, isTrue);
// Form element is removed from DOM.
expect(defaultTextEditingRoot.querySelectorAll('form'), hasLength(0));
expect(formsOnTheDom, hasLength(0));
expect(dormantForms, hasLength(0));
});
test('Moves the focus across input elements', () async {
@ -1540,7 +1540,7 @@ Future<void> testMain() async {
expect(spy.messages, isEmpty);
// Form stays on the DOM until autofill context is finalized.
expect(defaultTextEditingRoot.querySelectorAll('form'), isNotEmpty);
expect(formsOnTheDom, hasLength(1));
expect(dormantForms, hasLength(1));
});
test('singleTextField Autofill setEditableSizeAndTransform preserves'
@ -1613,7 +1613,7 @@ Future<void> testMain() async {
expect(spy.messages, isEmpty);
// Form stays on the DOM until autofill context is finalized.
expect(defaultTextEditingRoot.querySelectorAll('form'), isNotEmpty);
expect(formsOnTheDom, hasLength(1));
expect(dormantForms, hasLength(1));
});
test('multiTextField Autofill: setClient, setEditingState, show, '
@ -1667,7 +1667,7 @@ Future<void> testMain() async {
expect(spy.messages, isEmpty);
// Form stays on the DOM until autofill context is finalized.
expect(defaultTextEditingRoot.querySelectorAll('form'), isNotEmpty);
expect(formsOnTheDom, hasLength(1));
expect(dormantForms, hasLength(1));
});
test('No capitalization: setClient, setEditingState, show', () {
@ -2846,7 +2846,7 @@ Future<void> testMain() async {
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
final DomElement input = textEditing!.strategy.domElement!;
final DomElement form = textEditing!.configuration!.autofillGroup!.formElement;
final DomElement form = textEditing!.configuration!.autofillGroup!.formElement!;
// Input and form are appended to the right view.
expect(view.dom.textEditingHost.contains(input), isTrue);
@ -2886,7 +2886,7 @@ Future<void> testMain() async {
sendFrameworkMessage(codec.encodeMethodCall(show));
final DomElement input = textEditing!.strategy.domElement!;
final DomElement form = textEditing!.configuration!.autofillGroup!.formElement;
final DomElement form = textEditing!.configuration!.autofillGroup!.formElement!;
// Input and form are appended to view1.
expect(view1.dom.textEditingHost.contains(input), isTrue);
@ -3009,13 +3009,14 @@ Future<void> testMain() async {
// Number of elements if number of fields sent to the constructor minus
// one (for the focused text element).
expect(autofillForm.elements, hasLength(2));
expect(autofillForm.items, hasLength(2));
expect(autofillForm.elements, hasLength(3));
expect(autofillForm.items, hasLength(3));
expect(autofillForm.formElement, isNotNull);
expect(autofillForm.formIdentifier, 'field1*field2*field3');
final DomHTMLFormElement form = autofillForm.formElement;
// TODO(mdebbar): Things are different now. DOM elements aren't created immediately like they used to.
final DomHTMLFormElement form = autofillForm.formElement!;
// Note that we also add a submit button. Therefore the form element has
// 3 child nodes.
expect(form.childNodes, hasLength(3));
@ -3074,27 +3075,30 @@ Future<void> testMain() async {
<String>['username', 'password', 'newPassword'],
<String>['field1', 'fields2', 'field3'],
);
final Map<String, dynamic> focusedAutofillMap = createAutofillInfo('username', 'field1');
final EngineAutofillForm autofillForm = EngineAutofillForm.fromFrameworkMessage(
kImplicitViewId,
createAutofillInfo('username', 'field1'),
focusedAutofillMap,
fields,
)!;
final DomHTMLInputElement testInputElement = createDomHTMLInputElement();
autofillForm.placeForm(testInputElement);
final focusedAutofill = AutofillInfo.fromFrameworkMessage(focusedAutofillMap);
autofillForm.wakeUp(testInputElement, focusedAutofill);
// The focused element is appended to the form, form also has the button
// so in total it shoould have 4 elements.
final DomHTMLFormElement form = autofillForm.formElement;
final DomHTMLFormElement form = autofillForm.formElement!;
expect(form.childNodes, hasLength(4));
final formOnDom = defaultTextEditingRoot.querySelector('form')! as DomHTMLFormElement;
// Form is attached to the DOM.
expect(form, equals(formOnDom));
autofillForm.storeForm();
autofillForm.goDormant();
expect(defaultTextEditingRoot.querySelectorAll('form'), isNotEmpty);
expect(formsOnTheDom, hasLength(1));
expect(dormantForms, hasLength(1));
expect(dormantForms, {autofillForm.formIdentifier: autofillForm});
});
test('Validate single element form', () {
@ -3105,16 +3109,20 @@ Future<void> testMain() async {
fields,
)!;
// The focused element is the only field. Form should be empty after
// the initialization (focus element is appended later).
expect(autofillForm.elements, isEmpty);
expect(autofillForm.items, isEmpty);
// The focused element is the only field. Form should have a single element after
// the initialization.
expect(autofillForm.elements, hasLength(1));
expect(autofillForm.elements, contains('field1'));
expect(autofillForm.items, hasLength(1));
expect(autofillForm.items, contains('field1'));
expect(autofillForm.items['field1']!.autofillInfo.autofillHint, 'username');
expect(autofillForm.formElement, isNotNull);
final DomHTMLFormElement form = autofillForm.formElement;
// TODO(mdebbar): Things are different now. DOM elements aren't created immediately like they used to.
final DomHTMLFormElement form = autofillForm.formElement!;
// Submit button is added to the form.
expect(form.childNodes, isNotEmpty);
final inputElement = form.childNodes.toList()[0] as DomHTMLInputElement;
final inputElement = form.childNodes.toList().last as DomHTMLInputElement;
expect(inputElement.type, 'submit');
expect(inputElement.tabIndex, -1, reason: 'The input should not be reachable by keyboard');
@ -3134,37 +3142,45 @@ Future<void> testMain() async {
});
test('placeForm() should place element in correct position', () {
final Map<String, dynamic> focusedAutofillMap = createAutofillInfo('email', 'field1');
final List<dynamic> fields = createFieldValues(
<String>['email', 'username', 'password'],
<String>['field1', 'field2', 'field3'],
);
final EngineAutofillForm autofillForm = EngineAutofillForm.fromFrameworkMessage(
kImplicitViewId,
createAutofillInfo('email', 'field1'),
focusedAutofillMap,
fields,
)!;
expect(autofillForm.elements, hasLength(2));
// TODO(mdebbar): Things are different now. DOM elements aren't created immediately like they used to.
var formChildNodes =
autofillForm.formElement.childNodes.toList() as List<DomHTMLInputElement>;
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);
final focusedAutofill = AutofillInfo.fromFrameworkMessage(focusedAutofillMap);
autofillForm.wakeUp(testInputElement, focusedAutofill);
expect(autofillForm.elements['field1'], testInputElement);
expect(autofillForm.items['field1'], focusedAutofill);
formChildNodes = autofillForm.formElement!.childNodes.toList() as List<DomHTMLInputElement>;
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[0], testInputElement);
expect(formChildNodes[1].name, 'username');
expect(formChildNodes[2].name, 'current-password');
expect(formChildNodes[3].type, 'submit');
@ -3182,8 +3198,9 @@ Future<void> testMain() async {
createAutofillInfo('email', 'field1'),
fields,
)!;
// TODO(mdebbar): Things are different now. DOM elements aren't created immediately like they used to.
final formChildNodes =
autofillForm.formElement.childNodes.toList() as List<DomHTMLInputElement>;
autofillForm.formElement!.childNodes.toList() as List<DomHTMLInputElement>;
final DomHTMLInputElement username = formChildNodes[0];
final DomHTMLInputElement password = formChildNodes[1];
@ -3195,7 +3212,7 @@ Future<void> testMain() async {
expect(password.style.width, '0px');
expect(password.style.height, '0px');
expect(password.style.pointerEvents, isNot('none'));
expect(autofillForm.formElement.style.pointerEvents, isNot('none'));
expect(autofillForm.formElement!.style.pointerEvents, isNot('none'));
},
skip: isSafari,
);
@ -3210,8 +3227,9 @@ Future<void> testMain() async {
createAutofillInfo('email', 'field1'),
fields,
)!;
// TODO(mdebbar): Things are different now. DOM elements aren't created immediately like they used to.
final formChildNodes =
autofillForm.formElement.childNodes.toList() as List<DomHTMLInputElement>;
autofillForm.formElement!.childNodes.toList() as List<DomHTMLInputElement>;
final DomHTMLInputElement username = formChildNodes[0];
final DomHTMLInputElement password = formChildNodes[1];
expect(username.name, 'username');
@ -3222,28 +3240,30 @@ Future<void> testMain() async {
expect(password.style.width, isNot('0px'));
expect(password.style.height, isNot('0px'));
expect(password.style.pointerEvents, 'none');
expect(autofillForm.formElement.style.pointerEvents, 'none');
expect(autofillForm.formElement!.style.pointerEvents, 'none');
}, skip: !isSafari);
test(
'the focused element within a form should explicitly set pointer events on Safari',
() {
final Map<String, dynamic> focusedAutofillMap = createAutofillInfo('email', 'field1');
final List<dynamic> fields = createFieldValues(
<String>['email', 'username', 'password'],
<String>['field1', 'field2', 'field3'],
);
final EngineAutofillForm autofillForm = EngineAutofillForm.fromFrameworkMessage(
kImplicitViewId,
createAutofillInfo('email', 'field1'),
focusedAutofillMap,
fields,
)!;
final DomHTMLInputElement testInputElement = createDomHTMLInputElement();
testInputElement.name = 'email';
autofillForm.placeForm(testInputElement);
final focusedAutofill = AutofillInfo.fromFrameworkMessage(focusedAutofillMap);
autofillForm.wakeUp(testInputElement, focusedAutofill);
final formChildNodes =
autofillForm.formElement.childNodes.toList() as List<DomHTMLInputElement>;
autofillForm.formElement!.childNodes.toList() as List<DomHTMLInputElement>;
final DomHTMLInputElement email = formChildNodes[0];
final DomHTMLInputElement username = formChildNodes[1];
final DomHTMLInputElement password = formChildNodes[2];
@ -3253,7 +3273,7 @@ Future<void> testMain() async {
expect(password.name, 'current-password');
// pointer events are none on the form and all non-focused elements
expect(autofillForm.formElement.style.pointerEvents, 'none');
expect(autofillForm.formElement!.style.pointerEvents, 'none');
expect(username.style.pointerEvents, 'none');
expect(password.style.pointerEvents, 'none');
@ -4026,7 +4046,7 @@ void clearForms() {
while (defaultTextEditingRoot.querySelectorAll('form').isNotEmpty) {
defaultTextEditingRoot.querySelectorAll('form').last.remove();
}
formsOnTheDom.clear();
dormantForms.clear();
}
/// Waits until the text strategy closes and moves the focus accordingly.

View File

@ -1369,6 +1369,9 @@ mixin TextInputClient {
/// This method will only be called on iOS.
void showAutocorrectionPromptRect(int start, int end);
/// Requests that this client refocus the text input control.
void refocus();
/// Platform notified framework of closed connection.
///
/// [TextInputClient] should cleanup its connection and finalize editing.
@ -2050,6 +2053,7 @@ class TextInput {
assert(_debugEnsureInputActionWorksOnPlatform(configuration.inputAction));
_currentConnection = connection;
_currentConfiguration = configuration;
_lastConnection = connection;
_setClient(connection._client, configuration);
}
@ -2079,6 +2083,7 @@ class TextInput {
TextInputConnection? _currentConnection;
late TextInputConfiguration _currentConfiguration;
TextInputConnection? _lastConnection;
final Map<String, ScribbleClient> _scribbleClients = <String, ScribbleClient>{};
bool _scribbleInProgress = false;
@ -2150,6 +2155,15 @@ class TextInput {
case 'TextInputClient.scribbleInteractionFinished':
_scribbleInProgress = false;
return;
case 'TextInput.refocus':
final args = methodCall.arguments as List<dynamic>;
final clientId = args[0] as int;
if (_currentConnection == null &&
_lastConnection != null &&
_lastConnection!._id == clientId) {
_lastConnection!._client.refocus();
}
return;
}
if (_currentConnection == null) {
return;

View File

@ -4063,6 +4063,13 @@ class EditableTextState extends State<EditableText>
}
}
@override
void refocus() {
if (mounted && !_hasFocus) {
widget.focusNode.requestFocus();
}
}
@override
void connectionClosed() {
if (_hasInputConnection) {