* Support all keyboard actions. (#11344)
This commit is contained in:
@@ -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.
|
||||
|
||||
36
packages/flutter_test/test/test_text_input_test.dart
Normal file
36
packages/flutter_test/test/test_text_input_test.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
// 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>());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user