mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Merge 8a6b584671efa92e3a65eb0480d5b847a092e37a into 06df71c51446e96939c6a615b7c34ce9123806ba
This commit is contained in:
commit
0fc2499dec
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -4063,6 +4063,13 @@ class EditableTextState extends State<EditableText>
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void refocus() {
|
||||
if (mounted && !_hasFocus) {
|
||||
widget.focusNode.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void connectionClosed() {
|
||||
if (_hasInputConnection) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user