Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion packages/flutter/lib/src/services/text_formatter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
9 changes: 8 additions & 1 deletion packages/flutter/lib/src/widgets/editable_text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2092,7 +2092,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.
Expand Down
34 changes: 34 additions & 0 deletions packages/flutter/test/widgets/editable_text_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
37 changes: 37 additions & 0 deletions packages/flutter/test/widgets/form_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}