autocomplete.dart 22.3 KB
Newer Older
1 2 3 4
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:async';

7
import 'package:flutter/scheduler.dart';
8
import 'package:flutter/services.dart';
9

10
import 'actions.dart';
11 12 13 14 15
import 'basic.dart';
import 'container.dart';
import 'editable_text.dart';
import 'focus_manager.dart';
import 'framework.dart';
16
import 'inherited_notifier.dart';
17
import 'overlay.dart';
18
import 'shortcuts.dart';
19
import 'tap_region.dart';
20

21 22 23
// Examples can assume:
// late BuildContext context;

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 27 28
/// entered so far.
///
/// See also:
29
///
30
///   * [RawAutocomplete.optionsBuilder], which is of this type.
31
typedef 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 35 36
/// that the user has selected an option.
///
/// See also:
37
///
38
///   * [RawAutocomplete.onSelected], which is of this type.
39 40
typedef AutocompleteOnSelected<T extends Object> = void Function(T option);

41
/// The type of the [RawAutocomplete] callback which returns a [Widget] that
42 43 44
/// displays the specified [options] and calls [onSelected] if the user
/// selects an option.
///
45 46 47 48 49
/// The returned widget from this callback will be wrapped in an
/// [AutocompleteHighlightedOption] inherited widget. This will allow
/// this callback to determine which option is currently highlighted for
/// keyboard navigation.
///
50
/// See also:
51
///
52
///   * [RawAutocomplete.optionsViewBuilder], which is of this type.
53 54 55 56 57 58 59 60 61 62
typedef AutocompleteOptionsViewBuilder<T extends Object> = Widget Function(
  BuildContext context,
  AutocompleteOnSelected<T> onSelected,
  Iterable<T> options,
);

/// The type of the Autocomplete callback which returns the widget that
/// contains the input [TextField] or [TextFormField].
///
/// See also:
63
///
64
///   * [RawAutocomplete.fieldViewBuilder], which is of this type.
65 66 67 68 69 70 71
typedef AutocompleteFieldViewBuilder = Widget Function(
  BuildContext context,
  TextEditingController textEditingController,
  FocusNode focusNode,
  VoidCallback onFieldSubmitted,
);

72
/// The type of the [RawAutocomplete] callback that converts an option value to
73 74 75
/// a string which can be displayed in the widget's options menu.
///
/// See also:
76
///
77
///   * [RawAutocomplete.displayStringForOption], which is of this type.
78 79
typedef AutocompleteOptionToString<T extends Object> = String Function(T option);

80 81
// TODO(justinmc): Mention AutocompleteCupertino when it is implemented.
/// {@template flutter.widgets.RawAutocomplete.RawAutocomplete}
82 83 84 85 86 87
/// A widget for helping the user make a selection by entering some text and
/// choosing from among a list of options.
///
/// The user's text input is received in a field built with the
/// [fieldViewBuilder] parameter. The options to be displayed are determined
/// using [optionsBuilder] and rendered with [optionsViewBuilder].
88 89 90
/// {@endtemplate}
///
/// This is a core framework widget with very basic UI.
91
///
92
/// {@tool dartpad}
93 94 95
/// This example shows how to create a very basic autocomplete widget using the
/// [fieldViewBuilder] and [optionsViewBuilder] parameters.
///
96
/// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.0.dart **
97 98 99 100 101 102 103 104
/// {@end-tool}
///
/// The type parameter T represents the type of the options. Most commonly this
/// is a String, as in the example above. However, it's also possible to use
/// another type with a `toString` method, or a custom [displayStringForOption].
/// Options will be compared using `==`, so it may be beneficial to override
/// [Object.==] and [Object.hashCode] for custom types.
///
105
/// {@tool dartpad}
106 107 108
/// This example is similar to the previous example, but it uses a custom T data
/// type instead of directly using String.
///
109
/// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.1.dart **
110 111
/// {@end-tool}
///
112
/// {@tool dartpad}
113
/// This example shows the use of RawAutocomplete in a form.
114
///
115
/// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.2.dart **
116
/// {@end-tool}
117 118 119 120 121
///
/// See also:
///
///  * [Autocomplete], which is a Material-styled implementation that is based
/// on RawAutocomplete.
122 123
class RawAutocomplete<T extends Object> extends StatefulWidget {
  /// Create an instance of RawAutocomplete.
124
  ///
125 126
  /// [displayStringForOption], [optionsBuilder] and [optionsViewBuilder] must
  /// not be null.
127
  const RawAutocomplete({
128
    super.key,
129 130
    required this.optionsViewBuilder,
    required this.optionsBuilder,
131
    this.displayStringForOption = defaultStringForOption,
132 133
    this.fieldViewBuilder,
    this.focusNode,
134
    this.onSelected,
135
    this.textEditingController,
136
    this.initialValue,
137
  }) : assert(
138 139 140 141 142
         fieldViewBuilder != null
            || (key != null && focusNode != null && textEditingController != null),
         'Pass in a fieldViewBuilder, or otherwise create a separate field and pass in the FocusNode, TextEditingController, and a key. Use the key with RawAutocomplete.onFieldSubmitted.',
        ),
       assert((focusNode == null) == (textEditingController == null)),
143 144
       assert(
         !(textEditingController != null && initialValue != null),
145
         'textEditingController and initialValue cannot be simultaneously defined.',
146
       );
147

148
  /// {@template flutter.widgets.RawAutocomplete.fieldViewBuilder}
149 150 151
  /// Builds the field whose input is used to get the options.
  ///
  /// Pass the provided [TextEditingController] to the field built here so that
152
  /// RawAutocomplete can listen for changes.
153
  /// {@endtemplate}
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
  final AutocompleteFieldViewBuilder? fieldViewBuilder;

  /// The [FocusNode] that is used for the text field.
  ///
  /// {@template flutter.widgets.RawAutocomplete.split}
  /// The main purpose of this parameter is to allow the use of a separate text
  /// field located in another part of the widget tree instead of the text
  /// field built by [fieldViewBuilder]. For example, it may be desirable to
  /// place the text field in the AppBar and the options below in the main body.
  ///
  /// When following this pattern, [fieldViewBuilder] can return
  /// `SizedBox.shrink()` so that nothing is drawn where the text field would
  /// normally be. A separate text field can be created elsewhere, and a
  /// FocusNode and TextEditingController can be passed both to that text field
  /// and to RawAutocomplete.
  ///
170
  /// {@tool dartpad}
171 172 173
  /// This examples shows how to create an autocomplete widget with the text
  /// field in the AppBar and the results in the main body of the app.
  ///
174
  /// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.focus_node.0.dart **
175 176 177 178 179 180
  /// {@end-tool}
  /// {@endtemplate}
  ///
  /// If this parameter is not null, then [textEditingController] must also be
  /// not null.
  final FocusNode? focusNode;
181

182
  /// {@template flutter.widgets.RawAutocomplete.optionsViewBuilder}
183 184 185 186
  /// Builds the selectable options widgets from a list of options objects.
  ///
  /// The options are displayed floating below the field using a
  /// [CompositedTransformFollower] inside of an [Overlay], not at the same
187
  /// place in the widget tree as [RawAutocomplete].
188 189 190 191 192 193 194 195 196
  ///
  /// In order to track which item is highlighted by keyboard navigation, the
  /// resulting options will be wrapped in an inherited
  /// [AutocompleteHighlightedOption] widget.
  /// Inside this callback, the index of the highlighted option can be obtained
  /// from [AutocompleteHighlightedOption.of] to display the highlighted option
  /// with a visual highlight to indicate it will be the option selected from
  /// the keyboard.
  ///
197
  /// {@endtemplate}
198 199
  final AutocompleteOptionsViewBuilder<T> optionsViewBuilder;

200
  /// {@template flutter.widgets.RawAutocomplete.displayStringForOption}
201 202 203 204 205 206
  /// Returns the string to display in the field when the option is selected.
  ///
  /// This is useful when using a custom T type and the string to display is
  /// different than the string to search by.
  ///
  /// If not provided, will use `option.toString()`.
207
  /// {@endtemplate}
208 209
  final AutocompleteOptionToString<T> displayStringForOption;

210
  /// {@template flutter.widgets.RawAutocomplete.onSelected}
211 212 213 214 215
  /// Called when an option is selected by the user.
  ///
  /// Any [TextEditingController] listeners will not be called when the user
  /// selects an option, even though the field will update with the selected
  /// value, so use this to be informed of selection.
216
  /// {@endtemplate}
217 218
  final AutocompleteOnSelected<T>? onSelected;

219
  /// {@template flutter.widgets.RawAutocomplete.optionsBuilder}
220 221
  /// A function that returns the current selectable options objects given the
  /// current TextEditingValue.
222
  /// {@endtemplate}
223 224
  final AutocompleteOptionsBuilder<T> optionsBuilder;

225 226 227 228 229 230 231
  /// The [TextEditingController] that is used for the text field.
  ///
  /// {@macro flutter.widgets.RawAutocomplete.split}
  ///
  /// If this parameter is not null, then [focusNode] must also be not null.
  final TextEditingController? textEditingController;

232 233 234 235 236 237 238 239 240 241
  /// {@template flutter.widgets.RawAutocomplete.initialValue}
  /// The initial value to use for the text field.
  /// {@endtemplate}
  ///
  /// Setting the initial value does not notify [textEditingController]'s
  /// listeners, and thus will not cause the options UI to appear.
  ///
  /// This parameter is ignored if [textEditingController] is defined.
  final TextEditingValue? initialValue;

242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
  /// Calls [AutocompleteFieldViewBuilder]'s onFieldSubmitted callback for the
  /// RawAutocomplete widget indicated by the given [GlobalKey].
  ///
  /// This is not typically used unless a custom field is implemented instead of
  /// using [fieldViewBuilder]. In the typical case, the onFieldSubmitted
  /// callback is passed via the [AutocompleteFieldViewBuilder] signature. When
  /// not using fieldViewBuilder, the same callback can be called by using this
  /// static method.
  ///
  /// See also:
  ///
  ///  * [focusNode] and [textEditingController], which contain a code example
  ///    showing how to create a separate field outside of fieldViewBuilder.
  static void onFieldSubmitted<T extends Object>(GlobalKey key) {
    final _RawAutocompleteState<T> rawAutocomplete = key.currentState! as _RawAutocompleteState<T>;
    rawAutocomplete._onFieldSubmitted();
  }

260 261 262
  /// The default way to convert an option to a string in
  /// [displayStringForOption].
  ///
263 264
  /// Uses the `toString` method of the given `option`.
  static String defaultStringForOption(Object? option) {
265 266 267 268
    return option.toString();
  }

  @override
269
  State<RawAutocomplete<T>> createState() => _RawAutocompleteState<T>();
270 271
}

272
class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>> {
273 274
  final GlobalKey _fieldKey = GlobalKey();
  final LayerLink _optionsLayerLink = LayerLink();
275 276
  late TextEditingController _textEditingController;
  late FocusNode _focusNode;
277 278 279
  late final Map<Type, Action<Intent>> _actionMap;
  late final _AutocompleteCallbackAction<AutocompletePreviousOptionIntent> _previousOptionAction;
  late final _AutocompleteCallbackAction<AutocompleteNextOptionIntent> _nextOptionAction;
280
  late final _AutocompleteCallbackAction<DismissIntent> _hideOptionsAction;
281 282
  Iterable<T> _options = Iterable<T>.empty();
  T? _selection;
283 284
  bool _userHidOptions = false;
  String _lastFieldText = '';
285 286 287 288 289 290
  final ValueNotifier<int> _highlightedOptionIndex = ValueNotifier<int>(0);

  static const Map<ShortcutActivator, Intent> _shortcuts = <ShortcutActivator, Intent>{
    SingleActivator(LogicalKeyboardKey.arrowUp): AutocompletePreviousOptionIntent(),
    SingleActivator(LogicalKeyboardKey.arrowDown): AutocompleteNextOptionIntent(),
  };
291 292 293 294 295 296

  // The OverlayEntry containing the options.
  OverlayEntry? _floatingOptions;

  // True iff the state indicates that the options should be visible.
  bool get _shouldShowOptions {
297
    return !_userHidOptions && _focusNode.hasFocus && _selection == null && _options.isNotEmpty;
298 299 300
  }

  // Called when _textEditingController changes.
301
  Future<void> _onChangedField() async {
302
    final TextEditingValue value = _textEditingController.value;
303
    final Iterable<T> options = await widget.optionsBuilder(
304
      value,
305 306
    );
    _options = options;
307
    _updateHighlight(_highlightedOptionIndex.value);
308
    if (_selection != null
309
        && value.text != widget.displayStringForOption(_selection!)) {
310 311
      _selection = null;
    }
312 313 314 315 316 317 318

    // Make sure the options are no longer hidden if the content of the field
    // changes (ignore selection changes).
    if (value.text != _lastFieldText) {
      _userHidOptions = false;
      _lastFieldText = value.text;
    }
319
    _updateActions();
320 321 322 323 324
    _updateOverlay();
  }

  // Called when the field's FocusNode changes.
  void _onChangedFocus() {
325 326
    // Options should no longer be hidden when the field is re-focused.
    _userHidOptions = !_focusNode.hasFocus;
327
    _updateActions();
328 329 330 331 332
    _updateOverlay();
  }

  // Called from fieldViewBuilder when the user submits the field.
  void _onFieldSubmitted() {
333
    if (_options.isEmpty || _userHidOptions) {
334 335
      return;
    }
336
    _select(_options.elementAt(_highlightedOptionIndex.value));
337 338 339 340 341 342 343 344 345 346 347 348 349
  }

  // Select the given option and update the widget.
  void _select(T nextSelection) {
    if (nextSelection == _selection) {
      return;
    }
    _selection = nextSelection;
    final String selectionString = widget.displayStringForOption(nextSelection);
    _textEditingController.value = TextEditingValue(
      selection: TextSelection.collapsed(offset: selectionString.length),
      text: selectionString,
    );
350 351
    _updateActions();
    _updateOverlay();
352 353 354
    widget.onSelected?.call(_selection!);
  }

355 356 357 358 359
  void _updateHighlight(int newIndex) {
    _highlightedOptionIndex.value = _options.isEmpty ? 0 : newIndex % _options.length;
  }

  void _highlightPreviousOption(AutocompletePreviousOptionIntent intent) {
360 361
    if (_userHidOptions) {
      _userHidOptions = false;
362
      _updateActions();
363 364 365
      _updateOverlay();
      return;
    }
366 367 368 369
    _updateHighlight(_highlightedOptionIndex.value - 1);
  }

  void _highlightNextOption(AutocompleteNextOptionIntent intent) {
370 371
    if (_userHidOptions) {
      _userHidOptions = false;
372
      _updateActions();
373 374 375
      _updateOverlay();
      return;
    }
376 377 378
    _updateHighlight(_highlightedOptionIndex.value + 1);
  }

379
  Object? _hideOptions(DismissIntent intent) {
380 381
    if (!_userHidOptions) {
      _userHidOptions = true;
382
      _updateActions();
383
      _updateOverlay();
384
      return null;
385
    }
386
    return Actions.invoke(context, intent);
387 388
  }

389 390 391 392 393 394 395
  void _setActionsEnabled(bool enabled) {
    // The enabled state determines whether the action will consume the
    // key shortcut or let it continue on to the underlying text field.
    // They should only be enabled when the options are showing so shortcuts
    // can be used to navigate them.
    _previousOptionAction.enabled = enabled;
    _nextOptionAction.enabled = enabled;
396
    _hideOptionsAction.enabled = enabled;
397 398
  }

399 400 401 402 403
  void _updateActions() {
    _setActionsEnabled(_focusNode.hasFocus && _selection == null && _options.isNotEmpty);
  }

