YUI 3.x Home -

YUI Library Examples: Focus Manager Node Plugin: Accessible TabView

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

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.

  1. <h3 id="tabview-heading">Today's News</h3>
  2. <div id="tabview-1">
  3. <ul>
  4. <li class="yui-tab yui-tab-selected"><a href="#top-stories"><em>Top Stories</em></a></li>
  5. <li class="yui-tab"><a href="#world-news"><em>World</em></a></li>
  6. <li class="yui-tab"><a href="#entertainment-news"><em>Entertainment</em></a></li>
  7. <li class="yui-tab"><a href="#sports-news"><em>Sports</em></a></li>
  8. <li class="yui-tab"><a href="#technology-news"><em>Technology</em></a></li>
  9. </ul>
  10. <div>
  11. <div class="yui-tabpanel yui-tabpanel-selected" id="top-stories">
  12. <!-- Tab Panel Content Here -->
  13. </div>
  14. <div class="yui-tabpanel" id="world-news">
  15. <!-- Tab Panel Content Here -->
  16. </div>
  17. <div class="yui-tabpanel" id="entertainment-news">
  18. <!-- Tab Panel Content Here -->
  19. </div>
  20. <div class="yui-tabpanel" id="sports-news">
  21. <!-- Tab Panel Content Here -->
  22. </div>
  23. <div class="yui-tabpanel" id="technology-news">
  24. <!-- Tab Panel Content Here -->
  25. </div>
  26. </div>
  27. </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:

  1. function getFeed($sFeed) {
  2.  
  3. $params = array(
  4. "q" => ('select title,link from rss where url="http://rss.news.yahoo.com/rss/$sFeed"'),
  5. "format" => "json"
  6. );
  7.  
  8. $encoded_params = array();
  9.  
  10. foreach ($params as $k => $v) {
  11. $encoded_params[] = urlencode($k)."=".urlencode($v);
  12. }
  13.  
  14. $url = "http://query.yahooapis.com/v1/public/yql?".implode("&", $encoded_params);
  15.  
  16. $ch = curl_init();
  17. curl_setopt($ch, CURLOPT_URL, $url);
  18. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  19. $rsp = curl_exec($ch);
  20. curl_close($ch);
  21.  
  22. if ($rsp !== false) {
  23.  
  24. $rsp_obj = json_decode($rsp, true);
  25.  
  26. $results = $rsp_obj["query"]["results"]["item"];
  27.  
  28. $list = ""; // HTML output
  29.  
  30. $nResults = count($results);
  31.  
  32. if ($nResults > 10) {
  33. $nResults = 9;
  34. }
  35.  
  36. for ($i = 0; $i<= $nResults; $i++) {
  37.  
  38. $result = $results[$i];
  39.  
  40. $list.= <<< END_OF_HTML
  41.   <li>
  42.   <a href="${result["link"]}"><q>${result["title"]}</q></a>
  43.   </li>
  44. END_OF_HTML;
  45.  
  46. }
  47.  
  48. return ("<ul>" . $list . "</ul>");
  49.  
  50. }
  51.  
  52. }
  53.  
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.

  1. YUI({
  2.  
  3. base: "../../build/",
  4. modules: {
  5. "tabviewcss": {
  6. type: "css",
  7. fullpath: "assets/tabview.css"
  8. },
  9. "tabviewjs": {
  10. type: "js",
  11. fullpath: "assets/tabview.js",
  12. requires: ["node-focusmanager", "tabviewcss"]
  13. }
  14.  
  15. },
  16. timeout: 10000
  17.  
  18. }).use("tabviewjs", function(Y, result) {
  19.  
  20. // The callback supplied to use() will be executed regardless of
  21. // whether the operation was successful or not. The second parameter
  22. // is a result object that has the status of the operation. We can
  23. // use this to try to recover from failures or timeouts.
  24.  
  25. if (!result.success) {
  26.  
  27. Y.log("Load failure: " + result.msg, "warn", "Example");
  28.  
  29. // Show the tabview HTML if the loader failed that way the
  30. // original unskinned tabview will be visible so that the
  31. // user can interact with it either way.
  32.  
  33. document.documentElement.className = "";
  34.  
  35. }
  36.  
  37. });
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.

  1. YUI().use("*", function (Y) {
  2.  
  3. var tabView = Y.one("#tabview-1"),
  4. tabList = tabView.one("ul"),
  5. tabHeading = Y.one("#tabview-heading"),
  6. sInstructionalText = tabHeading.get("innerHTML");
  7. selectedTabAnchor = tabView.one(".yui-tab-selected>a"),
  8. bGeckoIEWin = ((Y.UA.gecko || Y.UA.ie) && navigator.userAgent.indexOf("Windows") > -1),
  9. panelMap = {};
  10.  
  11.  
  12. tabView.addClass("yui-tabview");
  13.  
  14. // Remove the "yui-loading" class from the documentElement
  15. // now that the necessary YUI dependencies are loaded and the
  16. // tabview has been skinned.
  17.  
  18. tabView.get("ownerDocument").get("documentElement").removeClass("yui-loading");
  19.  
  20. // Apply the ARIA roles, states and properties.
  21.  
  22. // Add some instructional text to the heading that will be read by
  23. // the screen reader when the first tab in the tabview is focused.
  24.  
  25. tabHeading.set("innerHTML", (sInstructionalText + " <em>Press the enter key to load the content of each tab.</em>"));
  26.  
  27. tabList.setAttrs({
  28. "aria-labelledby": "tabview-heading",
  29. role: "tablist"
  30. });
  31.  
  32. tabView.one("div").set("role", "presentation");
  33.  
  34.  
  35. tabView.plug(Y.Plugin.NodeFocusManager, {
  36. descendants: ".yui-tab>a",
  37. keys: { next: "down:39", // Right arrow
  38. previous: "down:37" }, // Left arrow
  39. focusClass: {
  40. className: "yui-tab-focus",
  41. fn: function (node) {
  42. return node.get("parentNode");
  43. }
  44. },
  45. circular: true
  46. });
  47.  
  48.  
  49. // If the list of tabs loses focus, set the activeDescendant
  50. // attribute to the currently selected tab.
  51.  
  52. tabView.focusManager.after("focusedChange", function (event) {
  53.  
  54. if (!event.newVal) { // The list of tabs has lost focus
  55. this.set("activeDescendant", selectedTabAnchor);
  56. }
  57.  
  58. });
  59.  
  60.  
  61. tabView.all(".yui-tab>a").each(function (anchor) {
  62.  
  63. var sHref = anchor.getAttribute("href", 2),
  64. sPanelID = sHref.substring(1, sHref.length),
  65. panel;
  66.  
  67. // Apply the ARIA roles, states and properties to each tab
  68.  
  69. anchor.set("role", "tab");
  70. anchor.get("parentNode").set("role", "presentation");
  71.  
  72.  
  73. // Remove the "href" attribute from the anchor element to
  74. // prevent JAWS and NVDA from reading the value of the "href"
  75. // attribute when the anchor is focused
  76.  
  77. if (bGeckoIEWin) {
  78. anchor.removeAttribute("href");
  79. }
  80.  
  81. // Cache a reference to id of the tab's corresponding panel
  82. // element so that it can be made visible when the tab
  83. // is clicked.
  84. panelMap[anchor.get("id")] = sPanelID;
  85.  
  86.  
  87. // Apply the ARIA roles, states and properties to each panel
  88.  
  89. panel = Y.one(("#" + sPanelID));
  90.  
  91. panel.setAttrs({
  92. role: "tabpanel",
  93. "aria-labelledby": anchor.get("id")
  94. });
  95.  
  96. });
  97.  
  98.  
  99. // Use the "delegate" custom event to listen for the "click" event
  100. // of each tab's <A> element.
  101.  
  102. tabView.delegate("click", function (event) {
  103.  
  104. var selectedPanel,
  105. sID = this.get("id");
  106.  
  107. // Deselect the currently selected tab and hide its
  108. // corresponding panel.
  109.  
  110. if (selectedTabAnchor) {
  111. selectedTabAnchor.get("parentNode").removeClass("yui-tab-selected");
  112. Y.one(("#" + panelMap[selectedTabAnchor.get("id")])).removeClass("yui-tabpanel-selected");
  113. }
  114.  
  115. selectedTabAnchor = this;
  116. selectedTabAnchor.get("parentNode").addClass("yui-tab-selected");
  117.  
  118. selectedPanel = Y.one(("#" + panelMap[sID]));
  119. selectedPanel.addClass("yui-tabpanel-selected");
  120.  
  121. creatingPaging(selectedPanel);
  122.  
  123. // Prevent the browser from following the URL specified by the
  124. // anchor's "href" attribute when clicked.
  125.  
  126. event.preventDefault();
  127.  
  128. }, ".yui-tab>a");
  129.  
  130.  
  131. // Since the anchor's "href" attribute has been removed, the
  132. // element will not fire the click event in Firefox when the
  133. // user presses the enter key. To fix this, dispatch the
  134. // "click" event to the anchor when the user presses the
  135. // enter key.
  136.  
  137. if (bGeckoIEWin) {
  138.  
  139. tabView.delegate("keydown", function (event) {
  140.  
  141. if (event.charCode === 13) {
  142. this.simulate("click");
  143. }
  144.  
  145. }, ">ul>li>a");
  146.  
  147. }
  148.  
  149. });
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.

  1. var creatingPaging = function (panel) {
  2.  
  3. var listitem,
  4. sHTML,
  5. paging;
  6.  
  7. if (!panel.one(".paging")) {
  8.  
  9. listitem = selectedTabAnchor.get("parentNode");
  10. sHTML = "";
  11.  
  12. if (listitem.previous()) {
  13. sHTML += '<button type="button" class="yui-tabview-prevbtn">Previous Tab Panel</button>';
  14. }
  15.  
  16. if (listitem.next()) {
  17. sHTML += '<button type="button" class="yui-tabview-nextbtn">Next Tab Panel</button>';
  18. }
  19.  
  20. paging = Y.Node.create('<div class="paging">' + sHTML + '</div>');
  21.  
  22. panel.appendChild(paging);
  23.  
  24. }
  25.  
  26. };
  27.  
  28. creatingPaging(Y.one(".yui-tabpanel-selected"));
  29.  
  30.  
  31. tabView.delegate("click", function (event) {
  32.  
  33. var node = selectedTabAnchor.get("parentNode").previous().one("a");
  34.  
  35. tabView.focusManager.focus(node);
  36. node.simulate("click");
  37.  
  38. }, ".yui-tabview-prevbtn");
  39.  
  40.  
  41. tabView.delegate("click", function (event) {
  42.  
  43. var node = selectedTabAnchor.get("parentNode").next().one("a");
  44.  
  45. tabView.focusManager.focus(node);
  46. node.simulate("click");
  47.  
  48. }, ".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");

Copyright © 2009 Yahoo! Inc. All rights reserved.

Privacy Policy - Terms of Service - Copyright Policy - Job Openings