1

Using Downshift, how would one implement settting the inputValue to the value of the currently highlighted item on ArrowUp/ArrowDown while persisting the filtered items until the user manually augments the inputValue?

e.g:

Google Typeahead example

Matt Richards
  • 1,477
  • 2
  • 14
  • 21

1 Answers1

1

The aforementioned behaviour can be implemented leveraging the stateReducer and useCombobox hook and as follows:

import React, { useState } from "react";
import { render } from "react-dom";
import { useCombobox } from "downshift";
import { items, menuStyles } from "./utils";

function stateReducer(state, actionAndChanges) {
  switch (actionAndChanges.type) {
    case useCombobox.stateChangeTypes.InputChange:
      return {
        ...actionAndChanges.changes,
        userInput: actionAndChanges.changes.inputValue
      };
    case useCombobox.stateChangeTypes.InputKeyDownArrowDown:
    case useCombobox.stateChangeTypes.InputKeyDownArrowUp:
      if (!actionAndChanges.changes.inputValue) return actionAndChanges.changes;

      return {
        ...actionAndChanges.changes,
        userInput: actionAndChanges.changes.inputValue,
        inputValue: actionAndChanges.getItemNodeFromIndex(
          actionAndChanges.changes.highlightedIndex
        ).innerText
      };
    default:
      return actionAndChanges.changes; // otherwise business as usual.
  }
}

function DropdownSelect() {
  const [inputItems, setInputItems] = useState(items);
  const {
    isOpen,
    getToggleButtonProps,
    getLabelProps,
    getMenuProps,
    getInputProps,
    getComboboxProps,
    highlightedIndex,
    getItemProps
  } = useCombobox({
    items: inputItems,
    stateReducer,
    onInputValueChange: ({ userInput, inputValue }) => {
      if (userInput === inputValue) {
        const filteredItems = items.filter(item =>
          item.toLowerCase().startsWith(inputValue.toLowerCase())
        );
        setInputItems(filteredItems);
      }
    }
  });

  return (
    <React.Fragment>
      <label {...getLabelProps()}>Choose an element:</label>
      <div style={{ display: "inline-block" }} {...getComboboxProps()}>
        <input {...getInputProps()} />
        <button {...getToggleButtonProps()} aria-label="toggle menu">
          &#8595;
        </button>
      </div>
      <ul {...getMenuProps()} style={menuStyles}>
        {isOpen &&
          inputItems.map((item, index) => (
            <li
              style={
                highlightedIndex === index ? { backgroundColor: "#bde4ff" } : {}
              }
              key={`${item}${index}`}
              {...getItemProps({ item, index })}
            >
              {item}
            </li>
          ))}
      </ul>
    </React.Fragment>
  );
}

render(<DropdownSelect />, document.getElementById("root"));

View the Code sandbox here

Matt Richards
  • 1,477
  • 2
  • 14
  • 21
  • I was looking for that a while ago and ended up making my own [component](https://codesandbox.io/s/react-autocomplete-component-hxxn2), feel free to fork it if you like.. – awran5 Apr 06 '20 at 12:25
  • @MattRichards Your answer is great. It really helps me. I want to ask. Why on your `onInputValueChange` function you are comparing `if (userInput === inputValue)`? What is the purpose of `userInput` value? Is it important? – John Chucks Jul 26 '20 at 15:14
  • https://github.com/downshift-js/downshift/issues/1118 has another example taking a similar approach. – christopher.theagen Feb 25 '22 at 21:41