Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit d935db3

Browse files
committed
Support keyboard types on mobile browsers
1 parent b3cbc03 commit d935db3

File tree

5 files changed

+216
-72
lines changed

5 files changed

+216
-72
lines changed

lib/web_ui/lib/src/engine.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ part 'engine/text/ruler.dart';
9292
part 'engine/text/unicode_range.dart';
9393
part 'engine/text/word_break_properties.dart';
9494
part 'engine/text/word_breaker.dart';
95-
part 'engine/text_editing.dart';
95+
part 'engine/text_editing/input_type.dart';
96+
part 'engine/text_editing/text_editing.dart';
9697
part 'engine/util.dart';
9798
part 'engine/validators.dart';
9899
part 'engine/vector_math.dart';

lib/web_ui/lib/src/engine/browser_detection.dart

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,19 +77,35 @@ OperatingSystem _operatingSystem;
7777
///
7878
/// This is used to implement operating system specific behavior such as
7979
/// soft keyboards.
80-
OperatingSystem get operatingSystem =>
81-
_operatingSystem ??= _detectOperatingSystem();
80+
OperatingSystem get operatingSystem {
81+
if (debugOperatingSystemOverride != null) {
82+
return debugOperatingSystemOverride;
83+
}
84+
return _operatingSystem ??= _detectOperatingSystem();
85+
}
86+
87+
/// Override the value of [operatingSystem].
88+
///
89+
/// Setting this to `null` lets [operatingSystem] detect the real OS that the
90+
/// app is running on.
91+
///
92+
/// This is intended to be used for testing and debugging only.
93+
OperatingSystem debugOperatingSystemOverride;
8294

8395
OperatingSystem _detectOperatingSystem() {
8496
final String platform = html.window.navigator.platform;
97+
final String userAgent = html.window.navigator.userAgent;
8598

8699
if (platform.startsWith('Mac')) {
87100
return OperatingSystem.macOs;
88101
} else if (platform.toLowerCase().contains('iphone') ||
89102
platform.toLowerCase().contains('ipad') ||
90103
platform.toLowerCase().contains('ipod')) {
91104
return OperatingSystem.iOs;
92-
} else if (platform.toLowerCase().contains('android')) {
105+
} else if (userAgent.contains('Android')) {
106+
// The Android OS reports itself as "Linux armv8l" in
107+
// [html.window.navigator.platform]. So we have to check the user-agent to
108+
// determine if the OS is Android or not.
93109
return OperatingSystem.android;
94110
} else if (platform.startsWith('Linux')) {
95111
return OperatingSystem.linux;
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
part of engine;
6+
7+
/// Various types of inputs used in text fields.
8+
///
9+
/// These types are coming from Flutter's [TextInputType]. Currently, we don't
10+
/// support all the types. We fallback to [EngineInputType.text] when Flutter
11+
/// sends a type that isn't supported.
12+
// TODO(flutter_web): Support more types.
13+
abstract class EngineInputType {
14+
const EngineInputType();
15+
16+
static EngineInputType fromName(String name) {
17+
switch (name) {
18+
case 'TextInputType.number':
19+
return number;
20+
case 'TextInputType.phone':
21+
return phone;
22+
case 'TextInputType.emailAddress':
23+
return emailAddress;
24+
case 'TextInputType.url':
25+
return url;
26+
case 'TextInputType.multiline':
27+
return multiline;
28+
29+
case 'TextInputType.text':
30+
default:
31+
return text;
32+
}
33+
}
34+
35+
/// Single-line text input type.
36+
static const TextInputType text = TextInputType();
37+
38+
/// Numeric input type.
39+
static const NumberInputType number = NumberInputType();
40+
41+
/// Phone number input type.
42+
static const PhoneInputType phone = PhoneInputType();
43+
44+
/// Email address input type.
45+
static const EmailInputType emailAddress = EmailInputType();
46+
47+
/// URL input type.
48+
static const UrlInputType url = UrlInputType();
49+
50+
/// Multi-line text input type.
51+
static const MultilineInputType multiline = MultilineInputType();
52+
53+
/// The HTML `inputmode` attribute to be set on the DOM element.
54+
///
55+
/// This HTML attribute helps the browser decide what kind of keyboard works
56+
/// best for this text field.
57+
///
58+
/// For various `inputmode` values supported by browsers, see:
59+
/// <https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inputmode>.
60+
String get inputmodeAttribute;
61+
62+
/// Create the appropriate DOM element for this input type.
63+
html.HtmlElement createDomElement() => html.InputElement();
64+
65+
/// Given a [domElement], set attributes that are specific to this input type.
66+
void configureDomElement(html.HtmlElement domElement) {
67+
if (inputmodeAttribute == null) {
68+
return;
69+
}
70+
71+
// Only apply `inputmode` in mobile browsers so that the right virtual
72+
// keyboard shows up.
73+
if (operatingSystem == OperatingSystem.iOs ||
74+
operatingSystem == OperatingSystem.android) {
75+
domElement.setAttribute('inputmode', inputmodeAttribute);
76+
}
77+
}
78+
}
79+
80+
/// Single-line text input type.
81+
class TextInputType extends EngineInputType {
82+
const TextInputType();
83+
84+
@override
85+
final String inputmodeAttribute = 'text';
86+
}
87+
88+
/// Numeric input type.
89+
class NumberInputType extends EngineInputType {
90+
const NumberInputType();
91+
92+
@override
93+
final String inputmodeAttribute = 'numeric';
94+
}
95+
96+
/// Phone number input type.
97+
class PhoneInputType extends EngineInputType {
98+
const PhoneInputType();
99+
100+
@override
101+
final String inputmodeAttribute = 'tel';
102+
}
103+
104+
/// Email address input type.
105+
class EmailInputType extends EngineInputType {
106+
const EmailInputType();
107+
108+
@override
109+
final String inputmodeAttribute = 'email';
110+
}
111+
112+
/// URL input type.
113+
class UrlInputType extends EngineInputType {
114+
const UrlInputType();
115+
116+
@override
117+
final String inputmodeAttribute = 'url';
118+
}
119+
120+
/// Multi-line text input type.
121+
class MultilineInputType extends EngineInputType {
122+
const MultilineInputType();
123+
124+
@override
125+
final String inputmodeAttribute = null;
126+
127+
@override
128+
html.HtmlElement createDomElement() => html.TextAreaElement();
129+
}

lib/web_ui/lib/src/engine/text_editing.dart renamed to lib/web_ui/lib/src/engine/text_editing/text_editing.dart

Lines changed: 7 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -163,31 +163,6 @@ class EditingState {
163163
}
164164
}
165165

166-
/// Various types of inputs used in text fields.
167-
///
168-
/// These types are coming from Flutter's [TextInputType]. Currently, we don't
169-
/// support all the types. We fallback to [InputType.text] when Flutter sends
170-
/// a type that isn't supported.
171-
// TODO(flutter_web): Support more types.
172-
enum InputType {
173-
/// Single-line plain text.
174-
text,
175-
176-
/// Multi-line text.
177-
multiline,
178-
}
179-
180-
InputType _getInputTypeFromString(String inputType) {
181-
switch (inputType) {
182-
case 'TextInputType.multiline':
183-
return InputType.multiline;
184-
185-
case 'TextInputType.text':
186-
default:
187-
return InputType.text;
188-
}
189-
}
190-
191166
/// Controls the appearance of the input control being edited.
192167
///
193168
/// For example, [inputType] determines whether we should use `<input>` or
@@ -201,12 +176,12 @@ class InputConfiguration {
201176
});
202177

203178
InputConfiguration.fromFlutter(Map<String, dynamic> flutterInputConfiguration)
204-
: inputType = _getInputTypeFromString(
179+
: inputType = EngineInputType.fromName(
205180
flutterInputConfiguration['inputType']['name']),
206181
obscureText = flutterInputConfiguration['obscureText'];
207182

208183
/// The type of information being edited in the input control.
209-
final InputType inputType;
184+
final EngineInputType inputType;
210185

211186
/// Whether to hide the text being edited.
212187
final bool obscureText;
@@ -341,6 +316,7 @@ class TextEditingElement {
341316
_subscriptions.add(domElement.onKeyUp.listen((event) {
342317
_handleChange(event);
343318
}));
319+
344320
/// In Firefox the context menu item "Select All" does not work without
345321
/// listening to onSelect. On the other browsers onSelectionChange is
346322
/// enough for covering "Select All" functionality.
@@ -370,19 +346,10 @@ class TextEditingElement {
370346
}
371347

372348
void _initDomElement(InputConfiguration inputConfig) {
373-
switch (inputConfig.inputType) {
374-
case InputType.text:
375-
domElement = owner.createInputElement();
376-
break;
377-
378-
case InputType.multiline:
379-
domElement = owner.createTextAreaElement();
380-
break;
381-
382-
default:
383-
throw UnsupportedError(
384-
'Unsupported input type: ${inputConfig.inputType}');
385-
}
349+
domElement = inputConfig.inputType.createDomElement();
350+
inputConfig.inputType.configureDomElement(domElement);
351+
_setStaticStyleAttributes(domElement);
352+
owner._setDynamicStyleAttributes(domElement);
386353
domRenderer.glassPaneElement.append(domElement);
387354
}
388355

@@ -757,20 +724,6 @@ class HybridTextEditing {
757724
void setStyleOutsideOfScreen(html.HtmlElement domElement) {
758725
domElement.style.transform = 'translate(-9999px, -9999px)';
759726
}
760-
761-
html.InputElement createInputElement() {
762-
final html.InputElement input = html.InputElement();
763-
_setStaticStyleAttributes(input);
764-
_setDynamicStyleAttributes(input);
765-
return input;
766-
}
767-
768-
html.TextAreaElement createTextAreaElement() {
769-
final html.TextAreaElement textarea = html.TextAreaElement();
770-
_setStaticStyleAttributes(textarea);
771-
_setDynamicStyleAttributes(textarea);
772-
return textarea;
773-
}
774727
}
775728

776729
/// Information on the font and alignment of a text editing element.

lib/web_ui/test/text_editing_test.dart

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,14 @@ TextEditingElement editingElement;
1818
EditingState lastEditingState;
1919

2020
final InputConfiguration singlelineConfig =
21-
InputConfiguration(inputType: InputType.text);
22-
final Map<String, dynamic> flutterSinglelineConfig = <String, dynamic>{
23-
'inputType': <String, String>{
24-
'name': 'TextInputType.text',
25-
},
26-
'obscureText': false,
27-
};
21+
InputConfiguration(inputType: EngineInputType.text);
22+
final Map<String, dynamic> flutterSinglelineConfig =
23+
createFlutterConfig('text');
2824

2925
final InputConfiguration multilineConfig =
30-
InputConfiguration(inputType: InputType.multiline);
31-
final Map<String, dynamic> flutterMultilineConfig = <String, dynamic>{
32-
'inputType': <String, String>{
33-
'name': 'TextInputType.multiline',
34-
},
35-
'obscureText': false,
36-
};
26+
InputConfiguration(inputType: EngineInputType.multiline);
27+
final Map<String, dynamic> flutterMultilineConfig =
28+
createFlutterConfig('multiline');
3729

3830
void trackEditingState(EditingState editingState) {
3931
lastEditingState = editingState;
@@ -711,6 +703,47 @@ void main() {
711703
// Confirm that [HybridTextEditing] didn't send any more messages.
712704
expect(spy.messages, isEmpty);
713705
});
706+
707+
test('sets correct input type', () {
708+
// Maps flutter input types to DOM inputmode values.
709+
const Map<String, String> inputTypesMap = <String, String>{
710+
'text': 'text',
711+
'number': 'numeric',
712+
'phone': 'tel',
713+
'emailAddress': 'email',
714+
'url': 'url',
715+
};
716+
int clientId = 0;
717+
718+
void testAllTypes() {
719+
for (String flutterInputType in inputTypesMap.keys) {
720+
final String domInputMode = inputTypesMap[flutterInputType];
721+
722+
final MethodCall setClient = MethodCall(
723+
'TextInput.setClient',
724+
<dynamic>[++clientId, createFlutterConfig(flutterInputType)],
725+
);
726+
textEditing.handleTextInput(codec.encodeMethodCall(setClient));
727+
728+
const MethodCall show = MethodCall('TextInput.show');
729+
textEditing.handleTextInput(codec.encodeMethodCall(show));
730+
731+
expect(
732+
textEditing.editingElement.domElement.getAttribute('inputmode'),
733+
domInputMode,
734+
);
735+
}
736+
}
737+
738+
debugOperatingSystemOverride = OperatingSystem.android;
739+
testAllTypes();
740+
debugOperatingSystemOverride = OperatingSystem.iOs;
741+
testAllTypes();
742+
debugOperatingSystemOverride = null;
743+
744+
const MethodCall hide = MethodCall('TextInput.hide');
745+
textEditing.handleTextInput(codec.encodeMethodCall(hide));
746+
});
714747
});
715748

716749
group('EditingState', () {
@@ -850,3 +883,15 @@ class PlatformMessagesSpy {
850883
ui.window.onPlatformMessage = _backup;
851884
}
852885
}
886+
887+
Map<String, dynamic> createFlutterConfig(
888+
String inputType, {
889+
bool obscureText = false,
890+
}) {
891+
return <String, dynamic>{
892+
'inputType': <String, String>{
893+
'name': 'TextInputType.$inputType',
894+
},
895+
'obscureText': obscureText,
896+
};
897+
}

0 commit comments

Comments
 (0)