Unverified Commit b64d652f authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Add 2 new keyboard types and infer keyboardType from autofill hints (#56641)

parent 70a88c3b
......@@ -591,6 +591,7 @@ class CupertinoTextField extends StatefulWidget {
final GestureTapCallback onTap;
/// {@macro flutter.widgets.editableText.autofillHints}
/// {@macro flutter.services.autofill.autofillHints}
final Iterable<String> autofillHints;
@override
......
......@@ -728,6 +728,7 @@ class TextField extends StatefulWidget {
final ScrollController scrollController;
/// {@macro flutter.widgets.editableText.autofillHints}
/// {@macro flutter.services.autofill.autofillHints}
final Iterable<String> autofillHints;
/// Indicates whether [debugCheckHasMaterialLocalizations] can be called
......
......@@ -7,9 +7,9 @@ import 'text_input.dart';
/// A collection of commonly used autofill hint strings on different platforms.
///
/// Each hint may not be supported on every platform, and may get translated to
/// different strings on different platforms. Please refer to their documentation
/// for what each value corresponds to on different platforms.
/// Each hint is pre-defined on at least one supported platform. See their
/// documentation for their availability on each platform, and the platform
/// values each autofill hint corresponds to.
class AutofillHints {
AutofillHints._();
......@@ -350,7 +350,7 @@ class AutofillHints {
/// * Otherwise, the hint string will be used as-is.
static const String nickname = 'nickname';
/// The input field expects a single-factor SMS login code.
/// The input field expects a SMS one-time code.
///
/// This hint will be translated to the below values on different platforms:
///
......@@ -649,9 +649,9 @@ class AutofillConfiguration {
/// {@template flutter.services.autofill.autofillHints}
/// For the best results, hint strings need to be understood by the platform's
/// autofill service. The common values of hint strings can be found in
/// [AutofillHints], as well as the platforms that understand each of them.
/// [AutofillHints], as well as their availability on different platforms.
///
/// If an autofillable input field needs to use a custom hint that translate to
/// If an autofillable input field needs to use a custom hint that translates to
/// different strings on different platforms, the easiest way to achieve that
/// is to return different hint strings based on the value of
/// [defaultTargetPlatform].
......@@ -668,6 +668,12 @@ class AutofillConfiguration {
/// * On web, only the first hint is accounted for and will be translated to
/// an "autocomplete" string.
///
/// Providing an autofill hint that is predefined on the platform does not
/// automatically grant the input field eligibility for autofill. Ultimately,
/// it comes down to the autofill service currently in charge to determine
/// whether an input field is suitable for autofill and what the autofill
/// candidates are.
///
/// See also:
///
/// * [AutofillHints], a list of autofill hint strings that is predefined on at
......
......@@ -157,14 +157,33 @@ class TextInputType {
/// Requests a keyboard with ready access to both letters and numbers.
static const TextInputType visiblePassword = TextInputType._(7);
/// Optimized for a person's name.
///
/// On iOS, requests the
/// [UIKeyboardType.namePhonePad](https://developer.apple.com/documentation/uikit/uikeyboardtype/namephonepad)
/// keyboard, a keyboard optimized for entering a person’s name or phone number.
/// Does not support auto-capitalization.
///
/// On Android, requests a keyboard optimized for
/// [TYPE_TEXT_VARIATION_PERSON_NAME](https://developer.android.com/reference/android/text/InputType#TYPE_TEXT_VARIATION_PERSON_NAME).
static const TextInputType name = TextInputType._(8);
/// Optimized for postal mailing addresses.
///
/// On iOS, requests the default keyboard.
///
/// On Android, requests a keyboard optimized for
/// [TYPE_TEXT_VARIATION_POSTAL_ADDRESS](https://developer.android.com/reference/android/text/InputType#TYPE_TEXT_VARIATION_POSTAL_ADDRESS).
static const TextInputType streetAddress = TextInputType._(9);
/// All possible enum values.
static const List<TextInputType> values = <TextInputType>[
text, multiline, number, phone, datetime, emailAddress, url, visiblePassword,
text, multiline, number, phone, datetime, emailAddress, url, visiblePassword, name, streetAddress,
];
// Corresponding string name for each of the [values].
static const List<String> _names = <String>[
'text', 'multiline', 'number', 'phone', 'datetime', 'emailAddress', 'url', 'visiblePassword',
'text', 'multiline', 'number', 'phone', 'datetime', 'emailAddress', 'url', 'visiblePassword', 'name', 'address',
];
// Enum value name, this is what enum.toString() would normally return.
......
......@@ -338,9 +338,10 @@ class EditableText extends StatefulWidget {
/// the number of lines. By default, it is one, meaning this is a single-line
/// text field. [maxLines] must be null or greater than zero.
///
/// If [keyboardType] is not set or is null, it will default to
/// [TextInputType.text] unless [maxLines] is greater than one, when it will
/// default to [TextInputType.multiline].
/// If [keyboardType] is not set or is null, its value will be inferred from
/// [autofillHints], if [autofillHints] is not empty. Otherwise it defaults to
/// [TextInputType.text] if [maxLines] is exactly one, and
/// [TextInputType.multiline] if [maxLines] is null or greater than one.
///
/// The text cursor is not shown if [showCursor] is false or if [showCursor]
/// is null (the default) and [readOnly] is true.
......@@ -452,7 +453,7 @@ class EditableText extends StatefulWidget {
assert(dragStartBehavior != null),
assert(toolbarOptions != null),
_strutStyle = strutStyle,
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
keyboardType = keyboardType ?? _inferKeyboardType(autofillHints: autofillHints, maxLines: maxLines),
inputFormatters = maxLines == 1
? <TextInputFormatter>[
BlacklistingTextInputFormatter.singleLineFormatter,
......@@ -1120,10 +1121,162 @@ class EditableText extends StatefulWidget {
/// The minimum platform SDK version that supports Autofill is API level 26
/// for Android, and iOS 10.0 for iOS.
///
/// {@macro flutter.services.autofill.autofillHints}
/// ### iOS-specific Concerns:
///
/// To provide the best user experience and ensure your app fully supports
/// password autofill on iOS, follow these steps:
///
/// * Set up your iOS app's
/// [associated domains](https://developer.apple.com/documentation/safariservices/supporting_associated_domains_in_your_app).
/// * Some autofill hints only work with specific [keyboardType]s. For example,
/// [AutofillHints.name] requires [TextInputType.name] and [AutofillHints.email]
/// works only with [TextInputType.email]. Make sure the input field has a
/// compatible [keyboardType]. Empirically, [TextInputType.name] works well
/// with many autofill hints that are predefined on iOS.
/// {@endtemplate}
/// {@macro flutter.services.autofill.autofillHints}
final Iterable<String> autofillHints;
// Infer the keyboard type of an `EditableText` if it's not specified.
static TextInputType _inferKeyboardType({
@required Iterable<String> autofillHints,
@required int maxLines,
}) {
if (autofillHints?.isEmpty ?? true) {
return maxLines == 1 ? TextInputType.text : TextInputType.multiline;
}
TextInputType returnValue;
final String effectiveHint = autofillHints.first;
// On iOS oftentimes specifying a text content type is not enough to qualify
// the input field for autofill. The keyboard type also needs to be compatible
// with the content type. To get autofill to work by default on EditableText,
// the keyboard type inference on iOS is done differently from other platforms.
//
// The entries with "autofill not working" comments are the iOS text content
// types that should work with the specified keyboard type but won't trigger
// (even within a native app). Tested on iOS 13.5.
if (!kIsWeb) {
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
const Map<String, TextInputType> iOSKeyboardType = <String, TextInputType> {
AutofillHints.addressCity : TextInputType.name,
AutofillHints.addressCityAndState : TextInputType.name, // Autofill not working.
AutofillHints.addressState : TextInputType.name,
AutofillHints.countryName : TextInputType.name,
AutofillHints.creditCardNumber : TextInputType.number, // Couldn't test.
AutofillHints.email : TextInputType.emailAddress,
AutofillHints.familyName : TextInputType.name,
AutofillHints.fullStreetAddress : TextInputType.name,
AutofillHints.givenName : TextInputType.name,
AutofillHints.jobTitle : TextInputType.name, // Autofill not working.
AutofillHints.location : TextInputType.name, // Autofill not working.
AutofillHints.middleName : TextInputType.name, // Autofill not working.
AutofillHints.name : TextInputType.name,
AutofillHints.namePrefix : TextInputType.name, // Autofill not working.
AutofillHints.nameSuffix : TextInputType.name, // Autofill not working.
AutofillHints.newPassword : TextInputType.text,
AutofillHints.newUsername : TextInputType.text,
AutofillHints.nickname : TextInputType.name, // Autofill not working.
AutofillHints.oneTimeCode : TextInputType.number,
AutofillHints.organizationName : TextInputType.text, // Autofill not working.
AutofillHints.password : TextInputType.text,
AutofillHints.postalCode : TextInputType.name,
AutofillHints.streetAddressLine1 : TextInputType.name,
AutofillHints.streetAddressLine2 : TextInputType.name, // Autofill not working.
AutofillHints.sublocality : TextInputType.name, // Autofill not working.
AutofillHints.telephoneNumber : TextInputType.name,
AutofillHints.url : TextInputType.url, // Autofill not working.
AutofillHints.username : TextInputType.text,
};
returnValue = iOSKeyboardType[effectiveHint];
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
break;
}
}
if (returnValue != null || maxLines != 1)
return returnValue ?? TextInputType.multiline;
const Map<String, TextInputType> inferKeyboardType = <String, TextInputType> {
AutofillHints.addressCity : TextInputType.streetAddress,
AutofillHints.addressCityAndState : TextInputType.streetAddress,
AutofillHints.addressState : TextInputType.streetAddress,
AutofillHints.birthday : TextInputType.datetime,
AutofillHints.birthdayDay : TextInputType.datetime,
AutofillHints.birthdayMonth : TextInputType.datetime,
AutofillHints.birthdayYear : TextInputType.datetime,
AutofillHints.countryCode : TextInputType.number,
AutofillHints.countryName : TextInputType.text,
AutofillHints.creditCardExpirationDate : TextInputType.datetime,
AutofillHints.creditCardExpirationDay : TextInputType.datetime,
AutofillHints.creditCardExpirationMonth : TextInputType.datetime,
AutofillHints.creditCardExpirationYear : TextInputType.datetime,
AutofillHints.creditCardFamilyName : TextInputType.name,
AutofillHints.creditCardGivenName : TextInputType.name,
AutofillHints.creditCardMiddleName : TextInputType.name,
AutofillHints.creditCardName : TextInputType.name,
AutofillHints.creditCardNumber : TextInputType.number,
AutofillHints.creditCardSecurityCode : TextInputType.number,
AutofillHints.creditCardType : TextInputType.text,
AutofillHints.email : TextInputType.emailAddress,
AutofillHints.familyName : TextInputType.name,
AutofillHints.fullStreetAddress : TextInputType.streetAddress,
AutofillHints.gender : TextInputType.text,
AutofillHints.givenName : TextInputType.name,
AutofillHints.impp : TextInputType.url,
AutofillHints.jobTitle : TextInputType.text,
AutofillHints.language : TextInputType.text,
AutofillHints.location : TextInputType.streetAddress,
AutofillHints.middleInitial : TextInputType.name,
AutofillHints.middleName : TextInputType.name,
AutofillHints.name : TextInputType.name,
AutofillHints.namePrefix : TextInputType.name,
AutofillHints.nameSuffix : TextInputType.name,
AutofillHints.newPassword : TextInputType.text,
AutofillHints.newUsername : TextInputType.text,
AutofillHints.nickname : TextInputType.text,
AutofillHints.oneTimeCode : TextInputType.text,
AutofillHints.organizationName : TextInputType.text,
AutofillHints.password : TextInputType.text,
AutofillHints.photo : TextInputType.text,
AutofillHints.postalAddress : TextInputType.streetAddress,
AutofillHints.postalAddressExtended : TextInputType.streetAddress,
AutofillHints.postalAddressExtendedPostalCode : TextInputType.number,
AutofillHints.postalCode : TextInputType.number,
AutofillHints.streetAddressLevel1 : TextInputType.streetAddress,
AutofillHints.streetAddressLevel2 : TextInputType.streetAddress,
AutofillHints.streetAddressLevel3 : TextInputType.streetAddress,
AutofillHints.streetAddressLevel4 : TextInputType.streetAddress,
AutofillHints.streetAddressLine1 : TextInputType.streetAddress,
AutofillHints.streetAddressLine2 : TextInputType.streetAddress,
AutofillHints.streetAddressLine3 : TextInputType.streetAddress,
AutofillHints.sublocality : TextInputType.streetAddress,
AutofillHints.telephoneNumber : TextInputType.phone,
AutofillHints.telephoneNumberAreaCode : TextInputType.phone,
AutofillHints.telephoneNumberCountryCode : TextInputType.phone,
AutofillHints.telephoneNumberDevice : TextInputType.phone,
AutofillHints.telephoneNumberExtension : TextInputType.phone,
AutofillHints.telephoneNumberLocal : TextInputType.phone,
AutofillHints.telephoneNumberLocalPrefix : TextInputType.phone,
AutofillHints.telephoneNumberLocalSuffix : TextInputType.phone,
AutofillHints.telephoneNumberNational : TextInputType.phone,
AutofillHints.transactionAmount : TextInputType.numberWithOptions(decimal: true),
AutofillHints.transactionCurrency : TextInputType.text,
AutofillHints.url : TextInputType.url,
AutofillHints.username : TextInputType.text,
};
return inferKeyboardType[effectiveHint] ?? TextInputType.text;
}
@override
EditableTextState createState() => EditableTextState();
......
......@@ -267,6 +267,134 @@ void main() {
);
});
group('Infer keyboardType from autofillHints', () {
testWidgets('infer keyboard types from autofillHints: ios',
(WidgetTester tester) async {
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
autofillHints: const <String>[AutofillHints.streetAddressLine1],
),
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = 'test';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType']['name'], equals('TextInputType.name'));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('infer keyboard types from autofillHints: non-ios',
(WidgetTester tester) async {
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
autofillHints: const <String>[AutofillHints.streetAddressLine1],
),
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = 'test';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType']['name'], equals('TextInputType.address'));
});
testWidgets('inferred keyboard types can be overridden: ios',
(WidgetTester tester) async {
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
keyboardType: TextInputType.text,
autofillHints: const <String>[AutofillHints.streetAddressLine1],
),
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = 'test';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType']['name'], equals('TextInputType.text'));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('inferred keyboard types can be overridden: non-ios',
(WidgetTester tester) async {
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
keyboardType: TextInputType.text,
autofillHints: const <String>[AutofillHints.streetAddressLine1],
),
),
),
),
);
await tester.tap(find.byType(EditableText));
await tester.showKeyboard(find.byType(EditableText));
controller.text = 'test';
await tester.idle();
expect(tester.testTextInput.editingState['text'], equals('test'));
expect(tester.testTextInput.setClientArgs['inputType']['name'], equals('TextInputType.text'));
});
});
testWidgets('multiline keyboard is requested when set explicitly', (WidgetTester tester) async {
await tester.pumpWidget(
MediaQuery(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment