mirror of
https://github.com/encounter/engine.git
synced 2026-07-10 03:18:43 -07:00
Support keyboard types on mobile browsers (#13044)
This commit is contained in:
@@ -426,7 +426,8 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/ruler.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/unicode_range.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/word_break_properties.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/word_breaker.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text_editing.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text_editing/input_type.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/util.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/validators.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/vector_math.dart
|
||||
|
||||
@@ -93,7 +93,8 @@ part 'engine/text/ruler.dart';
|
||||
part 'engine/text/unicode_range.dart';
|
||||
part 'engine/text/word_break_properties.dart';
|
||||
part 'engine/text/word_breaker.dart';
|
||||
part 'engine/text_editing.dart';
|
||||
part 'engine/text_editing/input_type.dart';
|
||||
part 'engine/text_editing/text_editing.dart';
|
||||
part 'engine/util.dart';
|
||||
part 'engine/validators.dart';
|
||||
part 'engine/vector_math.dart';
|
||||
|
||||
@@ -77,11 +77,24 @@ OperatingSystem _operatingSystem;
|
||||
///
|
||||
/// This is used to implement operating system specific behavior such as
|
||||
/// soft keyboards.
|
||||
OperatingSystem get operatingSystem =>
|
||||
_operatingSystem ??= _detectOperatingSystem();
|
||||
OperatingSystem get operatingSystem {
|
||||
if (debugOperatingSystemOverride != null) {
|
||||
return debugOperatingSystemOverride;
|
||||
}
|
||||
return _operatingSystem ??= _detectOperatingSystem();
|
||||
}
|
||||
|
||||
/// Override the value of [operatingSystem].
|
||||
///
|
||||
/// Setting this to `null` lets [operatingSystem] detect the real OS that the
|
||||
/// app is running on.
|
||||
///
|
||||
/// This is intended to be used for testing and debugging only.
|
||||
OperatingSystem debugOperatingSystemOverride;
|
||||
|
||||
OperatingSystem _detectOperatingSystem() {
|
||||
final String platform = html.window.navigator.platform;
|
||||
final String userAgent = html.window.navigator.userAgent;
|
||||
|
||||
if (platform.startsWith('Mac')) {
|
||||
return OperatingSystem.macOs;
|
||||
@@ -89,7 +102,10 @@ OperatingSystem _detectOperatingSystem() {
|
||||
platform.toLowerCase().contains('ipad') ||
|
||||
platform.toLowerCase().contains('ipod')) {
|
||||
return OperatingSystem.iOs;
|
||||
} else if (platform.toLowerCase().contains('android')) {
|
||||
} else if (userAgent.contains('Android')) {
|
||||
// The Android OS reports itself as "Linux armv8l" in
|
||||
// [html.window.navigator.platform]. So we have to check the user-agent to
|
||||
// determine if the OS is Android or not.
|
||||
return OperatingSystem.android;
|
||||
} else if (platform.startsWith('Linux')) {
|
||||
return OperatingSystem.linux;
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
part of engine;
|
||||
|
||||
/// Various types of inputs used in text fields.
|
||||
///
|
||||
/// These types are coming from Flutter's [TextInputType]. Currently, we don't
|
||||
/// support all the types. We fallback to [EngineInputType.text] when Flutter
|
||||
/// sends a type that isn't supported.
|
||||
// TODO(flutter_web): Support more types.
|
||||
abstract class EngineInputType {
|
||||
const EngineInputType();
|
||||
|
||||
static EngineInputType fromName(String name) {
|
||||
switch (name) {
|
||||
case 'TextInputType.number':
|
||||
return number;
|
||||
case 'TextInputType.phone':
|
||||
return phone;
|
||||
case 'TextInputType.emailAddress':
|
||||
return emailAddress;
|
||||
case 'TextInputType.url':
|
||||
return url;
|
||||
case 'TextInputType.multiline':
|
||||
return multiline;
|
||||
|
||||
case 'TextInputType.text':
|
||||
default:
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
/// Single-line text input type.
|
||||
static const TextInputType text = TextInputType();
|
||||
|
||||
/// Numeric input type.
|
||||
static const NumberInputType number = NumberInputType();
|
||||
|
||||
/// Phone number input type.
|
||||
static const PhoneInputType phone = PhoneInputType();
|
||||
|
||||
/// Email address input type.
|
||||
static const EmailInputType emailAddress = EmailInputType();
|
||||
|
||||
/// URL input type.
|
||||
static const UrlInputType url = UrlInputType();
|
||||
|
||||
/// Multi-line text input type.
|
||||
static const MultilineInputType multiline = MultilineInputType();
|
||||
|
||||
/// The HTML `inputmode` attribute to be set on the DOM element.
|
||||
///
|
||||
/// This HTML attribute helps the browser decide what kind of keyboard works
|
||||
/// best for this text field.
|
||||
///
|
||||
/// For various `inputmode` values supported by browsers, see:
|
||||
/// <https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inputmode>.
|
||||
String get inputmodeAttribute;
|
||||
|
||||
/// Create the appropriate DOM element for this input type.
|
||||
html.HtmlElement createDomElement() => html.InputElement();
|
||||
|
||||
/// Given a [domElement], set attributes that are specific to this input type.
|
||||
void configureDomElement(html.HtmlElement domElement) {
|
||||
if (inputmodeAttribute == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only apply `inputmode` in mobile browsers so that the right virtual
|
||||
// keyboard shows up.
|
||||
if (operatingSystem == OperatingSystem.iOs ||
|
||||
operatingSystem == OperatingSystem.android) {
|
||||
domElement.setAttribute('inputmode', inputmodeAttribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Single-line text input type.
|
||||
class TextInputType extends EngineInputType {
|
||||
const TextInputType();
|
||||
|
||||
@override
|
||||
final String inputmodeAttribute = 'text';
|
||||
}
|
||||
|
||||
/// Numeric input type.
|
||||
class NumberInputType extends EngineInputType {
|
||||
const NumberInputType();
|
||||
|
||||
@override
|
||||
final String inputmodeAttribute = 'numeric';
|
||||
}
|
||||
|
||||
/// Phone number input type.
|
||||
class PhoneInputType extends EngineInputType {
|
||||
const PhoneInputType();
|
||||
|
||||
@override
|
||||
final String inputmodeAttribute = 'tel';
|
||||
}
|
||||
|
||||
/// Email address input type.
|
||||
class EmailInputType extends EngineInputType {
|
||||
const EmailInputType();
|
||||
|
||||
@override
|
||||
final String inputmodeAttribute = 'email';
|
||||
}
|
||||
|
||||
/// URL input type.
|
||||
class UrlInputType extends EngineInputType {
|
||||
const UrlInputType();
|
||||
|
||||
@override
|
||||
final String inputmodeAttribute = 'url';
|
||||
}
|
||||
|
||||
/// Multi-line text input type.
|
||||
class MultilineInputType extends EngineInputType {
|
||||
const MultilineInputType();
|
||||
|
||||
@override
|
||||
final String inputmodeAttribute = null;
|
||||
|
||||
@override
|
||||
html.HtmlElement createDomElement() => html.TextAreaElement();
|
||||
}
|
||||
+7
-54
@@ -163,31 +163,6 @@ class EditingState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Various types of inputs used in text fields.
|
||||
///
|
||||
/// These types are coming from Flutter's [TextInputType]. Currently, we don't
|
||||
/// support all the types. We fallback to [InputType.text] when Flutter sends
|
||||
/// a type that isn't supported.
|
||||
// TODO(flutter_web): Support more types.
|
||||
enum InputType {
|
||||
/// Single-line plain text.
|
||||
text,
|
||||
|
||||
/// Multi-line text.
|
||||
multiline,
|
||||
}
|
||||
|
||||
InputType _getInputTypeFromString(String inputType) {
|
||||
switch (inputType) {
|
||||
case 'TextInputType.multiline':
|
||||
return InputType.multiline;
|
||||
|
||||
case 'TextInputType.text':
|
||||
default:
|
||||
return InputType.text;
|
||||
}
|
||||
}
|
||||
|
||||
/// Controls the appearance of the input control being edited.
|
||||
///
|
||||
/// For example, [inputType] determines whether we should use `<input>` or
|
||||
@@ -201,12 +176,12 @@ class InputConfiguration {
|
||||
});
|
||||
|
||||
InputConfiguration.fromFlutter(Map<String, dynamic> flutterInputConfiguration)
|
||||
: inputType = _getInputTypeFromString(
|
||||
: inputType = EngineInputType.fromName(
|
||||
flutterInputConfiguration['inputType']['name']),
|
||||
obscureText = flutterInputConfiguration['obscureText'];
|
||||
|
||||
/// The type of information being edited in the input control.
|
||||
final InputType inputType;
|
||||
final EngineInputType inputType;
|
||||
|
||||
/// Whether to hide the text being edited.
|
||||
final bool obscureText;
|
||||
@@ -341,6 +316,7 @@ class TextEditingElement {
|
||||
_subscriptions.add(domElement.onKeyUp.listen((event) {
|
||||
_handleChange(event);
|
||||
}));
|
||||
|
||||
/// In Firefox the context menu item "Select All" does not work without
|
||||
/// listening to onSelect. On the other browsers onSelectionChange is
|
||||
/// enough for covering "Select All" functionality.
|
||||
@@ -370,19 +346,10 @@ class TextEditingElement {
|
||||
}
|
||||
|
||||
void _initDomElement(InputConfiguration inputConfig) {
|
||||
switch (inputConfig.inputType) {
|
||||
case InputType.text:
|
||||
domElement = owner.createInputElement();
|
||||
break;
|
||||
|
||||
case InputType.multiline:
|
||||
domElement = owner.createTextAreaElement();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw UnsupportedError(
|
||||
'Unsupported input type: ${inputConfig.inputType}');
|
||||
}
|
||||
domElement = inputConfig.inputType.createDomElement();
|
||||
inputConfig.inputType.configureDomElement(domElement);
|
||||
_setStaticStyleAttributes(domElement);
|
||||
owner._setDynamicStyleAttributes(domElement);
|
||||
domRenderer.glassPaneElement.append(domElement);
|
||||
}
|
||||
|
||||
@@ -757,20 +724,6 @@ class HybridTextEditing {
|
||||
void setStyleOutsideOfScreen(html.HtmlElement domElement) {
|
||||
domElement.style.transform = 'translate(-9999px, -9999px)';
|
||||
}
|
||||
|
||||
html.InputElement createInputElement() {
|
||||
final html.InputElement input = html.InputElement();
|
||||
_setStaticStyleAttributes(input);
|
||||
_setDynamicStyleAttributes(input);
|
||||
return input;
|
||||
}
|
||||
|
||||
html.TextAreaElement createTextAreaElement() {
|
||||
final html.TextAreaElement textarea = html.TextAreaElement();
|
||||
_setStaticStyleAttributes(textarea);
|
||||
_setDynamicStyleAttributes(textarea);
|
||||
return textarea;
|
||||
}
|
||||
}
|
||||
|
||||
/// Information on the font and alignment of a text editing element.
|
||||
@@ -18,22 +18,14 @@ TextEditingElement editingElement;
|
||||
EditingState lastEditingState;
|
||||
|
||||
final InputConfiguration singlelineConfig =
|
||||
InputConfiguration(inputType: InputType.text);
|
||||
final Map<String, dynamic> flutterSinglelineConfig = <String, dynamic>{
|
||||
'inputType': <String, String>{
|
||||
'name': 'TextInputType.text',
|
||||
},
|
||||
'obscureText': false,
|
||||
};
|
||||
InputConfiguration(inputType: EngineInputType.text);
|
||||
final Map<String, dynamic> flutterSinglelineConfig =
|
||||
createFlutterConfig('text');
|
||||
|
||||
final InputConfiguration multilineConfig =
|
||||
InputConfiguration(inputType: InputType.multiline);
|
||||
final Map<String, dynamic> flutterMultilineConfig = <String, dynamic>{
|
||||
'inputType': <String, String>{
|
||||
'name': 'TextInputType.multiline',
|
||||
},
|
||||
'obscureText': false,
|
||||
};
|
||||
InputConfiguration(inputType: EngineInputType.multiline);
|
||||
final Map<String, dynamic> flutterMultilineConfig =
|
||||
createFlutterConfig('multiline');
|
||||
|
||||
void trackEditingState(EditingState editingState) {
|
||||
lastEditingState = editingState;
|
||||
@@ -315,6 +307,30 @@ void main() {
|
||||
HybridTextEditing textEditing;
|
||||
final PlatformMessagesSpy spy = PlatformMessagesSpy();
|
||||
|
||||
int clientId = 0;
|
||||
void showKeyboard({String inputType}) {
|
||||
final MethodCall setClient = MethodCall(
|
||||
'TextInput.setClient',
|
||||
<dynamic>[++clientId, createFlutterConfig(inputType)],
|
||||
);
|
||||
textEditing.handleTextInput(codec.encodeMethodCall(setClient));
|
||||
|
||||
const MethodCall show = MethodCall('TextInput.show');
|
||||
textEditing.handleTextInput(codec.encodeMethodCall(show));
|
||||
}
|
||||
|
||||
void hideKeyboard() {
|
||||
const MethodCall hide = MethodCall('TextInput.hide');
|
||||
textEditing.handleTextInput(codec.encodeMethodCall(hide));
|
||||
|
||||
const MethodCall clearClient = MethodCall('TextInput.clearClient');
|
||||
textEditing.handleTextInput(codec.encodeMethodCall(clearClient));
|
||||
}
|
||||
|
||||
String getEditingInputMode() {
|
||||
return textEditing.editingElement.domElement.getAttribute('inputmode');
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
textEditing = HybridTextEditing();
|
||||
spy.activate();
|
||||
@@ -422,6 +438,8 @@ void main() {
|
||||
|
||||
// Confirm that [HybridTextEditing] didn't send any messages.
|
||||
expect(spy.messages, isEmpty);
|
||||
|
||||
hideKeyboard();
|
||||
});
|
||||
|
||||
test('setClient, setEditingState, show, setEditingState, clearClient', () {
|
||||
@@ -546,11 +564,7 @@ void main() {
|
||||
expect(
|
||||
textEditing.editingElement.domElement.style.font, '12px sans-serif');
|
||||
|
||||
const MethodCall clearClient = MethodCall('TextInput.clearClient');
|
||||
textEditing.handleTextInput(codec.encodeMethodCall(clearClient));
|
||||
|
||||
// Confirm that [HybridTextEditing] didn't send any messages.
|
||||
expect(spy.messages, isEmpty);
|
||||
hideKeyboard();
|
||||
});
|
||||
|
||||
test(
|
||||
@@ -587,11 +601,7 @@ void main() {
|
||||
checkInputEditingState(
|
||||
textEditing.editingElement.domElement, 'xyz', 0, 0);
|
||||
|
||||
const MethodCall clearClient = MethodCall('TextInput.clearClient');
|
||||
textEditing.handleTextInput(codec.encodeMethodCall(clearClient));
|
||||
|
||||
// Confirm that [HybridTextEditing] didn't send any messages.
|
||||
expect(spy.messages, isEmpty);
|
||||
hideKeyboard();
|
||||
});
|
||||
|
||||
test('Syncs the editing state back to Flutter', () {
|
||||
@@ -650,8 +660,7 @@ void main() {
|
||||
],
|
||||
);
|
||||
|
||||
const MethodCall clearClient = MethodCall('TextInput.clearClient');
|
||||
textEditing.handleTextInput(codec.encodeMethodCall(clearClient));
|
||||
hideKeyboard();
|
||||
});
|
||||
|
||||
test('Multi-line mode also works', () {
|
||||
@@ -711,6 +720,52 @@ void main() {
|
||||
// Confirm that [HybridTextEditing] didn't send any more messages.
|
||||
expect(spy.messages, isEmpty);
|
||||
});
|
||||
|
||||
test('sets correct input type in Android', () {
|
||||
debugOperatingSystemOverride = OperatingSystem.android;
|
||||
|
||||
showKeyboard(inputType: 'text');
|
||||
expect(getEditingInputMode(), 'text');
|
||||
|
||||
showKeyboard(inputType: 'number');
|
||||
expect(getEditingInputMode(), 'numeric');
|
||||
|
||||
showKeyboard(inputType: 'phone');
|
||||
expect(getEditingInputMode(), 'tel');
|
||||
|
||||
showKeyboard(inputType: 'emailAddress');
|
||||
expect(getEditingInputMode(), 'email');
|
||||
|
||||
showKeyboard(inputType: 'url');
|
||||
expect(getEditingInputMode(), 'url');
|
||||
|
||||
hideKeyboard();
|
||||
|
||||
debugOperatingSystemOverride = null;
|
||||
});
|
||||
|
||||
test('sets correct input type in iOS', () {
|
||||
debugOperatingSystemOverride = OperatingSystem.iOs;
|
||||
|
||||
showKeyboard(inputType: 'text');
|
||||
expect(getEditingInputMode(), 'text');
|
||||
|
||||
showKeyboard(inputType: 'number');
|
||||
expect(getEditingInputMode(), 'numeric');
|
||||
|
||||
showKeyboard(inputType: 'phone');
|
||||
expect(getEditingInputMode(), 'tel');
|
||||
|
||||
showKeyboard(inputType: 'emailAddress');
|
||||
expect(getEditingInputMode(), 'email');
|
||||
|
||||
showKeyboard(inputType: 'url');
|
||||
expect(getEditingInputMode(), 'url');
|
||||
|
||||
hideKeyboard();
|
||||
|
||||
debugOperatingSystemOverride = null;
|
||||
});
|
||||
});
|
||||
|
||||
group('EditingState', () {
|
||||
@@ -850,3 +905,15 @@ class PlatformMessagesSpy {
|
||||
ui.window.onPlatformMessage = _backup;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> createFlutterConfig(
|
||||
String inputType, {
|
||||
bool obscureText = false,
|
||||
}) {
|
||||
return <String, dynamic>{
|
||||
'inputType': <String, String>{
|
||||
'name': 'TextInputType.$inputType',
|
||||
},
|
||||
'obscureText': obscureText,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user