Skip to content

Commit 9acbc1d

Browse files
authored
Fix the second TextFormField to trigger onTapOutside (#148206)
This PR attempts to fix flutter/flutter#127597
1 parent eba7b97 commit 9acbc1d

File tree

9 files changed

+365
-1
lines changed

9 files changed

+365
-1
lines changed

packages/flutter/lib/src/cupertino/text_field.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ class CupertinoTextField extends StatefulWidget {
221221
/// characters" and how it may differ from the intuitive meaning.
222222
const CupertinoTextField({
223223
super.key,
224+
this.groupId = EditableText,
224225
this.controller,
225226
this.focusNode,
226227
this.undoController,
@@ -351,6 +352,7 @@ class CupertinoTextField extends StatefulWidget {
351352
/// characters" and how it may differ from the intuitive meaning.
352353
const CupertinoTextField.borderless({
353354
super.key,
355+
this.groupId = EditableText,
354356
this.controller,
355357
this.focusNode,
356358
this.undoController,
@@ -446,6 +448,9 @@ class CupertinoTextField extends StatefulWidget {
446448
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
447449
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText);
448450

451+
/// {@macro flutter.widgets.editableText.groupId}
452+
final Object groupId;
453+
449454
/// Controls the text being edited.
450455
///
451456
/// If null, this widget will create its own [TextEditingController].
@@ -1387,6 +1392,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
13871392
selectionColor: _effectiveFocusNode.hasFocus ? selectionColor : null,
13881393
selectionControls: widget.selectionEnabled
13891394
? textSelectionControls : null,
1395+
groupId: widget.groupId,
13901396
onChanged: widget.onChanged,
13911397
onSelectionChanged: _handleSelectionChanged,
13921398
onEditingComplete: widget.onEditingComplete,

packages/flutter/lib/src/material/text_field.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ class TextField extends StatefulWidget {
259259
/// characters" and how it may differ from the intuitive meaning.
260260
const TextField({
261261
super.key,
262+
this.groupId = EditableText,
262263
this.controller,
263264
this.focusNode,
264265
this.undoController,
@@ -368,6 +369,9 @@ class TextField extends StatefulWidget {
368369
/// {@end-tool}
369370
final TextMagnifierConfiguration? magnifierConfiguration;
370371

372+
/// {@macro flutter.widgets.editableText.groupId}
373+
final Object groupId;
374+
371375
/// Controls the text being edited.
372376
///
373377
/// If null, this widget will create its own [TextEditingController].
@@ -1499,6 +1503,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
14991503
onEditingComplete: widget.onEditingComplete,
15001504
onSubmitted: widget.onSubmitted,
15011505
onAppPrivateCommand: widget.onAppPrivateCommand,
1506+
groupId: widget.groupId,
15021507
onSelectionHandleTapped: _handleSelectionHandleTapped,
15031508
onTapOutside: widget.onTapOutside,
15041509
inputFormatters: formatters,

packages/flutter/lib/src/material/text_form_field.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ class TextFormField extends FormField<String> {
101101
/// and [TextField.new], the constructor.
102102
TextFormField({
103103
super.key,
104+
this.groupId = EditableText,
104105
this.controller,
105106
String? initialValue,
106107
FocusNode? focusNode,
@@ -203,6 +204,7 @@ class TextFormField extends FormField<String> {
203204
return UnmanagedRestorationScope(
204205
bucket: field.bucket,
205206
child: TextField(
207+
groupId: groupId,
206208
restorationId: restorationId,
207209
controller: state._effectiveController,
208210
focusNode: focusNode,
@@ -279,6 +281,9 @@ class TextFormField extends FormField<String> {
279281
/// initialize its [TextEditingController.text] with [initialValue].
280282
final TextEditingController? controller;
281283

284+
/// {@macro flutter.widgets.editableText.groupId}
285+
final Object groupId;
286+
282287
/// {@template flutter.material.TextFormField.onChanged}
283288
/// Called when the user initiates a change to the TextField's
284289
/// value: when they have inserted or deleted text or reset the form.

packages/flutter/lib/src/widgets/editable_text.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,7 @@ class EditableText extends StatefulWidget {
841841
this.onAppPrivateCommand,
842842
this.onSelectionChanged,
843843
this.onSelectionHandleTapped,
844+
this.groupId = EditableText,
844845
this.onTapOutside,
845846
List<TextInputFormatter>? inputFormatters,
846847
this.mouseCursor,
@@ -1470,6 +1471,19 @@ class EditableText extends StatefulWidget {
14701471
/// {@macro flutter.widgets.SelectionOverlay.onSelectionHandleTapped}
14711472
final VoidCallback? onSelectionHandleTapped;
14721473

1474+
/// {@template flutter.widgets.editableText.groupId}
1475+
/// The group identifier for the [TextFieldTapRegion] of this text field.
1476+
///
1477+
/// Text fields with the same group identifier share the same tap region.
1478+
/// Defaults to the type of [EditableText].
1479+
///
1480+
/// See also:
1481+
///
1482+
/// * [TextFieldTapRegion], to give a [groupId] to a widget that is to be
1483+
/// included in a [EditableText]'s tap region that has [groupId] set.
1484+
/// {@endtemplate}
1485+
final Object groupId;
1486+
14731487
/// {@template flutter.widgets.editableText.onTapOutside}
14741488
/// Called for each tap that occurs outside of the[TextFieldTapRegion] group
14751489
/// when the text field is focused.
@@ -5171,6 +5185,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
51715185
compositeCallback: _compositeCallback,
51725186
enabled: _hasInputConnection,
51735187
child: TextFieldTapRegion(
5188+
groupId: _hasFocus ? widget.groupId : null,
51745189
onTapOutside: _hasFocus ? widget.onTapOutside ?? _defaultOnTapOutside : null,
51755190
debugLabel: kReleaseMode ? null : 'EditableText',
51765191
child: MouseRegion(

packages/flutter/lib/src/widgets/tap_region.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -635,5 +635,6 @@ class TextFieldTapRegion extends TapRegion {
635635
super.onTapInside,
636636
super.consumeOutsideTaps,
637637
super.debugLabel,
638-
}) : super(groupId: EditableText);
638+
super.groupId = EditableText,
639+
});
639640
}

packages/flutter/test/cupertino/text_field_test.dart

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -909,6 +909,91 @@ void main() {
909909
},
910910
);
911911

912+
testWidgets(
913+
'The second CupertinoTextField is clicked, triggers the onTapOutside callback of the previous CupertinoTextField',
914+
(WidgetTester tester) async {
915+
final GlobalKey keyA = GlobalKey();
916+
final GlobalKey keyB = GlobalKey();
917+
final GlobalKey keyC = GlobalKey();
918+
bool outsideClickA = false;
919+
bool outsideClickB = false;
920+
bool outsideClickC = false;
921+
await tester.pumpWidget(
922+
MaterialApp(
923+
home: Align(
924+
alignment: Alignment.topLeft,
925+
child: Column(
926+
children: <Widget>[
927+
const Text('Outside'),
928+
Material(
929+
child: CupertinoTextField(
930+
key: keyA,
931+
groupId: 'Group A',
932+
onTapOutside: (PointerDownEvent event) {
933+
outsideClickA = true;
934+
},
935+
),
936+
),
937+
Material(
938+
child: CupertinoTextField(
939+
key: keyB,
940+
groupId: 'Group B',
941+
onTapOutside: (PointerDownEvent event) {
942+
outsideClickB = true;
943+
},
944+
),
945+
),
946+
Material(
947+
child: CupertinoTextField(
948+
key: keyC,
949+
groupId: 'Group C',
950+
onTapOutside: (PointerDownEvent event) {
951+
outsideClickC = true;
952+
},
953+
),
954+
),
955+
],
956+
),
957+
),
958+
),
959+
);
960+
961+
await tester.pump();
962+
963+
Future<void> click(Finder finder) async {
964+
await tester.tap(finder);
965+
await tester.enterText(finder, 'Hello');
966+
await tester.pump();
967+
}
968+
969+
expect(outsideClickA, false);
970+
expect(outsideClickB, false);
971+
expect(outsideClickC, false);
972+
973+
await click(find.byKey(keyA));
974+
await tester.showKeyboard(find.byKey(keyA));
975+
await tester.idle();
976+
expect(outsideClickA, false);
977+
expect(outsideClickB, false);
978+
expect(outsideClickC, false);
979+
980+
await click(find.byKey(keyB));
981+
expect(outsideClickA, true);
982+
expect(outsideClickB, false);
983+
expect(outsideClickC, false);
984+
985+
await click(find.byKey(keyC));
986+
expect(outsideClickA, true);
987+
expect(outsideClickB, true);
988+
expect(outsideClickC, false);
989+
990+
await tester.tap(find.text('Outside'));
991+
expect(outsideClickA, true);
992+
expect(outsideClickB, true);
993+
expect(outsideClickC, true);
994+
},
995+
);
996+
912997
testWidgets(
913998
'decoration can be overridden',
914999
(WidgetTester tester) async {

packages/flutter/test/material/text_field_test.dart

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,92 @@ void main() {
797797
expect(editableTextWidget.onEditingComplete, onEditingComplete);
798798
});
799799

800+
// Regression test for https://github.com/flutter/flutter/issues/127597.
801+
testWidgets(
802+
'The second TextField is clicked, triggers the onTapOutside callback of the previous TextField',
803+
(WidgetTester tester) async {
804+
final GlobalKey keyA = GlobalKey();
805+
final GlobalKey keyB = GlobalKey();
806+
final GlobalKey keyC = GlobalKey();
807+
bool outsideClickA = false;
808+
bool outsideClickB = false;
809+
bool outsideClickC = false;
810+
await tester.pumpWidget(
811+
MaterialApp(
812+
home: Align(
813+
alignment: Alignment.topLeft,
814+
child: Column(
815+
children: <Widget>[
816+
const Text('Outside'),
817+
Material(
818+
child: TextField(
819+
key: keyA,
820+
groupId: 'Group A',
821+
onTapOutside: (PointerDownEvent event) {
822+
outsideClickA = true;
823+
},
824+
),
825+
),
826+
Material(
827+
child: TextField(
828+
key: keyB,
829+
groupId: 'Group B',
830+
onTapOutside: (PointerDownEvent event) {
831+
outsideClickB = true;
832+
},
833+
),
834+
),
835+
Material(
836+
child: TextField(
837+
key: keyC,
838+
groupId: 'Group C',
839+
onTapOutside: (PointerDownEvent event) {
840+
outsideClickC = true;
841+
},
842+
),
843+
),
844+
],
845+
),
846+
),
847+
),
848+
);
849+
850+
await tester.pump();
851+
852+
Future<void> click(Finder finder) async {
853+
await tester.tap(finder);
854+
await tester.enterText(finder, 'Hello');
855+
await tester.pump();
856+
}
857+
858+
expect(outsideClickA, false);
859+
expect(outsideClickB, false);
860+
expect(outsideClickC, false);
861+
862+
await click(find.byKey(keyA));
863+
await tester.showKeyboard(find.byKey(keyA));
864+
await tester.idle();
865+
expect(outsideClickA, false);
866+
expect(outsideClickB, false);
867+
expect(outsideClickC, false);
868+
869+
await click(find.byKey(keyB));
870+
expect(outsideClickA, true);
871+
expect(outsideClickB, false);
872+
expect(outsideClickC, false);
873+
874+
await click(find.byKey(keyC));
875+
expect(outsideClickA, true);
876+
expect(outsideClickB, true);
877+
expect(outsideClickC, false);
878+
879+
await tester.tap(find.text('Outside'));
880+
expect(outsideClickA, true);
881+
expect(outsideClickB, true);
882+
expect(outsideClickC, true);
883+
},
884+
);
885+
800886
testWidgets('TextField has consistent size', (WidgetTester tester) async {
801887
final Key textFieldKey = UniqueKey();
802888
String? textFieldValue;

0 commit comments

Comments
 (0)