Skip to content

Commit

Permalink
Fix the second TextFormField to trigger onTapOutside (#148206)
Browse files Browse the repository at this point in the history
This PR attempts to fix flutter/flutter#127597
  • Loading branch information
wyqlxf authored May 22, 2024
1 parent eba7b97 commit 9acbc1d
Show file tree
Hide file tree
Showing 9 changed files with 365 additions and 1 deletion.
6 changes: 6 additions & 0 deletions packages/flutter/lib/src/cupertino/text_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ class CupertinoTextField extends StatefulWidget {
/// characters" and how it may differ from the intuitive meaning.
const CupertinoTextField({
super.key,
this.groupId = EditableText,
this.controller,
this.focusNode,
this.undoController,
Expand Down Expand Up @@ -351,6 +352,7 @@ class CupertinoTextField extends StatefulWidget {
/// characters" and how it may differ from the intuitive meaning.
const CupertinoTextField.borderless({
super.key,
this.groupId = EditableText,
this.controller,
this.focusNode,
this.undoController,
Expand Down Expand Up @@ -446,6 +448,9 @@ class CupertinoTextField extends StatefulWidget {
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText);

/// {@macro flutter.widgets.editableText.groupId}
final Object groupId;

/// Controls the text being edited.
///
/// If null, this widget will create its own [TextEditingController].
Expand Down Expand Up @@ -1387,6 +1392,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
selectionColor: _effectiveFocusNode.hasFocus ? selectionColor : null,
selectionControls: widget.selectionEnabled
? textSelectionControls : null,
groupId: widget.groupId,
onChanged: widget.onChanged,
onSelectionChanged: _handleSelectionChanged,
onEditingComplete: widget.onEditingComplete,
Expand Down
5 changes: 5 additions & 0 deletions packages/flutter/lib/src/material/text_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ class TextField extends StatefulWidget {
/// characters" and how it may differ from the intuitive meaning.
const TextField({
super.key,
this.groupId = EditableText,
this.controller,
this.focusNode,
this.undoController,
Expand Down Expand Up @@ -368,6 +369,9 @@ class TextField extends StatefulWidget {
/// {@end-tool}
final TextMagnifierConfiguration? magnifierConfiguration;

/// {@macro flutter.widgets.editableText.groupId}
final Object groupId;

/// Controls the text being edited.
///
/// If null, this widget will create its own [TextEditingController].
Expand Down Expand Up @@ -1499,6 +1503,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
onEditingComplete: widget.onEditingComplete,
onSubmitted: widget.onSubmitted,
onAppPrivateCommand: widget.onAppPrivateCommand,
groupId: widget.groupId,
onSelectionHandleTapped: _handleSelectionHandleTapped,
onTapOutside: widget.onTapOutside,
inputFormatters: formatters,
Expand Down
5 changes: 5 additions & 0 deletions packages/flutter/lib/src/material/text_form_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ class TextFormField extends FormField<String> {
/// and [TextField.new], the constructor.
TextFormField({
super.key,
this.groupId = EditableText,
this.controller,
String? initialValue,
FocusNode? focusNode,
Expand Down Expand Up @@ -203,6 +204,7 @@ class TextFormField extends FormField<String> {
return UnmanagedRestorationScope(
bucket: field.bucket,
child: TextField(
groupId: groupId,
restorationId: restorationId,
controller: state._effectiveController,
focusNode: focusNode,
Expand Down Expand Up @@ -279,6 +281,9 @@ class TextFormField extends FormField<String> {
/// initialize its [TextEditingController.text] with [initialValue].
final TextEditingController? controller;

/// {@macro flutter.widgets.editableText.groupId}
final Object groupId;

/// {@template flutter.material.TextFormField.onChanged}
/// Called when the user initiates a change to the TextField's
/// value: when they have inserted or deleted text or reset the form.
Expand Down
15 changes: 15 additions & 0 deletions packages/flutter/lib/src/widgets/editable_text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,7 @@ class EditableText extends StatefulWidget {
this.onAppPrivateCommand,
this.onSelectionChanged,
this.onSelectionHandleTapped,
this.groupId = EditableText,
this.onTapOutside,
List<TextInputFormatter>? inputFormatters,
this.mouseCursor,
Expand Down Expand Up @@ -1470,6 +1471,19 @@ class EditableText extends StatefulWidget {
/// {@macro flutter.widgets.SelectionOverlay.onSelectionHandleTapped}
final VoidCallback? onSelectionHandleTapped;

/// {@template flutter.widgets.editableText.groupId}
/// The group identifier for the [TextFieldTapRegion] of this text field.
///
/// Text fields with the same group identifier share the same tap region.
/// Defaults to the type of [EditableText].
///
/// See also:
///
/// * [TextFieldTapRegion], to give a [groupId] to a widget that is to be
/// included in a [EditableText]'s tap region that has [groupId] set.
/// {@endtemplate}
final Object groupId;

/// {@template flutter.widgets.editableText.onTapOutside}
/// Called for each tap that occurs outside of the[TextFieldTapRegion] group
/// when the text field is focused.
Expand Down Expand Up @@ -5171,6 +5185,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
compositeCallback: _compositeCallback,
enabled: _hasInputConnection,
child: TextFieldTapRegion(
groupId: _hasFocus ? widget.groupId : null,
onTapOutside: _hasFocus ? widget.onTapOutside ?? _defaultOnTapOutside : null,
debugLabel: kReleaseMode ? null : 'EditableText',
child: MouseRegion(
Expand Down
3 changes: 2 additions & 1 deletion packages/flutter/lib/src/widgets/tap_region.dart
Original file line number Diff line number Diff line change
Expand Up @@ -635,5 +635,6 @@ class TextFieldTapRegion extends TapRegion {
super.onTapInside,
super.consumeOutsideTaps,
super.debugLabel,
}) : super(groupId: EditableText);
super.groupId = EditableText,
});
}
85 changes: 85 additions & 0 deletions packages/flutter/test/cupertino/text_field_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,91 @@ void main() {
},
);

testWidgets(
'The second CupertinoTextField is clicked, triggers the onTapOutside callback of the previous CupertinoTextField',
(WidgetTester tester) async {
final GlobalKey keyA = GlobalKey();
final GlobalKey keyB = GlobalKey();
final GlobalKey keyC = GlobalKey();
bool outsideClickA = false;
bool outsideClickB = false;
bool outsideClickC = false;
await tester.pumpWidget(
MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: Column(
children: <Widget>[
const Text('Outside'),
Material(
child: CupertinoTextField(
key: keyA,
groupId: 'Group A',
onTapOutside: (PointerDownEvent event) {
outsideClickA = true;
},
),
),
Material(
child: CupertinoTextField(
key: keyB,
groupId: 'Group B',
onTapOutside: (PointerDownEvent event) {
outsideClickB = true;
},
),
),
Material(
child: CupertinoTextField(
key: keyC,
groupId: 'Group C',
onTapOutside: (PointerDownEvent event) {
outsideClickC = true;
},
),
),
],
),
),
),
);

await tester.pump();

Future<void> click(Finder finder) async {
await tester.tap(finder);
await tester.enterText(finder, 'Hello');
await tester.pump();
}

expect(outsideClickA, false);
expect(outsideClickB, false);
expect(outsideClickC, false);

await click(find.byKey(keyA));
await tester.showKeyboard(find.byKey(keyA));
await tester.idle();
expect(outsideClickA, false);
expect(outsideClickB, false);
expect(outsideClickC, false);

await click(find.byKey(keyB));
expect(outsideClickA, true);
expect(outsideClickB, false);
expect(outsideClickC, false);

await click(find.byKey(keyC));
expect(outsideClickA, true);
expect(outsideClickB, true);
expect(outsideClickC, false);

await tester.tap(find.text('Outside'));
expect(outsideClickA, true);
expect(outsideClickB, true);
expect(outsideClickC, true);
},
);

testWidgets(
'decoration can be overridden',
(WidgetTester tester) async {
Expand Down
86 changes: 86 additions & 0 deletions packages/flutter/test/material/text_field_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,92 @@ void main() {
expect(editableTextWidget.onEditingComplete, onEditingComplete);
});

// Regression test for https://github.com/flutter/flutter/issues/127597.
testWidgets(
'The second TextField is clicked, triggers the onTapOutside callback of the previous TextField',
(WidgetTester tester) async {
final GlobalKey keyA = GlobalKey();
final GlobalKey keyB = GlobalKey();
final GlobalKey keyC = GlobalKey();
bool outsideClickA = false;
bool outsideClickB = false;
bool outsideClickC = false;
await tester.pumpWidget(
MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: Column(
children: <Widget>[
const Text('Outside'),
Material(
child: TextField(
key: keyA,
groupId: 'Group A',
onTapOutside: (PointerDownEvent event) {
outsideClickA = true;
},
),
),
Material(
child: TextField(
key: keyB,
groupId: 'Group B',
onTapOutside: (PointerDownEvent event) {
outsideClickB = true;
},
),
),
Material(
child: TextField(
key: keyC,
groupId: 'Group C',
onTapOutside: (PointerDownEvent event) {
outsideClickC = true;
},
),
),
],
),
),
),
);

await tester.pump();

Future<void> click(Finder finder) async {
await tester.tap(finder);
await tester.enterText(finder, 'Hello');
await tester.pump();
}

expect(outsideClickA, false);
expect(outsideClickB, false);
expect(outsideClickC, false);

await click(find.byKey(keyA));
await tester.showKeyboard(find.byKey(keyA));
await tester.idle();
expect(outsideClickA, false);
expect(outsideClickB, false);
expect(outsideClickC, false);

await click(find.byKey(keyB));
expect(outsideClickA, true);
expect(outsideClickB, false);
expect(outsideClickC, false);

await click(find.byKey(keyC));
expect(outsideClickA, true);
expect(outsideClickB, true);
expect(outsideClickC, false);

await tester.tap(find.text('Outside'));
expect(outsideClickA, true);
expect(outsideClickB, true);
expect(outsideClickC, true);
},
);

testWidgets('TextField has consistent size', (WidgetTester tester) async {
final Key textFieldKey = UniqueKey();
String? textFieldValue;
Expand Down
Loading

0 comments on commit 9acbc1d

Please sign in to comment.