blob: 6aa00558d80e01d32d4c8889f04e8abf191ab3bf [file] [log] [blame] [edit]
// 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.
/// @docImport 'package:flutter/material.dart';
library;
import 'dart:async';
import 'dart:math' as math show max;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'actions.dart';
import 'basic.dart';
import 'constants.dart';
import 'editable_text.dart';
import 'focus_manager.dart';
import 'framework.dart';
import 'inherited_notifier.dart';
import 'localizations.dart';
import 'media_query.dart';
import 'overlay.dart';
import 'shortcuts.dart';
import 'tap_region.dart';
// Examples can assume:
// late BuildContext context;
/// The type of the [RawAutocomplete] callback which computes the list of
/// optional completions for the widget's field, based on the text the user has
/// entered so far.
///
/// See also:
///
/// * [RawAutocomplete.optionsBuilder], which is of this type.
typedef AutocompleteOptionsBuilder<T extends Object> =
FutureOr<Iterable<T>> Function(TextEditingValue textEditingValue);
/// The type of the callback used by the [RawAutocomplete] widget to indicate
/// that the user has selected an option.
///
/// See also:
///
/// * [RawAutocomplete.onSelected], which is of this type.
typedef AutocompleteOnSelected<T extends Object> = void Function(T option);
/// The type of the [RawAutocomplete] callback which returns a [Widget] that
/// displays the specified [options] and calls [onSelected] if the user
/// selects an option.
///
/// The returned widget from this callback will be wrapped in an
/// [AutocompleteHighlightedOption] inherited widget. This will allow
/// this callback to determine which option is currently highlighted for
/// keyboard navigation.
///
/// See also:
///
/// * [RawAutocomplete.optionsViewBuilder], which is of this type.
typedef AutocompleteOptionsViewBuilder<T extends Object> =
Widget Function(
BuildContext context,
AutocompleteOnSelected<T> onSelected,
Iterable<T> options,
);
/// The type of the Autocomplete callback which returns the widget that
/// contains the input [TextField] or [TextFormField].
///
/// See also:
///
/// * [RawAutocomplete.fieldViewBuilder], which is of this type.
typedef AutocompleteFieldViewBuilder =
Widget Function(
BuildContext context,
TextEditingController textEditingController,
FocusNode focusNode,
VoidCallback onFieldSubmitted,
);
/// The type of the [RawAutocomplete] callback that converts an option value to
/// a string which can be displayed in the widget's options menu.
///
/// See also:
///
/// * [RawAutocomplete.displayStringForOption], which is of this type.
typedef AutocompleteOptionToString<T extends Object> = String Function(T option);
/// A direction in which to open the options-view overlay.
///
/// See also:
///
/// * [RawAutocomplete.optionsViewOpenDirection], which is of this type.
/// * [RawAutocomplete.optionsViewBuilder] to specify how to build the
/// selectable-options widget.
/// * [RawAutocomplete.fieldViewBuilder] to optionally specify how to build the
/// corresponding field widget.
enum OptionsViewOpenDirection {
/// Open upward.
///
/// The bottom edge of the options view will align with the top edge
/// of the text field built by [RawAutocomplete.fieldViewBuilder].
up,
/// Open downward.
///
/// The top edge of the options view will align with the bottom edge
/// of the text field built by [RawAutocomplete.fieldViewBuilder].
down,
/// Open in the direction with the most available space within the overlay.
///
/// The available space is calculated as the distance from the field's top
/// edge to the overlay's top edge (for upward opening) or from the field's
/// bottom edge to the overlay's bottom edge (for downward opening).
///
/// If both directions have the same available space, the options view opens
/// downward.
mostSpace,
}
// TODO(justinmc): Mention AutocompleteCupertino when it is implemented.
/// {@template flutter.widgets.RawAutocomplete.RawAutocomplete}
/// A widget for helping the user make a selection by entering some text and
/// choosing from among a list of options.
///
/// The user's text input is received in a field built with the
/// [fieldViewBuilder] parameter. The options to be displayed are determined
/// using [optionsBuilder] and rendered with [optionsViewBuilder].
///
/// The options view opens when the field gains focus or when the field's text
/// changes, as long as [optionsBuilder] returns at least one option. The options
/// view closes when the user selects an option, when there are no matching
/// options, or when the field loses focus.
/// {@endtemplate}
///
/// This is a core framework widget with very basic UI.
///
/// {@tool dartpad}
/// This example shows how to create a very basic autocomplete widget using the
/// [fieldViewBuilder] and [optionsViewBuilder] parameters.
///
/// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.0.dart **
/// {@end-tool}
///
/// The type parameter T represents the type of the options. Most commonly this
/// is a String, as in the example above. However, it's also possible to use
/// another type with a `toString` method, or a custom [displayStringForOption].
/// Options will be compared using `==`, so it may be beneficial to override
/// [Object.==] and [Object.hashCode] for custom types.
///
/// {@tool dartpad}
/// This example is similar to the previous example, but it uses a custom T data
/// type instead of directly using String.
///
/// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.1.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This example shows the use of RawAutocomplete in a form.
///
/// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.2.dart **
/// {@end-tool}
///
/// See also:
///
/// * [Autocomplete], which is a Material-styled implementation that is based
/// on RawAutocomplete.
class RawAutocomplete<T extends Object> extends StatefulWidget {
/// Create an instance of RawAutocomplete.
///
/// [displayStringForOption], [optionsBuilder] and [optionsViewBuilder] must
/// not be null.
const RawAutocomplete({
super.key,
required this.optionsViewBuilder,
required this.optionsBuilder,
this.optionsViewOpenDirection = OptionsViewOpenDirection.down,
this.displayStringForOption = defaultStringForOption,
this.fieldViewBuilder,
this.focusNode,
this.onSelected,
this.textEditingController,
this.initialValue,
}) : assert(
fieldViewBuilder != null ||
(key != null && focusNode != null && textEditingController != null),
'Pass in a fieldViewBuilder, or otherwise create a separate field and pass in the FocusNode, TextEditingController, and a key. Use the key with RawAutocomplete.onFieldSubmitted.',
),
assert((focusNode == null) == (textEditingController == null)),
assert(
!(textEditingController != null && initialValue != null),
'textEditingController and initialValue cannot be simultaneously defined.',
);
/// {@template flutter.widgets.RawAutocomplete.fieldViewBuilder}
/// Builds the field whose input is used to get the options.
///
/// Pass the provided [TextEditingController] to the field built here so that
/// RawAutocomplete can listen for changes.
/// {@endtemplate}
///
/// If this parameter is null, then a [SizedBox.shrink] is built instead.
/// For how that pattern can be useful, see [textEditingController].
final AutocompleteFieldViewBuilder? fieldViewBuilder;
/// The [FocusNode] that is used for the text field.
///
/// {@template flutter.widgets.RawAutocomplete.split}
/// The main purpose of this parameter is to allow the use of a separate text
/// field located in another part of the widget tree instead of the text
/// field built by [fieldViewBuilder]. For example, it may be desirable to
/// place the text field in the AppBar and the options below in the main body.
///
/// When following this pattern, [fieldViewBuilder] can be omitted,
/// so that a text field is not drawn where it would normally be.
/// A separate text field can be created elsewhere, and a
/// FocusNode and TextEditingController can be passed both to that text field
/// and to RawAutocomplete.
///
/// {@tool dartpad}
/// This examples shows how to create an autocomplete widget with the text
/// field in the AppBar and the results in the main body of the app.
///
/// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.focus_node.0.dart **
/// {@end-tool}
/// {@endtemplate}
///
/// If this parameter is not null, then [textEditingController] must also be
/// non-null.
final FocusNode? focusNode;
/// {@template flutter.widgets.RawAutocomplete.optionsViewBuilder}
/// Builds the selectable options widgets from a list of options objects.
///
/// The options are displayed floating below or above the field inside of an
/// [Overlay], not at the same place in the widget tree as [RawAutocomplete].
/// To control whether it opens upward or downward, use
/// [optionsViewOpenDirection].
///
/// In order to track which item is highlighted by keyboard navigation, the
/// resulting options will be wrapped in an inherited
/// [AutocompleteHighlightedOption] widget.
/// Inside this callback, the index of the highlighted option can be obtained
/// from [AutocompleteHighlightedOption.of] to display the highlighted option
/// with a visual highlight to indicate it will be the option selected from
/// the keyboard.
///
/// {@endtemplate}
final AutocompleteOptionsViewBuilder<T> optionsViewBuilder;
/// {@template flutter.widgets.RawAutocomplete.optionsViewOpenDirection}
/// Determines the direction in which to open the options view.
///
/// Defaults to [OptionsViewOpenDirection.down].
/// {@endtemplate}
final OptionsViewOpenDirection optionsViewOpenDirection;
/// {@template flutter.widgets.RawAutocomplete.displayStringForOption}
/// Returns the string to display in the field when the option is selected.
///
/// This is useful when using a custom T type and the string to display is
/// different than the string to search by.
///
/// If not provided, will use `option.toString()`.
/// {@endtemplate}
final AutocompleteOptionToString<T> displayStringForOption;
/// {@template flutter.widgets.RawAutocomplete.onSelected}
/// Called when an option is selected by the user.
/// {@endtemplate}
final AutocompleteOnSelected<T>? onSelected;
/// {@template flutter.widgets.RawAutocomplete.optionsBuilder}
/// A function that returns the current selectable options objects given the
/// current TextEditingValue.
/// {@endtemplate}
final AutocompleteOptionsBuilder<T> optionsBuilder;
/// The [TextEditingController] that is used for the text field.
///
/// {@macro flutter.widgets.RawAutocomplete.split}
///
/// If this parameter is not null, then [focusNode] must also be non-null.
final TextEditingController? textEditingController;
/// {@template flutter.widgets.RawAutocomplete.initialValue}
/// The initial value to use for the text field.
/// {@endtemplate}
///
/// Setting the initial value does not notify [textEditingController]'s
/// listeners, and thus will not cause the options UI to appear.
///
/// This parameter is ignored if [textEditingController] is defined.
final TextEditingValue? initialValue;
/// Calls [AutocompleteFieldViewBuilder]'s onFieldSubmitted callback for the
/// RawAutocomplete widget indicated by the given [GlobalKey].
///
/// This is not typically used unless a custom field is implemented instead of
/// using [fieldViewBuilder]. In the typical case, the onFieldSubmitted
/// callback is passed via the [AutocompleteFieldViewBuilder] signature. When
/// not using fieldViewBuilder, the same callback can be called by using this
/// static method.
///
/// See also:
///
/// * [focusNode] and [textEditingController], which contain a code example
/// showing how to create a separate field outside of fieldViewBuilder.
static void onFieldSubmitted<T extends Object>(GlobalKey key) {
final rawAutocomplete = key.currentState! as _RawAutocompleteState<T>;
rawAutocomplete._onFieldSubmitted();
}
/// The default way to convert an option to a string in
/// [displayStringForOption].
///
/// Uses the `toString` method of the given `option`.
static String defaultStringForOption(Object? option) {
return option.toString();
}
@override
State<RawAutocomplete<T>> createState() => _RawAutocompleteState<T>();
}
class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>> {
final OverlayPortalController _optionsViewController = OverlayPortalController(
debugLabel: '_RawAutocompleteState',
);
// The number of options to scroll by "page", such as when using the page
// up/down keys.
static const int _pageSize = 4;
/// Whether the field currently has focus.
///
/// This is used to determine whether the focus state has changed.
late bool _hasFocus;
/// Whether an option is currently being selected.
bool _selecting = false;
TextEditingController? _internalTextEditingController;
TextEditingController get _textEditingController {
return widget.textEditingController ??
(_internalTextEditingController ??= TextEditingController()..addListener(_onChangedField));
}
FocusNode? _internalFocusNode;
FocusNode get _focusNode {
return widget.focusNode ?? (_internalFocusNode ??= FocusNode()..addListener(_onFocusChange));
}
late final Map<Type, CallbackAction<Intent>> _actionMap = <Type, CallbackAction<Intent>>{
AutocompletePreviousOptionIntent: _AutocompleteCallbackAction<AutocompletePreviousOptionIntent>(
onInvoke: _highlightPreviousOption,
isEnabledCallback: () => _canShowOptionsView,
),
AutocompleteNextOptionIntent: _AutocompleteCallbackAction<AutocompleteNextOptionIntent>(
onInvoke: _highlightNextOption,
isEnabledCallback: () => _canShowOptionsView,
),
AutocompleteFirstOptionIntent: _AutocompleteCallbackAction<AutocompleteFirstOptionIntent>(
onInvoke: _highlightFirstOption,
isEnabledCallback: () => _canShowOptionsView,
),
AutocompleteLastOptionIntent: _AutocompleteCallbackAction<AutocompleteLastOptionIntent>(
onInvoke: _highlightLastOption,
isEnabledCallback: () => _canShowOptionsView,
),
AutocompleteNextPageOptionIntent: _AutocompleteCallbackAction<AutocompleteNextPageOptionIntent>(
onInvoke: _highlightNextPageOption,
isEnabledCallback: () => _canShowOptionsView,
),
AutocompletePreviousPageOptionIntent:
_AutocompleteCallbackAction<AutocompletePreviousPageOptionIntent>(
onInvoke: _highlightPreviousPageOption,
isEnabledCallback: () => _canShowOptionsView,
),
DismissIntent: CallbackAction<DismissIntent>(onInvoke: _hideOptions),
};
Iterable<T> _options = Iterable<T>.empty();
T? _selection;
// Set the initial value to null so when this widget gets focused for the first
// time it will try to run the options view builder.
String? _lastFieldText;
final ValueNotifier<int> _highlightedOptionIndex = ValueNotifier<int>(0);
static const Map<ShortcutActivator, Intent> _appleShortcuts = <ShortcutActivator, Intent>{
SingleActivator(LogicalKeyboardKey.arrowUp, meta: true): AutocompleteFirstOptionIntent(),
SingleActivator(LogicalKeyboardKey.arrowDown, meta: true): AutocompleteLastOptionIntent(),
};
static const Map<ShortcutActivator, Intent> _nonAppleShortcuts = <ShortcutActivator, Intent>{
SingleActivator(LogicalKeyboardKey.arrowUp, control: true): AutocompleteFirstOptionIntent(),
SingleActivator(LogicalKeyboardKey.arrowDown, control: true): AutocompleteLastOptionIntent(),
};
static const Map<ShortcutActivator, Intent> _commonShortcuts = <ShortcutActivator, Intent>{
SingleActivator(LogicalKeyboardKey.arrowUp): AutocompletePreviousOptionIntent(),
SingleActivator(LogicalKeyboardKey.arrowDown): AutocompleteNextOptionIntent(),
SingleActivator(LogicalKeyboardKey.pageUp): AutocompletePreviousPageOptionIntent(),
SingleActivator(LogicalKeyboardKey.pageDown): AutocompleteNextPageOptionIntent(),
};
static Map<ShortcutActivator, Intent> get _shortcuts => <ShortcutActivator, Intent>{
..._commonShortcuts,
...switch (defaultTargetPlatform) {
TargetPlatform.iOS => _appleShortcuts,
TargetPlatform.macOS => _appleShortcuts,
TargetPlatform.android => _nonAppleShortcuts,
TargetPlatform.linux => _nonAppleShortcuts,
TargetPlatform.windows => _nonAppleShortcuts,
TargetPlatform.fuchsia => _nonAppleShortcuts,
},
};
/// The options view is considered eligible to show only while the field has
/// focus and there is at least one option to display.
bool get _canShowOptionsView => _focusNode.hasFocus && _options.isNotEmpty;
void _onFocusChange() {
if (_focusNode.hasFocus != _hasFocus) {
_hasFocus = _focusNode.hasFocus;
// Gaining focus can open the options view (if there are options). Losing
// focus always closes it.
_updateOptionsViewVisibility();
}
}
/// Shows the options view when the field is focused and there is at least one
/// option to display; otherwise hides the options view.
void _updateOptionsViewVisibility() {
if (_canShowOptionsView) {
_optionsViewController.show();
} else {
_optionsViewController.hide();
}
}
void _announceSemantics(bool resultsAvailable) {
if (!MediaQuery.supportsAnnounceOf(context)) {
return;
}
final WidgetsLocalizations localizations = WidgetsLocalizations.of(context);
final String optionsHint = resultsAvailable
? localizations.searchResultsFound
: localizations.noResultsFound;
SemanticsService.announce(optionsHint, localizations.textDirection);
}
// Assigning an ID to every call of _onChangedField is necessary to avoid a
// situation where _options is updated by an older call when multiple
// _onChangedField calls are running simultaneously.
int _onChangedCallId = 0;
// Called when _textEditingController changes.
Future<void> _onChangedField() async {
// During a selection, changes to the field text should not trigger
// options update.
if (_selecting) {
return;
}
final TextEditingValue value = _textEditingController.value;
// Makes sure that options change only when content of the field changes.
var shouldUpdateOptions = false;
if (value.text != _lastFieldText) {
shouldUpdateOptions = true;
_onChangedCallId += 1;
}
_lastFieldText = value.text;
final int callId = _onChangedCallId;
final Iterable<T> options = await widget.optionsBuilder(value);
// Makes sure that previous call results do not replace new ones.
if (callId != _onChangedCallId || !shouldUpdateOptions) {
return;
}
if (_options.isEmpty != options.isEmpty) {
_announceSemantics(options.isNotEmpty);
}
_options = options;
_updateHighlight(_highlightedOptionIndex.value);
final T? selection = _selection;
if (selection != null && value.text != widget.displayStringForOption(selection)) {
_selection = null;
}
_updateOptionsViewVisibility();
}
// Called from fieldViewBuilder when the user submits the field.
void _onFieldSubmitted() {
if (_optionsViewController.isShowing) {
_select(_options.elementAt(_highlightedOptionIndex.value));
}
}
// Select the given option and update the widget.
void _select(T nextSelection) {
if (nextSelection == _selection) {
return;
}
_selecting = true;
_selection = nextSelection;
final String selectionString = widget.displayStringForOption(nextSelection);
_textEditingController.value = TextEditingValue(
selection: TextSelection.collapsed(offset: selectionString.length),
text: selectionString,
);
widget.onSelected?.call(nextSelection);
if (_optionsViewController.isShowing) {
_optionsViewController.hide(); // Close the options view after a selection is made.
}
_selecting = false;
}
void _updateHighlight(int nextIndex) {
_highlightedOptionIndex.value = _options.isEmpty ? 0 : nextIndex.clamp(0, _options.length - 1);
}
void _highlightPreviousOption(AutocompletePreviousOptionIntent intent) {
_highlightOption(_highlightedOptionIndex.value - 1);
}
void _highlightNextOption(AutocompleteNextOptionIntent intent) {
_highlightOption(_highlightedOptionIndex.value + 1);
}
void _highlightFirstOption(AutocompleteFirstOptionIntent intent) {
_highlightOption(0);
}
void _highlightLastOption(AutocompleteLastOptionIntent intent) {
_highlightOption(_options.length - 1);
}
void _highlightNextPageOption(AutocompleteNextPageOptionIntent intent) {
_highlightOption(_highlightedOptionIndex.value + _pageSize);
}
void _highlightPreviousPageOption(AutocompletePreviousPageOptionIntent intent) {
_highlightOption(_highlightedOptionIndex.value - _pageSize);
}
void _highlightOption(int index) {
assert(_canShowOptionsView);
_updateOptionsViewVisibility();
assert(_optionsViewController.isShowing);
_updateHighlight(index);
}
Object? _hideOptions(DismissIntent intent) {
if (_optionsViewController.isShowing) {
_optionsViewController.hide();
return null;
} else {
return Actions.invoke(context, intent);
}
}
// A big enough height for about one item in the default
// Autocomplete.optionsViewBuilder. The assumption is that the user likely
// wants the list of options to move to stay on the screen rather than get any
// smaller than this. Allows Autocomplete to work when it has very little
// screen height available (as in b/317115348) by positioning itself on top of
// the field, while in other cases to size itself based on the height under
// the field.
static const double _kMinUsableHeight = kMinInteractiveDimension;
Widget _buildOptionsView(BuildContext context, OverlayChildLayoutInfo layoutInfo) {
if (layoutInfo.childPaintTransform.determinant() == 0.0) {
// The child is not visible.
return const SizedBox.shrink();
}
final Size fieldSize = layoutInfo.childSize;
final Matrix4 invertTransform = layoutInfo.childPaintTransform.clone()..invert();
// This may not work well if the paint transform has rotation in it.
// MatrixUtils.transformRect returns the bounding rect of the rotated overlay
// rect.
final Rect overlayRectInField = MatrixUtils.transformRect(
invertTransform,
Offset.zero & layoutInfo.overlaySize,
);
final double spaceAbove = -overlayRectInField.top;
final double spaceBelow = overlayRectInField.bottom - fieldSize.height;
final bool opensUp = switch (widget.optionsViewOpenDirection) {
OptionsViewOpenDirection.up => true,
OptionsViewOpenDirection.down => false,
OptionsViewOpenDirection.mostSpace => spaceAbove > spaceBelow,
};
final double optionsViewMaxHeight = opensUp
? -overlayRectInField.top
: overlayRectInField.bottom - fieldSize.height;
final optionsViewBoundingBox = Size(
fieldSize.width,
math.max(optionsViewMaxHeight, _kMinUsableHeight),
);
final double originY = opensUp
? overlayRectInField.top
: overlayRectInField.bottom - optionsViewBoundingBox.height;
final Matrix4 transform = layoutInfo.childPaintTransform.clone()
..translateByDouble(0.0, originY, 0, 1);
final Widget child = Builder(
builder: (BuildContext context) => widget.optionsViewBuilder(context, _select, _options),
);
return Transform(
transform: transform,
child: Align(
alignment: Alignment.topLeft,
child: ConstrainedBox(
constraints: BoxConstraints.tight(optionsViewBoundingBox),
child: Align(
alignment: opensUp ? AlignmentDirectional.bottomStart : AlignmentDirectional.topStart,
child: TextFieldTapRegion(
child: AutocompleteHighlightedOption(
highlightIndexNotifier: _highlightedOptionIndex,
child: child,
),
),
),
),
),
);
}
@override
void initState() {
super.initState();
final TextEditingController initialController =
widget.textEditingController ??
(_internalTextEditingController = TextEditingController.fromValue(widget.initialValue));
initialController.addListener(_onChangedField);
_hasFocus = _focusNode.hasFocus;
widget.focusNode?.addListener(_onFocusChange);
}
@override
void didUpdateWidget(RawAutocomplete<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (!identical(oldWidget.textEditingController, widget.textEditingController)) {
oldWidget.textEditingController?.removeListener(_onChangedField);
if (oldWidget.textEditingController == null) {
_internalTextEditingController?.dispose();
_internalTextEditingController = null;
}
widget.textEditingController?.addListener(_onChangedField);
}
if (!identical(oldWidget.focusNode, widget.focusNode)) {
oldWidget.focusNode?.removeListener(_updateOptionsViewVisibility);
if (oldWidget.focusNode == null) {
_internalFocusNode?.dispose();
_internalFocusNode = null;
}
widget.focusNode?.addListener(_updateOptionsViewVisibility);
}
}
@override
void dispose() {
widget.textEditingController?.removeListener(_onChangedField);
_internalTextEditingController?.dispose();
widget.focusNode?.removeListener(_updateOptionsViewVisibility);
_internalFocusNode?.dispose();
_highlightedOptionIndex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final Widget fieldView =
widget.fieldViewBuilder?.call(
context,
_textEditingController,
_focusNode,
_onFieldSubmitted,
) ??
// Horizontally expand to make sure the options view's width won't be zero.
const SizedBox(width: double.infinity, height: 0.0);
return OverlayPortal.overlayChildLayoutBuilder(
controller: _optionsViewController,
overlayChildBuilder: _buildOptionsView,
child: TextFieldTapRegion(
child: Shortcuts(
shortcuts: _shortcuts,
child: Actions(actions: _actionMap, child: fieldView),
),
),
);
}
}
class _AutocompleteCallbackAction<T extends Intent> extends CallbackAction<T> {
_AutocompleteCallbackAction({required super.onInvoke, required this.isEnabledCallback});
// The enabled state determines whether the action will consume the
// key shortcut or let it continue on to the underlying text field.
// They should only be enabled when the options are showing so shortcuts
// can be used to navigate them.
final bool Function() isEnabledCallback;
@override
bool isEnabled(covariant T intent) => isEnabledCallback();
@override
bool consumesKey(covariant T intent) => isEnabled(intent);
}
/// An [Intent] to highlight the previous option in the autocomplete list.
class AutocompletePreviousOptionIntent extends Intent {
/// Creates an instance of AutocompletePreviousOptionIntent.
const AutocompletePreviousOptionIntent();
}
/// An [Intent] to highlight the next option in the autocomplete list.
class AutocompleteNextOptionIntent extends Intent {
/// Creates an instance of AutocompleteNextOptionIntent.
const AutocompleteNextOptionIntent();
}
/// An [Intent] to highlight the first option in the autocomplete list.
class AutocompleteFirstOptionIntent extends Intent {
/// Creates an instance of AutocompleteFirstOptionIntent.
const AutocompleteFirstOptionIntent();
}
/// An [Intent] to highlight the last option in the autocomplete list.
class AutocompleteLastOptionIntent extends Intent {
/// Creates an instance of AutocompleteLastOptionIntent.
const AutocompleteLastOptionIntent();
}
/// An [Intent] to highlight the option one page after the currently highlighted
/// option in the autocomplete list.
class AutocompleteNextPageOptionIntent extends Intent {
/// Creates an instance of AutocompleteNextPageOptionIntent.
const AutocompleteNextPageOptionIntent();
}
/// An [Intent] to highlight the option one page before the currently
/// highlighted option in the autocomplete list.
class AutocompletePreviousPageOptionIntent extends Intent {
/// Creates an instance of AutocompletePreviousPageOptionIntent.
const AutocompletePreviousPageOptionIntent();
}
/// An inherited widget used to indicate which autocomplete option should be
/// highlighted for keyboard navigation.
///
/// The `RawAutocomplete` widget will wrap the options view generated by the
/// `optionsViewBuilder` with this widget to provide the highlighted option's
/// index to the builder.
///
/// In the builder callback the index of the highlighted option can be obtained
/// by using the static [of] method:
///
/// ```dart
/// int highlightedIndex = AutocompleteHighlightedOption.of(context);
/// ```
///
/// which can then be used to tell which option should be given a visual
/// indication that will be the option selected with the keyboard.
class AutocompleteHighlightedOption extends InheritedNotifier<ValueNotifier<int>> {
/// Create an instance of AutocompleteHighlightedOption inherited widget.
const AutocompleteHighlightedOption({
super.key,
required ValueNotifier<int> highlightIndexNotifier,
required super.child,
}) : super(notifier: highlightIndexNotifier);
/// Returns the index of the highlighted option from the closest
/// [AutocompleteHighlightedOption] ancestor.
///
/// If there is no ancestor, it returns 0.
///
/// Typical usage is as follows:
///
/// ```dart
/// int highlightedIndex = AutocompleteHighlightedOption.of(context);
/// ```
static int of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<AutocompleteHighlightedOption>()
?.notifier
?.value ??
0;
}
}