Skip to content

[web] Keep the keyboard up during an iOS caret drag - #190014

Merged
auto-submit[bot] merged 13 commits into
flutter:masterfrom
flutter-zl:issue_189744_fix
Aug 10, 2026
Merged

[web] Keep the keyboard up during an iOS caret drag#190014
auto-submit[bot] merged 13 commits into
flutter:masterfrom
flutter-zl:issue_189744_fix

Conversation

@flutter-zl

@flutter-zlflutter-zl commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Fixes#189744

Problem
On iOS 27, long-press-dragging the selection caret in a Web TextField dismisses the keyboard mid-gesture. WebKit transiently blurs the hidden input with a null relatedTarget while the document keeps focus, then refocuses it a frame later,and the engine reacted to that blink on two listeners that each tore the input down. A plain input ignores the same blur, so the cause is Flutter's reaction, not a WebKit limitation.

Fix
On iOS both listeners now defer their teardown by 100ms and cancel it if the input refocuses, mirroring the existing #155265 deferred close. Done and tap-away never refocus so they still close, and the deferral is narrowed to the exact drag signature so every other focus transition is unaffected.

Demo
Before: https://flutter-demo-52-before.web.app (keyboard dismisses mid caret drag)
After: https://flutter-demo-52-after.web.app (keyboard stays up)

Repro on iOS 27 Safari: tap the field to raise the keyboard, then long-press and drag the selection caret. Before dismisses; after stays up. Done and tap-away still dismiss.

@flutter-dashboardflutter-dashboardBot added the CICD Run CI/CD label Jul 24, 2026
@github-actionsgithub-actionsBot added a: text input Entering text in a text field or keyboard related problems engine flutter/engine related. See also e: labels. platform-web Web applications specifically f: focus Focus traversal, gaining or losing focus team-web Owned by Web platform team labels Jul 24, 2026

@gemini-code-assistgemini-code-assistBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces deferred handling for focusout events and text connection closures on iOS Safari to prevent transient blurs (such as during caret or selection dragging) from prematurely dismissing the keyboard. This is achieved by implementing timers in ViewFocusBinding and DefaultTextEditingStrategy that delay the blur reporting, allowing an immediate refocus to cancel the operation. Corresponding unit tests have been added to verify these changes. There are no review comments, so I have no feedback to provide.

@Renzo-OlivaresRenzo-Olivares left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just a few small comments. I think the overall approach looks good.

/// iOS caret-drag deferral tests do not depend on the headless browser
/// reporting the test document as focused. Mirrors
/// `DefaultTextEditingStrategy.debugDocumentHasFocusOverride`.
bool? debugViewFocusDocumentHasFocusOverride;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: I think we should add a @visibleForTesting if this is only meant to be used in tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I see that we do use it in the focus out listener.

I think similar to _documentVisibilityState below, we should have a seperate private variable:

/// Overrides the result of [domDocument.visibilityState] for testing.
@visibleForTesting
String? debugDocumentVisibilityStateOverride;
boolget _documentHasFocus => debugDocumentHasFocusOverride ?? domDocument.hasFocus();
Stringget _documentVisibilityState =>
debugDocumentVisibilityStateOverride ?? domDocument.visibilityState;

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Added a _documentHasFocus getter mirroring DefaultTextEditingStrategy._documentHasFocus, and the focusout listener reads that instead of naming the override directly.

