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
22 changes: 21 additions & 1 deletion lib/web_ui/lib/src/engine/text_editing/text_editing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1389,6 +1389,17 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
Timer? _positionInputElementTimer;
static const Duration _delayBeforePlacement = Duration(milliseconds: 100);

/// This interval between the blur subscription and callback is considered to
/// be fast.
///
/// This is only used for iOS. The blur callback may trigger as soon as the
/// creation of the subscription. Occasionally in this case, the virtual
/// keyboard will quickly show and hide again.
///
/// Less than this interval allows the virtual keyboard to keep showing up
/// instead of hiding rapidly.
static const Duration _blurFastCallbackInterval = Duration(milliseconds: 200);

/// Whether or not the input element can be positioned at this point in time.
///
/// This is currently only used in iOS. It's set to false before focusing the
Expand Down Expand Up @@ -1453,6 +1464,9 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy {

_addTapListener();

// Record start time of blur subscription.
final Stopwatch blurWatch = Stopwatch()..start();

// On iOS, blur is trigerred in the following cases:
//
// 1. The browser app is sent to the background (or the tab is changed). In
Expand All @@ -1464,8 +1478,14 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
// programmatically, so we end up refocusing the input field. This is
// okay because the virtual keyboard will hide, and as soon as the user
// taps the text field again, the virtual keyboard will come up.
// 4. Safari sometimes sends a blur event immediately after activating the
// input field. In this case, we want to keep the focus on the input field.
// In order to detect this, we measure how much time has passed since the
// input field was activated. If the time is too short, we re-focus the
// input element.
subscriptions.add(activeDomElement.onBlur.listen((_) {
if (windowHasFocus) {
final bool isFastCallback = blurWatch.elapsed < _blurFastCallbackInterval;
if (windowHasFocus && isFastCallback) {
activeDomElement.focus();
} else {
owner.sendTextConnectionClosedToFrameworkIfAny();
Expand Down
49 changes: 49 additions & 0 deletions lib/web_ui/test/text_editing_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,55 @@ void testMain() {
// TODO(mdebbar): https://github.com/flutter/flutter/issues/50769
skip: browserEngine == BrowserEngine.edge);

test('focus and disconnection with delaying blur in iOS', () async {
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));

// Editing shouldn't have started yet.
expect(defaultTextEditingRoot.activeElement, null);

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

// The "setSizeAndTransform" message has to be here before we call
// checkInputEditingState, since on some platforms (e.g. Desktop Safari)
// we don't put the input element into the DOM until we get its correct
// dimensions from the framework.
final MethodCall setSizeAndTransform =
configureSetSizeAndTransformMethodCall(150, 50,
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));

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

// Delay for not to be a fast callback with blur.
await Future<void>.delayed(const Duration(milliseconds: 200));
// DOM element is blurred.
textEditing!.strategy.domElement!.blur();

expect(spy.messages, hasLength(1));
expect(spy.messages[0].channel, 'flutter/textinput');
expect(
spy.messages[0].methodName, 'TextInputClient.onConnectionClosed');
await Future<void>.delayed(Duration.zero);
// DOM element loses the focus.
expect(defaultTextEditingRoot.activeElement, null);
},
// Test on ios-safari only.
skip: browserEngine != BrowserEngine.webkit ||
operatingSystem != OperatingSystem.iOs);

test('finishAutofillContext closes connection no autofill element',
() async {
final MethodCall setClient = MethodCall(
Expand Down