Menu (or not)

This article marks the third time I try to create a useful menu pattern for accessible-app.com - but hopefully the third time's the charm. But before I explain the (hopefully) final and correct menu solution let's look back to past trails and (eventually failed) implementations.

Approach 1

At first, I aimed to build a (I think) typical web app menu based both on the "Application Menu" web accessibility tutorial, while at the same time creating something like Reach UI's menu button, but for Vue. To explain: Reach's MenuButton component can have to kinds of child components, MenuItem and MenuLink. The construct itself adheres to WAI Aria Practice's Action Menu button example and makes it easy for developers to use (aside: this is very important - authors who share their components and care for accessibility should strip/abstract as much of the "hard ARIA parts" away as possible and make their components or APIs easy to use).

Reach's example goes like this, and that was what I was aiming for as well.

<Menu>
  <MenuButton>
    Actions <span aria-hidden>▾</span>
  </MenuButton>
  <MenuList>
    <MenuItem onSelect={() => alert("Download")}>Download</MenuItem>
    <MenuItem onSelect={() => alert("Copy")}>Create a Copy</MenuItem>
    <MenuItem onSelect={() => alert("Mark as Draft")}>Mark as Draft</MenuItem>
    <MenuItem onSelect={() => alert("Delete")}>Delete</MenuItem>
    <MenuLink
      as="a"
      href="https://reach.tech/workshops"
    >Attend a Workshop</MenuLink>
  </MenuList>
</Menu>

So I thought: okay, problem solved, let's move on to the next web app pattern. But then I learned that WAI's authoring practice on menu navigation is somehow disputed among accessibility specialists and screen reader users find it problematic for navigation links. In said discussion, chigkm comments:

I know people have a good intention for using aria everywhere, and developers are probably thinking that they're doing extra job to accommodate AT users by putting menu and menuitem roles manually. However, it actually creates an inconvenience by triggering screen reader to go into application mode and disable html navigation keys for screen reader.

After having read all that I knew two things:

  • WAI ARIA authoring practices should not be treated as gospel, and before you implement any of them (or try to share knowledge based on an AP), check if you find a related discussion to said practice
  • I have to revisit the menu pattern for accessible app.

Approach 2

Only short time later I was reminded of details/summary, later stumbled upon a way GitHub uses this pattern and wrote a short article about it. GitHub's Mu-An Chiou also shared dedicated presentation slides with many of various usages.

Nice, I thought. They built a menu out of it and because of its in-built accordion-like open functionality you don't even have to implement a click handler (for the modern browsers, at least). Also it is good to follow the first rule of aria, that is: not to use it at all.

Case closed, let's move on for real this time.

Unfortunately only a few weeks later I read "I was wrong about JavaScript-free dropdowns" by Chris Ferdinandi. Chris followed the same reasoning, was also excited of having found an elegant solution. But then Scott O'Hara added a little context:

Scott explained to me that while those elements are perfectly valid for JavaScript-free accordions, they don’t work for dropdown menus. [...] Their use as a dropdown is what Scott calls “technically accessible.” The content is there and can be accessed, but it doesn’t provide an experience that’s clear and obvious to people using assistive technology. In that way, it’s “functionally inaccessible.”

Also, accessibility expert Adrian Roselli wrote about the topic in "Details / Summary Are Not [insert control here]" in his own way - his style of explaining this misunderstanding differs a little bit from Scott's, but still both mean the same thing: You must not build a menu with details/summary.

So back to the drawing board. Unfortunately, I didn't have much time in spring due to customer projects. And as soon as the stress was over I invested time in finally launching the first proper version of accessible-app.com, and I omitted the menu pattern for launch.

Approach 3

The plan for the third approach was at first - going back to the Authoring Practices, but doing it right this time. In the AP related GitHub issue people agree on Heydon Pickering's way of explaining things in Inclusive Components is far more clear than the authoring practice's formulations itself. Following Inclusive Component's Menus & Menu Buttons article meant building two menus, but not mixing action menu and navigation menu functionalities. For this purpose I chose the menu buttons "Shopping Cart" and "Account" in the demo app. The Shopping Cart menu was going to be a "pure" action menu, while "Account" only harbors links, making it a "pure" navigation menu. No re-purposing of the menu pattern for links, not mixing actions and links.

I built the following Shopping cart (action) menu:

<div class="c-shopping-cart">
<div class="shopping-cart__item-count">1
    <span class="u-visually-hidden"> items</span>
</div>
<button aria-haspopup="true" aria-expanded="true"
 class="o-button" aria-controls="someid">Shopping Cart</button>

<div role="menu" aria-hidden="false" id="someid">
    <ul role="presentation" class="c-shopping-cart__list">
        <li role="presentation" class="c-shopping-cart__list-item">
            <span id="product2" aria-hidden="true">
            Inclusive Components
            </span>
            <button role="menuitem" id="delete2"
                aria-labelledby="product2 delete2">
                <!-- Trash can icon here -->
                <span class="u-visually-hidden">
                    Remove from shopping cart
                </span>
            </button>
        </li>
    </ul>
    <button class="o-button o-button--bare" role="menuitem">
        Remove all items
    </button>
</div>

(Don't use this code example!)

This code example is a little unclear, but here's my reasoning behind it:

  • The Shopping Cart only offers menu actions such as removing one or all icons from the cart - no location changes, no links, just shopping cart modifications
  • Even if the visual representation is different, menus should only have actionable items, menuitems
  • I tried to use buttons for these actions, but overriding their roles with "menuitems". So I could re-use their event listeners and am also sure that the menuitems are in the tabindex (since they are buttons).
  • The accessible name of the menuitem that is related to removing one particular item on the cart is created of two parts - the product label itself, in this case id="product2" and the label of its delete button, id="delete2" (see aria-labelledby attribute which "glues" both of them together, so the expected output would be "Inclusive Components Remove from Shopping Cart":
<button role="menuitem" id="delete2"
    aria-labelledby="product2 delete2" />

(Don't use this code example!)

Regarding the rendered accessibility tree this is all fine. But before releasing it and writing about it I want to check this construct with someone and Heydon came to my mind. He has helped me in the past with the accessible routing pattern, is a supporter of accessible app since its inception and I met him just weeks ago and the A11yClub BarCamp in Düsseldorf (and many more great people, but that's maybe a separate article).

Anyway - he confirmed that the approach was actually correct, but pointed out a bug in VoiceOver, and concluded:

I do think the menuitem role is screwing it up. No harm in just having a list, with text + <button>s

While this does mean I'm still not finished with the menu pattern it gave me a reminder that you always have to watch out for bugs (in this case, VoiceOver bugs) regardless if your code is technically correct. Also that role="menu" is a beast of its own, should be used with caution even in an "action menu" context, and that it does not always pay off to try something "smart" but to go for the simple route.

Approach 3.5

The last - and hopefully final - approach for the Shopping Cart menu is the one I applied to the Account button after correcting the details/summary misunderstanding. It goes as follows:

Unsurprisingly the first part of your menu is the triggering button. Upon click interaction (a button also listens for the space bar and enter key press events when you bind a click event) it will toggle the visibility of the dropdown (the container in which you find the items of the menu - note that I avoid the expression "menuitem" here).

Said button has to tell the accessibility tree - and therefore eventually assistive technology - its state: is the content of the dropdown visible or not? That what aria-expanded is for. Note - it has to be applied on the button, not the container whose visibility is toggled. Further it does not hurt to make the relation between button explicit by aria-controls . Please not that aria-controls is no miracle worker, only supported in the JAWS screen reader, but after all, not causes any harm (please read inclusive-components.design for more information). Lastly make sure you actually change the visibility of the dropdown container. I solved it by toggling between aria-hidden="true|false". All together now:

<button aria-expanded="false" aria-controls="someid">Account</button>
<div hidden id="someid">
    <ul>
        <li><a href="/orders" >Past Orders</a></li>
        <li><a href="/settings">My Settings</a></li>
    </ul>
</div>

This code is for the Account button, obviously, but now the same principles apply for the Shopping Cart menu now: Once the cart is not empty a list will be rendered and every item will be a list item with its own remove button. Below this list there will be a "Remove all" button.

With a click on the button you change two things:

  • The value of aria-expanded on the button. This indicates the state of the dropdown the button controls - is it expanded or collapsed, open or closed?
  • The existence of the hidden attribute on the dropdown. If it exists you don't have to explicitly use CSS for hiding the dropdown container.

This is all - it's a simple but working example. Now after three and a half refactorings it feels like these are sometimes the best and most robust ones to recommend. At times at the beginning of the research it seems like you find the perfect, often even "official", tutorial that turns out just being only partly robust compared to the most simple way. I will definitely keep this in mind during the course of the accessible-app.com project.

**Update: 2019-06-28: Just like Paul J. Adam suggested I changed aria-hidden=true|false to mere hidden. I also extended the explaination what to change/swap on button click