Accessible menu buttons in vue.js
Marcus Herrmann
Posted on January 30, 2019
tldr; https://github.com/marcus-herrmann/vue-menu-button
My approach is to first map common widgets and scenarios in Single Page Apps to accessible-app.com (for this reason modal dialogs and routing are already part of the demo application). What you also see very often are menu buttons. Now Heydon's right about this - when talking about menu buttons, the terminology soon becomes blurred. There are some "menus" or "dropdowns" out there that will show navigation items on click or hover (sometimes the triggering entity is a navigation item itself) - and there is the Menu Button Design pattern" from WAI-ARIA Authoring Practices 1.1. The latter definition of the term is discussed below.
ARIA Menu Button chapter lists two examples of what they mean by that:
- Navigation menu buttons
- Action menu buttons
Differences
The difference between these two approaches lies in the content of the menu. Navigation menus contain links:
The menu items are made from HTML links and maintain their HTML link behaviors. That is, activating a menuitem loads the link target, and the browser's link context menu and associated actions are available.
While Action menus contain actions (duh). To my understanding, these menu actions have things in common with buttons (e.g. they don't change the URL), but lack many of their advantages (e.g. listening on "Enter" and "Space" key events, automatically being part of the tab-order).
Similarities
In standard navigation menus we often see the use of role="menu" and role="menu-item". In this context, these uses are wrong, in Menu Buttons however correct (again, see the ARIA practice). This leads to a general structure that both Navigation menu buttons and Action menu buttons share:
button
menu
menu-item
menu-item
menu-item
Now we have to convey relationships between this items. ARIA requires for menu button to have
-
aria-haspopup="true"
to indicate the button opens a menu -
aria-controls="IDREF"
to refer to the element controlled, in our case, the menu -
aria-expanded="true"
when the menu is open (aria-expanded="false"
or the removal of the attribute altogether communicate that the menu is closed
This gives us the following HTML for the button and the menu wrapper:
<button aria-haspopup="true" aria-controls="the-menu" aria-expanded="false">
Open me
</button>
<ul id="the-menu" role="menu">
(menu-item)
(menu-item)
(menu-item)
</ul>
I left out the markup for the menu-items deliberately, because it has to do with the decision where we want to build a Navigation menu button or Action menu button.
For Navigation menu button items it is:
<li role="none"><a role="menu-item">About Page</a></li>
Whereas for Action menu items:
<li role="menu-item" tabindex="-1">Print this page</li>
At first glance, this is odd. But when you look closer, you'll see why it all makes sense:
- The list with the id "the-list" is semantically speaking no list anymore because it has got
role="menu"
. So, henceforth, it is a menu - A menu expects to have menu-items as its children. Therefore we strip the
li
of its semantic meaning as listitem:- When it is a Navigation menu-item, we set the
li
's role to none, but give the anchor element inside it the role ofmenu-item
- When it is an Action menu-item, the
li
becomes a menu item. Also with the tabindex attribute we make sure we can set focus the item programmatically (more on that below).
- When it is a Navigation menu-item, we set the
Keyboard accessibility
Now that we established a menu - we essentially promised that the user can use it like one. And by "using" I mean that our menu reacts to the following keystrokes
- ESC/Enter: closing the menu, setting focus to menu button
- Up Arrow: Moves focus to the previous menu item, or: If focus is on the first menu item, moves focus to the last menu item.
- Down Arrow: Moves focus to the next menu item, or: If focus is on the last menu item, moves focus to the first menu item.
ReachUI's approach
Having read and studied the ARIA Authoring Practices I searched form menu button implementations in React and Vue. I found many pretty components with fancy animations and all, but all of them lacking both the menu button semantics and keyboard behaviours. Let's just assume that these scripts don't want to be menu buttons at all.
Luckily there's React's ReachUI and its creator Ryan Florence wants to supply a proper menu button component (find the "MenuButton (Dropdown)" component here). When I looked in its code examples I found something very interesting - a mix between Navigation and Action menu button:
<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
component="a"
href="https://reach.tech/workshops"
>Attend a Workshop</MenuLink>
</MenuList>
</Menu>
My vue-menu-button
This is an interesting approach I decided to emulate for Vue and especially for #accessibleapp. As of now, you can see the said button in use on vuejs.accessible-app.com ("Account") and find the code for vue-menu-button on GitHub.
Things I borrowed from ReachUI's component
- When your focus is on the button element and you hit either Up Arrow or Down Arrow the menu opens and movies focus to the first or last menu item respectively.
- The API of the component: If you want an action item, you can use
<MenuItem />
in Reach or<menu-item />
my Vue component. If you want to place a link (or Navigation list-item) you can write<MenuLink component="a" href="https://reach.tech/" ... >
in Reach and you can use a<menu-link />
component in my script. Bonus: you can place a<router-link>
in there, and it still works!
So here's the template of Accessible App's AccountButton component:
<template>
<menu-wrapper>
<template slot="menu-button"
>Account
</template>
<template slot="menu-content">
<menu-link>
<router-link to="/orders">Past Orders</router-link>
</menu-link>
<menu-link>
<router-link to="/settings">My Settings</router-link>
</menu-link>
<menu-item @click="doSomething">Clear my Shopping cart</menu-item>
</template>
</menu-wrapper>
</template>
I'm somehow aware that my script could be improved regarding its options and configuration (if you have suggestions - please contribute!). But what I wanted to do first is to show a way of how to write such a component in a semantic way, to listen to key events, to offer a reasonably API, and for example, how to solve the aria-controls
reference. I hope I succeeded. But even if I didn't, I immersed myself intensively in the Menu Button design pattern during my research - and that alone was worth the effort.
Posted on January 30, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.