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,