Bug 1286283 - support ARIA for devtools inspector tabs r?bgrinstead draft
authorRicky Chien <ricky060709@gmail.com>
Wed, 20 Jul 2016 14:06:44 +0800
changeset 395351 86b1ac8e681660c76997ff830a5e630eae840a5b
parent 394782 4a18b5cacb1b21a3e8b4b1dada6b2dd3dba51cb1
child 526979 8d3978d52153efd8454d0e7825af5e7bf8aafed6
push id24753
push userbmo:rchien@mozilla.com
push dateTue, 02 Aug 2016 02:17:19 +0000
reviewersbgrinstead
bugs1286283
milestone50.0a1
Bug 1286283 - support ARIA for devtools inspector tabs r?bgrinstead MozReview-Commit-ID: BOOWwPqmTbT
devtools/client/shared/components/tabs/tabs.js
devtools/client/shared/components/test/mochitest/chrome.ini
devtools/client/shared/components/test/mochitest/test_tabs_accessibility.html
--- a/devtools/client/shared/components/tabs/tabs.js
+++ b/devtools/client/shared/components/tabs/tabs.js
@@ -21,17 +21,17 @@ define(function (require, exports, modul
    *
    * <div class='tabs'>
    *  <nav class='tabs-navigation'>
    *    <ul class='tabs-menu'>
    *      <li class='tabs-menu-item is-active'>Tab #1</li>
    *      <li class='tabs-menu-item'>Tab #2</li>
    *    </ul>
    *  </nav>
-   *  <div class='tab-panel'>
+   *  <div class='panels'>
    *    The content of active panel here
    *  </div>
    * <div>
    */
   let Tabs = React.createClass({
     displayName: "Tabs",
 
     propTypes: {
@@ -179,46 +179,54 @@ define(function (require, exports, modul
         .map(tab => {
           return typeof tab === "function" ? tab() : tab;
         }).filter(tab => {
           return tab;
         }).map((tab, index) => {
           let ref = ("tab-menu-" + index);
           let title = tab.props.title;
           let tabClassName = tab.props.className;
+          let isTabSelected = this.state.tabActive === index;
 
           let classes = [
             "tabs-menu-item",
             tabClassName,
-            this.state.tabActive === index ? "is-active" : ""
+            isTabSelected ? "is-active" : ""
           ].join(" ");
 
           // Set tabindex to -1 (except the selected tab) so, it's focusable,
           // but not reachable via sequential tab-key navigation.
           // Changing selected tab (and so, moving focus) is done through
           // left and right arrow keys.
           // See also `onKeyDown()` event handler.
           return (
             DOM.li({
               ref: ref,
               key: index,
-              className: classes},
+              id: "tab-" + index,
+              className: classes,
+              role: "presentation",
+            },
               DOM.a({
                 href: "#",
                 tabIndex: this.state.tabActive === index ? 0 : -1,
-                onClick: this.onClickTab.bind(this, index)},
+                "aria-controls": "panel-" + index,
+                "aria-selected": isTabSelected,
+                role: "tab",
+                onClick: this.onClickTab.bind(this, index),
+              },
                 title
               )
             )
           );
         });
 
       return (
         DOM.nav({className: "tabs-navigation"},
-          DOM.ul({className: "tabs-menu"},
+          DOM.ul({className: "tabs-menu", role: "tablist"},
             tabs
           )
         )
       );
     },
 
     renderPanels: function () {
       if (!this.props.children) {
@@ -246,18 +254,22 @@ define(function (require, exports, modul
             visibility: selected ? "visible" : "hidden",
             height: selected ? "100%" : "0",
             width: selected ? "100%" : "0",
           };
 
           return (
             DOM.div({
               key: index,
+              id: "panel-" + index,
               style: style,
-              className: "tab-panel-box"},
+              className: "tab-panel-box",
+              role: "tabpanel",
+              "aria-labelledby": "tab-" + index,
+            },
               (selected || this.state.created[index]) ? tab : null
             )
           );
         });
 
       return (
         DOM.div({className: "panels"},
           panels
--- a/devtools/client/shared/components/test/mochitest/chrome.ini
+++ b/devtools/client/shared/components/test/mochitest/chrome.ini
@@ -22,16 +22,17 @@ support-files =
 [test_reps_object-with-url.html]
 [test_reps_regexp.html]
 [test_reps_string.html]
 [test_reps_stylesheet.html]
 [test_reps_text-node.html]
 [test_reps_undefined.html]
 [test_reps_window.html]
 [test_sidebar_toggle.html]
+[test_tabs_accessibility.html]
 [test_tree_01.html]
 [test_tree_02.html]
 [test_tree_03.html]
 [test_tree_04.html]
 [test_tree_05.html]
 [test_tree_06.html]
 [test_tree_07.html]
 [test_tree_08.html]
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tabs_accessibility.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Test tabs accessibility.
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Tabs component accessibility test</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+  try {
+    const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+    const React = browserRequire("devtools/client/shared/vendor/react");
+    const { Simulate } = React.addons.TestUtils;
+    const InspectorTabPanel = React.createFactory(browserRequire("devtools/client/inspector/components/inspector-tab-panel"));
+    const Tabbar = React.createFactory(browserRequire("devtools/client/shared/components/tabs/tabbar"));
+    const tabbar = Tabbar();
+    const tabbarReact = ReactDOM.render(tabbar, window.document.body);
+    const tabbarEl = ReactDOM.findDOMNode(tabbarReact);
+
+    // Setup for InspectorTabPanel
+    const tabpanels = document.createElement("div");
+    tabpanels.id = "tabpanels";
+    document.body.appendChild(tabpanels);
+
+    yield addTabWithPanel(0);
+    yield addTabWithPanel(1);
+
+    const tabAnchors = tabbarEl.querySelectorAll("li.tabs-menu-item a");
+
+    is(tabAnchors[0].parentElement.getAttribute("role"), "presentation", "li role is set correctly");
+    is(tabAnchors[0].getAttribute("role"), "tab", "Anchor role is set correctly");
+    is(tabAnchors[0].getAttribute("aria-selected"), "true", "Anchor aria-selected is set correctly by default");
+    is(tabAnchors[0].getAttribute("aria-controls"), "panel-0", "Anchor aria-controls is set correctly");
+    is(tabAnchors[1].parentElement.getAttribute("role"), "presentation", "li role is set correctly");
+    is(tabAnchors[1].getAttribute("role"), "tab", "Anchor role is set correctly");
+    is(tabAnchors[1].getAttribute("aria-selected"), "false", "Anchor aria-selected is set correctly by default");
+    is(tabAnchors[1].getAttribute("aria-controls"), "panel-1", "Anchor aria-controls is set correctly");
+
+    yield setState(tabbarReact, Object.assign({}, tabbarReact.state, {
+      activeTab: 1
+    }));
+
+    is(tabAnchors[0].getAttribute("aria-selected"), "false", "Anchor aria-selected is reset correctly");
+    is(tabAnchors[1].getAttribute("aria-selected"), "true", "Anchor aria-selected is reset correctly");
+
+    function addTabWithPanel(tabId) {
+      // Setup for InspectorTabPanel
+      let panel = document.createElement("div");
+      panel.id = `sidebar-panel-${tabId}`;
+      document.body.appendChild(panel);
+
+      return setState(tabbarReact, Object.assign({}, tabbarReact.state, {
+        tabs: tabbarReact.state.tabs.concat({id: `${tabId}`, title: `tab-${tabId}`, panel: InspectorTabPanel}),
+      }));
+    }
+  } catch(e) {
+    ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+  } finally {
+    SimpleTest.finish();
+  }
+});
+</script>
+</pre>
+</body>
+</html>