// https://github.com/flutter/flutter/issues/189744
if (isIosSafari) {
_pendingBlurConnectionCloseTimer?.cancel();
_pendingBlurConnectionCloseTimer = Timer(const Duration(milliseconds: 100), () {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we use the same Duration(milliseconds: 100) 2 times in this file and once in view_focus_binding.dart for the same focus related reasons. Consider extracting a shared const member that can be used across both files.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good catch. Done.

view.dom.rootElement.append(other);
other.focusWithoutScroll();
dispatchedViewFocusEvents.clear();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we be setting debugViewFocusDocumentHasFocusOverride in this test as well?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done.

/// [ViewFocusBinding] also matches on this class to detect blurs originating
/// from the text-editing element, so it is a production contract, not just a
/// testing hook.
static const String textEditingClass = 'flt-text-editing';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Instead of exposing this, have we considered instead adding a public member that signals this is an active text editing client to HybridTextEditing so that we don't expose this and rely on the name of a CSS class. And so the solution also works for a11y. As is it does not because I don't think flt-text-editing is added by SemanticsTextEditingStrategy only DefaultTextEditingStrategy.initializeTextEditing which is overriden by SemanticsTextEditingStrategy.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Replaced with a member on HybridTextEditing:

boolisActiveTextEditingElement(DomElement? element) =>
isEditing && element !=null&& element == strategy.domElement;

Renzo-Olivares
Renzo-Olivares previously approved these changes Aug 6, 2026

@Renzo-OlivaresRenzo-Olivares left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, w/ some small docs comments.

/// [ViewFocusBinding] uses this to recognize a `focusout` that originated
/// from the active text-editing element.
///
/// Prefer this over matching on [textEditingClass]. That class is applied by

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Consider shrinking this paragraph to the below in case [SemanticsTextEditingStrategy] ever changes so this documentation doesn't get stale.

/// Prefer this over matching on [textEditingClass]. That class is
/// not guaranteed to be applied by all strategies.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good suggestion. Done.

///
/// Sets the real singleton state rather than applying
/// [HybridTextEditing.textEditingClass], so these tests exercise the same signal
/// production code reads. The class is only applied by

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Consider softening this last sentence to:

The class is not guaranteed to be applied by all text editing strategies, so keying tests off it would not reflect the prodution code.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good suggestion. Done.

// semantics enabled the live editing element never carries it. Keying off
// the class left the fix dead under VoiceOver.
// `flt-text-editing` class, which is not guaranteed to be applied by all
// text editing strategies. An earlier revision matched on the class, which

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: I would remove this last sentence it's helpful for me as a reviewer but not to contributors in the future.

or maybe change to:

Matching on the class would leave the deferral dead for strategies that do not apply it.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Good suggestion. Done.

@Renzo-OlivaresRenzo-Olivares left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, thank you for the fix!

@flutter-zlflutter-zl added the autosubmit Merge PR when tree becomes green via auto submit App label Aug 6, 2026
@auto-submit

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/flutter/190014, because - The status or check suite Dashboard Checks has failed. Please fix the issues identified (or deflake) before re-applying this label.

@auto-submitauto-submitBot removed the autosubmit Merge PR when tree becomes green via auto submit App label Aug 6, 2026
@flutter-zlflutter-zl added the autosubmit Merge PR when tree becomes green via auto submit App label Aug 6, 2026
@auto-submitauto-submitBot removed the autosubmit Merge PR when tree becomes green via auto submit App label Aug 6, 2026
@auto-submit

Copy link
Copy Markdown
Contributor

auto label is removed for flutter/flutter/190014, Failed to enqueue flutter/flutter/190014 with HTTP 400: Pull request Required status check "Check Code Freeze" is queued..

@auto-submitauto-submitBot removed the autosubmit Merge PR when tree becomes green via auto submit App label Aug 7, 2026
@flutter-zlflutter-zl added the autosubmit Merge PR when tree becomes green via auto submit App label Aug 7, 2026
@auto-submitauto-submitBot removed the autosubmit Merge PR when tree becomes green via auto submit App label Aug 7, 2026
@auto-submit

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/flutter/190014, because - The status or check suite Google testing has failed. Please fix the issues identified (or deflake) before re-applying this label.

@flutter-zlflutter-zl added the autosubmit Merge PR when tree becomes green via auto submit App label Aug 7, 2026
@auto-submitauto-submitBot removed the autosubmit Merge PR when tree becomes green via auto submit App label Aug 7, 2026
@auto-submit

Copy link
Copy Markdown
Contributor

autosubmit label was removed for flutter/flutter/190014, because - The status or check suite Google testing has failed. Please fix the issues identified (or deflake) before re-applying this label.

@flutter-zlflutter-zl added the autosubmit Merge PR when tree becomes green via auto submit App label Aug 10, 2026
@auto-submit
auto-submitBot added this pull request to the merge queueAug 10, 2026
Merged via the queue into flutter:master with commit 347dff3Aug 10, 2026
21 of 22 checks passed
@flutter-dashboardflutter-dashboardBot removed the autosubmit Merge PR when tree becomes green via auto submit App label Aug 10, 2026
@jtmcdole

Copy link
Copy Markdown
Member

Reason for revert: This is failing in post submit on a suppressed test. Since this is a hard failure, we should revert:

00:10 +400 ~30 -1: text_editing_test.dart: GloballyPositionedTextEditingStrategy closes the text connection on iOS when the input is not refocused after a null-relatedTarget blur [E] Expected: an object with length of <1>
Actual: []
Which: has length of <0>

https://logs.chromium.org/logs/flutter/buildbucket/cr-buildbucket/8673753854704709425/+/u/test:_run_suite_safari-dart2js-canvaskit-engine/stdout

image

@jtmcdolejtmcdole added the revert Autorevert PR (with "Reason for revert:" comment) label Aug 11, 2026
@flutteractionsbot

Copy link
Copy Markdown
Contributor

Successfully created revert PR: #190926

@flutteractionsbotflutteractionsbot removed the revert Autorevert PR (with "Reason for revert:" comment) label Aug 11, 2026
zijiehe-google-com pushed a commit to zijiehe-google-com/flutter that referenced this pull request Aug 11, 2026
…190926)
Reverts: [[web] Keep the keyboard up during an iOS caret
drag](flutter#190014)
Initiated by: @jtmcdole
Reason for reverting: Original PR Author: @flutter-zl
Reviewed By: @Renzo-Olivares
The original PR description is provided below:
Fixesflutter#189744
**Problem**
On iOS 27, long-press-dragging the selection caret in a Web TextField
dismisses the keyboard mid-gesture. WebKit transiently blurs the hidden
input with a null relatedTarget while the document keeps focus, then
refocuses it a frame later,and the engine reacted to that blink on two
listeners that each tore the input down. A plain input ignores the same
blur, so the cause is Flutter's reaction, not a WebKit limitation.
**Fix**
On iOS both listeners now defer their teardown by 100ms and cancel it if
the input refocuses, mirroring the existing flutter#155265 deferred close. Done
and tap-away never refocus so they still close, and the deferral is
narrowed to the exact drag signature so every other focus transition is
unaffected.
**Demo**
Before: https://flutter-demo-52-before.web.app (keyboard dismisses mid
caret drag)
After: https://flutter-demo-52-after.web.app (keyboard stays up)
Repro on iOS 27 Safari: tap the field to raise the keyboard, then
long-press and drag the selection caret. Before dismisses; after stays
up. Done and tap-away still dismiss.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: text inputEntering text in a text field or keyboard related problemsCICDRun CI/CDengineflutter/engine related. See also e: labels.f: focusFocus traversal, gaining or losing focusplatform-webWeb applications specificallyteam-webOwned by Web platform team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[web][iOS 27] Keyboard dismisses when long-press-dragging the text selection caret

4 participants

@flutter-zl@jtmcdole@flutteractionsbot@Renzo-Olivares