diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index c525826d406..79b323cb29c 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -1230,9 +1230,6 @@ class EditableTextState extends State with AutomaticKeepAliveClien // _lastFormattedUnmodifiedTextEditingValue tracks the last value // that the formatter ran on and is used to prevent double-formatting. TextEditingValue _lastFormattedUnmodifiedTextEditingValue; - // _lastFormattedValue tracks the last post-format value, so that it can be - // reused without rerunning the formatter when the input value is repeated. - TextEditingValue _lastFormattedValue; // _receivedRemoteTextEditingValue is the direct value last passed in // updateEditingValue. This value does not get updated with the formatted // version. @@ -1663,29 +1660,15 @@ class EditableTextState extends State with AutomaticKeepAliveClien // Check if the new value is the same as the current local value, or is the same // as the post-formatting value of the previous pass. final bool textChanged = _value?.text != value?.text; - final bool isRepeatText = value?.text == _lastFormattedUnmodifiedTextEditingValue?.text; - final bool isRepeatSelection = value?.selection == _lastFormattedUnmodifiedTextEditingValue?.selection; - // Only format when the text has changed and there are available formatters. - if (!isRepeatText && textChanged && widget.inputFormatters != null && widget.inputFormatters.isNotEmpty) { - for (final TextInputFormatter formatter in widget.inputFormatters) { + final bool isRepeat = value?.text == _lastFormattedUnmodifiedTextEditingValue?.text; + if (textChanged && !isRepeat && widget.inputFormatters != null && widget.inputFormatters.isNotEmpty) { + for (final TextInputFormatter formatter in widget.inputFormatters) value = formatter.formatEditUpdate(_value, value); - } - // Always pass the text through the whitespace directionality formatter to - // maintain expected behavior with carets on trailing whitespace. - value = _whitespaceFormatter.formatEditUpdate(_value, value); - _lastFormattedValue = value; - } - // If the text has changed or the selection has changed, we should update the - // locally stored TextEditingValue to the new one. - if (!isRepeatText || !isRepeatSelection) { _value = value; - } else if (textChanged && _lastFormattedValue != null) { - _value = _lastFormattedValue; + _updateRemoteEditingValueIfNeeded(); + } else { + _value = value; } - // Always attempt to send the value. If the value has changed, then it will send, - // otherwise, it will short-circuit. - _updateRemoteEditingValueIfNeeded(); - if (textChanged && widget.onChanged != null) widget.onChanged(value.text); _lastFormattedUnmodifiedTextEditingValue = _receivedRemoteTextEditingValue; diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index 8362db89e17..43e12710323 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -4238,299 +4238,12 @@ void main() { expect(formatter.log, referenceLog); }); - - testWidgets('formatter logic handles repeat filtering', (WidgetTester tester) async { - final MockTextFormatter formatter = MockTextFormatter(); - await tester.pumpWidget( - MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: FocusScope( - node: focusScopeNode, - autofocus: true, - child: EditableText( - backgroundCursorColor: Colors.grey, - controller: controller, - focusNode: focusNode, - maxLines: 1, // Sets text keyboard implicitly. - style: textStyle, - cursorColor: cursorColor, - inputFormatters: [formatter], - ), - ), - ), - ), - ); - - await tester.tap(find.byType(EditableText)); - await tester.showKeyboard(find.byType(EditableText)); - controller.text = ''; - await tester.idle(); - - final EditableTextState state = - tester.state(find.byType(EditableText)); - expect(tester.testTextInput.editingState['text'], equals('')); - expect(state.wantKeepAlive, true); - - expect(formatter.formatCallCount, 0); - state.updateEditingValue(const TextEditingValue(text: '01')); - expect(formatter.formatCallCount, 1); - state.updateEditingValue(const TextEditingValue(text: '012')); - expect(formatter.formatCallCount, 2); - state.updateEditingValue(const TextEditingValue(text: '0123')); // Text change causes reformat - expect(formatter.formatCallCount, 3); - state.updateEditingValue(const TextEditingValue(text: '0123')); // Repeat, does not format - expect(formatter.formatCallCount, 3); - state.updateEditingValue(const TextEditingValue(text: '0123')); // Repeat, does not format - expect(formatter.formatCallCount, 3); - state.updateEditingValue(const TextEditingValue(text: '0123', selection: TextSelection.collapsed(offset: 2))); // Selection change does not reformat - expect(formatter.formatCallCount, 3); - state.updateEditingValue(const TextEditingValue(text: '0123', selection: TextSelection.collapsed(offset: 2))); // Repeat, does not format - expect(formatter.formatCallCount, 3); - state.updateEditingValue(const TextEditingValue(text: '0123', selection: TextSelection.collapsed(offset: 2))); // Repeat, does not format - expect(formatter.formatCallCount, 3); - - const List referenceLog = [ - '[1]: , 01', - '[1]: normal aa', - '[2]: aa, 012', - '[2]: normal aaaa', - '[3]: aaaa, 0123', - '[3]: normal aaaaaa', - ]; - - expect(formatter.log, referenceLog); - }); - - testWidgets('Whitespace directionality formatter input Arabic', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: 'testText'); - await tester.pumpWidget( - MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: FocusScope( - node: focusScopeNode, - autofocus: true, - child: EditableText( - backgroundCursorColor: Colors.blue, - controller: controller, - focusNode: focusNode, - maxLines: 1, // Sets text keyboard implicitly. - style: textStyle, - cursorColor: cursorColor, - ), - ), - ), - ), - ); - - await tester.tap(find.byType(EditableText)); - await tester.showKeyboard(find.byType(EditableText)); - controller.text = ''; - await tester.idle(); - - final EditableTextState state = - tester.state(find.byType(EditableText)); - expect(tester.testTextInput.editingState['text'], equals('')); - expect(state.wantKeepAlive, true); - - // Simple mixed directional input. - state.updateEditingValue(const TextEditingValue(text: 'h')); - state.updateEditingValue(const TextEditingValue(text: 'he')); - state.updateEditingValue(const TextEditingValue(text: 'hel')); - state.updateEditingValue(const TextEditingValue(text: 'hell')); - state.updateEditingValue(const TextEditingValue(text: 'hello')); - expect(state.currentTextEditingValue.text, equals('hello')); - state.updateEditingValue(const TextEditingValue(text: 'hello ')); - expect(state.currentTextEditingValue.text, equals('hello ')); - state.updateEditingValue(const TextEditingValue(text: 'hello ا')); - expect(state.currentTextEditingValue.text, equals('hello \u{200E}ا')); - state.updateEditingValue(const TextEditingValue(text: 'hello الْ')); - state.updateEditingValue(const TextEditingValue(text: 'hello الْعَ')); - state.updateEditingValue(const TextEditingValue(text: 'hello الْعَ ')); - expect(state.currentTextEditingValue.text, equals('hello \u{200E}الْعَ \u{200F}')); - state.updateEditingValue(const TextEditingValue(text: 'hello الْعَ بِيَّةُ')); - state.updateEditingValue(const TextEditingValue(text: 'hello الْعَ بِيَّةُ ')); - expect(state.currentTextEditingValue.text, equals('hello \u{200E}الْعَ بِيَّةُ \u{200F}')); - }); - - testWidgets('Whitespace directionality formatter doesn\'t overwrite existing Arabic', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: 'testText'); - await tester.pumpWidget( - MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: FocusScope( - node: focusScopeNode, - autofocus: true, - child: EditableText( - backgroundCursorColor: Colors.blue, - controller: controller, - focusNode: focusNode, - maxLines: 1, // Sets text keyboard implicitly. - style: textStyle, - cursorColor: cursorColor, - ), - ), - ), - ), - ); - - await tester.tap(find.byType(EditableText)); - await tester.showKeyboard(find.byType(EditableText)); - controller.text = ''; - await tester.idle(); - - final EditableTextState state = - tester.state(find.byType(EditableText)); - expect(tester.testTextInput.editingState['text'], equals('')); - expect(state.wantKeepAlive, true); - - // Does not overwrite existing RLM or LRM characters - state.updateEditingValue(const TextEditingValue(text: 'hello \u{200F}ا')); - expect(state.currentTextEditingValue.text, equals('hello \u{200F}ا')); - state.updateEditingValue(const TextEditingValue(text: 'hello \u{200F}ا \u{200E}ا ا ')); - expect(state.currentTextEditingValue.text, equals('hello \u{200F}ا \u{200E}ا ا \u{200F}')); - - // Handles only directionality markers. - state.updateEditingValue(const TextEditingValue(text: '\u{200E}\u{200F}')); - expect(state.currentTextEditingValue.text, equals('\u{200E}\u{200F}')); - state.updateEditingValue(const TextEditingValue(text: '\u{200E}\u{200F}\u{200E}\u{200F}\u{200E}\u{200F}')); - expect(state.currentTextEditingValue.text, equals('\u{200E}\u{200F}\u{200E}\u{200F}\u{200E}\u{200F}')); - state.updateEditingValue(const TextEditingValue(text: '\u{200E}\u{200F}\u{200F}\u{200F}')); - expect(state.currentTextEditingValue.text, equals('\u{200E}\u{200F}\u{200F}\u{200F}')); - }); - - testWidgets('Whitespace directionality formatter is not leaky Arabic', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: 'testText'); - await tester.pumpWidget( - MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: FocusScope( - node: focusScopeNode, - autofocus: true, - child: EditableText( - backgroundCursorColor: Colors.blue, - controller: controller, - focusNode: focusNode, - maxLines: 1, // Sets text keyboard implicitly. - style: textStyle, - cursorColor: cursorColor, - ), - ), - ), - ), - ); - - await tester.tap(find.byType(EditableText)); - await tester.showKeyboard(find.byType(EditableText)); - controller.text = ''; - await tester.idle(); - - final EditableTextState state = - tester.state(find.byType(EditableText)); - expect(tester.testTextInput.editingState['text'], equals('')); - expect(state.wantKeepAlive, true); - - // Can be passed through formatter repeatedly without leaking/growing. - state.updateEditingValue(const TextEditingValue(text: 'hello \u{200E}عَ \u{200F}عَ \u{200F}عَ \u{200F}')); - expect(state.currentTextEditingValue.text, equals('hello \u{200E}عَ \u{200F}عَ \u{200F}عَ \u{200F}')); - state.updateEditingValue(const TextEditingValue(text: 'hello \u{200E}عَ \u{200F}عَ \u{200F}عَ \u{200F}')); - expect(state.currentTextEditingValue.text, equals('hello \u{200E}عَ \u{200F}عَ \u{200F}عَ \u{200F}')); - state.updateEditingValue(const TextEditingValue(text: 'hello \u{200E}عَ \u{200F}عَ \u{200F}عَ \u{200F}')); - expect(state.currentTextEditingValue.text, equals('hello \u{200E}عَ \u{200F}عَ \u{200F}عَ \u{200F}')); - }); - - testWidgets('Whitespace directionality formatter emojis', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: 'testText'); - await tester.pumpWidget( - MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: FocusScope( - node: focusScopeNode, - autofocus: true, - child: EditableText( - backgroundCursorColor: Colors.blue, - controller: controller, - focusNode: focusNode, - maxLines: 1, // Sets text keyboard implicitly. - style: textStyle, - cursorColor: cursorColor, - ), - ), - ), - ), - ); - - await tester.tap(find.byType(EditableText)); - await tester.showKeyboard(find.byType(EditableText)); - controller.text = ''; - await tester.idle(); - - final EditableTextState state = - tester.state(find.byType(EditableText)); - expect(tester.testTextInput.editingState['text'], equals('')); - expect(state.wantKeepAlive, true); - - // Doesn't eat emojis - state.updateEditingValue(const TextEditingValue(text: '\u{200E}😀😁😂🤣😃 💑 👩‍❤️‍👩 👨‍❤️‍👨 💏 👩‍❤️‍💋‍👩 👨‍❤️‍💋‍👨 👪 👨‍👩‍👧 👨‍👩‍👧‍👦 👨‍👩‍👦‍👦 \u{200F}')); - expect(state.currentTextEditingValue.text, equals('\u{200E}😀😁😂🤣😃 💑 👩‍❤️‍👩 👨‍❤️‍👨 💏 👩‍❤️‍💋‍👩 👨‍❤️‍💋‍👨 👪 👨‍👩‍👧 👨‍👩‍👧‍👦 👨‍👩‍👦‍👦 \u{200F}')); - state.updateEditingValue(const TextEditingValue(text: '\u{200E}🇧🇼🇧🇷🇮🇴 🇻🇬🇧🇳wahhh!🇧🇬🇧🇫 🇧🇮🇰🇭عَ عَ 🇨🇲 🇨🇦🇮🇨 🇨🇻🇧🇶 🇰🇾🇨🇫 🇹🇩🇨🇱 🇨🇳🇨🇽\u{200F}')); - expect(state.currentTextEditingValue.text, equals('\u{200E}🇧🇼🇧🇷🇮🇴 🇻🇬🇧🇳wahhh!🇧🇬🇧🇫 🇧🇮🇰🇭عَ عَ \u{200F}🇨🇲 🇨🇦🇮🇨 🇨🇻🇧🇶 🇰🇾🇨🇫 🇹🇩🇨🇱 🇨🇳🇨🇽\u{200F}')); - }); - - testWidgets('Whitespace directionality formatter emojis', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(text: 'testText'); - await tester.pumpWidget( - MediaQuery( - data: const MediaQueryData(devicePixelRatio: 1.0), - child: Directionality( - textDirection: TextDirection.ltr, - child: FocusScope( - node: focusScopeNode, - autofocus: true, - child: EditableText( - backgroundCursorColor: Colors.blue, - controller: controller, - focusNode: focusNode, - maxLines: 1, // Sets text keyboard implicitly. - style: textStyle, - cursorColor: cursorColor, - ), - ), - ), - ), - ); - - await tester.tap(find.byType(EditableText)); - await tester.showKeyboard(find.byType(EditableText)); - controller.text = ''; - await tester.idle(); - - final EditableTextState state = - tester.state(find.byType(EditableText)); - expect(tester.testTextInput.editingState['text'], equals('')); - expect(state.wantKeepAlive, true); - - // Doesn't eat emojis - state.updateEditingValue(const TextEditingValue(text: '\u{200E}😀😁😂🤣😃 💑 👩‍❤️‍👩 👨‍❤️‍👨 💏 👩‍❤️‍💋‍👩 👨‍❤️‍💋‍👨 👪 👨‍👩‍👧 👨‍👩‍👧‍👦 👨‍👩‍👦‍👦 \u{200F}')); - expect(state.currentTextEditingValue.text, equals('\u{200E}😀😁😂🤣😃 💑 👩‍❤️‍👩 👨‍❤️‍👨 💏 👩‍❤️‍💋‍👩 👨‍❤️‍💋‍👨 👪 👨‍👩‍👧 👨‍👩‍👧‍👦 👨‍👩‍👦‍👦 \u{200F}')); - state.updateEditingValue(const TextEditingValue(text: '\u{200E}🇧🇼🇧🇷🇮🇴 🇻🇬🇧🇳wahhh!🇧🇬🇧🇫 🇧🇮🇰🇭عَ عَ 🇨🇲 🇨🇦🇮🇨 🇨🇻🇧🇶 🇰🇾🇨🇫 🇹🇩🇨🇱 🇨🇳🇨🇽\u{200F}')); - expect(state.currentTextEditingValue.text, equals('\u{200E}🇧🇼🇧🇷🇮🇴 🇻🇬🇧🇳wahhh!🇧🇬🇧🇫 🇧🇮🇰🇭عَ عَ \u{200F}🇨🇲 🇨🇦🇮🇨 🇨🇻🇧🇶 🇰🇾🇨🇫 🇹🇩🇨🇱 🇨🇳🇨🇽\u{200F}')); - }); } class MockTextFormatter extends TextInputFormatter { - MockTextFormatter() : formatCallCount = 0, log = []; + MockTextFormatter() : _counter = 0, log = []; - int formatCallCount; + int _counter; List log; @override @@ -4538,8 +4251,8 @@ class MockTextFormatter extends TextInputFormatter { TextEditingValue oldValue, TextEditingValue newValue, ) { - formatCallCount++; - log.add('[$formatCallCount]: ${oldValue.text}, ${newValue.text}'); + _counter++; + log.add('[$_counter]: ${oldValue.text}, ${newValue.text}'); TextEditingValue finalValue; if (newValue.text.length < oldValue.text.length) { finalValue = _handleTextDeletion(oldValue, newValue); @@ -4552,14 +4265,14 @@ class MockTextFormatter extends TextInputFormatter { TextEditingValue _handleTextDeletion( TextEditingValue oldValue, TextEditingValue newValue) { - final String result = 'a' * (formatCallCount - 2); - log.add('[$formatCallCount]: deleting $result'); + final String result = 'a' * (_counter - 2); + log.add('[$_counter]: deleting $result'); return TextEditingValue(text: result); } TextEditingValue _formatText(TextEditingValue value) { - final String result = 'a' * formatCallCount * 2; - log.add('[$formatCallCount]: normal $result'); + final String result = 'a' * _counter * 2; + log.add('[$_counter]: normal $result'); return TextEditingValue(text: result); } }