Unverified Commit 2c3652b8 authored by Alex Li's avatar Alex Li Committed by GitHub

[EditableText] Fix TextField crashed with composing and maxLength set (#63754)

parent acd82534
......@@ -341,6 +341,15 @@ class LengthLimitingTextInputFormatter extends TextInputFormatter {
/// counted as a single character, but because it is a combination of two
/// Unicode scalar values, '\u{1F44D}\u{1F3FD}', it is counted as two
/// characters.
///
/// ### Composing text behaviors
///
/// There is no guarantee for the final value before the composing ends.
/// So while the value is composing, the constraint of [maxLength] will be
/// temporary lifted until the composing ends.
///
/// In addition, if the current value already reached the [maxLength],
/// composing is not allowed.
final int? maxLength;
/// Truncate the given TextEditingValue to maxLength characters.
......@@ -367,9 +376,19 @@ class LengthLimitingTextInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue, // unused.
TextEditingValue oldValue,
TextEditingValue newValue,
) {
// Return the new value when the old value has not reached the max
// limit or the old value is composing too.
if (newValue.composing.isValid) {
if (maxLength != null && maxLength! > 0 &&
oldValue.text.characters.length == maxLength! &&
!oldValue.composing.isValid) {
return oldValue;
}
return newValue;
}
if (maxLength != null && maxLength! > 0 && newValue.text.characters.length > maxLength!) {
// If already at the maximum and tried to enter even more, keep the old
// value.
......
......@@ -2100,7 +2100,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
final bool textChanged = _value?.text != value?.text;
final bool isRepeat = value == _lastFormattedUnmodifiedTextEditingValue;
if (textChanged && widget.inputFormatters != null && widget.inputFormatters.isNotEmpty) {
// There's no need to format when starting to compose or when continuing
// an existing composition.
final bool isComposing = value?.composing?.isValid ?? false;
final bool isPreviouslyComposing = _lastFormattedUnmodifiedTextEditingValue?.composing?.isValid ?? false;
if ((textChanged || (!isComposing && isPreviouslyComposing)) &&
widget.inputFormatters != null &&
widget.inputFormatters.isNotEmpty) {
// Only format when the text has changed and there are available formatters.
// Pass through the formatter regardless of repeat status if the input value is
// different than the stored value.
......
......@@ -5341,6 +5341,40 @@ void main() {
expectToAssert(const TextEditingValue(text: 'test', composing: TextRange(start: 1, end: 9)), true);
expectToAssert(const TextEditingValue(text: 'test', composing: TextRange(start: -1, end: 9)), false);
});
// Regression test for https://github.com/flutter/flutter/issues/65374.
testWidgets('Length formatter will not cause crash while the TextEditingValue is composing', (WidgetTester tester) async {
final TextInputFormatter formatter = LengthLimitingTextInputFormatter(5);
final Widget widget = MaterialApp(
home: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: focusNode,
inputFormatters: <TextInputFormatter>[formatter],
style: textStyle,
cursorColor: cursorColor,
selectionControls: materialTextSelectionControls,
),
);
await tester.pumpWidget(widget);
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
state.updateEditingValue(const TextEditingValue(text: '12345'));
expect(state.currentTextEditingValue.composing, TextRange.empty);
state.updateEditingValue(const TextEditingValue(text: '12345', composing: TextRange(start: 2, end: 4)));
expect(state.currentTextEditingValue.composing, const TextRange(start: 2, end: 4));
// Formatter will not update format while the editing value is composing.
state.updateEditingValue(const TextEditingValue(text: '123456', composing: TextRange(start: 2, end: 5)));
expect(state.currentTextEditingValue.text, '123456');
expect(state.currentTextEditingValue.composing, const TextRange(start: 2, end: 5));
// After composing ends, formatter will update.
state.updateEditingValue(const TextEditingValue(text: '123456'));
expect(state.currentTextEditingValue.text, '12345');
expect(state.currentTextEditingValue.composing, TextRange.empty);
});
}
class MockTextFormatter extends TextInputFormatter {
......
......@@ -849,4 +849,41 @@ void main() {
}
expect(() => builder(), throwsAssertionError);
});
// Regression test for https://github.com/flutter/flutter/issues/65374.
testWidgets('Validate form should return correct validation if the value is composing', (WidgetTester tester) async {
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
String fieldValue;
final Widget widget = MaterialApp(
home: MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Material(
child: Form(
key: formKey,
child: TextFormField(
maxLength: 5,
onSaved: (String value) { fieldValue = value; },
validator: (String value) => value.length > 5 ? 'Exceeded' : null,
),
),
),
),
),
),
);
await tester.pumpWidget(widget);
final EditableTextState editableText = tester.state<EditableTextState>(find.byType(EditableText));
editableText.updateEditingValue(const TextEditingValue(text: '123456', composing: TextRange(start: 2, end: 5)));
expect(editableText.currentTextEditingValue.composing, const TextRange(start: 2, end: 5));
formKey.currentState.save();
expect(fieldValue, '123456');
expect(formKey.currentState.validate(), isFalse);
});
}
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