Skip to content

Commit

Permalink
using text capitalization value in web (flutter#19564)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
nturgut committed Jul 10, 2020
1 parent 3dc8163 commit b16c47d
Show file tree
Hide file tree
Showing 5 changed files with 281 additions and 60 deletions.
1 change: 1 addition & 0 deletions ci/licenses_golden/licenses_flutter
Expand Up @@ -521,6 +521,7 @@ 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/autofill_hint.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_capitalization.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
Expand Down
1 change: 1 addition & 0 deletions lib/web_ui/lib/src/engine.dart
Expand Up @@ -129,6 +129,7 @@ part 'engine/text/word_break_properties.dart';
part 'engine/text/word_breaker.dart';
part 'engine/text_editing/autofill_hint.dart';
part 'engine/text_editing/input_type.dart';
part 'engine/text_editing/text_capitalization.dart';
part 'engine/text_editing/text_editing.dart';
part 'engine/util.dart';
part 'engine/validators.dart';
Expand Down
91 changes: 91 additions & 0 deletions lib/web_ui/lib/src/engine/text_editing/text_capitalization.dart
@@ -0,0 +1,91 @@
// 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;

/// Controls the capitalization of the text.
///
/// This corresponds to Flutter's [TextCapitalization].
///
/// Uses `text-transform` css property.
/// See: https://developer.mozilla.org/en-US/docs/Web/CSS/text-transform
enum TextCapitalization {
/// Uppercase for the first letter of each word.
words,

/// Currently not implemented on Flutter Web. Uppercase for the first letter
/// of each sentence.
sentences,

/// Uppercase for each letter.
characters,

/// Lowercase for each letter.
none,
}

/// Helper class for text capitalization.
///
/// Uses `autocapitalize` attribute on input element.
/// See: https://developers.google.com/web/updates/2015/04/autocapitalize
/// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autocapitalize
class TextCapitalizationConfig {
final TextCapitalization textCapitalization;

const TextCapitalizationConfig.defaultCapitalization()
: textCapitalization = TextCapitalization.none;

TextCapitalizationConfig.fromInputConfiguration(String inputConfiguration)
: this.textCapitalization =
inputConfiguration == 'TextCapitalization.words'
? TextCapitalization.words
: inputConfiguration == 'TextCapitalization.characters'
? TextCapitalization.characters
: inputConfiguration == 'TextCapitalization.sentences'
? TextCapitalization.sentences
: TextCapitalization.none;

/// Sets `autocapitalize` attribute on input elements.
///
/// This attribute is only available for mobile browsers.
///
/// Note that in mobile browsers the onscreen keyboards provide sentence
/// level capitalization as default as apposed to no capitalization on desktop
/// browser.
///
/// See: https://developers.google.com/web/updates/2015/04/autocapitalize
/// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autocapitalize
void setAutocapitalizeAttribute(html.HtmlElement domElement) {
String autocapitalize = '';
switch (textCapitalization) {
case TextCapitalization.words:
// TODO: There is a bug for `words` level capitalization in IOS now.
// For now go back to default. Remove the check after bug is resolved.
// https://bugs.webkit.org/show_bug.cgi?id=148504
if (browserEngine == BrowserEngine.webkit) {
autocapitalize = 'sentences';
} else {
autocapitalize = 'words';
}
break;
case TextCapitalization.characters:
autocapitalize = 'characters';
break;
case TextCapitalization.sentences:
autocapitalize = 'sentences';
break;
case TextCapitalization.none:
default:
autocapitalize = 'off';
break;
}
if (domElement is html.InputElement) {
html.InputElement element = domElement;
element.setAttribute('autocapitalize', autocapitalize);
} else if (domElement is html.TextAreaElement) {
html.TextAreaElement element = domElement;
element.setAttribute('autocapitalize', autocapitalize);
}
}
}
101 changes: 71 additions & 30 deletions lib/web_ui/lib/src/engine/text_editing/text_editing.dart
Expand Up @@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.


part of engine;

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

// The focused text editing element will not be created here.
final AutofillInfo focusedElement =
Expand Down Expand Up @@ -170,16 +171,24 @@ class EngineAutofillForm {
keys.forEach((String key) {
final html.Element element = elements![key]!;
subscriptions.add(element.onInput.listen((html.Event e) {
_handleChange(element, key);
if (items![key] == null) {
throw StateError(
'Autofill would not work withuot Autofill value set');
} else {
final AutofillInfo autofillInfo = items![key] as AutofillInfo;
_handleChange(element, autofillInfo);
}
}));
});
return subscriptions;
}

void _handleChange(html.Element domElement, String? tag) {
EditingState newEditingState = EditingState.fromDomElement(domElement as html.HtmlElement?);
void _handleChange(html.Element domElement, AutofillInfo autofillInfo) {
EditingState newEditingState = EditingState.fromDomElement(
domElement as html.HtmlElement?,
textCapitalization: autofillInfo.textCapitalization);

_sendAutofillEditingState(tag, newEditingState);
_sendAutofillEditingState(autofillInfo.uniqueIdentifier, newEditingState);
}

/// Sends the 'TextInputClient.updateEditingStateWithTag' message to the framework.
Expand Down Expand Up @@ -207,7 +216,11 @@ class EngineAutofillForm {
/// These values are to be used when a text field have autofill enabled.
@visibleForTesting
class AutofillInfo {
AutofillInfo({required this.editingState, required this.uniqueIdentifier, required this.hint});
AutofillInfo(
{required this.editingState,
required this.uniqueIdentifier,
required this.hint,
required this.textCapitalization});

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

/// Information on how should autofilled text capitalized.
///
/// For example for [TextCapitalization.characters] each letter is converted
/// to upper case.
///
/// This value is not necessary for autofilling the focused element since
/// [DefaultTextEditingStrategy._inputConfiguration] already has this
/// information.
///
/// On the other hand for the multi element forms, for the input elements
/// other the focused field, we need to use this information.
final TextCapitalizationConfig textCapitalization;

/// Attribute used for autofill.
///
/// Used as a guidance to the browser as to the type of information expected
/// in the field.
/// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
final String hint;

factory AutofillInfo.fromFrameworkMessage(Map<String, dynamic> autofill) {
factory AutofillInfo.fromFrameworkMessage(Map<String, dynamic> autofill,
{TextCapitalizationConfig textCapitalization =
const TextCapitalizationConfig.defaultCapitalization()}) {
assert(autofill != null); // ignore: unnecessary_null_comparison
final String uniqueIdentifier = autofill['uniqueIdentifier']!;
final List<dynamic> hintsList = autofill['hints'];
Expand All @@ -233,7 +261,8 @@ class AutofillInfo {
return AutofillInfo(
uniqueIdentifier: uniqueIdentifier,
hint: BrowserAutofillHints.instance.flutterToEngine(hintsList[0]),
editingState: editingState);
editingState: editingState,
textCapitalization: textCapitalization);
}

void applyToDomElement(html.HtmlElement domElement,
Expand Down Expand Up @@ -302,7 +331,9 @@ class EditingState {
///
/// [domElement] can be a [InputElement] or a [TextAreaElement] depending on
/// the [InputType] of the text field.
factory EditingState.fromDomElement(html.HtmlElement? domElement) {
factory EditingState.fromDomElement(html.HtmlElement? domElement,
{TextCapitalizationConfig textCapitalization =
const TextCapitalizationConfig.defaultCapitalization()}) {
if (domElement is html.InputElement) {
html.InputElement element = domElement;
return EditingState(
Expand Down Expand Up @@ -352,10 +383,10 @@ class EditingState {
if (runtimeType != other.runtimeType) {
return false;
}
return other is EditingState
&& other.text == text
&& other.baseOffset == baseOffset
&& other.extentOffset == extentOffset;
return other is EditingState &&
other.text == text &&
other.baseOffset == baseOffset &&
other.extentOffset == extentOffset;
}

@override
Expand Down Expand Up @@ -396,6 +427,7 @@ class InputConfiguration {
required this.inputAction,
required this.obscureText,
required this.autocorrect,
required this.textCapitalization,
this.autofill,
this.autofillGroup,
});
Expand All @@ -407,9 +439,12 @@ class InputConfiguration {
inputAction = flutterInputConfiguration['inputAction'],
obscureText = flutterInputConfiguration['obscureText'],
autocorrect = flutterInputConfiguration['autocorrect'],
textCapitalization = TextCapitalizationConfig.fromInputConfiguration(
flutterInputConfiguration['textCapitalization']),
autofill = flutterInputConfiguration.containsKey('autofill')
? AutofillInfo.fromFrameworkMessage(flutterInputConfiguration['autofill'])
: null,
? AutofillInfo.fromFrameworkMessage(
flutterInputConfiguration['autofill'])
: null,
autofillGroup = EngineAutofillForm.fromFrameworkMessage(
flutterInputConfiguration['autofill'],
flutterInputConfiguration['fields']);
Expand All @@ -435,6 +470,8 @@ class InputConfiguration {
final AutofillInfo? autofill;

final EngineAutofillForm? autofillGroup;

final TextCapitalizationConfig textCapitalization;
}

typedef _OnChangeCallback = void Function(EditingState? editingState);
Expand Down Expand Up @@ -500,18 +537,18 @@ class GloballyPositionedTextEditingStrategy extends DefaultTextEditingStrategy {
void placeElement() {
super.placeElement();
if (hasAutofillGroup) {
_geometry?.applyToDomElement(focusedFormElement!);
placeForm();
// On Chrome, when a form is focused, it opens an autofill menu
// immeddiately.
// Flutter framework sends `setEditableSizeAndTransform` for informing
// the engine about the location of the text field. This call will
// arrive after `show` call.
// Therefore on Chrome we place the element when
// `setEditableSizeAndTransform` method is called and focus on the form
// only after placing it to the correct position. Hence autofill menu
// does not appear on top-left of the page.
focusedFormElement!.focus();
_geometry?.applyToDomElement(focusedFormElement!);
placeForm();
// On Chrome, when a form is focused, it opens an autofill menu
// immeddiately.
// Flutter framework sends `setEditableSizeAndTransform` for informing
// the engine about the location of the text field. This call will
// arrive after `show` call.
// Therefore on Chrome we place the element when
// `setEditableSizeAndTransform` method is called and focus on the form
// only after placing it to the correct position. Hence autofill menu
// does not appear on top-left of the page.
focusedFormElement!.focus();
} else {
_geometry?.applyToDomElement(domElement);
}
Expand Down Expand Up @@ -551,6 +588,7 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {
set domElement(html.HtmlElement element) {
_domElement = element;
}

html.HtmlElement? _domElement;

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

EditingState newEditingState = EditingState.fromDomElement(domElement);
EditingState newEditingState = EditingState.fromDomElement(domElement,
textCapitalization: _inputConfiguration.textCapitalization);

if (newEditingState != _lastEditingState) {
_lastEditingState = newEditingState;
Expand Down Expand Up @@ -818,6 +857,7 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
} else {
domRenderer.glassPaneElement!.append(domElement);
}
inputConfig.textCapitalization.setAutocapitalizeAttribute(domElement);
}

@override
Expand Down Expand Up @@ -948,6 +988,7 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
} else {
domRenderer.glassPaneElement!.append(domElement);
}
inputConfig.textCapitalization.setAutocapitalizeAttribute(domElement);
}

@override
Expand Down

0 comments on commit b16c47d

Please sign in to comment.