Skip to content

Commit b16c47d

Browse files
author
nturgut
authored
using text capitalization value in web (flutter#19564)
* using text capitalization value in web engine * update editing state * add capitalization support to autofill fields * add autocapitalize attribute for mobile browsers which effects on screen keyboards * removing changes on the input value. only keeping onscreen keyboard changes * update unit tests. tests are added for ios-safari. android chrome is still not supported * changing license files this time don't update LICENSES file * Update licenses_flutter * addresing reviewer comments
1 parent 3dc8163 commit b16c47d

File tree

5 files changed

+281
-60
lines changed

5 files changed

+281
-60
lines changed

ci/licenses_golden/licenses_flutter

+1
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/word_break_properties.dart
521521
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/word_breaker.dart
522522
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text_editing/autofill_hint.dart
523523
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text_editing/input_type.dart
524+
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text_editing/text_capitalization.dart
524525
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart
525526
FILE: ../../../flutter/lib/web_ui/lib/src/engine/util.dart
526527
FILE: ../../../flutter/lib/web_ui/lib/src/engine/validators.dart

lib/web_ui/lib/src/engine.dart

+1
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ part 'engine/text/word_break_properties.dart';
129129
part 'engine/text/word_breaker.dart';
130130
part 'engine/text_editing/autofill_hint.dart';
131131
part 'engine/text_editing/input_type.dart';
132+
part 'engine/text_editing/text_capitalization.dart';
132133
part 'engine/text_editing/text_editing.dart';
133134
part 'engine/util.dart';
134135
part 'engine/validators.dart';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
/// Controls the capitalization of the text.
8+
///
9+
/// This corresponds to Flutter's [TextCapitalization].
10+
///
11+
/// Uses `text-transform` css property.
12+
/// See: https://developer.mozilla.org/en-US/docs/Web/CSS/text-transform
13+
enum TextCapitalization {
14+
/// Uppercase for the first letter of each word.
15+
words,
16+
17+
/// Currently not implemented on Flutter Web. Uppercase for the first letter
18+
/// of each sentence.
19+
sentences,
20+
21+
/// Uppercase for each letter.
22+
characters,
23+
24+
/// Lowercase for each letter.
25+
none,
26+
}
27+
28+
/// Helper class for text capitalization.
29+
///
30+
/// Uses `autocapitalize` attribute on input element.
31+
/// See: https://developers.google.com/web/updates/2015/04/autocapitalize
32+
/// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autocapitalize
33+
class TextCapitalizationConfig {
34+
final TextCapitalization textCapitalization;
35+
36+
const TextCapitalizationConfig.defaultCapitalization()
37+
: textCapitalization = TextCapitalization.none;
38+
39+
TextCapitalizationConfig.fromInputConfiguration(String inputConfiguration)
40+
: this.textCapitalization =
41+
inputConfiguration == 'TextCapitalization.words'
42+
? TextCapitalization.words
43+
: inputConfiguration == 'TextCapitalization.characters'
44+
? TextCapitalization.characters
45+
: inputConfiguration == 'TextCapitalization.sentences'
46+
? TextCapitalization.sentences
47+
: TextCapitalization.none;
48+
49+
/// Sets `autocapitalize` attribute on input elements.
50+
///
51+
/// This attribute is only available for mobile browsers.
52+
///
53+
/// Note that in mobile browsers the onscreen keyboards provide sentence
54+
/// level capitalization as default as apposed to no capitalization on desktop
55+
/// browser.
56+
///
57+
/// See: https://developers.google.com/web/updates/2015/04/autocapitalize
58+
/// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autocapitalize
59+
void setAutocapitalizeAttribute(html.HtmlElement domElement) {
60+
String autocapitalize = '';
61+
switch (textCapitalization) {
62+
case TextCapitalization.words:
63+
// TODO: There is a bug for `words` level capitalization in IOS now.
64+
// For now go back to default. Remove the check after bug is resolved.
65+
// https://bugs.webkit.org/show_bug.cgi?id=148504
66+
if (browserEngine == BrowserEngine.webkit) {
67+
autocapitalize = 'sentences';
68+
} else {
69+
autocapitalize = 'words';
70+
}
71+
break;
72+
case TextCapitalization.characters:
73+
autocapitalize = 'characters';
74+
break;
75+
case TextCapitalization.sentences:
76+
autocapitalize = 'sentences';
77+
break;
78+
case TextCapitalization.none:
79+
default:
80+
autocapitalize = 'off';
81+
break;
82+
}
83+
if (domElement is html.InputElement) {
84+
html.InputElement element = domElement;
85+
element.setAttribute('autocapitalize', autocapitalize);
86+
} else if (domElement is html.TextAreaElement) {
87+
html.TextAreaElement element = domElement;
88+
element.setAttribute('autocapitalize', autocapitalize);
89+
}
90+
}
91+
}

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

+71-30
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
65
part of engine;
76

87
/// Make the content editable span visible to facilitate debugging.
@@ -115,8 +114,10 @@ class EngineAutofillForm {
115114
if (fields != null) {
116115
for (Map<String, dynamic> field in fields.cast<Map<String, dynamic>>()) {
117116
final Map<String, dynamic> autofillInfo = field['autofill'];
118-
final AutofillInfo autofill =
119-
AutofillInfo.fromFrameworkMessage(autofillInfo);
117+
final AutofillInfo autofill = AutofillInfo.fromFrameworkMessage(
118+
autofillInfo,
119+
textCapitalization: TextCapitalizationConfig.fromInputConfiguration(
120+
field['textCapitalization']));
120121

121122
// The focused text editing element will not be created here.
122123
final AutofillInfo focusedElement =
@@ -170,16 +171,24 @@ class EngineAutofillForm {
170171
keys.forEach((String key) {
171172
final html.Element element = elements![key]!;
172173
subscriptions.add(element.onInput.listen((html.Event e) {
173-
_handleChange(element, key);
174+
if (items![key] == null) {
175+
throw StateError(
176+
'Autofill would not work withuot Autofill value set');
177+
} else {
178+
final AutofillInfo autofillInfo = items![key] as AutofillInfo;
179+
_handleChange(element, autofillInfo);
180+
}
174181
}));
175182
});
176183
return subscriptions;
177184
}
178185

179-
void _handleChange(html.Element domElement, String? tag) {
180-
EditingState newEditingState = EditingState.fromDomElement(domElement as html.HtmlElement?);
186+
void _handleChange(html.Element domElement, AutofillInfo autofillInfo) {
187+
EditingState newEditingState = EditingState.fromDomElement(
188+
domElement as html.HtmlElement?,
189+
textCapitalization: autofillInfo.textCapitalization);
181190

182-
_sendAutofillEditingState(tag, newEditingState);
191+
_sendAutofillEditingState(autofillInfo.uniqueIdentifier, newEditingState);
183192
}
184193

185194
/// Sends the 'TextInputClient.updateEditingStateWithTag' message to the framework.
@@ -207,7 +216,11 @@ class EngineAutofillForm {
207216
/// These values are to be used when a text field have autofill enabled.
208217
@visibleForTesting
209218
class AutofillInfo {
210-
AutofillInfo({required this.editingState, required this.uniqueIdentifier, required this.hint});
219+
AutofillInfo(
220+
{required this.editingState,
221+
required this.uniqueIdentifier,
222+
required this.hint,
223+
required this.textCapitalization});
211224

212225
/// The current text and selection state of a text field.
213226
final EditingState editingState;
@@ -217,14 +230,29 @@ class AutofillInfo {
217230
/// Used as id of the text field.
218231
final String uniqueIdentifier;
219232

233+
/// Information on how should autofilled text capitalized.
234+
///
235+
/// For example for [TextCapitalization.characters] each letter is converted
236+
/// to upper case.
237+
///
238+
/// This value is not necessary for autofilling the focused element since
239+
/// [DefaultTextEditingStrategy._inputConfiguration] already has this
240+
/// information.
241+
///
242+
/// On the other hand for the multi element forms, for the input elements
243+
/// other the focused field, we need to use this information.
244+
final TextCapitalizationConfig textCapitalization;
245+
220246
/// Attribute used for autofill.
221247
///
222248
/// Used as a guidance to the browser as to the type of information expected
223249
/// in the field.
224250
/// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
225251
final String hint;
226252

227-
factory AutofillInfo.fromFrameworkMessage(Map<String, dynamic> autofill) {
253+
factory AutofillInfo.fromFrameworkMessage(Map<String, dynamic> autofill,
254+
{TextCapitalizationConfig textCapitalization =
255+
const TextCapitalizationConfig.defaultCapitalization()}) {
228256
assert(autofill != null); // ignore: unnecessary_null_comparison
229257
final String uniqueIdentifier = autofill['uniqueIdentifier']!;
230258
final List<dynamic> hintsList = autofill['hints'];
@@ -233,7 +261,8 @@ class AutofillInfo {
233261
return AutofillInfo(
234262
uniqueIdentifier: uniqueIdentifier,
235263
hint: BrowserAutofillHints.instance.flutterToEngine(hintsList[0]),
236-
editingState: editingState);
264+
editingState: editingState,
265+
textCapitalization: textCapitalization);
237266
}
238267

239268
void applyToDomElement(html.HtmlElement domElement,
@@ -302,7 +331,9 @@ class EditingState {
302331
///
303332
/// [domElement] can be a [InputElement] or a [TextAreaElement] depending on
304333
/// the [InputType] of the text field.
305-
factory EditingState.fromDomElement(html.HtmlElement? domElement) {
334+
factory EditingState.fromDomElement(html.HtmlElement? domElement,
335+
{TextCapitalizationConfig textCapitalization =
336+
const TextCapitalizationConfig.defaultCapitalization()}) {
306337
if (domElement is html.InputElement) {
307338
html.InputElement element = domElement;
308339
return EditingState(
@@ -352,10 +383,10 @@ class EditingState {
352383
if (runtimeType != other.runtimeType) {
353384
return false;
354385
}
355-
return other is EditingState
356-
&& other.text == text
357-
&& other.baseOffset == baseOffset
358-
&& other.extentOffset == extentOffset;
386+
return other is EditingState &&
387+
other.text == text &&
388+
other.baseOffset == baseOffset &&
389+
other.extentOffset == extentOffset;
359390
}
360391

361392
@override
@@ -396,6 +427,7 @@ class InputConfiguration {
396427
required this.inputAction,
397428
required this.obscureText,
398429
required this.autocorrect,
430+
required this.textCapitalization,
399431
this.autofill,
400432
this.autofillGroup,
401433
});
@@ -407,9 +439,12 @@ class InputConfiguration {
407439
inputAction = flutterInputConfiguration['inputAction'],
408440
obscureText = flutterInputConfiguration['obscureText'],
409441
autocorrect = flutterInputConfiguration['autocorrect'],
442+
textCapitalization = TextCapitalizationConfig.fromInputConfiguration(
443+
flutterInputConfiguration['textCapitalization']),
410444
autofill = flutterInputConfiguration.containsKey('autofill')
411-
? AutofillInfo.fromFrameworkMessage(flutterInputConfiguration['autofill'])
412-
: null,
445+
? AutofillInfo.fromFrameworkMessage(
446+
flutterInputConfiguration['autofill'])
447+
: null,
413448
autofillGroup = EngineAutofillForm.fromFrameworkMessage(
414449
flutterInputConfiguration['autofill'],
415450
flutterInputConfiguration['fields']);
@@ -435,6 +470,8 @@ class InputConfiguration {
435470
final AutofillInfo? autofill;
436471

437472
final EngineAutofillForm? autofillGroup;
473+
474+
final TextCapitalizationConfig textCapitalization;
438475
}
439476

440477
typedef _OnChangeCallback = void Function(EditingState? editingState);
@@ -500,18 +537,18 @@ class GloballyPositionedTextEditingStrategy extends DefaultTextEditingStrategy {
500537
void placeElement() {
501538
super.placeElement();
502539
if (hasAutofillGroup) {
503-
_geometry?.applyToDomElement(focusedFormElement!);
504-
placeForm();
505-
// On Chrome, when a form is focused, it opens an autofill menu
506-
// immeddiately.
507-
// Flutter framework sends `setEditableSizeAndTransform` for informing
508-
// the engine about the location of the text field. This call will
509-
// arrive after `show` call.
510-
// Therefore on Chrome we place the element when
511-
// `setEditableSizeAndTransform` method is called and focus on the form
512-
// only after placing it to the correct position. Hence autofill menu
513-
// does not appear on top-left of the page.
514-
focusedFormElement!.focus();
540+
_geometry?.applyToDomElement(focusedFormElement!);
541+
placeForm();
542+
// On Chrome, when a form is focused, it opens an autofill menu
543+
// immeddiately.
544+
// Flutter framework sends `setEditableSizeAndTransform` for informing
545+
// the engine about the location of the text field. This call will
546+
// arrive after `show` call.
547+
// Therefore on Chrome we place the element when
548+
// `setEditableSizeAndTransform` method is called and focus on the form
549+
// only after placing it to the correct position. Hence autofill menu
550+
// does not appear on top-left of the page.
551+
focusedFormElement!.focus();
515552
} else {
516553
_geometry?.applyToDomElement(domElement);
517554
}
@@ -551,6 +588,7 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {
551588
set domElement(html.HtmlElement element) {
552589
_domElement = element;
553590
}
591+
554592
html.HtmlElement? _domElement;
555593

556594
late InputConfiguration _inputConfiguration;
@@ -694,7 +732,8 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {
694732
void _handleChange(html.Event event) {
695733
assert(isEnabled);
696734

697-
EditingState newEditingState = EditingState.fromDomElement(domElement);
735+
EditingState newEditingState = EditingState.fromDomElement(domElement,
736+
textCapitalization: _inputConfiguration.textCapitalization);
698737

699738
if (newEditingState != _lastEditingState) {
700739
_lastEditingState = newEditingState;
@@ -818,6 +857,7 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
818857
} else {
819858
domRenderer.glassPaneElement!.append(domElement);
820859
}
860+
inputConfig.textCapitalization.setAutocapitalizeAttribute(domElement);
821861
}
822862

823863
@override
@@ -948,6 +988,7 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
948988
} else {
949989
domRenderer.glassPaneElement!.append(domElement);
950990
}
991+
inputConfig.textCapitalization.setAutocapitalizeAttribute(domElement);
951992
}
952993

953994
@override

0 commit comments

Comments
 (0)