How to iterate through list of items in React list using arrow keys

Viewed 47

I've seen similar posts on this issue, but when I try to implement what they suggested, it never works. I have the active state and my search results that are return on dispatch. When I type into the input search or click on it the menu opens below it and shows the results, yet when I hit the arrow keys it stays within the input bar and does not iterate/focus through each list item. I would like to make it so that when I hit either the down or up arrow key it would iterate through the search results and I can hit enter on any of them to trigger the search.

  const [active, setActive] = useState(0)
  const searchResults = useSelector((state) => Object.values(state.searchReducer.Search))

  const keyDown = (e) => {
    if(e.key === 38 && searchResults.length > 0){
      setActive(active - 1)
    }else if(e.key === 40 && active < searchResults.length - 1){
      setActive(active + 1)
    }
  }

 <BsSearch className="search-icon" />
      <form onSubmit={handleSubmit}>
      <input
        onClick={() => setMenu(true)}
        className="search-input"
        autoFocus
        onKeyDown={keyDown}
        type="search"
        placeholder="Search"
        onChange={(e) => setSearchVal(e.target.value)}
        value={searchVal}
      />
      </form>
      {menu && (
        <div className="search-cont">
          {searchResults.length > 0 ? (
            searchResults?.map((ele, i) => {
              return (
                <NavLink onClick={(() => {setSearchVal('')})}  key={i}  className="spot- 
                 links" to={`/users/${ele.id}`}>
                  <div
                    className="search-results-list"
                    id={active === i ? 'active' : null}
                  >
                    {ele.username}
                  </div>
                </NavLink>
              );
            })
    ```
1 Answers

With your current approach, you have several factors working against you:

  • The active key is only updated when you're focused on the input; if you're outside of the input, the key presses won't be captured
  • You're just setting the id to "active" for the selected element, which won't do anything other than change the id of the element
  • When you're updating the active key, you're not focusing on the active element

Resources:

Sandbox code: Edit confident-sinoussi-rlzdrf

Demo: https://rlzdrf.csb.app/


Code with notes (for simplicity, I'm using hardcoded search data with simple button interactions):

import { useCallback, useState, useEffect } from "react";
import Instructions from "./Instructions";
import SEARCH_DATA from "./searchData";

export default function App() {
  const [activeIndex, setActiveIndex] = useState(0);
  const [showResults, setShowResults] = useState(false);

  const handleClick = (name: string) => {
    alert(`You've selected ${name}!`);
  };

  // captures up/down/enter/escape keys
  const handleSelection = useCallback(
    (event: KeyboardEvent) => {
      // prevent key presses from being captured if the results are hidden
      if (!showResults) return;

      const { key } = event;
      const arrowUpPressed = key === "ArrowUp";
      const arrowDownPressed = key === "ArrowDown";
      const escapePressed = key === "Escape" || key === "Esc";
      const enterPressed = key === "Enter";

      const searchDataLength = SEARCH_DATA.length - 1;

      // increments active index (wraps around when at top)
      if (arrowUpPressed) {
        event.preventDefault();
        setActiveIndex((currentIndex) =>
          currentIndex - 1 >= 0 ? currentIndex - 1 : searchDataLength
        );

        // decrements active index (wraps around when at bottom)
      } else if (arrowDownPressed) {
        event.preventDefault();
        setActiveIndex((currentIndex) =>
          currentIndex + 1 <= searchDataLength ? currentIndex + 1 : 0
        );

        // alerts active selection
      } else if (enterPressed) {
        event.preventDefault();
        const target = event.target as HTMLElement;
        alert(`You've selected ${target.dataset.name}!`);

        // closes results
      } else if (escapePressed) {
        event.preventDefault();
        setActiveIndex(0);
        setShowResults(false);
      }
    },
    [showResults]
  );

  // globally listens for key presses
  useEffect(() => {
    document.addEventListener("keydown", handleSelection);

    return () => {
      document.removeEventListener("keydown", handleSelection);
    };
  }, [handleSelection]);

  // when the active selection changes, it focuses on the active element
  useEffect(() => {
    if (showResults) {
      const activeElement = document.querySelector(
        `[data-index='${activeIndex}']`
      ) as HTMLElement;

      if (activeElement) activeElement.focus();
    }
  }, [showResults, activeIndex]);

  return (
    <div className="app">
      <h1>Hello CodeSandbox</h1>
      <h2>Press the "Show Results" button!</h2>
      <Instructions />
      <button
        className="results"
        type="button"
        onClick={() => {
          // toggle results
          setShowResults((r) => !r);
          // reset active index to 0 when results are closed
          if (!showResults) setActiveIndex(0);
        }}
      >
        Show Results
      </button>
      {showResults && SEARCH_DATA.length > 0
        ? SEARCH_DATA.map(({ id, name }, index) => (
            <button
              type="button"
              role="presentation"
              data-index={index}
              data-name={name}
              className={`name ${activeIndex === index ? "active" : ""}`}
              key={id}
              onClick={() => {
                setActiveIndex(index);
                handleClick(name);
              }}
            >
              {name}
            </button>
          ))
        : null}
    </div>
  );
}

This is just one of many ways to accomplish the above, so feel free to explore other approaches. For example, you may want to draw focus into the search results and focus trap the keys. Also, if you're designing this for accessibility, it's best to do some research into the WCAG 2.1 standards -- they also provide some great examples for a variety of different use cases.

Related