Autofill save (#58731)
This commit is contained in:
parent
3215a0130b
commit
fe88a88a5a
@ -807,13 +807,11 @@ mixin AutofillScopeMixin implements AutofillScope {
|
|||||||
!autofillClients.any((AutofillClient client) => client.textInputConfiguration.autofillConfiguration == null),
|
!autofillClients.any((AutofillClient client) => client.textInputConfiguration.autofillConfiguration == null),
|
||||||
'Every client in AutofillScope.autofillClients must enable autofill',
|
'Every client in AutofillScope.autofillClients must enable autofill',
|
||||||
);
|
);
|
||||||
return TextInput.attach(
|
|
||||||
trigger,
|
final TextInputConfiguration inputConfiguration = _AutofillScopeTextInputConfiguration(
|
||||||
_AutofillScopeTextInputConfiguration(
|
allConfigurations: autofillClients.map((AutofillClient client) => client.textInputConfiguration),
|
||||||
allConfigurations: autofillClients
|
|
||||||
.map((AutofillClient client) => client.textInputConfiguration),
|
|
||||||
currentClientConfiguration: configuration,
|
currentClientConfiguration: configuration,
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
return TextInput.attach(trigger, inputConfiguration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -861,12 +861,14 @@ class TextInputConnection {
|
|||||||
TextInput._instance._show();
|
TextInput._instance._show();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Requests the platform autofill UI to appear.
|
/// Requests the system autofill UI to appear.
|
||||||
///
|
///
|
||||||
/// The call has no effect unless the currently attached client supports
|
/// Currently only works on Android. Other platforms do not respond to this
|
||||||
/// autofill, and the platform has a standalone autofill UI (for example, this
|
/// message.
|
||||||
/// call has no effect on iOS since its autofill UI is part of the software
|
///
|
||||||
/// keyboard).
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [EditableText], a [TextInputClient] that calls this method when focused.
|
||||||
void requestAutofill() {
|
void requestAutofill() {
|
||||||
assert(attached);
|
assert(attached);
|
||||||
TextInput._instance._requestAutofill();
|
TextInput._instance._requestAutofill();
|
||||||
@ -1228,4 +1230,58 @@ class TextInput {
|
|||||||
args,
|
args,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Finishes the current autofill context, and potentially saves the user
|
||||||
|
/// input for future use if `shouldSave` is true.
|
||||||
|
///
|
||||||
|
/// Typically, this method should be called when the user has finalized their
|
||||||
|
/// input. For example, in a [Form], it's typically done immediately before or
|
||||||
|
/// after its content is submitted.
|
||||||
|
///
|
||||||
|
/// The topmost [AutofillGroup]s also call [finishAutofillContext]
|
||||||
|
/// automatically when they are disposed. The default behavior can be
|
||||||
|
/// overridden in [AutofillGroup.onDisposeAction].
|
||||||
|
///
|
||||||
|
/// {@template flutter.services.autofill.autofillContext}
|
||||||
|
/// An autofill context is a collection of input fields that live in the
|
||||||
|
/// platform's text input plugin. The platform is encouraged to save the user
|
||||||
|
/// input stored in the current autofill context before the context is
|
||||||
|
/// destroyed, when [finishAutofillContext] is called with `shouldSave` set to
|
||||||
|
/// true.
|
||||||
|
///
|
||||||
|
/// Currently, there can only be at most one autofill context at any given
|
||||||
|
/// time. When any input field in an [AutofillGroup] requests for autofill
|
||||||
|
/// (which is done automatically when an autofillable [EditableText] gains
|
||||||
|
/// focus), the current autofill context will merge the content of that
|
||||||
|
/// [AutofillGroup] into itself. When there isn't an existing autofill context,
|
||||||
|
/// one will be created to hold the newly added input fields from the group.
|
||||||
|
///
|
||||||
|
/// Once added to an autofill context, an input field will stay in the context
|
||||||
|
/// until the context is destroyed. To prevent leaks, call [finishAutofillContext]
|
||||||
|
/// to signal the text input plugin that the user has finalized their input in
|
||||||
|
/// the current autofill context. The platform text input plugin either
|
||||||
|
/// encourages or discourages the platform from saving the user input based on
|
||||||
|
/// the value of the `shouldSave` parameter. The platform usually shows a
|
||||||
|
/// "Save for autofill?" prompt for user confirmation.
|
||||||
|
/// {@endtemplate}
|
||||||
|
///
|
||||||
|
/// On many platforms, calling [finishAutofillContext] shows the save user
|
||||||
|
/// input dialog and disrupts the user's flow. Ideally the dialog should only
|
||||||
|
/// be shown no more than once for every screen. Consider removing premature
|
||||||
|
/// [finishAutofillContext] calls to prevent showing the save user input UI
|
||||||
|
/// too frequently. However, calling [finishAutofillContext] when there's no
|
||||||
|
/// existing autofill context usually does not bring up the save user input
|
||||||
|
/// UI.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [AutofillGroup.onDisposeAction], a configurable action that runs when a
|
||||||
|
/// topmost [AutofillGroup] is getting disposed.
|
||||||
|
static void finishAutofillContext({ bool shouldSave = true }) {
|
||||||
|
assert(shouldSave != null);
|
||||||
|
TextInput._instance._channel.invokeMethod<void>(
|
||||||
|
'TextInput.finishAutofillContext',
|
||||||
|
shouldSave ,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,26 @@ import 'framework.dart';
|
|||||||
|
|
||||||
export 'package:flutter/services.dart' show AutofillHints;
|
export 'package:flutter/services.dart' show AutofillHints;
|
||||||
|
|
||||||
|
/// Predefined autofill context clean up actions.
|
||||||
|
enum AutofillContextAction {
|
||||||
|
/// Destroys the current autofill context after informing the platform to save
|
||||||
|
/// the user input from it.
|
||||||
|
///
|
||||||
|
/// Corresponds to calling [TextInput.finishAutofillContext] with
|
||||||
|
/// `shouldSave == true`.
|
||||||
|
commit,
|
||||||
|
|
||||||
|
/// Destroys the current autofill context without saving the user input.
|
||||||
|
///
|
||||||
|
/// Corresponds to calling [TextInput.finishAutofillContext] with
|
||||||
|
/// `shouldSave == false`.
|
||||||
|
cancel,
|
||||||
|
}
|
||||||
|
|
||||||
/// An [AutofillScope] widget that groups [AutofillClient]s together.
|
/// An [AutofillScope] widget that groups [AutofillClient]s together.
|
||||||
///
|
///
|
||||||
/// [AutofillClient]s within the same [AutofillScope] must be built together, and
|
/// [AutofillClient]s that share the same closest [AutofillGroup] ancestor must
|
||||||
/// they be will be autofilled together.
|
/// be built together, and they be will be autofilled together.
|
||||||
///
|
///
|
||||||
/// {@macro flutter.services.autofill.AutofillScope}
|
/// {@macro flutter.services.autofill.AutofillScope}
|
||||||
///
|
///
|
||||||
@ -20,12 +36,27 @@ export 'package:flutter/services.dart' show AutofillHints;
|
|||||||
/// it using the [AutofillGroupState.register] API. Typically, [AutofillGroup]
|
/// it using the [AutofillGroupState.register] API. Typically, [AutofillGroup]
|
||||||
/// will not pick up [AutofillClient]s that are not mounted, for example, an
|
/// will not pick up [AutofillClient]s that are not mounted, for example, an
|
||||||
/// [AutofillClient] within a [Scrollable] that has never been scrolled into the
|
/// [AutofillClient] within a [Scrollable] that has never been scrolled into the
|
||||||
/// viewport. To workaround this problem, ensure clients in the same [AutofillGroup]
|
/// viewport. To workaround this problem, ensure clients in the same
|
||||||
/// are built together:
|
/// [AutofillGroup] are built together.
|
||||||
|
///
|
||||||
|
/// The topmost [AutofillGroup] widgets (the ones that are closest to the root
|
||||||
|
/// widget) can be used to clean up the current autofill context when the
|
||||||
|
/// current autofill context is no longer relevant.
|
||||||
|
///
|
||||||
|
/// {@macro flutter.services.autofill.autofillContext}
|
||||||
|
///
|
||||||
|
/// By default, [onDisposeAction] is set to [AutofillContextAction.commit], in
|
||||||
|
/// which case when any of the topmost [AutofillGroup]s is being disposed, the
|
||||||
|
/// platform will be informed to save the user input from the current autofill
|
||||||
|
/// context, then the current autofill context will be destroyed, to free
|
||||||
|
/// resources. You can, for example, wrap a route that contains a [Form] full of
|
||||||
|
/// autofillable input fields in an [AutofillGroup], so the user input of the
|
||||||
|
/// [Form] can be saved for future autofill by the platform.
|
||||||
///
|
///
|
||||||
/// {@tool dartpad --template=stateful_widget_scaffold}
|
/// {@tool dartpad --template=stateful_widget_scaffold}
|
||||||
///
|
///
|
||||||
/// An example form with autofillable fields grouped into different `AutofillGroup`s.
|
/// An example form with autofillable fields grouped into different
|
||||||
|
/// `AutofillGroup`s.
|
||||||
///
|
///
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// bool isSameAddress = true;
|
/// bool isSameAddress = true;
|
||||||
@ -44,8 +75,8 @@ export 'package:flutter/services.dart' show AutofillHints;
|
|||||||
/// return ListView(
|
/// return ListView(
|
||||||
/// children: <Widget>[
|
/// children: <Widget>[
|
||||||
/// const Text('Shipping address'),
|
/// const Text('Shipping address'),
|
||||||
/// // The address fields are grouped together as some platforms are capable
|
/// // The address fields are grouped together as some platforms are
|
||||||
/// // of autofilling all these fields in one go.
|
/// // capable of autofilling all of these fields in one go.
|
||||||
/// AutofillGroup(
|
/// AutofillGroup(
|
||||||
/// child: Column(
|
/// child: Column(
|
||||||
/// children: <Widget>[
|
/// children: <Widget>[
|
||||||
@ -83,8 +114,8 @@ export 'package:flutter/services.dart' show AutofillHints;
|
|||||||
/// ),
|
/// ),
|
||||||
/// ),
|
/// ),
|
||||||
/// const Text('Credit Card Information'),
|
/// const Text('Credit Card Information'),
|
||||||
/// // The credit card number and the security code are grouped together as
|
/// // The credit card number and the security code are grouped together
|
||||||
/// // some platforms are capable of autofilling both fields.
|
/// // as some platforms are capable of autofilling both fields.
|
||||||
/// AutofillGroup(
|
/// AutofillGroup(
|
||||||
/// child: Column(
|
/// child: Column(
|
||||||
/// children: <Widget>[
|
/// children: <Widget>[
|
||||||
@ -111,6 +142,11 @@ export 'package:flutter/services.dart' show AutofillHints;
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
/// {@end-tool}
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [AutofillContextAction], an enum that contains predefined autofill context
|
||||||
|
/// clean up actions to be run when a topmost [AutofillGroup] is disposed.
|
||||||
class AutofillGroup extends StatefulWidget {
|
class AutofillGroup extends StatefulWidget {
|
||||||
/// Creates a scope for autofillable input fields.
|
/// Creates a scope for autofillable input fields.
|
||||||
///
|
///
|
||||||
@ -118,6 +154,7 @@ class AutofillGroup extends StatefulWidget {
|
|||||||
const AutofillGroup({
|
const AutofillGroup({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.child,
|
@required this.child,
|
||||||
|
this.onDisposeAction = AutofillContextAction.commit,
|
||||||
}) : assert(child != null),
|
}) : assert(child != null),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@ -137,6 +174,17 @@ class AutofillGroup extends StatefulWidget {
|
|||||||
/// {@macro flutter.widgets.child}
|
/// {@macro flutter.widgets.child}
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
|
/// The [AutofillContextAction] to be run when this [AutofillGroup] is the
|
||||||
|
/// topmost [AutofillGroup] and it's being disposed, in order to clean up the
|
||||||
|
/// current autofill context.
|
||||||
|
///
|
||||||
|
/// {@macro flutter.services.autofill.autofillContext}
|
||||||
|
///
|
||||||
|
/// Defaults to [AutofillContextAction.commit], which prompts the platform to
|
||||||
|
/// save the user input and destroy the current autofill context. No action
|
||||||
|
/// will be taken if [onDisposeAction] is set to null.
|
||||||
|
final AutofillContextAction onDisposeAction;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AutofillGroupState createState() => AutofillGroupState();
|
AutofillGroupState createState() => AutofillGroupState();
|
||||||
}
|
}
|
||||||
@ -160,6 +208,11 @@ class AutofillGroup extends StatefulWidget {
|
|||||||
class AutofillGroupState extends State<AutofillGroup> with AutofillScopeMixin {
|
class AutofillGroupState extends State<AutofillGroup> with AutofillScopeMixin {
|
||||||
final Map<String, AutofillClient> _clients = <String, AutofillClient>{};
|
final Map<String, AutofillClient> _clients = <String, AutofillClient>{};
|
||||||
|
|
||||||
|
// Whether this AutofillGroup widget is the topmost AutofillGroup (i.e., it
|
||||||
|
// has no AutofillGroup ancestor). Each topmost AutofillGroup runs its
|
||||||
|
// `AutofillGroup.onDisposeAction` when it gets disposed.
|
||||||
|
bool _isTopmostAutofillGroup = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AutofillClient getAutofillClient(String tag) => _clients[tag];
|
AutofillClient getAutofillClient(String tag) => _clients[tag];
|
||||||
|
|
||||||
@ -184,7 +237,7 @@ class AutofillGroupState extends State<AutofillGroup> with AutofillScopeMixin {
|
|||||||
_clients.putIfAbsent(client.autofillId, () => client);
|
_clients.putIfAbsent(client.autofillId, () => client);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 autofillable [TextInputClient]s in
|
||||||
@ -203,6 +256,12 @@ class AutofillGroupState extends State<AutofillGroup> with AutofillScopeMixin {
|
|||||||
_clients.remove(autofillId);
|
_clients.remove(autofillId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_isTopmostAutofillGroup = AutofillGroup.of(context) == null;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return _AutofillScope(
|
return _AutofillScope(
|
||||||
@ -210,6 +269,22 @@ class AutofillGroupState extends State<AutofillGroup> with AutofillScopeMixin {
|
|||||||
child: widget.child,
|
child: widget.child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
|
||||||
|
if (!_isTopmostAutofillGroup || widget.onDisposeAction == null)
|
||||||
|
return;
|
||||||
|
switch (widget.onDisposeAction) {
|
||||||
|
case AutofillContextAction.cancel:
|
||||||
|
TextInput.finishAutofillContext(shouldSave: false);
|
||||||
|
break;
|
||||||
|
case AutofillContextAction.commit:
|
||||||
|
TextInput.finishAutofillContext(shouldSave: true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AutofillScope extends InheritedWidget {
|
class _AutofillScope extends InheritedWidget {
|
||||||
|
@ -1410,6 +1410,9 @@ 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.
|
||||||
|
bool _isInAutofillContext = false;
|
||||||
|
|
||||||
// This value is an eyeball estimation of the time it takes for the iOS cursor
|
// This value is an eyeball estimation of the time it takes for the iOS cursor
|
||||||
// to ease in and out.
|
// to ease in and out.
|
||||||
static const Duration _fadeDuration = Duration(milliseconds: 250);
|
static const Duration _fadeDuration = Duration(milliseconds: 250);
|
||||||
@ -1470,6 +1473,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
_currentAutofillScope?.unregister(autofillId);
|
_currentAutofillScope?.unregister(autofillId);
|
||||||
_currentAutofillScope = newAutofillGroup;
|
_currentAutofillScope = newAutofillGroup;
|
||||||
newAutofillGroup?.register(this);
|
newAutofillGroup?.register(this);
|
||||||
|
_isInAutofillContext = _isInAutofillContext || _shouldBeInAutofillContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_didAutoFocus && widget.autofocus) {
|
if (!_didAutoFocus && widget.autofocus) {
|
||||||
@ -1494,6 +1498,8 @@ 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.focusNode != oldWidget.focusNode) {
|
if (widget.focusNode != oldWidget.focusNode) {
|
||||||
oldWidget.focusNode.removeListener(_handleFocusChanged);
|
oldWidget.focusNode.removeListener(_handleFocusChanged);
|
||||||
_focusAttachment?.detach();
|
_focusAttachment?.detach();
|
||||||
@ -1776,6 +1782,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool get _hasInputConnection => _textInputConnection != null && _textInputConnection.attached;
|
bool get _hasInputConnection => _textInputConnection != null && _textInputConnection.attached;
|
||||||
|
bool get _needsAutofill => widget.autofillHints?.isNotEmpty ?? false;
|
||||||
|
bool get _shouldBeInAutofillContext => _needsAutofill && currentAutofillScope != null;
|
||||||
|
|
||||||
void _openInputConnection() {
|
void _openInputConnection() {
|
||||||
if (widget.readOnly) {
|
if (widget.readOnly) {
|
||||||
@ -1785,14 +1793,24 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
final TextEditingValue localValue = _value;
|
final TextEditingValue localValue = _value;
|
||||||
_lastFormattedUnmodifiedTextEditingValue = localValue;
|
_lastFormattedUnmodifiedTextEditingValue = localValue;
|
||||||
|
|
||||||
_textInputConnection = (widget.autofillHints?.isNotEmpty ?? false) && currentAutofillScope != null
|
// When _needsAutofill == true && currentAutofillScope == null, autofill
|
||||||
|
// is allowed but saving the user input from the text field is
|
||||||
|
// discouraged.
|
||||||
|
//
|
||||||
|
// In case the autofillScope changes from a non-null value to null, or
|
||||||
|
// _needsAutofill changes to false from true, the platform needs to be
|
||||||
|
// notified to exclude this field from the autofill context. So we need to
|
||||||
|
// provide the autofillId.
|
||||||
|
_textInputConnection = _needsAutofill && currentAutofillScope != null
|
||||||
? currentAutofillScope.attach(this, textInputConfiguration)
|
? currentAutofillScope.attach(this, textInputConfiguration)
|
||||||
: TextInput.attach(this, textInputConfiguration);
|
: TextInput.attach(this, _createTextInputConfiguration(_isInAutofillContext || _needsAutofill));
|
||||||
_textInputConnection.show();
|
_textInputConnection.show();
|
||||||
_updateSizeAndTransform();
|
_updateSizeAndTransform();
|
||||||
// Request autofill AFTER the size and the transform have been sent to the
|
if (_needsAutofill) {
|
||||||
// platform side.
|
// Request autofill AFTER the size and the transform have been sent to
|
||||||
|
// the platform text input plugin.
|
||||||
_textInputConnection.requestAutofill();
|
_textInputConnection.requestAutofill();
|
||||||
|
}
|
||||||
|
|
||||||
final TextStyle style = widget.style;
|
final TextStyle style = widget.style;
|
||||||
_textInputConnection
|
_textInputConnection
|
||||||
@ -2223,9 +2241,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
@override
|
@override
|
||||||
String get autofillId => 'EditableText-$hashCode';
|
String get autofillId => 'EditableText-$hashCode';
|
||||||
|
|
||||||
@override
|
TextInputConfiguration _createTextInputConfiguration(bool needsAutofillConfiguration) {
|
||||||
TextInputConfiguration get textInputConfiguration {
|
assert(needsAutofillConfiguration != null);
|
||||||
final bool isAutofillEnabled = widget.autofillHints?.isNotEmpty ?? false;
|
|
||||||
return TextInputConfiguration(
|
return TextInputConfiguration(
|
||||||
inputType: widget.keyboardType,
|
inputType: widget.keyboardType,
|
||||||
obscureText: widget.obscureText,
|
obscureText: widget.obscureText,
|
||||||
@ -2239,14 +2256,19 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
),
|
),
|
||||||
textCapitalization: widget.textCapitalization,
|
textCapitalization: widget.textCapitalization,
|
||||||
keyboardAppearance: widget.keyboardAppearance,
|
keyboardAppearance: widget.keyboardAppearance,
|
||||||
autofillConfiguration: !isAutofillEnabled ? null : AutofillConfiguration(
|
autofillConfiguration: !needsAutofillConfiguration ? null : AutofillConfiguration(
|
||||||
uniqueIdentifier: autofillId,
|
uniqueIdentifier: autofillId,
|
||||||
autofillHints: widget.autofillHints.toList(growable: false),
|
autofillHints: widget.autofillHints?.toList(growable: false) ?? <String>[],
|
||||||
currentEditingValue: currentTextEditingValue,
|
currentEditingValue: currentTextEditingValue,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TextInputConfiguration get textInputConfiguration {
|
||||||
|
return _createTextInputConfiguration(_needsAutofill);
|
||||||
|
}
|
||||||
|
|
||||||
// null if no promptRect should be shown.
|
// null if no promptRect should be shown.
|
||||||
TextRange _currentPromptRectRange;
|
TextRange _currentPromptRectRange;
|
||||||
|
|
||||||
|
@ -5,8 +5,12 @@
|
|||||||
// @dart = 2.8
|
// @dart = 2.8
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
final Matcher _matchesCommit = isMethodCall('TextInput.finishAutofillContext', arguments: true);
|
||||||
|
final Matcher _matchesCancel = isMethodCall('TextInput.finishAutofillContext', arguments: false);
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('AutofillGroup has the right clients', (WidgetTester tester) async {
|
testWidgets('AutofillGroup has the right clients', (WidgetTester tester) async {
|
||||||
const Key outerKey = Key('outer');
|
const Key outerKey = Key('outer');
|
||||||
@ -43,16 +47,15 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(outerState.autofillClients, <EditableTextState>[clientState1]);
|
expect(outerState.autofillClients, <EditableTextState>[clientState1]);
|
||||||
|
// The second TextField doesn't have autofill enabled.
|
||||||
expect(innerState.autofillClients, <EditableTextState>[clientState2]);
|
expect(innerState.autofillClients, <EditableTextState>[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');
|
||||||
|
|
||||||
final List<String> hints = <String>[];
|
|
||||||
|
|
||||||
const TextField client1 = TextField(autofillHints: <String>['1']);
|
const TextField client1 = TextField(autofillHints: <String>['1']);
|
||||||
final TextField client2 = TextField(autofillHints: hints);
|
TextField client2 = const TextField(autofillHints: <String>[]);
|
||||||
|
|
||||||
StateSetter setState;
|
StateSetter setState;
|
||||||
|
|
||||||
@ -84,16 +87,16 @@ void main() {
|
|||||||
expect(scopeState.autofillClients, <EditableTextState>[clientState1]);
|
expect(scopeState.autofillClients, <EditableTextState>[clientState1]);
|
||||||
|
|
||||||
// Add to scope.
|
// Add to scope.
|
||||||
setState(() { hints.add('2'); });
|
setState(() { client2 = const TextField(autofillHints: <String>['2']); });
|
||||||
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(scopeState.autofillClients.length, 2);
|
|
||||||
expect(scopeState.autofillClients, contains(clientState1));
|
expect(scopeState.autofillClients, contains(clientState1));
|
||||||
expect(scopeState.autofillClients, contains(clientState2));
|
expect(scopeState.autofillClients, contains(clientState2));
|
||||||
|
expect(scopeState.autofillClients.length, 2);
|
||||||
|
|
||||||
// Remove from scope again.
|
// Remove from scope again.
|
||||||
setState(() { hints.clear(); });
|
setState(() { client2 = const TextField(autofillHints: <String>[]); });
|
||||||
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
@ -165,4 +168,120 @@ void main() {
|
|||||||
expect(outerState.autofillClients, contains(clientState3));
|
expect(outerState.autofillClients, contains(clientState3));
|
||||||
expect(innerState.autofillClients, <EditableTextState>[clientState2]);
|
expect(innerState.autofillClients, <EditableTextState>[clientState2]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('disposing AutofillGroups', (WidgetTester tester) async {
|
||||||
|
StateSetter setState;
|
||||||
|
const Key group1 = Key('group1');
|
||||||
|
const Key group2 = Key('group2');
|
||||||
|
const Key group3 = Key('group3');
|
||||||
|
const TextField placeholder = TextField(autofillHints: <String>[AutofillHints.name]);
|
||||||
|
|
||||||
|
List<Widget> children = const <Widget> [
|
||||||
|
AutofillGroup(
|
||||||
|
key: group1,
|
||||||
|
onDisposeAction: AutofillContextAction.commit,
|
||||||
|
child: AutofillGroup(child: placeholder),
|
||||||
|
),
|
||||||
|
AutofillGroup(key: group2, onDisposeAction: AutofillContextAction.cancel, child: placeholder),
|
||||||
|
AutofillGroup(
|
||||||
|
key: group3,
|
||||||
|
onDisposeAction: AutofillContextAction.commit,
|
||||||
|
child: AutofillGroup(child: placeholder),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: StatefulBuilder(
|
||||||
|
builder: (BuildContext context, StateSetter setter) {
|
||||||
|
setState = setter;
|
||||||
|
return Column(children: children);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
tester.testTextInput.log,
|
||||||
|
isNot(contains(_matchesCommit)),
|
||||||
|
);
|
||||||
|
|
||||||
|
tester.testTextInput.log.clear();
|
||||||
|
|
||||||
|
// Remove the first topmost group group1. Should commit.
|
||||||
|
setState(() {
|
||||||
|
children = const <Widget> [
|
||||||
|
AutofillGroup(key: group2, onDisposeAction: AutofillContextAction.cancel, child: placeholder),
|
||||||
|
AutofillGroup(
|
||||||
|
key: group3,
|
||||||
|
onDisposeAction: AutofillContextAction.commit,
|
||||||
|
child: AutofillGroup(child: placeholder),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
tester.testTextInput.log.single,
|
||||||
|
_matchesCommit,
|
||||||
|
);
|
||||||
|
|
||||||
|
tester.testTextInput.log.clear();
|
||||||
|
|
||||||
|
// Remove the topmost group group2. Should cancel.
|
||||||
|
setState(() {
|
||||||
|
children = const <Widget> [
|
||||||
|
AutofillGroup(
|
||||||
|
key: group3,
|
||||||
|
onDisposeAction: AutofillContextAction.commit,
|
||||||
|
child: AutofillGroup(child: placeholder),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
tester.testTextInput.log.single,
|
||||||
|
_matchesCancel,
|
||||||
|
);
|
||||||
|
|
||||||
|
tester.testTextInput.log.clear();
|
||||||
|
|
||||||
|
// Remove the inner group within group3. No action.
|
||||||
|
setState(() {
|
||||||
|
children = const <Widget> [
|
||||||
|
AutofillGroup(
|
||||||
|
key: group3,
|
||||||
|
onDisposeAction: AutofillContextAction.commit,
|
||||||
|
child: placeholder,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
tester.testTextInput.log,
|
||||||
|
isNot(contains('TextInput.finishAutofillContext')),
|
||||||
|
);
|
||||||
|
|
||||||
|
tester.testTextInput.log.clear();
|
||||||
|
|
||||||
|
// Remove the topmosts group group3. Should commit.
|
||||||
|
setState(() {
|
||||||
|
children = const <Widget> [
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
tester.testTextInput.log.single,
|
||||||
|
_matchesCommit,
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -4407,13 +4407,12 @@ void main() {
|
|||||||
'TextInput.setClient',
|
'TextInput.setClient',
|
||||||
'TextInput.show',
|
'TextInput.show',
|
||||||
'TextInput.setEditableSizeAndTransform',
|
'TextInput.setEditableSizeAndTransform',
|
||||||
'TextInput.requestAutofill',
|
|
||||||
'TextInput.setStyle',
|
'TextInput.setStyle',
|
||||||
'TextInput.setEditingState',
|
'TextInput.setEditingState',
|
||||||
'TextInput.setEditingState',
|
'TextInput.setEditingState',
|
||||||
'TextInput.show',
|
'TextInput.show',
|
||||||
];
|
];
|
||||||
expect(tester.testTextInput.log.length, 8);
|
expect(tester.testTextInput.log.length, 7);
|
||||||
int index = 0;
|
int index = 0;
|
||||||
for (final MethodCall m in tester.testTextInput.log) {
|
for (final MethodCall m in tester.testTextInput.log) {
|
||||||
expect(m.method, logOrder[index]);
|
expect(m.method, logOrder[index]);
|
||||||
@ -4452,7 +4451,6 @@ void main() {
|
|||||||
'TextInput.setClient',
|
'TextInput.setClient',
|
||||||
'TextInput.show',
|
'TextInput.show',
|
||||||
'TextInput.setEditableSizeAndTransform',
|
'TextInput.setEditableSizeAndTransform',
|
||||||
'TextInput.requestAutofill',
|
|
||||||
'TextInput.setStyle',
|
'TextInput.setStyle',
|
||||||
'TextInput.setEditingState',
|
'TextInput.setEditingState',
|
||||||
'TextInput.setEditingState',
|
'TextInput.setEditingState',
|
||||||
@ -4500,7 +4498,6 @@ void main() {
|
|||||||
'TextInput.setClient',
|
'TextInput.setClient',
|
||||||
'TextInput.show',
|
'TextInput.show',
|
||||||
'TextInput.setEditableSizeAndTransform',
|
'TextInput.setEditableSizeAndTransform',
|
||||||
'TextInput.requestAutofill',
|
|
||||||
'TextInput.setStyle',
|
'TextInput.setStyle',
|
||||||
'TextInput.setEditingState',
|
'TextInput.setEditingState',
|
||||||
'TextInput.setEditingState',
|
'TextInput.setEditingState',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user