В этой статье я научу вас, как создать доступное пользовательское раскрывающееся меню выбора в React.

Чтобы правильно воспроизвести элемент select, мы должны посмотреть, что это такое. Согласно MDN, неявная роль ARIA раскрывающегося списка select — это либо combobox, либо listbox. Для этого урока я буду использовать listbox.

Если вы хотите проверить CodePen, прежде чем углубляться в объяснение, вот ссылка на него.

Шаг 1. Настройте HTML.

Мы можем рассматривать элемент select как простой элемент button, при нажатии на который открывается список параметров, из которых мы можем выбирать. В коде это будет выглядеть примерно так:

const CustomSelect = () => {
  const [isOptionsOpen, setIsOptionsOpen] = useState(false);
  const [selectedOption, setSelectedOption] = useState(0);
  const optionsList = [
    "Option 1",
    "Option 2",
    "Option 3",
    "Option 4",
    "Option 5"
  ];
  const toggleOptions = () => {
    setIsOptionsOpen(!isOptionsOpen);
  };
  return (
    <div className="wrapper">
      <div className="container">
        <button
          type="button"
          onClick={toggleOptions}
        >
          Open Me!
        </button>
        <ul
          className={`options ${isOptionsOpen ? "show" : ""}`}
          tabIndex={-1}
        >
          {optionsList.map((option, index) => (
            <li
              tabIndex={0}
            >
              {option}
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
};

В этом примере список не отображается (display: none) по умолчанию и отображается только тогда, когда isOptionsOpen возвращает true путем добавления другого класса с именем show.

Важно использовать семантические элементы, такие как button[type="button"] и ul, чтобы сообщить вспомогательным технологиям, таким как программа чтения с экрана, что элемент будет выполнять какое-либо действие при нажатии (button) или что это список вещей в группе (ul).

tab-index=-1 в ненумерованном списке заставит сфокусироваться на нем, как только раскрывающийся список будет открыт, в то время как tab-index=0 в элементах списка сообщает устройству, что эти элементы могут быть пролистаны вкладками.

Шаг 2. Прикрепите правильные роли ARIA.

Для нашего элемента кнопки мы можем прикрепить атрибут с именем aria-haspopup, чтобы указать, что он имеет всплывающее окно при нажатии, и установить listbox в качестве его значения, чтобы указать, что это за всплывающее окно. Мы также должны добавить атрибут aria-expanded, чтобы указать, развернут или свернут наш пользовательский элемент выбора. Теперь наша кнопка будет выглядеть следующим образом:

<button
    type="button"
    aria-haspopup="listbox"
    aria-expanded={isOptionsOpen}
    onClick={toggleOptions}
>
    Open Me!
</button>

Затем мы должны добавить role в наш список опций и установить его на listbox. Добавление aria-activedescendant в наш список также позволит нам указать, какой вариант выбран в данный момент.

Наконец, каждая из наших опций должна иметь role из option, чтобы указать, что они являются опцией, aria-selected, чтобы сообщить нам, выбрана ли эта опция в данный момент или нет, и идентификатор, на который может ссылаться наш aria-activedescendant.

<ul
    className={`options ${isOptionsOpen ? "show" : ""}`}
    role="listbox"
    aria-activedescendant={optionsList[selectedOption]}
    tabIndex={-1}
>
    {optionsList.map((option, index) => (
        <li
	    id={option}
            role="option"
            aria-selected={selectedOption == index}
            tabIndex={0}
        >
            {option}
        </li>
    ))}
</ul>

Шаг 3. Добавьте функциональность.

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

// Set button text to equal whatever option is selected
<button
    type="button"
    aria-haspopup="listbox"
    aria-expanded={isOptionsOpen}
    className={isOptionsOpen ? "expanded" : ""}
    onClick={toggleOptions}
>
    {optionsList[selectedOption]}
</button>
<ul
    className={`options ${isOptionsOpen ? "show" : ""}`}
    role="listbox"
    aria-activedescendant={optionsList[selectedOption]}
    tabIndex={-1}
>
    {optionsList.map((option, index) => (
        <li
            id={option}
	    role="option"
	    aria-selected={selectedOption == index}
	    tabIndex={0}
	    // Upon clicking, set as selected option then close the dropdown
	    onClick={() => {
	        setSelectedOption(index);
		setIsOptionsOpen(false);
	    }}
	>
	    {option}
	</li>
    ))}
</ul>

Кроме того, мы установили текст кнопки равным любой опции, выбранной в данный момент.

Шаг 4. Настройте навигацию с помощью клавиатуры.

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

// Event handler for keydowns
const handleKeyDown = (index) => (e) => {
    switch (e.key) {
      case " ":
      case "SpaceBar":
      case "Enter":
        e.preventDefault();
        setSelectedOption(index);
        setIsOptionsOpen(false);
        break;
      default:
        break;
    }
};
// Attach the event handler to each option
{optionsList.map((option, index) => (
    <li
	id={option}
        role="option"
        aria-selected={selectedOption == index}
        tabIndex={0}
        onKeyDown={handleKeyDown(index)}
        onClick={() => {
	    setSelectedOption(index);
            setIsOptionsOpen(false);
        }}
    >
	{option}
    </li>
))}

И это в основном все! Теперь мы можем стилизовать наш настраиваемый раскрывающийся список так, как нам нравится.

Опять же, если вы хотите ознакомиться с кодом целиком, вот ссылка на него на CodePen.

Удачного кодирования!

Если вам понравилась эта статья или вы сочли ее полезной, подпишитесь на меня здесь или в Твиттере. Спасибо за прочтение!