diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 1f7918577f932..0b69a49b0444a 100644 --- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -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 @@ -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 @@ -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(); diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index 486b3e87c57fc..6552d01dc1268 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -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', [123, flutterSinglelineConfig]); + sendFrameworkMessage(codec.encodeMethodCall(setClient)); + + const MethodCall setEditingState = + MethodCall('TextInput.setEditingState', { + '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.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.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(