blob: 570482cb0827d3d837f5ca775717a3be59aeba70 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/services.dart';
import 'framework.dart';
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.
///
/// [AutofillClient]s that share the same closest [AutofillGroup] ancestor must
/// be built together, and they be will be autofilled together.
///
/// {@macro flutter.services.AutofillScope}
///
/// The [AutofillGroup] widget only knows about [AutofillClient]s registered to
/// it using the [AutofillGroupState.register] API. Typically, [AutofillGroup]
/// will not pick up [AutofillClient]s that are not mounted, for example, an
/// [AutofillClient] within a [Scrollable] that has never been scrolled into the
/// viewport. To workaround this problem, ensure clients in the same
/// [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.TextInput.finishAutofillContext}
///
/// 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}
/// An example form with autofillable fields grouped into different
/// [AutofillGroup]s.
///
/// ** See code in examples/api/lib/widgets/autofill/autofill_group.0.dart **
/// {@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 {
/// Creates a scope for autofillable input fields.
///
/// The [child] argument must not be null.
const AutofillGroup({
super.key,
required this.child,
this.onDisposeAction = AutofillContextAction.commit,
}) : assert(child != null);
/// Returns the closest [AutofillGroupState] which encloses the given context.
///
/// {@macro flutter.widgets.AutofillGroupState}
///
/// See also:
///
/// * [EditableTextState], where this method is used to retrieve the closest
/// [AutofillGroupState].
static AutofillGroupState? of(BuildContext context) {
final _AutofillScope? scope = context.dependOnInheritedWidgetOfExactType<_AutofillScope>();
return scope?._scope;
}
/// {@macro flutter.widgets.ProxyWidget.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.TextInput.finishAutofillContext}
///
/// Defaults to [AutofillContextAction.commit], which prompts the platform to
/// save the user input and destroy the current autofill context.
final AutofillContextAction onDisposeAction;
@override
AutofillGroupState createState() => AutofillGroupState();
}
/// State associated with an [AutofillGroup] widget.
///
/// {@template flutter.widgets.AutofillGroupState}
/// An [AutofillGroupState] can be used to register an [AutofillClient] when it
/// enters this [AutofillGroup] (for example, when an [EditableText] is mounted or
/// reparented onto the [AutofillGroup]'s subtree), and unregister an
/// [AutofillClient] when it exits (for example, when an [EditableText] gets
/// unmounted or reparented out of the [AutofillGroup]'s subtree).
///
/// The [AutofillGroupState] class also provides an [AutofillGroupState.attach]
/// method that can be called by [TextInputClient]s that support autofill,
/// instead of [TextInput.attach], to create a [TextInputConnection] to interact
/// with the platform's text input system.
/// {@endtemplate}
///
/// Typically obtained using [AutofillGroup.of].
class AutofillGroupState extends State<AutofillGroup> with AutofillScopeMixin {
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
AutofillClient? getAutofillClient(String autofillId) => _clients[autofillId];
@override
Iterable<AutofillClient> get autofillClients {
return _clients.values
.where((AutofillClient client) => client.textInputConfiguration.autofillConfiguration.enabled);
}
/// Adds the [AutofillClient] to this [AutofillGroup].
///
/// Typically, this is called by [TextInputClient]s that support autofill (for
/// example, [EditableTextState]) in [State.didChangeDependencies], when the
/// input field should be registered to a new [AutofillGroup].
///
/// See also:
///
/// * [EditableTextState.didChangeDependencies], where this method is called
/// to update the current [AutofillScope] when needed.
void register(AutofillClient client) {
assert(client != null);
_clients.putIfAbsent(client.autofillId, () => client);
}
/// Removes an [AutofillClient] with the given `autofillId` from this
/// [AutofillGroup].
///
/// 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:
///
/// * [EditableTextState.didChangeDependencies], where this method is called
/// to unregister from the previous [AutofillScope].
/// * [EditableTextState.dispose], where this method is called to unregister
/// from the current [AutofillScope] when the widget is about to be removed
/// from the tree.
void unregister(String autofillId) {
assert(autofillId != null && _clients.containsKey(autofillId));
_clients.remove(autofillId);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_isTopmostAutofillGroup = AutofillGroup.of(context) == null;
}
@override
Widget build(BuildContext context) {
return _AutofillScope(
autofillScopeState: this,
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();
break;
}
}
}
class _AutofillScope extends InheritedWidget {
const _AutofillScope({
required super.child,
AutofillGroupState? autofillScopeState,
}) : _scope = autofillScopeState;
final AutofillGroupState? _scope;
AutofillGroup get client => _scope!.widget;
@override
bool updateShouldNotify(_AutofillScope old) => _scope != old._scope;
}