Fix rendereditable to check the latest text before setting the selection (#78919)
* Fix rendereditable to check the latest text before setting the selection
* add regression comment
* addressing comments and fix tests
diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart
index 02eda54..86c3ed2 100644
--- a/packages/flutter/lib/src/rendering/editable.dart
+++ b/packages/flutter/lib/src/rendering/editable.dart
@@ -564,6 +564,21 @@
}
void _setSelection(TextSelection nextSelection, SelectionChangedCause cause) {
+ if (nextSelection.isValid) {
+ // The nextSelection is calculated based on _plainText, which can be out
+ // of sync with the textSelectionDelegate.textEditingValue by one frame.
+ // This is due to the render editable and editable text handle pointer
+ // event separately. If the editable text changes the text during the
+ // event handler, the render editable will use the outdated text stored in
+ // the _plainText when handling the pointer event.
+ //
+ // If this happens, we need to make sure the new selection is still valid.
+ final int textLength = textSelectionDelegate.textEditingValue.text.length;
+ nextSelection = nextSelection.copyWith(
+ baseOffset: math.min(nextSelection.baseOffset, textLength),
+ extentOffset: math.min(nextSelection.extentOffset, textLength),
+ );
+ }
_handleSelectionChange(nextSelection, cause);
_setTextEditingValue(
textSelectionDelegate.textEditingValue.copyWith(selection: nextSelection),
diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart
index ee46a93..857ddd0 100644
--- a/packages/flutter/test/material/text_field_test.dart
+++ b/packages/flutter/test/material/text_field_test.dart
@@ -444,6 +444,33 @@
expect(renderEditable.cursorColor!.alpha, 0);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
+ // Regression test for https://github.com/flutter/flutter/issues/78918.
+ testWidgets('RenderEditable sets correct text editing value', (WidgetTester tester) async {
+ final TextEditingController controller = TextEditingController(text: 'how are you');
+ final UniqueKey icon = UniqueKey();
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Material(
+ child: TextField(
+ controller: controller,
+ decoration: InputDecoration(
+ suffixIcon: IconButton(
+ key: icon,
+ icon: const Icon(Icons.cancel),
+ onPressed: () => controller.clear(),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ await tester.tap(find.byKey(icon));
+ await tester.pump();
+ expect(controller.text, '');
+ expect(controller.selection, const TextSelection.collapsed(offset: 0, affinity: TextAffinity.upstream));
+ });
+
testWidgets('Cursor radius is 2.0', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
diff --git a/packages/flutter/test/rendering/editable_test.dart b/packages/flutter/test/rendering/editable_test.dart
index bd2875d..1081703 100644
--- a/packages/flutter/test/rendering/editable_test.dart
+++ b/packages/flutter/test/rendering/editable_test.dart
@@ -413,7 +413,9 @@
});
test('selects correct place with offsets', () {
- final TextSelectionDelegate delegate = FakeEditableTextState();
+ const String text = 'test\ntest';
+ final TextSelectionDelegate delegate = FakeEditableTextState()
+ ..textEditingValue = const TextEditingValue(text: text);
final ViewportOffset viewportOffset = ViewportOffset.zero();
late TextSelection currentSelection;
final RenderEditable editable = RenderEditable(
@@ -431,7 +433,7 @@
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
- text: 'test\ntest',
+ text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
@@ -498,7 +500,9 @@
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61026
test('selects correct place when offsets are flipped', () {
- final TextSelectionDelegate delegate = FakeEditableTextState();
+ const String text = 'abc def ghi';
+ final TextSelectionDelegate delegate = FakeEditableTextState()
+ ..textEditingValue = const TextEditingValue(text: text);
final ViewportOffset viewportOffset = ViewportOffset.zero();
late TextSelection currentSelection;
final RenderEditable editable = RenderEditable(
@@ -512,7 +516,7 @@
currentSelection = selection;
},
text: const TextSpan(
- text: 'abc def ghi',
+ text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
@@ -534,9 +538,11 @@
test('selection does not flicker as user is dragging', () {
int selectionChangedCount = 0;
TextSelection? updatedSelection;
- final TextSelectionDelegate delegate = FakeEditableTextState();
- const TextSpan text = TextSpan(
- text: 'abc def ghi',
+ const String text = 'abc def ghi';
+ final TextSelectionDelegate delegate = FakeEditableTextState()
+ ..textEditingValue = const TextEditingValue(text: text);
+ const TextSpan span = TextSpan(
+ text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
@@ -553,7 +559,7 @@
},
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
- text: text,
+ text: span,
);
layout(editable1);
@@ -574,7 +580,7 @@
selectionChangedCount++;
updatedSelection = selection;
},
- text: text,
+ text: span,
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
);
@@ -933,8 +939,10 @@
expect(delegate.textEditingValue.text, '01232345');
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
- test('arrow keys and delete handle surrogate pairs correctly', () async {
- final TextSelectionDelegate delegate = FakeEditableTextState();
+ test('arrow keys and delete handle surrogate pairs correctly case 2', () async {
+ const String text = '\u{1F44D}';
+ final TextSelectionDelegate delegate = FakeEditableTextState()
+ ..textEditingValue = const TextEditingValue(text: text);
final ViewportOffset viewportOffset = ViewportOffset.zero();
late TextSelection currentSelection;
final RenderEditable editable = RenderEditable(
@@ -951,7 +959,7 @@
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
- text: '\u{1F44D}', // Thumbs up
+ text: text, // Thumbs up
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
@@ -1038,7 +1046,9 @@
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
test('arrow keys with selection text', () async {
- final TextSelectionDelegate delegate = FakeEditableTextState();
+ const String text = '012345';
+ final TextSelectionDelegate delegate = FakeEditableTextState()
+ ..textEditingValue = const TextEditingValue(text: text);
final ViewportOffset viewportOffset = ViewportOffset.zero();
late TextSelection currentSelection;
final RenderEditable editable = RenderEditable(
@@ -1055,7 +1065,7 @@
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
- text: '012345', // Thumbs up
+ text: text, // Thumbs up
style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'),
),
selection: const TextSelection.collapsed(
@@ -1096,7 +1106,9 @@
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/58068
test('arrow keys with selection text and shift', () async {
- final TextSelectionDelegate delegate = FakeEditableTextState();
+ const String text = '012345';
+ final TextSelectionDelegate delegate = FakeEditableTextState()
+ ..textEditingValue = const TextEditingValue(text: text);
final ViewportOffset viewportOffset = ViewportOffset.zero();
late TextSelection currentSelection;
final RenderEditable editable = RenderEditable(
@@ -1113,7 +1125,7 @@
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
- text: '012345', // Thumbs up
+ text: text, // Thumbs up
style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'),
),
selection: const TextSelection.collapsed(
@@ -1158,7 +1170,9 @@
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/58068
test('respects enableInteractiveSelection', () async {
- final TextSelectionDelegate delegate = FakeEditableTextState();
+ const String text = '012345';
+ final TextSelectionDelegate delegate = FakeEditableTextState()
+ ..textEditingValue = const TextEditingValue(text: text);
final ViewportOffset viewportOffset = ViewportOffset.zero();
late TextSelection currentSelection;
final RenderEditable editable = RenderEditable(
@@ -1175,7 +1189,7 @@
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
text: const TextSpan(
- text: '012345', // Thumbs up
+ text: text, // Thumbs up
style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'),
),
selection: const TextSelection.collapsed(