  bool _floatingOptionsUpdateScheduled = false;
404 405
  // Hide or show the options overlay, if needed.
  void _updateOverlay() {
406 407 408 409 410 411 412 413 414 415 416 417
    if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
      if (!_floatingOptionsUpdateScheduled) {
        _floatingOptionsUpdateScheduled = true;
        SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
          _floatingOptionsUpdateScheduled = false;
          _updateOverlay();
        });
      }
      return;
    }

    _floatingOptions?.remove();
418
    if (_shouldShowOptions) {
419
      final OverlayEntry newFloatingOptions = OverlayEntry(
420 421 422 423 424
        builder: (BuildContext context) {
          return CompositedTransformFollower(
            link: _optionsLayerLink,
            showWhenUnlinked: false,
            targetAnchor: Alignment.bottomLeft,
425 426 427 428 429 430 431 432 433
            child: TextFieldTapRegion(
              child: AutocompleteHighlightedOption(
                highlightIndexNotifier: _highlightedOptionIndex,
                child: Builder(
                  builder: (BuildContext context) {
                    return widget.optionsViewBuilder(context, _select, _options);
                  }
                )
              ),
434
            ),
435 436 437
          );
        },
      );
438
      Overlay.of(context, rootOverlay: true, debugRequiredFor: widget).insert(newFloatingOptions);
439 440
      _floatingOptions = newFloatingOptions;
    } else {
441 442 443 444
      _floatingOptions = null;
    }
  }

445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484
  // Handle a potential change in textEditingController by properly disposing of
  // the old one and setting up the new one, if needed.
  void _updateTextEditingController(TextEditingController? old, TextEditingController? current) {
    if ((old == null && current == null) || old == current) {
      return;
    }
    if (old == null) {
      _textEditingController.removeListener(_onChangedField);
      _textEditingController.dispose();
      _textEditingController = current!;
    } else if (current == null) {
      _textEditingController.removeListener(_onChangedField);
      _textEditingController = TextEditingController();
    } else {
      _textEditingController.removeListener(_onChangedField);
      _textEditingController = current;
    }
    _textEditingController.addListener(_onChangedField);
  }

  // Handle a potential change in focusNode by properly disposing of the old one
  // and setting up the new one, if needed.
  void _updateFocusNode(FocusNode? old, FocusNode? current) {
    if ((old == null && current == null) || old == current) {
      return;
    }
    if (old == null) {
      _focusNode.removeListener(_onChangedFocus);
      _focusNode.dispose();
      _focusNode = current!;
    } else if (current == null) {
      _focusNode.removeListener(_onChangedFocus);
      _focusNode = FocusNode();
    } else {
      _focusNode.removeListener(_onChangedFocus);
      _focusNode = current;
    }
    _focusNode.addListener(_onChangedFocus);
  }

485 486 487
  @override
  void initState() {
    super.initState();
488
    _textEditingController = widget.textEditingController ?? TextEditingController.fromValue(widget.initialValue);
489
    _textEditingController.addListener(_onChangedField);
490
    _focusNode = widget.focusNode ?? FocusNode();
491
    _focusNode.addListener(_onChangedFocus);
492 493
    _previousOptionAction = _AutocompleteCallbackAction<AutocompletePreviousOptionIntent>(onInvoke: _highlightPreviousOption);
    _nextOptionAction = _AutocompleteCallbackAction<AutocompleteNextOptionIntent>(onInvoke: _highlightNextOption);
494
    _hideOptionsAction = _AutocompleteCallbackAction<DismissIntent>(onInvoke: _hideOptions);
495 496 497
    _actionMap = <Type, Action<Intent>> {
      AutocompletePreviousOptionIntent: _previousOptionAction,
      AutocompleteNextOptionIntent: _nextOptionAction,
498
      DismissIntent: _hideOptionsAction,
499
    };
500 501
    _updateActions();
    _updateOverlay();
502 503 504
  }

  @override
