From 533a5dde96b35798c501811f3ba378b53bc4b7e4 Mon Sep 17 00:00:00 2001 From: Tomasz Gucio <72562119+tgucio@users.noreply.github.com> Date: Thu, 24 Feb 2022 01:49:22 +0100 Subject: [PATCH] Call bringIntoView after RenderEditable updates on paste (#98604) --- .../lib/src/widgets/editable_text.dart | 26 ++++++++-- .../test/widgets/editable_text_test.dart | 48 ++++++++++++++++++- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 99ae704991..7955af72c5 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -1689,7 +1689,10 @@ class EditableTextState extends State with AutomaticKeepAliveClien Clipboard.setData(ClipboardData(text: selection.textInside(text))); _replaceText(ReplaceTextIntent(textEditingValue, '', selection, cause)); if (cause == SelectionChangedCause.toolbar) { - bringIntoView(textEditingValue.selection.extent); + // Schedule a call to bringIntoView() after renderEditable updates. + SchedulerBinding.instance.addPostFrameCallback((_) { + bringIntoView(textEditingValue.selection.extent); + }); hideToolbar(); } _clipboardStatus?.update(); @@ -1715,7 +1718,10 @@ class EditableTextState extends State with AutomaticKeepAliveClien _replaceText(ReplaceTextIntent(textEditingValue, data.text!, selection, cause)); if (cause == SelectionChangedCause.toolbar) { - bringIntoView(textEditingValue.selection.extent); + // Schedule a call to bringIntoView() after renderEditable updates. + SchedulerBinding.instance.addPostFrameCallback((_) { + bringIntoView(textEditingValue.selection.extent); + }); hideToolbar(); } } @@ -3095,10 +3101,20 @@ class EditableTextState extends State with AutomaticKeepAliveClien } void _replaceText(ReplaceTextIntent intent) { - userUpdateTextEditingValue( - intent.currentTextEditingValue.replaced(intent.replacementRange, intent.replacementText), - intent.cause, + final TextEditingValue oldValue = _value; + final TextEditingValue newValue = intent.currentTextEditingValue.replaced( + intent.replacementRange, + intent.replacementText, ); + userUpdateTextEditingValue(newValue, intent.cause); + + // If there's no change in text and selection (e.g. when selecting and + // pasting identical text), the widget won't be rebuilt on value update. + // Handle this by calling _didChangeTextEditingValue() so caret and scroll + // updates can happen. + if (newValue == oldValue) { + _didChangeTextEditingValue(); + } } late final Action _replaceTextAction = CallbackAction(onInvoke: _replaceText); diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index 5c7f821b38..f37aab5edb 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -10948,12 +10948,12 @@ void main() { // Paste await resetSelectionAndScrollOffset(); - textSelectionDelegate.pasteText(SelectionChangedCause.keyboard); + await textSelectionDelegate.pasteText(SelectionChangedCause.keyboard); await tester.pump(); expect(scrollController.offset, maxScrollExtent); await resetSelectionAndScrollOffset(); - textSelectionDelegate.pasteText(SelectionChangedCause.toolbar); + await textSelectionDelegate.pasteText(SelectionChangedCause.toolbar); await tester.pump(); expect(scrollController.offset.roundToDouble(), 0.0); @@ -10980,6 +10980,50 @@ void main() { expect(scrollController.offset.roundToDouble(), 0.0); }); + testWidgets('Should not scroll on paste if caret already visible', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/96658. + final ScrollController scrollController = ScrollController(); + final TextEditingController controller = TextEditingController( + text: 'Lorem ipsum please paste here: \n${".\n" * 50}', + ); + final FocusNode focusNode = FocusNode(); + + await tester.pumpWidget( + MaterialApp( + home: Center( + child: SizedBox( + height: 600.0, + width: 600.0, + child: EditableText( + controller: controller, + scrollController: scrollController, + focusNode: focusNode, + maxLines: null, + style: const TextStyle(fontSize: 36.0), + backgroundCursorColor: Colors.grey, + cursorColor: cursorColor, + ), + ), + ), + ) + ); + + await Clipboard.setData(const ClipboardData(text: 'Fairly long text to be pasted')); + focusNode.requestFocus(); + + final EditableTextState state = + tester.state(find.byType(EditableText)); + + expect(scrollController.offset, 0.0); + + controller.selection = const TextSelection.collapsed(offset: 31); + await state.pasteText(SelectionChangedCause.toolbar); + await tester.pumpAndSettle(); + + // No scroll should happen as the caret is in the viewport all the time. + expect(scrollController.offset, 0.0); + }); + testWidgets('Autofill enabled by default', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); await tester.pumpWidget(