React:如何使用箭头键在列表中导航

86

我已经用单个文本框和下面的列表(使用语义化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>
        )
    }
}

我不确定你想要问什么,提供一个组件图片会有助于得到答案。 - Khalid Azam
5个回答

97

试试这样:

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,以便您不会延迟或干扰标准输入编辑行为。

在渲染循环中,您只需将索引与光标进行比较,以查看哪个处于活动状态。

如果您基于字段输入来过滤结果集,那么您可以在任何时候重置光标为零,以便始终保持行为一致。


已编辑以修复向下箭头条件。不小心输入了>而不是<。 - shadymoses
9
建议使用 e.key,因为 e.keyCode 已被弃用。 - Fareed Alnamrouti
好的,已经相应更新了。 - shadymoses
1
@shadymoses 我认为如果你使用了key属性,你需要列出实际的键值(例如 'ArrowDown')而不是键码。此外,我知道在浏览器支持方面有一些怪癖关于 e.key,所以在使用之前最好先检查一下。 - Mark McKelvy
2
你是对的。e.keyCode 给出了数字键码并且得到更好的支持。我再次更新了答案。 - shadymoses
显示剩余2条评论

87

接受的答案对我非常有用,谢谢!我改编了那个解决方案,并制作了一个 react hooks 版本,或许会对别人有用:

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

import "./styles.css";

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

  React.useEffect(() => {
    const downHandler = ({ key }) => {
      if (key === targetKey) {
        setKeyPressed(true);
      }
    }
  
    const upHandler = ({ key }) => {
      if (key === targetKey) {
        setKeyPressed(false);
      }
    };

    window.addEventListener("keydown", downHandler);
    window.addEventListener("keyup", upHandler);

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

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

useKeyPress功能归因于这篇文章


4
值得注意/归功于这个文章useKeyPress功能。 - abest
2
你应该在 useEffect 中添加一个依赖数组,并将 downHandlerupHandler 移动到 useEffect 函数内部。这样,它就不会在每次渲染时重新创建监听器了。 - Fernando Rojo
这太棒了!有人能解释一下在useKeyPress中定义的React.useEffect吗?我不明白为什么事件侦听器以那种特定的方式被附加和分离。 - corescan
1
@FernandoRojo是正确的,否则应用程序会非常缓慢。 - Werthis
1
@FernandoRojo 是对的,否则这个应用程序会非常慢。 - undefined
显示剩余4条评论

18

这个解决方案和@joshweir提供的基本相同,但是使用了Typescript。另外,我使用了'ref'代替了'window'对象,并且只将事件监听器添加到输入文本框中。

import React, { useState, useEffect, Dispatch, SetStateAction, createRef, RefObject } from "react";

const useKeyPress = function (targetKey: string, ref: RefObject<HTMLInputElement>) {
    const [keyPressed, setKeyPressed] = useState(false);


    const downHandler = ({ key }: { key: string }) => {
        if (key === targetKey) {
            setKeyPressed(true);
        }
    }

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

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

        return () => {
            ref.current?.removeEventListener("keydown", downHandler);
            ref.current?.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 i = items[0]
type itemType = { id: number, name: string }

type ListItemType = {
    item: itemType
    , active: boolean
    , setSelected: Dispatch<SetStateAction<SetStateAction<itemType | undefined>>>
    , setHovered: Dispatch<SetStateAction<itemType | undefined>>
}

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

const ListExample = () => {
    const searchBox = createRef<HTMLInputElement>()
    const [selected, setSelected] = useState<React.SetStateAction<itemType | undefined>>(undefined);
    const downPress = useKeyPress("ArrowDown", searchBox);
    const upPress = useKeyPress("ArrowUp", searchBox);
    const enterPress = useKeyPress("Enter", searchBox);
    const [cursor, setCursor] = useState<number>(0);
    const [hovered, setHovered] = useState<itemType | undefined>(undefined);
    const [searchItem, setSearchItem] = useState<string>("")


    const handelChange = (e: React.SyntheticEvent<HTMLInputElement>) => {
        setSelected(undefined)
        setSearchItem(e.currentTarget.value)
    }

    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 || items.length && hovered) {
            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>
            <div>
                <input ref={searchBox} type="text" onChange={handelChange} value={selected ? selected.name : searchItem} />
                {items.map((item, i) => (
                    <ListItem

                        key={item.id}
                        active={i === cursor}
                        item={item}
                        setSelected={setSelected}
                        setHovered={setHovered}
                    />
                ))}
            </div>
        </div>
    );
};

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

为什么需要悬停状态? - Werthis

1

这是我的尝试,不过缺点是它需要正确传递ref的渲染子节点:

import React, { useRef, useState, cloneElement, Children, isValidElement } from "react";

export const ArrowKeyListManager: React.FC = ({ children }) => {
  const [cursor, setCursor] = useState(0)
  const items = useRef<HTMLElement[]>([])

  const onKeyDown = (e) => {
    let newCursor = 0
    if (e.key === 'ArrowDown') {
      newCursor = Math.min(cursor + 1, items.current.length - 1)
    } else if (e.key === 'ArrowUp') {
      newCursor = Math.max(0, cursor - 1)
    }
    setCursor(newCursor)
    const node = items.current[newCursor]
    node?.focus()
  }

  return (
    <div onKeyDown={onKeyDown} {...props}>
      {Children.map(children, (child, index) => {
        if (isValidElement(child)) {
          return cloneElement(child, {
            ref: (n: HTMLElement) => {
              items.current[index] = n
            },
          })
        }
      })}
    </div>
  )
}

使用方法:

function App() {
  return (
    <ArrowKeyListManager>
        <button onClick={() => alert('first')}>First</button>
        <button onClick={() => alert('second')}>Second</button>
        <button onClick={() => alert('third')}>third</button>
     </ArrowKeyListManager>
  );
}

0

这是一个带有子项的列表,可以通过按左右和上下键进行导航。

步骤:

  1. 使用数据的 map 函数创建一个对象数组,用作列表。

  2. 创建 useEffect 并添加 Eventlistener 监听窗口中的 keydown 操作。

  3. 创建 handleKeyDown 函数以配置导航行为,通过跟踪按下的键来使用它们的键码。

    keyup: e.keyCode === 38

    keydown: e.keyCode === 40

    keyright: e.keyCode === 39

    keyleft: e.keyCode === 37

  4. 添加状态

let [activeMainMenu, setActiveMainMenu] = useState(-1);

let [activeSubMenu, setActiveSubMenu] = useState(-1);

通过映射对象数组来渲染:
     <ul ref={WrapperRef}>
       {navigationItems.map((navigationItem, Mainindex) => {
         return (
           <li key={Mainindex}>
             {activeMainMenu === Mainindex
               ? "active"
               : navigationItem.navigationCategory}
             <ul>
               {navigationItem.navigationSubCategories &&
                 navigationItem.navigationSubCategories.map(
                   (navigationSubcategory, index) => {
                     return (
                       <li key={index}>
                         {activeSubMenu === index
                           ? "active"
                           : navigationSubcategory.subCategory}
                       </li>
                     );
                   }
                 )}
             </ul>
           </li>
         );
       })}
     </ul>

请在以下链接中找到上述解决方案:

https://codesandbox.io/s/nested-list-accessible-with-keys-9pm3i1?file=/src/App.js:2811-3796


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接