Uh oh!
There was an error while loading. Please reload this page.
[web] Keep the keyboard up during an iOS caret drag - #190014
Conversation
There was a problem hiding this comment.
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-Olivares
left a comment
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
nit: I think we should add a @visibleForTesting if this is only meant to be used in tests.
There was a problem hiding this comment.
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:
flutter/engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart
Lines 1536 to 1543 in de01d5d
There was a problem hiding this comment.
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), () { |
There was a problem hiding this comment.
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.
| view.dom.rootElement.append(other); | ||
| other.focusWithoutScroll(); | ||
| dispatchedViewFocusEvents.clear(); | ||
There was a problem hiding this comment.
Should we be setting debugViewFocusDocumentHasFocusOverride in this test as well?
| /// [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'; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Replaced with a member on HybridTextEditing:
boolisActiveTextEditingElement(DomElement? element) =>
isEditing && element !=null&& element == strategy.domElement;5eb2bf6 to
b1d5e63Compare
Renzo-Olivares
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Good suggestion. Done.
Renzo-Olivares
left a comment
There was a problem hiding this comment.
LGTM, thank you for the fix!
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 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.. |
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. |
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. |
Uh oh!
There was an error while loading. Please reload this page.
jtmcdole
commented
Aug 11, 2026
flutteractionsbot
commented
Aug 11, 2026
Successfully created revert PR: #190926 |
…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.

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.