1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'package:flutter/material.dart';
6library;
7
8import 'dart:async';
9import 'dart:math' as math show max;
10
11import 'package:flutter/foundation.dart';
12import 'package:flutter/rendering.dart';
13import 'package:flutter/services.dart';
14
15import 'actions.dart';
16import 'basic.dart';
17import 'constants.dart';
18import 'editable_text.dart';
19import 'focus_manager.dart';
20import 'framework.dart';
21import 'inherited_notifier.dart';
22import 'overlay.dart';
23import 'shortcuts.dart';
24import 'tap_region.dart';
25
26// Examples can assume:
27// late BuildContext context;
28
29/// The type of the [RawAutocomplete] callback which computes the list of
30/// optional completions for the widget's field, based on the text the user has
31/// entered so far.
32///
33/// See also:
34///
35/// * [RawAutocomplete.optionsBuilder], which is of this type.
36typedef AutocompleteOptionsBuilder<T extends Object> =
37 FutureOr<Iterable<T>> Function(TextEditingValue textEditingValue);
38
39/// The type of the callback used by the [RawAutocomplete] widget to indicate
40/// that the user has selected an option.
41///
42/// See also:
43///
44/// * [RawAutocomplete.onSelected], which is of this type.
45typedef AutocompleteOnSelected<T extends Object> = void Function(T option);
46
47/// The type of the [RawAutocomplete] callback which returns a [Widget] that
48/// displays the specified [options] and calls [onSelected] if the user
49/// selects an option.
50///
51/// The returned widget from this callback will be wrapped in an
52/// [AutocompleteHighlightedOption] inherited widget. This will allow
53/// this callback to determine which option is currently highlighted for
54/// keyboard navigation.
55///
56/// See also:
57///
58/// * [RawAutocomplete.optionsViewBuilder], which is of this type.
59typedef AutocompleteOptionsViewBuilder<T extends Object> =
60 Widget Function(
61 BuildContext context,
62 AutocompleteOnSelected<T> onSelected,
63 Iterable<T> options,
64 );
65
66/// The type of the Autocomplete callback which returns the widget that
67/// contains the input [TextField] or [TextFormField].
68///
69/// See also:
70///
71/// * [RawAutocomplete.fieldViewBuilder], which is of this type.
72typedef AutocompleteFieldViewBuilder =
73 Widget Function(
74 BuildContext context,
75 TextEditingController textEditingController,
76 FocusNode focusNode,
77 VoidCallback onFieldSubmitted,
78 );
79
80/// The type of the [RawAutocomplete] callback that converts an option value to
81/// a string which can be displayed in the widget's options menu.
82///
83/// See also:
84///
85/// * [RawAutocomplete.displayStringForOption], which is of this type.
86typedef AutocompleteOptionToString<T extends Object> = String Function(T option);
87
88/// A direction in which to open the options-view overlay.
89///
90/// See also:
91///
92/// * [RawAutocomplete.optionsViewOpenDirection], which is of this type.
93/// * [RawAutocomplete.optionsViewBuilder] to specify how to build the
94/// selectable-options widget.
95/// * [RawAutocomplete.fieldViewBuilder] to optionally specify how to build the
96/// corresponding field widget.
97enum OptionsViewOpenDirection {
98 /// Open upward.
99 ///
100 /// The bottom edge of the options view will align with the top edge
101 /// of the text field built by [RawAutocomplete.fieldViewBuilder].
102 up,
103
104 /// Open downward.
105 ///
106 /// The top edge of the options view will align with the bottom edge
107 /// of the text field built by [RawAutocomplete.fieldViewBuilder].
108 down,
109}
110
111// TODO(justinmc): Mention AutocompleteCupertino when it is implemented.
112/// {@template flutter.widgets.RawAutocomplete.RawAutocomplete}
113/// A widget for helping the user make a selection by entering some text and
114/// choosing from among a list of options.
115///
116/// The user's text input is received in a field built with the
117/// [fieldViewBuilder] parameter. The options to be displayed are determined
118/// using [optionsBuilder] and rendered with [optionsViewBuilder].
119/// {@endtemplate}
120///
121/// This is a core framework widget with very basic UI.
122///
123/// {@tool dartpad}
124/// This example shows how to create a very basic autocomplete widget using the
125/// [fieldViewBuilder] and [optionsViewBuilder] parameters.
126///
127/// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.0.dart **
128/// {@end-tool}
129///
130/// The type parameter T represents the type of the options. Most commonly this
131/// is a String, as in the example above. However, it's also possible to use
132/// another type with a `toString` method, or a custom [displayStringForOption].
133/// Options will be compared using `==`, so it may be beneficial to override
134/// [Object.==] and [Object.hashCode] for custom types.
135///
136/// {@tool dartpad}
137/// This example is similar to the previous example, but it uses a custom T data
138/// type instead of directly using String.
139///
140/// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.1.dart **
141/// {@end-tool}
142///
143/// {@tool dartpad}
144/// This example shows the use of RawAutocomplete in a form.
145///
146/// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.2.dart **
147/// {@end-tool}
148///
149/// See also:
150///
151/// * [Autocomplete], which is a Material-styled implementation that is based
152/// on RawAutocomplete.
153class RawAutocomplete<T extends Object> extends StatefulWidget {
154 /// Create an instance of RawAutocomplete.
155 ///
156 /// [displayStringForOption], [optionsBuilder] and [optionsViewBuilder] must
157 /// not be null.
158 const RawAutocomplete({
159 super.key,
160 required this.optionsViewBuilder,
161 required this.optionsBuilder,
162 this.optionsViewOpenDirection = OptionsViewOpenDirection.down,
163 this.displayStringForOption = defaultStringForOption,
164 this.fieldViewBuilder,
165 this.focusNode,
166 this.onSelected,
167 this.textEditingController,
168 this.initialValue,
169 }) : assert(
170 fieldViewBuilder != null ||
171 (key != null && focusNode != null && textEditingController != null),
172 '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.',
173 ),
174 assert((focusNode == null) == (textEditingController == null)),
175 assert(
176 !(textEditingController != null && initialValue != null),
177 'textEditingController and initialValue cannot be simultaneously defined.',
178 );
179
180 /// {@template flutter.widgets.RawAutocomplete.fieldViewBuilder}
181 /// Builds the field whose input is used to get the options.
182 ///
183 /// Pass the provided [TextEditingController] to the field built here so that
184 /// RawAutocomplete can listen for changes.
185 /// {@endtemplate}
186 ///
187 /// If this parameter is null, then a [SizedBox.shrink] is built instead.
188 /// For how that pattern can be useful, see [textEditingController].
189 final AutocompleteFieldViewBuilder? fieldViewBuilder;
190
191 /// The [FocusNode] that is used for the text field.
192 ///
193 /// {@template flutter.widgets.RawAutocomplete.split}
194 /// The main purpose of this parameter is to allow the use of a separate text
195 /// field located in another part of the widget tree instead of the text
196 /// field built by [fieldViewBuilder]. For example, it may be desirable to
197 /// place the text field in the AppBar and the options below in the main body.
198 ///
199 /// When following this pattern, [fieldViewBuilder] can be omitted,
200 /// so that a text field is not drawn where it would normally be.
201 /// A separate text field can be created elsewhere, and a
202 /// FocusNode and TextEditingController can be passed both to that text field
203 /// and to RawAutocomplete.
204 ///
205 /// {@tool dartpad}
206 /// This examples shows how to create an autocomplete widget with the text
207 /// field in the AppBar and the results in the main body of the app.
208 ///
209 /// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.focus_node.0.dart **
210 /// {@end-tool}
211 /// {@endtemplate}
212 ///
213 /// If this parameter is not null, then [textEditingController] must also be
214 /// non-null.
215 final FocusNode? focusNode;
216
217 /// {@template flutter.widgets.RawAutocomplete.optionsViewBuilder}
218 /// Builds the selectable options widgets from a list of options objects.
219 ///
220 /// The options are displayed floating below or above the field inside of an
221 /// [Overlay], not at the same place in the widget tree as [RawAutocomplete].
222 /// To control whether it opens upward or downward, use
223 /// [optionsViewOpenDirection].
224 ///
225 /// In order to track which item is highlighted by keyboard navigation, the
226 /// resulting options will be wrapped in an inherited
227 /// [AutocompleteHighlightedOption] widget.
228 /// Inside this callback, the index of the highlighted option can be obtained
229 /// from [AutocompleteHighlightedOption.of] to display the highlighted option
230 /// with a visual highlight to indicate it will be the option selected from
231 /// the keyboard.
232 ///
233 /// {@endtemplate}
234 final AutocompleteOptionsViewBuilder<T> optionsViewBuilder;
235
236 /// {@template flutter.widgets.RawAutocomplete.optionsViewOpenDirection}
237 /// The direction in which to open the options-view overlay.
238 ///
239 /// Defaults to [OptionsViewOpenDirection.down].
240 /// {@endtemplate}
241 final OptionsViewOpenDirection optionsViewOpenDirection;
242
243 /// {@template flutter.widgets.RawAutocomplete.displayStringForOption}
244 /// Returns the string to display in the field when the option is selected.
245 ///
246 /// This is useful when using a custom T type and the string to display is
247 /// different than the string to search by.
248 ///
249 /// If not provided, will use `option.toString()`.
250 /// {@endtemplate}
251 final AutocompleteOptionToString<T> displayStringForOption;
252
253 /// {@template flutter.widgets.RawAutocomplete.onSelected}
254 /// Called when an option is selected by the user.
255 /// {@endtemplate}
256 final AutocompleteOnSelected<T>? onSelected;
257
258 /// {@template flutter.widgets.RawAutocomplete.optionsBuilder}
259 /// A function that returns the current selectable options objects given the
260 /// current TextEditingValue.
261 /// {@endtemplate}
262 final AutocompleteOptionsBuilder<T> optionsBuilder;
263
264 /// The [TextEditingController] that is used for the text field.
265 ///
266 /// {@macro flutter.widgets.RawAutocomplete.split}
267 ///
268 /// If this parameter is not null, then [focusNode] must also be non-null.
269 final TextEditingController? textEditingController;
270
271 /// {@template flutter.widgets.RawAutocomplete.initialValue}
272 /// The initial value to use for the text field.
273 /// {@endtemplate}
274 ///
275 /// Setting the initial value does not notify [textEditingController]'s
276 /// listeners, and thus will not cause the options UI to appear.
277 ///
278 /// This parameter is ignored if [textEditingController] is defined.
279 final TextEditingValue? initialValue;
280
281 /// Calls [AutocompleteFieldViewBuilder]'s onFieldSubmitted callback for the
282 /// RawAutocomplete widget indicated by the given [GlobalKey].
283 ///
284 /// This is not typically used unless a custom field is implemented instead of
285 /// using [fieldViewBuilder]. In the typical case, the onFieldSubmitted
286 /// callback is passed via the [AutocompleteFieldViewBuilder] signature. When
287 /// not using fieldViewBuilder, the same callback can be called by using this
288 /// static method.
289 ///
290 /// See also:
291 ///
292 /// * [focusNode] and [textEditingController], which contain a code example
293 /// showing how to create a separate field outside of fieldViewBuilder.
294 static void onFieldSubmitted<T extends Object>(GlobalKey key) {
295 final _RawAutocompleteState<T> rawAutocomplete = key.currentState! as _RawAutocompleteState<T>;
296 rawAutocomplete._onFieldSubmitted();
297 }
298
299 /// The default way to convert an option to a string in
300 /// [displayStringForOption].
301 ///
302 /// Uses the `toString` method of the given `option`.
303 static String defaultStringForOption(Object? option) {
304 return option.toString();
305 }
306
307 @override
308 State<RawAutocomplete<T>> createState() => _RawAutocompleteState<T>();
309}
310
311class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>> {
312 final OverlayPortalController _optionsViewController = OverlayPortalController(
313 debugLabel: '_RawAutocompleteState',
314 );
315
316 // The number of options to scroll by "page", such as when using the page
317 // up/down keys.
318 static const int _pageSize = 4;
319
320 TextEditingController? _internalTextEditingController;
321 TextEditingController get _textEditingController {
322 return widget.textEditingController ??
323 (_internalTextEditingController ??= TextEditingController()..addListener(_onChangedField));
324 }
325
326 FocusNode? _internalFocusNode;
327 FocusNode get _focusNode {
328 return widget.focusNode ??
329 (_internalFocusNode ??= FocusNode()..addListener(_updateOptionsViewVisibility));
330 }
331
332 late final Map<Type, CallbackAction<Intent>> _actionMap = <Type, CallbackAction<Intent>>{
333 AutocompletePreviousOptionIntent: _AutocompleteCallbackAction<AutocompletePreviousOptionIntent>(
334 onInvoke: _highlightPreviousOption,
335 isEnabledCallback: () => _canShowOptionsView,
336 ),
337 AutocompleteNextOptionIntent: _AutocompleteCallbackAction<AutocompleteNextOptionIntent>(
338 onInvoke: _highlightNextOption,
339 isEnabledCallback: () => _canShowOptionsView,
340 ),
341 AutocompleteFirstOptionIntent: _AutocompleteCallbackAction<AutocompleteFirstOptionIntent>(
342 onInvoke: _highlightFirstOption,
343 isEnabledCallback: () => _canShowOptionsView,
344 ),
345 AutocompleteLastOptionIntent: _AutocompleteCallbackAction<AutocompleteLastOptionIntent>(
346 onInvoke: _highlightLastOption,
347 isEnabledCallback: () => _canShowOptionsView,
348 ),
349 AutocompleteNextPageOptionIntent: _AutocompleteCallbackAction<AutocompleteNextPageOptionIntent>(
350 onInvoke: _highlightNextPageOption,
351 isEnabledCallback: () => _canShowOptionsView,
352 ),
353 AutocompletePreviousPageOptionIntent:
354 _AutocompleteCallbackAction<AutocompletePreviousPageOptionIntent>(
355 onInvoke: _highlightPreviousPageOption,
356 isEnabledCallback: () => _canShowOptionsView,
357 ),
358 DismissIntent: CallbackAction<DismissIntent>(onInvoke: _hideOptions),
359 };
360
361 Iterable<T> _options = Iterable<T>.empty();
362 T? _selection;
363 // Set the initial value to null so when this widget gets focused for the first
364 // time it will try to run the options view builder.
365 String? _lastFieldText;
366 final ValueNotifier<int> _highlightedOptionIndex = ValueNotifier<int>(0);
367
368 static const Map<ShortcutActivator, Intent> _appleShortcuts = <ShortcutActivator, Intent>{
369 SingleActivator(LogicalKeyboardKey.arrowUp, meta: true): AutocompleteFirstOptionIntent(),
370 SingleActivator(LogicalKeyboardKey.arrowDown, meta: true): AutocompleteLastOptionIntent(),
371 };
372
373 static const Map<ShortcutActivator, Intent> _nonAppleShortcuts = <ShortcutActivator, Intent>{
374 SingleActivator(LogicalKeyboardKey.arrowUp, control: true): AutocompleteFirstOptionIntent(),
375 SingleActivator(LogicalKeyboardKey.arrowDown, control: true): AutocompleteLastOptionIntent(),
376 };
377
378 static const Map<ShortcutActivator, Intent> _commonShortcuts = <ShortcutActivator, Intent>{
379 SingleActivator(LogicalKeyboardKey.arrowUp): AutocompletePreviousOptionIntent(),
380 SingleActivator(LogicalKeyboardKey.arrowDown): AutocompleteNextOptionIntent(),
381 SingleActivator(LogicalKeyboardKey.pageUp): AutocompletePreviousPageOptionIntent(),
382 SingleActivator(LogicalKeyboardKey.pageDown): AutocompleteNextPageOptionIntent(),
383 };
384
385 static Map<ShortcutActivator, Intent> get _shortcuts => <ShortcutActivator, Intent>{
386 ..._commonShortcuts,
387 ...switch (defaultTargetPlatform) {
388 TargetPlatform.iOS => _appleShortcuts,
389 TargetPlatform.macOS => _appleShortcuts,
390 TargetPlatform.android => _nonAppleShortcuts,
391 TargetPlatform.linux => _nonAppleShortcuts,
392 TargetPlatform.windows => _nonAppleShortcuts,
393 TargetPlatform.fuchsia => _nonAppleShortcuts,
394 },
395 };
396
397 bool get _canShowOptionsView => _focusNode.hasFocus && _selection == null && _options.isNotEmpty;
398
399 void _updateOptionsViewVisibility() {
400 if (_canShowOptionsView) {
401 _optionsViewController.show();
402 } else {
403 _optionsViewController.hide();
404 }
405 }
406
407 // Assigning an ID to every call of _onChangedField is necessary to avoid a
408 // situation where _options is updated by an older call when multiple
409 // _onChangedField calls are running simultaneously.
410 int _onChangedCallId = 0;
411 // Called when _textEditingController changes.
412 Future<void> _onChangedField() async {
413 final TextEditingValue value = _textEditingController.value;
414
415 // Makes sure that options change only when content of the field changes.
416 bool shouldUpdateOptions = false;
417 if (value.text != _lastFieldText) {
418 shouldUpdateOptions = true;
419 _onChangedCallId += 1;
420 }
421 _lastFieldText = value.text;
422 final int callId = _onChangedCallId;
423 final Iterable<T> options = await widget.optionsBuilder(value);
424
425 // Makes sure that previous call results do not replace new ones.
426 if (callId != _onChangedCallId || !shouldUpdateOptions) {
427 return;
428 }
429 _options = options;
430 _updateHighlight(_highlightedOptionIndex.value);
431 final T? selection = _selection;
432 if (selection != null && value.text != widget.displayStringForOption(selection)) {
433 _selection = null;
434 }
435
436 _updateOptionsViewVisibility();
437 }
438
439 // Called from fieldViewBuilder when the user submits the field.
440 void _onFieldSubmitted() {
441 if (_optionsViewController.isShowing) {
442 _select(_options.elementAt(_highlightedOptionIndex.value));
443 }
444 }
445
446 // Select the given option and update the widget.
447 void _select(T nextSelection) {
448 if (nextSelection == _selection) {
449 return;
450 }
451 _selection = nextSelection;
452 final String selectionString = widget.displayStringForOption(nextSelection);
453 _textEditingController.value = TextEditingValue(
454 selection: TextSelection.collapsed(offset: selectionString.length),
455 text: selectionString,
456 );
457 widget.onSelected?.call(nextSelection);
458 _updateOptionsViewVisibility();
459 }
460
461 void _updateHighlight(int nextIndex) {
462 _highlightedOptionIndex.value = _options.isEmpty ? 0 : nextIndex.clamp(0, _options.length - 1);
463 }
464
465 void _highlightPreviousOption(AutocompletePreviousOptionIntent intent) {
466 _highlightOption(_highlightedOptionIndex.value - 1);
467 }
468
469 void _highlightNextOption(AutocompleteNextOptionIntent intent) {
470 _highlightOption(_highlightedOptionIndex.value + 1);
471 }
472
473 void _highlightFirstOption(AutocompleteFirstOptionIntent intent) {
474 _highlightOption(0);
475 }
476
477 void _highlightLastOption(AutocompleteLastOptionIntent intent) {
478 _highlightOption(_options.length - 1);
479 }
480
481 void _highlightNextPageOption(AutocompleteNextPageOptionIntent intent) {
482 _highlightOption(_highlightedOptionIndex.value + _pageSize);
483 }
484
485 void _highlightPreviousPageOption(AutocompletePreviousPageOptionIntent intent) {
486 _highlightOption(_highlightedOptionIndex.value - _pageSize);
487 }
488
489 void _highlightOption(int index) {
490 assert(_canShowOptionsView);
491 _updateOptionsViewVisibility();
492 assert(_optionsViewController.isShowing);
493 _updateHighlight(index);
494 }
495
496 Object? _hideOptions(DismissIntent intent) {
497 if (_optionsViewController.isShowing) {
498 _optionsViewController.hide();
499 return null;
500 } else {
501 return Actions.invoke(context, intent);
502 }
503 }
504
505 // A big enough height for about one item in the default
506 // Autocomplete.optionsViewBuilder. The assumption is that the user likely
507 // wants the list of options to move to stay on the screen rather than get any
508 // smaller than this. Allows Autocomplete to work when it has very little
509 // screen height available (as in b/317115348) by positioning itself on top of
510 // the field, while in other cases to size itself based on the height under
511 // the field.
512 static const double _kMinUsableHeight = kMinInteractiveDimension;
513
514 Widget _buildOptionsView(BuildContext context, OverlayChildLayoutInfo layoutInfo) {
515 if (layoutInfo.childPaintTransform.determinant() == 0.0) {
516 // The child is not visible.
517 return const SizedBox.shrink();
518 }
519 final Size fieldSize = layoutInfo.childSize;
520 final Matrix4 invertTransform = layoutInfo.childPaintTransform.clone()..invert();
521
522 // This may not work well if the paint transform has rotation in it.
523 // MatrixUtils.transformRect returns the bounding rect of the rotated overlay
524 // rect.
525 final Rect overlayRectInField = MatrixUtils.transformRect(
526 invertTransform,
527 Offset.zero & layoutInfo.overlaySize,
528 );
529
530 final double optionsViewMaxHeight = switch (widget.optionsViewOpenDirection) {
531 OptionsViewOpenDirection.up => -overlayRectInField.top,
532 OptionsViewOpenDirection.down => overlayRectInField.bottom - fieldSize.height,
533 };
534
535 final Size optionsViewBoundingBox = Size(
536 fieldSize.width,
537 math.max(optionsViewMaxHeight, _kMinUsableHeight),
538 );
539
540 final double originY = switch (widget.optionsViewOpenDirection) {
541 OptionsViewOpenDirection.up => overlayRectInField.top,
542 OptionsViewOpenDirection.down => overlayRectInField.bottom - optionsViewBoundingBox.height,
543 };
544
545 final Matrix4 transform = layoutInfo.childPaintTransform.clone()
546 ..translateByDouble(0.0, originY, 0, 1);
547 final Widget child = Builder(
548 builder: (BuildContext context) => widget.optionsViewBuilder(context, _select, _options),
549 );
550 return Transform(
551 transform: transform,
552 child: Align(
553 alignment: Alignment.topLeft,
554 child: ConstrainedBox(
555 constraints: BoxConstraints.tight(optionsViewBoundingBox),
556 child: Align(
557 alignment: switch (widget.optionsViewOpenDirection) {
558 OptionsViewOpenDirection.up => AlignmentDirectional.bottomStart,
559 OptionsViewOpenDirection.down => AlignmentDirectional.topStart,
560 },
561 child: TextFieldTapRegion(
562 child: AutocompleteHighlightedOption(
563 highlightIndexNotifier: _highlightedOptionIndex,
564 child: child,
565 ),
566 ),
567 ),
568 ),
569 ),
570 );
571 }
572
573 @override
574 void initState() {
575 super.initState();
576 final TextEditingController initialController =
577 widget.textEditingController ??
578 (_internalTextEditingController = TextEditingController.fromValue(widget.initialValue));
579 initialController.addListener(_onChangedField);
580 widget.focusNode?.addListener(_updateOptionsViewVisibility);
581 }
582
583 @override
584 void didUpdateWidget(RawAutocomplete<T> oldWidget) {
585 super.didUpdateWidget(oldWidget);
586 if (!identical(oldWidget.textEditingController, widget.textEditingController)) {
587 oldWidget.textEditingController?.removeListener(_onChangedField);
588 if (oldWidget.textEditingController == null) {
589 _internalTextEditingController?.dispose();
590 _internalTextEditingController = null;
591 }
592 widget.textEditingController?.addListener(_onChangedField);
593 }
594 if (!identical(oldWidget.focusNode, widget.focusNode)) {
595 oldWidget.focusNode?.removeListener(_updateOptionsViewVisibility);
596 if (oldWidget.focusNode == null) {
597 _internalFocusNode?.dispose();
598 _internalFocusNode = null;
599 }
600 widget.focusNode?.addListener(_updateOptionsViewVisibility);
601 }
602 }
603
604 @override
605 void dispose() {
606 widget.textEditingController?.removeListener(_onChangedField);
607 _internalTextEditingController?.dispose();
608 widget.focusNode?.removeListener(_updateOptionsViewVisibility);
609 _internalFocusNode?.dispose();
610 _highlightedOptionIndex.dispose();
611 super.dispose();
612 }
613
614 @override
615 Widget build(BuildContext context) {
616 final Widget fieldView =
617 widget.fieldViewBuilder?.call(
618 context,
619 _textEditingController,
620 _focusNode,
621 _onFieldSubmitted,
622 ) ??
623 // Horizontally expand to make sure the options view's width won't be zero.
624 const SizedBox(width: double.infinity, height: 0.0);
625 return OverlayPortal.overlayChildLayoutBuilder(
626 controller: _optionsViewController,
627 overlayChildBuilder: _buildOptionsView,
628 child: TextFieldTapRegion(
629 child: Shortcuts(
630 shortcuts: _shortcuts,
631 child: Actions(actions: _actionMap, child: fieldView),
632 ),
633 ),
634 );
635 }
636}
637
638class _AutocompleteCallbackAction<T extends Intent> extends CallbackAction<T> {
639 _AutocompleteCallbackAction({required super.onInvoke, required this.isEnabledCallback});
640
641 // The enabled state determines whether the action will consume the
642 // key shortcut or let it continue on to the underlying text field.
643 // They should only be enabled when the options are showing so shortcuts
644 // can be used to navigate them.
645 final bool Function() isEnabledCallback;
646
647 @override
648 bool isEnabled(covariant T intent) => isEnabledCallback();
649
650 @override
651 bool consumesKey(covariant T intent) => isEnabled(intent);
652}
653
654/// An [Intent] to highlight the previous option in the autocomplete list.
655class AutocompletePreviousOptionIntent extends Intent {
656 /// Creates an instance of AutocompletePreviousOptionIntent.
657 const AutocompletePreviousOptionIntent();
658}
659
660/// An [Intent] to highlight the next option in the autocomplete list.
661class AutocompleteNextOptionIntent extends Intent {
662 /// Creates an instance of AutocompleteNextOptionIntent.
663 const AutocompleteNextOptionIntent();
664}
665
666/// An [Intent] to highlight the first option in the autocomplete list.
667class AutocompleteFirstOptionIntent extends Intent {
668 /// Creates an instance of AutocompleteFirstOptionIntent.
669 const AutocompleteFirstOptionIntent();
670}
671
672/// An [Intent] to highlight the last option in the autocomplete list.
673class AutocompleteLastOptionIntent extends Intent {
674 /// Creates an instance of AutocompleteLastOptionIntent.
675 const AutocompleteLastOptionIntent();
676}
677
678/// An [Intent] to highlight the option one page after the currently highlighted
679/// option in the autocomplete list.
680class AutocompleteNextPageOptionIntent extends Intent {
681 /// Creates an instance of AutocompleteNextPageOptionIntent.
682 const AutocompleteNextPageOptionIntent();
683}
684
685/// An [Intent] to highlight the option one page before the currently
686/// highlighted option in the autocomplete list.
687class AutocompletePreviousPageOptionIntent extends Intent {
688 /// Creates an instance of AutocompletePreviousPageOptionIntent.
689 const AutocompletePreviousPageOptionIntent();
690}
691
692/// An inherited widget used to indicate which autocomplete option should be
693/// highlighted for keyboard navigation.
694///
695/// The `RawAutocomplete` widget will wrap the options view generated by the
696/// `optionsViewBuilder` with this widget to provide the highlighted option's
697/// index to the builder.
698///
699/// In the builder callback the index of the highlighted option can be obtained
700/// by using the static [of] method:
701///
702/// ```dart
703/// int highlightedIndex = AutocompleteHighlightedOption.of(context);
704/// ```
705///
706/// which can then be used to tell which option should be given a visual
707/// indication that will be the option selected with the keyboard.
708class AutocompleteHighlightedOption extends InheritedNotifier<ValueNotifier<int>> {
709 /// Create an instance of AutocompleteHighlightedOption inherited widget.
710 const AutocompleteHighlightedOption({
711 super.key,
712 required ValueNotifier<int> highlightIndexNotifier,
713 required super.child,
714 }) : super(notifier: highlightIndexNotifier);
715
716 /// Returns the index of the highlighted option from the closest
717 /// [AutocompleteHighlightedOption] ancestor.
718 ///
719 /// If there is no ancestor, it returns 0.
720 ///
721 /// Typical usage is as follows:
722 ///
723 /// ```dart
724 /// int highlightedIndex = AutocompleteHighlightedOption.of(context);
725 /// ```
726 static int of(BuildContext context) {
727 return context
728 .dependOnInheritedWidgetOfExactType<AutocompleteHighlightedOption>()
729 ?.notifier
730 ?.value ??
731 0;
732 }
733}
734