[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:
nturgut 2021-01-05 12:08:09 -08:00 committed by GitHub
parent 90fd5c978f
commit 125efacea5
3 changed files with 146 additions and 27 deletions

View File

@ -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')

View File

@ -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;

View File

@ -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(