mui / material-ui

Material UI: Comprehensive React component library that implements Google's Material Design. Free forever.
https://mui.com/material-ui/
MIT License
94.11k stars 32.34k forks source link

[Autocomplete] New API to control the highlighted option #20852

Open KamalAman opened 4 years ago

KamalAman commented 4 years ago

Summary 💡

Typing into the auto-complete and pressing enter should select something, such as the suggested option. The suggested option to select should be highlighted.

Examples 🌈

For example type in Good into the playground and press enter, nothing gets selected.

Lets say I have a text matching score when searching for Good as following: The Good, the Bad and the Ugly = 0.3 Goodfellas = 0.8

I would want Goodfellas to be highlighted and selected when enter is pressed.

Motivation 🔦

The current auto-complete logic makes suggesting an option difficult.

Passing in a highlightedValue/suggestedValue would make the above possible. Set the first value in the list to be highlighted if one is not provided.

esseswann commented 4 years ago

The threshold for auto focus should probably be configurable because in certain cases one would not want motivate user to use doubtful matches

KamalAman commented 4 years ago

@esseswann, What i am thinking is that autocomplete does not know anything about the threshold. The only api change I would make is a new prop called highlightedValue or suggestedValue and then any concept of a threshold would live in the developers component.

esseswann commented 4 years ago

Sounds reasonable though it should probably be something about focused. Also there should be a discussion on weather this prop should allow focusing on only the first item

oliviertassinari commented 4 years ago

@KamalAman So, you are supporting the position of a autoHighlight value to true by default, for the combo box cases.

oliviertassinari commented 4 years ago

Yeah, ok, looking at more implementations, UI libraries & final products, this sounds like a sensible default. So we can 1. turn autoHighlight = false into autoHighlight = !props.freeSolo and 2. account for the change in https://material-ui.com/components/autocomplete/#creatable. What do you guys think?

KamalAman commented 4 years ago

@oliviertassinari, I think autoHighlight = !props.freeSolo makes sense so that it is on by default when freeSolo is false.

However, the main feature i would like to see is being able to pass in a highlightedValue such that the value is highlighted (and scrolled to, which is easy) if it is deemed to be a better choice by some external logic.

oliviertassinari commented 4 years ago

@KamalAman Ok, I think that you are making a compelling use case for:

What do you think of this API to anticipate for a future case when somebody will need to control the highlighted option?

https://github.com/mui-org/material-ui/issues/20588#issuecomment-614725233

Regarding the implementation, how do you envision it?

CliffChaney commented 4 years ago

I can't vote for this suggestion enough. To fit my use case I would amend one tiny bit. If the input value completely matches one of the options - the matched option would be the one selected. So either an additional option that performs this requested function ONLY when there's an exact match or this same option with a preference to "choose" the exact match first. Consider an autocomplete color picker. User types in "Blue" to see options for "Light Blue" and "Alice Blue" etc. But pressing Tab (or Enter) should auto select "Blue" since it was a valid choice to begin with. State lookup would be the same way. User entering "Iowa" might simply finish typing the word and press tab. It's literally counter-intuitive to have to arrow-down or otherwise select "Iowa" from the list when I've already typed it in. In my humble experience, most autocomplete fields work this way.

KamalAman commented 4 years ago

@oliviertassinari

I envision that the implementation for the highlighted value uses a partially controlled state where:

To control the Good for Goodfellas use case i should suggest a use lazy implementation as such,

const top100Films = [...]
export default function ComboBoxWithControlledHighlightedValues() {
  const [inputValue, onInputChange ] = useState('');

  const highlightedValue = useMemo(() => {
     const regex = new RegExp(`^${inputValue}`, 'i')
     return top100Films.find(({ title }) => regex.test(title))
  }, [inputValue, top100Films])

  return (
    <Autocomplete
      id="combo-box-demo"
      options={top100Films}
      getOptionLabel={(option) => option.title}
      style={{ width: 300 }}
      renderInput={(params) => <TextField {...params} label="Combo box" variant="outlined" />}
      inputValue={inputValue}
      onInputChange={onInputChange}
      highlightedValue={highlightedValue}
    />
  );
}
oliviertassinari commented 4 years ago

@KamalAman Ok, so if I summarize, the priority is to move forward with https://github.com/mui-org/material-ui/issues/20852#issuecomment-623022304. Then the developers that want to improve the DX, can sort the filtered options, to place the best match at the top of the list, like Google is doing (moving the options instead of moving the highlight).

Regarding moving the highlight, I think that we could consider this API:

