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
5import 'dart:async';
6
7import 'package:flutter/scheduler.dart';
8import 'package:flutter/services.dart';
9
10import 'actions.dart';
11import 'basic.dart';
12import 'container.dart';
13import 'editable_text.dart';
14import 'focus_manager.dart';
15import 'framework.dart';
16import 'inherited_notifier.dart';
17import 'overlay.dart';
18import 'shortcuts.dart';
19import 'tap_region.dart';
20
21// Examples can assume:
22// late BuildContext context;
23
24/// The type of the [RawAutocomplete] callback which computes the list of
25/// optional completions for the widget's field, based on the text the user has
26/// entered so far.
27///
28/// See also:
29///
30/// * [RawAutocomplete.optionsBuilder], which is of this type.
31typedef AutocompleteOptionsBuilder<T extends Object> = FutureOr<Iterable<T>> Function(TextEditingValue textEditingValue);
32
33/// The type of the callback used by the [RawAutocomplete] widget to indicate
34/// that the user has selected an option.
35///
36/// See also:
37///
38/// * [RawAutocomplete.onSelected], which is of this type.
39typedef AutocompleteOnSelected<T extends Object> = void Function(T option);
40
41/// The type of the [RawAutocomplete] callback which returns a [Widget] that
42/// displays the specified [options] and calls [onSelected] if the user
43/// selects an option.
44///
45/// The returned widget from this callback will be wrapped in an
46/// [AutocompleteHighlightedOption] inherited widget. This will allow
47/// this callback to determine which option is currently highlighted for
48/// keyboard navigation.
49///
50/// See also:
51///
52/// * [RawAutocomplete.optionsViewBuilder], which is of this type.
53typedef AutocompleteOptionsViewBuilder<T extends Object> = Widget Function(
54 BuildContext context,
55 AutocompleteOnSelected<T> onSelected,
56 Iterable<T> options,
57);
58
59/// The type of the Autocomplete callback which returns the widget that
60/// contains the input [TextField] or [TextFormField].
61///
62/// See also:
63///
64/// * [RawAutocomplete.fieldViewBuilder], which is of this type.
65typedef AutocompleteFieldViewBuilder = Widget Function(
66 BuildContext context,
67 TextEditingController textEditingController,
68 FocusNode focusNode,
69 VoidCallback onFieldSubmitted,
70);
71
72/// The type of the [RawAutocomplete] callback that converts an option value to
73/// a string which can be displayed in the widget's options menu.
74///
75/// See also:
76///
77/// * [RawAutocomplete.displayStringForOption], which is of this type.
78typedef AutocompleteOptionToString<T extends Object> = String Function(T option);
79
80/// A direction in which to open the options-view overlay.
81///
82/// See also:
83///
84/// * [RawAutocomplete.optionsViewOpenDirection], which is of this type.
85/// * [RawAutocomplete.optionsViewBuilder] to specify how to build the
86/// selectable-options widget.
87/// * [RawAutocomplete.fieldViewBuilder] to optionally specify how to build the
88/// corresponding field widget.
89enum OptionsViewOpenDirection {
90 /// Open upward.
91 ///
92 /// The bottom edge of the options view will align with the top edge
93 /// of the text field built by [RawAutocomplete.fieldViewBuilder].
94 up,
95
96 /// Open downward.
97 ///
98 /// The top edge of the options view will align with the bottom edge
99 /// of the text field built by [RawAutocomplete.fieldViewBuilder].
100 down,
101}
102
103// TODO(justinmc): Mention AutocompleteCupertino when it is implemented.
104/// {@template flutter.widgets.RawAutocomplete.RawAutocomplete}
105/// A widget for helping the user make a selection by entering some text and
106/// choosing from among a list of options.
107///
108/// The user's text input is received in a field built with the
109/// [fieldViewBuilder] parameter. The options to be displayed are determined
110/// using [optionsBuilder] and rendered with [optionsViewBuilder].
111/// {@endtemplate}
112///
113/// This is a core framework widget with very basic UI.
114///
115/// {@tool dartpad}
116/// This example shows how to create a very basic autocomplete widget using the
117/// [fieldViewBuilder] and [optionsViewBuilder] parameters.
118///
119/// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.0.dart **
120/// {@end-tool}
121///
122/// The type parameter T represents the type of the options. Most commonly this
123/// is a String, as in the example above. However, it's also possible to use
124/// another type with a `toString` method, or a custom [displayStringForOption].
125/// Options will be compared using `==`, so it may be beneficial to override
126/// [Object.==] and [Object.hashCode] for custom types.
127///
128/// {@tool dartpad}
129/// This example is similar to the previous example, but it uses a custom T data
130/// type instead of directly using String.
131///
132/// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.1.dart **
133/// {@end-tool}
134///
135/// {@tool dartpad}
136/// This example shows the use of RawAutocomplete in a form.
137///
138/// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.2.dart **
139/// {@end-tool}
140///
141/// See also:
142///
143/// * [Autocomplete], which is a Material-styled implementation that is based
144/// on RawAutocomplete.
145class RawAutocomplete<T extends Object> extends StatefulWidget {
146 /// Create an instance of RawAutocomplete.
147 ///
148 /// [displayStringForOption], [optionsBuilder] and [optionsViewBuilder] must
149 /// not be null.
150 const RawAutocomplete({
151 super.key,
152 required this.optionsViewBuilder,
153 required this.optionsBuilder,
154 this.optionsViewOpenDirection = OptionsViewOpenDirection.down,
155 this.displayStringForOption = defaultStringForOption,
156 this.fieldViewBuilder,
157 this.focusNode,
158 this.onSelected,
159 this.textEditingController,
160 this.initialValue,
161 }) : assert(
162 fieldViewBuilder != null
163 || (key != null && focusNode != null && textEditingController != null),
164 '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.',
165 ),
166 assert((focusNode == null) == (textEditingController == null)),
167 assert(
168 !(textEditingController != null && initialValue != null),
169 'textEditingController and initialValue cannot be simultaneously defined.',
170 );
171
172 /// {@template flutter.widgets.RawAutocomplete.fieldViewBuilder}
173 /// Builds the field whose input is used to get the options.
174 ///
175 /// Pass the provided [TextEditingController] to the field built here so that
176 /// RawAutocomplete can listen for changes.
177 /// {@endtemplate}
178 ///
179 /// If this parameter is null, then a [SizedBox.shrink] is built instead.
180 /// For how that pattern can be useful, see [textEditingController].
181 final AutocompleteFieldViewBuilder? fieldViewBuilder;
182
183 /// The [FocusNode] that is used for the text field.
184 ///
185 /// {@template flutter.widgets.RawAutocomplete.split}
186 /// The main purpose of this parameter is to allow the use of a separate text
187 /// field located in another part of the widget tree instead of the text
188 /// field built by [fieldViewBuilder]. For example, it may be desirable to
189 /// place the text field in the AppBar and the options below in the main body.
190 ///
191 /// When following this pattern, [fieldViewBuilder] can be omitted,
192 /// so that a text field is not drawn where it would normally be.
193 /// A separate text field can be created elsewhere, and a
194 /// FocusNode and TextEditingController can be passed both to that text field
195 /// and to RawAutocomplete.
196 ///
197 /// {@tool dartpad}
198 /// This examples shows how to create an autocomplete widget with the text
199 /// field in the AppBar and the results in the main body of the app.
200 ///
201 /// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.focus_node.0.dart **
202 /// {@end-tool}
203 /// {@endtemplate}
204 ///
205 /// If this parameter is not null, then [textEditingController] must also be
206 /// not null.
207 final FocusNode? focusNode;
208
209 /// {@template flutter.widgets.RawAutocomplete.optionsViewBuilder}
210 /// Builds the selectable options widgets from a list of options objects.
211 ///
212 /// The options are displayed floating below or above the field using a
213 /// [CompositedTransformFollower] inside of an [Overlay], not at the same
214 /// place in the widget tree as [RawAutocomplete]. To control whether it opens
215 /// upward or downward, use [optionsViewOpenDirection].
216 ///
217 /// In order to track which item is highlighted by keyboard navigation, the
218 /// resulting options will be wrapped in an inherited
219 /// [AutocompleteHighlightedOption] widget.
220 /// Inside this callback, the index of the highlighted option can be obtained
221 /// from [AutocompleteHighlightedOption.of] to display the highlighted option
222 /// with a visual highlight to indicate it will be the option selected from
223 /// the keyboard.
224 ///
225 /// {@endtemplate}
226 final AutocompleteOptionsViewBuilder<T> optionsViewBuilder;
227
228 /// {@template flutter.widgets.RawAutocomplete.optionsViewOpenDirection}
229 /// The direction in which to open the options-view overlay.
230 ///
231 /// Defaults to [OptionsViewOpenDirection.down].
232 /// {@endtemplate}
233 final OptionsViewOpenDirection optionsViewOpenDirection;
234
235 /// {@template flutter.widgets.RawAutocomplete.displayStringForOption}
236 /// Returns the string to display in the field when the option is selected.
237 ///
238 /// This is useful when using a custom T type and the string to display is
239 /// different than the string to search by.
240 ///
241 /// If not provided, will use `option.toString()`.
242 /// {@endtemplate}
243 final AutocompleteOptionToString<T> displayStringForOption;
244
245 /// {@template flutter.widgets.RawAutocomplete.onSelected}
246 /// Called when an option is selected by the user.
247 /// {@endtemplate}
248 final AutocompleteOnSelected<T>? onSelected;
249
250 /// {@template flutter.widgets.RawAutocomplete.optionsBuilder}
251 /// A function that returns the current selectable options objects given the
252 /// current TextEditingValue.
253 /// {@endtemplate}
254 final AutocompleteOptionsBuilder<T> optionsBuilder;
255
256 /// The [TextEditingController] that is used for the text field.
257 ///
258 /// {@macro flutter.widgets.RawAutocomplete.split}
259 ///
260 /// If this parameter is not null, then [focusNode] must also be not null.
261 final TextEditingController? textEditingController;
262
263 /// {@template flutter.widgets.RawAutocomplete.initialValue}
264 /// The initial value to use for the text field.
265 /// {@endtemplate}
266 ///
267 /// Setting the initial value does not notify [textEditingController]'s
268 /// listeners, and thus will not cause the options UI to appear.
269 ///
270 /// This parameter is ignored if [textEditingController] is defined.
271 final TextEditingValue? initialValue;
272
273 /// Calls [AutocompleteFieldViewBuilder]'s onFieldSubmitted callback for the
274 /// RawAutocomplete widget indicated by the given [GlobalKey].
275 ///
276 /// This is not typically used unless a custom field is implemented instead of
277 /// using [fieldViewBuilder]. In the typical case, the onFieldSubmitted
278 /// callback is passed via the [AutocompleteFieldViewBuilder] signature. When
279 /// not using fieldViewBuilder, the same callback can be called by using this
280 /// static method.
281 ///
282 /// See also:
283 ///
284 /// * [focusNode] and [textEditingController], which contain a code example
285 /// showing how to create a separate field outside of fieldViewBuilder.
286 static void onFieldSubmitted<T extends Object>(GlobalKey key) {
287 final _RawAutocompleteState<T> rawAutocomplete = key.currentState! as _RawAutocompleteState<T>;
288 rawAutocomplete._onFieldSubmitted();
289 }
290
291 /// The default way to convert an option to a string in
292 /// [displayStringForOption].
293 ///
294 /// Uses the `toString` method of the given `option`.
295 static String defaultStringForOption(Object? option) {
296 return option.toString();
297 }
298
299 @override
300 State<RawAutocomplete<T>> createState() => _RawAutocompleteState<T>();
301}
302
303class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>> {
304 final GlobalKey _fieldKey = GlobalKey();
305 final LayerLink _optionsLayerLink = LayerLink();
306 late TextEditingController _textEditingController;
307 late FocusNode _focusNode;
308 late final Map<Type, Action<Intent>> _actionMap;
309 late final _AutocompleteCallbackAction<AutocompletePreviousOptionIntent> _previousOptionAction;
310 late final _AutocompleteCallbackAction<AutocompleteNextOptionIntent> _nextOptionAction;
311 late final _AutocompleteCallbackAction<DismissIntent> _hideOptionsAction;
312 Iterable<T> _options = Iterable<T>.empty();
313 T? _selection;
314 bool _userHidOptions = false;
315 String _lastFieldText = '';
316 final ValueNotifier<int> _highlightedOptionIndex = ValueNotifier<int>(0);
317
318 static const Map<ShortcutActivator, Intent> _shortcuts = <ShortcutActivator, Intent>{
319 SingleActivator(LogicalKeyboardKey.arrowUp): AutocompletePreviousOptionIntent(),
320 SingleActivator(LogicalKeyboardKey.arrowDown): AutocompleteNextOptionIntent(),
321 };
322
323 // The OverlayEntry containing the options.
324 OverlayEntry? _floatingOptions;
325
326 // True iff the state indicates that the options should be visible.
327 bool get _shouldShowOptions {
328 return !_userHidOptions && _focusNode.hasFocus && _selection == null && _options.isNotEmpty;
329 }
330
331 // Called when _textEditingController changes.
332 Future<void> _onChangedField() async {
333 final TextEditingValue value = _textEditingController.value;
334 final Iterable<T> options = await widget.optionsBuilder(
335 value,
336 );
337 _options = options;
338 _updateHighlight(_highlightedOptionIndex.value);
339 if (_selection != null
340 && value.text != widget.displayStringForOption(_selection!)) {
341 _selection = null;
342 }
343
344 // Make sure the options are no longer hidden if the content of the field
345 // changes (ignore selection changes).
346 if (value.text != _lastFieldText) {
347 _userHidOptions = false;
348 _lastFieldText = value.text;
349 }
350 _updateActions();
351 _updateOverlay();
352 }
353
354 // Called when the field's FocusNode changes.
355 void _onChangedFocus() {
356 // Options should no longer be hidden when the field is re-focused.
357 _userHidOptions = !_focusNode.hasFocus;
358 _updateActions();
359 _updateOverlay();
360 }
361
362 // Called from fieldViewBuilder when the user submits the field.
363 void _onFieldSubmitted() {
364 if (_options.isEmpty || _userHidOptions) {
365 return;
366 }
367 _select(_options.elementAt(_highlightedOptionIndex.value));
368 }
369
370 // Select the given option and update the widget.
371 void _select(T nextSelection) {
372 if (nextSelection == _selection) {
373 return;
374 }
375 _selection = nextSelection;
376 final String selectionString = widget.displayStringForOption(nextSelection);
377 _textEditingController.value = TextEditingValue(
378 selection: TextSelection.collapsed(offset: selectionString.length),
379 text: selectionString,
380 );
381 _updateActions();
382 _updateOverlay();
383 widget.onSelected?.call(_selection!);
384 }
385
386 void _updateHighlight(int newIndex) {
387 _highlightedOptionIndex.value = _options.isEmpty ? 0 : newIndex % _options.length;
388 }
389
390 void _highlightPreviousOption(AutocompletePreviousOptionIntent intent) {
391 if (_userHidOptions) {
392 _userHidOptions = false;
393 _updateActions();
394 _updateOverlay();
395 return;
396 }
397 _updateHighlight(_highlightedOptionIndex.value - 1);
398 }
399
400 void _highlightNextOption(AutocompleteNextOptionIntent intent) {
401 if (_userHidOptions) {
402 _userHidOptions = false;
403 _updateActions();
404 _updateOverlay();
405 return;
406 }
407 _updateHighlight(_highlightedOptionIndex.value + 1);
408 }
409
410 Object? _hideOptions(DismissIntent intent) {
411 if (!_userHidOptions) {
412 _userHidOptions = true;
413 _updateActions();
414 _updateOverlay();
415 return null;
416 }
417 return Actions.invoke(context, intent);
418 }
419
420 void _setActionsEnabled(bool enabled) {
421 // The enabled state determines whether the action will consume the
422 // key shortcut or let it continue on to the underlying text field.
423 // They should only be enabled when the options are showing so shortcuts
424 // can be used to navigate them.
425 _previousOptionAction.enabled = enabled;
426 _nextOptionAction.enabled = enabled;
427 _hideOptionsAction.enabled = enabled;
428 }
429
430 void _updateActions() {
431 _setActionsEnabled(_focusNode.hasFocus && _selection == null && _options.isNotEmpty);
432 }
433
434 bool _floatingOptionsUpdateScheduled = false;
435 // Hide or show the options overlay, if needed.
436 void _updateOverlay() {
437 if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
438 if (!_floatingOptionsUpdateScheduled) {
439 _floatingOptionsUpdateScheduled = true;
440 SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
441 _floatingOptionsUpdateScheduled = false;
442 _updateOverlay();
443 }, debugLabel: 'RawAutoComplete.updateOverlay');
444 }
445 return;
446 }
447
448 _floatingOptions?.remove();
449 _floatingOptions?.dispose();
450 if (_shouldShowOptions) {
451 final OverlayEntry newFloatingOptions = OverlayEntry(
452 builder: (BuildContext context) {
453 return CompositedTransformFollower(
454 link: _optionsLayerLink,
455 showWhenUnlinked: false,
456 targetAnchor: switch (widget.optionsViewOpenDirection) {
457 OptionsViewOpenDirection.up => Alignment.topLeft,
458 OptionsViewOpenDirection.down => Alignment.bottomLeft,
459 },
460 followerAnchor: switch (widget.optionsViewOpenDirection) {
461 OptionsViewOpenDirection.up => Alignment.bottomLeft,
462 OptionsViewOpenDirection.down => Alignment.topLeft,
463 },
464 child: TextFieldTapRegion(
465 child: AutocompleteHighlightedOption(
466 highlightIndexNotifier: _highlightedOptionIndex,
467 child: Builder(
468 builder: (BuildContext context) {
469 return widget.optionsViewBuilder(context, _select, _options);
470 }
471 )
472 ),
473 ),
474 );
475 },
476 );
477 Overlay.of(context, rootOverlay: true, debugRequiredFor: widget).insert(newFloatingOptions);
478 _floatingOptions = newFloatingOptions;
479 } else {
480 _floatingOptions = null;
481 }
482 }
483
484 // Handle a potential change in textEditingController by properly disposing of
485 // the old one and setting up the new one, if needed.
486 void _updateTextEditingController(TextEditingController? old, TextEditingController? current) {
487 if ((old == null && current == null) || old == current) {
488 return;
489 }
490 if (old == null) {
491 _textEditingController.removeListener(_onChangedField);
492 _textEditingController.dispose();
493 _textEditingController = current!;
494 } else if (current == null) {
495 _textEditingController.removeListener(_onChangedField);
496 _textEditingController = TextEditingController();
497 } else {
498 _textEditingController.removeListener(_onChangedField);
499 _textEditingController = current;
500 }
501 _textEditingController.addListener(_onChangedField);
502 }
503
504 // Handle a potential change in focusNode by properly disposing of the old one
505 // and setting up the new one, if needed.
506 void _updateFocusNode(FocusNode? old, FocusNode? current) {
507 if ((old == null && current == null) || old == current) {
508 return;
509 }
510 if (old == null) {
511 _focusNode.removeListener(_onChangedFocus);
512 _focusNode.dispose();
513 _focusNode = current!;
514 } else if (current == null) {
515 _focusNode.removeListener(_onChangedFocus);
516 _focusNode = FocusNode();
517 } else {
518 _focusNode.removeListener(_onChangedFocus);
519 _focusNode = current;
520 }
521 _focusNode.addListener(_onChangedFocus);
522 }
523
524 @override
525 void initState() {
526 super.initState();
527 _textEditingController = widget.textEditingController ?? TextEditingController.fromValue(widget.initialValue);
528 _textEditingController.addListener(_onChangedField);
529 _focusNode = widget.focusNode ?? FocusNode();
530 _focusNode.addListener(_onChangedFocus);
531 _previousOptionAction = _AutocompleteCallbackAction<AutocompletePreviousOptionIntent>(onInvoke: _highlightPreviousOption);
532 _nextOptionAction = _AutocompleteCallbackAction<AutocompleteNextOptionIntent>(onInvoke: _highlightNextOption);
533 _hideOptionsAction = _AutocompleteCallbackAction<DismissIntent>(onInvoke: _hideOptions);
534 _actionMap = <Type, Action<Intent>> {
535 AutocompletePreviousOptionIntent: _previousOptionAction,
536 AutocompleteNextOptionIntent: _nextOptionAction,
537 DismissIntent: _hideOptionsAction,
538 };
539 _updateActions();
540 _updateOverlay();
541 }
542
543 @override
544 void didUpdateWidget(RawAutocomplete<T> oldWidget) {
545 super.didUpdateWidget(oldWidget);
546 _updateTextEditingController(
547 oldWidget.textEditingController,
548 widget.textEditingController,
549 );
550 _updateFocusNode(oldWidget.focusNode, widget.focusNode);
551 _updateActions();
552 _updateOverlay();
553 }
554
555 @override
556 void dispose() {
557 _textEditingController.removeListener(_onChangedField);
558 if (widget.textEditingController == null) {
559 _textEditingController.dispose();
560 }
561 _focusNode.removeListener(_onChangedFocus);
562 if (widget.focusNode == null) {
563 _focusNode.dispose();
564 }
565 _floatingOptions?.remove();
566 _floatingOptions?.dispose();
567 _floatingOptions = null;
568 _highlightedOptionIndex.dispose();
569 super.dispose();
570 }
571
572 @override
573 Widget build(BuildContext context) {
574 return TextFieldTapRegion(
575 child: Container(
576 key: _fieldKey,
577 child: Shortcuts(
578 shortcuts: _shortcuts,
579 child: Actions(
580 actions: _actionMap,
581 child: CompositedTransformTarget(
582 link: _optionsLayerLink,
583 child: widget.fieldViewBuilder == null
584 ? const SizedBox.shrink()
585 : widget.fieldViewBuilder!(
586 context,
587 _textEditingController,
588 _focusNode,
589 _onFieldSubmitted,
590 ),
591 ),
592 ),
593 ),
594 ),
595 );
596 }
597}
598
599class _AutocompleteCallbackAction<T extends Intent> extends CallbackAction<T> {
600 _AutocompleteCallbackAction({
601 required super.onInvoke,
602 this.enabled = true,
603 });
604
605 bool enabled;
606
607 @override
608 bool isEnabled(covariant T intent) => enabled;
609
610 @override
611 bool consumesKey(covariant T intent) => enabled;
612}
613
614/// An [Intent] to highlight the previous option in the autocomplete list.
615class AutocompletePreviousOptionIntent extends Intent {
616 /// Creates an instance of AutocompletePreviousOptionIntent.
617 const AutocompletePreviousOptionIntent();
618}
619
620/// An [Intent] to highlight the next option in the autocomplete list.
621class AutocompleteNextOptionIntent extends Intent {
622 /// Creates an instance of AutocompleteNextOptionIntent.
623 const AutocompleteNextOptionIntent();
624}
625
626/// An inherited widget used to indicate which autocomplete option should be
627/// highlighted for keyboard navigation.
628///
629/// The `RawAutoComplete` widget will wrap the options view generated by the
630/// `optionsViewBuilder` with this widget to provide the highlighted option's
631/// index to the builder.
632///
633/// In the builder callback the index of the highlighted option can be obtained
634/// by using the static [of] method:
635///
636/// ```dart
637/// int highlightedIndex = AutocompleteHighlightedOption.of(context);
638/// ```
639///
640/// which can then be used to tell which option should be given a visual
641/// indication that will be the option selected with the keyboard.
642class AutocompleteHighlightedOption extends InheritedNotifier<ValueNotifier<int>> {
643 /// Create an instance of AutocompleteHighlightedOption inherited widget.
644 const AutocompleteHighlightedOption({
645 super.key,
646 required ValueNotifier<int> highlightIndexNotifier,
647 required super.child,
648 }) : super(notifier: highlightIndexNotifier);
649
650 /// Returns the index of the highlighted option from the closest
651 /// [AutocompleteHighlightedOption] ancestor.
652 ///
653 /// If there is no ancestor, it returns 0.
654 ///
655 /// Typical usage is as follows:
656 ///
657 /// ```dart
658 /// int highlightedIndex = AutocompleteHighlightedOption.of(context);
659 /// ```
660 static int of(BuildContext context) {
661 return context.dependOnInheritedWidgetOfExactType<AutocompleteHighlightedOption>()?.notifier?.value ?? 0;
662 }
663}
664