505
  void didUpdateWidget(RawAutocomplete<T> oldWidget) {
506
    super.didUpdateWidget(oldWidget);
507 508 509 510 511
    _updateTextEditingController(
      oldWidget.textEditingController,
      widget.textEditingController,
    );
    _updateFocusNode(oldWidget.focusNode, widget.focusNode);
512 513
    _updateActions();
    _updateOverlay();
514 515 516 517 518
  }

  @override
  void dispose() {
    _textEditingController.removeListener(_onChangedField);
519 520 521
    if (widget.textEditingController == null) {
      _textEditingController.dispose();
    }
522
    _focusNode.removeListener(_onChangedFocus);
523 524 525
    if (widget.focusNode == null) {
      _focusNode.dispose();
    }
526 527 528 529 530 531 532
    _floatingOptions?.remove();
    _floatingOptions = null;
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550
    return TextFieldTapRegion(
      child: Container(
        key: _fieldKey,
        child: Shortcuts(
          shortcuts: _shortcuts,
          child: Actions(
            actions: _actionMap,
            child: CompositedTransformTarget(
              link: _optionsLayerLink,
              child: widget.fieldViewBuilder == null
                ? const SizedBox.shrink()
                : widget.fieldViewBuilder!(
                    context,
                    _textEditingController,
                    _focusNode,
                    _onFieldSubmitted,
                  ),
            ),
551 552
          ),
        ),
553 554 555 556
      ),
    );
  }
}
557 558 559

class _AutocompleteCallbackAction<T extends Intent> extends CallbackAction<T> {
  _AutocompleteCallbackAction({
560
    required super.onInvoke,
561
    this.enabled = true,
562
  });
563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595

  bool enabled;

  @override
  bool isEnabled(covariant T intent) => enabled;

  @override
  bool consumesKey(covariant T intent) => enabled;
}

/// An [Intent] to highlight the previous option in the autocomplete list.
class AutocompletePreviousOptionIntent extends Intent {
  /// Creates an instance of AutocompletePreviousOptionIntent.
  const AutocompletePreviousOptionIntent();
}

/// An [Intent] to highlight the next option in the autocomplete list.
class AutocompleteNextOptionIntent extends Intent {
  /// Creates an instance of AutocompleteNextOptionIntent.
  const AutocompleteNextOptionIntent();
}

/// An inherited widget used to indicate which autocomplete option should be
/// highlighted for keyboard navigation.
///
/// The `RawAutoComplete` widget will wrap the options view generated by the
/// `optionsViewBuilder` with this widget to provide the highlighted option's
/// index to the builder.
///
/// In the builder callback the index of the highlighted option can be obtained
/// by using the static [of] method:
///
/// ```dart
596
/// int highlightedIndex = AutocompleteHighlightedOption.of(context);
597 598 599 600 601 602 603
/// ```
///
/// which can then be used to tell which option should be given a visual
/// indication that will be the option selected with the keyboard.
class AutocompleteHighlightedOption extends InheritedNotifier<ValueNotifier<int>> {
  /// Create an instance of AutocompleteHighlightedOption inherited widget.
  const AutocompleteHighlightedOption({
604
    super.key,
605
    required ValueNotifier<int> highlightIndexNotifier,
606 607
    required super.child,
  }) : super(notifier: highlightIndexNotifier);
608 609 610 611 612 613 614 615 616

  /// Returns the index of the highlighted option from the closest
  /// [AutocompleteHighlightedOption] ancestor.
  ///
  /// If there is no ancestor, it returns 0.
  ///
  /// Typical usage is as follows:
  ///
  /// ```dart
617
  /// int highlightedIndex = AutocompleteHighlightedOption.of(context);
618 619 620 621 622
  /// ```
  static int of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<AutocompleteHighlightedOption>()?.notifier?.value ?? 0;
  }
}