mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Revert repeater (#52752)
Reverts repeat filter logic for text formatter
This commit is contained in:
parent
d1b99f43b0
commit
08768e87e4
@ -1230,9 +1230,6 @@ class EditableTextState extends State<EditableText> 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<EditableText> 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;
|
||||
|
||||
@ -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: <TextInputFormatter>[formatter],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byType(EditableText));
|
||||
await tester.showKeyboard(find.byType(EditableText));
|
||||
controller.text = '';
|
||||
await tester.idle();
|
||||
|
||||
final EditableTextState state =
|
||||
tester.state<EditableTextState>(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<String> referenceLog = <String>[
|
||||
'[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<EditableTextState>(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<EditableTextState>(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<EditableTextState>(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<EditableTextState>(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<EditableTextState>(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 = <String>[];
|
||||
MockTextFormatter() : _counter = 0, log = <String>[];
|
||||
|
||||
int formatCallCount;
|
||||
int _counter;
|
||||
List<String> 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);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user