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
}
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();
_startOrStopCursorTimerIfNeeded();
_updateOrDisposeSelectionOverlayIfNeeded();
......@@ -3726,21 +3735,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (!widget.readOnly) {
_scheduleShowCaretOnScreen(withAnimation: true);
}
final bool shouldSelectAll = widget.selectionEnabled && kIsWeb
&& !_isMultiline && !_nextFocusChangeIsInternal;
if (shouldSelectAll) {
// On native web, single line <input> tags select all when receiving
// focus.
_handleSelectionChanged(
TextSelection(
baseOffset: 0,
extentOffset: _value.text.length,
),
null,
);
} else if (!_value.selection.isValid) {
// Place cursor at the end if the selection is invalid when we receive focus.
_handleSelectionChanged(TextSelection.collapsed(offset: _value.text.length), null);
final TextSelection? updatedSelection = _adjustedSelectionWhenFocused();
if (updatedSelection != null) {
_handleSelectionChanged(updatedSelection, null);
}
} else {
WidgetsBinding.instance.removeObserver(this);
......@@ -3749,6 +3746,24 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
updateKeepAlive();
}
TextSelection? _adjustedSelectionWhenFocused() {
TextSelection? selection;
final bool shouldSelectAll = widget.selectionEnabled && kIsWeb
&& !_isMultiline && !_nextFocusChangeIsInternal;
if (shouldSelectAll) {
// On native web, single line <input> tags select all when receiving
// focus.
selection = TextSelection(
baseOffset: 0,
extentOffset: _value.text.length,
);
} else if (!_value.selection.isValid) {
// Place cursor at the end if the selection is invalid when we receive focus.
selection = TextSelection.collapsed(offset: _value.text.length);
}
return selection;
}
void _compositeCallback(Layer layer) {
// The callback can be invoked when the layer is detached.
// The input connection can be closed by the platform in which case this
......
......@@ -6744,8 +6744,8 @@ void main() {
variant: KeySimulatorTransitModeVariant.all()
);
// Regressing test for https://github.com/flutter/flutter/issues/78219
testWidgets('Paste does not crash when the section is inValid', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/78219
testWidgets('Paste does not crash after calling TextController.text setter', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
final TextEditingController controller = TextEditingController();
final TextField textField = TextField(
......@@ -6778,7 +6778,7 @@ void main() {
await tester.tap(find.byType(TextField));
await tester.pumpAndSettle();
// This setter will set `selection` invalid.
// Clear the text.
controller.text = '';
// Paste clipboardContent to the text field.
......@@ -6790,10 +6790,12 @@ void main() {
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
await tester.pumpAndSettle();
// Do nothing.
expect(find.text(clipboardContent), findsNothing);
expect(controller.selection, const TextSelection.collapsed(offset: -1));
}, variant: KeySimulatorTransitModeVariant.all());
// Clipboard content is correctly pasted.
expect(find.text(clipboardContent), findsOneWidget);
},
skip: areKeyEventsHandledByPlatform, // [intended] only applies to platforms where we handle key events.
variant: KeySimulatorTransitModeVariant.all(),
);
testWidgets('Cut test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
......
......@@ -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: ',
() {
final TargetPlatformVariant allExceptApple = TargetPlatformVariant.all(excluding: <TargetPlatform>{TargetPlatform.macOS, TargetPlatform.iOS});
......
......@@ -621,11 +621,13 @@ void main() {
expect(paragraph1.selections.isEmpty, isTrue);
expect(paragraph2.selections.isEmpty, isTrue);
// Reset selection and focus selectable region.
controller.selection = const TextSelection.collapsed(offset: -1);
// Focus selectable region.
selectableRegionFocus.requestFocus();
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.
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, control: true));
expect(controller.selection, const TextSelection.collapsed(offset: -1));
......@@ -672,11 +674,13 @@ void main() {
expect(paragraph1.selections.isEmpty, isTrue);
expect(paragraph2.selections.isEmpty, isTrue);
// Reset selection and focus selectable region.
controller.selection = const TextSelection.collapsed(offset: -1);
// Focus selectable region.
selectableRegionFocus.requestFocus();
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.
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.keyA, meta: true));
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