javascript React:如何通过箭头键在列表中导航

jjhzyzn0  于 2023-02-11  发布在  Java
关注(0)|答案(5)|浏览(147)

我构建了一个简单组件,它只有一个文本输入,下面是一个列表(使用语义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>
        )
    }
}
5sxhfpxr

5sxhfpxr1#

试试这样的方法:

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来查看箭头键,这样就不会延迟或扰乱标准输入编辑行为。
在渲染循环中,您只需对照光标检查索引,看看哪个是活动的。
如果根据字段中的输入过滤结果集,则只要在过滤结果集时将光标重置为零,就可以始终保持行为一致。

l7wslrjt

l7wslrjt2#

被接受的答案对我来说非常有用,谢谢!我改编了这个解决方案,并制作了一个react hooks flavoured version,也许它会对某人有用:

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(() => {
    function 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功能归属于此post

at0kjp5o

at0kjp5o3#

与@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);
ktecyv1j

ktecyv1j4#

这是我的尝试,缺点是它需要渲染的子对象正确传递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>
  );
}
jmp7cifd

jmp7cifd5#

它是一个包含子元素的列表,可以通过按左右键和上下键来导航。
食谱。
1.创建一个对象数组,该数组将用作对数据使用map函数的列表。
1.创建一个useEffect并添加一个Eventlistener来侦听窗口中的按下键操作。
1.创建handleKeyDown函数,以便通过跟踪按下的键来配置导航行为,为此使用其键码。
密钥启动:例如,密钥代码=== 38
按键关闭:例如,按键代码=== 40
密钥权限:例如,密钥代码=== 39
按键左移:例如,按键代码=== 37
1.添加状态
令[活动主菜单,设置活动主菜单]=使用状态(-1);
令[活动子菜单,设置活动子菜单]=使用状态(-1);
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

相关问题