>웹 프론트엔드 >JS 튜토리얼 >React Hooks를 사용하여 접근 가능한 탐색 메뉴바 만들기

React Hooks를 사용하여 접근 가능한 탐색 메뉴바 만들기

WBOY
WBOY원래의
2024-08-08 13:25:29984검색

Building an Accessible Navigation Menubar with React Hooks

실수로 게시되었습니다! 나중에 다시 방문해 주세요!

소개

접근 가능한 웹 애플리케이션을 만드는 것은 단지 좋은 습관이 아니라 이제는 필수입니다. 최근에 a11y를 중심으로 네비게이션 메뉴바를 만들 기회가 있었습니다. 조사하면서 나는 대부분의 메뉴바가 ARIA 패턴을 따르지 않는다는 사실을 깨달았습니다. 예를 들어 메뉴 항목을 탭으로 이동하는 대신 메뉴 표시줄을 화살표 키로 탐색하고 자체 포커스를 관리해야 한다는 것을 알고 계셨습니까?

일부 튜토리얼을 찾았지만 결국 완전히 따르지는 못했습니다. 제가 이 글을 쓰는 이유는 제가 만든 것이 공유할 가치가 있다고 생각하기 때문입니다. 여러분도 작은 구성 요소와 맞춤형 후크에 관심이 있다면 말이죠.

몇 가지 개발 단계를 통해 이 블로그를 구성할 예정이지만 단계별 가이드를 작성하는 것이 목표는 아닙니다. 나는 당신이 React의 기본과 사용자 정의 후크의 작동 방식을 알고 있다고 믿습니다.

지금은 주요 구현 세부정보만 공유하고 있지만 앞으로 시간이 더 있으면 코드 샌드박스 예제로 이 문서를 업데이트할 계획입니다.

우리는 무엇을 만들고 있나요?

이 블로그에서는 많은 웹 애플리케이션의 상단이나 측면에 표시되는 것과 같은 탐색 메뉴 표시줄을 구축하고 있습니다. 이 메뉴 표시줄에는 일부 메뉴 항목에 마우스 입력/나가기 시 열리거나 닫히는 하위 메뉴가 있을 수 있습니다.

HTML 마크업

무엇보다도 시맨틱 HTML과 적절한 역할 및 ARIA 속성이 접근성에 필수적입니다. 메뉴바 패턴에 대한 자세한 내용은 여기 공식 문서에서 확인할 수 있습니다.

다음은 적절한 HTML 마크업의 예입니다.

<nav aria-label="Accessible Menubar">
  <menu role="menubar">
    <li role="none">
      <a role="menuitem" href="/">Home</a>
    </li>
    <li role="none">
      <a role="menuitem" href="/about">About</a>
    </li>
    <li role="none">
      <button 
        role="menuitem" 
        aria-haspopup="true"
        aria-expanded="false"
      >
        Expand Me!
      </button>
      <menu role="menu">
        <li role="none">
          <a role="menuitem" href="/sub-item-1">Sub Menu Item 1</a>
        </li>
        <li role="none">
          <a role="menuitem" href="/sub-item-2">Sub Menu Item 2</a>
        </li>
      </menu>
    </li>
  </menu>
</nav>

의미론적 HTML을 위해 버튼 태그를 사용하고 있다는 점에 주목하세요. 버튼에는 스크린 리더에게 경고를 보내는 aria-haspopup도 있어야 합니다. 마지막으로 메뉴 상태에 따라 적절한 aria-expanded 속성을 지정해야 합니다.

구성요소

필요한 구성요소를 살펴보겠습니다. 당연히 메뉴 항목 컴포넌트뿐 아니라 전체 메뉴 컴포넌트도 필요합니다.

일부 메뉴 항목에는 하위 메뉴가 있고 일부에는 하위 메뉴가 없습니다. 하위 메뉴가 있는 메뉴 항목은 마우스 오버 및 키보드 이벤트 시 하위 메뉴 열기/닫기 상태를 관리해야 합니다. 따라서 자체 구성요소여야 합니다.

하위 메뉴도 자체 구성요소여야 합니다. 하위 메뉴도 메뉴 항목의 컨테이너일 뿐이지만 상태를 관리하거나 키보드 이벤트를 처리하지는 않습니다. 이는 최상위 탐색 메뉴와 구별됩니다.

결국 다음 구성 요소를 작성하게 되었습니다.

  • 메뉴바의 가장 바깥쪽 레이어에 대한 NavMenu.
  • 개별 메뉴 항목에 대한 MenuItem입니다.
    • 메뉴항목링크
    • MenuItemWithSubMenu
  • 확장된 하위 메뉴에 대한 하위 메뉴입니다. MenuItem은 하위 메뉴 내에 재귀적으로 중첩될 수 있습니다.

집중관리

매우 쉽게 말하면 '포커스 관리'는 구성요소가 어떤 하위에 포커스가 있는지 알아야 함을 의미합니다. 따라서 사용자의 포커스가 떠났다가 다시 돌아오면 이전에 포커스가 맞춰진 하위 항목에 다시 포커스가 맞춰집니다.

포커스 관리를 위한 일반적인 기술은 "로빙 탭 인덱스"입니다. 여기서 그룹의 포커스된 요소는 탭 인덱스가 0이고 다른 요소의 탭 인덱스는 -1입니다. 이렇게 하면 사용자가 포커스 그룹으로 돌아갈 때 탭 인덱스가 0인 요소에 자동으로 포커스가 부여됩니다.

NavMenu의 첫 번째 구현은 다음과 같습니다.

export function NavMenu ({ menuItems }) {
  // state for the currently focused index
  const [focusedIndex, setFocusedIndex] = useState(0);

  // functions to update focused index
  const goToStart = () => setCurrentIndex(0);
  const goToEnd = () => setCurrentIndex(menuItems.length - 1);
  const goToPrev = () => {
    const index = currentIndex === 0 ? menuItems.length - 1 : currentIndex - 1;
    setCurrentIndex(index);
  };
  const goToNext = () => {
    const index = currentIndex === menuItems.length - 1 ? 0 : currentIndex + 1;
    setCurrentIndex(index);
  };

  // key down handler according to aria specification
  const handleKeyDown = (e) => {
    e.stopPropagation();
    switch (e.code) {
      case "ArrowLeft":
      case "ArrowUp":
        e.preventDefault();
        goToPrev();
        break;
      case "ArrowRight":
      case "ArrowDown": 
        e.preventDefault();
        goToNext();
        break;
      case "End":
        e.preventDefault();
        goToEnd();
        break;
      case "Home":
        e.preventDefault();
        goToStart();
        break;
      default:
        break;
    }
  }

  return (
    <nav>
      <menu role="menubar" onKeyDown={handleKeyDown}>
        {menuItems.map((item, index) => 
          <MenuItem
            key={item.label}
            item={item}
            index={index}
            focusedIndex={focusedIndex}
            setFocusedIndex={setFocusedIndex}
          />
        )}
      </menu>
    </nav>
  );
}

e.preventDefault()는 ArrowDown으로 페이지를 스크롤하는 것과 같은 일을 방지하기 위해 사용됩니다.

MenuItem 구성요소는 다음과 같습니다. 하위 메뉴가 있는 항목은 잠시 무시해 보겠습니다. focusIndex가 변경될 때마다 useEffect, usePrevious 및 element.focus()를 사용하여 요소에 초점을 맞추고 있습니다.

export function MenuItem ({ item, index, focusedIndex, setFocusedIndex }) {
  const linkRef = useRef(null);
  const prevFocusedIndex = usePrevious(focusedIndex);
  const isFocused = index === focusedIndex;

  useEffect(() => {
    if (linkRef.current 
      && prevFocusedIndex !== currentIndex 
      && isFocused) {
      linkRef.current.focus()
    }
  }, [isFocused, prevFocusedIndex, focusedIndex]);

  const handleFocus = () => {
    if (focusedIndex !== index) {
      setFocusedIndex(index);
    }
  };

  return (
    <li role="none">
      <a 
        ref={linkRef} 
        role="menuitem"
        tabIndex={isFocused ? 0 : -1}
        onFocus={handleFocus}
      >
        {item.label}
      </a>
    </li>
  );
}

ref(하위 메뉴가 있는 메뉴 항목에 대한 버튼)가 있어야 하는 태그이므로 해당 태그에 초점을 맞추면 Enter 키 탐색과 같은 기본 키보드 동작이 예상대로 작동합니다. 게다가 포커스가 있는 요소에 따라 탭 인덱스가 제대로 할당되어 있습니다.

포커스 이벤트가 키/마우스 이벤트에서 발생하지 않는 경우를 대비해 포커스 이벤트에 대한 이벤트 핸들러를 추가하고 있습니다. 다음은 웹 문서의 인용문입니다.

모든 포커스 변경이 키와 마우스 이벤트를 통해 이루어질 것이라고 가정하지 마십시오. 스크린 리더와 같은 보조 기술을 사용하면 포커스 가능한 요소에 포커스를 설정할 수 있습니다.

조정 #1

위에 설명된 useEffect를 따르면 사용자가 키보드를 사용하여 탐색하지 않은 경우에도 첫 번째 요소에 포커스가 있음을 알 수 있습니다. 이 문제를 해결하려면 활성 요소를 확인하고 사용자가 일부 키보드 이벤트를 시작한 경우에만 focus()를 호출하여 초점을 본문에서 다른 곳으로 이동할 수 있습니다.

  useEffect(() => {
    if (linkRef.current 
      && document.activeElement !== document.body // only call focus when user uses keyboard navigation
      && prevFocusedIndex !== focusedIndex
      && isCurrent) {
      linkRef.current.focus();
    }
  }, [isCurrent, focusedIndex, prevFocusedIndex]);

Logic Reuse and Custom Hook

So far, we have functional NavMenu and MenuItemLink components. Let's move on to menu item with sub menus.

As I was quickly building it out, I realized that this menu item will share the majority of the logic

위 내용은 React Hooks를 사용하여 접근 가능한 탐색 메뉴바 만들기의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.