mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Add 2 new keyboard types and infer keyboardType from autofill hints (#56641)
This commit is contained in:
parent
70a88c3b83
commit
b64d652fd1
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user