const top100Films = [...]
export default function ComboBoxWithControlledHighlightedValues() {
  const [inputValue, setInputChange] = useState('');
  const [highlight, setHighlight] = useState(-1);

  return (
    <Autocomplete
      id="combo-box-demo"
      options={top100Films}
      getOptionLabel={(option) => option.title}
      style={{ width: 300 }}
      renderInput={(params) => <TextField {...params} label="Combo box" variant="outlined" />}
      inputValue={inputValue}
      onInputChange={(_, newInputValue) => { setInputChange(newInputValue); }}
      highlight={highlight}
      onHighlightChange={(_, newHighlight, reason) => {
        if (reason === 'input') {
          const regex = new RegExp(`^${inputValue}`, 'i')
          setHighlight(top100Films.find(({ title }) => regex.test(title)));
          return;
        }

        setHighlight(newHighlight);
      }}
    />
  );
}

A note on the implementation, we would very likely want to keep the highlight state inside a ref if not controlled. Would the above work for you guys?

CliffChaney commented 4 years ago

@KamalAman Thank you for the suggestion. But I did a bad job of explaining my issue/suggestion.

My problem is NOT with the Autocomplete selection algorithm. My problem is the final selection that is recorded in the input box. I was merely trying to relate my use case with what I perceived to be the feature being suggested. Please let me try again.

In my "Iowa" example, assume that I typed the case exactly. User tabs into Autocomplete field for US States and types "Iowa". In my implementation, the Autocomplete list will now show only one option - and the textbox portion of the control will show "Iowa" as well.

Now, the user - seeing "Iowa" in the input box - simply presses Tab to move to the next field.

Perhaps I have some option configured wrong. But in my usage of Autocomplete, the entered value of "Iowa" simply goes away and the user is moved to the next field.

In order to actually select the value of "Iowa" the user is forced to tap-on or click "Iowa" in the dropdown - and then they can tab to the next field.

That is the part that is counterintuitive to me. I can see the value in the input box portion of the control. I can see that I've entered "Iowa". I can see from the dropdown portion of the control that "Iowa" is a valid option. But I cannot use it without officially selecting it from the list (e.g. down-arrow, clicking it, etc).

Thinking about every autocomplete implementation that I've used - literally, every one - this is how they work. I'm not forced to select the option from the dropdown list. I can simply type it and my input is saved. There is no requirement to select from the list of suggestions.

I should amend that I would expect everything else about Autocomplete to work as-is. So, if the user does NOT type a valid value from the autocomplete list - and say, freeSolo is false - then it would behave essentially as it does now (i.e. tabbing out would clear/reject the input).

Hopefully that makes more sense. And in the likely event that I've implemented something incorrectly, I will gladly accept direction! Thank you!

oliviertassinari commented 4 years ago

@CliffChaney What's your use case for the component? A search field (like Google search)? A combo box (like react-select)? It sounds like you are looking for a search field, set freeSolo={true} and you will get the behavior you need.

CliffChaney commented 4 years ago

@oliviertassinari Thank you for your interest! No. I don't want freeSolo={true}. Though, I will likely end up using that - and then adding validation. I want a combo box. I haven't tried react-select, so I cannot comment on it.

I want a combo box implementation where the user is forced to select from the suggested list. One literal implementation I have right now is an Autocomplete implementation of all CSS colors. I want the user to be able to type "Blue" and see all the "Blue" options. Autocomplete works PERFECT for that... Where it gets confusing to the user is when they type in "Azure" and are forced to click on it before continuing. In a similar use case, I have a long list of product codes. Again, I need the Autocomplete functionality to help new users find the code they're after and ensure they pick the right one. (I've implemented renderOption to show a description with the code.) But for experienced users Autocomplete gets in the way. They know the 6 digit code they want and would like to simply type it in and tab to the next field.

oliviertassinari commented 4 years ago

@CliffChaney autoSelect={true} autoHighlight={true} in this case?

CliffChaney commented 4 years ago

@oliviertassinari That didn't quite work for me. But it's damn close.

Problem with that is when you need to make changes. When I tab back into that field and type a new value - unless I select it from the list - when I press tab, the old value is put back for me.

All that said... I JUST found a solution that leverages something I didn't know about getOptionLabel. Hopefully this isn't something that will get "fixed". And it might not work everywhere, but it worked for me.

Fortunately, all my autocomplete lists are structures (e.g. { code, value }).

What I JUST discovered with freeSolo={true} is that getOptionLabel is called a final time when the user exits the field. And on the final time, it passes getOptionLabel the entered string instead of an object from my options list. Honestly, this feels like it shouldn't work this way. But I was able to leverage it.

