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