Unverified Commit 0e72f992 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Restart input connection after `EditableText.onSubmitted` (#84307)

parent 7fbfb1fa
...@@ -2006,9 +2006,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2006,9 +2006,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
} }
final ValueChanged<String>? onSubmitted = widget.onSubmitted;
if (onSubmitted == null) {
return;
}
// Invoke optional callback with the user's submitted content. // Invoke optional callback with the user's submitted content.
try { try {
widget.onSubmitted?.call(_value.text); onSubmitted(_value.text);
} catch (exception, stack) { } catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails( FlutterError.reportError(FlutterErrorDetails(
exception: exception, exception: exception,
...@@ -2017,6 +2022,20 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2017,6 +2022,20 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
context: ErrorDescription('while calling onSubmitted for $action'), context: ErrorDescription('while calling onSubmitted for $action'),
)); ));
} }
// If `shouldUnfocus` is true, the text field should no longer be focused
// after the microtask queue is drained. But in case the developer cancelled
// the focus change in the `onSubmitted` callback by focusing this input
// field again, reset the soft keyboard.
// See https://github.com/flutter/flutter/issues/84240.
//
// `_restartConnectionIfNeeded` creates a new TextInputConnection to replace
// the current one. This on iOS switches to a new input view and on Android
// restarts the input method, and in both cases the soft keyboard will be
// reset.
if (shouldUnfocus) {
_scheduleRestartConnection();
}
} }
int _batchEditDepth = 0; int _batchEditDepth = 0;
...@@ -2159,6 +2178,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2159,6 +2178,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
textAlign: widget.textAlign, textAlign: widget.textAlign,
) )
..setEditingState(localValue); ..setEditingState(localValue);
_lastKnownRemoteTextEditingValue = localValue;
} else { } else {
_textInputConnection!.show(); _textInputConnection!.show();
} }
...@@ -2181,6 +2201,47 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2181,6 +2201,47 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
} }
bool _restartConnectionScheduled = false;
void _scheduleRestartConnection() {
if (_restartConnectionScheduled) {
return;
}
_restartConnectionScheduled = true;
scheduleMicrotask(_restartConnectionIfNeeded);
}
// Discards the current [TextInputConnection] and establishes a new one.
//
// This method is rarely needed. This is currently used to reset the input
// type when the "submit" text input action is triggered and the developer
// puts the focus back to this input field..
void _restartConnectionIfNeeded() {
_restartConnectionScheduled = false;
if (!_hasInputConnection || !_shouldCreateInputConnection) {
return;
}
_textInputConnection!.close();
_textInputConnection = null;
_lastKnownRemoteTextEditingValue = null;
final AutofillScope? currentAutofillScope = _needsAutofill ? this.currentAutofillScope : null;
final TextInputConnection newConnection = currentAutofillScope?.attach(this, textInputConfiguration)
?? TextInput.attach(this, _effectiveAutofillClient.textInputConfiguration);
_textInputConnection = newConnection;
final TextStyle style = widget.style;
newConnection
..setStyle(
fontFamily: style.fontFamily,
fontSize: style.fontSize,
fontWeight: style.fontWeight,
textDirection: _textDirection,
textAlign: widget.textAlign,
)
..setEditingState(_value);
_lastKnownRemoteTextEditingValue = _value;
}
@override @override
void connectionClosed() { void connectionClosed() {
if (_hasInputConnection) { if (_hasInputConnection) {
......
...@@ -2032,6 +2032,71 @@ void main() { ...@@ -2032,6 +2032,71 @@ void main() {
assert(!onEditingCompleteCalled); assert(!onEditingCompleteCalled);
}); });
testWidgets(
'finalizeEditing should reset the input connection when shouldUnfocus is true but the unfocus is cancelled',
(WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/84240 .
Widget widget = MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1!,
cursorColor: Colors.blue,
focusNode: focusNode,
controller: controller,
onSubmitted: (String value) {},
),
);
await tester.pumpWidget(widget);
focusNode.requestFocus();
await tester.pump();
assert(focusNode.hasFocus);
tester.testTextInput.log.clear();
// This should unfocus the field. Don't restart the input.
await tester.testTextInput.receiveAction(TextInputAction.done);
expect(tester.testTextInput.log, isNot(containsAllInOrder(<Matcher>[
matchesMethodCall('TextInput.clearClient'),
matchesMethodCall('TextInput.setClient'),
])));
widget = MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1!,
cursorColor: Colors.blue,
focusNode: focusNode,
controller: controller,
onSubmitted: (String value) {
focusNode.requestFocus();
},
),
);
await tester.pumpWidget(widget);
focusNode.requestFocus();
await tester.pump();
assert(focusNode.hasFocus);
tester.testTextInput.log.clear();
// This will attempt to unfocus the field but the onSubmitted callback
// will cancel that. Restart the input connection in this case.
await tester.testTextInput.receiveAction(TextInputAction.done);
expect(tester.testTextInput.log, containsAllInOrder(<Matcher>[
matchesMethodCall('TextInput.clearClient'),
matchesMethodCall('TextInput.setClient'),
]));
tester.testTextInput.log.clear();
// TextInputAction.unspecified does not unfocus the input field by default.
await tester.testTextInput.receiveAction(TextInputAction.unspecified);
expect(tester.testTextInput.log, isNot(containsAllInOrder(<Matcher>[
matchesMethodCall('TextInput.clearClient'),
matchesMethodCall('TextInput.setClient'),
])));
});
testWidgets( testWidgets(
'iOS autocorrection rectangle should appear on demand and dismiss when the text changes or when focus is lost', 'iOS autocorrection rectangle should appear on demand and dismiss when the text changes or when focus is lost',
(WidgetTester tester) async { (WidgetTester tester) async {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment