Fix: On the Web, cannot support multiline inputting when registering customized TextInputControl (flutter/engine#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.
This commit is contained in:
ᠵᠠᠭᠠᠨᠵᠠᠪ ᠪᠤᠷᠵᠢᠭᠤᠳ ᠰᠠᠴᠤᠷᠠᠭ 2024-02-07 23:15:00 +08:00 committed by GitHub
parent bb1dab9840
commit af574f2aae
3 changed files with 117 additions and 4 deletions

View File

@ -14,7 +14,7 @@ import '../dom.dart';
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 @@ abstract class EngineInputType {
case 'TextInputType.multiline':
return multiline;
case 'TextInputType.none':
return none;
return isMultiline ? multilineNone : none;
case 'TextInputType.text':
default:
return text;
@ -37,6 +37,9 @@ abstract class EngineInputType {
/// 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 @@ class NoTextInputType extends EngineInputType {
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();

View File

@ -961,6 +961,7 @@ class InputConfiguration {
: 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 @@ abstract class DefaultTextEditingStrategy with CompositionAwareMixin implements
activeDomElement.setAttribute('type', 'password');
}
if (config.inputType == EngineInputType.none) {
if (config.inputType.inputmodeAttribute == 'none') {
activeDomElement.setAttribute('inputmode', 'none');
}

View File

@ -441,6 +441,31 @@ Future<void> testMain() async {
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 @@ Future<void> testMain() async {
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 @@ Future<void> testMain() async {
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 @@ Future<void> testMain() async {
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 @@ Future<void> testMain() async {
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 @@ Map<String, dynamic> createFlutterConfig(
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,