Skip to content

Conversation

@markusaksli-nc
Copy link
Contributor

@markusaksli-nc markusaksli-nc commented Feb 4, 2022

Allows the Autocomplete options to be hidden by pressing ESC. As for revealing the options again here's how it should work:

  • When the text is edited (but not when moving the caret around or selecting text)
  • When the field is re-focused
  • When up or down arrows are pressed for navigating autocomplete options

This is mostly based off of the angular implementation of material autocomplete but there are some discrepancies:

  • Due to _selection == null in
    return _focusNode.hasFocus && _selection == null && _options.isNotEmpty;

    once a selection has been made the autocomplete options will not appear to show the only completed option.
  • Options will not be shown on empty input by default (well it's up to optionsBuilder).

Another interesting detail is that in the angular material implementation the highlighted selection index gets reset when the options are hidden. This is different from something like the search bar in YouTube, where the highlighted option remains the same when the options are shown again by pressing the up or down arrow. The latter is how it works in the current version of the PR.

Basic autocomplete sample
import 'package:flutter/material.dart';

void main() => runApp(const AutocompleteExampleApp());

class AutocompleteExampleApp extends StatelessWidget {
  const AutocompleteExampleApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Autocomplete Basic'),
        ),
        body: const Center(
          child: AutocompleteBasicExample(),
        ),
      ),
    );
  }
}

class AutocompleteBasicExample extends StatelessWidget {
  const AutocompleteBasicExample({Key? key}) : super(key: key);

  static const List<String> _kOptions = <String>[
    'aardvark',
    'bobcat',
    'chameleon',
  ];

  @override
  Widget build(BuildContext context) {
    return Autocomplete<String>(
      optionsBuilder: (TextEditingValue textEditingValue) {
        if (textEditingValue.text == '') {
          return const Iterable<String>.empty();
        }
        return _kOptions.where((String option) {
          return option.contains(textEditingValue.text.toLowerCase());
        });
      },
      onSelected: (String selection) {
        debugPrint('You just selected $selection');
      },
    );
  }
}

Fixes #84416

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I signed the CLA.
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is test-exempt.
  • All existing and new tests are passing.

@flutter-dashboard flutter-dashboard bot added the framework flutter/packages/flutter repository. See also f: labels. label Feb 4, 2022
@flutter-dashboard
Copy link

It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption to this rule, contact Hixie on the #hackers channel in Chat (don't just cc him here, he won't see it! He's on Discord!).

If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix?

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing.

@markusaksli-nc
Copy link
Contributor Author

cc @jonahwilliams @gspencergoog

@HansMuller HansMuller requested a review from justinmc February 4, 2022 22:53
Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking this on, I have multiple times been using an Autocomplete and instinctively tried to dismiss the options with escape, only to be disappointed. We should support this feature.

The biggest point that I brought up in my comments is that we might be able to use Shortcuts instead of onKeyDown. Otherwise my comments are pretty minor and I like the approach.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a space after if here, and in a few other places in this PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the point of _lastText is (tell me if I'm wrong):

Say you're typing into the field with the menu visible, and then you hit escape to close it. It will only reopen again if you modify the text. If you just change the selection then the menu will stay hidden.

Do you need to set _lastText when _onKeyEvent hides the menu too?

As a nit, maybe add a comment at the definition of _lastText and/or here to explain this stuff a bit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You understood correctly but I decided to remove the check for _hidden here to remove any doubts about this being checked against an outdated value.

Comment on lines 339 to 340
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I trust your judgement on this. Both seem like reasonable choices to me. We can keep an eye out for any issues in the future in case users disagree (as well as for the other inconsistencies with Angular Material that you pointed out).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe if we use Shortcuts, as mentioned below, then users could do whichever behavior they want here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I feel like this _hidden piece of state can be implemented in a way that's more clear. My problems with it are:

  1. It's called _hidden, but it's not a direct indication of whether the options are currently hidden or not. It's possible that _hidden is false but the options are not shown.
  2. It encompasses both whether the user has recently pressed escape and the current focus, which is already used elsewhere as _focusNode.hasFocus.

What if we call it something like _userHidOptions and don't change it in _onChangedFocus? Just use _focusNode.hasFocus when you need to know that.

Then we know what it means when it's true (the user hid the options recently and that hasn't yet been reset) and we have a better idea of what implications it has (the options may be hidden, but check _shouldShowOptions to be sure).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that this piece of state also encompasses if the field has focus is just a byproduct of the fact that it needs to be reset when the field is un-focused and re-focused. It might be more clear if the bit in _onChangedFocus is changed to something like

  // Called when the field's FocusNode changes.
  void _onChangedFocus() {
    // Options should no longer be hidden when the field is re-focused.
    if(_userHidOptions && _focusNode.hasFocus) {
      _userHidOptions = false;
    }
    _updateOverlay();
  }

but I just figured this is more concise. If you think it would help readability I'm not against this change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you went with my idea, it's not perfect as you pointed out but hopefully it helps improve readability a bit. I'll try to keep an eye out in case anyone is confused by this and we can reconsider.

@markusaksli-nc
Copy link
Contributor Author

markusaksli-nc commented Feb 5, 2022

Thanks for the fast review @justinmc. I think the Shortcuts solution also makes a lot more sense because the _focusNode can refer to widget.focusNode so it might be unexpectedly overriding onKeyEvent.

One slight snag I ran into was that shortcuts need to be disabled and enabled according to the old _shouldShowOptions without !_userHidOptions. This left me with this silly choice of copying everything from there to _setActionsEnabled in _updateOverlay or changing its definition to something like _canShowOptions and having _canShowOptions && !_userHidOptions in _updateOverlay.

Either way it looks a bit silly with _shouldShowOptions now being used in just one spot.

Also added a test, I just left in a bunch of hiding with the escape key but it could maybe be made more concise somehow?

@markusaksli-nc
Copy link
Contributor Author

Just noticed that this could be using the DismissIntent instead of a custom one (not sure which is preferable).

@gspencergoog
Copy link
Contributor

I'd say DismissIntent is preferable.

Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

Just nits otherwise (DismissIntent and a few other small things).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to @gspencergoog's suggestion to use DismissIntent instead of this new one.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call saving this value locally, I guess it could change after the await.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: "gets changes" => "gets changed" or "changes"? Or maybe I'm reading that wrong...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The styleguide says never use "we" (though that's mainly for public docs).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is super thorough and easy to follow 👍

@markusaksli-nc
Copy link
Contributor Author

Fixed the nit, going to merge and check that this plays well with #98511

@markusaksli-nc
Copy link
Contributor Author

markusaksli-nc commented Feb 16, 2022

Update: they did not play nice together at all, see #98600

engine-flutter-autoroll added a commit to engine-flutter-autoroll/plugins that referenced this pull request Feb 16, 2022
clocksmith pushed a commit to clocksmith/flutter that referenced this pull request Mar 8, 2022
@TahaTesser TahaTesser deleted the dismiss-autocomplete branch September 23, 2022 20:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Web][Desktop]: The dropdown of an AutoComplete widget should be dismissed by an ESC hit or by clicking outside area.

4 participants