This adds multiline text widget support. (#12120) Add multiline keyboard support to editable text widget. Fixes #8028.
diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index 09fa0f3..2638912 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart
@@ -66,8 +66,10 @@ /// null. /// /// 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 - /// text field. If it is not null, it must be greater than zero. + /// the number of lines. By default, it is one, meaning this is a single-line + /// 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 /// [autocorrect] arguments must not be null. @@ -76,7 +78,7 @@ this.controller, this.focusNode, this.decoration: const InputDecoration(), - this.keyboardType: TextInputType.text, + TextInputType keyboardType: TextInputType.text, this.style, this.textAlign: TextAlign.start, this.autofocus: false, @@ -92,6 +94,7 @@ assert(obscureText != null), assert(autocorrect != null), assert(maxLines == null || maxLines > 0), + keyboardType = maxLines == 1 ? keyboardType : TextInputType.multiline, super(key: key); /// Controls the text being edited. @@ -115,7 +118,9 @@ /// 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; /// The style to use for the text being edited.
diff --git a/packages/flutter/lib/src/services/text_input.dart b/packages/flutter/lib/src/services/text_input.dart index b36eb9f..5b6b75a 100644 --- a/packages/flutter/lib/src/services/text_input.dart +++ b/packages/flutter/lib/src/services/text_input.dart
@@ -22,6 +22,13 @@ /// Requests the default platform keyboard. 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. /// /// Requests a keyboard with ready access to the decimal point and number @@ -56,6 +63,10 @@ enum TextInputAction { /// Complete the text input operation. 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. @@ -67,15 +78,18 @@ class TextInputConfiguration { /// 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({ this.inputType: TextInputType.text, this.obscureText: false, this.autocorrect: true, this.actionLabel, + this.inputAction: TextInputAction.done, }) : assert(inputType != 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. final TextInputType inputType; @@ -93,6 +107,9 @@ /// What text to display in the text input control's action button. 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. Map<String, dynamic> toJSON() { return <String, dynamic>{ @@ -100,6 +117,7 @@ 'obscureText': obscureText, 'autocorrect': autocorrect, 'actionLabel': actionLabel, + 'inputAction': inputAction.toString(), }; } } @@ -278,6 +296,8 @@ switch (action) { case 'TextInputAction.done': return TextInputAction.done; + case 'TextInputAction.newline': + return TextInputAction.newline; } throw new FlutterError('Unknown text input action: $action'); }
diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index aba7051..317d11d 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart
@@ -141,8 +141,12 @@ /// Creates a basic text input control. /// /// 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 - /// text field. If it is not null, it must be greater than zero. + /// the number of lines. By default, it is one, meaning this is a single-line + /// 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] /// arguments must not be null. @@ -161,7 +165,7 @@ this.autofocus: false, this.selectionColor, this.selectionControls, - this.keyboardType, + TextInputType keyboardType, this.onChanged, this.onSubmitted, this.onSelectionChanged, @@ -175,6 +179,7 @@ assert(textAlign != null), assert(maxLines == null || maxLines > 0), assert(autofocus != null), + keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline), inputFormatters = maxLines == 1 ? ( <TextInputFormatter>[BlacklistingTextInputFormatter.singleLineFormatter] @@ -376,10 +381,17 @@ @override void performAction(TextInputAction action) { - widget.controller.clearComposing(); - widget.focusNode.unfocus(); - if (widget.onSubmitted != null) - widget.onSubmitted(_value.text); + switch (action) { + case TextInputAction.done: + widget.controller.clearComposing(); + 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() { @@ -419,8 +431,16 @@ if (!_hasInputConnection) { final TextEditingValue localValue = _value; _lastKnownRemoteTextEditingValue = localValue; - _textInputConnection = TextInput.attach(this, new TextInputConfiguration(inputType: widget.keyboardType, obscureText: widget.obscureText, autocorrect: widget.autocorrect)) - ..setEditingState(localValue); + _textInputConnection = TextInput.attach(this, + new TextInputConfiguration( + inputType: widget.keyboardType, + obscureText: widget.obscureText, + autocorrect: widget.autocorrect, + inputAction: widget.keyboardType == TextInputType.multiline + ? TextInputAction.newline + : TextInputAction.done + ) + )..setEditingState(localValue); } _textInputConnection.show(); }
diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart new file mode 100644 index 0000000..5947885 --- /dev/null +++ b/packages/flutter/test/widgets/editable_text_test.dart
@@ -0,0 +1,156 @@ +// 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')); + }); +}
diff --git a/packages/flutter_test/lib/src/test_text_input.dart b/packages/flutter_test/lib/src/test_text_input.dart index ed2791b..353180c 100644 --- a/packages/flutter_test/lib/src/test_text_input.dart +++ b/packages/flutter_test/lib/src/test_text_input.dart
@@ -28,6 +28,9 @@ int _client = 0; + /// Arguments supplied to the TextInput.setClient method call. + Map<String, dynamic> setClientArgs; + /// The last set of arguments that [TextInputConnection.setEditingState] sent /// to the embedder. /// @@ -40,6 +43,7 @@ switch (methodCall.method) { case 'TextInput.setClient': _client = methodCall.arguments[0]; + setClientArgs = methodCall.arguments[1]; break; case 'TextInput.clearClient': _client = 0;