Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
2a65505e
Unverified
Commit
2a65505e
authored
Jul 02, 2018
by
matthew-carroll
Committed by
GitHub
Jul 02, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Support all keyboard actions. (#11344) (#18855)
* Support all keyboard actions. (#11344)
parent
af5d4c68
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
830 additions
and
77 deletions
+830
-77
text_field.dart
packages/flutter/lib/src/material/text_field.dart
+8
-0
text_input.dart
packages/flutter/lib/src/services/text_input.dart
+251
-3
editable_text.dart
packages/flutter/lib/src/widgets/editable_text.dart
+85
-5
text_form_field_test.dart
packages/flutter/test/material/text_form_field_test.dart
+1
-1
editable_text_test.dart
packages/flutter/test/widgets/editable_text_test.dart
+411
-52
test_text_input.dart
packages/flutter_test/lib/src/test_text_input.dart
+36
-14
test_text_input_test.dart
packages/flutter_test/test/test_text_input_test.dart
+36
-0
widget_tester_test.dart
packages/flutter_test/test/widget_tester_test.dart
+2
-2
No files found.
packages/flutter/lib/src/material/text_field.dart
View file @
2a65505e
...
...
@@ -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
,
...
...
packages/flutter/lib/src/services/text_input.dart
View file @
2a65505e
...
...
@@ -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
;
}
}
packages/flutter/lib/src/widgets/editable_text.dart
View file @
2a65505e
...
...
@@ -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
);
}
...
...
packages/flutter/test/material/text_form_field_test.dart
View file @
2a65505e
...
...
@@ -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
);
});
...
...
packages/flutter/test/widgets/editable_text_test.dart
View file @
2a65505e
...
...
@@ -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'
);
...
...
packages/flutter_test/lib/src/test_text_input.dart
View file @
2a65505e
...
...
@@ -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.
...
...
packages/flutter_test/test/test_text_input_test.dart
0 → 100644
View file @
2a65505e
// 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
packages/flutter_test/test/widget_tester_test.dart
View file @
2a65505e
...
...
@@ -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
));
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment