source

대응: 화살표 키로 목록을 탐색하는 방법

nicesource 2023. 3. 10. 22:07
반응형

대응: 화살표 키로 목록을 탐색하는 방법

하나의 텍스트 입력과 그 이하의 목록(시맨틱 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>
        )
    }
}

다음과 같은 방법을 사용해 보십시오.

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따라서 표준 입력 편집 동작이 지연되거나 엉망이 되는 일은 없습니다.

렌더링 루프에서는 인덱스를 커서와 대조하여 활성 인덱스를 확인합니다.

필드로부터의 입력에 근거해 결과 세트를 필터링 하는 경우는, 세트를 필터링 할 때마다 커서가 0으로 리셋 되어 동작이 항상 일관되게 됩니다.

받아들여진 답변은 저에게 매우 유용했어요!저는 그 솔루션을 채택하여 리액트 향미 버전을 만들었습니다.누군가에게 도움이 될지도 모릅니다.

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 투고에는 기능이 포함되어 있습니다.

@joshweir가 제공한 솔루션과 거의 동일하지만, Typescript에 기재되어 있습니다.또한 'window' 객체 대신 'ref'를 사용하여 이벤트 청취자를 입력 텍스트 상자에만 추가했습니다.

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

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


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

이것은 나의 시도입니다. 단점은 렌더링된 아이들이 통과해야 한다는 것입니다.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>
  );
}

왼쪽 오른쪽 및 위쪽 키 바인딩을 누르면 탐색할 수 있는 자녀가 있는 목록입니다.

요리법.

  1. 데이터의 지도 함수를 사용하여 목록으로 사용할 개체 배열을 만듭니다.

  2. useEffect를 만들고 창에서 키다운 액션을 수신할 이벤트 리스트너를 추가합니다.

  3. handleKeyDown 함수를 생성하여 누른 키를 추적하여 네비게이션 동작을 설정합니다.키 코드를 사용합니다.

    키업: e.keyCode === 38

    keydown : e.keyCode === 40

    keyright: e.keyCode ===

    키 왼쪽: e.keyCode === 37

  4. 상태 추가

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

[activeSubMenu, setActiveSubMenu] = useState(-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

언급URL : https://stackoverflow.com/questions/42036865/react-how-to-navigate-through-list-by-arrow-keys

반응형