Text selection menu show/hide cases (#35219)
Show and hide the text selection menu at the correct time with various gestures in the text field.
diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart
index cce50a1..8b98aff 100644
--- a/packages/flutter/lib/src/material/text_field.dart
+++ b/packages/flutter/lib/src/material/text_field.dart
@@ -762,6 +762,7 @@
}
void _handleSingleTapUp(TapUpDetails details) {
+ _editableTextKey.currentState.hideToolbar();
if (widget.selectionEnabled) {
switch (Theme.of(context).platform) {
case TargetPlatform.iOS:
diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart
index 36809a8..6cb99d4 100644
--- a/packages/flutter/lib/src/widgets/editable_text.dart
+++ b/packages/flutter/lib/src/widgets/editable_text.dart
@@ -1447,7 +1447,7 @@
_showCaretOnScreen();
if (!_value.selection.isValid) {
// Place cursor at the end if the selection is invalid when we receive focus.
- widget.controller.selection = TextSelection.collapsed(offset: _value.text.length);
+ _handleSelectionChanged(TextSelection.collapsed(offset: _value.text.length), renderEditable, null);
}
} else {
WidgetsBinding.instance.removeObserver(this);
@@ -1500,6 +1500,9 @@
@override
void hideToolbar() {
+ if (_selectionOverlay == null || !_selectionOverlay.toolbarIsVisible) {
+ return;
+ }
_selectionOverlay?.hide();
}
diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart
index d0c2cc1..a4ebbd1 100644
--- a/packages/flutter/test/cupertino/text_field_test.dart
+++ b/packages/flutter/test/cupertino/text_field_test.dart
@@ -3650,5 +3650,31 @@
expect(tester.getTopLeft(find.byType(EditableText)).dy, closeTo(329.0, .0001));
});
});
+
+ testWidgets(
+ 'Long press on an autofocused field shows the selection menu',
+ (WidgetTester tester) async {
+ await tester.pumpWidget(
+ CupertinoApp(
+ home: Center(
+ child: ConstrainedBox(
+ constraints: BoxConstraints.loose(const Size(200, 200)),
+ child: const CupertinoTextField(
+ autofocus: true,
+ ),
+ ),
+ ),
+ ),
+ );
+ // This extra pump allows the selection set by autofocus to propagate to
+ // the RenderEditable.
+ await tester.pump();
+
+ // Long press shows the selection menu.
+ await tester.longPressAt(textOffsetToPosition(tester, 0));
+ await tester.pump();
+ expect(find.text('Paste'), findsOneWidget);
+ },
+ );
});
}
diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart
index 0605c0a..a432de8 100644
--- a/packages/flutter/test/material/text_field_test.dart
+++ b/packages/flutter/test/material/text_field_test.dart
@@ -92,19 +92,21 @@
}
Widget boilerplate({ Widget child }) {
- return Localizations(
- locale: const Locale('en', 'US'),
- delegates: <LocalizationsDelegate<dynamic>>[
- WidgetsLocalizationsDelegate(),
- MaterialLocalizationsDelegate(),
- ],
- child: Directionality(
- textDirection: TextDirection.ltr,
- child: MediaQuery(
- data: const MediaQueryData(size: Size(800.0, 600.0)),
- child: Center(
- child: Material(
- child: child,
+ return MaterialApp(
+ home: Localizations(
+ locale: const Locale('en', 'US'),
+ delegates: <LocalizationsDelegate<dynamic>>[
+ WidgetsLocalizationsDelegate(),
+ MaterialLocalizationsDelegate(),
+ ],
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: MediaQuery(
+ data: const MediaQueryData(size: Size(800.0, 600.0)),
+ child: Center(
+ child: Material(
+ child: child,
+ ),
),
),
),
@@ -5342,6 +5344,66 @@
);
testWidgets(
+ 'A single tap hides the selection menu',
+ (WidgetTester tester) async {
+ final TextEditingController controller = TextEditingController(
+ text: '',
+ );
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Material(
+ child: Center(
+ child: TextField(
+ controller: controller,
+ ),
+ ),
+ ),
+ ),
+ );
+
+ // Long press shows the selection menu.
+ await tester.longPress(find.byType(TextField));
+ await tester.pump();
+ expect(find.text('PASTE'), findsOneWidget);
+
+ // Tap hides the selection menu.
+ await tester.tap(find.byType(TextField));
+ await tester.pump();
+ expect(find.text('PASTE'), findsNothing);
+ },
+ );
+
+ testWidgets(
+ 'Long press on an autofocused field shows the selection menu',
+ (WidgetTester tester) async {
+ final TextEditingController controller = TextEditingController(
+ text: '',
+ );
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Material(
+ child: Center(
+ child: TextField(
+ autofocus: true,
+ controller: controller,
+ ),
+ ),
+ ),
+ ),
+ );
+ // This extra pump allows the selection set by autofocus to propagate to
+ // the RenderEditable.
+ await tester.pump();
+
+ // Long press shows the selection menu.
+ expect(find.text('PASTE'), findsNothing);
+ await tester.longPress(find.byType(TextField));
+ await tester.pump();
+ expect(find.text('PASTE'), findsOneWidget);
+ },
+ );
+
+ testWidgets(
'double tap hold selects word (iOS)',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
diff --git a/packages/flutter/test/widgets/editable_text_show_on_screen_test.dart b/packages/flutter/test/widgets/editable_text_show_on_screen_test.dart
index b097721..e778c75 100644
--- a/packages/flutter/test/widgets/editable_text_show_on_screen_test.dart
+++ b/packages/flutter/test/widgets/editable_text_show_on_screen_test.dart
@@ -202,27 +202,29 @@
final PageController pageController = PageController(initialPage: 1);
await tester.pumpWidget(
- MediaQuery(
- data: const MediaQueryData(devicePixelRatio: 1.0),
- child: Directionality(
- textDirection: TextDirection.ltr,
- child: Material(
- child: PageView(
- controller: pageController,
- children: <Widget>[
- Container(
- color: Colors.red,
- ),
- Container(
- child: TextField(
- controller: textController,
+ MaterialApp(
+ home: MediaQuery(
+ data: const MediaQueryData(devicePixelRatio: 1.0),
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: Material(
+ child: PageView(
+ controller: pageController,
+ children: <Widget>[
+ Container(
+ color: Colors.red,
),
- color: Colors.green,
- ),
- Container(
- color: Colors.red,
- ),
- ],
+ Container(
+ child: TextField(
+ controller: textController,
+ ),
+ color: Colors.green,
+ ),
+ Container(
+ color: Colors.red,
+ ),
+ ],
+ ),
),
),
),
diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart
index 0f5dce9..8b66e89 100644
--- a/packages/flutter/test/widgets/editable_text_test.dart
+++ b/packages/flutter/test/widgets/editable_text_test.dart
@@ -809,23 +809,25 @@
return StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
- return MediaQuery(
- data: const MediaQueryData(devicePixelRatio: 1.0),
- child: Directionality(
- textDirection: TextDirection.ltr,
- child: Center(
- child: Material(
- child: EditableText(
- backgroundCursorColor: Colors.grey,
- controller: currentController,
- focusNode: focusNode,
- style: Typography(platform: TargetPlatform.android)
- .black
- .subhead,
- cursorColor: Colors.blue,
- selectionControls: materialTextSelectionControls,
- keyboardType: TextInputType.text,
- onChanged: (String value) { },
+ return MaterialApp(
+ home: MediaQuery(
+ data: const MediaQueryData(devicePixelRatio: 1.0),
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: Center(
+ child: Material(
+ child: EditableText(
+ backgroundCursorColor: Colors.grey,
+ controller: currentController,
+ focusNode: focusNode,
+ style: Typography(platform: TargetPlatform.android)
+ .black
+ .subhead,
+ cursorColor: Colors.blue,
+ selectionControls: materialTextSelectionControls,
+ keyboardType: TextInputType.text,
+ onChanged: (String value) { },
+ ),
),
),
),
diff --git a/packages/flutter/test/widgets/form_test.dart b/packages/flutter/test/widgets/form_test.dart
index 73cf030..33b2e7b 100644
--- a/packages/flutter/test/widgets/form_test.dart
+++ b/packages/flutter/test/widgets/form_test.dart
@@ -11,16 +11,18 @@
String fieldValue;
Widget builder() {
- return MediaQuery(
- data: const MediaQueryData(devicePixelRatio: 1.0),
- child: Directionality(
- textDirection: TextDirection.ltr,
- child: Center(
- child: Material(
- child: Form(
- key: formKey,
- child: TextFormField(
- onSaved: (String value) { fieldValue = value; },
+ return MaterialApp(
+ home: MediaQuery(
+ data: const MediaQueryData(devicePixelRatio: 1.0),
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: Center(
+ child: Material(
+ child: Form(
+ key: formKey,
+ child: TextFormField(
+ onSaved: (String value) { fieldValue = value; },
+ ),
),
),
),
@@ -36,7 +38,7 @@
Future<void> checkText(String testValue) async {
await tester.enterText(find.byType(TextFormField), testValue);
formKey.currentState.save();
- // pump'ing is unnecessary because callback happens regardless of frames
+ // Pumping is unnecessary because callback happens regardless of frames.
expect(fieldValue, equals(testValue));
}
@@ -48,15 +50,17 @@
String fieldValue;
Widget builder() {
- return MediaQuery(
- data: const MediaQueryData(devicePixelRatio: 1.0),
- child: Directionality(
- textDirection: TextDirection.ltr,
- child: Center(
- child: Material(
- child: Form(
- child: TextField(
- onChanged: (String value) { fieldValue = value; },
+ return MaterialApp(
+ home: MediaQuery(
+ data: const MediaQueryData(devicePixelRatio: 1.0),
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: Center(
+ child: Material(
+ child: Form(
+ child: TextField(
+ onChanged: (String value) { fieldValue = value; },
+ ),
),
),
),
@@ -84,17 +88,19 @@
String errorText(String value) => value + '/error';
Widget builder(bool autovalidate) {
- return MediaQuery(
- data: const MediaQueryData(devicePixelRatio: 1.0),
- child: Directionality(
- textDirection: TextDirection.ltr,
- child: Center(
- child: Material(
- child: Form(
- key: formKey,
- autovalidate: autovalidate,
- child: TextFormField(
- validator: errorText,
+ return MaterialApp(
+ home: MediaQuery(
+ data: const MediaQueryData(devicePixelRatio: 1.0),
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: Center(
+ child: Material(
+ child: Form(
+ key: formKey,
+ autovalidate: autovalidate,
+ child: TextFormField(
+ validator: errorText,
+ ),
),
),
),
@@ -138,24 +144,26 @@
String errorText(String input) => '${fieldKey.currentState.value}/error';
Widget builder() {
- return MediaQuery(
- data: const MediaQueryData(devicePixelRatio: 1.0),
- child: Directionality(
- textDirection: TextDirection.ltr,
- child: Center(
- child: Material(
- child: Form(
- key: formKey,
- autovalidate: true,
- child: ListView(
- children: <Widget>[
- TextFormField(
- key: fieldKey,
- ),
- TextFormField(
- validator: errorText,
- ),
- ],
+ return MaterialApp(
+ home: MediaQuery(
+ data: const MediaQueryData(devicePixelRatio: 1.0),
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: Center(
+ child: Material(
+ child: Form(
+ key: formKey,
+ autovalidate: true,
+ child: ListView(
+ children: <Widget>[
+ TextFormField(
+ key: fieldKey,
+ ),
+ TextFormField(
+ validator: errorText,
+ ),
+ ],
+ ),
),
),
),
@@ -184,16 +192,18 @@
final GlobalKey<FormFieldState<String>> inputKey = GlobalKey<FormFieldState<String>>();
Widget builder() {
- return MediaQuery(
- data: const MediaQueryData(devicePixelRatio: 1.0),
- child: Directionality(
- textDirection: TextDirection.ltr,
- child: Center(
- child: Material(
- child: Form(
- child: TextFormField(
- key: inputKey,
- initialValue: 'hello',
+ return MaterialApp(
+ home: MediaQuery(
+ data: const MediaQueryData(devicePixelRatio: 1.0),
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: Center(
+ child: Material(
+ child: Form(
+ child: TextFormField(
+ key: inputKey,
+ initialValue: 'hello',
+ ),
),
),
),
@@ -227,16 +237,18 @@
final GlobalKey<FormFieldState<String>> inputKey = GlobalKey<FormFieldState<String>>();
Widget builder() {
- return MediaQuery(
- data: const MediaQueryData(devicePixelRatio: 1.0),
- child: Directionality(
- textDirection: TextDirection.ltr,
- child: Center(
- child: Material(
- child: Form(
- child: TextFormField(
- key: inputKey,
- controller: controller,
+ return MaterialApp(
+ home: MediaQuery(
+ data: const MediaQueryData(devicePixelRatio: 1.0),
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: Center(
+ child: Material(
+ child: Form(
+ child: TextFormField(
+ key: inputKey,
+ controller: controller,
+ ),
),
),
),
@@ -272,18 +284,20 @@
final TextEditingController controller = TextEditingController(text: 'Plover');
Widget builder() {
- return MediaQuery(
- data: const MediaQueryData(devicePixelRatio: 1.0),
- child: Directionality(
- textDirection: TextDirection.ltr,
- child: Center(
- child: Material(
- child: Form(
- key: formKey,
- child: TextFormField(
- key: inputKey,
- controller: controller,
- // initialValue is 'Plover'
+ return MaterialApp(
+ home: MediaQuery(
+ data: const MediaQueryData(devicePixelRatio: 1.0),
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: Center(
+ child: Material(
+ child: Form(
+ key: formKey,
+ child: TextFormField(
+ key: inputKey,
+ controller: controller,
+ // initialValue is 'Plover'
+ ),
),
),
),
@@ -322,16 +336,18 @@
return StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
- return MediaQuery(
- data: const MediaQueryData(devicePixelRatio: 1.0),
- child: Directionality(
- textDirection: TextDirection.ltr,
- child: Center(
- child: Material(
- child: Form(
- child: TextFormField(
- key: inputKey,
- controller: currentController,
+ return MaterialApp(
+ home: MediaQuery(
+ data: const MediaQueryData(devicePixelRatio: 1.0),
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: Center(
+ child: Material(
+ child: Form(
+ child: TextFormField(
+ key: inputKey,
+ controller: currentController,
+ ),
),
),
),
@@ -420,18 +436,20 @@
String fieldValue;
Widget builder(bool remove) {
- return MediaQuery(
- data: const MediaQueryData(devicePixelRatio: 1.0),
- child: Directionality(
- textDirection: TextDirection.ltr,
- child: Center(
- child: Material(
- child: Form(
- key: formKey,
- child: remove ? Container() : TextFormField(
- autofocus: true,
- onSaved: (String value) { fieldValue = value; },
- validator: (String value) { return value.isEmpty ? null : 'yes'; },
+ return MaterialApp(
+ home: MediaQuery(
+ data: const MediaQueryData(devicePixelRatio: 1.0),
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: Center(
+ child: Material(
+ child: Form(
+ key: formKey,
+ child: remove ? Container() : TextFormField(
+ autofocus: true,
+ onSaved: (String value) { fieldValue = value; },
+ validator: (String value) { return value.isEmpty ? null : 'yes'; },
+ ),
),
),
),