Unverified Commit 33755f20 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

[autofill] opt-out instead of opt-in (#86312)

parent 81142c1f
...@@ -293,7 +293,7 @@ class CupertinoTextField extends StatefulWidget { ...@@ -293,7 +293,7 @@ class CupertinoTextField extends StatefulWidget {
this.onTap, this.onTap,
this.scrollController, this.scrollController,
this.scrollPhysics, this.scrollPhysics,
this.autofillHints, this.autofillHints = const <String>[],
this.restorationId, this.restorationId,
this.enableIMEPersonalizedLearning = true, this.enableIMEPersonalizedLearning = true,
}) : assert(textAlign != null), }) : assert(textAlign != null),
...@@ -449,7 +449,7 @@ class CupertinoTextField extends StatefulWidget { ...@@ -449,7 +449,7 @@ class CupertinoTextField extends StatefulWidget {
this.onTap, this.onTap,
this.scrollController, this.scrollController,
this.scrollPhysics, this.scrollPhysics,
this.autofillHints, this.autofillHints = const <String>[],
this.restorationId, this.restorationId,
this.enableIMEPersonalizedLearning = true, this.enableIMEPersonalizedLearning = true,
}) : assert(textAlign != null), }) : assert(textAlign != null),
...@@ -837,7 +837,7 @@ class CupertinoTextField extends StatefulWidget { ...@@ -837,7 +837,7 @@ class CupertinoTextField extends StatefulWidget {
} }
} }
class _CupertinoTextFieldState extends State<CupertinoTextField> with RestorationMixin, AutomaticKeepAliveClientMixin<CupertinoTextField> implements TextSelectionGestureDetectorBuilderDelegate { class _CupertinoTextFieldState extends State<CupertinoTextField> with RestorationMixin, AutomaticKeepAliveClientMixin<CupertinoTextField> implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient {
final GlobalKey _clearGlobalKey = GlobalKey(); final GlobalKey _clearGlobalKey = GlobalKey();
RestorableTextEditingController? _controller; RestorableTextEditingController? _controller;
...@@ -1098,6 +1098,28 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio ...@@ -1098,6 +1098,28 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
}, },
); );
} }
// AutofillClient implementation start.
@override
String get autofillId => _editableText.autofillId;
@override
void autofill(TextEditingValue newEditingValue) => _editableText.autofill(newEditingValue);
@override
TextInputConfiguration get textInputConfiguration {
final List<String>? autofillHints = widget.autofillHints?.toList(growable: false);
final AutofillConfiguration autofillConfiguration = autofillHints != null
? AutofillConfiguration(
uniqueIdentifier: autofillId,
autofillHints: autofillHints,
currentEditingValue: _effectiveController.value,
hintText: widget.placeholder,
)
: AutofillConfiguration.disabled;
return _editableText.textInputConfiguration.copyWith(autofillConfiguration: autofillConfiguration);
}
// AutofillClient implementation end.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
...@@ -1242,7 +1264,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio ...@@ -1242,7 +1264,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
scrollController: widget.scrollController, scrollController: widget.scrollController,
scrollPhysics: widget.scrollPhysics, scrollPhysics: widget.scrollPhysics,
enableInteractiveSelection: widget.enableInteractiveSelection, enableInteractiveSelection: widget.enableInteractiveSelection,
autofillHints: widget.autofillHints, autofillClient: this,
restorationId: 'editable', restorationId: 'editable',
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
), ),
......
...@@ -692,6 +692,7 @@ class _SelectableTextState extends State<SelectableText> with AutomaticKeepAlive ...@@ -692,6 +692,7 @@ class _SelectableTextState extends State<SelectableText> with AutomaticKeepAlive
enableInteractiveSelection: widget.enableInteractiveSelection, enableInteractiveSelection: widget.enableInteractiveSelection,
dragStartBehavior: widget.dragStartBehavior, dragStartBehavior: widget.dragStartBehavior,
scrollPhysics: widget.scrollPhysics, scrollPhysics: widget.scrollPhysics,
autofillHints: null,
), ),
); );
......
...@@ -345,7 +345,7 @@ class TextField extends StatefulWidget { ...@@ -345,7 +345,7 @@ class TextField extends StatefulWidget {
this.buildCounter, this.buildCounter,
this.scrollController, this.scrollController,
this.scrollPhysics, this.scrollPhysics,
this.autofillHints, this.autofillHints = const <String>[],
this.restorationId, this.restorationId,
this.enableIMEPersonalizedLearning = true, this.enableIMEPersonalizedLearning = true,
}) : assert(textAlign != null), }) : assert(textAlign != null),
...@@ -827,7 +827,7 @@ class TextField extends StatefulWidget { ...@@ -827,7 +827,7 @@ class TextField extends StatefulWidget {
} }
} }
class _TextFieldState extends State<TextField> with RestorationMixin implements TextSelectionGestureDetectorBuilderDelegate { class _TextFieldState extends State<TextField> with RestorationMixin implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient {
RestorableTextEditingController? _controller; RestorableTextEditingController? _controller;
TextEditingController get _effectiveController => widget.controller ?? _controller!.value; TextEditingController get _effectiveController => widget.controller ?? _controller!.value;
...@@ -1094,6 +1094,29 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements ...@@ -1094,6 +1094,29 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
} }
} }
// AutofillClient implementation start.
@override
String get autofillId => _editableText!.autofillId;
@override
void autofill(TextEditingValue newEditingValue) => _editableText!.autofill(newEditingValue);
@override
TextInputConfiguration get textInputConfiguration {
final List<String>? autofillHints = widget.autofillHints?.toList(growable: false);
final AutofillConfiguration autofillConfiguration = autofillHints != null
? AutofillConfiguration(
uniqueIdentifier: autofillId,
autofillHints: autofillHints,
currentEditingValue: _effectiveController.value,
hintText: (widget.decoration ?? const InputDecoration()).hintText,
)
: AutofillConfiguration.disabled;
return _editableText!.textInputConfiguration.copyWith(autofillConfiguration: autofillConfiguration);
}
// AutofillClient implementation end.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
...@@ -1240,7 +1263,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements ...@@ -1240,7 +1263,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
dragStartBehavior: widget.dragStartBehavior, dragStartBehavior: widget.dragStartBehavior,
scrollController: widget.scrollController, scrollController: widget.scrollController,
scrollPhysics: widget.scrollPhysics, scrollPhysics: widget.scrollPhysics,
autofillHints: widget.autofillHints, autofillClient: this,
autocorrectionTextRectColor: autocorrectionTextRectColor, autocorrectionTextRectColor: autocorrectionTextRectColor,
restorationId: 'editable', restorationId: 'editable',
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
......
...@@ -631,12 +631,40 @@ class AutofillConfiguration { ...@@ -631,12 +631,40 @@ class AutofillConfiguration {
/// Creates autofill related configuration information that can be sent to the /// Creates autofill related configuration information that can be sent to the
/// platform. /// platform.
const AutofillConfiguration({ const AutofillConfiguration({
required String uniqueIdentifier,
required List<String> autofillHints,
required TextEditingValue currentEditingValue,
String? hintText,
}) : this._(
enabled: true,
uniqueIdentifier: uniqueIdentifier,
autofillHints: autofillHints,
currentEditingValue: currentEditingValue,
hintText: hintText,
);
const AutofillConfiguration._({
required this.enabled,
required this.uniqueIdentifier, required this.uniqueIdentifier,
required this.autofillHints, this.autofillHints = const <String>[],
this.hintText,
required this.currentEditingValue, required this.currentEditingValue,
}) : assert(uniqueIdentifier != null), }) : assert(uniqueIdentifier != null),
assert(autofillHints != null); assert(autofillHints != null);
/// An [AutofillConfiguration] that indicates the [AutofillClient] does not
/// wish to be autofilled.
static const AutofillConfiguration disabled = AutofillConfiguration._(
enabled: false,
uniqueIdentifier: '',
currentEditingValue: TextEditingValue.empty,
);
/// Whether autofill should be enabled for the [AutofillClient].
///
/// To retrieve a disabled [AutofillConfiguration], use [disabled].
final bool enabled;
/// A string that uniquely identifies the current [AutofillClient]. /// A string that uniquely identifies the current [AutofillClient].
/// ///
/// The identifier needs to be unique within the [AutofillScope] for the /// The identifier needs to be unique within the [AutofillScope] for the
...@@ -648,7 +676,7 @@ class AutofillConfiguration { ...@@ -648,7 +676,7 @@ class AutofillConfiguration {
/// A list of strings that helps the autofill service identify the type of the /// A list of strings that helps the autofill service identify the type of the
/// [AutofillClient]. /// [AutofillClient].
/// ///
/// Must not be null or empty. /// Must not be null.
/// ///
/// {@template flutter.services.AutofillConfiguration.autofillHints} /// {@template flutter.services.AutofillConfiguration.autofillHints}
/// For the best results, hint strings need to be understood by the platform's /// For the best results, hint strings need to be understood by the platform's
...@@ -697,14 +725,23 @@ class AutofillConfiguration { ...@@ -697,14 +725,23 @@ class AutofillConfiguration {
/// The current [TextEditingValue] of the [AutofillClient]. /// The current [TextEditingValue] of the [AutofillClient].
final TextEditingValue currentEditingValue; final TextEditingValue currentEditingValue;
/// The optional hint text placed on the view that typically suggests what
/// sort of input the field accepts, for example "enter your password here".
///
/// If the developer does not specify any [autofillHints], the [hintText] can
/// be a useful indication to the platform autofill service.
final String? hintText;
/// Returns a representation of this object as a JSON object. /// Returns a representation of this object as a JSON object.
Map<String, dynamic> toJson() { Map<String, dynamic>? toJson() {
assert(autofillHints.isNotEmpty); return enabled
return <String, dynamic>{ ? <String, dynamic>{
'uniqueIdentifier': uniqueIdentifier, 'uniqueIdentifier': uniqueIdentifier,
'hints': autofillHints, 'hints': autofillHints,
'editingValue': currentEditingValue.toJSON(), 'editingValue': currentEditingValue.toJSON(),
}; if (hintText != null) 'hintText': hintText,
}
: null;
} }
} }
...@@ -715,7 +752,7 @@ class AutofillConfiguration { ...@@ -715,7 +752,7 @@ class AutofillConfiguration {
abstract class AutofillClient { abstract class AutofillClient {
/// The unique identifier of this [AutofillClient]. /// The unique identifier of this [AutofillClient].
/// ///
/// Must not be null. /// Must not be null and the identifier must not be changed.
String get autofillId; String get autofillId;
/// The [TextInputConfiguration] that describes this [AutofillClient]. /// The [TextInputConfiguration] that describes this [AutofillClient].
...@@ -726,7 +763,7 @@ abstract class AutofillClient { ...@@ -726,7 +763,7 @@ abstract class AutofillClient {
/// Requests this [AutofillClient] update its [TextEditingValue] to the given /// Requests this [AutofillClient] update its [TextEditingValue] to the given
/// value. /// value.
void updateEditingValue(TextEditingValue newEditingValue); void autofill(TextEditingValue newEditingValue);
} }
/// An ordered group within which [AutofillClient]s are logically connected. /// An ordered group within which [AutofillClient]s are logically connected.
...@@ -806,7 +843,7 @@ mixin AutofillScopeMixin implements AutofillScope { ...@@ -806,7 +843,7 @@ mixin AutofillScopeMixin implements AutofillScope {
TextInputConnection attach(TextInputClient trigger, TextInputConfiguration configuration) { TextInputConnection attach(TextInputClient trigger, TextInputConfiguration configuration) {
assert(trigger != null); assert(trigger != null);
assert( assert(
!autofillClients.any((AutofillClient client) => client.textInputConfiguration.autofillConfiguration == null), !autofillClients.any((AutofillClient client) => !client.textInputConfiguration.autofillConfiguration.enabled),
'Every client in AutofillScope.autofillClients must enable autofill', 'Every client in AutofillScope.autofillClients must enable autofill',
); );
......
...@@ -466,7 +466,7 @@ class TextInputConfiguration { ...@@ -466,7 +466,7 @@ class TextInputConfiguration {
this.inputAction = TextInputAction.done, this.inputAction = TextInputAction.done,
this.keyboardAppearance = Brightness.light, this.keyboardAppearance = Brightness.light,
this.textCapitalization = TextCapitalization.none, this.textCapitalization = TextCapitalization.none,
this.autofillConfiguration, this.autofillConfiguration = AutofillConfiguration.disabled,
this.enableIMEPersonalizedLearning = true, this.enableIMEPersonalizedLearning = true,
}) : assert(inputType != null), }) : assert(inputType != null),
assert(obscureText != null), assert(obscureText != null),
...@@ -503,7 +503,7 @@ class TextInputConfiguration { ...@@ -503,7 +503,7 @@ class TextInputConfiguration {
/// to the platform. This will prevent the corresponding input field from /// to the platform. This will prevent the corresponding input field from
/// participating in autofills triggered by other fields. Additionally, on /// participating in autofills triggered by other fields. Additionally, on
/// Android and web, setting [autofillConfiguration] to null disables autofill. /// Android and web, setting [autofillConfiguration] to null disables autofill.
final AutofillConfiguration? autofillConfiguration; final AutofillConfiguration autofillConfiguration;
/// {@template flutter.services.TextInputConfiguration.smartDashesType} /// {@template flutter.services.TextInputConfiguration.smartDashesType}
/// Whether to allow the platform to automatically format dashes. /// Whether to allow the platform to automatically format dashes.
...@@ -607,8 +607,41 @@ class TextInputConfiguration { ...@@ -607,8 +607,41 @@ class TextInputConfiguration {
/// {@endtemplate} /// {@endtemplate}
final bool enableIMEPersonalizedLearning; final bool enableIMEPersonalizedLearning;
/// Creates a copy of this [TextInputConfiguration] with the given fields
/// replaced with new values.
TextInputConfiguration copyWith({
TextInputType? inputType,
bool? readOnly,
bool? obscureText,
bool? autocorrect,
SmartDashesType? smartDashesType,
SmartQuotesType? smartQuotesType,
bool? enableSuggestions,
String? actionLabel,
TextInputAction? inputAction,
Brightness? keyboardAppearance,
TextCapitalization? textCapitalization,
bool? enableIMEPersonalizedLearning,
AutofillConfiguration? autofillConfiguration,
}) {
return TextInputConfiguration(
inputType: inputType ?? this.inputType,
readOnly: readOnly ?? this.readOnly,
obscureText: obscureText ?? this.obscureText,
autocorrect: autocorrect ?? this.autocorrect,
smartDashesType: smartDashesType ?? this.smartDashesType,
smartQuotesType: smartQuotesType ?? this.smartQuotesType,
enableSuggestions: enableSuggestions ?? this.enableSuggestions,
inputAction: inputAction ?? this.inputAction,
textCapitalization: textCapitalization ?? this.textCapitalization,
keyboardAppearance: keyboardAppearance ?? this.keyboardAppearance,
enableIMEPersonalizedLearning: enableIMEPersonalizedLearning?? this.enableIMEPersonalizedLearning,
autofillConfiguration: autofillConfiguration ?? this.autofillConfiguration,
);
}
/// Returns a representation of this object as a JSON object. /// Returns a representation of this object as a JSON object.
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final Map<String, dynamic>? autofill = autofillConfiguration.toJson();
return <String, dynamic>{ return <String, dynamic>{
'inputType': inputType.toJson(), 'inputType': inputType.toJson(),
'readOnly': readOnly, 'readOnly': readOnly,
...@@ -622,7 +655,7 @@ class TextInputConfiguration { ...@@ -622,7 +655,7 @@ class TextInputConfiguration {
'textCapitalization': textCapitalization.toString(), 'textCapitalization': textCapitalization.toString(),
'keyboardAppearance': keyboardAppearance.toString(), 'keyboardAppearance': keyboardAppearance.toString(),
'enableIMEPersonalizedLearning': enableIMEPersonalizedLearning, 'enableIMEPersonalizedLearning': enableIMEPersonalizedLearning,
if (autofillConfiguration != null) 'autofill': autofillConfiguration!.toJson(), if (autofill != null) 'autofill': autofill,
}; };
} }
} }
...@@ -1470,7 +1503,10 @@ class TextInput { ...@@ -1470,7 +1503,10 @@ class TextInput {
final TextEditingValue textEditingValue = TextEditingValue.fromJSON( final TextEditingValue textEditingValue = TextEditingValue.fromJSON(
editingValue[tag] as Map<String, dynamic>, editingValue[tag] as Map<String, dynamic>,
); );
scope?.getAutofillClient(tag)?.updateEditingValue(textEditingValue); final AutofillClient? client = scope?.getAutofillClient(tag);
if (client != null && client.textInputConfiguration.autofillConfiguration.enabled) {
client.autofill(textEditingValue);
}
} }
return; return;
......
...@@ -134,7 +134,7 @@ class AutofillGroupState extends State<AutofillGroup> with AutofillScopeMixin { ...@@ -134,7 +134,7 @@ class AutofillGroupState extends State<AutofillGroup> with AutofillScopeMixin {
@override @override
Iterable<AutofillClient> get autofillClients { Iterable<AutofillClient> get autofillClients {
return _clients.values return _clients.values
.where((AutofillClient client) => client.textInputConfiguration.autofillConfiguration != null); .where((AutofillClient client) => client.textInputConfiguration.autofillConfiguration.enabled);
} }
/// Adds the [AutofillClient] to this [AutofillGroup]. /// Adds the [AutofillClient] to this [AutofillGroup].
...@@ -155,9 +155,8 @@ class AutofillGroupState extends State<AutofillGroup> with AutofillScopeMixin { ...@@ -155,9 +155,8 @@ class AutofillGroupState extends State<AutofillGroup> with AutofillScopeMixin {
/// Removes an [AutofillClient] with the given `autofillId` from this /// Removes an [AutofillClient] with the given `autofillId` from this
/// [AutofillGroup]. /// [AutofillGroup].
/// ///
/// Typically, this should be called by autofillable [TextInputClient]s in /// Typically, this should be called by a text field when it's being disposed,
/// [State.dispose] and [State.didChangeDependencies], when the input field /// or before it's registered with a different [AutofillGroup].
/// needs to be removed from the [AutofillGroup] it is currently registered to.
/// ///
/// See also: /// See also:
/// ///
......
...@@ -456,7 +456,8 @@ class EditableText extends StatefulWidget { ...@@ -456,7 +456,8 @@ class EditableText extends StatefulWidget {
paste: true, paste: true,
selectAll: true, selectAll: true,
), ),
this.autofillHints, this.autofillHints = const <String>[],
this.autofillClient,
this.clipBehavior = Clip.hardEdge, this.clipBehavior = Clip.hardEdge,
this.restorationId, this.restorationId,
this.scrollBehavior, this.scrollBehavior,
...@@ -499,10 +500,6 @@ class EditableText extends StatefulWidget { ...@@ -499,10 +500,6 @@ class EditableText extends StatefulWidget {
assert(dragStartBehavior != null), assert(dragStartBehavior != null),
assert(toolbarOptions != null), assert(toolbarOptions != null),
assert(clipBehavior != null), assert(clipBehavior != null),
assert(
!readOnly || autofillHints == null,
"Read-only fields can't have autofill hints.",
),
assert(enableIMEPersonalizedLearning != null), assert(enableIMEPersonalizedLearning != null),
_strutStyle = strutStyle, _strutStyle = strutStyle,
keyboardType = keyboardType ?? _inferKeyboardType(autofillHints: autofillHints, maxLines: maxLines), keyboardType = keyboardType ?? _inferKeyboardType(autofillHints: autofillHints, maxLines: maxLines),
...@@ -1163,15 +1160,17 @@ class EditableText extends StatefulWidget { ...@@ -1163,15 +1160,17 @@ class EditableText extends StatefulWidget {
/// A list of strings that helps the autofill service identify the type of this /// A list of strings that helps the autofill service identify the type of this
/// text input. /// text input.
/// ///
/// When set to null or empty, this text input will not send its autofill /// When set to null, this text input will not send its autofill information
/// information to the platform, preventing it from participating in /// to the platform, preventing it from participating in autofills triggered
/// autofills triggered by a different [AutofillClient], even if they're in the /// by a different [AutofillClient], even if they're in the same
/// same [AutofillScope]. Additionally, on Android and web, setting this to /// [AutofillScope]. Additionally, on Android and web, setting this to null
/// null or empty will disable autofill for this text field. /// will disable autofill for this text field.
/// ///
/// The minimum platform SDK version that supports Autofill is API level 26 /// The minimum platform SDK version that supports Autofill is API level 26
/// for Android, and iOS 10.0 for iOS. /// for Android, and iOS 10.0 for iOS.
/// ///
/// Defaults to an empty list.
///
/// ### Setting up iOS autofill: /// ### Setting up iOS autofill:
/// ///
/// To provide the best user experience and ensure your app fully supports /// To provide the best user experience and ensure your app fully supports
...@@ -1229,6 +1228,12 @@ class EditableText extends StatefulWidget { ...@@ -1229,6 +1228,12 @@ class EditableText extends StatefulWidget {
/// {@macro flutter.services.AutofillConfiguration.autofillHints} /// {@macro flutter.services.AutofillConfiguration.autofillHints}
final Iterable<String>? autofillHints; final Iterable<String>? autofillHints;
/// The [AutofillClient] that controls this input field's autofill behavior.
///
/// When null, this widget's [EditableTextState] will be used as the
/// [AutofillClient]. This property may override [autofillHints].
final AutofillClient? autofillClient;
/// {@macro flutter.material.Material.clipBehavior} /// {@macro flutter.material.Material.clipBehavior}
/// ///
/// Defaults to [Clip.hardEdge]. /// Defaults to [Clip.hardEdge].
...@@ -1278,12 +1283,11 @@ class EditableText extends StatefulWidget { ...@@ -1278,12 +1283,11 @@ class EditableText extends StatefulWidget {
required Iterable<String>? autofillHints, required Iterable<String>? autofillHints,
required int? maxLines, required int? maxLines,
}) { }) {
if (autofillHints?.isEmpty ?? true) { if (autofillHints == null || autofillHints.isEmpty) {
return maxLines == 1 ? TextInputType.text : TextInputType.multiline; return maxLines == 1 ? TextInputType.text : TextInputType.multiline;
} }
TextInputType? returnValue; final String effectiveHint = autofillHints.first;
final String effectiveHint = autofillHints!.first;
// On iOS oftentimes specifying a text content type is not enough to qualify // 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 // the input field for autofill. The keyboard type also needs to be compatible
...@@ -1328,7 +1332,10 @@ class EditableText extends StatefulWidget { ...@@ -1328,7 +1332,10 @@ class EditableText extends StatefulWidget {
AutofillHints.username : TextInputType.text, AutofillHints.username : TextInputType.text,
}; };
returnValue = iOSKeyboardType[effectiveHint]; final TextInputType? keyboardType = iOSKeyboardType[effectiveHint];
if (keyboardType != null) {
return keyboardType;
}
break; break;
case TargetPlatform.android: case TargetPlatform.android:
case TargetPlatform.fuchsia: case TargetPlatform.fuchsia:
...@@ -1338,8 +1345,9 @@ class EditableText extends StatefulWidget { ...@@ -1338,8 +1345,9 @@ class EditableText extends StatefulWidget {
} }
} }
if (returnValue != null || maxLines != 1) if (maxLines != 1) {
return returnValue ?? TextInputType.multiline; return TextInputType.multiline;
}
const Map<String, TextInputType> inferKeyboardType = <String, TextInputType> { const Map<String, TextInputType> inferKeyboardType = <String, TextInputType> {
AutofillHints.addressCity : TextInputType.streetAddress, AutofillHints.addressCity : TextInputType.streetAddress,
...@@ -1474,8 +1482,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1474,8 +1482,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override @override
AutofillScope? get currentAutofillScope => _currentAutofillScope; AutofillScope? get currentAutofillScope => _currentAutofillScope;
// Is this field in the current autofill context. AutofillClient get _effectiveAutofillClient => widget.autofillClient ?? this;
bool _isInAutofillContext = false;
/// Whether to create an input connection with the platform for text editing /// Whether to create an input connection with the platform for text editing
/// or not. /// or not.
...@@ -1548,8 +1555,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1548,8 +1555,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (currentAutofillScope != newAutofillGroup) { if (currentAutofillScope != newAutofillGroup) {
_currentAutofillScope?.unregister(autofillId); _currentAutofillScope?.unregister(autofillId);
_currentAutofillScope = newAutofillGroup; _currentAutofillScope = newAutofillGroup;
newAutofillGroup?.register(this); _currentAutofillScope?.register(_effectiveAutofillClient);
_isInAutofillContext = _isInAutofillContext || _shouldBeInAutofillContext;
} }
if (!_didAutoFocus && widget.autofocus) { if (!_didAutoFocus && widget.autofocus) {
...@@ -1574,7 +1580,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1574,7 +1580,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_selectionOverlay?.update(_value); _selectionOverlay?.update(_value);
} }
_selectionOverlay?.handlesVisible = widget.showSelectionHandles; _selectionOverlay?.handlesVisible = widget.showSelectionHandles;
_isInAutofillContext = _isInAutofillContext || _shouldBeInAutofillContext;
if (widget.autofillClient != oldWidget.autofillClient) {
_currentAutofillScope?.unregister(oldWidget.autofillClient?.autofillId ?? autofillId);
_currentAutofillScope?.register(_effectiveAutofillClient);
}
if (widget.focusNode != oldWidget.focusNode) { if (widget.focusNode != oldWidget.focusNode) {
oldWidget.focusNode.removeListener(_handleFocusChanged); oldWidget.focusNode.removeListener(_handleFocusChanged);
...@@ -1597,7 +1607,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1597,7 +1607,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (kIsWeb && _hasInputConnection) { if (kIsWeb && _hasInputConnection) {
if (oldWidget.readOnly != widget.readOnly) { if (oldWidget.readOnly != widget.readOnly) {
_textInputConnection!.updateConfig(textInputConfiguration); _textInputConnection!.updateConfig(_effectiveAutofillClient.textInputConfiguration);
} }
} }
...@@ -1980,8 +1990,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1980,8 +1990,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
bool get _hasInputConnection => _textInputConnection?.attached ?? false; bool get _hasInputConnection => _textInputConnection?.attached ?? false;
bool get _needsAutofill => widget.autofillHints?.isNotEmpty ?? false; /// Whether to send the autofill information to the autofill service. True by
bool get _shouldBeInAutofillContext => _needsAutofill && currentAutofillScope != null; /// default.
bool get _needsAutofill => widget.autofillHints?.isNotEmpty ?? true;
void _openInputConnection() { void _openInputConnection() {
if (!_shouldCreateInputConnection) { if (!_shouldCreateInputConnection) {
...@@ -1999,8 +2010,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1999,8 +2010,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// notified to exclude this field from the autofill context. So we need to // notified to exclude this field from the autofill context. So we need to
// provide the autofillId. // provide the autofillId.
_textInputConnection = _needsAutofill && currentAutofillScope != null _textInputConnection = _needsAutofill && currentAutofillScope != null
? currentAutofillScope!.attach(this, textInputConfiguration) ? currentAutofillScope!.attach(this, _effectiveAutofillClient.textInputConfiguration)
: TextInput.attach(this, _createTextInputConfiguration(_isInAutofillContext || _needsAutofill)); : TextInput.attach(this, _effectiveAutofillClient.textInputConfiguration);
_textInputConnection!.show(); _textInputConnection!.show();
_updateSizeAndTransform(); _updateSizeAndTransform();
_updateComposingRectIfNeeded(); _updateComposingRectIfNeeded();
...@@ -2523,8 +2534,17 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2523,8 +2534,17 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override @override
String get autofillId => 'EditableText-$hashCode'; String get autofillId => 'EditableText-$hashCode';
TextInputConfiguration _createTextInputConfiguration(bool needsAutofillConfiguration) { @override
assert(needsAutofillConfiguration != null); TextInputConfiguration get textInputConfiguration {
final List<String>? autofillHints = widget.autofillHints?.toList(growable: false);
final AutofillConfiguration autofillConfiguration = autofillHints != null
? AutofillConfiguration(
uniqueIdentifier: autofillId,
autofillHints: autofillHints,
currentEditingValue: currentTextEditingValue,
)
: AutofillConfiguration.disabled;
return TextInputConfiguration( return TextInputConfiguration(
inputType: widget.keyboardType, inputType: widget.keyboardType,
readOnly: widget.readOnly, readOnly: widget.readOnly,
...@@ -2539,19 +2559,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2539,19 +2559,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
), ),
textCapitalization: widget.textCapitalization, textCapitalization: widget.textCapitalization,
keyboardAppearance: widget.keyboardAppearance, keyboardAppearance: widget.keyboardAppearance,
autofillConfiguration: !needsAutofillConfiguration ? null : AutofillConfiguration( autofillConfiguration: autofillConfiguration,
uniqueIdentifier: autofillId,
autofillHints: widget.autofillHints?.toList(growable: false) ?? <String>[],
currentEditingValue: currentTextEditingValue,
),
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
); );
} }
@override @override
TextInputConfiguration get textInputConfiguration { void autofill(TextEditingValue value) => updateEditingValue(value);
return _createTextInputConfiguration(_needsAutofill);
}
// null if no promptRect should be shown. // null if no promptRect should be shown.
TextRange? _currentPromptRectRange; TextRange? _currentPromptRectRange;
......
...@@ -4776,6 +4776,22 @@ void main() { ...@@ -4776,6 +4776,22 @@ void main() {
}, },
); );
testWidgets('autofill info has placeholder text', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoTextField(
placeholder: 'placeholder text',
),
),
);
await tester.tap(find.byType(CupertinoTextField));
expect(
tester.testTextInput.setClientArgs?['autofill'],
containsPair('hintText', 'placeholder text'),
);
});
testWidgets('textDirection is passed to EditableText', (WidgetTester tester) async { testWidgets('textDirection is passed to EditableText', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const CupertinoApp( const CupertinoApp(
......
...@@ -9924,4 +9924,27 @@ void main() { ...@@ -9924,4 +9924,27 @@ void main() {
expect(prefixTapCount, 1); expect(prefixTapCount, 1);
expect(suffixTapCount, 1); expect(suffixTapCount, 1);
}); });
testWidgets('autofill info has hint text', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: Center(
child: TextField(
decoration: InputDecoration(
hintText: 'placeholder text'
),
),
),
),
),
);
await tester.tap(find.byType(TextField));
expect(
tester.testTextInput.setClientArgs?['autofill'],
containsPair('hintText', 'placeholder text'),
);
});
} }
...@@ -10,7 +10,7 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -10,7 +10,7 @@ import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
group('TextInput message channels', () { group('AutofillClient', () {
late FakeTextChannel fakeTextChannel; late FakeTextChannel fakeTextChannel;
final FakeAutofillScope scope = FakeAutofillScope(); final FakeAutofillScope scope = FakeAutofillScope();
...@@ -25,21 +25,19 @@ void main() { ...@@ -25,21 +25,19 @@ void main() {
TextInput.setChannel(SystemChannels.textInput); TextInput.setChannel(SystemChannels.textInput);
}); });
test('throws if the hint list is empty', () async { test('Does not throw if the hint list is empty', () async {
Map<String, dynamic>? json; Object? exception;
try { try {
const AutofillConfiguration config = AutofillConfiguration( const AutofillConfiguration(
uniqueIdentifier: 'id', uniqueIdentifier: 'id',
autofillHints: <String>[], autofillHints: <String>[],
currentEditingValue: TextEditingValue.empty, currentEditingValue: TextEditingValue.empty,
); );
json = config.toJson();
} catch (e) { } catch (e) {
expect(e.toString(), contains('isNotEmpty')); exception = e;
} }
expect(json, isNull); expect(exception, isNull);
}); });
test( test(
...@@ -140,6 +138,9 @@ class FakeAutofillClient implements TextInputClient, AutofillClient { ...@@ -140,6 +138,9 @@ class FakeAutofillClient implements TextInputClient, AutofillClient {
void showAutocorrectionPromptRect(int start, int end) { void showAutocorrectionPromptRect(int start, int end) {
latestMethodCall = 'showAutocorrectionPromptRect'; latestMethodCall = 'showAutocorrectionPromptRect';
} }
@override
void autofill(TextEditingValue newEditingValue) => updateEditingValue(newEditingValue);
} }
class FakeAutofillScope with AutofillScopeMixin implements AutofillScope { class FakeAutofillScope with AutofillScopeMixin implements AutofillScope {
......
...@@ -25,7 +25,7 @@ void main() { ...@@ -25,7 +25,7 @@ void main() {
client1, client1,
AutofillGroup( AutofillGroup(
key: innerKey, key: innerKey,
child: Column(children: const <Widget>[client2, TextField()]), child: Column(children: const <Widget>[client2, TextField(autofillHints: null)]),
), ),
]), ]),
), ),
...@@ -36,23 +36,19 @@ void main() { ...@@ -36,23 +36,19 @@ void main() {
final AutofillGroupState innerState = tester.state<AutofillGroupState>(find.byKey(innerKey)); final AutofillGroupState innerState = tester.state<AutofillGroupState>(find.byKey(innerKey));
final AutofillGroupState outerState = tester.state<AutofillGroupState>(find.byKey(outerKey)); final AutofillGroupState outerState = tester.state<AutofillGroupState>(find.byKey(outerKey));
final EditableTextState clientState1 = tester.state<EditableTextState>( final State<TextField> clientState1 = tester.state<State<TextField>>(find.byWidget(client1));
find.descendant(of: find.byWidget(client1), matching: find.byType(EditableText)), final State<TextField> clientState2 = tester.state<State<TextField>>(find.byWidget(client2));
);
final EditableTextState clientState2 = tester.state<EditableTextState>(
find.descendant(of: find.byWidget(client2), matching: find.byType(EditableText)),
);
expect(outerState.autofillClients, <EditableTextState>[clientState1]); expect(outerState.autofillClients.toList(), <State<TextField>>[clientState1]);
// The second TextField doesn't have autofill enabled. // The second TextField in the AutofillGroup doesn't have autofill enabled.
expect(innerState.autofillClients, <EditableTextState>[clientState2]); expect(innerState.autofillClients.toList(), <State<TextField>>[clientState2]);
}); });
testWidgets('new clients can be added & removed to a scope', (WidgetTester tester) async { testWidgets('new clients can be added & removed to a scope', (WidgetTester tester) async {
const Key scopeKey = Key('scope'); const Key scopeKey = Key('scope');
const TextField client1 = TextField(autofillHints: <String>['1']); const TextField client1 = TextField(autofillHints: <String>['1']);
TextField client2 = const TextField(autofillHints: <String>[]); TextField client2 = const TextField(autofillHints: null);
late StateSetter setState; late StateSetter setState;
...@@ -74,14 +70,10 @@ void main() { ...@@ -74,14 +70,10 @@ void main() {
final AutofillGroupState scopeState = tester.state<AutofillGroupState>(find.byKey(scopeKey)); final AutofillGroupState scopeState = tester.state<AutofillGroupState>(find.byKey(scopeKey));
final EditableTextState clientState1 = tester.state<EditableTextState>( final State<TextField> clientState1 = tester.state<State<TextField>>(find.byWidget(client1));
find.descendant(of: find.byWidget(client1), matching: find.byType(EditableText)), final State<TextField> clientState2 = tester.state<State<TextField>>(find.byWidget(client2));
);
final EditableTextState clientState2 = tester.state<EditableTextState>(
find.descendant(of: find.byWidget(client2), matching: find.byType(EditableText)),
);
expect(scopeState.autofillClients, <EditableTextState>[clientState1]); expect(scopeState.autofillClients.toList(), <State<TextField>>[clientState1]);
// Add to scope. // Add to scope.
setState(() { client2 = const TextField(autofillHints: <String>['2']); }); setState(() { client2 = const TextField(autofillHints: <String>['2']); });
...@@ -93,11 +85,11 @@ void main() { ...@@ -93,11 +85,11 @@ void main() {
expect(scopeState.autofillClients.length, 2); expect(scopeState.autofillClients.length, 2);
// Remove from scope again. // Remove from scope again.
setState(() { client2 = const TextField(autofillHints: <String>[]); }); setState(() { client2 = const TextField(autofillHints: null); });
await tester.pump(); await tester.pump();
expect(scopeState.autofillClients, <EditableTextState>[clientState1]); expect(scopeState.autofillClients, <State<TextField>>[clientState1]);
}); });
testWidgets('AutofillGroup has the right clients after reparenting', (WidgetTester tester) async { testWidgets('AutofillGroup has the right clients after reparenting', (WidgetTester tester) async {
...@@ -131,16 +123,9 @@ void main() { ...@@ -131,16 +123,9 @@ void main() {
final AutofillGroupState innerState = tester.state<AutofillGroupState>(find.byKey(innerKey)); final AutofillGroupState innerState = tester.state<AutofillGroupState>(find.byKey(innerKey));
final AutofillGroupState outerState = tester.state<AutofillGroupState>(find.byKey(outerKey)); final AutofillGroupState outerState = tester.state<AutofillGroupState>(find.byKey(outerKey));
final EditableTextState clientState1 = tester.state<EditableTextState>( final State<TextField> clientState1 = tester.state<State<TextField>>(find.byWidget(client1));
find.descendant(of: find.byWidget(client1), matching: find.byType(EditableText)), final State<TextField> clientState2 = tester.state<State<TextField>>(find.byWidget(client2));
); final State<TextField> clientState3 = tester.state<State<TextField>>(find.byKey(keyClient3));
final EditableTextState clientState2 = tester.state<EditableTextState>(
find.descendant(of: find.byWidget(client2), matching: find.byType(EditableText)),
);
final EditableTextState clientState3 = tester.state<EditableTextState>(
find.descendant(of: find.byKey(keyClient3), matching: find.byType(EditableText)),
);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
...@@ -163,7 +148,7 @@ void main() { ...@@ -163,7 +148,7 @@ void main() {
expect(outerState.autofillClients.length, 2); expect(outerState.autofillClients.length, 2);
expect(outerState.autofillClients, contains(clientState1)); expect(outerState.autofillClients, contains(clientState1));
expect(outerState.autofillClients, contains(clientState3)); expect(outerState.autofillClients, contains(clientState3));
expect(innerState.autofillClients, <EditableTextState>[clientState2]); expect(innerState.autofillClients, <State<TextField>>[clientState2]);
}); });
testWidgets('disposing AutofillGroups', (WidgetTester tester) async { testWidgets('disposing AutofillGroups', (WidgetTester tester) async {
...@@ -270,8 +255,7 @@ void main() { ...@@ -270,8 +255,7 @@ void main() {
// Remove the topmosts group group3. Should commit. // Remove the topmosts group group3. Should commit.
setState(() { setState(() {
children = const <Widget> [ children = const <Widget> [];
];
}); });
await tester.pump(); await tester.pump();
......
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