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

Autofill save (#58731)

parent 3215a013
...@@ -807,13 +807,11 @@ mixin AutofillScopeMixin implements AutofillScope { ...@@ -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 { ...@@ -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 { ...@@ -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'; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 @@ ...@@ -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() { ...@@ -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() { ...@@ -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() { ...@@ -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() { ...@@ -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() { ...@@ -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() { ...@@ -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',
......
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