mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Calling onConnectionClosed when the input element is blurred (flutter/engine#14484)
* close connection call * closing connection on blur * remove the timer and check the window focus directly. address reviewer comments. * addressing reviewer comments
This commit is contained in:
parent
a68728ff94
commit
c0dfb3386f
@ -62,6 +62,16 @@ class DomRenderer {
|
||||
static const String _staleHotRestartStore = '__flutter_state';
|
||||
List<html.Element> _staleHotRestartState;
|
||||
|
||||
/// Used to decide if the browser tab still has the focus.
|
||||
///
|
||||
/// This information is useful for deciding on the blur behavior.
|
||||
/// See [DefaultTextEditingStrategy].
|
||||
///
|
||||
/// This getter calls the `hasFocus` method of the `Document` interface.
|
||||
/// See for more details:
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/Document/hasFocus
|
||||
bool get windowHasFocus => js_util.callMethod(html.document, 'hasFocus', []);
|
||||
|
||||
void _setupHotRestart() {
|
||||
// This persists across hot restarts to clear stale DOM.
|
||||
_staleHotRestartState =
|
||||
|
||||
@ -343,6 +343,33 @@ class DefaultTextEditingStrategy implements TextEditingStrategy {
|
||||
_subscriptions.add(domElement.onKeyDown.listen(_maybeSendAction));
|
||||
|
||||
_subscriptions.add(html.document.onSelectionChange.listen(_handleChange));
|
||||
|
||||
// The behavior for blur in DOM elements changes depending on the reason of
|
||||
// blur:
|
||||
//
|
||||
// (1) If the blur is triggered due to tab change or browser minimize, same
|
||||
// element receives the focus as soon as the page reopens. Hence, text
|
||||
// editing connection does not need to be closed. In this case we dot blur
|
||||
// the DOM element.
|
||||
//
|
||||
// (2) On the other hand if the blur is triggered due to interaction with
|
||||
// another element on the page, the current text connection is obsolete so
|
||||
// connection close request is send to Flutter.
|
||||
//
|
||||
// See [HybridTextEditing.sendTextConnectionClosedToFlutterIfAny].
|
||||
//
|
||||
// In order to detect between these two cases, after a blur event is
|
||||
// triggered [domRenderer.windowHasFocus] method which checks the window
|
||||
// focus is called.
|
||||
_subscriptions.add(domElement.onBlur.listen((_) {
|
||||
if (domRenderer.windowHasFocus) {
|
||||
// Focus is still on the body. Continue with blur.
|
||||
owner.sendTextConnectionClosedToFrameworkIfAny();
|
||||
} else {
|
||||
// Refocus.
|
||||
domElement.focus();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@override
|
||||
@ -508,7 +535,18 @@ class IOSTextEditingStrategy extends DefaultTextEditingStrategy {
|
||||
domElement.style.transform = 'translate(-9999px, -9999px)';
|
||||
|
||||
_canPosition = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void addEventHandlers() {
|
||||
// Subscribe to text and selection changes.
|
||||
_subscriptions.add(domElement.onInput.listen(_handleChange));
|
||||
|
||||
_subscriptions.add(domElement.onKeyDown.listen(_maybeSendAction));
|
||||
|
||||
_subscriptions.add(html.document.onSelectionChange.listen(_handleChange));
|
||||
|
||||
// Position the DOM element after it is focused.
|
||||
_subscriptions.add(domElement.onFocus.listen((_) {
|
||||
// Cancel previous timer if exists.
|
||||
_positionInputElementTimer?.cancel();
|
||||
@ -516,15 +554,15 @@ class IOSTextEditingStrategy extends DefaultTextEditingStrategy {
|
||||
_canPosition = true;
|
||||
positionElement();
|
||||
});
|
||||
}));
|
||||
|
||||
// When the virtual keyboard is closed on iOS, onBlur is triggered.
|
||||
_subscriptions.add(domElement.onBlur.listen((_) {
|
||||
// Cancel the timer since there is no need to set the location of the
|
||||
// input element anymore. It needs to be focused again to be editable
|
||||
// by the user.
|
||||
_positionInputElementTimer?.cancel();
|
||||
_positionInputElementTimer = null;
|
||||
}));
|
||||
// On iOS, blur is trigerred if the virtual keyboard is closed or the
|
||||
// browser is sent to background or the browser tab is changed.
|
||||
//
|
||||
// Since in all these cases, the connection needs to be closed,
|
||||
// [domRenderer.windowHasFocus] is not checked in [IOSTextEditingStrategy].
|
||||
_subscriptions.add(domElement.onBlur.listen((_) {
|
||||
owner.sendTextConnectionClosedToFrameworkIfAny();
|
||||
}));
|
||||
}
|
||||
|
||||
@ -567,19 +605,24 @@ class AndroidTextEditingStrategy extends DefaultTextEditingStrategy {
|
||||
|
||||
@override
|
||||
void addEventHandlers() {
|
||||
super.addEventHandlers();
|
||||
// Chrome on Android will hide the onscreen keyboard when you tap outside
|
||||
// the text box. Instead, we want the framework to tell us to hide the
|
||||
// keyboard via `TextInput.clearClient` or `TextInput.hide`.
|
||||
if (browserEngine == BrowserEngine.blink ||
|
||||
browserEngine == BrowserEngine.unknown) {
|
||||
_subscriptions.add(domElement.onBlur.listen((_) {
|
||||
if (isEnabled) {
|
||||
// Refocus.
|
||||
domElement.focus();
|
||||
}
|
||||
}));
|
||||
}
|
||||
// Subscribe to text and selection changes.
|
||||
_subscriptions.add(domElement.onInput.listen(_handleChange));
|
||||
|
||||
_subscriptions.add(domElement.onKeyDown.listen(_maybeSendAction));
|
||||
|
||||
_subscriptions.add(html.document.onSelectionChange.listen(_handleChange));
|
||||
|
||||
_subscriptions.add(domElement.onBlur.listen((_) {
|
||||
if (domRenderer.windowHasFocus) {
|
||||
// Chrome on Android will hide the onscreen keyboard when you tap outside
|
||||
// the text box. Instead, we want the framework to tell us to hide the
|
||||
// keyboard via `TextInput.clearClient` or `TextInput.hide`. Therefore
|
||||
// refocus as long as [domRenderer.windowHasFocus] is true.
|
||||
domElement.focus();
|
||||
} else {
|
||||
owner.sendTextConnectionClosedToFrameworkIfAny();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@ -597,27 +640,49 @@ class FirefoxTextEditingStrategy extends DefaultTextEditingStrategy {
|
||||
|
||||
_subscriptions.add(domElement.onKeyDown.listen(_maybeSendAction));
|
||||
|
||||
/// Detects changes in text selection.
|
||||
///
|
||||
/// In Firefox, when cursor moves, neither selectionChange nor onInput
|
||||
/// events are triggered. We are listening to keyup event. Selection start,
|
||||
/// end values are used to decide if the text cursor moved.
|
||||
///
|
||||
/// Specific keycodes are not checked since users/applications can bind
|
||||
/// their own keys to move the text cursor.
|
||||
/// Decides if the selection has changed (cursor moved) compared to the
|
||||
/// previous values.
|
||||
///
|
||||
/// After each keyup, the start/end values of the selection is compared to
|
||||
/// the previously saved editing state.
|
||||
// Detects changes in text selection.
|
||||
//
|
||||
// In Firefox, when cursor moves, neither selectionChange nor onInput
|
||||
// events are triggered. We are listening to keyup event. Selection start,
|
||||
// end values are used to decide if the text cursor moved.
|
||||
//
|
||||
// Specific keycodes are not checked since users/applications can bind
|
||||
// their own keys to move the text cursor.
|
||||
// Decides if the selection has changed (cursor moved) compared to the
|
||||
// previous values.
|
||||
//
|
||||
// After each keyup, the start/end values of the selection is compared to
|
||||
// the previously saved editing state.
|
||||
_subscriptions.add(domElement.onKeyUp.listen((event) {
|
||||
_handleChange(event);
|
||||
}));
|
||||
|
||||
/// In Firefox the context menu item "Select All" does not work without
|
||||
/// listening to onSelect. On the other browsers onSelectionChange is
|
||||
/// enough for covering "Select All" functionality.
|
||||
// In Firefox the context menu item "Select All" does not work without
|
||||
// listening to onSelect. On the other browsers onSelectionChange is
|
||||
// enough for covering "Select All" functionality.
|
||||
_subscriptions.add(domElement.onSelect.listen(_handleChange));
|
||||
|
||||
// For Firefox, we also use the same approach as the parent class.
|
||||
//
|
||||
// Do not blur the DOM element if the user goes to another tab or minimizes
|
||||
// the browser. See [super.addEventHandlers] for more comments.
|
||||
//
|
||||
// The different part is, in Firefox, we are not able to get correct value
|
||||
// when we check the window focus like [domRendered.windowHasFocus].
|
||||
//
|
||||
// However [document.activeElement] always equals to [domElement] if the
|
||||
// user goes to another tab, minimizes the browser or opens the dev tools.
|
||||
// Hence [document.activeElement] is checked in this listener.
|
||||
_subscriptions.add(domElement.onBlur.listen((_) {
|
||||
html.Element activeElement = html.document.activeElement;
|
||||
if (activeElement != domElement) {
|
||||
// Focus is still on the body. Continue with blur.
|
||||
owner.sendTextConnectionClosedToFrameworkIfAny();
|
||||
} else {
|
||||
// Refocus.
|
||||
domElement.focus();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@ -683,7 +748,6 @@ class PersistentTextEditingElement extends DefaultTextEditingStrategy {
|
||||
// Refocus after setting editing state.
|
||||
domElement.focus();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Text editing singleton.
|
||||
@ -857,6 +921,24 @@ class HybridTextEditing {
|
||||
_emptyCallback,
|
||||
);
|
||||
}
|
||||
|
||||
void sendTextConnectionClosedToFrameworkIfAny() {
|
||||
if (isEditing) {
|
||||
stopEditing();
|
||||
ui.window.onPlatformMessage(
|
||||
'flutter/textinput',
|
||||
const JSONMethodCodec().encodeMethodCall(
|
||||
MethodCall(
|
||||
'TextInputClient.onConnectionClosed',
|
||||
<dynamic>[
|
||||
_clientId,
|
||||
],
|
||||
),
|
||||
),
|
||||
_emptyCallback,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Information on the font and alignment of a text editing element.
|
||||
|
||||
@ -598,6 +598,43 @@ void main() {
|
||||
expect(spy.messages, isEmpty);
|
||||
});
|
||||
|
||||
test('close connection on blur', () async {
|
||||
final MethodCall setClient = MethodCall(
|
||||
'TextInput.setClient', <dynamic>[123, flutterSinglelineConfig]);
|
||||
textEditing.handleTextInput(codec.encodeMethodCall(setClient));
|
||||
|
||||
const MethodCall setEditingState =
|
||||
MethodCall('TextInput.setEditingState', <String, dynamic>{
|
||||
'text': 'abcd',
|
||||
'selectionBase': 2,
|
||||
'selectionExtent': 3,
|
||||
});
|
||||
textEditing.handleTextInput(codec.encodeMethodCall(setEditingState));
|
||||
|
||||
// Editing shouldn't have started yet.
|
||||
expect(document.activeElement, document.body);
|
||||
|
||||
const MethodCall show = MethodCall('TextInput.show');
|
||||
textEditing.handleTextInput(codec.encodeMethodCall(show));
|
||||
|
||||
checkInputEditingState(
|
||||
textEditing.editingElement.domElement, 'abcd', 2, 3);
|
||||
|
||||
// DOM element is blurred.
|
||||
textEditing.editingElement.domElement.blur();
|
||||
|
||||
expect(spy.messages, hasLength(1));
|
||||
MethodCall call = spy.messages[0];
|
||||
spy.messages.clear();
|
||||
expect(call.method, 'TextInputClient.onConnectionClosed');
|
||||
expect(
|
||||
call.arguments,
|
||||
<dynamic>[
|
||||
123, // Client ID
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('setClient, setEditingState, show, setClient', () {
|
||||
final MethodCall setClient = MethodCall(
|
||||
'TextInput.setClient', <dynamic>[123, flutterSinglelineConfig]);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user