| // 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; |
| } |
| } |