未验证 提交 b16c47d5 编写于 作者: N nturgut 提交者: GitHub

using text capitalization value in web (#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
上级 3dc81635
......@@ -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
......
......@@ -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';
......
// 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);
}
}
}
......@@ -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.
......@@ -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 =
......@@ -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.
......@@ -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;
......@@ -217,6 +230,19 @@ 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
......@@ -224,7 +250,9 @@ class AutofillInfo {
/// 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'];
......@@ -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,
......@@ -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(
......@@ -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
......@@ -396,6 +427,7 @@ class InputConfiguration {
required this.inputAction,
required this.obscureText,
required this.autocorrect,
required this.textCapitalization,
this.autofill,
this.autofillGroup,
});
......@@ -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']);
......@@ -435,6 +470,8 @@ class InputConfiguration {
final AutofillInfo? autofill;
final EngineAutofillForm? autofillGroup;
final TextCapitalizationConfig textCapitalization;
}
typedef _OnChangeCallback = void Function(EditingState? editingState);
......@@ -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);
}
......@@ -551,6 +588,7 @@ abstract class DefaultTextEditingStrategy implements TextEditingStrategy {
set domElement(html.HtmlElement element) {
_domElement = element;
}
html.HtmlElement? _domElement;
late InputConfiguration _inputConfiguration;
......@@ -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;
......@@ -818,6 +857,7 @@ class IOSTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
} else {
domRenderer.glassPaneElement!.append(domElement);
}
inputConfig.textCapitalization.setAutocapitalizeAttribute(domElement);
}
@override
......@@ -948,6 +988,7 @@ class AndroidTextEditingStrategy extends GloballyPositionedTextEditingStrategy {
} else {
domRenderer.glassPaneElement!.append(domElement);
}
inputConfig.textCapitalization.setAutocapitalizeAttribute(domElement);
}
@override
......
......@@ -31,16 +31,19 @@ final InputConfiguration singlelineConfig = InputConfiguration(
obscureText: false,
inputAction: 'TextInputAction.done',
autocorrect: true,
textCapitalization: TextCapitalizationConfig.fromInputConfiguration(
'TextCapitalization.none'),
);
final Map<String, dynamic> flutterSinglelineConfig =
createFlutterConfig('text');
final InputConfiguration multilineConfig = InputConfiguration(
inputType: EngineInputType.multiline,
obscureText: false,
inputAction: 'TextInputAction.newline',
autocorrect: true,
);
inputType: EngineInputType.multiline,
obscureText: false,
inputAction: 'TextInputAction.newline',
autocorrect: true,
textCapitalization: TextCapitalizationConfig.fromInputConfiguration(
'TextCapitalization.none'));
final Map<String, dynamic> flutterMultilineConfig =
createFlutterConfig('multiline');
......@@ -108,11 +111,12 @@ void main() {
test('Knows how to create password fields', () {
final InputConfiguration config = InputConfiguration(
inputType: EngineInputType.text,
inputAction: 'TextInputAction.done',
obscureText: true,
autocorrect: true,
);
inputType: EngineInputType.text,
inputAction: 'TextInputAction.done',
obscureText: true,
autocorrect: true,
textCapitalization: TextCapitalizationConfig.fromInputConfiguration(
'TextCapitalization.none'));
editingElement.enable(
config,
onChange: trackEditingState,
......@@ -128,11 +132,12 @@ void main() {
test('Knows to turn autocorrect off', () {
final InputConfiguration config = InputConfiguration(
inputType: EngineInputType.text,
inputAction: 'TextInputAction.done',
obscureText: false,
autocorrect: false,
);
inputType: EngineInputType.text,
inputAction: 'TextInputAction.done',
obscureText: false,
autocorrect: false,
textCapitalization: TextCapitalizationConfig.fromInputConfiguration(
'TextCapitalization.none'));
editingElement.enable(
config,
onChange: trackEditingState,
......@@ -148,11 +153,12 @@ void main() {
test('Knows to turn autocorrect on', () {
final InputConfiguration config = InputConfiguration(
inputType: EngineInputType.text,
inputAction: 'TextInputAction.done',
obscureText: false,
autocorrect: true,
);
inputType: EngineInputType.text,
inputAction: 'TextInputAction.done',
obscureText: false,
autocorrect: true,
textCapitalization: TextCapitalizationConfig.fromInputConfiguration(
'TextCapitalization.none'));
editingElement.enable(
config,
onChange: trackEditingState,
......@@ -287,11 +293,12 @@ void main() {
test('Triggers input action', () {
final InputConfiguration config = InputConfiguration(
inputType: EngineInputType.text,
obscureText: false,
inputAction: 'TextInputAction.done',
autocorrect: true,
);
inputType: EngineInputType.text,
obscureText: false,
inputAction: 'TextInputAction.done',
autocorrect: true,
textCapitalization: TextCapitalizationConfig.fromInputConfiguration(
'TextCapitalization.none'));
editingElement.enable(
config,
onChange: trackEditingState,
......@@ -313,11 +320,12 @@ void main() {
test('Does not trigger input action in multi-line mode', () {
final InputConfiguration config = InputConfiguration(
inputType: EngineInputType.multiline,
obscureText: false,
inputAction: 'TextInputAction.done',
autocorrect: true,
);
inputType: EngineInputType.multiline,
obscureText: false,
inputAction: 'TextInputAction.done',
autocorrect: true,
textCapitalization: TextCapitalizationConfig.fromInputConfiguration(
'TextCapitalization.none'));
editingElement.enable(
config,
onChange: trackEditingState,
......@@ -859,6 +867,82 @@ void main() {
expect(document.getElementsByTagName('form'), isEmpty);
});
test(
'No capitilization: setClient, setEditingState, show', () {
// Create a configuration with an AutofillGroup of four text fields.
final Map<String, dynamic> capitilizeWordsConfig = createFlutterConfig(
'text',
textCapitalization: 'TextCapitalization.none');
final MethodCall setClient = MethodCall(
'TextInput.setClient', <dynamic>[123, capitilizeWordsConfig]);
sendFrameworkMessage(codec.encodeMethodCall(setClient));
const MethodCall setEditingState1 =
MethodCall('TextInput.setEditingState', <String, dynamic>{
'text': '',
'selectionBase': 0,
'selectionExtent': 0,
});
sendFrameworkMessage(codec.encodeMethodCall(setEditingState1));
const MethodCall show = MethodCall('TextInput.show');
sendFrameworkMessage(codec.encodeMethodCall(show));
spy.messages.clear();
// Test for mobile Safari. `sentences` is the default attribute for
// mobile browsers. Check if `off` is added to the input element.
if (browserEngine == BrowserEngine.webkit &&
operatingSystem == OperatingSystem.iOs) {
expect(
textEditing.editingElement.domElement
.getAttribute('autocapitalize'),
'off');
} else {
expect(
textEditing.editingElement.domElement
.getAttribute('autocapitalize'),
isNull);
}
spy.messages.clear();
hideKeyboard();
});
test(
'All characters capitilization: setClient, setEditingState, show', () {
// Create a configuration with an AutofillGroup of four text fields.
final Map<String, dynamic> capitilizeWordsConfig = createFlutterConfig(
'text',
textCapitalization: 'TextCapitalization.characters');
final MethodCall setClient = MethodCall(
'TextInput.setClient', <dynamic>[123, capitilizeWordsConfig]);
sendFrameworkMessage(codec.encodeMethodCall(setClient));
const MethodCall setEditingState1 =
MethodCall('TextInput.setEditingState', <String, dynamic>{
'text': '',
'selectionBase': 0,
'selectionExtent': 0,
});
sendFrameworkMessage(codec.encodeMethodCall(setEditingState1));
const MethodCall show = MethodCall('TextInput.show');
sendFrameworkMessage(codec.encodeMethodCall(show));
spy.messages.clear();
// Test for mobile Safari.
if (browserEngine == BrowserEngine.webkit &&
operatingSystem == OperatingSystem.iOs) {
expect(
textEditing.editingElement.domElement
.getAttribute('autocapitalize'),
'characters');
}
spy.messages.clear();
hideKeyboard();
});
test(
'setClient, setEditableSizeAndTransform, setStyle, setEditingState, show, clearClient',
() {
......@@ -1710,6 +1794,7 @@ Map<String, dynamic> createFlutterConfig(
String inputType, {
bool obscureText = false,
bool autocorrect = true,
String textCapitalization = 'TextCapitalization.none',
String inputAction,
String autofillHint,
List<String> autofillHintsForFields,
......@@ -1721,6 +1806,7 @@ Map<String, dynamic> createFlutterConfig(
'obscureText': obscureText,
'autocorrect': autocorrect,
'inputAction': inputAction ?? 'TextInputAction.done',
'textCapitalization': textCapitalization,
if (autofillHint != null)
'autofill': createAutofillInfo(autofillHint, autofillHint),
if (autofillHintsForFields != null)
......@@ -1763,5 +1849,6 @@ Map<String, dynamic> createOneFieldValue(String hint, String uniqueId) =>
'signed': null,
'decimal': null
},
'textCapitalization': 'TextCapitalization.none',
'autofill': createAutofillInfo(hint, uniqueId)
};
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册