cms icon indicating copy to clipboard operation
cms copied to clipboard

A11Y: Make tabs accessible

Open JonKaric opened this issue 4 years ago • 1 comments

This will fix statamic/a11y#1

Currently, publish tabs output this markup which makes it impossible to navigate if you're not using a mouse.

<div>
  <div class="publish-tabs tabs">
    <a class="active">First tab</a>
    <a class="">Second tab</a>
    <a class="">Third tab</a>
  </div>

  <div class="flex justify-between">
      <div class="publish-section rounded-tl-none" style="">...</div>
      <div class="publish-section rounded-tl-none" style="">...</div>
      <div class="publish-section rounded-tl-none" style="">...</div>
      <div class="publish-section rounded-tl-none" style="">...</div>
  </div>
</div>

What we need to do

The element that serves as the container for the set of tabs has role tablist.

Each element that serves as a tab has role tab and is contained within the element with role tablist.

Each element that contains the content panel for a tab has role tabpanel.

If the tab list has a visible label, the element with role tablist has aria-labelledby set to a value that refers to the labelling element. Otherwise, the tablist element has a label provided by aria-label. [1]

<div>
  <div class="publish-tabs tabs" role="tablist" aria-label="Edit entry">
      <button class="active" role="tab">Main</button>
      <button role="tab">Settings</button>
      <button role="tab">Example third tab</button>
  </div>

  <div class="flex justify-between">
      <div role="tabpanel" >...</div>
      <div role="tabpanel">...</div>
      <div role="tabpanel">...</div>
  </div>
  
</div>

[1] We need to best describe what the tabs are & what they do in a very short label.


Continued:


Each element with role tab has the property aria-controls referring to its associated tabpanel element.

This means we need to add id attributes to the tabpanel elements, so aria-controls knows where to look. [2]

The active tab element has the state aria-selected set to true and all other tab elements have it set to false.

Each element with role tabpanel has the property aria-labelledby referring to its associated tab element.

Again we need to add id attributes, but this time to the tabs themselves. [2]

<div>
<div class="publish-tabs tabs" role="tablist" aria-label="Edit entry">
    <button class="active" role="tab" aria-controls="Main-Tab" aria-selected="true" id="main">Main</button>
    <button role="tab" aria-controls="Settings-Tab" aria-selected="false" id="Settings">Settings</button>
    <button role="tab" aria-controls="ExampleThirdTab-Tab" aria-selected="false" id="ExampleThirdTab">Example third tab</button>
  </div>

  <div class="flex justify-between">
      <div role="tabpanel" id="Main-Tab" aria-labelledby="Main">...</div>
      <div role="tabpanel" id="Settings-Tab" aria-labelledby="Settings">...</div>
      <div role="tabpanel" id="ExampleThirdTab-Tab" aria-labelledby="ExampleThirdTab">...</div>
  </div>
  
</div>

[2] id attributes should be CamelCased to give screen readers better clues that they should be read as individual words.

Lastly: Implementing roving tabindex

Roving tab index is the concept of having only the outermost element focusable via tab. This helps users by not forcing them to tab into every focusable element to get to the main content.

How this works in tabs

  • When a user tabs into the tabs component the element with role="tab" is focused.
  • The user is then able to navigate the tabs by using the following keys: left arrow, right arrow, home and end.
  • The next time a user presses tab, the next element that is focused is the element with role="tabpanel" that corresponds with the currently active tab.

How to accomplish this

  1. All elements with role="tab" should have tabindex="-1" set, except for the first active tab, where no tabindex attribute is needed.

  2. All elements with role="tabpanel" should be hidden. Except for the first active tab

  3. When a navigation key (see above) is activated to navigate the tabs: 3.1) The tab you're moving to should have the tabindex attribute removed. 3.2) The tab you're moving from should have the tabindex attribute set to -1. 3.3) The tabpanel you're moving to should be unhidden. 3.4) The tabpanel you're moving from should be hidden.

  4. All elements with role="tabpanel" should have tabindex="0" set.

Functionality

  • Left Arrow: Moves focus to the previous tab.
  • Right Arrow: Moves focus to the next tab.
  • Space or Enter: Activates the tab if it was not activated automatically on focus.
  • (optional) Home: Activated the first tab in the tab list.
  • (optional) End: Activated the last tab in the tab list.
  • aria-selected needs to change to true or false when the tab is activated or inactivated.
  • Roving tabindex (see above)

Taking it a step further

(From https://www.w3.org/TR/wai-aria-practices/#tabpanel)

It is recommended that tabs activate automatically when they receive focus as long as their associated tab panels are displayed without noticeable latency. This typically requires tab panel content to be preloaded. Otherwise, automatic activation slows focus movement, which significantly hampers users' ability to navigate efficiently across the tab list. For additional guidance, see § 6.4 Deciding When to Make Selection Automatically Follow Focus.

Extended reading

Tabs

https://www.w3.org/TR/wai-aria-practices/#tabpanel

https://www.w3.org/TR/wai-aria-practices-1.1/examples/tabs/tabs-1/tabs.html

https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/Tab_Role

Roving tabindex

https://www.w3.org/TR/wai-aria-practices/#kbd_roving_tabindex

https://developer.mozilla.org/en-US/docs/Web/Accessibility/Keyboard-navigable_JavaScript_widgets#technique_1_roving_tabindex

JonKaric avatar Apr 20 '21 11:04 JonKaric

Hi Jon, just dropping in to say that’s some awesome research and you’ve summarized it nicely👍. If I find some spare time I’ll try to implement this (unless someone else already did)!

jelleroorda avatar Apr 21 '21 13:04 jelleroorda