diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart index 350088384236..f59ed40060f3 100644 --- a/packages/flutter/lib/src/rendering/editable.dart +++ b/packages/flutter/lib/src/rendering/editable.dart @@ -1958,6 +1958,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, extentOffset: extentOffset, affinity: fromPosition.affinity, ); + _setSelection(newSelection, cause); } diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index ec755a7e7db9..759a002c5894 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -2441,6 +2441,23 @@ class EditableTextState extends State with AutomaticKeepAliveClien _selectionOverlay?.updateForScroll(); } + void _createSelectionOverlay() { + _selectionOverlay = TextSelectionOverlay( + clipboardStatus: _clipboardStatus, + context: context, + value: _value, + debugRequiredFor: widget, + toolbarLayerLink: _toolbarLayerLink, + startHandleLayerLink: _startHandleLayerLink, + endHandleLayerLink: _endHandleLayerLink, + renderObject: renderEditable, + selectionControls: widget.selectionControls, + selectionDelegate: this, + dragStartBehavior: widget.dragStartBehavior, + onSelectionHandleTapped: widget.onSelectionHandleTapped, + ); + } + @pragma('vm:notify-debugger-on-exception') void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) { // We return early if the selection is not valid. This can happen when the @@ -2478,20 +2495,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien _selectionOverlay = null; } else { if (_selectionOverlay == null) { - _selectionOverlay = TextSelectionOverlay( - clipboardStatus: _clipboardStatus, - context: context, - value: _value, - debugRequiredFor: widget, - toolbarLayerLink: _toolbarLayerLink, - startHandleLayerLink: _startHandleLayerLink, - endHandleLayerLink: _endHandleLayerLink, - renderObject: renderEditable, - selectionControls: widget.selectionControls, - selectionDelegate: this, - dragStartBehavior: widget.dragStartBehavior, - onSelectionHandleTapped: widget.onSelectionHandleTapped, - ); + _createSelectionOverlay(); } else { _selectionOverlay!.update(_value); } @@ -2943,6 +2947,18 @@ class EditableTextState extends State with AutomaticKeepAliveClien if (shouldShowCaret) { _scheduleShowCaretOnScreen(withAnimation: true); } + + // Even if the value doesn't change, it may be necessary to focus and build + // the selection overlay. For example, this happens when right clicking an + // unfocused field that previously had a selection in the same spot. + if (value == textEditingValue) { + if (!widget.focusNode.hasFocus) { + widget.focusNode.requestFocus(); + _createSelectionOverlay(); + } + return; + } + _formatAndSetValue(value, cause, userInteraction: true); } diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index f3b99cd2320e..009853db6311 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart @@ -5663,4 +5663,56 @@ void main() { variant: TargetPlatformVariant.all(), skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. ); + + testWidgets('Can right click to focus multiple times', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/pull/103228 + final FocusNode focusNode1 = FocusNode(); + final FocusNode focusNode2 = FocusNode(); + final UniqueKey key1 = UniqueKey(); + final UniqueKey key2 = UniqueKey(); + await tester.pumpWidget( + CupertinoApp( + home: Column( + children: [ + CupertinoTextField( + key: key1, + focusNode: focusNode1, + ), + CupertinoTextField( + key: key2, + focusNode: focusNode2, + ), + ], + ), + ), + ); + + // Interact with the field to establish the input connection. + await tester.tapAt( + tester.getCenter(find.byKey(key1)), + buttons: kSecondaryMouseButton, + ); + await tester.pump(); + + expect(focusNode1.hasFocus, isTrue); + expect(focusNode2.hasFocus, isFalse); + + await tester.tapAt( + tester.getCenter(find.byKey(key2)), + buttons: kSecondaryMouseButton, + ); + await tester.pump(); + + expect(focusNode1.hasFocus, isFalse); + expect(focusNode2.hasFocus, isTrue); + + await tester.tapAt( + tester.getCenter(find.byKey(key1)), + buttons: kSecondaryMouseButton, + ); + await tester.pump(); + + expect(focusNode1.hasFocus, isTrue); + expect(focusNode2.hasFocus, isFalse); + }); } diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index ea54311fc848..7b1f4c426166 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -11399,4 +11399,58 @@ void main() { variant: TargetPlatformVariant.all(), skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. ); + + testWidgets('Can right click to focus multiple times', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/pull/103228 + final FocusNode focusNode1 = FocusNode(); + final FocusNode focusNode2 = FocusNode(); + final UniqueKey key1 = UniqueKey(); + final UniqueKey key2 = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Column( + children: [ + TextField( + key: key1, + focusNode: focusNode1, + ), + TextField( + key: key2, + focusNode: focusNode2, + ), + ], + ), + ), + ), + ); + + // Interact with the field to establish the input connection. + await tester.tapAt( + tester.getCenter(find.byKey(key1)), + buttons: kSecondaryButton, + ); + await tester.pump(); + + expect(focusNode1.hasFocus, isTrue); + expect(focusNode2.hasFocus, isFalse); + + await tester.tapAt( + tester.getCenter(find.byKey(key2)), + buttons: kSecondaryButton, + ); + await tester.pump(); + + expect(focusNode1.hasFocus, isFalse); + expect(focusNode2.hasFocus, isTrue); + + await tester.tapAt( + tester.getCenter(find.byKey(key1)), + buttons: kSecondaryButton, + ); + await tester.pump(); + + expect(focusNode1.hasFocus, isTrue); + expect(focusNode2.hasFocus, isFalse); + }); }