downshift icon indicating copy to clipboard operation
downshift copied to clipboard

highlightedIndex value should appear in the text field

Open john45321 opened this issue 4 years ago • 9 comments

  • downshift version: 5.4.6
  • node version: 12.13.1
  • npm version: 6.12.1
  • yarn version: 1.22.4

Hey, First thanks for the awesome library. I'am filtering data from the array of objects. I want to achieve this functionality in my current downshift sandbox. When you press the keys, the highlighted index value should appear in the text field.

Example: highlightedIndex

I'm using a useComboxbox hook with the custom stateReducer to track the changes of press keys. But how can I get the innerText of the list item i.e highlightedIndex to appear on the input field.

Currently my input field is: highlightedIndex


const stateReducer = (state, { type, changes }) => {
  // userInput is new field which is came through the dispatch action
  switch (type) {
    case useCombobox.stateChangeTypes.InputChange:
      return {
        // return normal changes.
        ...changes,
        userInput: changes.inputValue,
      };
    // Without break, the program continues to the next labeled statement.
    case useCombobox.stateChangeTypes.InputKeyDownArrowDown:
    case useCombobox.stateChangeTypes.InputKeyDownArrowUp:
      // An empty string is falsely value, we will make it true.
      if (!changes.inputValue) {
        return changes;
      }
      return {
        // return normal changes.
        ...changes,
        userInput: changes.inputValue,
      };
    default:
      return changes; // return normal changes.
  }
};

const App = () => {
  // Array of objects
  const [inputItems, setInputItems] = useState(data);

  // State for all primitive types
  const [value, setValue] = useState('');

  /**
   * It will return the new filtered array.
   * @param data Array<Object> - The array to iterate over
   * @param inputValue {string} - Your input value
   * @return Array<Object> - Return the new filtered array
   */
  const filterByName = (data, inputValue) => {
    return data.filter((item) => {
      return item.name.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1;
    });
  };

  // props for the combobox
  const comboboxProps = {
    className: 'search has-icons-left has-buttons-right',
  };

  // props for the input
  const inputProps = {
    type: 'text',
    className: 'form-control',
    placeholder: 'Enter the state',
  };

  // props for the menu
  const menuProps = {
    className: 'menu',
  };

  // useComboBox
  const {
    isOpen,
    getComboboxProps,
    getInputProps,
    getMenuProps,
    getItemProps,
    highlightedIndex,
    closeMenu,
    selectItem,
  } = useCombobox({
    stateReducer,
    items: inputItems,
    onInputValueChange: ({ userInput, inputValue }) => {
      setValue(inputValue);
      // An empty string is falsely value, we will make it true
      // and close the menu. If the input value is empty.
      if (!inputValue) {
        closeMenu();
      } else {
        if (userInput === inputValue) {
          setInputItems(filterByName(data, inputValue));
        }
      }
    },
    itemToString: (item) => (item ? item.name : ''),
  });

  return (
    <div className="app">
      <div {...getComboboxProps(comboboxProps)}>
        <input
          {...getInputProps({
            ...inputProps,
            // onKeyDown: To prevent opening if there is no input value.
            onKeyDown: (e) => {
              // Here value is our local state that we created to track the value for the
              // clear button
              if (!value) {
                return (e.nativeEvent.preventDownshiftDefault = true);
              }
            },
          })}
        />
        <span className="icon is-left">
          <MarkerIcon />
        </span>
        {typeof value === 'string' && value.length > 0 ? (
          <span className="button is-right">
            <button
              className="btn btn-clear"
              onClick={() => {
                selectItem(null);
                closeMenu();
              }}
            >
              Clear
            </button>
          </span>
        ) : null}
        {/* Suggestions */}
        <ul {...getMenuProps(menuProps)}>
          {isOpen &&
            inputItems.map((item, index) => (
              <li
                key={index}
                {...getItemProps({ item, index })}
                style={
                  highlightedIndex === index
                    ? { backgroundColor: '#f5f5f5' }
                    : null
                }
              >
                {item.name}
              </li>
            ))}
        </ul>
      </div>
    </div>
  );
};

export default App;

Codesandbox: view here

john45321 avatar Jul 16 '20 05:07 john45321

https://codesandbox.io/s/usecombobox-variations-state-reducer-pfjl9?file=/index.js But it does not have any filtering by input value.

If you want both, you probably need two inputValue variables. One for filtering, one for showing it in the <input> field, and you have to do anything related to UI by yourself, such as setSelectionRange or similar stuff, depending what you want. But it should be achievable.

silviuaavram avatar Jul 20 '20 18:07 silviuaavram

It can probably be a very good example to add to the docsite, so if you manage to create it, give a link to it so I can show it in downshiftjs.com as well

silviuaavram avatar Jul 20 '20 18:07 silviuaavram

Thank you @silviuaavram for your valuable time. Sure, I will link to it, but I could not figure out. How to display the filtered index value on the input field? If I add something like this to stateReducer:

return {
  ...changes,
  inputValue: data[changes.highlightedIndex].name,
  userInput: changes.inputValue
};

This will show the index value of the old data, not the filter because the index always starts at 0. And the old data will remain static.

data-index

Sandbox: View here

john45321 avatar Jul 21 '20 16:07 john45321

@john45321 inputValue: data[changes.highlightedIndex].name, it should be the filtered items instead of data.

silviuaavram avatar Jul 23 '20 12:07 silviuaavram

@silviuaavram Thanks for your help. I've solved it with your help but I'm still curious. I put my stateReducer inside the component and all the filtered items were stored inside the state. Now here's the line. inputValue: inputItems[changes.highlightedIndex].name. You could add this code sandbox example on the docsite.

downshift-example

But I want to ask Does it affects the performance because every time when the component is re-render the new stateReducer will also created on each render?

john45321 avatar Jul 26 '20 15:07 john45321

Nice!

I don't think creating a new stateReducer is costly, it's just a function. But I'm no expert.

silviuaavram avatar Jul 31 '20 15:07 silviuaavram

@john45321 can you add the example to https://codesandbox.io/s/github/kentcdodds/downshift-examples? It should be possible directly via codesandbox, which is super cool and straightforward. You can clone the repo, make the changes, and create a PR with the changes directly in codesandbox. Let me know if you take this one.

silviuaavram avatar Sep 12 '20 08:09 silviuaavram

Hi @silviuaavram, I have tried to take on this issue hopefully you don't mind. I have created a pull request here for it: updated input value example

please let me know if there are any issues.

Littletonconnor avatar Nov 09 '20 05:11 Littletonconnor

@Littletonconnor Hi thanks very much for the example! One more thing before merging, since I took a look at it one more time.

Altering the state with userInput should not be done, since it's not used internally by the hook.

But great news, you don't need userInput at all. Just do:

    onInputValueChange: ({type, inputValue}) => {
      if (
        type !== useCombobox.stateChangeTypes.InputKeyDownArrowDown &&
        type !== useCombobox.stateChangeTypes.InputKeyDownArrowUp
      ) {
        setInputItems(filterByName(inputValue))
      }
    },

You have access to the type in the handlers as well, not only in stateReducer!

So that being said, you can code this one (better than above I just wanted to make sure it works :D) and remove the not needed anymore parts from the state reducer. Once you improve this in your code please improve the PR for downshift-examples as well and we should be good to go!

Thank you again for contributing!

silviuaavram avatar Nov 23 '20 18:11 silviuaavram