Search code examples
javascriptreactjsdownshift

Downshift: Set inputValue to value of currently highlighted item on keyboard navigation


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


Solution

  • 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