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