Unverified Commit 2a65505e authored by matthew-carroll's avatar matthew-carroll Committed by GitHub

Support all keyboard actions. (#11344) (#18855)

* Support all keyboard actions. (#11344)
parent af5d4c68
......@@ -102,6 +102,7 @@ class TextField extends StatefulWidget {
this.focusNode,
this.decoration = const InputDecoration(),
TextInputType keyboardType = TextInputType.text,
this.textInputAction = TextInputAction.done,
this.style,
this.textAlign = TextAlign.start,
this.autofocus = false,
......@@ -115,6 +116,7 @@ class TextField extends StatefulWidget {
this.inputFormatters,
this.enabled,
}) : assert(keyboardType != null),
assert(textInputAction != null),
assert(textAlign != null),
assert(autofocus != null),
assert(obscureText != null),
......@@ -151,6 +153,11 @@ class TextField extends StatefulWidget {
/// [TextInputType.multiline] keyboard type is used.
final TextInputType keyboardType;
/// The type of action button to use for the keyboard.
///
/// Defaults to [TextInputAction.done]. Must not be null.
final TextInputAction textInputAction;
/// The style to use for the text being edited.
///
/// This text style is also used as the base style for the [decoration].
......@@ -473,6 +480,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
controller: controller,
focusNode: focusNode,
keyboardType: widget.keyboardType,
textInputAction: widget.textInputAction,
style: style,
textAlign: widget.textAlign,
autofocus: widget.autofocus,
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:io' show Platform;
import 'dart:ui' show TextAffinity, hashValues;
import 'package:flutter/foundation.dart';
......@@ -134,20 +135,200 @@ class TextInputType {
}
/// An action the user has requested the text input control to perform.
///
/// Each action represents a logical meaning, and also configures the soft
/// keyboard to display a certain kind of action button. The visual appearance
/// of the action button might differ between versions of the same OS.
///
/// Despite the logical meaning of each action, choosing a particular
/// [TextInputAction] does not necessarily cause any specific behavior to
/// happen. It is up to the developer to ensure that the behavior that occurs
/// when an action button is pressed is appropriate for the action button chosen.
///
/// For example: If the user presses the keyboard action button on iOS when it
/// reads "Emergency Call", the result should not be a focus change to the next
/// TextField. This behavior is not logically appropriate for a button that says
/// "Emergency Call".
///
/// See [EditableText] for more information about customizing action button
/// behavior.
///
/// Most [TextInputAction]s are supported equally by both Android and iOS.
/// However, there is not a complete, direct mapping between Android's IME input
/// types and iOS's keyboard return types. Therefore, some [TextInputAction]s
/// are inappropriate for one of the platforms. If a developer chooses an
/// inappropriate [TextInputAction] when running in debug mode, an error will be
/// thrown. If the same thing is done in release mode, then instead of sending
/// the inappropriate value, Android will use "unspecified" on the platform
/// side and iOS will use "default" on the platform side.
///
/// See also:
///
/// * [TextInput], which configures the platform's keyboard setup.
/// * [EditableText], which invokes callbacks when the action button is pressed.
enum TextInputAction {
/// Complete the text input operation.
/// Logical meaning: There is no relevant input action for the current input
/// source, e.g., [TextField].
///
/// Android: Corresponds to Android's "IME_ACTION_NONE". The keyboard setup
/// is decided by the OS. The keyboard will likely show a return key.
///
/// iOS: iOS does not have a keyboard return type of "none." It is
/// inappropriate to choose this [TextInputAction] when running on iOS.
none,
/// Logical meaning: Let the OS decide which action is most appropriate.
///
/// Android: Corresponds to Android's "IME_ACTION_UNSPECIFIED". The OS chooses
/// which keyboard action to display. The decision will likely be a done
/// button or a return key.
///
/// iOS: Corresponds to iOS's "UIReturnKeyDefault". The title displayed in
/// the action button is "return".
unspecified,
/// Logical meaning: The user is done providing input to a group of inputs
/// (like a form). Some kind of finalization behavior should now take place.
///
/// Android: Corresponds to Android's "IME_ACTION_DONE". The OS displays a
/// button that represents completion, e.g., a checkmark button.
///
/// iOS: Corresponds to iOS's "UIReturnKeyDone". The title displayed in the
/// action button is "Done".
done,
/// The action to take when the enter button is pressed in a multi-line
/// text field (which is typically to do nothing).
/// Logical meaning: The user has entered some text that represents a
/// destination, e.g., a restaurant name. The "go" button is intended to take
/// the user to a part of the app that corresponds to this destination.
///
/// Android: Corresponds to Android's "IME_ACTION_GO". The OS displays a
/// button that represents taking "the user to the target of the text they
/// typed", e.g., a right-facing arrow button.
///
/// iOS: Corresponds to iOS's "UIReturnKeyGo". The title displayed in the
/// action button is "Go".
go,
/// Logical meaning: Execute a search query.
///
/// Android: Corresponds to Android's "IME_ACTION_SEARCH". The OS displays a
/// button that represents a search, e.g., a magnifying glass button.
///
/// iOS: Corresponds to iOS's "UIReturnKeySearch". The title displayed in the
/// action button is "Search".
search,
/// Logical meaning: Sends something that the user has composed, e.g., an
/// email or a text message.
///
/// Android: Corresponds to Android's "IME_ACTION_SEND". The OS displays a
/// button that represents sending something, e.g., a paper plane button.
///
/// iOS: Corresponds to iOS's "UIReturnKeySend". The title displayed in the
/// action button is "Send".
send,
/// Logical meaning: The user is done with the current input source and wants
/// to move to the next one.
///
/// Android: Corresponds to Android's "IME_ACTION_NEXT". The OS displays a
/// button that represents moving forward, e.g., a right-facing arrow button.
///
/// iOS: Corresponds to iOS's "UIReturnKeyNext". The title displayed in the
/// action button is "Next".
next,
/// Logical meaning: The user wishes to return to the previous input source
/// in the group, e.g., a form with multiple [TextField]s.
///
/// Android: Corresponds to Android's "IME_ACTION_PREVIOUS". The OS displays a
/// button that represents moving backward, e.g., a left-facing arrow button.
///
/// iOS: iOS does not have a keyboard return type of "previous." It is
/// inappropriate to choose this [TextInputAction] when running on iOS.
previous,
/// Logical meaning: In iOS apps, it is common for a "Back" button and
/// "Continue" button to appear at the top of the screen. However, when the
/// keyboard is open, these buttons are often hidden off-screen. Therefore,
/// the purpose of the "Continue" return key on iOS is to make the "Continue"
/// button available when the user is entering text.
///
/// Historical context aside, [TextInputAction.continueAction] can be used any
/// time that the term "Continue" seems most appropriate for the given action.
///
/// Android: Android does not have an IME input type of "continue." It is
/// inappropriate to choose this [TextInputAction] when running on Android.
///
/// iOS: Corresponds to iOS's "UIReturnKeyContinue". The title displayed in the
/// action button is "Continue". This action is only available on iOS 9.0+.
///
/// The reason that this value has "Action" post-fixed to it is because
/// "continue" is a reserved word in Dart, as well as many other languages.
continueAction,
/// Logical meaning: The user wants to join something, e.g., a wireless
/// network.
///
/// Android: Android does not have an IME input type of "join." It is
/// inappropriate to choose this [TextInputAction] when running on Android.
///
/// iOS: Corresponds to iOS's "UIReturnKeyJoin". The title displayed in the
/// action button is "Join".
join,
/// Logical meaning: The user wants routing options, e.g., driving directions.
///
/// Android: Android does not have an IME input type of "route." It is
/// inappropriate to choose this [TextInputAction] when running on Android.
///
/// iOS: Corresponds to iOS's "UIReturnKeyRoute". The title displayed in the
/// action button is "Route".
route,
/// Logical meaning: Initiate a call to emergency services.
///
/// Android: Android does not have an IME input type of "emergencyCall." It is
/// inappropriate to choose this [TextInputAction] when running on Android.
///
/// iOS: Corresponds to iOS's "UIReturnKeyEmergencyCall". The title displayed
/// in the action button is "Emergency Call".
emergencyCall,
/// Logical meaning: Insert a newline character in the focused text input,
/// e.g., [TextField].
///
/// Android: Corresponds to Android's "IME_ACTION_NONE". The OS displays a
/// button that represents a new line, e.g., a carriage return button.
///
/// iOS: Corresponds to iOS's "UIReturnKeyDefault". The title displayed in the
/// action button is "return".
///
/// The term [TextInputAction.newline] exists in Flutter but not in Android
/// or iOS. The reason for introducing this term is so that developers can
/// achieve the common result of inserting new lines without needing to
/// understand the various IME actions on Android and return keys on iOS.
/// Thus, [TextInputAction.newline] is a convenience term that alleviates the
/// need to understand the underlying platforms to achieve this common behavior.
newline,
}
/// Controls the visual appearance of the text input control.
///
/// Many [TextInputAction]s are common between Android and iOS. However, if an
/// [inputAction] is provided that is not supported by the current
/// platform in debug mode, an error will be thrown when the corresponding
/// text input is attached. For example, providing iOS's "emergencyCall"
/// action when running on an Android device will result in an error when in
/// debug mode. In release mode, incompatible [TextInputAction]s are replaced
/// either with "unspecified" on Android, or "default" on iOS. Appropriate
/// [inputAction]s can be chosen by checking the current platform and then
/// selecting the appropriate action.
///
/// See also:
///
/// * [TextInput.attach]
/// * [TextInputAction]
@immutable
class TextInputConfiguration {
/// Creates configuration information for a text input control.
......@@ -368,6 +549,28 @@ class TextInputConnection {
TextInputAction _toTextInputAction(String action) {
switch (action) {
case 'TextInputAction.none':
return TextInputAction.none;
case 'TextInputAction.unspecified':
return TextInputAction.unspecified;
case 'TextInputAction.go':
return TextInputAction.go;
case 'TextInputAction.search':
return TextInputAction.search;
case 'TextInputAction.send':
return TextInputAction.send;
case 'TextInputAction.next':
return TextInputAction.next;
case 'TextInputAction.previuos':
return TextInputAction.previous;
case 'TextInputAction.continue_action':
return TextInputAction.continueAction;
case 'TextInputAction.join':
return TextInputAction.join;
case 'TextInputAction.route':
return TextInputAction.route;
case 'TextInputAction.emergencyCall':
return TextInputAction.emergencyCall;
case 'TextInputAction.done':
return TextInputAction.done;
case 'TextInputAction.newline':
......@@ -426,6 +629,32 @@ final _TextInputClientHandler _clientHandler = new _TextInputClientHandler();
/// An interface to the system's text input control.
class TextInput {
static const List<TextInputAction> _androidSupportedInputActions = <TextInputAction>[
TextInputAction.none,
TextInputAction.unspecified,
TextInputAction.done,
TextInputAction.send,
TextInputAction.go,
TextInputAction.search,
TextInputAction.next,
TextInputAction.previous,
TextInputAction.newline,
];
static const List<TextInputAction> _iOSSupportedInputActions = <TextInputAction>[
TextInputAction.unspecified,
TextInputAction.done,
TextInputAction.send,
TextInputAction.go,
TextInputAction.search,
TextInputAction.next,
TextInputAction.newline,
TextInputAction.continueAction,
TextInputAction.join,
TextInputAction.route,
TextInputAction.emergencyCall,
];
TextInput._();
/// Begin interacting with the text input control.
......@@ -441,6 +670,7 @@ class TextInput {
static TextInputConnection attach(TextInputClient client, TextInputConfiguration configuration) {
assert(client != null);
assert(configuration != null);
assert(_debugEnsureInputActionWorksOnPlatform(configuration.inputAction));
final TextInputConnection connection = new TextInputConnection._(client);
_clientHandler._currentConnection = connection;
SystemChannels.textInput.invokeMethod(
......@@ -449,4 +679,22 @@ class TextInput {
);
return connection;
}
static bool _debugEnsureInputActionWorksOnPlatform(TextInputAction inputAction) {
assert(() {
if (Platform.isIOS) {
assert(
_iOSSupportedInputActions.contains(inputAction),
'The requested TextInputAction "$inputAction" is not supported on iOS.',
);
} else if (Platform.isAndroid) {
assert(
_androidSupportedInputActions.contains(inputAction),
'The requested TextInputAction "$inputAction" is not supported on Android.',
);
}
return true;
}());
return true;
}
}
......@@ -133,6 +133,36 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
/// movement. This widget does not provide any focus management (e.g.,
/// tap-to-focus).
///
/// ## Input Actions
///
/// A [TextInputAction] can be provided to customize the appearance of the
/// action button on the soft keyboard for Android and iOS. The default action
/// is [TextInputAction.done].
///
/// Many [TextInputAction]s are common between Android and iOS. However, if an
/// [inputAction] is provided that is not supported by the current
/// platform in debug mode, an error will be thrown when the corresponding
/// EditableText receives focus. For example, providing iOS's "emergencyCall"
/// action when running on an Android device will result in an error when in
/// debug mode. In release mode, incompatible [TextInputAction]s are replaced
/// either with "unspecified" on Android, or "default" on iOS. Appropriate
/// [inputAction]s can be chosen by checking the current platform and then
/// selecting the appropriate action.
///
/// ## Lifecycle
///
/// Upon completion of editing, like pressing the "done" button on the keyboard,
/// two actions take place:
///
/// 1st: Editing is finalized. The default behavior of this step includes
/// an invocation of [onChanged]. That default behavior can be overridden.
/// See [onEditingComplete] for details.
///
/// 2nd: [onSubmitted] is invoked with the user's input value.
///
/// [onSubmitted] can be used to manually move focus to another input widget
/// when a user finishes with the currently focused input widget.
///
/// Rather than using this widget directly, consider using [TextField], which
/// is a full-featured, material-design text input field with placeholder text,
/// labels, and [Form] integration.
......@@ -171,7 +201,9 @@ class EditableText extends StatefulWidget {
this.selectionColor,
this.selectionControls,
TextInputType keyboardType,
this.textInputAction = TextInputAction.done,
this.onChanged,
this.onEditingComplete,
this.onSubmitted,
this.onSelectionChanged,
List<TextInputFormatter> inputFormatters,
......@@ -280,9 +312,30 @@ class EditableText extends StatefulWidget {
/// The type of keyboard to use for editing the text.
final TextInputType keyboardType;
/// The type of action button to use with the soft keyboard.
final TextInputAction textInputAction;
/// Called when the text being edited changes.
final ValueChanged<String> onChanged;
/// Called when the user submits editable content (e.g., user presses the "done"
/// button on the keyboard).
///
/// The default implementation of [onEditingComplete] executes 2 different
/// behaviors based on the situation:
///
/// - When a completion action is pressed, such as "done", "go", "send", or
/// "search", the user's content is submitted to the [controller] and then
/// focus is given up.
///
/// - When a non-completion action is pressed, such as "next" or "previous",
/// the user's content is submitted to the [controller], but focus is not
/// given up because developers may want to immediately move focus to
/// another input widget within [onSubmitted].
///
/// Providing [onEditingComplete] prevents the aforementioned default behavior.
final VoidCallback onEditingComplete;
/// Called when the user indicates that they are done editing the text in the field.
final ValueChanged<String> onSubmitted;
......@@ -405,14 +458,41 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override
void performAction(TextInputAction action) {
switch (action) {
case TextInputAction.newline:
// Do nothing for a "newline" action: the newline is already inserted.
break;
case TextInputAction.done:
widget.controller.clearComposing();
widget.focusNode.unfocus();
case TextInputAction.go:
case TextInputAction.send:
case TextInputAction.search:
// Take any actions necessary now that the user has completed editing.
if (widget.onEditingComplete != null) {
widget.onEditingComplete();
} else {
// Default behavior if the developer did not provide an
// onEditingComplete callback: Finalize editing and remove focus.
widget.controller.clearComposing();
widget.focusNode.unfocus();
}
// Invoke optional callback with the user's submitted content.
if (widget.onSubmitted != null)
widget.onSubmitted(_value.text);
break;
case TextInputAction.newline:
// Do nothing for a "newline" action: the newline is already inserted.
default:
if (widget.onEditingComplete != null) {
widget.onEditingComplete();
} else {
// Default behavior if the developer did not provide an
// onEditingComplete callback: Finalize editing, but don't give up
// focus because this keyboard action does not imply the user is done
// inputting information.
widget.controller.clearComposing();
}
// Invoke optional callback with the user's submitted content.
if (widget.onSubmitted != null)
widget.onSubmitted(_value.text);
break;
}
}
......@@ -467,7 +547,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
autocorrect: widget.autocorrect,
inputAction: widget.keyboardType == TextInputType.multiline
? TextInputAction.newline
: TextInputAction.done
: widget.textInputAction,
)
)..setEditingState(localValue);
}
......
......@@ -45,7 +45,7 @@ void main() {
);
await tester.showKeyboard(find.byType(TextField));
tester.testTextInput.receiveAction(TextInputAction.done);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pump();
expect(_called, true);
});
......
......@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
......@@ -22,15 +24,53 @@ void main() {
debugResetSemanticsIdCounter();
});
// Tests that the desired keyboard action button is requested.
//
// More technically, when an EditableText is given a particular [action], Flutter
// requests [serializedActionName] when attaching to the platform's input
// system.
Future<Null> _desiredKeyboardActionIsRequested({
WidgetTester tester,
TextInputAction action,
String serializedActionName,
}) async {
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new FocusScope(
node: focusScopeNode,
autofocus: true,
child: new EditableText(
controller: controller,
focusNode: focusNode,
textInputAction: action,
style: textStyle,
cursorColor: cursorColor,
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = 'test';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputAction'], equals(serializedActionName));
}
testWidgets('has expected defaults', (WidgetTester tester) async {
await tester.pumpWidget(new Directionality(
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new EditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
)));
),
),
);
final EditableText editableText =
tester.firstWidget(find.byType(EditableText));
......@@ -41,17 +81,21 @@ void main() {
testWidgets('text keyboard is requested when maxLines is default',
(WidgetTester tester) async {
await tester.pumpWidget(new Directionality(
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new FocusScope(
node: focusScopeNode,
autofocus: true,
child: new EditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
))));
node: focusScopeNode,
autofocus: true,
child: new EditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = 'test';
......@@ -65,20 +109,141 @@ void main() {
equals('TextInputAction.done'));
});
testWidgets('Keyboard is configured for "unspecified" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.unspecified,
serializedActionName: 'TextInputAction.unspecified',
);
});
testWidgets('Keyboard is configured for "none" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.none,
serializedActionName: 'TextInputAction.none',
);
});
testWidgets('Keyboard is configured for "done" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.done,
serializedActionName: 'TextInputAction.done',
);
});
testWidgets('Keyboard is configured for "send" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.send,
serializedActionName: 'TextInputAction.send',
);
});
testWidgets('Keyboard is configured for "go" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.go,
serializedActionName: 'TextInputAction.go',
);
});
testWidgets('Keyboard is configured for "search" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.search,
serializedActionName: 'TextInputAction.search',
);
});
testWidgets('Keyboard is configured for "send" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.send,
serializedActionName: 'TextInputAction.send',
);
});
testWidgets('Keyboard is configured for "next" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.next,
serializedActionName: 'TextInputAction.next',
);
});
testWidgets('Keyboard is configured for "previous" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.previous,
serializedActionName: 'TextInputAction.previous',
);
});
testWidgets('Keyboard is configured for "continue" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.continueAction,
serializedActionName: 'TextInputAction.continueAction',
);
});
testWidgets('Keyboard is configured for "join" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.join,
serializedActionName: 'TextInputAction.join',
);
});
testWidgets('Keyboard is configured for "route" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.route,
serializedActionName: 'TextInputAction.route',
);
});
testWidgets('Keyboard is configured for "emergencyCall" action when explicitly requested',
(WidgetTester tester) async {
await _desiredKeyboardActionIsRequested(
tester: tester,
action: TextInputAction.emergencyCall,
serializedActionName: 'TextInputAction.emergencyCall',
);
});
testWidgets('multiline keyboard is requested when set explicitly',
(WidgetTester tester) async {
await tester.pumpWidget(new Directionality(
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new FocusScope(
node: focusScopeNode,
autofocus: true,
child: new EditableText(
controller: controller,
focusNode: focusNode,
keyboardType: TextInputType.multiline,
style: textStyle,
cursorColor: cursorColor,
))));
node: focusScopeNode,
autofocus: true,
child: new EditableText(
controller: controller,
focusNode: focusNode,
keyboardType: TextInputType.multiline,
style: textStyle,
cursorColor: cursorColor,
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
......@@ -91,19 +256,23 @@ void main() {
testWidgets('Correct keyboard is requested when set explicitly and maxLines > 1',
(WidgetTester tester) async {
await tester.pumpWidget(new Directionality(
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new FocusScope(
node: focusScopeNode,
autofocus: true,
child: new EditableText(
controller: controller,
focusNode: focusNode,
keyboardType: TextInputType.phone,
maxLines: 3,
style: textStyle,
cursorColor: cursorColor,
))));
node: focusScopeNode,
autofocus: true,
child: new EditableText(
controller: controller,
focusNode: focusNode,
keyboardType: TextInputType.phone,
maxLines: 3,
style: textStyle,
cursorColor: cursorColor,
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
......@@ -116,18 +285,22 @@ void main() {
testWidgets('multiline keyboard is requested when set implicitly',
(WidgetTester tester) async {
await tester.pumpWidget(new Directionality(
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new FocusScope(
node: focusScopeNode,
autofocus: true,
child: new EditableText(
controller: controller,
focusNode: focusNode,
maxLines: 3, // Sets multiline keyboard implicitly.
style: textStyle,
cursorColor: cursorColor,
))));
node: focusScopeNode,
autofocus: true,
child: new EditableText(
controller: controller,
focusNode: focusNode,
maxLines: 3, // Sets multiline keyboard implicitly.
style: textStyle,
cursorColor: cursorColor,
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
......@@ -140,18 +313,22 @@ void main() {
testWidgets('single line inputs have correct default keyboard',
(WidgetTester tester) async {
await tester.pumpWidget(new Directionality(
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new FocusScope(
node: focusScopeNode,
autofocus: true,
child: new EditableText(
controller: controller,
focusNode: focusNode,
maxLines: 1, // Sets text keyboard implicitly.
style: textStyle,
cursorColor: cursorColor,
))));
node: focusScopeNode,
autofocus: true,
child: new EditableText(
controller: controller,
focusNode: focusNode,
maxLines: 1, // Sets text keyboard implicitly.
style: textStyle,
cursorColor: cursorColor,
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
......@@ -201,6 +378,188 @@ void main() {
expect(changedValue, clipboardContent);
});
testWidgets('Loses focus by default when "done" action is pressed', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>();
final FocusNode focusNode = new FocusNode();
final Widget widget = new MaterialApp(
home: new EditableText(
key: editableTextKey,
controller: new TextEditingController(),
focusNode: focusNode,
style: new Typography(platform: TargetPlatform.android).black.subhead,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
),
);
await tester.pumpWidget(widget);
// Select EditableText to give it focus.
final Finder textFinder = find.byKey(editableTextKey);
await tester.tap(textFinder);
await tester.pump();
assert(focusNode.hasFocus);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pump();
// Lost focus because "done" was pressed.
expect(focusNode.hasFocus, false);
});
testWidgets('Does not lose focus by default when "next" action is pressed', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>();
final FocusNode focusNode = new FocusNode();
final Widget widget = new MaterialApp(
home: new EditableText(
key: editableTextKey,
controller: new TextEditingController(),
focusNode: focusNode,
style: new Typography(platform: TargetPlatform.android).black.subhead,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
),
);
await tester.pumpWidget(widget);
// Select EditableText to give it focus.
final Finder textFinder = find.byKey(editableTextKey);
await tester.tap(textFinder);
await tester.pump();
assert(focusNode.hasFocus);
await tester.testTextInput.receiveAction(TextInputAction.next);
await tester.pump();
// Still has focus after pressing "next".
expect(focusNode.hasFocus, true);
});
testWidgets('Does not lose focus by default when "done" action is pressed and onEditingComplete is provided',
(WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>();
final FocusNode focusNode = new FocusNode();
final Widget widget = new MaterialApp(
home: new EditableText(
key: editableTextKey,
controller: new TextEditingController(),
focusNode: focusNode,
style: new Typography(platform: TargetPlatform.android).black.subhead,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
onEditingComplete: () {
// This prevents the default focus change behavior on submission.
},
),
);
await tester.pumpWidget(widget);
// Select EditableText to give it focus.
final Finder textFinder = find.byKey(editableTextKey);
await tester.tap(textFinder);
await tester.pump();
assert(focusNode.hasFocus);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pump();
// Still has focus even though "done" was pressed because onEditingComplete
// was provided and it overrides the default behavior.
expect(focusNode.hasFocus, true);
});
testWidgets('When "done" is pressed callbacks are invoked: onEditingComplete > onSubmitted',
(WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>();
final FocusNode focusNode = new FocusNode();
bool onEditingCompleteCalled = false;
bool onSubmittedCalled = false;
final Widget widget = new MaterialApp(
home: new EditableText(
key: editableTextKey,
controller: new TextEditingController(),
focusNode: focusNode,
style: new Typography(platform: TargetPlatform.android).black.subhead,
cursorColor: Colors.blue,
onEditingComplete: () {
onEditingCompleteCalled = true;
expect(onSubmittedCalled, false);
},
onSubmitted: (String value) {
onSubmittedCalled = true;
expect(onEditingCompleteCalled, true);
},
),
);
await tester.pumpWidget(widget);
// Select EditableText to give it focus.
final Finder textFinder = find.byKey(editableTextKey);
await tester.tap(textFinder);
await tester.pump();
assert(focusNode.hasFocus);
// The execution path starting with receiveAction() will trigger the
// onEditingComplete and onSubmission callbacks.
await tester.testTextInput.receiveAction(TextInputAction.done);
// The expectations we care about are up above in the onEditingComplete
// and onSubmission callbacks.
});
testWidgets('When "next" is pressed callbacks are invoked: onEditingComplete > onSubmitted',
(WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>();
final FocusNode focusNode = new FocusNode();
bool onEditingCompleteCalled = false;
bool onSubmittedCalled = false;
final Widget widget = new MaterialApp(
home: new EditableText(
key: editableTextKey,
controller: new TextEditingController(),
focusNode: focusNode,
style: new Typography(platform: TargetPlatform.android).black.subhead,
cursorColor: Colors.blue,
onEditingComplete: () {
onEditingCompleteCalled = true;
assert(!onSubmittedCalled);
},
onSubmitted: (String value) {
onSubmittedCalled = true;
assert(onEditingCompleteCalled);
},
),
);
await tester.pumpWidget(widget);
// Select EditableText to give it focus.
final Finder textFinder = find.byKey(editableTextKey);
await tester.tap(textFinder);
await tester.pump();
assert(focusNode.hasFocus);
// The execution path starting with receiveAction() will trigger the
// onEditingComplete and onSubmission callbacks.
await tester.testTextInput.receiveAction(TextInputAction.done);
// The expectations we care about are up above in the onEditingComplete
// and onSubmission callbacks.
});
testWidgets('Changing controller updates EditableText', (WidgetTester tester) async {
final GlobalKey<EditableTextState> editableTextKey = new GlobalKey<EditableTextState>();
final TextEditingController controller1 = new TextEditingController(text: 'Wibble');
......
......@@ -6,6 +6,7 @@ import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/foundation.dart';
import 'widget_tester.dart';
......@@ -126,21 +127,42 @@ class TestTextInput {
/// Simulates the user pressing one of the [TextInputAction] buttons.
/// Does not check that the [TextInputAction] performed is an acceptable one
/// based on the `inputAction` [setClientArgs].
void receiveAction(TextInputAction action) {
// Not using the `expect` function because in the case of a FlutterDriver
// test this code does not run in a package:test test zone.
if (_client == 0)
throw new TestFailure('Tried to use TestTextInput with no keyboard attached. You must use WidgetTester.showKeyboard() first.');
BinaryMessages.handlePlatformMessage(
SystemChannels.textInput.name,
SystemChannels.textInput.codec.encodeMethodCall(
new MethodCall(
'TextInputClient.performAction',
<dynamic>[_client, action.toString()],
Future<Null> receiveAction(TextInputAction action) async {
return TestAsyncUtils.guard(() {
// Not using the `expect` function because in the case of a FlutterDriver
// test this code does not run in a package:test test zone.
if (_client == 0) {
throw new TestFailure('Tried to use TestTextInput with no keyboard attached. You must use WidgetTester.showKeyboard() first.');
}
final Completer<Null> completer = new Completer<Null>();
BinaryMessages.handlePlatformMessage(
SystemChannels.textInput.name,
SystemChannels.textInput.codec.encodeMethodCall(
new MethodCall(
'TextInputClient.performAction',
<dynamic>[_client, action.toString()],
),
),
),
(ByteData data) { /* response from framework is discarded */ },
);
(ByteData data) {
try {
// Decoding throws a PlatformException if the data represents an
// error, and that's all we care about here.
SystemChannels.textInput.codec.decodeEnvelope(data);
// No error was found. Complete without issue.
completer.complete();
} catch (error) {
// An exception occurred as a result of receiveAction()'ing. Report
// that error.
completer.completeError(error);
}
},
);
return completer.future;
});
}
/// Simulates the user hiding the onscreen keyboard.
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('receiveAction() forwards exception when exception occurs during action processing',
(WidgetTester tester) async {
// Setup a widget that can receive focus so that we can open the keyboard.
final Widget widget = new MaterialApp(
home: const Material(
child: const TextField(),
),
);
await tester.pumpWidget(widget);
// Keyboard must be shown for receiveAction() to function.
await tester.showKeyboard(find.byType(TextField));
// Register a handler for the text input channel that throws an error. This
// error should be reported within a PlatformException by TestTextInput.
SystemChannels.textInput.setMethodCallHandler((MethodCall call) {
throw new FlutterError('A fake error occurred during action processing.');
});
try {
await tester.testTextInput.receiveAction(TextInputAction.done);
fail('Expected a PlatformException, but it was not thrown.');
} catch (e) {
expect(e, isInstanceOf<PlatformException>());
}
});
}
\ No newline at end of file
......@@ -518,10 +518,10 @@ void main() {
),
);
await tester.showKeyboard(find.byType(TextField));
tester.testTextInput.receiveAction(TextInputAction.done);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pump();
await tester.showKeyboard(find.byType(TextField));
tester.testTextInput.receiveAction(TextInputAction.done);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pump();
await tester.showKeyboard(find.byType(TextField));
await tester.showKeyboard(find.byType(TextField));
......
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