Unverified Commit ff53fbe1 authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

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.
parent ef42c36a
......@@ -762,6 +762,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
}
void _handleSingleTapUp(TapUpDetails details) {
_editableTextKey.currentState.hideToolbar();
if (widget.selectionEnabled) {
switch (Theme.of(context).platform) {
case TargetPlatform.iOS:
......
......@@ -1447,7 +1447,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_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 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override
void hideToolbar() {
if (_selectionOverlay == null || !_selectionOverlay.toolbarIsVisible) {
return;
}
_selectionOverlay?.hide();
}
......
......@@ -3650,5 +3650,31 @@ void main() {
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);
},
);
});
}
......@@ -92,19 +92,21 @@ Widget overlayWithEntry(OverlayEntry entry) {
}
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,
),
),
),
),
......@@ -5341,6 +5343,66 @@ void main() {
},
);
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 {
......
......@@ -202,27 +202,29 @@ void main() {
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,
),
],
),
),
),
),
......
......@@ -809,23 +809,25 @@ void main() {
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) { },
),
),
),
),
......
......@@ -11,16 +11,18 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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'; },
),
),
),
),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment