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
}
}
final ValueChanged<String>? onSubmitted = widget.onSubmitted;
if (onSubmitted == null) {
return;
}
// Invoke optional callback with the user's submitted content.
try {
widget.onSubmitted?.call(_value.text);
onSubmitted(_value.text);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
......@@ -2017,6 +2022,20 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
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;
......@@ -2159,6 +2178,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
textAlign: widget.textAlign,
)
..setEditingState(localValue);
_lastKnownRemoteTextEditingValue = localValue;
} else {
_textInputConnection!.show();
}
......@@ -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
void connectionClosed() {
if (_hasInputConnection) {
......
......@@ -2032,6 +2032,71 @@ void main() {
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(
'iOS autocorrection rectangle should appear on demand and dismiss when the text changes or when focus is lost',
(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