Unverified Commit 0b1832ba authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Make `FilteringTextInputFormatter`'s filtering Selection/Composing Region agnostic (#89327)

parent b2550fe5
......@@ -741,9 +741,38 @@ class TextEditingValue {
final String text;
/// The range of text that is currently selected.
///
/// When [selection] is a [TextSelection] that has the same non-negative
/// `baseOffset` and `extentOffset`, the [selection] property represents the
/// caret position.
///
/// If the current [selection] has a negative `baseOffset` or `extentOffset`,
/// then the text currently does not have a selection or a caret location, and
/// most text editing operations that rely on the current selection (for
/// instance, insert a character at the caret location) will do nothing.
final TextSelection selection;
/// The range of text that is still being composed.
///
/// Composing regions are created by input methods (IMEs) to indicate the text
/// within a certain range is provisional. For instance, the Android Gboard
/// app's English keyboard puts the current word under the caret into a
/// composing region to indicate the word is subject to autocorrect or
/// prediction changes.
///
/// Composing regions can also be used for performing multistage input, which
/// is typically used by IMEs designed for phoetic keyboard to enter
/// ideographic symbols. As an example, many CJK keyboards require the user to
/// enter a latin alphabet sequence and then convert it to CJK characters. On
/// iOS, the default software keyboards do not have a dedicated view to show
/// the unfinished latin sequence, so it's displayed directly in the text
/// field, inside of a composing region.
///
/// The composing region should typically only be changed by the IME, or the
/// user via interacting with the IME.
///
/// If the range represented by this property is [TextRange.empty], then the
/// text is not currently being composed.
final TextRange composing;
/// A value that corresponds to the empty string with no selection and no composing range.
......
......@@ -64,6 +64,7 @@ void main() {
const TextEditingValue(text: 'Int* the W*ds'),
);
// "Into the Wo|ods|"
const TextEditingValue selectedIntoTheWoods = TextEditingValue(text: 'Into the Woods', selection: TextSelection(baseOffset: 11, extentOffset: 14));
expect(
FilteringTextInputFormatter('o', allow: true, replacementString: '*').formatEditUpdate(testOldValue, selectedIntoTheWoods),
......@@ -79,7 +80,7 @@ void main() {
);
expect(
FilteringTextInputFormatter(RegExp('o+'), allow: false, replacementString: '*').formatEditUpdate(testOldValue, selectedIntoTheWoods),
const TextEditingValue(text: 'Int* the W**ds', selection: TextSelection(baseOffset: 11, extentOffset: 14)),
const TextEditingValue(text: 'Int* the W*ds', selection: TextSelection(baseOffset: 11, extentOffset: 13)),
);
});
......@@ -624,4 +625,237 @@ void main() {
// cursor must be now at fourth position (right after the number 9)
expect(formatted.selection.baseOffset, equals(4));
});
test('FilteringTextInputFormatter should filter independent of selection', () {
// Regression test for https://github.com/flutter/flutter/issues/80842.
final TextInputFormatter formatter = FilteringTextInputFormatter.deny('abc', replacementString: '*');
const TextEditingValue oldValue = TextEditingValue.empty;
const TextEditingValue newValue = TextEditingValue(text: 'abcabcabc');
final String filteredText = formatter.formatEditUpdate(oldValue, newValue).text;
for (int i = 0; i < newValue.text.length; i += 1) {
final String text = formatter.formatEditUpdate(
oldValue,
newValue.copyWith(selection: TextSelection.collapsed(offset: i)),
).text;
expect(filteredText, text);
}
});
test('FilteringTextInputFormatter should filter independent of composingRegion', () {
final TextInputFormatter formatter = FilteringTextInputFormatter.deny('abc', replacementString: '*');
const TextEditingValue oldValue = TextEditingValue.empty;
const TextEditingValue newValue = TextEditingValue(text: 'abcabcabc');
final String filteredText = formatter.formatEditUpdate(oldValue, newValue).text;
for (int i = 0; i < newValue.text.length; i += 1) {
final String text = formatter.formatEditUpdate(
oldValue,
newValue.copyWith(composing: TextRange.collapsed(i)),
).text;
expect(filteredText, text);
}
});
group('FilteringTextInputFormatter region', () {
const TextEditingValue oldValue = TextEditingValue.empty;
test('Preserves selection region', () {
const TextEditingValue newValue = TextEditingValue(text: 'AAABBBCCC');
// AAA | BBB | CCC => AAA | **** | CCC
expect(
FilteringTextInputFormatter.deny('BBB', replacementString: '****').formatEditUpdate(
oldValue,
newValue.copyWith(
selection: const TextSelection(baseOffset: 6, extentOffset: 3),
),
).selection,
const TextSelection(baseOffset: 7, extentOffset: 3),
);
// AAA | BBB CCC | => AAA | **** CCC |
expect(
FilteringTextInputFormatter.deny('BBB', replacementString: '****').formatEditUpdate(
oldValue,
newValue.copyWith(
selection: const TextSelection(baseOffset: 9, extentOffset: 3),
),
).selection,
const TextSelection(baseOffset: 10, extentOffset: 3),
);
// AAA BBB | CCC | => AAA **** | CCC |
expect(
FilteringTextInputFormatter.deny('BBB', replacementString: '****').formatEditUpdate(
oldValue,
newValue.copyWith(
selection: const TextSelection(baseOffset: 9, extentOffset: 6),
),
).selection,
const TextSelection(baseOffset: 10, extentOffset: 7),
);
// AAAB | B | BCCC => AAA***|CCC
// Same length replacement, keep the selection at where it is.
expect(
FilteringTextInputFormatter.deny('BBB', replacementString: '***').formatEditUpdate(
oldValue,
newValue.copyWith(
selection: const TextSelection(baseOffset: 5, extentOffset: 4),
),
).selection,
const TextSelection(baseOffset: 5, extentOffset: 4),
);
// AAA | BBB | CCC => AAA | CCC
expect(
FilteringTextInputFormatter.deny('BBB', replacementString: '').formatEditUpdate(
oldValue,
newValue.copyWith(
selection: const TextSelection(baseOffset: 6, extentOffset: 3),
),
).selection,
const TextSelection(baseOffset: 3, extentOffset: 3),
);
expect(
FilteringTextInputFormatter.deny('BBB', replacementString: '').formatEditUpdate(
oldValue,
newValue.copyWith(
selection: const TextSelection(baseOffset: 6, extentOffset: 3),
),
).selection,
const TextSelection(baseOffset: 3, extentOffset: 3),
);
// The unfortunate case, we don't know for sure where to put the selection
// so put it after the replacement string.
// AAAB|B|BCCC => AAA****|CCC
expect(
FilteringTextInputFormatter.deny('BBB', replacementString: '****').formatEditUpdate(
oldValue,
newValue.copyWith(
selection: const TextSelection(baseOffset: 5, extentOffset: 4),
),
).selection,
const TextSelection(baseOffset: 7, extentOffset: 7),
);
});
test('Preserves selection region, allow', () {
const TextEditingValue newValue = TextEditingValue(text: 'AAABBBCCC');
// AAA | BBB | CCC => **** | BBB | ****
expect(
FilteringTextInputFormatter.allow('BBB', replacementString: '****').formatEditUpdate(
oldValue,
newValue.copyWith(
selection: const TextSelection(baseOffset: 6, extentOffset: 3),
),
).selection,
const TextSelection(baseOffset: 7, extentOffset: 4),
);
// | AAABBBCCC | => | ****BBB**** |
expect(
FilteringTextInputFormatter.allow('BBB', replacementString: '****').formatEditUpdate(
oldValue,
newValue.copyWith(
selection: const TextSelection(baseOffset: 9, extentOffset: 0),
),
).selection,
const TextSelection(baseOffset: 11, extentOffset: 0),
);
// AAABBB | CCC | => ****BBB | **** |
expect(
FilteringTextInputFormatter.allow('BBB', replacementString: '****').formatEditUpdate(
oldValue,
newValue.copyWith(
selection: const TextSelection(baseOffset: 9, extentOffset: 6),
),
).selection,
const TextSelection(baseOffset: 11, extentOffset: 7),
);
// Overlapping matches: AAA | BBBBB | CCC => | BBB |
expect(
FilteringTextInputFormatter.allow('BBB', replacementString: '').formatEditUpdate(
oldValue,
const TextEditingValue(
text: 'AAABBBBBCCC',
selection: TextSelection(baseOffset: 8, extentOffset: 3),
),
).selection,
const TextSelection(baseOffset: 3, extentOffset: 0),
);
});
test('Preserves composing region', () {
const TextEditingValue newValue = TextEditingValue(text: 'AAABBBCCC');
// AAA | BBB | CCC => AAA | **** | CCC
expect(
FilteringTextInputFormatter.deny('BBB', replacementString: '****').formatEditUpdate(
oldValue,
newValue.copyWith(
composing: const TextRange(start: 3, end: 6),
),
).composing,
const TextRange(start: 3, end: 7),
);
// AAA | BBB CCC | => AAA | **** CCC |
expect(
FilteringTextInputFormatter.deny('BBB', replacementString: '****').formatEditUpdate(
oldValue,
newValue.copyWith(
composing: const TextRange(start: 3, end: 9),
),
).composing,
const TextRange(start: 3, end: 10),
);
// AAA BBB | CCC | => AAA **** | CCC |
expect(
FilteringTextInputFormatter.deny('BBB', replacementString: '****').formatEditUpdate(
oldValue,
newValue.copyWith(
composing: const TextRange(start: 6, end: 9),
),
).composing,
const TextRange(start: 7, end: 10),
);
// AAAB | B | BCCC => AAA*** | CCC
// Same length replacement, don't move the composing region.
expect(
FilteringTextInputFormatter.deny('BBB', replacementString: '***').formatEditUpdate(
oldValue,
newValue.copyWith(
composing: const TextRange(start: 4, end: 5),
),
).composing,
const TextRange(start: 4, end: 5),
);
// AAA | BBB | CCC => | AAA CCC
expect(
FilteringTextInputFormatter.deny('BBB', replacementString: '').formatEditUpdate(
oldValue,
newValue.copyWith(
composing: const TextRange(start: 3, end: 6),
),
).composing,
TextRange.empty,
);
});
});
}
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