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';
9
10import 'package:flutter/services.dart';
11
12import 'actions.dart';
13import 'basic.dart';
14import 'editable_text.dart';
15import 'focus_manager.dart';
16import 'framework.dart';
17import 'inherited_notifier.dart';
18import 'overlay.dart';
19import 'shortcuts.dart';
20import '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.
32typedef 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.
40typedef 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.
54typedef 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.
66typedef 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.
79typedef 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.
90enum 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.
146class 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
304class _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
529class _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.
549class 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.
555class 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.
576class 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