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 { ...@@ -102,6 +102,7 @@ class TextField extends StatefulWidget {
this.focusNode, this.focusNode,
this.decoration = const InputDecoration(), this.decoration = const InputDecoration(),
TextInputType keyboardType = TextInputType.text, TextInputType keyboardType = TextInputType.text,
this.textInputAction = TextInputAction.done,
this.style, this.style,
this.textAlign = TextAlign.start, this.textAlign = TextAlign.start,
this.autofocus = false, this.autofocus = false,
...@@ -115,6 +116,7 @@ class TextField extends StatefulWidget { ...@@ -115,6 +116,7 @@ class TextField extends StatefulWidget {
this.inputFormatters, this.inputFormatters,
this.enabled, this.enabled,
}) : assert(keyboardType != null), }) : assert(keyboardType != null),
assert(textInputAction != null),
assert(textAlign != null), assert(textAlign != null),
assert(autofocus != null), assert(autofocus != null),
assert(obscureText != null), assert(obscureText != null),
...@@ -151,6 +153,11 @@ class TextField extends StatefulWidget { ...@@ -151,6 +153,11 @@ class TextField extends StatefulWidget {
/// [TextInputType.multiline] keyboard type is used. /// [TextInputType.multiline] keyboard type is used.
final TextInputType keyboardType; 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. /// The style to use for the text being edited.
/// ///
/// This text style is also used as the base style for the [decoration]. /// This text style is also used as the base style for the [decoration].
...@@ -473,6 +480,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi ...@@ -473,6 +480,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
keyboardType: widget.keyboardType, keyboardType: widget.keyboardType,
textInputAction: widget.textInputAction,
style: style, style: style,
textAlign: widget.textAlign, textAlign: widget.textAlign,
autofocus: widget.autofocus, autofocus: widget.autofocus,
......
...@@ -133,6 +133,36 @@ class TextEditingController extends ValueNotifier<TextEditingValue> { ...@@ -133,6 +133,36 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
/// movement. This widget does not provide any focus management (e.g., /// movement. This widget does not provide any focus management (e.g.,
/// tap-to-focus). /// 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 /// Rather than using this widget directly, consider using [TextField], which
/// is a full-featured, material-design text input field with placeholder text, /// is a full-featured, material-design text input field with placeholder text,
/// labels, and [Form] integration. /// labels, and [Form] integration.
...@@ -171,7 +201,9 @@ class EditableText extends StatefulWidget { ...@@ -171,7 +201,9 @@ class EditableText extends StatefulWidget {
this.selectionColor, this.selectionColor,
this.selectionControls, this.selectionControls,
TextInputType keyboardType, TextInputType keyboardType,
this.textInputAction = TextInputAction.done,
this.onChanged, this.onChanged,
this.onEditingComplete,
this.onSubmitted, this.onSubmitted,
this.onSelectionChanged, this.onSelectionChanged,
List<TextInputFormatter> inputFormatters, List<TextInputFormatter> inputFormatters,
...@@ -280,9 +312,30 @@ class EditableText extends StatefulWidget { ...@@ -280,9 +312,30 @@ class EditableText extends StatefulWidget {
/// The type of keyboard to use for editing the text. /// The type of keyboard to use for editing the text.
final TextInputType keyboardType; final TextInputType keyboardType;
/// The type of action button to use with the soft keyboard.
final TextInputAction textInputAction;
/// Called when the text being edited changes. /// Called when the text being edited changes.
final ValueChanged<String> onChanged; 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. /// Called when the user indicates that they are done editing the text in the field.
final ValueChanged<String> onSubmitted; final ValueChanged<String> onSubmitted;
...@@ -405,14 +458,41 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -405,14 +458,41 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override @override
void performAction(TextInputAction action) { void performAction(TextInputAction action) {
switch (action) { switch (action) {
case TextInputAction.newline:
// Do nothing for a "newline" action: the newline is already inserted.
break;
case TextInputAction.done: case TextInputAction.done:
widget.controller.clearComposing(); case TextInputAction.go:
widget.focusNode.unfocus(); 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) if (widget.onSubmitted != null)
widget.onSubmitted(_value.text); widget.onSubmitted(_value.text);
break; break;
case TextInputAction.newline: default:
// Do nothing for a "newline" action: the newline is already inserted. 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; break;
} }
} }
...@@ -467,7 +547,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -467,7 +547,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
autocorrect: widget.autocorrect, autocorrect: widget.autocorrect,
inputAction: widget.keyboardType == TextInputType.multiline inputAction: widget.keyboardType == TextInputType.multiline
? TextInputAction.newline ? TextInputAction.newline
: TextInputAction.done : widget.textInputAction,
) )
)..setEditingState(localValue); )..setEditingState(localValue);
} }
......
...@@ -45,7 +45,7 @@ void main() { ...@@ -45,7 +45,7 @@ void main() {
); );
await tester.showKeyboard(find.byType(TextField)); await tester.showKeyboard(find.byType(TextField));
tester.testTextInput.receiveAction(TextInputAction.done); await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pump(); await tester.pump();
expect(_called, true); expect(_called, true);
}); });
......
...@@ -6,6 +6,7 @@ import 'dart:async'; ...@@ -6,6 +6,7 @@ import 'dart:async';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'widget_tester.dart'; import 'widget_tester.dart';
...@@ -126,21 +127,42 @@ class TestTextInput { ...@@ -126,21 +127,42 @@ class TestTextInput {
/// Simulates the user pressing one of the [TextInputAction] buttons. /// Simulates the user pressing one of the [TextInputAction] buttons.
/// Does not check that the [TextInputAction] performed is an acceptable one /// Does not check that the [TextInputAction] performed is an acceptable one
/// based on the `inputAction` [setClientArgs]. /// based on the `inputAction` [setClientArgs].
void receiveAction(TextInputAction action) { Future<Null> receiveAction(TextInputAction action) async {
// Not using the `expect` function because in the case of a FlutterDriver return TestAsyncUtils.guard(() {
// test this code does not run in a package:test test zone. // Not using the `expect` function because in the case of a FlutterDriver
if (_client == 0) // test this code does not run in a package:test test zone.
throw new TestFailure('Tried to use TestTextInput with no keyboard attached. You must use WidgetTester.showKeyboard() first.'); if (_client == 0) {
BinaryMessages.handlePlatformMessage( throw new TestFailure('Tried to use TestTextInput with no keyboard attached. You must use WidgetTester.showKeyboard() first.');
SystemChannels.textInput.name, }
SystemChannels.textInput.codec.encodeMethodCall(
new MethodCall( final Completer<Null> completer = new Completer<Null>();
'TextInputClient.performAction',
<dynamic>[_client, action.toString()], BinaryMessages.handlePlatformMessage(
SystemChannels.textInput.name,
SystemChannels.textInput.codec.encodeMethodCall(
new MethodCall(
'TextInputClient.performAction',
<dynamic>[_client, action.toString()],
),
), ),
), (ByteData data) {
(ByteData data) { /* response from framework is discarded */ }, 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. /// 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() { ...@@ -518,10 +518,10 @@ void main() {
), ),
); );
await tester.showKeyboard(find.byType(TextField)); await tester.showKeyboard(find.byType(TextField));
tester.testTextInput.receiveAction(TextInputAction.done); await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pump(); await tester.pump();
await tester.showKeyboard(find.byType(TextField)); await tester.showKeyboard(find.byType(TextField));
tester.testTextInput.receiveAction(TextInputAction.done); await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pump(); await tester.pump();
await tester.showKeyboard(find.byType(TextField)); await tester.showKeyboard(find.byType(TextField));
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