In case someone comes looking for this, here's my code for getOptionLabel:

      getOptionLabel={(op) => {
        if (op.name) return (op.name)
        const tranCode = tranCodeList.find((tc) => tc.name === op)
        if (tranCode) return tranCode.name
        return ''
      }}

If it's passed an object - as expected - it returns the label. If it's passed a string - the user must be blurring the field - check to make sure the option is valid and return it. Invalid options return an empty string.

This appears to work for me! It can also be easily modified to allow for partial matches, ignore-case comparisons, etc...

KamalAman commented 4 years ago

@oliviertassinari Yeah, it seems like that api would work for us. Yes keeping the highlight state inside a ref if not controlled makes perfect sense. We don't want to pull the highlighted value to the top of the list since the list is sorted/grouped.

oliviertassinari commented 4 years ago

@KamalAman Great. I have added the "good to take" GitHub issue label as we now have a clear resolution path. If you wish to work on a pull request, feel free to :).

youjingwong commented 4 years ago

I would like to start working on this.

To summarize: 1) Change default for autoHighlight autoHighlight = !props.freeSolo 2) Update demo 3) Handle new prop highlight. Or should it be named defaultHighlighted, mirroring the internal ref that's being used for autoHighlight?

oliviertassinari commented 4 years ago

Note that this issue has a dependency on #22170 and #22073, we discuss similar problems.

@mnajdova Do you want to help @NoNonsense126 on this issue? The issue was open a long time ago, I have forgotten the context.

jatin-rao commented 3 years ago

hi, @oliviertassinari Just wanted to know will this feature be implemented in near future (adding highlight option in Autocomplete), since I needed something like this in one of my other project.

oliviertassinari commented 3 years ago

@jatin-rao no, idea, maybe it will happen in 12 months. What's your exact use case?

jatin-rao commented 3 years ago

Ok @oliviertassinari so my use case is as Follows:

My Autocomplete should work like a time picker. Like the user will be shown suggestions of time from 12:00 am to 12:00 pm with 15 minutes interval like (12:00AM, 12:15AM, 12:30AM and so on till 11:45PM) and when an user edits the time in text field like he writes 10:40AM so the autocomplete should automatically highlight the closest option like 10:45AM in popup menu. Even if it can't highlight the closest option, it should be able to move the scrollbar in popup to 10:45AM.

I have searched a lot about how to change focus inside popup programmatically but was not able to find anything. Do you have any suggestion on how to achieve this?

hanstarj commented 3 years ago

I also wish to see this change soon. But what is a reasonable workaround for my use case?

My use case is basically Notion's tag field:

  1. The field can take multiple tags.
  2. On focus, no items should be highlighted i.e autoHighlight=false
  3. If user types, the first item is highlighted (call it inputValue-highlighted item)
  4. User-highlighted item (by up/down keys or hover) takes precedence over inputValue-highlighted item, if it still exists in the options list

The highlight prop will solve my problem, but it seems taking time (a year has passed). How can I work around this?

chaosmirage commented 2 years ago

I needed to focus on the first item after opening the Listbox to start navigation through the keyboard from the first item when there is already a selected item in the list (the autoHighlight prop doesn't fit).

After analyzing the Autocomplete implementation I found a workaround. Of course it is fragile, but if don't have another way it can be used at your own risk.

https://codesandbox.io/s/select-first-item-w0v2ku?file=/demo.tsx

nathggns commented 1 year ago

This may be necessary for implementing a "command bar" style UI (as seen on MUI's own docs) using Autocomplete, where you don't want the option that happens to appear under the whereever the cursor happens to be when pressing command+K to be "focused". It doesn't appear possible to implement this UI easily using the APIs available with Autocomplete

yoyos commented 2 months ago

Hi, in 2024 this would still be a nice feature for my use case:

A list of games where you are not required to select something. How would you suggest the user and scroll to the next match that will be played when opening the list ?

Thanks !

confrodog commented 2 weeks ago

Any progress on this feature, a controlled highlight prop?

The Autocomplete from MUI v5 behaves like this:

  1. user types in input, the dropdown opens.
  2. user hovers an option
  3. user hits the Enter key, the hovered / highlighted option will be submitted to the onChange.

Also, when an option is highlighted by hovering and user hits "Up" or "Down" key, the highlighted value moves from where the hovered value is.

I would like to be able to control the highlighted value so I can easily prevent the highlighted-by-hovering from being submitted (figured a way around this with onHighlightChange and onChange) and be able to change the Up and Down keys to move from where highlight was prior to hovering.