5
<title>Example: Accessible Menu Button</title>
6
<link rel="stylesheet" href="http://yui.yahooapis.com/3.4.0pr3/build/cssgrids/grids-min.css">
7
<link rel="stylesheet" href="../assets/css/main.css">
8
<link rel="stylesheet" href="../assets/vendor/prettify/prettify-min.css">
9
<script src="../../build/yui/yui-min.js"></script>
14
<h1>Example: Accessible Menu Button</h1>
17
<a href="#toc" class="jump">Jump to Table of Contents</a>
21
<div id="main" class="yui3-u">
22
<div class="content"><div class="intro">
24
This example illustrates how to use the Focus Manager Node Plugin,
25
Event's <a href="http://yuilibrary.com/yui/docs/api/classes/YUI.html#event_delegate">delegation support</a> and
26
<a href="http://yuilibrary.com/yui/docs/api/classes/YUI.html#event_mouseenter">mouseenter</a> event, along with
27
the <a href="../overlay/index.html">Overlay widget</a> and Node's support for the
28
<a href="http://www.w3.org/TR/wai-aria/">WAI-ARIA Roles and States</a> to
29
create an accessible menu button.
35
/* The following two styles are necessary to override style rules in the
43
text-decoration: none;
47
/* Hide the button and list while it is being transformed into a menu button. */
48
.yui3-js-enabled .yui3-menubutton-loading #menu-1,
49
.yui3-js-enabled .yui3-menubutton-loading #button-1 {
55
<div class="yui3-menubutton-loading">
56
<a id="button-1" href="#menu-1"><span><span>Move To</span></span></a>
59
<li><input type="button" name="button-1" value="Inbox"></li>
60
<li><input type="button" name="button-2" value="Archive"></li>
61
<li><input type="button" name="button-3" value="Trash"></li>
71
fullpath: "../assets/node-focusmanager/menubutton.css"
75
fullpath: "../assets/node-focusmanager/menubutton.js",
76
requires: ["menubuttoncss", "node-focusmanager", "node-event-simulate", "overlay"]
79
}).use("menubuttonjs");
84
<h2 id="setting-up-the-html">Setting Up the HTML</h2>
87
For a menu button, start with an <code><a></code> element whose
88
<code>href</code> attribute is set to the id of an <code><div></code>
89
that wraps a list of <code><input></code> elements.
90
Therefore, without JavaScript and CSS, the menu button is simply an in-page
91
link to a set of additional buttons.
94
<pre class="code prettyprint"><div class="yui3-menubutton-loading">
95
<a id="button-1" href="#menu-1"><span><span>Move To</span></span></a>
96
<div id="menu-1">
98
<li><input type="button" name="button-1" value="Inbox"></li>
99
<li><input type="button" name="button-2" value="Archive"></li>
100
<li><input type="button" name="button-3" value="Trash"></li>
103
</div></pre>
106
<h2 id="progressive-enhancement">Progressive Enhancement</h2>
109
To account for the scenario where the user has CSS enabled in their browser but JavaScript
110
is disabled, the CSS used to style the menu button will be loaded via JavaScript
111
using the YUI instance's <a href="../yui/index.html#loader">built-in Loader</a>.
114
<pre class="code prettyprint">YUI({
116
"menubuttoncss": {
117
type: "css",
118
fullpath: "../assets/node-focusmanager/menubutton.css"
120
"menubuttonjs": {
121
type: "js",
122
fullpath: "../assets/node-focusmanager/menubutton.js",
123
requires: ["menubuttoncss", "node-focusmanager", "node-event-simulate", "overlay"]
126
}).use("menubuttonjs");</pre>
130
To prevent the user from seeing a flash unstyled content when JavaScript is enabled,
131
a style rule is created using YUI's <code>yui3-js-enabled</code> class name that will temporarily
132
hide the markup while the JavaScript and CSS are in the process of loading. For more on using the
133
<code>yui3-js-enabled</code> class name, see the
134
<a href="../widget/index.html#progressive">Hiding Progressively Enhanced Markup</a> section of the
135
<a href="../widget/index.html">YUI Widget landing page</a>.
138
<pre class="code prettyprint">/* Hide the button and list while it is being transformed into a menu button. */
140
.yui3-js-enabled .yui3-menubutton-loading #menu-1,
141
.yui3-js-enabled .yui3-menubutton-loading #button-1 {
146
<h2 id="aria-support">ARIA Support</h2>
149
Through the use of CSS and JavaScript the HTML for the menu button can be
150
transformed into something that looks and behaves like a desktop menu button,
151
but users of screen readers won't perceive it as an atomic widget, but rather
152
simply as a set of HTML elements. However, through the application
154
<a href="http://www.w3.org/TR/wai-aria/">WAI-ARIA Roles and States</a>, it is
155
possible to improve the semantics of the markup such that users of screen
156
readers perceive it as a menu button control.
159
<h2 id="keyboard-functionality">Keyboard Functionality</h2>
162
The keyboard functionality for the button's menu will be provided by the Focus
163
Manager Node Plugin. The Focus Manager's
164
<a href="http://yuilibrary.com/yui/docs/api/classes/plugin.NodeFocusManager.html#config_descendants"><code>descendants</code></a>
165
attribute is set to a value of "input", so that only one menuitem in the
166
button's menu is in the browser's default tab flow. This allows users
167
navigating via the keyboard to use the tab key to quickly move into and out of
168
the menu. Once the menu has focus, the user can move focus among each menuitem
169
using the up and down arrows keys, as defined by the value of the
170
<a href="http://yuilibrary.com/yui/docs/api/classes/plugin.NodeFocusManager.html#config_keys"><code>keys</code></a>
172
<a href="http://yuilibrary.com/yui/docs/api/classes/plugin.NodeFocusManager.html#config_focusClass"><code>focusClass</code></a>
173
attribute is used to apply a class of <code>yui-menuitem-active</code> to
174
the parent <code><li></code> of each
175
<code><input></code> when it is focused, making it easy to style the
176
menuitem's focused state in all browsers.
178
<pre class="code prettyprint">YUI().use("*", function (Y) {
180
var menuButton = Y.one("#button-1"),
184
var initMenu = function () {
186
menu = new Y.Overlay({
187
contentBox: "#menu-1",
195
Y.one("#menu-1").setStyle("display", "");
197
var boundingBox = menu.get("boundingBox"),
198
contentBox = menu.get("contentBox");
200
boundingBox.addClass("yui3-buttonmenu");
201
contentBox.addClass("yui3-buttonmenu-content");
204
// Append a decorator element to the bounding box to render the shadow.
206
boundingBox.append('<div class="yui3-menu-shadow"></div>');
209
// Apply the ARIA roles, states and properties to the menu.
211
boundingBox.setAttrs({
212
role: "menu",
213
"aria-labelledby": menuLabelID
216
boundingBox.all("input").set("role", "menuitem");
219
// For NVDA: Add the role of "presentation" to each LI
220
// element to prevent NVDA from announcing the
221
// "listitem" role.
223
boundingBox.all("div,ul,li").set("role", "presentation");
226
// Use the FocusManager Node Plugin to manage the focusability
227
// of each menuitem in the menu.
229
contentBox.plug(Y.Plugin.NodeFocusManager, {
231
descendants: "input",
232
keys: { next: "down:40", // Down arrow
233
previous: "down:38" }, // Up arrow
235
className: "yui3-menuitem-active",
236
fn: function (node) {
237
return node.get("parentNode");
245
// Subscribe to the change event for the "focused" attribute
246
// to listen for when the menu initially gains focus, and
247
// when the menu has lost focus completely.
249
contentBox.focusManager.after("focusedChange", function (event) {
251
if (!event.newVal) { // The menu has lost focus
253
// Set the "activeDescendant" attribute to 0 when the
254
// menu is hidden so that the user can tab from the
255
// button to the first item in the menu the next time
256
// the menu is made visible.
258
this.set("activeDescendant", 0);
265
// Hide the button's menu if the user presses the escape key
266
// while focused either on the button or its menu.
268
Y.on("key", function () {
273
}, [menuButton, boundingBox] ,"down:27");
278
// Set the width and height of the menu's bounding box -
279
// this is necessary for IE 6 so that the CSS for the
280
// shadow element can simply set the shadow's width and
281
// height to 100% to ensure that dimensions of the shadow
282
// are always sync'd to the that of its parent menu.
284
menu.on("visibleChange", function (event) {
288
boundingBox.setStyles({ height: "", width: "" });
290
boundingBox.setStyles({
291
height: (boundingBox.get("offsetHeight") + "px"),
292
width: (boundingBox.get("offsetWidth") + "px") });
301
menu.after("visibleChange", function (event) {
303
var bVisible = event.newVal;
305
// Focus the first item when the menu is made visible
306
// to allow users to navigate the menu via the keyboard
310
// Need to set focus via a timer for Webkit and Opera
311
Y.Lang.later(0, contentBox.focusManager, contentBox.focusManager.focus);
315
boundingBox.set("aria-hidden", (!bVisible));
320
// Hide the menu when one of menu items is clicked.
322
boundingBox.delegate("click", function (event) {
324
alert("You clicked " + this.one("input").get("value"));
326
contentBox.focusManager.blur();
332
// Focus each menuitem as the user moves the mouse over
333
// the menu.
335
boundingBox.delegate("mouseenter", function (event) {
337
var focusManager = contentBox.focusManager;
339
if (focusManager.get("focused")) {
340
focusManager.focus(this.one("input"));
346
// Hide the menu if the user clicks outside of it or if the
347
// user doesn't click on the button
349
boundingBox.get("ownerDocument").on("mousedown", function (event) {
351
var oTarget = event.target;
353
if (!oTarget.compareTo(menuButton) &&
354
!menuButton.contains(oTarget) &&
355
!oTarget.compareTo(boundingBox) &&
356
!boundingBox.contains(oTarget)) {
367
menuButton.addClass("yui3-menubutton");
370
// Hide the list until it is transformed into a menu
372
Y.one("#menu-1").setStyle("display", "none");
375
// Remove the "yui3-menubutton-loading" class from the parent container
376
// now that the necessary YUI dependencies are loaded and the
377
// menu button has been skinned.
379
menuButton.ancestor(".yui3-menubutton-loading").removeClass("yui3-menubutton-loading");
382
// Apply the ARIA roles, states and properties to the anchor.
384
menuButton.setAttrs({
385
role: "button",
386
"aria-haspopup": true
390
// Remove the "href" attribute from the anchor element to
391
// prevent JAWS and NVDA from reading the value of the "href"
392
// attribute when the anchor is focused.
394
if ((Y.UA.gecko || Y.UA.ie) && navigator.userAgent.indexOf("Windows") > -1) {
396
menuButton.removeAttribute("href");
398
// Since the anchor's "href" attribute has been removed, the
399
// element will not fire the click event in Firefox when the
400
// user presses the enter key. To fix this, dispatch the
401
// "click" event to the anchor when the user presses the
402
// enter key.
404
Y.on("key", function (event) {
406
menuButton.simulate("click");
408
}, menuButton, "down:13");
413
// Set the "tabIndex" attribute of the anchor element to 0 to
414
// place it in the browser's default tab flow. This is
415
// necessary since 1) anchor elements are not in the default
416
// tab flow in Opera and 2) removing the "href" attribute
417
// prevents the anchor from firing its "click" event
418
// in Firefox.
420
menuButton.set("tabIndex", 0);
422
// Since there is some intermediary markup (<span>s) between the anchor element with the role
423
// of "button" applied and the text label for the anchor - we need to use the
424
// "aria-labelledby" attribute to ensure that screen readers announce the text label for the
427
var menuLabel = menuButton.one("span span"),
428
menuLabelID = Y.stamp(menuLabel);
430
menuLabel.set("id", menuLabelID);
431
menuButton.set("aria-labelledby", menuLabelID);
433
var showMenu = function (event) {
435
// For performance: Defer the creation of the menu until
436
// the first time the button is clicked.
443
if (!menu.get("visible")) {
445
menu.set("align", {
447
points: [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]
454
// Prevent the anchor element from being focused
455
// when the users mouses down on it.
456
event.preventDefault();
461
// Bind both a "mousedown" and "click" event listener to
462
// ensure the button's menu can be invoked using both the
463
// mouse and the keyboard.
465
menuButton.on("mousedown", showMenu);
466
menuButton.on("click", showMenu);
473
<div id="sidebar" class="yui3-u">
475
<div id="toc" class="sidebox">
477
<h2 class="no-toc">Table of Contents</h2>
483
<a href="#setting-up-the-html">Setting Up the HTML</a>
486
<a href="#progressive-enhancement">Progressive Enhancement</a>
489
<a href="#aria-support">ARIA Support</a>
492
<a href="#keyboard-functionality">Keyboard Functionality</a>
500
<div class="sidebox">
502
<h2 class="no-toc">Examples</h2>
506
<ul class="examples">
509
<li data-description="Creating an accessible toolbar using the Focus Manager Node Plugin and Node's support for the WAI-ARIA Roles and States.">
510
<a href="node-focusmanager-1.html">Accessible Toolbar</a>
515
<li data-description="Creating an accessible menu button using the Focus Manager Node Plugin, Event's delegation support and mouseenter event, along with the Overlay widget and Node's support for the WAI-ARIA Roles and States.">
516
<a href="node-focusmanager-3.html">Accessible Menu Button</a>
530
<script src="../assets/vendor/prettify/prettify-min.js"></script>
531
<script>prettyPrint();</script>