«Если рабочий хочет хорошо выполнять свою работу, он должен сначала заточить свои инструменты» — Конфуций, «Аналитики Конфуция. Лу Лингун»
титульная страница > программирование > Создание доступной навигационной панели меню с помощью React Hooks

Создание доступной навигационной панели меню с помощью React Hooks

Опубликовано 25 августа 2024 г.
Просматривать:322

Building an Accessible Navigation Menubar with React Hooks

Случайно опубликовано! Пожалуйста, зайдите позже, чтобы узнать больше!

Введение

Создание доступных веб-приложений — это не просто хорошая практика, теперь это необходимость. Недавно у меня появилась возможность создать навигационное меню с акцентом на a11y. В ходе исследования я понял, что большинство строк меню не соответствуют шаблону ARIA. Например, знаете ли вы, что вместо перемещения по пунктам меню с помощью табуляции по строке меню следует перемещаться с помощью клавиш со стрелками и управлять собственным фокусом?

Хотя я нашел несколько руководств, я не полностью им следовал. Я пишу это, потому что считаю, что то, что я в итоге создал, стоит того, чтобы поделиться им — если вам также нравятся небольшие компоненты и специальные хуки.

Хотя я структурирую этот блог с помощью некоторых этапов разработки, моя цель не состоит в том, чтобы написать пошаговое руководство. Я надеюсь, что вы знаете основы React и то, как работают пользовательские перехватчики.

Сейчас я делюсь только ключевыми деталями реализации, но планирую дополнить эту статью примером песочницы кода в будущем, когда у меня будет больше времени.

Что мы строим?

В этом блоге мы создаем навигационное меню, подобное тем, которые вы видите вверху или сбоку многих веб-приложений. В этой строке меню некоторые пункты меню могут иметь подменю, которые открываются/закрываются при вводе/выключении мыши.

HTML-разметка

Прежде всего, для доступности необходимы семантический HTML и соответствующие роли и атрибуты ARIA. Подробнее о шаблоне меню можно прочитать в официальном документе здесь.

Вот пример соответствующей HTML-разметки:

Обратите внимание, что мы используем тег кнопки для семантического HTML. Кнопка также должна иметь aria-haspopup для оповещения программ чтения с экрана. Наконец, соответствующий атрибут aria-expanded должен быть назначен в зависимости от состояния меню.

Компоненты

Давайте пройдемся по компонентам, которые нам нужны. Очевидно, нам нужен общий компонент меню, а также компонент пункта меню.

Некоторые пункты меню имеют подменю, а некоторые нет. Пункты меню с подменю должны будут управлять своими состояниями для открытия/закрытия подменю при наведении курсора мыши и событиях клавиатуры. Поэтому это должен быть отдельный компонент.

Подменю также должны быть самостоятельными компонентами. Хотя подменю также являются просто контейнерами для пунктов меню, они не управляют своим состоянием и не обрабатывают события клавиатуры. Это отличает их от навигационного меню верхнего уровня.

В итоге я написал следующие компоненты:

  • NavMenu для внешнего слоя меню.
  • MenuItem для отдельных пунктов меню.
    • Ссылка на элемент меню
    • MenuItemWithSubMenu
  • SubMenu для расширенного подменю. 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 (
    
  );
}

Элемент e.preventDefault() предназначен для предотвращения прокрутки страницы такими вещами, как ArrowDown.

Вот компонент MenuItem. Давайте на секунду проигнорируем пункты с подменю. Мы используем 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 (
    
  • {item.label}
  • ); }

    Обратите внимание, что это тег, который должен иметь ссылку (кнопка для пункта меню с подменю), поэтому, когда они находятся в фокусе, поведение клавиатуры по умолчанию сработает, как и ожидалось, например, навигация по 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]);
    

    Повторное использование логики и собственный хук

    На данный момент у нас есть функциональные компоненты NavMenu и MenuItemLink. Перейдем к пункту меню с подменю.

    Поскольку я быстро его разрабатывал, я понял, что этот пункт меню будет использовать большую часть логики

    Заявление о выпуске Эта статья воспроизведена по адресу: https://dev.to/godsamit/building-an-accessible-navigation-menubar-with-react-hooks-blh?1. Если есть какие-либо нарушения, свяжитесь с [email protected], чтобы удалить это
    Последний учебник Более>

    Изучайте китайский

    Отказ от ответственности: Все предоставленные ресурсы частично взяты из Интернета. В случае нарушения ваших авторских прав или других прав и интересов, пожалуйста, объясните подробные причины и предоставьте доказательства авторских прав или прав и интересов, а затем отправьте их по электронной почте: [email protected]. Мы сделаем это за вас как можно скорее.

    Copyright© 2022 湘ICP备2022001581号-3