Focus Manager Node Plugin: Accessible TabView
+ ++This example illustrates how to create an accessible tabview widget using the +Focus Manager Node Plugin, +Event's delegation support, and +Node's support for the +WAI-ARIA Roles and States. +
Today's News
+-
+
- Top Stories +
- World +
- Entertainment +
- Sports +
- Technology +
Top Stories
+-
+
Senate Finance panel rejects govt insurance option + (AP) +
+ -
+
NYC terror suspect pleads not guilty, kept in jail + (AP) +
+ -
+
Iran put nuclear site near base in case of attack + (AP) +
+ -
+
Taliban roadway attacks spread fear in Afghanistan + (AP) +
+ -
+
Sharpton, Gingrich launch school tour in Philly + (AP) +
+ -
+
Skull piece thought to be Hitler's is from woman + (AP) +
+ -
+
Nero's rotating banquet hall unveiled in Rome + (AP) +
+ -
+
TLC's 'Jon & Kate' is soon to be 'Kate Plus Eight' + (AP) +
+ -
+
Cops: Skater faces charges in right-of-way dispute + (AP) +
+ -
+
AL Central race gets wild, Twins beat Tigers in 10 + (AP) +
+
World News
+-
+
Iran put nuclear site near base in case of attack + (AP) +
+ -
+
Taliban roadway attacks spread fear in Afghanistan + (AP) +
+ -
+
Polanski asks Swiss court to free him from custody + (AP) +
+ -
+
Flood kills 246 in Philippines; survivors seek aid + (AP) +
+ -
+
Tsunami hits American Samoa + (AP) +
+ -
+
Iran Sanctions: Would Blocking Gas Imports Hurt Tehran? + (Time.com) +
+ -
+
BA launches US business-class route + (AFP) +
+ -
+
Israeli envoys to US for talks on peacemaking + (AP) +
+ -
+
Arias warns Honduran elections won't be recognized + (AP) +
+ -
+
Group: Guinea protest death toll climbs to 157 + (AP) +
+
Entertainment News
+-
+
Polanski asks Swiss court to free him from custody + (AP) +
+ -
+
DJ AM's death ruled accidental drug overdose + (AP) +
+ -
+
Conductor Levine withdrawing from upcoming shows + (AP) +
+ -
+
50 years later, 'Twilight Zone' bridges time + (AP) +
+ -
+
TLC's 'Jon & Kate' is soon to be 'Kate Plus Eight' + (AP) +
+ -
+
Barbra Streisand gets nostalgic on latest CD + (AP) +
+ -
+
Appeals court dismisses Dan Rather's suit vs. CBS + (Reuters) +
+ -
+
Stockholm marks film festival with giant ice screen + (Reuters) +
+ -
+
Video company asks for $6.3M in Lennon film case + (AP) +
+ -
+
Bloomberg seen as top BusinessWeek bidder: source + (Reuters) +
+
Sports News
+-
+
AL Central race gets wild, Twins beat Tigers in 10 + (AP) +
+ -
+
With Pennington out, Dolphins get Thigpen + (AP) +
+ -
+
Johnson giving thumbs-up to mom after surgery + (AP) +
+ -
+
Rio puts President Lula at heart of Olympic bid + (AP) +
+ -
+
Crosby practices, expects to play Friday + (AP) +
+ -
+
Palmer to receive Congressional Gold Medal + (AP) +
+ -
+
Dolphins acquire Thigpen from Chiefs + (Reuters) +
+ -
+
Hall of Famer Mike Schmidt on 200 Ks + (AP) +
+ -
+
NBA Rockets open pre-season without Yao, McGrady + (AFP) +
+ -
+
Wyoming coach laid up with kidney stone + (AP) +
+
Technology News
+-
+
YouTube says Warner Music videos back in months + (AP) +
+ -
+
T-Mobile to sell Motorola Android phone Oct. 19 + (AP) +
+ -
+
GM's trial program selling cars on eBay set to end + (AP) +
+ -
+
Among the new features in CNN iPhone app: a price + (AP) +
+ -
+
Microsoft to release free antivirus PC software + (AP) +
+ -
+
Voter group challenges Diebold voting machine sale + (AP) +
+ -
+
Warner music videos returning to YouTube + (AFP) +
+ -
+
Garmin Rolls Out Navigation Smartphone on AT&T + (NewsFactor) +
+ -
+
Vodafone Egypt sees 160,000 broadband users in 2009 + (Reuters) +
+ -
+
Warner, YouTube confirm music video deal + (Reuters) +
+
Setting Up the HTML
+
+The tabs in the tabview widget will be represented by a list of
+<a> elements whose href attribute is set to
+the id of an <div> element that contains its content.
+Therefore, without JavaScript and CSS, the tabs function as in-page links.
+
<h3 id="tabview-heading">Today's News</h3> <div id="tabview-1"> <ul> <li class="yui-tab yui-tab-selected"><a href="#top-stories"><em>Top Stories</em></a></li> <li class="yui-tab"><a href="#world-news"><em>World</em></a></li> <li class="yui-tab"><a href="#entertainment-news"><em>Entertainment</em></a></li> <li class="yui-tab"><a href="#sports-news"><em>Sports</em></a></li> <li class="yui-tab"><a href="#technology-news"><em>Technology</em></a></li> </ul> <div> <div class="yui-tabpanel yui-tabpanel-selected" id="top-stories"> <!-- Tab Panel Content Here --> </div> <div class="yui-tabpanel" id="world-news"> <!-- Tab Panel Content Here --> </div> <div class="yui-tabpanel" id="entertainment-news"> <!-- Tab Panel Content Here --> </div> <div class="yui-tabpanel" id="sports-news"> <!-- Tab Panel Content Here --> </div> <div class="yui-tabpanel" id="technology-news"> <!-- Tab Panel Content Here --> </div> </div> </div>
<h3 id="tabview-heading">Today's News</h3> +<div id="tabview-1"> + <ul> + <li class="yui-tab yui-tab-selected"><a href="#top-stories"><em>Top Stories</em></a></li> + <li class="yui-tab"><a href="#world-news"><em>World</em></a></li> + <li class="yui-tab"><a href="#entertainment-news"><em>Entertainment</em></a></li> + <li class="yui-tab"><a href="#sports-news"><em>Sports</em></a></li> + <li class="yui-tab"><a href="#technology-news"><em>Technology</em></a></li> + </ul> + <div> + <div class="yui-tabpanel yui-tabpanel-selected" id="top-stories"> + <!-- Tab Panel Content Here --> + </div> + <div class="yui-tabpanel" id="world-news"> + <!-- Tab Panel Content Here --> + </div> + <div class="yui-tabpanel" id="entertainment-news"> + <!-- Tab Panel Content Here --> + </div> + <div class="yui-tabpanel" id="sports-news"> + <!-- Tab Panel Content Here --> + </div> + <div class="yui-tabpanel" id="technology-news"> + <!-- Tab Panel Content Here --> + </div> + </div> +</div>
+For this example the content of each tab panel is created on the server using +the YQL API to fetch the title and +URL for news stories made available from the various +Yahoo! News RSS feeds. +Here's the PHP: +
+ +
function getFeed($sFeed) { $params = array( "q" => ('select title,link from rss where url="http://rss.news.yahoo.com/rss/$sFeed"'), "format" => "json" ); $encoded_params = array(); foreach ($params as $k => $v) { $encoded_params[] = urlencode($k)."=".urlencode($v); } $url = "http://query.yahooapis.com/v1/public/yql?".implode("&", $encoded_params); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $rsp = curl_exec($ch); curl_close($ch); if ($rsp !== false) { $rsp_obj = json_decode($rsp, true); $results = $rsp_obj["query"]["results"]["item"]; $list = ""; // HTML output $nResults = count($results); if ($nResults > 10) { $nResults = 9; } for ($i = 0; $i<= $nResults; $i++) { $result = $results[$i]; $list.= <<< END_OF_HTML <li> <a href="${result["link"]}"><q>${result["title"]}</q></a> </li> END_OF_HTML; } return ("<ul>" . $list . "</ul>"); } }
function getFeed($sFeed) { + + $params = array( + "q" => ('select title,link from rss where url="http://rss.news.yahoo.com/rss/$sFeed"'), + "format" => "json" + ); + + $encoded_params = array(); + + foreach ($params as $k => $v) { + $encoded_params[] = urlencode($k)."=".urlencode($v); + } + + $url = "http://query.yahooapis.com/v1/public/yql?".implode("&", $encoded_params); + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + $rsp = curl_exec($ch); + curl_close($ch); + + if ($rsp !== false) { + + $rsp_obj = json_decode($rsp, true); + + $results = $rsp_obj["query"]["results"]["item"]; + + $list = ""; // HTML output + + $nResults = count($results); + + if ($nResults > 10) { + $nResults = 9; + } + + for ($i = 0; $i<= $nResults; $i++) { + + $result = $results[$i]; + + $list.= <<< END_OF_HTML + <li> + <a href="${result["link"]}"><q>${result["title"]}</q></a> + </li> +END_OF_HTML; + + } + + return ("<ul>" . $list . "</ul>"); + + } + +} +
Progressive Enhancement
+
+The markup above will be transformed using both CSS and JavaScript. To account
+for the scenario where the user has CSS enabled in their browser but JavaScript
+is disabled, the CSS used to style the tabview will be loaded via JavaScript
+using the YUI instance's built-in Loader.
+Additionally, a small block of JavaScript will be placed in the
+<head> used to temporarily hide the markup
+while the JavaScript and CSS are in the process of loading to prevent the user
+from seeing a flash unstyled content.
+
YUI({ base: "../../build/", modules: { "tabviewcss": { type: "css", fullpath: "assets/tabview.css" }, "tabviewjs": { type: "js", fullpath: "assets/tabview.js", requires: ["node-focusmanager", "tabviewcss"] } }, timeout: 10000 }).use("tabviewjs", function(Y, result) { // The callback supplied to use() will be executed regardless of // whether the operation was successful or not. The second parameter // is a result object that has the status of the operation. We can // use this to try to recover from failures or timeouts. if (!result.success) { Y.log("Load failure: " + result.msg, "warn", "Example"); // Show the tabview HTML if the loader failed that way the // original unskinned tabview will be visible so that the // user can interact with it either way. document.documentElement.className = ""; } });
YUI({ + + base: "../../build/", + modules: { + "tabviewcss": { + type: "css", + fullpath: "assets/tabview.css" + }, + "tabviewjs": { + type: "js", + fullpath: "assets/tabview.js", + requires: ["node-focusmanager", "tabviewcss"] + } + + }, + timeout: 10000 + +}).use("tabviewjs", function(Y, result) { + + // The callback supplied to use() will be executed regardless of + // whether the operation was successful or not. The second parameter + // is a result object that has the status of the operation. We can + // use this to try to recover from failures or timeouts. + + if (!result.success) { + + Y.log("Load failure: " + result.msg, "warn", "Example"); + + // Show the tabview HTML if the loader failed that way the + // original unskinned tabview will be visible so that the + // user can interact with it either way. + + document.documentElement.className = ""; + + } + +});
ARIA Support
++Through the use of CSS and JavaScript the HTML for the tabview can be +transformed into something that looks and behaves like a desktop tab control, +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 tab control. +
+ + +Keyboard Functionality
+
+The keyboard functionality for the tabview widget will be provided by the
+Focus Manager Node Plugin. The Focus Manager's
+descendants
+attribute is set to a value of ".yui-tab>a", so that only one tab in the tabview
+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 tabview. Once
+the tabview has focus, the user can move focus among each tab using the left
+and right arrows keys, as defined by the value of the
+keys
+attribute. Lastly, the
+focusClass
+attribute is used to apply a class of yui-tab-focus to the parent
+<li> of each <a> when it is focused,
+making it easy to style the tab's focused state in each of the
+A-Grade browsers.
+
+
YUI().use("*", function (Y) { var tabView = Y.one("#tabview-1"), tabList = tabView.one("ul"), tabHeading = Y.one("#tabview-heading"), sInstructionalText = tabHeading.get("innerHTML"); selectedTabAnchor = tabView.one(".yui-tab-selected>a"), bGeckoIEWin = ((Y.UA.gecko || Y.UA.ie) && navigator.userAgent.indexOf("Windows") > -1), panelMap = {}; tabView.addClass("yui-tabview"); // Remove the "yui-loading" class from the documentElement // now that the necessary YUI dependencies are loaded and the // tabview has been skinned. tabView.get("ownerDocument").get("documentElement").removeClass("yui-loading"); // Apply the ARIA roles, states and properties. // Add some instructional text to the heading that will be read by // the screen reader when the first tab in the tabview is focused. tabHeading.set("innerHTML", (sInstructionalText + " <em>Press the enter key to load the content of each tab.</em>")); tabList.setAttrs({ "aria-labelledby": "tabview-heading", role: "tablist" }); tabView.one("div").set("role", "presentation"); tabView.plug(Y.Plugin.NodeFocusManager, { descendants: ".yui-tab>a", keys: { next: "down:39", // Right arrow previous: "down:37" }, // Left arrow focusClass: { className: "yui-tab-focus", fn: function (node) { return node.get("parentNode"); } }, circular: true }); // If the list of tabs loses focus, set the activeDescendant // attribute to the currently selected tab. tabView.focusManager.after("focusedChange", function (event) { if (!event.newVal) { // The list of tabs has lost focus this.set("activeDescendant", selectedTabAnchor); } }); tabView.all(".yui-tab>a").each(function (anchor) { var sHref = anchor.getAttribute("href", 2), sPanelID = sHref.substring(1, sHref.length), panel; // Apply the ARIA roles, states and properties to each tab anchor.set("role", "tab"); anchor.get("parentNode").set("role", "presentation"); // 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 (bGeckoIEWin) { anchor.removeAttribute("href"); } // Cache a reference to id of the tab's corresponding panel // element so that it can be made visible when the tab // is clicked. panelMap[anchor.get("id")] = sPanelID; // Apply the ARIA roles, states and properties to each panel panel = Y.one(("#" + sPanelID)); panel.setAttrs({ role: "tabpanel", "aria-labelledby": anchor.get("id") }); }); // Use the "delegate" custom event to listen for the "click" event // of each tab's <A> element. tabView.delegate("click", function (event) { var selectedPanel, sID = this.get("id"); // Deselect the currently selected tab and hide its // corresponding panel. if (selectedTabAnchor) { selectedTabAnchor.get("parentNode").removeClass("yui-tab-selected"); Y.one(("#" + panelMap[selectedTabAnchor.get("id")])).removeClass("yui-tabpanel-selected"); } selectedTabAnchor = this; selectedTabAnchor.get("parentNode").addClass("yui-tab-selected"); selectedPanel = Y.one(("#" + panelMap[sID])); selectedPanel.addClass("yui-tabpanel-selected"); creatingPaging(selectedPanel); // Prevent the browser from following the URL specified by the // anchor's "href" attribute when clicked. event.preventDefault(); }, ".yui-tab>a"); // 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. if (bGeckoIEWin) { tabView.delegate("keydown", function (event) { if (event.charCode === 13) { this.simulate("click"); } }, ">ul>li>a"); } });
YUI().use("*", function (Y) { + + var tabView = Y.one("#tabview-1"), + tabList = tabView.one("ul"), + tabHeading = Y.one("#tabview-heading"), + sInstructionalText = tabHeading.get("innerHTML"); + selectedTabAnchor = tabView.one(".yui-tab-selected>a"), + bGeckoIEWin = ((Y.UA.gecko || Y.UA.ie) && navigator.userAgent.indexOf("Windows") > -1), + panelMap = {}; + + + tabView.addClass("yui-tabview"); + + // Remove the "yui-loading" class from the documentElement + // now that the necessary YUI dependencies are loaded and the + // tabview has been skinned. + + tabView.get("ownerDocument").get("documentElement").removeClass("yui-loading"); + + // Apply the ARIA roles, states and properties. + + // Add some instructional text to the heading that will be read by + // the screen reader when the first tab in the tabview is focused. + + tabHeading.set("innerHTML", (sInstructionalText + " <em>Press the enter key to load the content of each tab.</em>")); + + tabList.setAttrs({ + "aria-labelledby": "tabview-heading", + role: "tablist" + }); + + tabView.one("div").set("role", "presentation"); + + + tabView.plug(Y.Plugin.NodeFocusManager, { + descendants: ".yui-tab>a", + keys: { next: "down:39", // Right arrow + previous: "down:37" }, // Left arrow + focusClass: { + className: "yui-tab-focus", + fn: function (node) { + return node.get("parentNode"); + } + }, + circular: true + }); + + + // If the list of tabs loses focus, set the activeDescendant + // attribute to the currently selected tab. + + tabView.focusManager.after("focusedChange", function (event) { + + if (!event.newVal) { // The list of tabs has lost focus + this.set("activeDescendant", selectedTabAnchor); + } + + }); + + + tabView.all(".yui-tab>a").each(function (anchor) { + + var sHref = anchor.getAttribute("href", 2), + sPanelID = sHref.substring(1, sHref.length), + panel; + + // Apply the ARIA roles, states and properties to each tab + + anchor.set("role", "tab"); + anchor.get("parentNode").set("role", "presentation"); + + + // 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 (bGeckoIEWin) { + anchor.removeAttribute("href"); + } + + // Cache a reference to id of the tab's corresponding panel + // element so that it can be made visible when the tab + // is clicked. + panelMap[anchor.get("id")] = sPanelID; + + + // Apply the ARIA roles, states and properties to each panel + + panel = Y.one(("#" + sPanelID)); + + panel.setAttrs({ + role: "tabpanel", + "aria-labelledby": anchor.get("id") + }); + + }); + + + // Use the "delegate" custom event to listen for the "click" event + // of each tab's <A> element. + + tabView.delegate("click", function (event) { + + var selectedPanel, + sID = this.get("id"); + + // Deselect the currently selected tab and hide its + // corresponding panel. + + if (selectedTabAnchor) { + selectedTabAnchor.get("parentNode").removeClass("yui-tab-selected"); + Y.one(("#" + panelMap[selectedTabAnchor.get("id")])).removeClass("yui-tabpanel-selected"); + } + + selectedTabAnchor = this; + selectedTabAnchor.get("parentNode").addClass("yui-tab-selected"); + + selectedPanel = Y.one(("#" + panelMap[sID])); + selectedPanel.addClass("yui-tabpanel-selected"); + + creatingPaging(selectedPanel); + + // Prevent the browser from following the URL specified by the + // anchor's "href" attribute when clicked. + + event.preventDefault(); + + }, ".yui-tab>a"); + + + // 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. + + if (bGeckoIEWin) { + + tabView.delegate("keydown", function (event) { + + if (event.charCode === 13) { + this.simulate("click"); + } + + }, ">ul>li>a"); + + } + +});
Accessibility Sugar
++One of the challenges faced by users of screen readers is knowing when you've +left the context of a given control. In the case of this tabview, if it +were adjacent to another ARIA-enabled widget, the user would know they've +left the tabview when the screen reader announces the role of the adjacent +widget. However, if the tabview is sitting alongside standard HTML content, it +would be really difficult for the user to know when they've left the context of +the active panel. +
++One solution to this problem is to add some additional navigation as the last +child of each tab panel that allows the user to move to the previous and next +panel in the tabview. This will not only help alert users of screen readers +that they've reached the end of the tab's panel, but allow all +keyboard users to move more quickly to the next/previous panel. Without this +additionally navigation, keyboard users would typically have to press +shift+tab to navigate back up to the list of tabs to move to the next/previous +tab. +
+
var creatingPaging = function (panel) { var listitem, sHTML, paging; if (!panel.one(".paging")) { listitem = selectedTabAnchor.get("parentNode"); sHTML = ""; if (listitem.previous()) { sHTML += '<button type="button" class="yui-tabview-prevbtn">Previous Tab Panel</button>'; } if (listitem.next()) { sHTML += '<button type="button" class="yui-tabview-nextbtn">Next Tab Panel</button>'; } paging = Y.Node.create('<div class="paging">' + sHTML + '</div>'); panel.appendChild(paging); } }; creatingPaging(Y.one(".yui-tabpanel-selected")); tabView.delegate("click", function (event) { var node = selectedTabAnchor.get("parentNode").previous().one("a"); tabView.focusManager.focus(node); node.simulate("click"); }, ".yui-tabview-prevbtn"); tabView.delegate("click", function (event) { var node = selectedTabAnchor.get("parentNode").next().one("a"); tabView.focusManager.focus(node); node.simulate("click"); }, ".yui-tabview-nextbtn");
var creatingPaging = function (panel) { + + var listitem, + sHTML, + paging; + + if (!panel.one(".paging")) { + + listitem = selectedTabAnchor.get("parentNode"); + sHTML = ""; + + if (listitem.previous()) { + sHTML += '<button type="button" class="yui-tabview-prevbtn">Previous Tab Panel</button>'; + } + + if (listitem.next()) { + sHTML += '<button type="button" class="yui-tabview-nextbtn">Next Tab Panel</button>'; + } + + paging = Y.Node.create('<div class="paging">' + sHTML + '</div>'); + + panel.appendChild(paging); + + } + +}; + +creatingPaging(Y.one(".yui-tabpanel-selected")); + + +tabView.delegate("click", function (event) { + + var node = selectedTabAnchor.get("parentNode").previous().one("a"); + + tabView.focusManager.focus(node); + node.simulate("click"); + +}, ".yui-tabview-prevbtn"); + + +tabView.delegate("click", function (event) { + + var node = selectedTabAnchor.get("parentNode").next().one("a"); + + tabView.focusManager.focus(node); + node.simulate("click"); + +}, ".yui-tabview-nextbtn");

