autocomplete.dart 20.8 KB
Newer Older
1 2 3 4 5
// 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.

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

8
import 'actions.dart';
9 10 11 12 13
import 'basic.dart';
import 'container.dart';
import 'editable_text.dart';
import 'focus_manager.dart';
import 'framework.dart';
14
import 'inherited_notifier.dart';
15
import 'overlay.dart';
16
import 'shortcuts.dart';
17

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

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

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

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

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

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

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

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

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

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

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

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

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

258 259 260 261 262
  /// 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) {
263 264 265 266
    return option.toString();
  }

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

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

  static const Map<ShortcutActivator, Intent> _shortcuts = <ShortcutActivator, Intent>{
    SingleActivator(LogicalKeyboardKey.arrowUp): AutocompletePreviousOptionIntent(),
    SingleActivator(LogicalKeyboardKey.arrowDown): AutocompleteNextOptionIntent(),
  };
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300

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

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

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

  // Called when the field's FocusNode changes.
  void _onChangedFocus() {
    _updateOverlay();
  }

  // Called from fieldViewBuilder when the user submits the field.
  void _onFieldSubmitted() {
    if (_options.isEmpty) {
      return;
    }
319
    _select(_options.elementAt(_highlightedOptionIndex.value));
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
  }

  // 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,
    );
    widget.onSelected?.call(_selection!);
  }

336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
  void _updateHighlight(int newIndex) {
    _highlightedOptionIndex.value = _options.isEmpty ? 0 : newIndex % _options.length;
  }

  void _highlightPreviousOption(AutocompletePreviousOptionIntent intent) {
    _updateHighlight(_highlightedOptionIndex.value - 1);
  }

  void _highlightNextOption(AutocompleteNextOptionIntent intent) {
    _updateHighlight(_highlightedOptionIndex.value + 1);
  }

  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;
  }

357 358
  // Hide or show the options overlay, if needed.
  void _updateOverlay() {
359
    _setActionsEnabled(_shouldShowOptions);
360 361 362 363 364 365 366 367
    if (_shouldShowOptions) {
      _floatingOptions?.remove();
      _floatingOptions = OverlayEntry(
        builder: (BuildContext context) {
          return CompositedTransformFollower(
            link: _optionsLayerLink,
            showWhenUnlinked: false,
            targetAnchor: Alignment.bottomLeft,
368 369 370 371 372 373 374 375
            child: AutocompleteHighlightedOption(
              highlightIndexNotifier: _highlightedOptionIndex,
              child: Builder(
                builder: (BuildContext context) {
                  return widget.optionsViewBuilder(context, _select, _options);
                }
              )
            ),
376 377 378 379 380 381 382 383 384 385
          );
        },
      );
      Overlay.of(context, rootOverlay: true)!.insert(_floatingOptions!);
    } else if (_floatingOptions != null) {
      _floatingOptions!.remove();
      _floatingOptions = null;
    }
  }

386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425
  // 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);
  }

426 427 428
  @override
  void initState() {
    super.initState();
429
    _textEditingController = widget.textEditingController ?? TextEditingController.fromValue(widget.initialValue);
430
    _textEditingController.addListener(_onChangedField);
431
    _focusNode = widget.focusNode ?? FocusNode();
432
    _focusNode.addListener(_onChangedFocus);
433 434 435 436 437 438
    _previousOptionAction = _AutocompleteCallbackAction<AutocompletePreviousOptionIntent>(onInvoke: _highlightPreviousOption);
    _nextOptionAction = _AutocompleteCallbackAction<AutocompleteNextOptionIntent>(onInvoke: _highlightNextOption);
    _actionMap = <Type, Action<Intent>> {
      AutocompletePreviousOptionIntent: _previousOptionAction,
      AutocompleteNextOptionIntent: _nextOptionAction,
    };
439
    SchedulerBinding.instance!.addPostFrameCallback((Duration _) {
440 441 442 443 444
      _updateOverlay();
    });
  }

  @override
445
  void didUpdateWidget(RawAutocomplete<T> oldWidget) {
446
    super.didUpdateWidget(oldWidget);
447 448 449 450 451
    _updateTextEditingController(
      oldWidget.textEditingController,
      widget.textEditingController,
    );
    _updateFocusNode(oldWidget.focusNode, widget.focusNode);
452
    SchedulerBinding.instance!.addPostFrameCallback((Duration _) {
453 454 455 456 457 458 459
      _updateOverlay();
    });
  }

  @override
  void dispose() {
    _textEditingController.removeListener(_onChangedField);
460 461 462
    if (widget.textEditingController == null) {
      _textEditingController.dispose();
    }
463
    _focusNode.removeListener(_onChangedFocus);
464 465 466
    if (widget.focusNode == null) {
      _focusNode.dispose();
    }
467 468 469 470 471 472 473 474 475
    _floatingOptions?.remove();
    _floatingOptions = null;
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      key: _fieldKey,
476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491
      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,
                ),
          ),
        ),
492 493 494 495
      ),
    );
  }
}
496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565

class _AutocompleteCallbackAction<T extends Intent> extends CallbackAction<T> {
  _AutocompleteCallbackAction({
    required OnInvokeCallback<T> onInvoke,
    this.enabled = true,
  }) : super(onInvoke: onInvoke);

  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.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class AutocompletePreviousOptionIntent extends Intent {
  /// Creates an instance of AutocompletePreviousOptionIntent.
  const AutocompletePreviousOptionIntent();
}

/// An [Intent] to highlight the next option in the autocomplete list.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
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({
    Key? key,
    required ValueNotifier<int> highlightIndexNotifier,
    required Widget child,
  }) : super(key: key, notifier: highlightIndexNotifier, child: child);

  /// 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;
  }
}