Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
0b1832ba
Unverified
Commit
0b1832ba
authored
Sep 11, 2021
by
LongCatIsLooong
Committed by
GitHub
Sep 11, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Make `FilteringTextInputFormatter`'s filtering Selection/Composing Region agnostic (#89327)
parent
b2550fe5
Changes
3
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
426 additions
and
77 deletions
+426
-77
text_formatter.dart
packages/flutter/lib/src/services/text_formatter.dart
+162
-76
text_input.dart
packages/flutter/lib/src/services/text_input.dart
+29
-0
text_formatter_test.dart
packages/flutter/test/services/text_formatter_test.dart
+235
-1
No files found.
packages/flutter/lib/src/services/text_formatter.dart
View file @
0b1832ba
...
...
@@ -121,6 +121,94 @@ class _SimpleTextInputFormatter extends TextInputFormatter {
}
}
// A mutable, half-open range [`base`, `extent`) within a string.
class
_MutableTextRange
{
_MutableTextRange
(
this
.
base
,
this
.
extent
);
static
_MutableTextRange
?
fromComposingRange
(
TextRange
range
)
{
return
range
.
isValid
&&
!
range
.
isCollapsed
?
_MutableTextRange
(
range
.
start
,
range
.
end
)
:
null
;
}
static
_MutableTextRange
?
fromTextSelection
(
TextSelection
selection
)
{
return
selection
.
isValid
?
_MutableTextRange
(
selection
.
baseOffset
,
selection
.
extentOffset
)
:
null
;
}
/// The start index of the range, inclusive.
///
/// The value of [base] should always be greater than or equal to 0, and can
/// be larger than, smaller than, or equal to [extent].
int
base
;
/// The end index of the range, exclusive.
///
/// The value of [extent] should always be greater than or equal to 0, and can
/// be larger than, smaller than, or equal to [base].
int
extent
;
}
// The intermediate state of a [FilteringTextInputFormatter] when it's
// formatting a new user input.
class
_TextEditingValueAccumulator
{
_TextEditingValueAccumulator
(
this
.
inputValue
)
:
selection
=
_MutableTextRange
.
fromTextSelection
(
inputValue
.
selection
),
composingRegion
=
_MutableTextRange
.
fromComposingRange
(
inputValue
.
composing
);
// The original string that was sent to the [FilteringTextInputFormatter] as
// input.
final
TextEditingValue
inputValue
;
/// The [StringBuffer] that contains the string which has already been
/// formatted.
///
/// In a [FilteringTextInputFormatter], typically the replacement string,
/// instead of the original string within the given range, is written to this
/// [StringBuffer].
final
StringBuffer
stringBuffer
=
StringBuffer
();
/// The updated selection, as well as the original selection from the input
/// [TextEditingValue] of the [FilteringTextInputFormatter].
///
/// This parameter will be null if the input [TextEditingValue.selection] is
/// invalid.
final
_MutableTextRange
?
selection
;
/// The updated composing region, as well as the original composing region
/// from the input [TextEditingValue] of the [FilteringTextInputFormatter].
///
/// This parameter will be null if the input [TextEditingValue.composing] is
/// invalid or collapsed.
final
_MutableTextRange
?
composingRegion
;
// Whether this state object has reached its end-of-life.
bool
debugFinalized
=
false
;
TextEditingValue
finalize
()
{
debugFinalized
=
true
;
final
_MutableTextRange
?
selection
=
this
.
selection
;
final
_MutableTextRange
?
composingRegion
=
this
.
composingRegion
;
return
TextEditingValue
(
text:
stringBuffer
.
toString
(),
composing:
composingRegion
==
null
||
composingRegion
.
base
==
composingRegion
.
extent
?
TextRange
.
empty
:
TextRange
(
start:
composingRegion
.
base
,
end:
composingRegion
.
extent
),
selection:
selection
==
null
?
const
TextSelection
.
collapsed
(
offset:
-
1
)
:
TextSelection
(
baseOffset:
selection
.
base
,
extentOffset:
selection
.
extent
,
// Try to preserve the selection affinity and isDirectional. This
// may not make sense if the selection has changed.
affinity:
inputValue
.
selection
.
affinity
,
isDirectional:
inputValue
.
selection
.
isDirectional
,
),
);
}
}
/// A [TextInputFormatter] that prevents the insertion of characters
/// matching (or not matching) a particular pattern.
///
...
...
@@ -159,33 +247,26 @@ class FilteringTextInputFormatter extends TextInputFormatter {
/// The [filterPattern] and [replacementString] arguments
/// must not be null.
FilteringTextInputFormatter
.
allow
(
this
.
filterPattern
,
{
this
.
replacementString
=
''
,
})
:
assert
(
filterPattern
!=
null
),
assert
(
replacementString
!=
null
),
allow
=
true
;
Pattern
filterPattern
,
{
String
replacementString
=
''
,
})
:
this
(
filterPattern
,
allow:
true
,
replacementString:
replacementString
);
/// Creates a formatter that blocks characters matching a pattern.
///
/// The [filterPattern] and [replacementString] arguments
/// must not be null.
FilteringTextInputFormatter
.
deny
(
this
.
filterPattern
,
{
this
.
replacementString
=
''
,
})
:
assert
(
filterPattern
!=
null
),
assert
(
replacementString
!=
null
),
allow
=
false
;
Pattern
filterPattern
,
{
String
replacementString
=
''
,
})
:
this
(
filterPattern
,
allow:
false
,
replacementString:
replacementString
);
/// A [Pattern] to match
and
replace in incoming [TextEditingValue]s.
/// A [Pattern] to match
or
replace in incoming [TextEditingValue]s.
///
/// The behavior of the pattern depends on the [allow] property. If
/// it is true, then this is an allow list, specifying a pattern that
/// characters must match to be accepted. Otherwise, it is a deny list,
/// specifying a pattern that characters must not match to be accepted.
///
/// In general, the pattern should only match one character at a
/// time. See the discussion at [replacementString].
///
/// {@tool snippet}
/// Typically the pattern is a regular expression, as in:
///
...
...
@@ -246,16 +327,18 @@ class FilteringTextInputFormatter extends TextInputFormatter {
/// string) because both of the "o"s would be matched simultaneously
/// by the pattern.
///
/// Additionally, each segment of the string before, during, and
/// after the current selection in the [TextEditingValue] is handled
/// separately. This means that, in the case of the "Into the Woods"
/// example above, if the selection ended between the two "o"s in
/// "Woods", even if the pattern was `RegExp('o+')`, the result
/// would be "Int* the W**ds", since the two "o"s would be handled
/// in separate passes.
///
/// See also [String.splitMapJoin], which is used to implement this
/// behavior in both cases.
/// The filter may adjust the selection and the composing region of the text
/// after applying the text replacement, such that they still cover the same
/// text. For instance, if the pattern was `o+` and the last character "s" was
/// selected: "Into The Wood|s|", then the result will be "Into The W*d|s|",
/// with the selection still around the same character "s" despite that it is
/// now the 12th character.
///
/// In the case where one end point of the selection (or the composing region)
/// is strictly inside the banned pattern (for example, "Into The |Wo|ods"),
/// that endpoint will be moved to the end of the replacement string (it will
/// become "Into The |W*|ds" if the pattern was `o+` and the original text and
/// selection were "Into The |Wo|ods").
final
String
replacementString
;
@override
...
...
@@ -263,16 +346,61 @@ class FilteringTextInputFormatter extends TextInputFormatter {
TextEditingValue
oldValue
,
// unused.
TextEditingValue
newValue
,
)
{
return
_selectionAwareTextManipulation
(
newValue
,
(
String
substring
)
{
return
substring
.
splitMapJoin
(
filterPattern
,
onMatch:
!
allow
?
(
Match
match
)
=>
replacementString
:
null
,
onNonMatch:
allow
?
(
String
nonMatch
)
=>
nonMatch
.
isNotEmpty
?
replacementString
:
''
:
null
,
);
},
);
final
_TextEditingValueAccumulator
formatState
=
_TextEditingValueAccumulator
(
newValue
);
assert
(!
formatState
.
debugFinalized
);
final
Iterable
<
Match
>
matches
=
filterPattern
.
allMatches
(
newValue
.
text
);
Match
?
previousMatch
;
for
(
final
Match
match
in
matches
)
{
assert
(
match
.
end
>
match
.
start
);
// Compute the non-match region between this `Match` and the previous
// `Match`. Depending on the value of `allow`, either the match region or
// the non-match region is the banned pattern.
//
// The non-matching region.
_processRegion
(
allow
,
previousMatch
?.
end
??
0
,
match
.
start
,
formatState
);
assert
(!
formatState
.
debugFinalized
);
// The matched region.
_processRegion
(!
allow
,
match
.
start
,
match
.
end
,
formatState
);
assert
(!
formatState
.
debugFinalized
);
previousMatch
=
match
;
}
// Handle the last non-matching region between the last match region and the
// end of the text.
_processRegion
(
allow
,
previousMatch
?.
end
??
0
,
newValue
.
text
.
length
,
formatState
);
assert
(!
formatState
.
debugFinalized
);
return
formatState
.
finalize
();
}
void
_processRegion
(
bool
isBannedRegion
,
int
regionStart
,
int
regionEnd
,
_TextEditingValueAccumulator
state
)
{
final
String
replacementString
=
isBannedRegion
?
(
regionStart
==
regionEnd
?
''
:
this
.
replacementString
)
:
state
.
inputValue
.
text
.
substring
(
regionStart
,
regionEnd
);
state
.
stringBuffer
.
write
(
replacementString
);
if
(
replacementString
.
length
==
regionEnd
-
regionStart
)
{
// We don't have to adjust the indices if the replaced string and the
// replacement string have the same length.
return
;
}
int
adjustIndex
(
int
originalIndex
)
{
// Add to the index the length of the replacement if needed.
// The condition of the ternary operator is chosen such that the index
// will be placed **after** the replacement text, if the index was
// strictly inside the banned pattern.
final
int
replacedLength
=
regionStart
>=
originalIndex
?
0
:
replacementString
.
length
;
// Subtract from the index the length of the banned pattern before the index.
return
replacedLength
-
(
originalIndex
.
clamp
(
regionStart
,
regionEnd
)
-
regionStart
);
}
state
.
selection
?.
base
+=
adjustIndex
(
state
.
inputValue
.
selection
.
baseOffset
);
state
.
selection
?.
extent
+=
adjustIndex
(
state
.
inputValue
.
selection
.
extentOffset
);
state
.
composingRegion
?.
base
+=
adjustIndex
(
state
.
inputValue
.
composing
.
start
);
state
.
composingRegion
?.
extent
+=
adjustIndex
(
state
.
inputValue
.
composing
.
end
);
}
/// A [TextInputFormatter] that forces input to be a single line.
...
...
@@ -527,45 +655,3 @@ class LengthLimitingTextInputFormatter extends TextInputFormatter {
}
}
}
TextEditingValue
_selectionAwareTextManipulation
(
TextEditingValue
value
,
String
Function
(
String
substring
)
substringManipulation
,
)
{
final
int
selectionStartIndex
=
value
.
selection
.
start
;
final
int
selectionEndIndex
=
value
.
selection
.
end
;
String
manipulatedText
;
TextSelection
?
manipulatedSelection
;
if
(
selectionStartIndex
<
0
||
selectionEndIndex
<
0
)
{
manipulatedText
=
substringManipulation
(
value
.
text
);
}
else
{
final
String
beforeSelection
=
substringManipulation
(
value
.
text
.
substring
(
0
,
selectionStartIndex
),
);
final
String
inSelection
=
substringManipulation
(
value
.
text
.
substring
(
selectionStartIndex
,
selectionEndIndex
),
);
final
String
afterSelection
=
substringManipulation
(
value
.
text
.
substring
(
selectionEndIndex
),
);
manipulatedText
=
beforeSelection
+
inSelection
+
afterSelection
;
if
(
value
.
selection
.
baseOffset
>
value
.
selection
.
extentOffset
)
{
manipulatedSelection
=
value
.
selection
.
copyWith
(
baseOffset:
beforeSelection
.
length
+
inSelection
.
length
,
extentOffset:
beforeSelection
.
length
,
);
}
else
{
manipulatedSelection
=
value
.
selection
.
copyWith
(
baseOffset:
beforeSelection
.
length
,
extentOffset:
beforeSelection
.
length
+
inSelection
.
length
,
);
}
}
return
TextEditingValue
(
text:
manipulatedText
,
selection:
manipulatedSelection
??
const
TextSelection
.
collapsed
(
offset:
-
1
),
composing:
manipulatedText
==
value
.
text
?
value
.
composing
:
TextRange
.
empty
,
);
}
packages/flutter/lib/src/services/text_input.dart
View file @
0b1832ba
...
...
@@ -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.
...
...
packages/flutter/test/services/text_formatter_test.dart
View file @
0b1832ba
...
...
@@ -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
,
);
});
});
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment