Commit 4389f070 authored by gspencergoog's avatar gspencergoog Committed by GitHub

This adds multiline text widget support. (#12120)

Add multiline keyboard support to editable text widget.  Fixes #8028.
parent 26707862
...@@ -66,8 +66,10 @@ class TextField extends StatefulWidget { ...@@ -66,8 +66,10 @@ class TextField extends StatefulWidget {
/// null. /// null.
/// ///
/// The [maxLines] property can be set to null to remove the restriction on /// The [maxLines] property can be set to null to remove the restriction on
/// the number of lines. By default, it is 1, meaning this is a single-line /// the number of lines. By default, it is one, meaning this is a single-line
/// text field. If it is not null, it must be greater than zero. /// text field. [maxLines] must not be zero. If [maxLines] is not one, then
/// [keyboardType] is ignored, and the [TextInputType.multiline] keyboard type
/// is used.
/// ///
/// The [keyboardType], [textAlign], [autofocus], [obscureText], and /// The [keyboardType], [textAlign], [autofocus], [obscureText], and
/// [autocorrect] arguments must not be null. /// [autocorrect] arguments must not be null.
...@@ -76,7 +78,7 @@ class TextField extends StatefulWidget { ...@@ -76,7 +78,7 @@ class TextField extends StatefulWidget {
this.controller, this.controller,
this.focusNode, this.focusNode,
this.decoration: const InputDecoration(), this.decoration: const InputDecoration(),
this.keyboardType: TextInputType.text, TextInputType keyboardType: TextInputType.text,
this.style, this.style,
this.textAlign: TextAlign.start, this.textAlign: TextAlign.start,
this.autofocus: false, this.autofocus: false,
...@@ -92,6 +94,7 @@ class TextField extends StatefulWidget { ...@@ -92,6 +94,7 @@ class TextField extends StatefulWidget {
assert(obscureText != null), assert(obscureText != null),
assert(autocorrect != null), assert(autocorrect != null),
assert(maxLines == null || maxLines > 0), assert(maxLines == null || maxLines > 0),
keyboardType = maxLines == 1 ? keyboardType : TextInputType.multiline,
super(key: key); super(key: key);
/// Controls the text being edited. /// Controls the text being edited.
...@@ -115,7 +118,9 @@ class TextField extends StatefulWidget { ...@@ -115,7 +118,9 @@ class TextField extends StatefulWidget {
/// The type of keyboard to use for editing the text. /// The type of keyboard to use for editing the text.
/// ///
/// Defaults to [TextInputType.text]. Cannot be null. /// Defaults to [TextInputType.text]. Must not be null. If
/// [maxLines] is not one, then [keyboardType] is ignored, and the
/// [TextInputType.multiline] keyboard type is used.
final TextInputType keyboardType; final TextInputType keyboardType;
/// The style to use for the text being edited. /// The style to use for the text being edited.
......
...@@ -22,6 +22,13 @@ enum TextInputType { ...@@ -22,6 +22,13 @@ enum TextInputType {
/// Requests the default platform keyboard. /// Requests the default platform keyboard.
text, text,
/// Optimize for multi-line textual information.
///
/// Requests the default platform keyboard, but accepts newlines when the
/// enter key is pressed. This is the input type used for all multi-line text
/// fields.
multiline,
/// Optimize for numerical information. /// Optimize for numerical information.
/// ///
/// Requests a keyboard with ready access to the decimal point and number /// Requests a keyboard with ready access to the decimal point and number
...@@ -56,6 +63,10 @@ enum TextInputType { ...@@ -56,6 +63,10 @@ enum TextInputType {
enum TextInputAction { enum TextInputAction {
/// Complete the text input operation. /// Complete the text input operation.
done, done,
/// The action to take when the enter button is pressed in a multi-line
/// text field (which is typically to do nothing).
newline,
} }
/// Controls the visual appearance of the text input control. /// Controls the visual appearance of the text input control.
...@@ -67,15 +78,18 @@ enum TextInputAction { ...@@ -67,15 +78,18 @@ enum TextInputAction {
class TextInputConfiguration { class TextInputConfiguration {
/// Creates configuration information for a text input control. /// Creates configuration information for a text input control.
/// ///
/// The [inputType], [obscureText], and [autocorrect] arguments must not be null. /// All arguments have default values, except [actionLabel]. Only
/// [actionLabel] may be null.
const TextInputConfiguration({ const TextInputConfiguration({
this.inputType: TextInputType.text, this.inputType: TextInputType.text,
this.obscureText: false, this.obscureText: false,
this.autocorrect: true, this.autocorrect: true,
this.actionLabel, this.actionLabel,
this.inputAction: TextInputAction.done,
}) : assert(inputType != null), }) : assert(inputType != null),
assert(obscureText != null), assert(obscureText != null),
assert(autocorrect != null); assert(autocorrect != null),
assert(inputAction != null);
/// The type of information for which to optimize the text input control. /// The type of information for which to optimize the text input control.
final TextInputType inputType; final TextInputType inputType;
...@@ -93,6 +107,9 @@ class TextInputConfiguration { ...@@ -93,6 +107,9 @@ class TextInputConfiguration {
/// What text to display in the text input control's action button. /// What text to display in the text input control's action button.
final String actionLabel; final String actionLabel;
/// What kind of action to request for the action button on the IME.
final TextInputAction inputAction;
/// Returns a representation of this object as a JSON object. /// Returns a representation of this object as a JSON object.
Map<String, dynamic> toJSON() { Map<String, dynamic> toJSON() {
return <String, dynamic>{ return <String, dynamic>{
...@@ -100,6 +117,7 @@ class TextInputConfiguration { ...@@ -100,6 +117,7 @@ class TextInputConfiguration {
'obscureText': obscureText, 'obscureText': obscureText,
'autocorrect': autocorrect, 'autocorrect': autocorrect,
'actionLabel': actionLabel, 'actionLabel': actionLabel,
'inputAction': inputAction.toString(),
}; };
} }
} }
...@@ -278,6 +296,8 @@ TextInputAction _toTextInputAction(String action) { ...@@ -278,6 +296,8 @@ TextInputAction _toTextInputAction(String action) {
switch (action) { switch (action) {
case 'TextInputAction.done': case 'TextInputAction.done':
return TextInputAction.done; return TextInputAction.done;
case 'TextInputAction.newline':
return TextInputAction.newline;
} }
throw new FlutterError('Unknown text input action: $action'); throw new FlutterError('Unknown text input action: $action');
} }
......
...@@ -141,8 +141,12 @@ class EditableText extends StatefulWidget { ...@@ -141,8 +141,12 @@ class EditableText extends StatefulWidget {
/// Creates a basic text input control. /// Creates a basic text input control.
/// ///
/// The [maxLines] property can be set to null to remove the restriction on /// The [maxLines] property can be set to null to remove the restriction on
/// the number of lines. By default, it is 1, meaning this is a single-line /// the number of lines. By default, it is one, meaning this is a single-line
/// text field. If it is not null, it must be greater than zero. /// text field. [maxLines] must be null or greater than zero.
///
/// If [keyboardType] is not set or is null, it will default to
/// [TextInputType.text] unless [maxLines] is greater than one, when it will
/// default to [TextInputType.multiline].
/// ///
/// The [controller], [focusNode], [style], [cursorColor], and [textAlign] /// The [controller], [focusNode], [style], [cursorColor], and [textAlign]
/// arguments must not be null. /// arguments must not be null.
...@@ -161,7 +165,7 @@ class EditableText extends StatefulWidget { ...@@ -161,7 +165,7 @@ class EditableText extends StatefulWidget {
this.autofocus: false, this.autofocus: false,
this.selectionColor, this.selectionColor,
this.selectionControls, this.selectionControls,
this.keyboardType, TextInputType keyboardType,
this.onChanged, this.onChanged,
this.onSubmitted, this.onSubmitted,
this.onSelectionChanged, this.onSelectionChanged,
...@@ -175,6 +179,7 @@ class EditableText extends StatefulWidget { ...@@ -175,6 +179,7 @@ class EditableText extends StatefulWidget {
assert(textAlign != null), assert(textAlign != null),
assert(maxLines == null || maxLines > 0), assert(maxLines == null || maxLines > 0),
assert(autofocus != null), assert(autofocus != null),
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
inputFormatters = maxLines == 1 inputFormatters = maxLines == 1
? ( ? (
<TextInputFormatter>[BlacklistingTextInputFormatter.singleLineFormatter] <TextInputFormatter>[BlacklistingTextInputFormatter.singleLineFormatter]
...@@ -376,10 +381,17 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -376,10 +381,17 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override @override
void performAction(TextInputAction action) { void performAction(TextInputAction action) {
widget.controller.clearComposing(); switch (action) {
widget.focusNode.unfocus(); case TextInputAction.done:
if (widget.onSubmitted != null) widget.controller.clearComposing();
widget.onSubmitted(_value.text); widget.focusNode.unfocus();
if (widget.onSubmitted != null)
widget.onSubmitted(_value.text);
break;
case TextInputAction.newline:
// Do nothing for a "newline" action: the newline is already inserted.
break;
}
} }
void _updateRemoteEditingValueIfNeeded() { void _updateRemoteEditingValueIfNeeded() {
...@@ -419,8 +431,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -419,8 +431,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (!_hasInputConnection) { if (!_hasInputConnection) {
final TextEditingValue localValue = _value; final TextEditingValue localValue = _value;
_lastKnownRemoteTextEditingValue = localValue; _lastKnownRemoteTextEditingValue = localValue;
_textInputConnection = TextInput.attach(this, new TextInputConfiguration(inputType: widget.keyboardType, obscureText: widget.obscureText, autocorrect: widget.autocorrect)) _textInputConnection = TextInput.attach(this,
..setEditingState(localValue); new TextInputConfiguration(
inputType: widget.keyboardType,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
inputAction: widget.keyboardType == TextInputType.multiline
? TextInputAction.newline
: TextInputAction.done
)
)..setEditingState(localValue);
} }
_textInputConnection.show(); _textInputConnection.show();
} }
......
// Copyright 2017 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_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/services.dart';
void main() {
final TextEditingController controller = new TextEditingController();
final FocusNode focusNode = new FocusNode();
final FocusScopeNode focusScopeNode = new FocusScopeNode();
final TextStyle textStyle = const TextStyle();
final Color cursorColor = const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);
testWidgets('has expected defaults', (WidgetTester tester) async {
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));
expect(editableText.maxLines, equals(1));
expect(editableText.obscureText, isFalse);
expect(editableText.autocorrect, isTrue);
});
testWidgets('text keyboard is requested when maxLines is default',
(WidgetTester tester) async {
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,
))));
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = 'test';
await tester.idle();
final EditableText editableText =
tester.firstWidget(find.byType(EditableText));
expect(editableText.maxLines, equals(1));
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType'],
equals('TextInputType.text'));
expect(tester.testTextInput.setClientArgs['inputAction'],
equals('TextInputAction.done'));
});
testWidgets('multiline keyboard is requested when set explicitly',
(WidgetTester tester) async {
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,
))));
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['inputType'], equals('TextInputType.multiline'));
expect(tester.testTextInput.setClientArgs['inputAction'], equals('TextInputAction.newline'));
});
testWidgets('Correct keyboard is requested when set explicitly and maxLines > 1',
(WidgetTester tester) async {
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,
))));
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['inputType'], equals('TextInputType.phone'));
expect(tester.testTextInput.setClientArgs['inputAction'], equals('TextInputAction.done'));
});
testWidgets('multiline keyboard is requested when set implicitly',
(WidgetTester tester) async {
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,
))));
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['inputType'], equals('TextInputType.multiline'));
expect(tester.testTextInput.setClientArgs['inputAction'], equals('TextInputAction.newline'));
});
testWidgets('single line inputs have correct default keyboard',
(WidgetTester tester) async {
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,
))));
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['inputType'], equals('TextInputType.text'));
expect(tester.testTextInput.setClientArgs['inputAction'], equals('TextInputAction.done'));
});
}
...@@ -28,6 +28,9 @@ class TestTextInput { ...@@ -28,6 +28,9 @@ class TestTextInput {
int _client = 0; int _client = 0;
/// Arguments supplied to the TextInput.setClient method call.
Map<String, dynamic> setClientArgs;
/// The last set of arguments that [TextInputConnection.setEditingState] sent /// The last set of arguments that [TextInputConnection.setEditingState] sent
/// to the embedder. /// to the embedder.
/// ///
...@@ -40,6 +43,7 @@ class TestTextInput { ...@@ -40,6 +43,7 @@ class TestTextInput {
switch (methodCall.method) { switch (methodCall.method) {
case 'TextInput.setClient': case 'TextInput.setClient':
_client = methodCall.arguments[0]; _client = methodCall.arguments[0];
setClientArgs = methodCall.arguments[1];
break; break;
case 'TextInput.clearClient': case 'TextInput.clearClient':
_client = 0; _client = 0;
......
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