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 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d2e4ccc
Make sure the elements can't be reached by keyboard
tugorez Apr 23, 2024
ea09d6e
Move the focus to the <flutter-view /> instead of blur.
tugorez Apr 23, 2024
edab1dc
Add listener to the subscription (otherwise never gets cleaned up).
tugorez May 10, 2024
8bbc206
Delay the input removal
tugorez May 10, 2024
ed2f851
Prevent default on pointer down on flutter views
tugorez May 10, 2024
78521ec
Format
tugorez May 10, 2024
72d3956
Add a test that checks focus goes from one input to another
tugorez May 10, 2024
7a6c596
Remove the blur listeners
tugorez May 13, 2024
11b7dae
Enable view focus binding
tugorez May 14, 2024
df75938
Disable view focus binding.
tugorez May 14, 2024
edfdacf
Add ios awaits
tugorez May 14, 2024
7daf2a1
Remove safari desktop delay
tugorez May 17, 2024
7e811da
Remove spaces
tugorez May 17, 2024
c23c944
Bring blur handlers back
tugorez May 18, 2024
08ab966
Format
tugorez May 18, 2024
8c99cb4
Add mising blur handler
tugorez May 18, 2024
153eee4
Remove blur events again lol
tugorez May 18, 2024
e8cf8e8
Prevent scroll on focus
tugorez May 18, 2024
5cf50ac
Make the linter happy
tugorez May 20, 2024
6f85439
Disable view focus
tugorez May 20, 2024
e931fbc
Formatting
tugorez May 20, 2024
d70596d
Refactor focus active dom element
tugorez May 20, 2024
e5af604
Enable view focus binding
tugorez May 20, 2024
e54f9ad
Use handleBlur
tugorez Jun 5, 2024
9a194d3
Apply feedback
tugorez Jun 6, 2024
dca6a02
Apply feedback
tugorez Jun 6, 2024
318a2dd
Apply feedback
tugorez Jun 6, 2024
2a6c339
Merge branch 'main' into focus-management-input
ditman Jun 13, 2024
e35705e
Do not attach a blur handler on Safari
ditman Jun 18, 2024
39c5c8d
Makes shiftKey in DomKeyboardEvent nullable.
ditman Jun 18, 2024
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
Prev Previous commit
Next Next commit
Remove safari desktop delay
  • Loading branch information
tugorez committed May 17, 2024
commit 7daf2a1963218bf904f5d68d2ccd5cdc295ae978
47 changes: 18 additions & 29 deletions lib/web_ui/lib/src/engine/text_editing/text_editing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1165,35 +1165,24 @@ class SafariDesktopTextEditingStrategy extends DefaultTextEditingStrategy {
void placeElement() {
geometry?.applyToDomElement(activeDomElement);
if (hasAutofillGroup) {
// 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);
});
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
111 changes: 11 additions & 100 deletions lib/web_ui/test/engine/text_editing_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -728,8 +728,6 @@ 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 Down Expand Up @@ -789,8 +787,6 @@ 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 @@ -831,8 +827,6 @@ Future<void> testMain() async {
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

await waitForDesktopSafariFocus();

const MethodCall setEditingState =
MethodCall('TextInput.setEditingState', <String, dynamic>{
'text': 'abcd',
Expand Down Expand Up @@ -918,8 +912,6 @@ 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 @@ -1014,8 +1006,6 @@ 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 @@ -1072,8 +1062,6 @@ 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 @@ -1128,8 +1116,6 @@ 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 @@ -1183,8 +1169,6 @@ 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 @@ -1211,51 +1195,6 @@ Future<void> testMain() async {
expect(formsOnTheDom, hasLength(0));
});

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('Moves the focus across input elements', () async {
final List<DomEvent> focusinEvents = <DomEvent>[];
final DomEventListener handleFocusIn = createDomEventListener(focusinEvents.add);
Expand All @@ -1274,29 +1213,35 @@ Future<void> testMain() async {
'selectionBase': 2,
'selectionExtent': 3,
});
const MethodCall clearClient = MethodCall('TextInput.clearClient');
final MethodCall setSizeAndTransform = configureSetSizeAndTransformMethodCall(
150,
50,
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList(),
);
const MethodCall show = MethodCall('TextInput.show');

const MethodCall clearClient = MethodCall('TextInput.clearClient');

domDocument.body!.addEventListener('focusin', handleFocusIn);
sendFrameworkMessage(codec.encodeMethodCall(setClient1));
sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
sendFrameworkMessage(codec.encodeMethodCall(show));
await waitForDesktopSafariFocus();
final DomElement firstInput = textEditing!.strategy.domElement!;
expect(domDocument.activeElement, firstInput);

sendFrameworkMessage(codec.encodeMethodCall(setClient2));
sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
sendFrameworkMessage(codec.encodeMethodCall(show));
await waitForDesktopSafariFocus();
final DomElement secondInput = textEditing!.strategy.domElement!;
expect(domDocument.activeElement, secondInput);
expect(firstInput, isNot(secondInput));

sendFrameworkMessage(codec.encodeMethodCall(clearClient));
await waitForTextStrategyStopPropagation();
domDocument.body!.removeEventListener('focusin', handleFocusIn);

expect(focusinEvents, hasLength(3));
expect(firstInput, isNot(secondInput));
expect(focusinEvents[0].target, firstInput);
expect(focusinEvents[1].target, secondInput);
expect(focusinEvents[2].target, implicitViewRootElement);
Expand Down Expand Up @@ -1333,7 +1278,6 @@ 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 @@ -1389,7 +1333,6 @@ 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 Down Expand Up @@ -1431,7 +1374,6 @@ 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 @@ -1502,7 +1444,6 @@ 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 Down Expand Up @@ -1558,7 +1499,6 @@ 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(
Expand Down Expand Up @@ -1870,8 +1810,6 @@ 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 @@ -2045,8 +1983,6 @@ Future<void> testMain() async {
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

await waitForDesktopSafariFocus();

final DomHTMLInputElement input = textEditing!.strategy.domElement! as
DomHTMLInputElement;

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

await waitForDesktopSafariFocus();

final DomHTMLInputElement input = textEditing!.strategy.domElement! as
DomHTMLInputElement;

Expand Down Expand Up @@ -2206,7 +2140,6 @@ 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 @@ -2276,8 +2209,6 @@ 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 @@ -2374,8 +2305,6 @@ Future<void> testMain() async {
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

await waitForDesktopSafariFocus();

expect(textEditing!.strategy.domElement!.tagName, 'INPUT');
expect(getEditingInputMode(), 'none');
});
Expand All @@ -2397,8 +2326,6 @@ Future<void> testMain() async {
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

await waitForDesktopSafariFocus();

expect(textEditing!.strategy.domElement!.tagName, 'TEXTAREA');
expect(getEditingInputMode(), 'none');
});
Expand Down Expand Up @@ -2635,11 +2562,8 @@ Future<void> testMain() async {
final MethodCall setSizeAndTransform = configureSetSizeAndTransformMethodCall(10, 10, transform);
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

await waitForDesktopSafariFocus();

final DomElement input = textEditing!.strategy.domElement!;


// Input is appended to the right view.
expect(view.dom.textEditingHost.contains(input), isTrue);

Expand Down Expand Up @@ -2719,8 +2643,6 @@ Future<void> testMain() async {
final MethodCall setSizeAndTransform = configureSetSizeAndTransformMethodCall(10, 10, transform);
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

await waitForDesktopSafariFocus();

final DomElement input = textEditing!.strategy.domElement!;
final DomElement form = textEditing!.configuration!.autofillGroup!.formElement;

Expand Down Expand Up @@ -2764,8 +2686,6 @@ Future<void> testMain() async {
const MethodCall show = MethodCall('TextInput.show');
sendFrameworkMessage(codec.encodeMethodCall(show));

await waitForDesktopSafariFocus();

final DomElement input = textEditing!.strategy.domElement!;
final DomElement form = textEditing!.configuration!.autofillGroup!.formElement;

Expand Down Expand Up @@ -3807,15 +3727,6 @@ 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);
}
}

/// After stopped the focus remains on the input element to give the engine
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence seems to be missing a word or something. Specifically: I understand everything starting from "the focus remains ...". But I couldn't understand "After stopped".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PTAL

/// an oportunity to move the focus to the right element before sending it back
/// to the flutter view. This helps preventing the keyboard from jumping when the focus
Expand Down