Skip to content

Commit 09cc1c4

Browse files
authored
Fix cursor disappear on undo. (#122402)
Fix cursor disappear on undo.
1 parent cd351ae commit 09cc1c4

File tree

3 files changed

+68
-14
lines changed

3 files changed

+68
-14
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4510,7 +4510,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
45104510
userUpdateTextEditingValue(value, SelectionChangedCause.keyboard);
45114511
},
45124512
shouldChangeUndoStack: (TextEditingValue? oldValue, TextEditingValue newValue) {
4513-
if (newValue == TextEditingValue.empty) {
4513+
if (!newValue.selection.isValid) {
45144514
return false;
45154515
}
45164516

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

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,19 @@ class UndoHistoryState<T> extends State<UndoHistory<T>> with UndoManagerClient {
100100

101101
@override
102102
void undo() {
103-
_update(_stack.undo());
103+
if (_stack.currentValue == null) {
104+
// Returns early if there is not a first value registered in the history.
105+
// This is important because, if an undo is received while the initial
106+
// value is being pushed (a.k.a when the field gets the focus but the
107+
// throttling delay is pending), the initial push should not be canceled.
108+
return;
109+
}
110+
if (_throttleTimer?.isActive ?? false) {
111+
_throttleTimer?.cancel(); // Cancel ongoing push, if any.
112+
_update(_stack.currentValue);
113+
} else {
114+
_update(_stack.undo());
115+
}
104116
_updateState();
105117
}
106118

@@ -455,27 +467,17 @@ typedef _Throttled<T> = Timer Function(T currentArg);
455467
_Throttled<T> _throttle<T>({
456468
required Duration duration,
457469
required _Throttleable<T> function,
458-
// If true, calls at the start of the timer.
459-
bool leadingEdge = false,
460470
}) {
461471
Timer? timer;
462-
bool calledDuringTimer = false;
463472
late T arg;
464473

465474
return (T currentArg) {
466475
arg = currentArg;
467-
if (timer != null) {
468-
calledDuringTimer = true;
476+
if (timer != null && timer!.isActive) {
469477
return timer!;
470478
}
471-
if (leadingEdge) {
472-
function(arg);
473-
}
474-
calledDuringTimer = false;
475479
timer = Timer(duration, () {
476-
if (!leadingEdge || calledDuringTimer) {
477-
function(arg);
478-
}
480+
function(arg);
479481
timer = null;
480482
});
481483
return timer!;

packages/flutter/test/widgets/editable_text_test.dart

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13011,6 +13011,58 @@ testWidgets('Floating cursor ending with selection', (WidgetTester tester) async
1301113011
// On web, these keyboard shortcuts are handled by the browser.
1301213012
}, variant: TargetPlatformVariant.only(TargetPlatform.android), skip: kIsWeb); // [intended]
1301313013

13014+
// Regression test for https://github.com/flutter/flutter/issues/120194.
13015+
testWidgets('Cursor does not jump after undo', (WidgetTester tester) async {
13016+
// Initialize the controller with a non empty text.
13017+
final TextEditingController controller = TextEditingController(text: textA);
13018+
final FocusNode focusNode = FocusNode();
13019+
await tester.pumpWidget(boilerplate(controller, focusNode));
13020+
13021+
// Focus the field and wait for throttling delay to get the initial
13022+
// state saved in text editing history.
13023+
focusNode.requestFocus();
13024+
await tester.pump();
13025+
await waitForThrottling(tester);
13026+
expect(controller.value, textACollapsedAtEnd);
13027+
13028+
// Insert some text.
13029+
await tester.enterText(find.byType(EditableText), textAB);
13030+
expect(controller.value, textABCollapsedAtEnd);
13031+
13032+
// Undo the insertion without waiting for the throttling delay.
13033+
await sendUndo(tester);
13034+
expect(controller.value.selection.isValid, true);
13035+
expect(controller.value, textACollapsedAtEnd);
13036+
13037+
// On web, these keyboard shortcuts are handled by the browser.
13038+
}, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended]
13039+
13040+
testWidgets('Initial value is recorded when an undo is received just after getting the focus', (WidgetTester tester) async {
13041+
// Initialize the controller with a non empty text.
13042+
final TextEditingController controller = TextEditingController(text: textA);
13043+
final FocusNode focusNode = FocusNode();
13044+
await tester.pumpWidget(boilerplate(controller, focusNode));
13045+
13046+
// Focus the field and do not wait for throttling delay before calling undo.
13047+
focusNode.requestFocus();
13048+
await tester.pump();
13049+
await sendUndo(tester);
13050+
await waitForThrottling(tester);
13051+
expect(controller.value, textACollapsedAtEnd);
13052+
13053+
// Insert some text.
13054+
await tester.enterText(find.byType(EditableText), textAB);
13055+
expect(controller.value, textABCollapsedAtEnd);
13056+
13057+
// Undo the insertion.
13058+
await sendUndo(tester);
13059+
13060+
// Initial text should have been recorded and restored.
13061+
expect(controller.value, textACollapsedAtEnd);
13062+
13063+
// On web, these keyboard shortcuts are handled by the browser.
13064+
}, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended]
13065+
1301413066
testWidgets('Can make changes in the middle of the history', (WidgetTester tester) async {
1301513067
final TextEditingController controller = TextEditingController();
1301613068
final FocusNode focusNode = FocusNode();

0 commit comments

Comments
 (0)