autocomplete.dart 22.2 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

20
/// The type of the [RawAutocomplete] callback which computes the list of
21
/// optional completions for the widget's field, based on the text the user has
22 23 24
/// entered so far.
///
/// See also:
25
///
26
///   * [RawAutocomplete.optionsBuilder], which is of this type.
27
typedef AutocompleteOptionsBuilder<T extends Object> = FutureOr<Iterable<T>> Function(TextEditingValue textEditingValue);
28

29
/// The type of the callback used by the [RawAutocomplete] widget to indicate
30 31 32
/// that the user has selected an option.
///
/// See also:
33
///
34
///   * [RawAutocomplete.onSelected], which is of this type.
35 36
typedef AutocompleteOnSelected<T extends Object> = void Function(T option);

37
/// The type of the [RawAutocomplete] callback which returns a [Widget] that
38 39 40
/// displays the specified [options] and calls [onSelected] if the user
/// selects an option.
///
41 42 43 44 45
/// 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.
///
46
/// See also:
47
///
48
///   * [RawAutocomplete.optionsViewBuilder], which is of this type.
49 50 51 52 53 54 55 56 57 58
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:
59
///
60
///   * [RawAutocomplete.fieldViewBuilder], which is of this type.
61 62 63 64 65 66 67
typedef AutocompleteFieldViewBuilder = Widget Function(
  BuildContext context,
  TextEditingController textEditingController,
  FocusNode focusNode,
  VoidCallback onFieldSubmitted,
);

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

76 77
// TODO(justinmc): Mention AutocompleteCupertino when it is implemented.
/// {@template flutter.widgets.RawAutocomplete.RawAutocomplete}
78 79 80 81 82 83
/// 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].
84 85 86
/// {@endtemplate}
///
/// This is a core framework widget with very basic UI.
87
///
88
/// {@tool dartpad}
89 90 91
/// This example shows how to create a very basic autocomplete widget using the
/// [fieldViewBuilder] and [optionsViewBuilder] parameters.
///
92
/// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.0.dart **
93 94 95 96 97 98 99 100
/// {@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.
///
101
/// {@tool dartpad}
102 103 104
/// This example is similar to the previous example, but it uses a custom T data
/// type instead of directly using String.
///
105
/// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.1.dart **
106 107
/// {@end-tool}
///
108
/// {@tool dartpad}
109
/// This example shows the use of RawAutocomplete in a form.
110
///
111
/// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.2.dart **
112
/// {@end-tool}
113 114 115 116 117
///
/// See also:
///
///  * [Autocomplete], which is a Material-styled implementation that is based
/// on RawAutocomplete.
118 119
class RawAutocomplete<T extends Object> extends StatefulWidget {
  /// Create an instance of RawAutocomplete.
120
  ///
121 122
  /// [displayStringForOption], [optionsBuilder] and [optionsViewBuilder] must
  /// not be null.
123
  const RawAutocomplete({
124
    super.key,
125 126
    required this.optionsViewBuilder,
    required this.optionsBuilder,
127
    this.displayStringForOption = defaultStringForOption,
128 129
    this.fieldViewBuilder,
    this.focusNode,
130
    this.onSelected,
131
    this.textEditingController,
132
    this.initialValue,
133
  }) : assert(displayStringForOption != null),
134 135 136 137 138
       assert(
         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.',
        ),
139 140
       assert(optionsBuilder != null),
       assert(optionsViewBuilder != null),
141
       assert((focusNode == null) == (textEditingController == null)),
142 143
       assert(
         !(textEditingController != null && initialValue != null),
144
         'textEditingController and initialValue cannot be simultaneously defined.',
145
       );
146

147
  /// {@template flutter.widgets.RawAutocomplete.fieldViewBuilder}
148 149 150
  /// Builds the field whose input is used to get the options.
  ///
  /// Pass the provided [TextEditingController] to the field built here so that
151
  /// RawAutocomplete can listen for changes.
152
  /// {@endtemplate}
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
  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.
  ///
169
  /// {@tool dartpad}
170 171 172
  /// 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.
  ///
173
  /// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.focus_node.0.dart **
174 175 176 177 178 179
  /// {@end-tool}
  /// {@endtemplate}
  ///
  /// If this parameter is not null, then [textEditingController] must also be
  /// not null.
  final FocusNode? focusNode;
180

181
  /// {@template flutter.widgets.RawAutocomplete.optionsViewBuilder}
182 183 184 185
  /// 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
186
  /// place in the widget tree as [RawAutocomplete].
187 188 189 190 191 192 193 194 195
  ///
  /// 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.
  ///
196
  /// {@endtemplate}
197 198
  final AutocompleteOptionsViewBuilder<T> optionsViewBuilder;

199
  /// {@template flutter.widgets.RawAutocomplete.displayStringForOption}
200 201 202 203 204 205
  /// 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()`.
206
  /// {@endtemplate}
207 208
  final AutocompleteOptionToString<T> displayStringForOption;

209
  /// {@template flutter.widgets.RawAutocomplete.onSelected}
210 211 212 213 214
  /// 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.
215
  /// {@endtemplate}
216 217
  final AutocompleteOnSelected<T>? onSelected;

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

224 225 226 227 228 229 230
  /// 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;

231 232 233 234 235 236 237 238 239 240
  /// {@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;

241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
  /// 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();
  }

259 260 261 262 263
  /// The default way to convert an option to a string in
  /// [displayStringForOption].
  ///
  /// Simply uses the `toString` method on the option.
  static String defaultStringForOption(dynamic option) {
264 265 266 267
    return option.toString();
  }

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

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

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

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

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

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

    // 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;
    }
318
    _updateActions();
319 320 321 322 323
    _updateOverlay();
  }

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

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

  // 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,
    );
349 350
    _updateActions();
    _updateOverlay();
351 352 353
    widget.onSelected?.call(_selection!);
  }

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

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

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

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

388 389 390 391 392 393 394
  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;
395
    _hideOptionsAction.enabled = enabled;
396 397
  }

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

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

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

442 443 444 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
  // 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);
  }

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

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

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

  @override
  Widget build(BuildContext context) {
    return Container(
      key: _fieldKey,
532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547
      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,
                ),
          ),
        ),
548 549 550 551
      ),
    );
  }
}
552 553 554

class _AutocompleteCallbackAction<T extends Intent> extends CallbackAction<T> {
  _AutocompleteCallbackAction({
555
    required super.onInvoke,
556
    this.enabled = true,
557
  });
558 559 560 561 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 596 597 598

  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
/// final highlightedIndex = AutocompleteHighlightedOption.of(context);
/// ```
///
/// 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({
599
    super.key,
600
    required ValueNotifier<int> highlightIndexNotifier,
601 602
    required super.child,
  }) : super(notifier: highlightIndexNotifier);
603 604 605 606 607 608 609 610 611 612 613 614 615 616 617

  /// 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
  /// final highlightedIndex = AutocompleteHighlightedOption.of(context);
  /// ```
  static int of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<AutocompleteHighlightedOption>()?.notifier?.value ?? 0;
  }
}