[EditableText] Fix TextField crashed with composing and maxLength set (#63754)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user