React: Как перемещаться по списку с помощью клавиш со стрелками

У меня есть простой компонент с одним текстовым вводом и ниже этого списка (с использованием семантики ui).

Теперь я хотел бы использовать клавиши со стрелками для навигации по списку.

  • Прежде всего, я должен выбрать первый элемент. Но как мне получить доступ к определенному элементу списка?
  • Во-вторых, я получаю информацию о текущем выбранном элементе и выбираю следующий элемент. Как получить информацию о том, какой элемент выбран?

Выбор будет означать, чтобы добавить класс active к элементу или есть лучшая идея для этого?

export default class Example extends Component {
    constructor(props) {
        super(props)
        this.handleChange = this.handleChange.bind(this)
        this.state = { result: [] }
    }
    handleChange(event) {
        // arrow up/down button should select next/previous list element
    }
    render() {
        return (
            <Container>
                <Input onChange={ this.handleChange }/>
                <List>
                    {
                        result.map(i => {
                            return (
                                <List.Item key={ i._id } >
                                    <span>{ i.title }</span>
                                </List.Item>
                            )
                        })
                    }
                </List>
            </Container>
        )
    }
}

Ответ 1

Попробуйте что-то вроде этого:

export default class Example extends Component {
  constructor(props) {
    super(props)
    this.handleKeyDown = this.handleKeyDown.bind(this)
    this.state = {
      cursor: 0,
      result: []
    }
  }

  handleKeyDown(e) {
    const { cursor, result } = this.state
    // arrow up/down button should select next/previous list element
    if (e.keyCode === 38 && cursor > 0) {
      this.setState( prevState => ({
        cursor: prevState.cursor - 1
      }))
    } else if (e.keyCode === 40 && cursor < result.length - 1) {
      this.setState( prevState => ({
        cursor: prevState.cursor + 1
      }))
    }
  }

  render() {
    const { cursor } = this.state

    return (
      <Container>
        <Input onKeyDown={ this.handleKeyDown }/>
        <List>
          {
            result.map((item, i) => (
              <List.Item
                key={ item._id }
                className={cursor === i ? 'active' : null}
              >
                <span>{ item.title }</span>
              </List.Item>
            ))
          }
        </List>
      </Container>
    )
  }
}

Курсор отслеживает вашу позицию в списке, поэтому, когда пользователь нажимает клавишу со стрелкой вверх или вниз, вы соответственно уменьшаете/увеличиваете курсор. Курсор должен совпадать с индексами массива.

Вы, вероятно, хотите, чтобы onKeyDown наблюдал за клавишами со стрелками вместо onChange, чтобы у вас не было задержки или путаницы с вашим стандартным поведением редактирования ввода.

В цикле рендеринга вы просто сравниваете индекс с курсором, чтобы увидеть, какой из них активен.

Если вы фильтруете набор результатов на основе входных данных из поля, вы можете просто сбросить курсор в ноль каждый раз, когда вы фильтруете набор, чтобы вы всегда могли поддерживать согласованное поведение.

Ответ 2

Принятый ответ был очень полезен для меня, спасибо! Я адаптировал это решение и создал версию с привязкой к реагирующим хукам, может быть, она кому-нибудь пригодится:

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

const useKeyPress = function(targetKey) {
  const [keyPressed, setKeyPressed] = useState(false);

  function downHandler({ key }) {
    if (key === targetKey) {
      setKeyPressed(true);
    }
  }

  const upHandler = ({ key }) => {
    if (key === targetKey) {
      setKeyPressed(false);
    }
  };

  React.useEffect(() => {
    window.addEventListener("keydown", downHandler);
    window.addEventListener("keyup", upHandler);

    return () => {
      window.removeEventListener("keydown", downHandler);
      window.removeEventListener("keyup", upHandler);
    };
  });

  return keyPressed;
};

const items = [
  { id: 1, name: "Josh Weir" },
  { id: 2, name: "Sarah Weir" },
  { id: 3, name: "Alicia Weir" },
  { id: 4, name: "Doo Weir" },
  { id: 5, name: "Grooft Weir" }
];

const ListItem = ({ item, active, setSelected, setHovered }) => (
  <div
    className={'item ${active ? "active" : ""}'}
    onClick={() => setSelected(item)}
    onMouseEnter={() => setHovered(item)}
    onMouseLeave={() => setHovered(undefined)}
  >
    {item.name}
  </div>
);

const ListExample = () => {
  const [selected, setSelected] = useState(undefined);
  const downPress = useKeyPress("ArrowDown");
  const upPress = useKeyPress("ArrowUp");
  const enterPress = useKeyPress("Enter");
  const [cursor, setCursor] = useState(0);
  const [hovered, setHovered] = useState(undefined);

  useEffect(() => {
    if (items.length && downPress) {
      setCursor(prevState =>
        prevState < items.length - 1 ? prevState + 1 : prevState
      );
    }
  }, [downPress]);
  useEffect(() => {
    if (items.length && upPress) {
      setCursor(prevState => (prevState > 0 ? prevState - 1 : prevState));
    }
  }, [upPress]);
  useEffect(() => {
    if (items.length && enterPress) {
      setSelected(items[cursor]);
    }
  }, [cursor, enterPress]);
  useEffect(() => {
    if (items.length && hovered) {
      setCursor(items.indexOf(hovered));
    }
  }, [hovered]);

  return (
    <div>
      <p>
        <small>
          Use up down keys and hit enter to select, or use the mouse
        </small>
      </p>
      <span>Selected: {selected ? selected.name : "none"}</span>
      {items.map((item, i) => (
        <ListItem
          key={item.id}
          active={i === cursor}
          item={item}
          setSelected={setSelected}
          setHovered={setHovered}
        />
      ))}
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<ListExample />, rootElement);