From 8d5903f74699ce992237ccaea9d10b6434e1732b Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Thu, 24 Feb 2022 12:06:55 -0800 Subject: [PATCH] Pasting collapses the selection and puts it after the pasted content (#98679) Desktop selection behavior improvement. --- .../lib/src/widgets/editable_text.dart | 12 +- .../test/widgets/editable_text_test.dart | 136 ++++++++++++++++++ 2 files changed, 147 insertions(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 7955af72c5..ed5f53351f 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -1716,7 +1716,17 @@ class EditableTextState extends State with AutomaticKeepAliveClien return; } - _replaceText(ReplaceTextIntent(textEditingValue, data.text!, selection, cause)); + // After the paste, the cursor should be collapsed and located after the + // pasted content. + final int lastSelectionIndex = math.max(selection.baseOffset, selection.extentOffset); + final TextEditingValue collapsedTextEditingValue = textEditingValue.copyWith( + selection: TextSelection.collapsed(offset: lastSelectionIndex), + ); + + userUpdateTextEditingValue( + collapsedTextEditingValue.replaced(selection, data.text!), + cause, + ); if (cause == SelectionChangedCause.toolbar) { // Schedule a call to bringIntoView() after renderEditable updates. SchedulerBinding.instance.addPostFrameCallback((_) { diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index f37aab5edb..497b1d4f2c 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -49,6 +49,8 @@ enum HandlePositionInViewport { leftEdge, rightEdge, within, } +typedef _VoidFutureCallback = Future Function(); + void main() { final MockClipboard mockClipboard = MockClipboard(); TestWidgetsFlutterBinding.ensureInitialized() @@ -11519,6 +11521,140 @@ void main() { }, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended] }); + testWidgets('pasting with the keyboard collapses the selection and places it after the pasted content', (WidgetTester tester) async { + Future testPasteSelection(WidgetTester tester, _VoidFutureCallback paste) async { + final TextEditingController controller = TextEditingController(); + await tester.pumpWidget( + MaterialApp( + home: EditableText( + backgroundCursorColor: Colors.grey, + controller: controller, + focusNode: focusNode, + style: textStyle, + cursorColor: cursorColor, + selectionControls: materialTextSelectionControls, + ), + ), + ); + + await tester.pump(); + expect(controller.text, ''); + + await tester.enterText(find.byType(EditableText), '12345'); + expect(controller.value, const TextEditingValue( + text: '12345', + selection: TextSelection.collapsed(offset: 5), + )); + + await sendKeys( + tester, + [ + LogicalKeyboardKey.arrowLeft, + LogicalKeyboardKey.arrowLeft, + LogicalKeyboardKey.arrowLeft, + LogicalKeyboardKey.arrowLeft, + LogicalKeyboardKey.arrowLeft, + ], + shift: true, + targetPlatform: defaultTargetPlatform, + ); + + expect(controller.value, const TextEditingValue( + text: '12345', + selection: TextSelection(baseOffset: 5, extentOffset: 0), + )); + + await sendKeys( + tester, + [ + LogicalKeyboardKey.keyC, + ], + shortcutModifier: true, + targetPlatform: defaultTargetPlatform, + ); + expect(controller.value, const TextEditingValue( + text: '12345', + selection: TextSelection(baseOffset: 5, extentOffset: 0), + )); + + // Pasting content of equal length, reversed selection. + await paste(); + expect(controller.value, const TextEditingValue( + text: '12345', + selection: TextSelection.collapsed(offset: 5), + )); + + // Pasting content of longer length, forward selection. + await sendKeys( + tester, + [ + LogicalKeyboardKey.arrowLeft, + ], + targetPlatform: defaultTargetPlatform, + ); + await sendKeys( + tester, + [ + LogicalKeyboardKey.arrowRight, + ], + shift: true, + targetPlatform: defaultTargetPlatform, + ); + expect(controller.value, const TextEditingValue( + text: '12345', + selection: TextSelection(baseOffset: 4, extentOffset: 5), + )); + await paste(); + expect(controller.value, const TextEditingValue( + text: '123412345', + selection: TextSelection.collapsed(offset: 9), + )); + + // Pasting content of shorter length, forward selection. + await sendKeys( + tester, + [ + LogicalKeyboardKey.keyA, + ], + shortcutModifier: true, + targetPlatform: defaultTargetPlatform, + ); + expect(controller.value, const TextEditingValue( + text: '123412345', + selection: TextSelection(baseOffset: 0, extentOffset: 9), + )); + await paste(); + // Pump to allow postFrameCallbacks to finish before dispose. + await tester.pump(); + expect(controller.value, const TextEditingValue( + text: '12345', + selection: TextSelection.collapsed(offset: 5), + )); + } + + // Test pasting with the keyboard. + await testPasteSelection(tester, () { + return sendKeys( + tester, + [ + LogicalKeyboardKey.keyV, + ], + shortcutModifier: true, + targetPlatform: defaultTargetPlatform, + ); + }); + + // Test pasting with the toolbar. + await testPasteSelection(tester, () async { + final EditableTextState state = + tester.state(find.byType(EditableText)); + expect(state.showToolbar(), true); + await tester.pumpAndSettle(); + expect(find.text('Paste'), findsOneWidget); + return tester.tap(find.text('Paste')); + }); + }, skip: kIsWeb); // [intended] + // Regression test for https://github.com/flutter/flutter/issues/98322. testWidgets('EditableText consumes ActivateIntent and ButtonActivateIntent', (WidgetTester tester) async { bool receivedIntent = false;