Unverified Commit aa6a0a9d authored by Bruno Leroux's avatar Bruno Leroux Committed by GitHub

Fix EditableText misplaces caret when selection is invalid (#123777)

Fix EditableText misplaces caret when selection is invalid
parent 1ea013dc
...@@ -3706,6 +3706,15 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -3706,6 +3706,15 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
void _didChangeTextEditingValue() { void _didChangeTextEditingValue() {
if (_hasFocus && !_value.selection.isValid) {
// If this field is focused and the selection is invalid, place the cursor at
// the end. Does not rely on _handleFocusChanged because it makes selection
// handles visible on Android.
// Unregister as a listener to the text controller while making the change.
widget.controller.removeListener(_didChangeTextEditingValue);
widget.controller.selection = _adjustedSelectionWhenFocused()!;
widget.controller.addListener(_didChangeTextEditingValue);
}
_updateRemoteEditingValueIfNeeded(); _updateRemoteEditingValueIfNeeded();
_startOrStopCursorTimerIfNeeded(); _startOrStopCursorTimerIfNeeded();
_updateOrDisposeSelectionOverlayIfNeeded(); _updateOrDisposeSelectionOverlayIfNeeded();
...@@ -3726,27 +3735,33 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -3726,27 +3735,33 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (!widget.readOnly) { if (!widget.readOnly) {
_scheduleShowCaretOnScreen(withAnimation: true); _scheduleShowCaretOnScreen(withAnimation: true);
} }
final TextSelection? updatedSelection = _adjustedSelectionWhenFocused();
if (updatedSelection != null) {
_handleSelectionChanged(updatedSelection, null);
}
} else {
WidgetsBinding.instance.removeObserver(this);
setState(() { _currentPromptRectRange = null; });
}
updateKeepAlive();
}
TextSelection? _adjustedSelectionWhenFocused() {
TextSelection? selection;
final bool shouldSelectAll = widget.selectionEnabled && kIsWeb final bool shouldSelectAll = widget.selectionEnabled && kIsWeb
&& !_isMultiline && !_nextFocusChangeIsInternal; && !_isMultiline && !_nextFocusChangeIsInternal;
if (shouldSelectAll) { if (shouldSelectAll) {
// On native web, single line <input> tags select all when receiving // On native web, single line <input> tags select all when receiving
// focus. // focus.
_handleSelectionChanged( selection = TextSelection(
TextSelection(
baseOffset: 0, baseOffset: 0,
extentOffset: _value.text.length, extentOffset: _value.text.length,
),
null,
); );
} else if (!_value.selection.isValid) { } else if (!_value.selection.isValid) {
// Place cursor at the end if the selection is invalid when we receive focus. // Place cursor at the end if the selection is invalid when we receive focus.
_handleSelectionChanged(TextSelection.collapsed(offset: _value.text.length), null); selection = TextSelection.collapsed(offset: _value.text.length);
}
} else {
WidgetsBinding.instance.removeObserver(this);
setState(() { _currentPromptRectRange = null; });
} }
updateKeepAlive(); return selection;
} }
void _compositeCallback(Layer layer) { void _compositeCallback(Layer layer) {
......
...@@ -6744,8 +6744,8 @@ void main() { ...@@ -6744,8 +6744,8 @@ void main() {
variant: KeySimulatorTransitModeVariant.all() variant: KeySimulatorTransitModeVariant.all()
); );
// Regressing test for https://github.com/flutter/flutter/issues/78219 // Regression test for https://github.com/flutter/flutter/issues/78219
testWidgets('Paste does not crash when the section is inValid', (WidgetTester tester) async { testWidgets('Paste does not crash after calling TextController.text setter', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
final TextField textField = TextField( final TextField textField = TextField(
...@@ -6778,7 +6778,7 @@ void main() { ...@@ -6778,7 +6778,7 @@ void main() {
await tester.tap(find.byType(TextField)); await tester.tap(find.byType(TextField));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// This setter will set `selection` invalid. // Clear the text.
controller.text = ''; controller.text = '';
// Paste clipboardContent to the text field. // Paste clipboardContent to the text field.
...@@ -6790,10 +6790,12 @@ void main() { ...@@ -6790,10 +6790,12 @@ void main() {
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Do nothing. // Clipboard content is correctly pasted.
expect(find.text(clipboardContent), findsNothing); expect(find.text(clipboardContent), findsOneWidget);
expect(controller.selection, const TextSelection.collapsed(offset: -1)); },
}, variant: KeySimulatorTransitModeVariant.all()); skip: areKeyEventsHandledByPlatform, // [intended] only applies to platforms where we handle key events.
variant: KeySimulatorTransitModeVariant.all(),
);
testWidgets('Cut test', (WidgetTester tester) async { testWidgets('Cut test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
......
...@@ -90,38 +90,6 @@ void main() { ...@@ -90,38 +90,6 @@ void main() {
); );
} }
testWidgets(
'Movement/Deletion shortcuts do nothing when the selection is invalid',
(WidgetTester tester) async {
await tester.pumpWidget(buildEditableText());
controller.text = testText;
controller.selection = const TextSelection.collapsed(offset: -1);
await tester.pump();
const List<LogicalKeyboardKey> triggers = <LogicalKeyboardKey>[
LogicalKeyboardKey.backspace,
LogicalKeyboardKey.delete,
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowUp,
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.pageUp,
LogicalKeyboardKey.pageDown,
LogicalKeyboardKey.home,
LogicalKeyboardKey.end,
];
for (final SingleActivator activator in triggers.expand(allModifierVariants)) {
await sendKeyCombination(tester, activator);
await tester.pump();
expect(controller.text, testText, reason: activator.toString());
expect(controller.selection, const TextSelection.collapsed(offset: -1), reason: activator.toString());
}
},
skip: kIsWeb, // [intended] on web these keys are handled by the browser.
variant: TargetPlatformVariant.all(),
);
group('Common text editing shortcuts: ', group('Common text editing shortcuts: ',
() { () {
final TargetPlatformVariant allExceptApple = TargetPlatformVariant.all(excluding: <TargetPlatform>{TargetPlatform.macOS, TargetPlatform.iOS}); final TargetPlatformVariant allExceptApple = TargetPlatformVariant.all(excluding: <TargetPlatform>{TargetPlatform.macOS, TargetPlatform.iOS});
......
...@@ -55,6 +55,13 @@ enum HandlePositionInViewport { ...@@ -55,6 +55,13 @@ enum HandlePositionInViewport {
typedef _VoidFutureCallback = Future<void> Function(); typedef _VoidFutureCallback = Future<void> Function();
TextEditingValue collapsedAtEnd(String text) {
return TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: text.length),
);
}
void main() { void main() {
final MockClipboard mockClipboard = MockClipboard(); final MockClipboard mockClipboard = MockClipboard();
TestWidgetsFlutterBinding.ensureInitialized() TestWidgetsFlutterBinding.ensureInitialized()
...@@ -5641,7 +5648,7 @@ void main() { ...@@ -5641,7 +5648,7 @@ void main() {
tester.testTextInput.log.clear(); tester.testTextInput.log.clear();
controller.value = TextEditingValue(text: 'a' * 100, composing: const TextRange(start: 0, end: 10)); controller.value = collapsedAtEnd('a' * 100).copyWith(composing: const TextRange(start: 0, end: 10));
await tester.pump(); await tester.pump();
expect(tester.testTextInput.log, contains( expect(tester.testTextInput.log, contains(
...@@ -9447,7 +9454,7 @@ void main() { ...@@ -9447,7 +9454,7 @@ void main() {
}); });
group('EditableText does not send editing values more than once', () { group('EditableText does not send editing values more than once', () {
final TextEditingController controller = TextEditingController(text: testText); Widget boilerplate(TextEditingController controller) {
final EditableText editableText = EditableText( final EditableText editableText = EditableText(
showSelectionHandles: true, showSelectionHandles: true,
maxLines: 2, maxLines: 2,
...@@ -9458,75 +9465,84 @@ void main() { ...@@ -9458,75 +9465,84 @@ void main() {
style: Typography.material2018().black.titleMedium!.copyWith(fontFamily: 'Roboto'), style: Typography.material2018().black.titleMedium!.copyWith(fontFamily: 'Roboto'),
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
inputFormatters: <TextInputFormatter>[LengthLimitingTextInputFormatter(6)], inputFormatters: <TextInputFormatter>[LengthLimitingTextInputFormatter(6)],
onChanged: (String s) => controller.text += ' onChanged', onChanged: (String s) {
controller.value = collapsedAtEnd('${controller.text} onChanged');
},
); );
final Widget widget = MediaQuery( controller.addListener(() {
if (!controller.text.endsWith('listener')) {
controller.value = collapsedAtEnd('${controller.text} listener');
}
});
return MediaQuery(
data: const MediaQueryData(), data: const MediaQueryData(),
child: Directionality( child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: editableText, child: editableText,
), ),
); );
controller.addListener(() {
if (!controller.text.endsWith('listener')) {
controller.text += ' listener';
} }
});
testWidgets('input from text input plugin', (WidgetTester tester) async { testWidgets('input from text input plugin', (WidgetTester tester) async {
await tester.pumpWidget(widget); final TextEditingController controller = TextEditingController(text: testText);
await tester.pumpWidget(boilerplate(controller));
// Connect. // Connect.
await tester.showKeyboard(find.byType(EditableText)); await tester.showKeyboard(find.byType(EditableText));
tester.testTextInput.log.clear(); tester.testTextInput.log.clear();
final EditableTextState state = tester.state<EditableTextState>(find.byWidget(editableText)); final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
state.updateEditingValue(const TextEditingValue(text: 'remoteremoteremote')); state.updateEditingValue(collapsedAtEnd('remoteremoteremote'));
// Apply in order: length formatter -> listener -> onChanged -> listener. // Apply in order: length formatter -> listener -> onChanged -> listener.
expect(controller.text, 'remote listener onChanged listener'); const String expectedText = 'remote listener onChanged listener';
expect(controller.text, expectedText);
final List<TextEditingValue> updates = tester.testTextInput.log final List<TextEditingValue> updates = tester.testTextInput.log
.where((MethodCall call) => call.method == 'TextInput.setEditingState') .where((MethodCall call) => call.method == 'TextInput.setEditingState')
.map((MethodCall call) => TextEditingValue.fromJSON(call.arguments as Map<String, dynamic>)) .map((MethodCall call) => TextEditingValue.fromJSON(call.arguments as Map<String, dynamic>))
.toList(growable: false); .toList(growable: false);
expect(updates, const <TextEditingValue>[TextEditingValue(text: 'remote listener onChanged listener')]); expect(updates, <TextEditingValue>[collapsedAtEnd(expectedText)]);
tester.testTextInput.log.clear(); tester.testTextInput.log.clear();
// If by coincidence the text input plugin sends the same value back, // If by coincidence the text input plugin sends the same value back,
// do nothing. // do nothing.
state.updateEditingValue(const TextEditingValue(text: 'remote listener onChanged listener')); state.updateEditingValue(collapsedAtEnd(expectedText));
expect(controller.text, 'remote listener onChanged listener'); expect(controller.text, 'remote listener onChanged listener');
expect(tester.testTextInput.log, isEmpty); expect(tester.testTextInput.log, isEmpty);
}); });
testWidgets('input from text selection menu', (WidgetTester tester) async { testWidgets('input from text selection menu', (WidgetTester tester) async {
await tester.pumpWidget(widget); final TextEditingController controller = TextEditingController(text: testText);
await tester.pumpWidget(boilerplate(controller));
// Connect. // Connect.
await tester.showKeyboard(find.byType(EditableText)); await tester.showKeyboard(find.byType(EditableText));
tester.testTextInput.log.clear(); tester.testTextInput.log.clear();
final EditableTextState state = tester.state<EditableTextState>(find.byWidget(editableText)); final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
state.userUpdateTextEditingValue(const TextEditingValue(text: 'remoteremoteremote'), SelectionChangedCause.keyboard); state.userUpdateTextEditingValue(
collapsedAtEnd('remoteremoteremote'),
SelectionChangedCause.keyboard,
);
// Apply in order: length formatter -> listener -> onChanged -> listener.
expect(controller.text, 'remote listener onChanged listener');
final List<TextEditingValue> updates = tester.testTextInput.log final List<TextEditingValue> updates = tester.testTextInput.log
.where((MethodCall call) => call.method == 'TextInput.setEditingState') .where((MethodCall call) => call.method == 'TextInput.setEditingState')
.map((MethodCall call) => TextEditingValue.fromJSON(call.arguments as Map<String, dynamic>)) .map((MethodCall call) => TextEditingValue.fromJSON(call.arguments as Map<String, dynamic>))
.toList(growable: false); .toList(growable: false);
expect(updates, const <TextEditingValue>[TextEditingValue(text: 'remote listener onChanged listener')]); const String expectedText = 'remote listener onChanged listener';
expect(updates, <TextEditingValue>[collapsedAtEnd(expectedText)]);
tester.testTextInput.log.clear(); tester.testTextInput.log.clear();
}); });
testWidgets('input from controller', (WidgetTester tester) async { testWidgets('input from controller', (WidgetTester tester) async {
await tester.pumpWidget(widget); final TextEditingController controller = TextEditingController(text: testText);
await tester.pumpWidget(boilerplate(controller));
// Connect. // Connect.
await tester.showKeyboard(find.byType(EditableText)); await tester.showKeyboard(find.byType(EditableText));
...@@ -9538,7 +9554,7 @@ void main() { ...@@ -9538,7 +9554,7 @@ void main() {
.map((MethodCall call) => TextEditingValue.fromJSON(call.arguments as Map<String, dynamic>)) .map((MethodCall call) => TextEditingValue.fromJSON(call.arguments as Map<String, dynamic>))
.toList(growable: false); .toList(growable: false);
expect(updates, const <TextEditingValue>[TextEditingValue(text: 'remoteremoteremote listener')]); expect(updates, <TextEditingValue>[collapsedAtEnd('remoteremoteremote listener')]);
}); });
testWidgets('input from changing controller', (WidgetTester tester) async { testWidgets('input from changing controller', (WidgetTester tester) async {
...@@ -9825,7 +9841,7 @@ void main() { ...@@ -9825,7 +9841,7 @@ void main() {
)); ));
await tester.showKeyboard(find.byType(EditableText)); await tester.showKeyboard(find.byType(EditableText));
controller.text += '...'; controller.value = collapsedAtEnd('${controller.text}...');
await tester.idle(); await tester.idle();
final List<String> logOrder = <String>[ final List<String> logOrder = <String>[
...@@ -9858,7 +9874,7 @@ void main() { ...@@ -9858,7 +9874,7 @@ void main() {
}); });
final TextInputFormatter formatter = TextInputFormatter.withFunction((TextEditingValue oldValue, TextEditingValue newValue) { final TextInputFormatter formatter = TextInputFormatter.withFunction((TextEditingValue oldValue, TextEditingValue newValue) {
if (newValue.text == 'I will be modified by the formatter.') { if (newValue.text == 'I will be modified by the formatter.') {
newValue = const TextEditingValue(text: 'Flutter is the best!'); newValue = collapsedAtEnd('Flutter is the best!');
} }
return newValue; return newValue;
}); });
...@@ -9925,8 +9941,8 @@ void main() { ...@@ -9925,8 +9941,8 @@ void main() {
methodCall, methodCall,
isMethodCall('TextInput.setEditingState', arguments: <String, dynamic>{ isMethodCall('TextInput.setEditingState', arguments: <String, dynamic>{
'text': 'Flutter is the best!', 'text': 'Flutter is the best!',
'selectionBase': -1, 'selectionBase': 20,
'selectionExtent': -1, 'selectionExtent': 20,
'selectionAffinity': 'TextAffinity.downstream', 'selectionAffinity': 'TextAffinity.downstream',
'selectionIsDirectional': false, 'selectionIsDirectional': false,
'composingBase': -1, 'composingBase': -1,
...@@ -9937,8 +9953,9 @@ void main() { ...@@ -9937,8 +9953,9 @@ void main() {
log.clear(); log.clear();
// setEditingState is called when the [controller.value] is modified by local. // setEditingState is called when the [controller.value] is modified by local.
String text = 'I love flutter!';
setState(() { setState(() {
controller.text = 'I love flutter!'; controller.value = collapsedAtEnd(text);
}); });
expect(log.length, 1); expect(log.length, 1);
methodCall = log[0]; methodCall = log[0];
...@@ -9946,8 +9963,8 @@ void main() { ...@@ -9946,8 +9963,8 @@ void main() {
methodCall, methodCall,
isMethodCall('TextInput.setEditingState', arguments: <String, dynamic>{ isMethodCall('TextInput.setEditingState', arguments: <String, dynamic>{
'text': 'I love flutter!', 'text': 'I love flutter!',
'selectionBase': -1, 'selectionBase': text.length,
'selectionExtent': -1, 'selectionExtent': text.length,
'selectionAffinity': 'TextAffinity.downstream', 'selectionAffinity': 'TextAffinity.downstream',
'selectionIsDirectional': false, 'selectionIsDirectional': false,
'composingBase': -1, 'composingBase': -1,
...@@ -9959,8 +9976,9 @@ void main() { ...@@ -9959,8 +9976,9 @@ void main() {
// Currently `_receivedRemoteTextEditingValue` equals 'I will be modified by the formatter.', // Currently `_receivedRemoteTextEditingValue` equals 'I will be modified by the formatter.',
// setEditingState will be called when set the [controller.value] to `_receivedRemoteTextEditingValue` by local. // setEditingState will be called when set the [controller.value] to `_receivedRemoteTextEditingValue` by local.
text = 'I will be modified by the formatter.';
setState(() { setState(() {
controller.text = 'I will be modified by the formatter.'; controller.value = collapsedAtEnd(text);
}); });
expect(log.length, 1); expect(log.length, 1);
methodCall = log[0]; methodCall = log[0];
...@@ -9968,8 +9986,8 @@ void main() { ...@@ -9968,8 +9986,8 @@ void main() {
methodCall, methodCall,
isMethodCall('TextInput.setEditingState', arguments: <String, dynamic>{ isMethodCall('TextInput.setEditingState', arguments: <String, dynamic>{
'text': 'I will be modified by the formatter.', 'text': 'I will be modified by the formatter.',
'selectionBase': -1, 'selectionBase': text.length,
'selectionExtent': -1, 'selectionExtent': text.length,
'selectionAffinity': 'TextAffinity.downstream', 'selectionAffinity': 'TextAffinity.downstream',
'selectionIsDirectional': false, 'selectionIsDirectional': false,
'composingBase': -1, 'composingBase': -1,
...@@ -9986,7 +10004,7 @@ void main() { ...@@ -9986,7 +10004,7 @@ void main() {
return null; return null;
}); });
final TextInputFormatter formatter = TextInputFormatter.withFunction((TextEditingValue oldValue, TextEditingValue newValue) { final TextInputFormatter formatter = TextInputFormatter.withFunction((TextEditingValue oldValue, TextEditingValue newValue) {
return const TextEditingValue(text: 'Flutter is the best!'); return collapsedAtEnd('Flutter is the best!');
}); });
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
...@@ -10032,11 +10050,8 @@ void main() { ...@@ -10032,11 +10050,8 @@ void main() {
final EditableTextState state = tester.firstState(find.byType(EditableText)); final EditableTextState state = tester.firstState(find.byType(EditableText));
// setEditingState is called when remote value modified by the formatter. // setEditingState is called when remote value modified by the formatter.
state.updateEditingValue(TextEditingValue( state.updateEditingValue(collapsedAtEnd('I will be modified by the formatter.'));
text: 'I will be modified by the formatter.', expect(log.length, 2);
selection: controller.selection,
));
expect(log.length, 1);
expect(log, contains(matchesMethodCall( expect(log, contains(matchesMethodCall(
'TextInput.setEditingState', 'TextInput.setEditingState',
args: allOf( args: allOf(
...@@ -10046,10 +10061,8 @@ void main() { ...@@ -10046,10 +10061,8 @@ void main() {
log.clear(); log.clear();
state.updateEditingValue(const TextEditingValue( state.updateEditingValue(collapsedAtEnd('I will be modified by the formatter.'));
text: 'I will be modified by the formatter.', expect(log.length, 2);
));
expect(log.length, 1);
expect(log, contains(matchesMethodCall( expect(log, contains(matchesMethodCall(
'TextInput.setEditingState', 'TextInput.setEditingState',
args: allOf( args: allOf(
...@@ -10531,9 +10544,10 @@ void main() { ...@@ -10531,9 +10544,10 @@ void main() {
expect(state.wantKeepAlive, true); expect(state.wantKeepAlive, true);
expect(formatter.formatCallCount, 0); expect(formatter.formatCallCount, 0);
state.updateEditingValue(const TextEditingValue(text: 'test')); state.updateEditingValue(collapsedAtEnd('test'));
state.updateEditingValue(const TextEditingValue(text: 'test', composing: TextRange(start: 1, end: 2))); state.updateEditingValue(collapsedAtEnd('test').copyWith(composing: const TextRange(start: 1, end: 2)));
state.updateEditingValue(const TextEditingValue(text: '0')); // pass to formatter once to check the values. // Pass to formatter once to check the values.
state.updateEditingValue(collapsedAtEnd('test'));
expect(formatter.lastOldValue.composing, const TextRange(start: 1, end: 2)); expect(formatter.lastOldValue.composing, const TextRange(start: 1, end: 2));
expect(formatter.lastOldValue.text, 'test'); expect(formatter.lastOldValue.text, 'test');
}); });
...@@ -15528,6 +15542,65 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async ...@@ -15528,6 +15542,65 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
); );
}); });
testWidgets('Selection is updated when the field has focus and the new selection is invalid', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/120631.
final TextEditingController controller = TextEditingController();
controller.text = 'Text';
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: EditableText(
key: ValueKey<String>(controller.text),
controller: controller,
focusNode: focusNode,
style: Typography.material2018().black.titleMedium!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
),
),
);
expect(focusNode.hasFocus, isFalse);
expect(
controller.selection,
const TextSelection.collapsed(offset: -1),
);
// Tab to focus the field.
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pumpAndSettle();
expect(focusNode.hasFocus, isTrue);
expect(
controller.selection,
kIsWeb
? TextSelection(
baseOffset: 0,
extentOffset: controller.text.length,
)
: TextSelection.collapsed(
offset: controller.text.length,
),
);
// Update text without specifying the selection.
controller.text = 'Updated';
// As the TextField is focused the selection should be automatically adjusted.
expect(focusNode.hasFocus, isTrue);
expect(
controller.selection,
kIsWeb
? TextSelection(
baseOffset: 0,
extentOffset: controller.text.length,
)
: TextSelection.collapsed(
offset: controller.text.length,
),
);
});
testWidgets('when having focus stolen between frames on web', (WidgetTester tester) async { testWidgets('when having focus stolen between frames on web', (WidgetTester tester) async {
final TextEditingController controller1 = TextEditingController(); final TextEditingController controller1 = TextEditingController();
controller1.text = 'Text1'; controller1.text = 'Text1';
......
...@@ -621,11 +621,13 @@ void main() { ...@@ -621,11 +621,13 @@ void main() {
expect(paragraph1.selections.isEmpty, isTrue); expect(paragraph1.selections.isEmpty, isTrue);
expect(paragraph2.selections.isEmpty, isTrue); expect(paragraph2.selections.isEmpty, isTrue);
// Reset selection and focus selectable region. // Focus selectable region.
controller.selection = const TextSelection.collapsed(offset: -1);
selectableRegionFocus.requestFocus(); selectableRegionFocus.requestFocus();
await tester.pump(); await tester.pump();
// Reset controller selection once the TextField is unfocused.
controller.selection = const TextSelection.collapsed(offset: -1);
// Make sure keyboard select all will be handled by selectable region now. // Make sure keyboard select all will be handled by selectable region now.
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, control: true)); await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, control: true));
expect(controller.selection, const TextSelection.collapsed(offset: -1)); expect(controller.selection, const TextSelection.collapsed(offset: -1));
...@@ -672,11 +674,13 @@ void main() { ...@@ -672,11 +674,13 @@ void main() {
expect(paragraph1.selections.isEmpty, isTrue); expect(paragraph1.selections.isEmpty, isTrue);
expect(paragraph2.selections.isEmpty, isTrue); expect(paragraph2.selections.isEmpty, isTrue);
// Reset selection and focus selectable region. // Focus selectable region.
controller.selection = const TextSelection.collapsed(offset: -1);
selectableRegionFocus.requestFocus(); selectableRegionFocus.requestFocus();
await tester.pump(); await tester.pump();
// Reset controller selection once the TextField is unfocused.
controller.selection = const TextSelection.collapsed(offset: -1);
// Make sure keyboard select all will be handled by selectable region now. // Make sure keyboard select all will be handled by selectable region now.
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, meta: true)); await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, meta: true));
expect(controller.selection, const TextSelection.collapsed(offset: -1)); expect(controller.selection, const TextSelection.collapsed(offset: -1));
......
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