Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 29 additions & 18 deletions lib/web_ui/lib/src/engine/text_editing/text_editing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1077,24 +1077,35 @@ class SafariDesktopTextEditingStrategy extends DefaultTextEditingStrategy {
void placeElement() {
geometry?.applyToDomElement(activeDomElement);
if (hasAutofillGroup) {
placeForm();
// On Safari Desktop, when a form is focused, it opens an autofill menu
// immediately.
// Flutter framework sends `setEditableSizeAndTransform` for informing
// the engine about the location of the text field. This call may arrive
// after the first `show` call, depending on the text input widget's
// implementation. Therefore form is placed, when
// `setEditableSizeAndTransform` method is called and focus called on the
// form only after placing it to the correct position and only once after
// that. Calling focus multiple times causes flickering.
focusedFormElement!.focus();

// Set the last editing state if it exists, this is critical for a
// users ongoing work to continue uninterrupted when there is an update to
// the transform.
// If domElement is not focused cursor location will not be correct.
activeDomElement.focus();
lastEditingState?.applyToDomElement(activeDomElement);
// We listen to pointerdown events on the Flutter View element and programatically
// focus our inputs. However, these inputs are focused before the pointerdown
// events conclude. Thus, the browser triggers a blur event immediately after
// focusing these inputs. This causes issues with Safari Desktop's autofill
// dialog (ref: https://github.com/flutter/flutter/issues/127960).
// In order to guarantee that we only focus after the pointerdown event concludes,
// we wrap the form autofill placement and focus logic in a zero-duration Timer.
// This ensures that our input doesn't have instantaneous focus/blur events
// occur on it and fixes the autofill dialog bug as a result.
Timer(Duration.zero, () {
placeForm();
// On Safari Desktop, when a form is focused, it opens an autofill menu
// immediately.
// Flutter framework sends `setEditableSizeAndTransform` for informing
// the engine about the location of the text field. This call may arrive
// after the first `show` call, depending on the text input widget's
// implementation. Therefore form is placed, when
// `setEditableSizeAndTransform` method is called and focus called on the
// form only after placing it to the correct position and only once after
// that. Calling focus multiple times causes flickering.
focusedFormElement!.focus();

// Set the last editing state if it exists, this is critical for a
// users ongoing work to continue uninterrupted when there is an update to
// the transform.
// If domElement is not focused cursor location will not be correct.
activeDomElement.focus();
lastEditingState?.applyToDomElement(activeDomElement);
});
}
}

Expand Down
124 changes: 103 additions & 21 deletions lib/web_ui/test/engine/text_editing_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ Future<void> testMain() async {
//No-op and without crashing.
});

test('setClient, show, setEditingState, hide', () {
test('setClient, show, setEditingState, hide', () async {
final MethodCall setClient = MethodCall(
'TextInput.setClient', <dynamic>[123, flutterSinglelineConfig]);
sendFrameworkMessage(codec.encodeMethodCall(setClient));
Expand All @@ -532,6 +532,8 @@ Future<void> testMain() async {
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

await waitForDesktopSafariFocus();

checkInputEditingState(textEditing!.strategy.domElement, '', 0, 0);

const MethodCall setEditingState =
Expand All @@ -555,7 +557,7 @@ Future<void> testMain() async {
expect(spy.messages, isEmpty);
});

test('setClient, setEditingState, show, clearClient', () {
test('setClient, setEditingState, show, clearClient', () async {
final MethodCall setClient = MethodCall(
'TextInput.setClient', <dynamic>[123, flutterSinglelineConfig]);
sendFrameworkMessage(codec.encodeMethodCall(setClient));
Expand Down Expand Up @@ -583,6 +585,8 @@ Future<void> testMain() async {
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

await waitForDesktopSafariFocus();

checkInputEditingState(
textEditing!.strategy.domElement, 'abcd', 2, 3);

Expand All @@ -595,20 +599,15 @@ Future<void> testMain() async {
expect(spy.messages, isEmpty);
});

test('setClient, setEditingState, setSizeAndTransform, show - input element is put into the DOM', () {
test('setClient, setEditingState, setSizeAndTransform, show - input element is put into the DOM Safari Desktop', () async {
editingStrategy = SafariDesktopTextEditingStrategy(textEditing!);
textEditing!.debugTextEditingStrategyOverride = editingStrategy;
final MethodCall setClient = MethodCall(
'TextInput.setClient', <dynamic>[123, flutterSinglelineConfig]);
sendFrameworkMessage(codec.encodeMethodCall(setClient));

const MethodCall setEditingState =
MethodCall('TextInput.setEditingState', <String, dynamic>{
'text': 'abcd',
'selectionBase': 2,
'selectionExtent': 3,
});
sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
const MethodCall show = MethodCall('TextInput.show');
sendFrameworkMessage(codec.encodeMethodCall(show));

// Editing shouldn't have started yet.
expect(domDocument.activeElement, domDocument.body);
Expand All @@ -622,12 +621,19 @@ Future<void> testMain() async {
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

const MethodCall show = MethodCall('TextInput.show');
sendFrameworkMessage(codec.encodeMethodCall(show));
await waitForDesktopSafariFocus();

const MethodCall setEditingState =
MethodCall('TextInput.setEditingState', <String, dynamic>{
'text': 'abcd',
'selectionBase': 2,
'selectionExtent': 3,
});
sendFrameworkMessage(codec.encodeMethodCall(setEditingState));

expect(defaultTextEditingRoot.ownerDocument?.activeElement,
textEditing!.strategy.domElement);
});
}, skip: !isSafari);

test('setClient, setEditingState, show, updateConfig, clearClient', () {
final MethodCall setClient = MethodCall('TextInput.setClient', <dynamic>[
Expand Down Expand Up @@ -700,6 +706,8 @@ Future<void> testMain() async {
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

await waitForDesktopSafariFocus();

checkInputEditingState(
textEditing!.strategy.domElement, 'abcd', 2, 3);
expect(textEditing!.isEditing, isTrue);
Expand Down Expand Up @@ -796,6 +804,8 @@ Future<void> testMain() async {
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

await waitForDesktopSafariFocus();

checkInputEditingState(
textEditing!.strategy.domElement, 'abcd', 2, 3);

Expand Down Expand Up @@ -852,6 +862,8 @@ Future<void> testMain() async {
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

await waitForDesktopSafariFocus();

// Form is added to DOM.
expect(defaultTextEditingRoot.querySelectorAll('form'), isNotEmpty);

Expand Down Expand Up @@ -906,6 +918,8 @@ Future<void> testMain() async {
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

await waitForDesktopSafariFocus();

// Form is added to DOM.
expect(defaultTextEditingRoot.querySelectorAll('form'), isNotEmpty);
final DomHTMLFormElement formElement =
Expand Down Expand Up @@ -959,6 +973,8 @@ Future<void> testMain() async {
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

await waitForDesktopSafariFocus();

// Form is added to DOM.
expect(defaultTextEditingRoot.querySelectorAll('form'), isNotEmpty);
final DomHTMLFormElement formElement =
Expand All @@ -985,7 +1001,52 @@ Future<void> testMain() async {
expect(formsOnTheDom, hasLength(0));
});

test('setClient, setEditingState, show, setClient', () {
test('form is not placed and input is not focused until after tick on Desktop Safari', () async {
// Create a configuration with an AutofillGroup of four text fields.
final Map<String, dynamic> flutterMultiAutofillElementConfig =
createFlutterConfig('text',
autofillHint: 'username',
autofillHintsForFields: <String>[
'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));

final MethodCall setSizeAndTransform =
configureSetSizeAndTransformMethodCall(150, 50,
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

// Prior to tick, form should not exist and no elements should be focused.
expect(defaultTextEditingRoot.querySelectorAll('form'), isEmpty);
expect(domDocument.activeElement, domDocument.body);

await waitForDesktopSafariFocus();

// Form is added to DOM.
expect(defaultTextEditingRoot.querySelectorAll('form'), isNotEmpty);

final DomHTMLInputElement inputElement =
textEditing!.strategy.domElement! as DomHTMLInputElement;
expect(domDocument.activeElement, inputElement);
}, skip: !isSafari);

test('setClient, setEditingState, show, setClient', () async {
final MethodCall setClient = MethodCall(
'TextInput.setClient', <dynamic>[123, flutterSinglelineConfig]);
sendFrameworkMessage(codec.encodeMethodCall(setClient));
Expand Down Expand Up @@ -1013,6 +1074,8 @@ Future<void> testMain() async {
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

await waitForDesktopSafariFocus();

checkInputEditingState(
textEditing!.strategy.domElement, 'abcd', 2, 3);

Expand All @@ -1030,7 +1093,7 @@ Future<void> testMain() async {
hideKeyboard();
});

test('setClient, setEditingState, show, setEditingState, clearClient', () {
test('setClient, setEditingState, show, setEditingState, clearClient', () async {
final MethodCall setClient = MethodCall(
'TextInput.setClient', <dynamic>[123, flutterSinglelineConfig]);
sendFrameworkMessage(codec.encodeMethodCall(setClient));
Expand Down Expand Up @@ -1063,6 +1126,7 @@ Future<void> testMain() async {
});
sendFrameworkMessage(codec.encodeMethodCall(setEditingState2));

await waitForDesktopSafariFocus();
// The second [setEditingState] should override the first one.
checkInputEditingState(
textEditing!.strategy.domElement, 'xyz', 0, 2);
Expand All @@ -1076,7 +1140,7 @@ Future<void> testMain() async {

test(
'singleTextField Autofill: setClient, setEditingState, show, '
'setSizeAndTransform, setEditingState, clearClient', () {
'setSizeAndTransform, setEditingState, clearClient', () async {
// Create a configuration with focused element has autofil hint.
final Map<String, dynamic> flutterSingleAutofillElementConfig =
createFlutterConfig('text', autofillHint: 'username');
Expand Down Expand Up @@ -1104,6 +1168,7 @@ Future<void> testMain() async {
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

await waitForDesktopSafariFocus();
// The second [setEditingState] should override the first one.
checkInputEditingState(
textEditing!.strategy.domElement, 'abcd', 2, 3);
Expand All @@ -1125,7 +1190,7 @@ Future<void> testMain() async {

test(
'singleTextField Autofill setEditableSizeAndTransform preserves'
'editing state', () {
'editing state', () async {
// Create a configuration with focused element has autofil hint.
final Map<String, dynamic> flutterSingleAutofillElementConfig =
createFlutterConfig('text', autofillHint: 'username');
Expand Down Expand Up @@ -1174,6 +1239,7 @@ Future<void> testMain() async {
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
sendFrameworkMessage(codec.encodeMethodCall(updateSizeAndTransform));

await waitForDesktopSafariFocus();
// Check the element still has focus. User can keep editing.
expect(defaultTextEditingRoot.ownerDocument?.activeElement,
textEditing!.strategy.domElement);
Expand All @@ -1194,7 +1260,7 @@ Future<void> testMain() async {

test(
'multiTextField Autofill: setClient, setEditingState, show, '
'setSizeAndTransform setEditingState, clearClient', () {
'setSizeAndTransform setEditingState, clearClient', () async {
// Create a configuration with an AutofillGroup of four text fields.
final Map<String, dynamic> flutterMultiAutofillElementConfig =
createFlutterConfig('text',
Expand Down Expand Up @@ -1229,6 +1295,8 @@ Future<void> testMain() async {
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

await waitForDesktopSafariFocus();

// The second [setEditingState] should override the first one.
checkInputEditingState(
textEditing!.strategy.domElement, 'abcd', 2, 3);
Expand Down Expand Up @@ -1514,7 +1582,7 @@ Future<void> testMain() async {

test(
'negative base offset and selection extent values in editing state is handled',
() {
() async {
final MethodCall setClient = MethodCall(
'TextInput.setClient', <dynamic>[123, flutterSinglelineConfig]);
sendFrameworkMessage(codec.encodeMethodCall(setClient));
Expand All @@ -1539,6 +1607,8 @@ Future<void> testMain() async {
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

await waitForDesktopSafariFocus();

// Check if the selection range is correct.
checkInputEditingState(
textEditing!.strategy.domElement, 'xyz', 1, 2);
Expand Down Expand Up @@ -1687,7 +1757,7 @@ Future<void> testMain() async {
hideKeyboard();
});

test('multiTextField Autofill sync updates back to Flutter', () {
test('multiTextField Autofill sync updates back to Flutter', () async {
// Create a configuration with an AutofillGroup of four text fields.
const String hintForFirstElement = 'familyName';
final Map<String, dynamic> flutterMultiAutofillElementConfig =
Expand Down Expand Up @@ -1723,6 +1793,7 @@ Future<void> testMain() async {
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

await waitForDesktopSafariFocus();
// The second [setEditingState] should override the first one.
checkInputEditingState(
textEditing!.strategy.domElement, 'abcd', 2, 3);
Expand Down Expand Up @@ -1769,7 +1840,7 @@ Future<void> testMain() async {
hideKeyboard();
});

test('Multi-line mode also works', () {
test('Multi-line mode also works', () async {
final MethodCall setClient = MethodCall(
'TextInput.setClient', <dynamic>[123, flutterMultilineConfig]);
sendFrameworkMessage(codec.encodeMethodCall(setClient));
Expand All @@ -1790,6 +1861,8 @@ Future<void> testMain() async {
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

await waitForDesktopSafariFocus();

final DomHTMLTextAreaElement textarea = textEditing!.strategy.domElement!
as DomHTMLTextAreaElement;
checkTextAreaEditingState(textarea, '', 0, 0);
Expand Down Expand Up @@ -2868,3 +2941,12 @@ void clearForms() {
}
formsOnTheDom.clear();
}

/// On Desktop Safari, the editing element is focused after a zero-duration timer
/// to prevent autofill popup flickering. We must wait a tick for this placement
/// before referencing these elements.
Future<void> waitForDesktopSafariFocus() async {
if (textEditing.strategy is SafariDesktopTextEditingStrategy) {
await Future<void>.delayed(Duration.zero);
}
}