Skip to content

Commit a00ba24

Browse files
authored
Fix done button click not blur in iOS keyboard (flutter#31718)
1 parent 09d7bcc commit a00ba24

File tree

2 files changed

+70
-1
lines changed

2 files changed

+70
-1
lines changed

lib/web_ui/lib/src/engine/text_editing/text_editing.dart

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1389,6 +1389,17 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
13891389
Timer? _positionInputElementTimer;
13901390
static const Duration _delayBeforePlacement = Duration(milliseconds: 100);
13911391

1392+
/// This interval between the blur subscription and callback is considered to
1393+
/// be fast.
1394+
///
1395+
/// This is only used for iOS. The blur callback may trigger as soon as the
1396+
/// creation of the subscription. Occasionally in this case, the virtual
1397+
/// keyboard will quickly show and hide again.
1398+
///
1399+
/// Less than this interval allows the virtual keyboard to keep showing up
1400+
/// instead of hiding rapidly.
1401+
static const Duration _blurFastCallbackInterval = Duration(milliseconds: 200);
1402+
13921403
/// Whether or not the input element can be positioned at this point in time.
13931404
///
13941405
/// This is currently only used in iOS. It's set to false before focusing the
@@ -1453,6 +1464,9 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
14531464

14541465
_addTapListener();
14551466

1467+
// Record start time of blur subscription.
1468+
final Stopwatch blurWatch = Stopwatch()..start();
1469+
14561470
// On iOS, blur is trigerred in the following cases:
14571471
//
14581472
// 1. The browser app is sent to the background (or the tab is changed). In
@@ -1464,8 +1478,14 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
14641478
// programmatically, so we end up refocusing the input field. This is
14651479
// okay because the virtual keyboard will hide, and as soon as the user
14661480
// taps the text field again, the virtual keyboard will come up.
1481+
// 4. Safari sometimes sends a blur event immediately after activating the
1482+
// input field. In this case, we want to keep the focus on the input field.
1483+
// In order to detect this, we measure how much time has passed since the
1484+
// input field was activated. If the time is too short, we re-focus the
1485+
// input element.
14671486
subscriptions.add(activeDomElement.onBlur.listen((_) {
1468-
if (windowHasFocus) {
1487+
final bool isFastCallback = blurWatch.elapsed < _blurFastCallbackInterval;
1488+
if (windowHasFocus && isFastCallback) {
14691489
activeDomElement.focus();
14701490
} else {
14711491
owner.sendTextConnectionClosedToFrameworkIfAny();

lib/web_ui/test/text_editing_test.dart

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,55 @@ void testMain() {
675675
// TODO(mdebbar): https://github.com/flutter/flutter/issues/50769
676676
skip: browserEngine == BrowserEngine.edge);
677677

678+
test('focus and disconnection with delaying blur in iOS', () async {
679+
final MethodCall setClient = MethodCall(
680+
'TextInput.setClient', <dynamic>[123, flutterSinglelineConfig]);
681+
sendFrameworkMessage(codec.encodeMethodCall(setClient));
682+
683+
const MethodCall setEditingState =
684+
MethodCall('TextInput.setEditingState', <String, dynamic>{
685+
'text': 'abcd',
686+
'selectionBase': 2,
687+
'selectionExtent': 3,
688+
});
689+
sendFrameworkMessage(codec.encodeMethodCall(setEditingState));
690+
691+
// Editing shouldn't have started yet.
692+
expect(defaultTextEditingRoot.activeElement, null);
693+
694+
const MethodCall show = MethodCall('TextInput.show');
695+
sendFrameworkMessage(codec.encodeMethodCall(show));
696+
697+
// The "setSizeAndTransform" message has to be here before we call
698+
// checkInputEditingState, since on some platforms (e.g. Desktop Safari)
699+
// we don't put the input element into the DOM until we get its correct
700+
// dimensions from the framework.
701+
final MethodCall setSizeAndTransform =
702+
configureSetSizeAndTransformMethodCall(150, 50,
703+
Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
704+
sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
705+
706+
checkInputEditingState(
707+
textEditing!.strategy.domElement, 'abcd', 2, 3);
708+
expect(textEditing!.isEditing, isTrue);
709+
710+
// Delay for not to be a fast callback with blur.
711+
await Future<void>.delayed(const Duration(milliseconds: 200));
712+
// DOM element is blurred.
713+
textEditing!.strategy.domElement!.blur();
714+
715+
expect(spy.messages, hasLength(1));
716+
expect(spy.messages[0].channel, 'flutter/textinput');
717+
expect(
718+
spy.messages[0].methodName, 'TextInputClient.onConnectionClosed');
719+
await Future<void>.delayed(Duration.zero);
720+
// DOM element loses the focus.
721+
expect(defaultTextEditingRoot.activeElement, null);
722+
},
723+
// Test on ios-safari only.
724+
skip: browserEngine != BrowserEngine.webkit ||
725+
operatingSystem != OperatingSystem.iOs);
726+
678727
test('finishAutofillContext closes connection no autofill element',
679728
() async {
680729
final MethodCall setClient = MethodCall(

0 commit comments

Comments
 (0)