diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 12d3b72cb894b..54317417ff696 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -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 diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index 947bf1c6aac16..0fc6c7635fa7d 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.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'; diff --git a/lib/web_ui/lib/src/engine/browser_detection.dart b/lib/web_ui/lib/src/engine/browser_detection.dart index fd1c8bfe19205..9cdc61edbd49c 100644 --- a/lib/web_ui/lib/src/engine/browser_detection.dart +++ b/lib/web_ui/lib/src/engine/browser_detection.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; 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 new file mode 100644 index 0000000000000..7df6b2cbb4151 --- /dev/null +++ b/lib/web_ui/lib/src/engine/text_editing/input_type.dart @@ -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: + /// . + 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(); +} diff --git a/lib/web_ui/lib/src/engine/text_editing.dart b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart similarity index 94% rename from lib/web_ui/lib/src/engine/text_editing.dart rename to lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 3491fcc7d4bc4..a024c08373ca7 100644 --- a/lib/web_ui/lib/src/engine/text_editing.dart +++ b/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -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 `` or @@ -201,12 +176,12 @@ class InputConfiguration { }); InputConfiguration.fromFlutter(Map 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. diff --git a/lib/web_ui/test/text_editing_test.dart b/lib/web_ui/test/text_editing_test.dart index 78c584189575e..09f1fc1d2fb87 100644 --- a/lib/web_ui/test/text_editing_test.dart +++ b/lib/web_ui/test/text_editing_test.dart @@ -18,22 +18,14 @@ TextEditingElement editingElement; EditingState lastEditingState; final InputConfiguration singlelineConfig = - InputConfiguration(inputType: InputType.text); -final Map flutterSinglelineConfig = { - 'inputType': { - 'name': 'TextInputType.text', - }, - 'obscureText': false, -}; + InputConfiguration(inputType: EngineInputType.text); +final Map flutterSinglelineConfig = + createFlutterConfig('text'); final InputConfiguration multilineConfig = - InputConfiguration(inputType: InputType.multiline); -final Map flutterMultilineConfig = { - 'inputType': { - 'name': 'TextInputType.multiline', - }, - 'obscureText': false, -}; + InputConfiguration(inputType: EngineInputType.multiline); +final Map 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', + [++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 createFlutterConfig( + String inputType, { + bool obscureText = false, +}) { + return { + 'inputType': { + 'name': 'TextInputType.$inputType', + }, + 'obscureText': obscureText, + }; +}