+ This example illustrates how to use 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 to + create an accessible menu button. +
+Setting Up the HTML
+ +
+For a menu button, start with an <a> element whose
+href attribute is set to the id of an <div>
+that wraps a list of <input> elements.
+Therefore, without JavaScript and CSS, the menu button is simply an in-page
+link to a set of additional buttons.
+
<div class="yui3-menubutton-loading"> + <a id="button-1" href="#menu-1"><span><span>Move To</span></span></a> + <div id="menu-1"> + <ul> + <li><input type="button" name="button-1" value="Inbox"></li> + <li><input type="button" name="button-2" value="Archive"></li> + <li><input type="button" name="button-3" value="Trash"></li> + </ul> + </div> +</div>+ + +
Progressive Enhancement
+ ++To account for the scenario where the user has CSS enabled in their browser but JavaScript +is disabled, the CSS used to style the menu button will be loaded via JavaScript +using the YUI instance's built-in Loader. +
+ +YUI({
+ modules: {
+ "menubuttoncss": {
+ type: "css",
+ fullpath: "../assets/node-focusmanager/menubutton.css"
+ },
+ "menubuttonjs": {
+ type: "js",
+ fullpath: "../assets/node-focusmanager/menubutton.js",
+ requires: ["menubuttoncss", "node-focusmanager", "node-event-simulate", "overlay"]
+ }
+ }
+}).use("menubuttonjs");
+
+
+
+To prevent the user from seeing a flash unstyled content when JavaScript is enabled,
+a style rule is created using YUI's yui3-js-enabled class name that will temporarily
+hide the markup while the JavaScript and CSS are in the process of loading. For more on using the
+yui3-js-enabled class name, see the
+Hiding Progressively Enhanced Markup section of the
+YUI Widget landing page.
+
/* Hide the button and list while it is being transformed into a menu button. */
+
+.yui3-js-enabled .yui3-menubutton-loading #menu-1,
+.yui3-js-enabled .yui3-menubutton-loading #button-1 {
+ display: none;
+}
+
+
+ARIA Support
+ ++Through the use of CSS and JavaScript the HTML for the menu button can be +transformed into something that looks and behaves like a desktop menu button, +but users of screen readers won't perceive it as an atomic widget, but rather +simply as a set of HTML elements. However, through the application +of the +WAI-ARIA Roles and States, it is +possible to improve the semantics of the markup such that users of screen +readers perceive it as a menu button control. +
+ +Keyboard Functionality
+ +
+The keyboard functionality for the button's menu will be provided by the Focus
+Manager Node Plugin. The Focus Manager's
+descendants
+attribute is set to a value of "input", so that only one menuitem in the
+button's menu is in the browser's default tab flow. This allows users
+navigating via the keyboard to use the tab key to quickly move into and out of
+the menu. Once the menu has focus, the user can move focus among each menuitem
+using the up and down arrows keys, as defined by the value of the
+keys
+attribute. The
+focusClass
+attribute is used to apply a class of yui-menuitem-active to
+the parent <li> of each
+<input> when it is focused, making it easy to style the
+menuitem's focused state in all browsers.
+
+
YUI().use("*", function (Y) {
+
+ var menuButton = Y.one("#button-1"),
+ menu;
+
+
+ var initMenu = function () {
+
+ menu = new Y.Overlay({
+ contentBox: "#menu-1",
+ visible: false,
+ tabIndex: null
+ });
+
+ menu.render();
+
+
+ Y.one("#menu-1").setStyle("display", "");
+
+ var boundingBox = menu.get("boundingBox"),
+ contentBox = menu.get("contentBox");
+
+ boundingBox.addClass("yui3-buttonmenu");
+ contentBox.addClass("yui3-buttonmenu-content");
+
+
+ // Append a decorator element to the bounding box to render the shadow.
+
+ boundingBox.append('<div class="yui3-menu-shadow"></div>');
+
+
+ // Apply the ARIA roles, states and properties to the menu.
+
+ boundingBox.setAttrs({
+ role: "menu",
+ "aria-labelledby": menuLabelID
+ });
+
+ boundingBox.all("input").set("role", "menuitem");
+
+
+ // For NVDA: Add the role of "presentation" to each LI
+ // element to prevent NVDA from announcing the
+ // "listitem" role.
+
+ boundingBox.all("div,ul,li").set("role", "presentation");
+
+
+ // Use the FocusManager Node Plugin to manage the focusability
+ // of each menuitem in the menu.
+
+ contentBox.plug(Y.Plugin.NodeFocusManager, {
+
+ descendants: "input",
+ keys: { next: "down:40", // Down arrow
+ previous: "down:38" }, // Up arrow
+ focusClass: {
+ className: "yui3-menuitem-active",
+ fn: function (node) {
+ return node.get("parentNode");
+ }
+ },
+ circular: true
+
+ });
+
+
+ // Subscribe to the change event for the "focused" attribute
+ // to listen for when the menu initially gains focus, and
+ // when the menu has lost focus completely.
+
+ contentBox.focusManager.after("focusedChange", function (event) {
+
+ if (!event.newVal) { // The menu has lost focus
+
+ // Set the "activeDescendant" attribute to 0 when the
+ // menu is hidden so that the user can tab from the
+ // button to the first item in the menu the next time
+ // the menu is made visible.
+
+ this.set("activeDescendant", 0);
+
+ }
+
+ });
+
+
+ // Hide the button's menu if the user presses the escape key
+ // while focused either on the button or its menu.
+
+ Y.on("key", function () {
+
+ menu.hide();
+ menuButton.focus();
+
+ }, [menuButton, boundingBox] ,"down:27");
+
+
+ if (Y.UA.ie === 6) {
+
+ // Set the width and height of the menu's bounding box -
+ // this is necessary for IE 6 so that the CSS for the
+ // shadow element can simply set the shadow's width and
+ // height to 100% to ensure that dimensions of the shadow
+ // are always sync'd to the that of its parent menu.
+
+ menu.on("visibleChange", function (event) {
+
+ if (event.newVal) {
+
+ boundingBox.setStyles({ height: "", width: "" });
+
+ boundingBox.setStyles({
+ height: (boundingBox.get("offsetHeight") + "px"),
+ width: (boundingBox.get("offsetWidth") + "px") });
+
+ }
+
+ });
+
+ }
+
+
+ menu.after("visibleChange", function (event) {
+
+ var bVisible = event.newVal;
+
+ // Focus the first item when the menu is made visible
+ // to allow users to navigate the menu via the keyboard
+
+ if (bVisible) {
+
+ // Need to set focus via a timer for Webkit and Opera
+ Y.Lang.later(0, contentBox.focusManager, contentBox.focusManager.focus);
+
+ }
+
+ boundingBox.set("aria-hidden", (!bVisible));
+
+ });
+
+
+ // Hide the menu when one of menu items is clicked.
+
+ boundingBox.delegate("click", function (event) {
+
+ alert("You clicked " + this.one("input").get("value"));
+
+ contentBox.focusManager.blur();
+ menu.hide();
+
+ }, "li");
+
+
+ // Focus each menuitem as the user moves the mouse over
+ // the menu.
+
+ boundingBox.delegate("mouseenter", function (event) {
+
+ var focusManager = contentBox.focusManager;
+
+ if (focusManager.get("focused")) {
+ focusManager.focus(this.one("input"));
+ }
+
+ }, "li");
+
+
+ // Hide the menu if the user clicks outside of it or if the
+ // user doesn't click on the button
+
+ boundingBox.get("ownerDocument").on("mousedown", function (event) {
+
+ var oTarget = event.target;
+
+ if (!oTarget.compareTo(menuButton) &&
+ !menuButton.contains(oTarget) &&
+ !oTarget.compareTo(boundingBox) &&
+ !boundingBox.contains(oTarget)) {
+
+ menu.hide();
+
+ }
+
+ });
+
+ };
+
+
+ menuButton.addClass("yui3-menubutton");
+
+
+ // Hide the list until it is transformed into a menu
+
+ Y.one("#menu-1").setStyle("display", "none");
+
+
+ // Remove the "yui3-menubutton-loading" class from the parent container
+ // now that the necessary YUI dependencies are loaded and the
+ // menu button has been skinned.
+
+ menuButton.ancestor(".yui3-menubutton-loading").removeClass("yui3-menubutton-loading");
+
+
+ // Apply the ARIA roles, states and properties to the anchor.
+
+ menuButton.setAttrs({
+ role: "button",
+ "aria-haspopup": true
+ });
+
+
+ // Remove the "href" attribute from the anchor element to
+ // prevent JAWS and NVDA from reading the value of the "href"
+ // attribute when the anchor is focused.
+
+ if ((Y.UA.gecko || Y.UA.ie) && navigator.userAgent.indexOf("Windows") > -1) {
+
+ menuButton.removeAttribute("href");
+
+ // Since the anchor's "href" attribute has been removed, the
+ // element will not fire the click event in Firefox when the
+ // user presses the enter key. To fix this, dispatch the
+ // "click" event to the anchor when the user presses the
+ // enter key.
+
+ Y.on("key", function (event) {
+
+ menuButton.simulate("click");
+
+ }, menuButton, "down:13");
+
+ }
+
+
+ // Set the "tabIndex" attribute of the anchor element to 0 to
+ // place it in the browser's default tab flow. This is
+ // necessary since 1) anchor elements are not in the default
+ // tab flow in Opera and 2) removing the "href" attribute
+ // prevents the anchor from firing its "click" event
+ // in Firefox.
+
+ menuButton.set("tabIndex", 0);
+
+ // Since there is some intermediary markup (<span>s) between the anchor element with the role
+ // of "button" applied and the text label for the anchor - we need to use the
+ // "aria-labelledby" attribute to ensure that screen readers announce the text label for the
+ // button.
+
+ var menuLabel = menuButton.one("span span"),
+ menuLabelID = Y.stamp(menuLabel);
+
+ menuLabel.set("id", menuLabelID);
+ menuButton.set("aria-labelledby", menuLabelID);
+
+ var showMenu = function (event) {
+
+ // For performance: Defer the creation of the menu until
+ // the first time the button is clicked.
+
+ if (!menu) {
+ initMenu();
+ }
+
+
+ if (!menu.get("visible")) {
+
+ menu.set("align", {
+ node: menuButton,
+ points: [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]
+ });
+
+ menu.show();
+
+ }
+
+ // Prevent the anchor element from being focused
+ // when the users mouses down on it.
+ event.preventDefault();
+
+ };
+
+
+ // Bind both a "mousedown" and "click" event listener to
+ // ensure the button's menu can be invoked using both the
+ // mouse and the keyboard.
+
+ menuButton.on("mousedown", showMenu);
+ menuButton.on("click", showMenu);
+
+});
+
+