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);