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

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

* Support all keyboard actions. (#11344)
parent af5d4c68
......@@ -102,6 +102,7 @@ class TextField extends StatefulWidget {
this.focusNode,
this.decoration = const InputDecoration(),
TextInputType keyboardType = TextInputType.text,
this.textInputAction = TextInputAction.done,
this.style,
this.textAlign = TextAlign.start,
this.autofocus = false,
......@@ -115,6 +116,7 @@ class TextField extends StatefulWidget {
this.inputFormatters,
this.enabled,
}) : assert(keyboardType != null),
assert(textInputAction != null),
assert(textAlign != null),
assert(autofocus != null),
assert(obscureText != null),
......@@ -151,6 +153,11 @@ class TextField extends StatefulWidget {
/// [TextInputType.multiline] keyboard type is used.
final TextInputType keyboardType;
/// The type of action button to use for the keyboard.
///
/// Defaults to [TextInputAction.done]. Must not be null.
final TextInputAction textInputAction;
/// The style to use for the text being edited.
///
/// This text style is also used as the base style for the [decoration].
......@@ -473,6 +480,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
controller: controller,
focusNode: focusNode,
keyboardType: widget.keyboardType,
textInputAction: widget.textInputAction,
style: style,
textAlign: widget.textAlign,
autofocus: widget.autofocus,
......
......@@ -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:
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);
});
......
......@@ -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,11 +127,16 @@ 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) {
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)
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(
......@@ -139,8 +145,24 @@ class TestTextInput {
<dynamic>[_client, action.toString()],
),
),
(ByteData data) { /* response from framework is discarded */ },
(ByteData data) {
try {
// Decoding throws a PlatformException if the data represents an
// error, and that's all we care about here.
SystemChannels.textInput.codec.decodeEnvelope(data);
// No error was found. Complete without issue.
completer.complete();
} catch (error) {
// An exception occurred as a result of receiveAction()'ing. Report
// that error.
completer.completeError(error);
}
},
);
return completer.future;
});
}
/// Simulates the user hiding the onscreen keyboard.
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('receiveAction() forwards exception when exception occurs during action processing',
(WidgetTester tester) async {
// Setup a widget that can receive focus so that we can open the keyboard.
final Widget widget = new MaterialApp(
home: const Material(
child: const TextField(),
),
);
await tester.pumpWidget(widget);
// Keyboard must be shown for receiveAction() to function.
await tester.showKeyboard(find.byType(TextField));
// Register a handler for the text input channel that throws an error. This
// error should be reported within a PlatformException by TestTextInput.
SystemChannels.textInput.setMethodCallHandler((MethodCall call) {
throw new FlutterError('A fake error occurred during action processing.');
});
try {
await tester.testTextInput.receiveAction(TextInputAction.done);
fail('Expected a PlatformException, but it was not thrown.');
} catch (e) {
expect(e, isInstanceOf<PlatformException>());
}
});
}
\ No newline at end of file
......@@ -518,10 +518,10 @@ void main() {
),
);
await tester.showKeyboard(find.byType(TextField));
tester.testTextInput.receiveAction(TextInputAction.done);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pump();
await tester.showKeyboard(find.byType(TextField));
tester.testTextInput.receiveAction(TextInputAction.done);
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pump();
await tester.showKeyboard(find.byType(TextField));
await tester.showKeyboard(find.byType(TextField));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment