Skip to content

Commit aa6a0a9

Browse files
authored
Fix EditableText misplaces caret when selection is invalid (#123777)
Fix EditableText misplaces caret when selection is invalid
1 parent 1ea013d commit aa6a0a9

File tree

5 files changed

+183
-121
lines changed

5 files changed

+183
-121
lines changed

packages/flutter/lib/src/widgets/editable_text.dart

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3706,6 +3706,15 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
37063706
}
37073707

37083708
void _didChangeTextEditingValue() {
3709+
if (_hasFocus && !_value.selection.isValid) {
3710+
// If this field is focused and the selection is invalid, place the cursor at
3711+
// the end. Does not rely on _handleFocusChanged because it makes selection
3712+
// handles visible on Android.
3713+
// Unregister as a listener to the text controller while making the change.
3714+
widget.controller.removeListener(_didChangeTextEditingValue);
3715+
widget.controller.selection = _adjustedSelectionWhenFocused()!;
3716+
widget.controller.addListener(_didChangeTextEditingValue);
3717+
}
37093718
_updateRemoteEditingValueIfNeeded();
37103719
_startOrStopCursorTimerIfNeeded();
37113720
_updateOrDisposeSelectionOverlayIfNeeded();
@@ -3726,21 +3735,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
37263735
if (!widget.readOnly) {
37273736
_scheduleShowCaretOnScreen(withAnimation: true);
37283737
}
3729-
final bool shouldSelectAll = widget.selectionEnabled && kIsWeb
3730-
&& !_isMultiline && !_nextFocusChangeIsInternal;
3731-
if (shouldSelectAll) {
3732-
// On native web, single line <input> tags select all when receiving
3733-
// focus.
3734-
_handleSelectionChanged(
3735-
TextSelection(
3736-
baseOffset: 0,
3737-
extentOffset: _value.text.length,
3738-
),
3739-
null,
3740-
);
3741-
} else if (!_value.selection.isValid) {
3742-
// Place cursor at the end if the selection is invalid when we receive focus.
3743-
_handleSelectionChanged(TextSelection.collapsed(offset: _value.text.length), null);
3738+
final TextSelection? updatedSelection = _adjustedSelectionWhenFocused();
3739+
if (updatedSelection != null) {
3740+
_handleSelectionChanged(updatedSelection, null);
37443741
}
37453742
} else {
37463743
WidgetsBinding.instance.removeObserver(this);
@@ -3749,6 +3746,24 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
37493746
updateKeepAlive();
37503747
}
37513748

3749+
TextSelection? _adjustedSelectionWhenFocused() {
3750+
TextSelection? selection;
3751+
final bool shouldSelectAll = widget.selectionEnabled && kIsWeb
3752+
&& !_isMultiline && !_nextFocusChangeIsInternal;
3753+
if (shouldSelectAll) {
3754+
// On native web, single line <input> tags select all when receiving
3755+
// focus.
3756+
selection = TextSelection(
3757+
baseOffset: 0,
3758+
extentOffset: _value.text.length,
3759+
);
3760+
} else if (!_value.selection.isValid) {
3761+
// Place cursor at the end if the selection is invalid when we receive focus.
3762+
selection = TextSelection.collapsed(offset: _value.text.length);
3763+
}
3764+
return selection;
3765+
}
3766+
37523767
void _compositeCallback(Layer layer) {
37533768
// The callback can be invoked when the layer is detached.
37543769
// The input connection can be closed by the platform in which case this

packages/flutter/test/material/text_field_test.dart

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6744,8 +6744,8 @@ void main() {
67446744
variant: KeySimulatorTransitModeVariant.all()
67456745
);
67466746

6747-
// Regressing test for https://github.com/flutter/flutter/issues/78219
6748-
testWidgets('Paste does not crash when the section is inValid', (WidgetTester tester) async {
6747+
// Regression test for https://github.com/flutter/flutter/issues/78219
6748+
testWidgets('Paste does not crash after calling TextController.text setter', (WidgetTester tester) async {
67496749
final FocusNode focusNode = FocusNode();
67506750
final TextEditingController controller = TextEditingController();
67516751
final TextField textField = TextField(
@@ -6778,7 +6778,7 @@ void main() {
67786778
await tester.tap(find.byType(TextField));
67796779
await tester.pumpAndSettle();
67806780

6781-
// This setter will set `selection` invalid.
6781+
// Clear the text.
67826782
controller.text = '';
67836783

67846784
// Paste clipboardContent to the text field.
@@ -6790,10 +6790,12 @@ void main() {
67906790
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
67916791
await tester.pumpAndSettle();
67926792

6793-
// Do nothing.
6794-
expect(find.text(clipboardContent), findsNothing);
6795-
expect(controller.selection, const TextSelection.collapsed(offset: -1));
6796-
}, variant: KeySimulatorTransitModeVariant.all());
6793+
// Clipboard content is correctly pasted.
6794+
expect(find.text(clipboardContent), findsOneWidget);
6795+
},
6796+
skip: areKeyEventsHandledByPlatform, // [intended] only applies to platforms where we handle key events.
6797+
variant: KeySimulatorTransitModeVariant.all(),
6798+
);
67976799

67986800
testWidgets('Cut test', (WidgetTester tester) async {
67996801
final FocusNode focusNode = FocusNode();

packages/flutter/test/widgets/editable_text_shortcuts_test.dart

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -90,38 +90,6 @@ void main() {
9090
);
9191
}
9292

93-
testWidgets(
94-
'Movement/Deletion shortcuts do nothing when the selection is invalid',
95-
(WidgetTester tester) async {
96-
await tester.pumpWidget(buildEditableText());
97-
controller.text = testText;
98-
controller.selection = const TextSelection.collapsed(offset: -1);
99-
await tester.pump();
100-
101-
const List<LogicalKeyboardKey> triggers = <LogicalKeyboardKey>[
102-
LogicalKeyboardKey.backspace,
103-
LogicalKeyboardKey.delete,
104-
LogicalKeyboardKey.arrowLeft,
105-
LogicalKeyboardKey.arrowRight,
106-
LogicalKeyboardKey.arrowUp,
107-
LogicalKeyboardKey.arrowDown,
108-
LogicalKeyboardKey.pageUp,
109-
LogicalKeyboardKey.pageDown,
110-
LogicalKeyboardKey.home,
111-
LogicalKeyboardKey.end,
112-
];
113-
114-
for (final SingleActivator activator in triggers.expand(allModifierVariants)) {
115-
await sendKeyCombination(tester, activator);
116-
await tester.pump();
117-
expect(controller.text, testText, reason: activator.toString());
118-
expect(controller.selection, const TextSelection.collapsed(offset: -1), reason: activator.toString());
119-
}
120-
},
121-
skip: kIsWeb, // [intended] on web these keys are handled by the browser.
122-
variant: TargetPlatformVariant.all(),
123-
);
124-
12593
group('Common text editing shortcuts: ',
12694
() {
12795
final TargetPlatformVariant allExceptApple = TargetPlatformVariant.all(excluding: <TargetPlatform>{TargetPlatform.macOS, TargetPlatform.iOS});

0 commit comments

Comments
 (0)