Support keyboard types on mobile browsers (#13044)

This commit is contained in:
Mouad Debbar
2019-10-14 13:10:01 -07:00
committed by GitHub
parent 139051fd58
commit c643366c19
6 changed files with 252 additions and 85 deletions
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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();
}
@@ -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.
+93 -26
View File
@@ -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,
};
}