mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] fixing text editing for autofill with semantics (flutter/engine#23361)
* fixing text editing for autofill with semantics * fix tests. add tests. unskip tests * removing new. adding issue to comment
This commit is contained in:
parent
90fd5c978f
commit
125efacea5
@ -927,6 +927,7 @@ class SemanticsObject {
|
||||
..height = '${rect.height}px';
|
||||
}
|
||||
} else {
|
||||
// TODO: https://github.com/flutter/flutter/issues/73347
|
||||
if (isDesktop) {
|
||||
element.style
|
||||
..removeProperty('transform-origin')
|
||||
|
||||
@ -15,12 +15,16 @@ part of engine;
|
||||
/// This class is still responsible for hooking up the DOM element with the
|
||||
/// [HybridTextEditing] instance so that changes are communicated to Flutter.
|
||||
class SemanticsTextEditingStrategy extends DefaultTextEditingStrategy {
|
||||
/// The semantics object which this text editing element belongs to.
|
||||
final SemanticsObject semanticsObject;
|
||||
|
||||
/// Creates a [SemanticsTextEditingStrategy] that eagerly instantiates
|
||||
/// [domElement] so the caller can insert it before calling
|
||||
/// [SemanticsTextEditingStrategy.enable].
|
||||
SemanticsTextEditingStrategy(
|
||||
SemanticsTextEditingStrategy(SemanticsObject semanticsObject,
|
||||
HybridTextEditing owner, html.HtmlElement domElement)
|
||||
: super(owner) {
|
||||
: this.semanticsObject = semanticsObject,
|
||||
super(owner) {
|
||||
// Make sure the DOM element is of a type that we support for text editing.
|
||||
// TODO(yjbanov): move into initializer list when https://github.com/dart-lang/sdk/issues/37881 is fixed.
|
||||
assert((domElement is html.InputElement) ||
|
||||
@ -44,6 +48,20 @@ class SemanticsTextEditingStrategy extends DefaultTextEditingStrategy {
|
||||
_subscriptions.clear();
|
||||
_lastEditingState = null;
|
||||
|
||||
// If focused element is a part of a form, it needs to stay on the DOM
|
||||
// until the autofill context of the form is finalized.
|
||||
// More details on `TextInput.finishAutofillContext` call.
|
||||
if (_appendedToForm &&
|
||||
_inputConfiguration.autofillGroup?.formElement != null) {
|
||||
// We want to save the domElement with the form. However we still
|
||||
// need to keep the text editing domElement attached to the semantics
|
||||
// tree. In order to simplify the logic we will create a clone of the
|
||||
// element.
|
||||
final html.Node textFieldClone = domElement.clone(false);
|
||||
domElement = textFieldClone as html.HtmlElement;
|
||||
_inputConfiguration.autofillGroup?.storeForm();
|
||||
}
|
||||
|
||||
// If the text element still has focus, remove focus from the editable
|
||||
// element to cause the keyboard to hide.
|
||||
// Otherwise, the keyboard stays on screen even when the user navigates to
|
||||
@ -87,6 +105,7 @@ class SemanticsTextEditingStrategy extends DefaultTextEditingStrategy {
|
||||
_inputConfiguration = inputConfig;
|
||||
_onChange = onChange;
|
||||
_onAction = onAction;
|
||||
_applyConfiguration(inputConfig);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -96,6 +115,25 @@ class SemanticsTextEditingStrategy extends DefaultTextEditingStrategy {
|
||||
// Refocus after setting editing state.
|
||||
domElement.focus();
|
||||
}
|
||||
|
||||
@override
|
||||
void placeElement() {
|
||||
// If this text editing element is a part of an autofill group.
|
||||
if (hasAutofillGroup) {
|
||||
placeForm();
|
||||
}
|
||||
domElement.focus();
|
||||
}
|
||||
|
||||
@override
|
||||
void placeForm() {
|
||||
// Switch domElement's parent from semantics object to form.
|
||||
domElement.remove();
|
||||
_inputConfiguration.autofillGroup!.formElement.append(domElement);
|
||||
semanticsObject.element
|
||||
.append(_inputConfiguration.autofillGroup!.formElement);
|
||||
_appendedToForm = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages semantics objects that represent editable text fields.
|
||||
@ -114,6 +152,7 @@ class TextField extends RoleManager {
|
||||
? html.TextAreaElement()
|
||||
: html.InputElement();
|
||||
textEditingElement = SemanticsTextEditingStrategy(
|
||||
semanticsObject,
|
||||
textEditing,
|
||||
editableDomElement,
|
||||
);
|
||||
@ -191,8 +230,8 @@ class TextField extends RoleManager {
|
||||
void _initializeForWebkit() {
|
||||
// Safari for desktop is also initialized as the other browsers.
|
||||
if (operatingSystem == OperatingSystem.macOs) {
|
||||
_initializeForBlink();
|
||||
return;
|
||||
_initializeForBlink();
|
||||
return;
|
||||
}
|
||||
num? lastTouchStartOffsetX;
|
||||
num? lastTouchStartOffsetY;
|
||||
|
||||
@ -365,6 +365,13 @@ void testMain() {
|
||||
InputElement testInputElement;
|
||||
HybridTextEditing testTextEditing;
|
||||
|
||||
final PlatformMessagesSpy spy = PlatformMessagesSpy();
|
||||
|
||||
/// Emulates sending of a message by the framework to the engine.
|
||||
void sendFrameworkMessage(dynamic message) {
|
||||
textEditing.channel.handleTextInput(message, (ByteData data) {});
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
testInputElement = InputElement();
|
||||
testTextEditing = HybridTextEditing();
|
||||
@ -375,18 +382,77 @@ void testMain() {
|
||||
testInputElement = null;
|
||||
});
|
||||
|
||||
test('autofill form lifecycle works', () async {
|
||||
editingElement = SemanticsTextEditingStrategy(
|
||||
SemanticsObject(5, null), testTextEditing, testInputElement);
|
||||
// Create a configuration with an AutofillGroup of four text fields.
|
||||
final Map<String, dynamic> flutterMultiAutofillElementConfig =
|
||||
createFlutterConfig('text',
|
||||
autofillHint: 'username',
|
||||
autofillHintsForFields: [
|
||||
'username',
|
||||
'email',
|
||||
'name',
|
||||
'telephoneNumber'
|
||||
]);
|
||||
final MethodCall setClient = MethodCall('TextInput.setClient',
|
||||
<dynamic>[123, flutterMultiAutofillElementConfig]);
|
||||
sendFrameworkMessage(codec.encodeMethodCall(setClient));
|
||||
|
||||
const MethodCall setEditingState1 =
|
||||
MethodCall('TextInput.setEditingState', <String, dynamic>{
|
||||
'text': 'abcd',
|
||||
'selectionBase': 2,
|
||||
'selectionExtent': 3,
|
||||
});
|
||||
sendFrameworkMessage(codec.encodeMethodCall(setEditingState1));
|
||||
|
||||
const MethodCall show = MethodCall('TextInput.show');
|
||||
sendFrameworkMessage(codec.encodeMethodCall(show));
|
||||
|
||||
// The transform is changed. For example after a validation error, red
|
||||
// line appeared under the input field.
|
||||
final MethodCall setSizeAndTransform =
|
||||
configureSetSizeAndTransformMethodCall(150, 50,
|
||||
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
|
||||
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
|
||||
|
||||
// Form is added to DOM.
|
||||
expect(document.getElementsByTagName('form'), isNotEmpty);
|
||||
|
||||
const MethodCall clearClient = MethodCall('TextInput.clearClient');
|
||||
sendFrameworkMessage(codec.encodeMethodCall(clearClient));
|
||||
|
||||
// Confirm that [HybridTextEditing] didn't send any messages.
|
||||
expect(spy.messages, isEmpty);
|
||||
// Form stays on the DOM until autofill context is finalized.
|
||||
expect(document.getElementsByTagName('form'), isNotEmpty);
|
||||
expect(formsOnTheDom, hasLength(1));
|
||||
|
||||
const MethodCall finishAutofillContext =
|
||||
MethodCall('TextInput.finishAutofillContext', false);
|
||||
sendFrameworkMessage(codec.encodeMethodCall(finishAutofillContext));
|
||||
|
||||
// Form element is removed from DOM.
|
||||
expect(document.getElementsByTagName('form'), hasLength(0));
|
||||
expect(formsOnTheDom, hasLength(0));
|
||||
},
|
||||
// TODO(nurhan): https://github.com/flutter/flutter/issues/50769
|
||||
skip: browserEngine == BrowserEngine.edge);
|
||||
|
||||
test('Does not accept dom elements of a wrong type', () {
|
||||
// A regular <span> shouldn't be accepted.
|
||||
final HtmlElement span = SpanElement();
|
||||
expect(
|
||||
() => SemanticsTextEditingStrategy(HybridTextEditing(), span),
|
||||
() => SemanticsTextEditingStrategy(
|
||||
SemanticsObject(5, null), HybridTextEditing(), span),
|
||||
throwsAssertionError,
|
||||
);
|
||||
});
|
||||
|
||||
test('Do not re-acquire focus', () {
|
||||
editingElement =
|
||||
SemanticsTextEditingStrategy(HybridTextEditing(), testInputElement);
|
||||
editingElement = SemanticsTextEditingStrategy(
|
||||
SemanticsObject(5, null), HybridTextEditing(), testInputElement);
|
||||
|
||||
expect(document.activeElement, document.body);
|
||||
|
||||
@ -408,8 +474,8 @@ void testMain() {
|
||||
skip: browserEngine == BrowserEngine.edge);
|
||||
|
||||
test('Does not dispose and recreate dom elements in persistent mode', () {
|
||||
editingElement =
|
||||
SemanticsTextEditingStrategy(HybridTextEditing(), testInputElement);
|
||||
editingElement = SemanticsTextEditingStrategy(
|
||||
SemanticsObject(5, null), HybridTextEditing(), testInputElement);
|
||||
|
||||
// The DOM element should've been eagerly created.
|
||||
expect(testInputElement, isNotNull);
|
||||
@ -454,8 +520,8 @@ void testMain() {
|
||||
skip: browserEngine == BrowserEngine.edge);
|
||||
|
||||
test('Refocuses when setting editing state', () {
|
||||
editingElement =
|
||||
SemanticsTextEditingStrategy(HybridTextEditing(), testInputElement);
|
||||
editingElement = SemanticsTextEditingStrategy(
|
||||
SemanticsObject(5, null), HybridTextEditing(), testInputElement);
|
||||
|
||||
document.body.append(testInputElement);
|
||||
editingElement.enable(
|
||||
@ -474,8 +540,8 @@ void testMain() {
|
||||
|
||||
test('Works in multi-line mode', () {
|
||||
final TextAreaElement textarea = TextAreaElement();
|
||||
editingElement =
|
||||
SemanticsTextEditingStrategy(HybridTextEditing(), textarea);
|
||||
editingElement = SemanticsTextEditingStrategy(
|
||||
SemanticsObject(5, null), HybridTextEditing(), textarea);
|
||||
|
||||
expect(editingElement.domElement, textarea);
|
||||
expect(document.activeElement, document.body);
|
||||
@ -731,8 +797,8 @@ void testMain() {
|
||||
operatingSystem == OperatingSystem.iOs) {
|
||||
expect(spy.messages, hasLength(1));
|
||||
expect(spy.messages[0].channel, 'flutter/textinput');
|
||||
expect(spy.messages[0].methodName,
|
||||
'TextInputClient.onConnectionClosed');
|
||||
expect(
|
||||
spy.messages[0].methodName, 'TextInputClient.onConnectionClosed');
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
// DOM element loses the focus.
|
||||
expect(document.activeElement, document.body);
|
||||
@ -787,10 +853,8 @@ void testMain() {
|
||||
// Input element is removed from DOM.
|
||||
expect(document.getElementsByTagName('input'), hasLength(0));
|
||||
},
|
||||
// TODO(nurhan): https://github.com/flutter/flutter/issues/50590
|
||||
// TODO(nurhan): https://github.com/flutter/flutter/issues/50769
|
||||
skip: (browserEngine == BrowserEngine.webkit ||
|
||||
browserEngine == BrowserEngine.edge));
|
||||
skip: browserEngine == BrowserEngine.edge);
|
||||
|
||||
test('finishAutofillContext removes form from DOM', () async {
|
||||
// Create a configuration with an AutofillGroup of four text fields.
|
||||
@ -818,6 +882,13 @@ void testMain() {
|
||||
const MethodCall show = MethodCall('TextInput.show');
|
||||
sendFrameworkMessage(codec.encodeMethodCall(show));
|
||||
|
||||
// The transform is changed. For example after a validation error, red
|
||||
// line appeared under the input field.
|
||||
final MethodCall setSizeAndTransform =
|
||||
configureSetSizeAndTransformMethodCall(150, 50,
|
||||
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
|
||||
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
|
||||
|
||||
// Form is added to DOM.
|
||||
expect(document.getElementsByTagName('form'), isNotEmpty);
|
||||
|
||||
@ -838,10 +909,8 @@ void testMain() {
|
||||
expect(document.getElementsByTagName('form'), hasLength(0));
|
||||
expect(formsOnTheDom, hasLength(0));
|
||||
},
|
||||
// TODO(nurhan): https://github.com/flutter/flutter/issues/50590
|
||||
// TODO(nurhan): https://github.com/flutter/flutter/issues/50769
|
||||
skip: (browserEngine == BrowserEngine.webkit ||
|
||||
browserEngine == BrowserEngine.edge));
|
||||
skip: browserEngine == BrowserEngine.edge);
|
||||
|
||||
test('finishAutofillContext with save submits forms', () async {
|
||||
// Create a configuration with an AutofillGroup of four text fields.
|
||||
@ -869,6 +938,13 @@ void testMain() {
|
||||
const MethodCall show = MethodCall('TextInput.show');
|
||||
sendFrameworkMessage(codec.encodeMethodCall(show));
|
||||
|
||||
// The transform is changed. For example after a validation error, red
|
||||
// line appeared under the input field.
|
||||
final MethodCall setSizeAndTransform =
|
||||
configureSetSizeAndTransformMethodCall(150, 50,
|
||||
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
|
||||
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
|
||||
|
||||
// Form is added to DOM.
|
||||
expect(document.getElementsByTagName('form'), isNotEmpty);
|
||||
FormElement formElement = document.getElementsByTagName('form')[0];
|
||||
@ -886,10 +962,8 @@ void testMain() {
|
||||
// `submit` action is called on form.
|
||||
await expectLater(await submittedForm.future, true);
|
||||
},
|
||||
// TODO(nurhan): https://github.com/flutter/flutter/issues/50590
|
||||
// TODO(nurhan): https://github.com/flutter/flutter/issues/50769
|
||||
skip: (browserEngine == BrowserEngine.webkit ||
|
||||
browserEngine == BrowserEngine.edge));
|
||||
skip: browserEngine == BrowserEngine.edge);
|
||||
|
||||
test('forms submits for focused input', () async {
|
||||
// Create a configuration with an AutofillGroup of four text fields.
|
||||
@ -917,6 +991,13 @@ void testMain() {
|
||||
const MethodCall show = MethodCall('TextInput.show');
|
||||
sendFrameworkMessage(codec.encodeMethodCall(show));
|
||||
|
||||
// The transform is changed. For example after a validation error, red
|
||||
// line appeared under the input field.
|
||||
final MethodCall setSizeAndTransform =
|
||||
configureSetSizeAndTransformMethodCall(150, 50,
|
||||
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
|
||||
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
|
||||
|
||||
// Form is added to DOM.
|
||||
expect(document.getElementsByTagName('form'), isNotEmpty);
|
||||
FormElement formElement = document.getElementsByTagName('form')[0];
|
||||
@ -940,10 +1021,8 @@ void testMain() {
|
||||
expect(document.getElementsByTagName('form'), hasLength(0));
|
||||
expect(formsOnTheDom, hasLength(0));
|
||||
},
|
||||
// TODO(nurhan): https://github.com/flutter/flutter/issues/50590
|
||||
// TODO(nurhan): https://github.com/flutter/flutter/issues/50769
|
||||
skip: (browserEngine == BrowserEngine.webkit ||
|
||||
browserEngine == BrowserEngine.edge));
|
||||
skip: browserEngine == BrowserEngine.edge);
|
||||
|
||||
test('setClient, setEditingState, show, setClient', () {
|
||||
final MethodCall setClient = MethodCall(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user