This commit is contained in:
parent
99023f0686
commit
ef7258d66d
@ -697,15 +697,6 @@ class EditableText extends StatefulWidget {
|
|||||||
/// context, the English phrase will be on the right and the Hebrew phrase on
|
/// context, the English phrase will be on the right and the Hebrew phrase on
|
||||||
/// its left.
|
/// its left.
|
||||||
///
|
///
|
||||||
/// When LTR text is entered into an RTL field, or RTL text is entered into an
|
|
||||||
/// LTR field, [LRM](https://en.wikipedia.org/wiki/Left-to-right_mark) or
|
|
||||||
/// [RLM](https://en.wikipedia.org/wiki/Right-to-left_mark) characters will be
|
|
||||||
/// inserted alongside whitespace characters, respectively. This is to
|
|
||||||
/// eliminate ambiguous directionality in whitespace and ensure proper caret
|
|
||||||
/// placement. These characters will affect the length of the string and may
|
|
||||||
/// need to be parsed out when doing things like string comparison with other
|
|
||||||
/// text.
|
|
||||||
///
|
|
||||||
/// Defaults to the ambient [Directionality], if any.
|
/// Defaults to the ambient [Directionality], if any.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final TextDirection? textDirection;
|
final TextDirection? textDirection;
|
||||||
@ -2243,8 +2234,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
_lastBottomViewInset = WidgetsBinding.instance!.window.viewInsets.bottom;
|
_lastBottomViewInset = WidgetsBinding.instance!.window.viewInsets.bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
late final _WhitespaceDirectionalityFormatter _whitespaceFormatter = _WhitespaceDirectionalityFormatter(textDirection: _textDirection);
|
|
||||||
|
|
||||||
void _formatAndSetValue(TextEditingValue value, SelectionChangedCause? cause, {bool userInteraction = false}) {
|
void _formatAndSetValue(TextEditingValue value, SelectionChangedCause? cause, {bool userInteraction = false}) {
|
||||||
// Only apply input formatters if the text has changed (including uncommited
|
// Only apply input formatters if the text has changed (including uncommited
|
||||||
// text in the composing region), or when the user committed the composing
|
// text in the composing region), or when the user committed the composing
|
||||||
@ -2263,14 +2252,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
value,
|
value,
|
||||||
(TextEditingValue newValue, TextInputFormatter formatter) => formatter.formatEditUpdate(_value, newValue),
|
(TextEditingValue newValue, TextInputFormatter formatter) => formatter.formatEditUpdate(_value, newValue),
|
||||||
) ?? value;
|
) ?? value;
|
||||||
|
|
||||||
// Always pass the text through the whitespace directionality formatter to
|
|
||||||
// maintain expected behavior with carets on trailing whitespace.
|
|
||||||
// TODO(LongCatIsLooong): The if statement here is for retaining the
|
|
||||||
// previous behavior. The input formatter logic will be updated in an
|
|
||||||
// upcoming PR.
|
|
||||||
if (widget.inputFormatters?.isNotEmpty ?? false)
|
|
||||||
value = _whitespaceFormatter.formatEditUpdate(_value, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put all optional user callback invocations in a batch edit to prevent
|
// Put all optional user callback invocations in a batch edit to prevent
|
||||||
@ -2885,184 +2866,3 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
..setPromptRectRange(promptRectRange);
|
..setPromptRectRange(promptRectRange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This formatter inserts [Unicode.RLM] and [Unicode.LRM] into the
|
|
||||||
// string in order to preserve expected caret behavior when trailing
|
|
||||||
// whitespace is inserted.
|
|
||||||
//
|
|
||||||
// When typing in a direction that opposes the base direction
|
|
||||||
// of the paragraph, un-enclosed whitespace gets the directionality
|
|
||||||
// of the paragraph. This is often at odds with what is immediately
|
|
||||||
// being typed causing the caret to jump to the wrong side of the text.
|
|
||||||
// This formatter makes use of the RLM and LRM to cause the text
|
|
||||||
// shaper to inherently treat the whitespace as being surrounded
|
|
||||||
// by the directionality of the previous non-whitespace codepoint.
|
|
||||||
class _WhitespaceDirectionalityFormatter extends TextInputFormatter {
|
|
||||||
// The [textDirection] should be the base directionality of the
|
|
||||||
// paragraph/editable.
|
|
||||||
_WhitespaceDirectionalityFormatter({TextDirection? textDirection})
|
|
||||||
: _baseDirection = textDirection,
|
|
||||||
_previousNonWhitespaceDirection = textDirection;
|
|
||||||
|
|
||||||
// Using regex here instead of ICU is suboptimal, but is enough
|
|
||||||
// to produce the correct results for any reasonable input where this
|
|
||||||
// is even relevant. Using full ICU would be a much heavier change,
|
|
||||||
// requiring exposure of the C++ ICU API.
|
|
||||||
//
|
|
||||||
// LTR covers most scripts and symbols, including but not limited to Latin,
|
|
||||||
// ideographic scripts (Chinese, Japanese, etc), Cyrilic, Indic, and
|
|
||||||
// SE Asian scripts.
|
|
||||||
final RegExp _ltrRegExp = RegExp(r'[A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF\u2C00-\uFB1C\uFDFE-\uFE6F\uFEFD-\uFFFF]');
|
|
||||||
// RTL covers Arabic, Hebrew, and other RTL languages such as Urdu,
|
|
||||||
// Aramic, Farsi, Dhivehi.
|
|
||||||
final RegExp _rtlRegExp = RegExp(r'[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]');
|
|
||||||
// Although whitespaces are not the only codepoints that have weak directionality,
|
|
||||||
// these are the primary cause of the caret being misplaced.
|
|
||||||
final RegExp _whitespaceRegExp = RegExp(r'\s');
|
|
||||||
|
|
||||||
final TextDirection? _baseDirection;
|
|
||||||
// Tracks the directionality of the most recently encountered
|
|
||||||
// codepoint that was not whitespace. This becomes the direction of
|
|
||||||
// marker inserted to fully surround ambiguous whitespace.
|
|
||||||
TextDirection? _previousNonWhitespaceDirection;
|
|
||||||
|
|
||||||
// Prevents the formatter from attempting more expensive formatting
|
|
||||||
// operations mixed directionality is found.
|
|
||||||
bool _hasOpposingDirection = false;
|
|
||||||
|
|
||||||
// See [Unicode.RLM] and [Unicode.LRM].
|
|
||||||
//
|
|
||||||
// We do not directly use the [Unicode] constants since they are strings.
|
|
||||||
static const int _rlm = 0x200F;
|
|
||||||
static const int _lrm = 0x200E;
|
|
||||||
|
|
||||||
@override
|
|
||||||
TextEditingValue formatEditUpdate(
|
|
||||||
TextEditingValue oldValue,
|
|
||||||
TextEditingValue newValue,
|
|
||||||
) {
|
|
||||||
// Skip formatting (which can be more expensive) if there are no cases of
|
|
||||||
// mixing directionality. Once a case of mixed directionality is found,
|
|
||||||
// always perform the formatting.
|
|
||||||
if (!_hasOpposingDirection) {
|
|
||||||
_hasOpposingDirection = _baseDirection == TextDirection.ltr ?
|
|
||||||
_rtlRegExp.hasMatch(newValue.text) : _ltrRegExp.hasMatch(newValue.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_hasOpposingDirection) {
|
|
||||||
_previousNonWhitespaceDirection = _baseDirection;
|
|
||||||
|
|
||||||
final List<int> outputCodepoints = <int>[];
|
|
||||||
|
|
||||||
// We add/subtract from these as we insert/remove markers.
|
|
||||||
int selectionBase = newValue.selection.baseOffset;
|
|
||||||
int selectionExtent = newValue.selection.extentOffset;
|
|
||||||
int composingStart = newValue.composing.start;
|
|
||||||
int composingEnd = newValue.composing.end;
|
|
||||||
|
|
||||||
void addToLength() {
|
|
||||||
selectionBase += outputCodepoints.length <= selectionBase ? 1 : 0;
|
|
||||||
selectionExtent += outputCodepoints.length <= selectionExtent ? 1 : 0;
|
|
||||||
|
|
||||||
composingStart += outputCodepoints.length <= composingStart ? 1 : 0;
|
|
||||||
composingEnd += outputCodepoints.length <= composingEnd ? 1 : 0;
|
|
||||||
}
|
|
||||||
void subtractFromLength() {
|
|
||||||
selectionBase -= outputCodepoints.length < selectionBase ? 1 : 0;
|
|
||||||
selectionExtent -= outputCodepoints.length < selectionExtent ? 1 : 0;
|
|
||||||
|
|
||||||
composingStart -= outputCodepoints.length < composingStart ? 1 : 0;
|
|
||||||
composingEnd -= outputCodepoints.length < composingEnd ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
final bool isBackspace = oldValue.text.runes.length - newValue.text.runes.length == 1 &&
|
|
||||||
isDirectionalityMarker(oldValue.text.runes.last) &&
|
|
||||||
oldValue.text.substring(0, oldValue.text.length - 1) == newValue.text;
|
|
||||||
|
|
||||||
bool previousWasWhitespace = false;
|
|
||||||
bool previousWasDirectionalityMarker = false;
|
|
||||||
int? previousNonWhitespaceCodepoint;
|
|
||||||
int index = 0;
|
|
||||||
for (final int codepoint in newValue.text.runes) {
|
|
||||||
if (isWhitespace(codepoint)) {
|
|
||||||
// Only compute the directionality of the non-whitespace
|
|
||||||
// when the value is needed.
|
|
||||||
if (!previousWasWhitespace && previousNonWhitespaceCodepoint != null) {
|
|
||||||
_previousNonWhitespaceDirection = getDirection(previousNonWhitespaceCodepoint);
|
|
||||||
}
|
|
||||||
// If we already added directionality for this run of whitespace,
|
|
||||||
// "shift" the marker added to the end of the whitespace run.
|
|
||||||
if (previousWasWhitespace) {
|
|
||||||
subtractFromLength();
|
|
||||||
outputCodepoints.removeLast();
|
|
||||||
}
|
|
||||||
// Handle trailing whitespace deleting the directionality char instead of the whitespace.
|
|
||||||
if (isBackspace && index == newValue.text.runes.length - 1) {
|
|
||||||
// Do not append the whitespace to the outputCodepoints.
|
|
||||||
subtractFromLength();
|
|
||||||
} else {
|
|
||||||
outputCodepoints.add(codepoint);
|
|
||||||
addToLength();
|
|
||||||
outputCodepoints.add(_previousNonWhitespaceDirection == TextDirection.rtl ? _rlm : _lrm);
|
|
||||||
}
|
|
||||||
|
|
||||||
previousWasWhitespace = true;
|
|
||||||
previousWasDirectionalityMarker = false;
|
|
||||||
} else if (isDirectionalityMarker(codepoint)) {
|
|
||||||
// Handle pre-existing directionality markers. Use pre-existing marker
|
|
||||||
// instead of the one we add.
|
|
||||||
if (previousWasWhitespace) {
|
|
||||||
subtractFromLength();
|
|
||||||
outputCodepoints.removeLast();
|
|
||||||
}
|
|
||||||
outputCodepoints.add(codepoint);
|
|
||||||
|
|
||||||
previousWasWhitespace = false;
|
|
||||||
previousWasDirectionalityMarker = true;
|
|
||||||
} else {
|
|
||||||
// If the whitespace was already enclosed by the same directionality,
|
|
||||||
// we can remove the artificially added marker.
|
|
||||||
if (!previousWasDirectionalityMarker &&
|
|
||||||
previousWasWhitespace &&
|
|
||||||
getDirection(codepoint) == _previousNonWhitespaceDirection) {
|
|
||||||
subtractFromLength();
|
|
||||||
outputCodepoints.removeLast();
|
|
||||||
}
|
|
||||||
// Normal character, track its codepoint add it to the string.
|
|
||||||
previousNonWhitespaceCodepoint = codepoint;
|
|
||||||
outputCodepoints.add(codepoint);
|
|
||||||
|
|
||||||
previousWasWhitespace = false;
|
|
||||||
previousWasDirectionalityMarker = false;
|
|
||||||
}
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
final String formatted = String.fromCharCodes(outputCodepoints);
|
|
||||||
return TextEditingValue(
|
|
||||||
text: formatted,
|
|
||||||
selection: TextSelection(
|
|
||||||
baseOffset: selectionBase,
|
|
||||||
extentOffset: selectionExtent,
|
|
||||||
affinity: newValue.selection.affinity,
|
|
||||||
isDirectional: newValue.selection.isDirectional
|
|
||||||
),
|
|
||||||
composing: TextRange(start: composingStart, end: composingEnd),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return newValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isWhitespace(int value) {
|
|
||||||
return _whitespaceRegExp.hasMatch(String.fromCharCode(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isDirectionalityMarker(int value) {
|
|
||||||
return value == _rlm || value == _lrm;
|
|
||||||
}
|
|
||||||
|
|
||||||
TextDirection getDirection(int value) {
|
|
||||||
// Use the LTR version as short-circuiting will be more efficient since
|
|
||||||
// there are more LTR codepoints.
|
|
||||||
return _ltrRegExp.hasMatch(String.fromCharCode(value)) ? TextDirection.ltr : TextDirection.rtl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -121,6 +121,62 @@ void main() {
|
|||||||
equals(serializedActionName));
|
equals(serializedActionName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regression test for https://github.com/flutter/flutter/issues/34538.
|
||||||
|
testWidgets('RTL arabic correct caret placement after trailing whitespace', (WidgetTester tester) async {
|
||||||
|
final TextEditingController controller = TextEditingController();
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MediaQuery(
|
||||||
|
data: const MediaQueryData(),
|
||||||
|
child: Directionality(
|
||||||
|
textDirection: TextDirection.rtl,
|
||||||
|
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));
|
||||||
|
await tester.idle();
|
||||||
|
|
||||||
|
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
|
||||||
|
|
||||||
|
// Simulates Gboard Persian input.
|
||||||
|
state.updateEditingValue(const TextEditingValue(text: 'گ', selection: TextSelection.collapsed(offset: 1)));
|
||||||
|
await tester.pump();
|
||||||
|
double previousCaretXPosition = state.renderEditable.getLocalRectForCaret(state.textEditingValue.selection.base).left;
|
||||||
|
|
||||||
|
state.updateEditingValue(const TextEditingValue(text: 'گی', selection: TextSelection.collapsed(offset: 2)));
|
||||||
|
await tester.pump();
|
||||||
|
double caretXPosition = state.renderEditable.getLocalRectForCaret(state.textEditingValue.selection.base).left;
|
||||||
|
expect(caretXPosition, lessThan(previousCaretXPosition));
|
||||||
|
previousCaretXPosition = caretXPosition;
|
||||||
|
|
||||||
|
state.updateEditingValue(const TextEditingValue(text: 'گیگ', selection: TextSelection.collapsed(offset: 3)));
|
||||||
|
await tester.pump();
|
||||||
|
caretXPosition = state.renderEditable.getLocalRectForCaret(state.textEditingValue.selection.base).left;
|
||||||
|
expect(caretXPosition, lessThan(previousCaretXPosition));
|
||||||
|
previousCaretXPosition = caretXPosition;
|
||||||
|
|
||||||
|
// Enter a whitespace in a RTL input field moves the caret to the left.
|
||||||
|
state.updateEditingValue(const TextEditingValue(text: 'گیگ ', selection: TextSelection.collapsed(offset: 4)));
|
||||||
|
await tester.pump();
|
||||||
|
caretXPosition = state.renderEditable.getLocalRectForCaret(state.textEditingValue.selection.base).left;
|
||||||
|
expect(caretXPosition, lessThan(previousCaretXPosition));
|
||||||
|
|
||||||
|
expect(state.currentTextEditingValue.text, equals('گیگ '));
|
||||||
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/78550.
|
||||||
|
|
||||||
testWidgets('has expected defaults', (WidgetTester tester) async {
|
testWidgets('has expected defaults', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MediaQuery(
|
MediaQuery(
|
||||||
@ -6136,271 +6192,6 @@ void main() {
|
|||||||
expect(formatter.lastOldValue.text, 'test');
|
expect(formatter.lastOldValue.text, 'test');
|
||||||
});
|
});
|
||||||
|
|
||||||
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 ', composing: TextRange(start: 4, end: 5)));
|
|
||||||
expect(state.currentTextEditingValue.text, equals('hello '));
|
|
||||||
state.updateEditingValue(const TextEditingValue(text: 'hello ا', composing: TextRange(start: 4, end: 6)));
|
|
||||||
expect(state.currentTextEditingValue.text, equals('hello \u{200E}ا'));
|
|
||||||
expect(state.currentTextEditingValue.composing, equals(const TextRange(start: 4, end: 7)));
|
|
||||||
state.updateEditingValue(const TextEditingValue(text: 'hello الْ', composing: TextRange(start: 4, end: 7)));
|
|
||||||
state.updateEditingValue(const TextEditingValue(text: 'hello الْعَ', composing: TextRange(start: 4, end: 8)));
|
|
||||||
state.updateEditingValue(const TextEditingValue(text: 'hello الْعَ ', composing: TextRange(start: 4, end: 9)));
|
|
||||||
expect(state.currentTextEditingValue.text, equals('hello \u{200E}الْعَ \u{200F}'));
|
|
||||||
expect(state.currentTextEditingValue.composing, equals(const TextRange(start: 4, end: 10)));
|
|
||||||
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}'));
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('Whitespace directionality formatter handles deletion of trailing whitespace', (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);
|
|
||||||
|
|
||||||
// Simulate deleting only the trailing RTL mark.
|
|
||||||
state.updateEditingValue(const TextEditingValue(text: 'hello \u{200E}الْعَ بِيَّةُ \u{200F}'));
|
|
||||||
state.updateEditingValue(const TextEditingValue(text: 'hello \u{200E}الْعَ بِيَّةُ '));
|
|
||||||
// The trailing space should be gone here.
|
|
||||||
expect(state.currentTextEditingValue.text, equals('hello \u{200E}الْعَ بِيَّةُ'));
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('EditableText changes mouse cursor when hovered', (WidgetTester tester) async {
|
testWidgets('EditableText changes mouse cursor when hovered', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MediaQuery(
|
MediaQuery(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user