Fix: On the Web, cannot support multiline inputting when registering customized TextInputControl (#45522)
For [flutter/flutter/125875](https://github.com/flutter/flutter/issues/125875)
When registering customized TextInputControl, the _PlatformTextInputControl sends inputType = TextInputType.none to the engine. After receiving TextInputType.none, the engine on the Web will create a `<input>` element instead of `<textarea>`. So there is no way to input \n(multiline).
This is my solution. I tested in Android Chrome, iOS Safari, and macOS Chrome. It works for me. But I'm not sure about other use cases. I pleasure, If someone gives me suggestions.
diff --git a/lib/web_ui/lib/src/engine/text_editing/input_type.dart b/lib/web_ui/lib/src/engine/text_editing/input_type.dart
index c9f07c5..20cceed 100644
--- a/lib/web_ui/lib/src/engine/text_editing/input_type.dart
+++ b/lib/web_ui/lib/src/engine/text_editing/input_type.dart
@@ -14,7 +14,7 @@
abstract class EngineInputType {
const EngineInputType();
- static EngineInputType fromName(String name, {bool isDecimal = false}) {
+ static EngineInputType fromName(String name, {bool isDecimal = false, bool isMultiline = false}) {
switch (name) {
case 'TextInputType.number':
return isDecimal ? decimal : number;
@@ -27,7 +27,7 @@
case 'TextInputType.multiline':
return multiline;
case 'TextInputType.none':
- return none;
+ return isMultiline ? multilineNone : none;
case 'TextInputType.text':
default:
return text;
@@ -37,6 +37,9 @@
/// No text input.
static const NoTextInputType none = NoTextInputType();
+ /// Multi-line no text input.
+ static const MultilineNoTextInputType multilineNone = MultilineNoTextInputType();
+
/// Single-line text input type.
static const TextInputType text = TextInputType();
@@ -94,6 +97,31 @@
String get inputmodeAttribute => 'none';
}
+/// See: https://github.com/flutter/flutter/issues/125875
+/// Multi-line no text input from system virtual keyboard.
+///
+/// Use this for inputting multiple lines with a customized keyboard.
+///
+/// When Flutter uses a custom virtual keyboard, it sends [TextInputType.none]
+/// with a [isMultiline] flag to block the system virtual keyboard.
+///
+/// For [MultilineNoTextInputType] (mapped to [TextInputType.none] with
+/// [isMultiline] = true), it creates a <textarea> element with the
+/// inputmode="none" attribute.
+///
+/// For [NoTextInputType] (mapped to [TextInputType.none] with
+/// [isMultiline] = false), it creates an <input> element with the
+/// inputmode="none" attribute.
+class MultilineNoTextInputType extends MultilineInputType {
+ const MultilineNoTextInputType();
+
+ @override
+ String? get inputmodeAttribute => 'none';
+
+ @override
+ DomHTMLElement createDomElement() => createDomHTMLTextAreaElement();
+}
+
/// Single-line text input type.
class TextInputType extends EngineInputType {
const TextInputType();
diff --git a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart
index af1b98a..1974977 100644
--- a/lib/web_ui/lib/src/engine/text_editing/text_editing.dart
+++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart
@@ -961,6 +961,7 @@
: inputType = EngineInputType.fromName(
flutterInputConfiguration.readJson('inputType').readString('name'),
isDecimal: flutterInputConfiguration.readJson('inputType').tryBool('decimal') ?? false,
+ isMultiline: flutterInputConfiguration.readJson('inputType').tryBool('isMultiline') ?? false,
),
inputAction =
flutterInputConfiguration.tryString('inputAction') ?? 'TextInputAction.done',
@@ -1280,7 +1281,7 @@
activeDomElement.setAttribute('type', 'password');
}
- if (config.inputType == EngineInputType.none) {
+ if (config.inputType.inputmodeAttribute == 'none') {
activeDomElement.setAttribute('inputmode', 'none');
}
diff --git a/lib/web_ui/test/engine/text_editing_test.dart b/lib/web_ui/test/engine/text_editing_test.dart
index 7a0ceca..9b601f3 100644
--- a/lib/web_ui/test/engine/text_editing_test.dart
+++ b/lib/web_ui/test/engine/text_editing_test.dart
@@ -441,6 +441,31 @@
expect(event.defaultPrevented, isFalse);
});
+ test('Triggers input action in multiline-none mode', () {
+ final InputConfiguration config = InputConfiguration(
+ inputType: EngineInputType.multilineNone,
+ );
+ editingStrategy!.enable(
+ config,
+ onChange: trackEditingState,
+ onAction: trackInputAction,
+ );
+
+ // No input action so far.
+ expect(lastInputAction, isNull);
+
+ final DomKeyboardEvent event = dispatchKeyboardEvent(
+ editingStrategy!.domElement!,
+ 'keydown',
+ keyCode: _kReturnKeyCode,
+ );
+
+ // Input action is triggered!
+ expect(lastInputAction, 'TextInputAction.done');
+ // And default behavior of keyboard event shouldn't have been prevented.
+ expect(event.defaultPrevented, isFalse);
+ });
+
test('Triggers input action and prevent new line key event for single line field', () {
// Regression test for https://github.com/flutter/flutter/issues/113559
final InputConfiguration config = InputConfiguration();
@@ -509,13 +534,14 @@
required String inputType,
String? inputAction,
bool decimal = false,
+ bool isMultiline = false,
}) {
final MethodCall setClient = MethodCall(
'TextInput.setClient',
<dynamic>[
++clientId,
createFlutterConfig(inputType,
- inputAction: inputAction, decimal: decimal),
+ inputAction: inputAction, decimal: decimal, isMultiline: isMultiline),
],
);
sendFrameworkMessage(codec.encodeMethodCall(setClient));
@@ -2133,6 +2159,52 @@
expect(spy.messages, isEmpty);
});
+ test('none mode works', () async {
+ final MethodCall setClient = MethodCall(
+ 'TextInput.setClient', <dynamic>[123, createFlutterConfig('none')]);
+ sendFrameworkMessage(codec.encodeMethodCall(setClient));
+
+ const MethodCall show = MethodCall('TextInput.show');
+ sendFrameworkMessage(codec.encodeMethodCall(show));
+
+ // The "setSizeAndTransform" message has to be here before we call
+ // checkInputEditingState, since on some platforms (e.g. Desktop Safari)
+ // we don't put the input element into the DOM until we get its correct
+ // dimensions from the framework.
+ final MethodCall setSizeAndTransform =
+ configureSetSizeAndTransformMethodCall(150, 50,
+ Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
+ sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
+
+ await waitForDesktopSafariFocus();
+
+ expect(textEditing!.strategy.domElement!.tagName, 'INPUT');
+ expect(getEditingInputMode(), 'none');
+ });
+
+ test('none multiline mode works', () async {
+ final MethodCall setClient = MethodCall(
+ 'TextInput.setClient', <dynamic>[123, createFlutterConfig('none', isMultiline: true)]);
+ sendFrameworkMessage(codec.encodeMethodCall(setClient));
+
+ const MethodCall show = MethodCall('TextInput.show');
+ sendFrameworkMessage(codec.encodeMethodCall(show));
+
+ // The "setSizeAndTransform" message has to be here before we call
+ // checkInputEditingState, since on some platforms (e.g. Desktop Safari)
+ // we don't put the input element into the DOM until we get its correct
+ // dimensions from the framework.
+ final MethodCall setSizeAndTransform =
+ configureSetSizeAndTransformMethodCall(150, 50,
+ Matrix4.translationValues(10.0, 20.0, 30.0).storage.toList());
+ sendFrameworkMessage(codec.encodeMethodCall(setSizeAndTransform));
+
+ await waitForDesktopSafariFocus();
+
+ expect(textEditing!.strategy.domElement!.tagName, 'TEXTAREA');
+ expect(getEditingInputMode(), 'none');
+ });
+
test('sets correct input type in Android', () {
debugOperatingSystemOverride = OperatingSystem.android;
debugBrowserEngineOverride = BrowserEngine.blink;
@@ -2164,6 +2236,11 @@
showKeyboard(inputType: 'none');
expect(getEditingInputMode(), 'none');
+ expect(textEditing!.strategy.domElement!.tagName, 'INPUT');
+
+ showKeyboard(inputType: 'none', isMultiline: true);
+ expect(getEditingInputMode(), 'none');
+ expect(textEditing!.strategy.domElement!.tagName, 'TEXTAREA');
hideKeyboard();
});
@@ -2253,6 +2330,11 @@
showKeyboard(inputType: 'none');
expect(getEditingInputMode(), 'none');
+ expect(textEditing!.strategy.domElement!.tagName, 'INPUT');
+
+ showKeyboard(inputType: 'none', isMultiline: true);
+ expect(getEditingInputMode(), 'none');
+ expect(textEditing!.strategy.domElement!.tagName, 'TEXTAREA');
hideKeyboard();
}
@@ -3214,11 +3296,13 @@
List<String>? autofillHintsForFields,
bool decimal = false,
bool enableDeltaModel = false,
+ bool isMultiline = false,
}) {
return <String, dynamic>{
'inputType': <String, dynamic>{
'name': 'TextInputType.$inputType',
if (decimal) 'decimal': true,
+ if (isMultiline) 'isMultiline': true,
},
'readOnly': readOnly,
'obscureText': obscureText,