diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 78ab735c3db..e4c1b60a43d 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -1643,7 +1643,11 @@ class EditableTextState extends State with AutomaticKeepAliveClien } } - _formatAndSetValue(value); + if (_isSelectionOnlyChange(value)) { + _handleSelectionChanged(value.selection, renderEditable, SelectionChangedCause.keyboard); + } else { + _formatAndSetValue(value); + } if (_hasInputConnection) { // To keep the cursor from blinking while typing, we want to restart the @@ -1653,6 +1657,10 @@ class EditableTextState extends State with AutomaticKeepAliveClien } } + bool _isSelectionOnlyChange(TextEditingValue value) { + return value.text == _value.text && value.composing == _value.composing; + } + @override void performAction(TextInputAction action) { switch (action) { diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 97ceb30da4a..cdee033c2a0 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -7795,6 +7795,48 @@ void main() { }, ); + testWidgets('Does not show handles when updated from the web engine', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController( + text: 'abc def ghi', + ); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: TextField(controller: controller), + ), + ), + ); + + // Interact with the text field to establish the input connection. + final Offset topLeft = tester.getTopLeft(find.byType(EditableText)); + final TestGesture gesture = await tester.startGesture( + topLeft + const Offset(0.0, 5.0), + kind: PointerDeviceKind.mouse, + ); + addTearDown(gesture.removePointer); + await tester.pump(const Duration(milliseconds: 50)); + await gesture.up(); + await tester.pumpAndSettle(); + + final EditableTextState state = tester.state(find.byType(EditableText)); + expect(state.selectionOverlay.handlesAreVisible, isFalse); + expect(controller.selection, const TextSelection.collapsed(offset: 0)); + + if (kIsWeb) { + tester.testTextInput.updateEditingValue(const TextEditingValue( + selection: TextSelection(baseOffset: 2, extentOffset: 7), + )); + // Wait for all the `setState` calls to be flushed. + await tester.pumpAndSettle(); + expect( + state.currentTextEditingValue.selection, + const TextSelection(baseOffset: 2, extentOffset: 7), + ); + expect(state.selectionOverlay.handlesAreVisible, isFalse); + } + }); + testWidgets('Tapping selection handles toggles the toolbar', (WidgetTester tester) async { final TextEditingController controller = TextEditingController( text: 'abc def ghi', diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index a10a6210678..73c5de118b2 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -1398,6 +1398,46 @@ void main() { } }); + testWidgets('Read-only fields do not format text', (WidgetTester tester) async { + SelectionChangedCause selectionCause; + + final TextEditingController controller = + TextEditingController(text: 'Lorem ipsum dolor sit amet'); + + await tester.pumpWidget( + MaterialApp( + home: EditableText( + readOnly: true, + controller: controller, + backgroundCursorColor: Colors.grey, + focusNode: focusNode, + style: textStyle, + cursorColor: cursorColor, + selectionControls: materialTextSelectionControls, + onSelectionChanged: (TextSelection selection, SelectionChangedCause cause) { + selectionCause = cause; + }, + ), + ), + ); + + // Interact with the field to establish the input connection. + final Offset topLeft = tester.getTopLeft(find.byType(EditableText)); + await tester.tapAt(topLeft + const Offset(0.0, 5.0)); + await tester.pump(); + + expect(tester.testTextInput.hasAnyClients, kIsWeb ? isTrue : isFalse); + if (kIsWeb) { + tester.testTextInput.updateEditingValue(const TextEditingValue( + text: 'Foo bar', + selection: TextSelection(baseOffset: 0, extentOffset: 3), + )); + // On web, the only way a text field can be updated from the engine is if + // a keyboard is used. + expect(selectionCause, SelectionChangedCause.keyboard); + } + }); + testWidgets('Fires onChanged when text changes via TextSelectionOverlay', (WidgetTester tester) async { String changedValue; final Widget widget = MaterialApp( diff --git a/packages/flutter/test/widgets/selectable_text_test.dart b/packages/flutter/test/widgets/selectable_text_test.dart index b0a64ad7d63..e0cf03e7248 100644 --- a/packages/flutter/test/widgets/selectable_text_test.dart +++ b/packages/flutter/test/widgets/selectable_text_test.dart @@ -3971,4 +3971,45 @@ void main() { TargetPlatform.windows, }), ); + + testWidgets('Does not show handles when updated from the web engine', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Material( + child: SelectableText('abc def ghi'), + ), + ), + ); + + // Interact with the selectable text to establish the input connection. + final Offset topLeft = tester.getTopLeft(find.byType(EditableText)); + final TestGesture gesture = await tester.startGesture( + topLeft + const Offset(0.0, 5.0), + kind: PointerDeviceKind.mouse, + ); + addTearDown(gesture.removePointer); + await tester.pump(const Duration(milliseconds: 50)); + await gesture.up(); + await tester.pumpAndSettle(); + + final EditableTextState state = tester.state(find.byType(EditableText)); + expect(state.selectionOverlay.handlesAreVisible, isFalse); + expect( + state.currentTextEditingValue.selection, + const TextSelection.collapsed(offset: 0), + ); + + if (kIsWeb) { + tester.testTextInput.updateEditingValue(const TextEditingValue( + selection: TextSelection(baseOffset: 2, extentOffset: 7), + )); + // Wait for all the `setState` calls to be flushed. + await tester.pumpAndSettle(); + expect( + state.currentTextEditingValue.selection, + const TextSelection(baseOffset: 2, extentOffset: 7), + ); + expect(state.selectionOverlay.handlesAreVisible, isFalse); + } + }); }