Bug 1297651 - Move toolsidebar.js into tabbar.js and make it more generic r?honza draft
authorMichael Ratcliffe <mratcliffe@mozilla.com>
Wed, 02 Nov 2016 16:16:54 +0000
changeset 433449 4650835e2030a778e8a557770ac0d8cb2e4496fc
parent 433311 3b80868f7a8fe0361918a814fbbbfb9308ae0c0a
child 535887 d7bb50f2fbf7c61adc3b90f5fe7ecd2aeba5bf6d
push id34576
push userbmo:mratcliffe@mozilla.com
push dateThu, 03 Nov 2016 17:55:43 +0000
reviewershonza
bugs1297651
milestone52.0a1
Bug 1297651 - Move toolsidebar.js into tabbar.js and make it more generic r?honza We were previously working at cross purposes as I thought Honza wanted me to use high order functions to compose and decorate the components. Having asked and gained clarification I understand that Honza wanted me to pass the extra methods in using props. Actually, it is quite tidy this way. MozReview-Commit-ID: 58qJ6C0Lsof
devtools/client/animationinspector/animation-controller.js
devtools/client/animationinspector/test/browser_animation_refresh_when_active.js
devtools/client/animationinspector/test/head.js
devtools/client/inspector/components/box-model.js
devtools/client/inspector/components/inspector-tab-panel.js
devtools/client/inspector/computed/computed.js
devtools/client/inspector/fonts/fonts.js
devtools/client/inspector/inspector.js
devtools/client/inspector/rules/rules.js
devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js
devtools/client/inspector/shared/test/head.js
devtools/client/inspector/test/browser_inspector_addSidebarTab.js
devtools/client/inspector/test/browser_inspector_pseudoclass-lock.js
devtools/client/inspector/test/browser_inspector_sidebarstate.js
devtools/client/inspector/test/shared-head.js
devtools/client/inspector/toolsidebar.js
devtools/client/responsivedesign/test/head.js
devtools/client/shared/components/tabs/moz.build
devtools/client/shared/components/tabs/paneltab.js
devtools/client/shared/components/tabs/tabbar.js
devtools/client/shared/components/test/mochitest/test_tabs_accessibility.html
devtools/client/shared/test/browser_telemetry_sidebar.js
--- a/devtools/client/animationinspector/animation-controller.js
+++ b/devtools/client/animationinspector/animation-controller.js
@@ -210,17 +210,17 @@ var AnimationsController = {
     if (this.isListeningToMutations) {
       this.animationsFront.off("mutations", this.onAnimationMutations);
     }
   },
 
   isPanelVisible: function () {
     return gToolbox.currentToolId === "inspector" &&
            gInspector.sidebar &&
-           gInspector.sidebar.getCurrentTabID() == "animationinspector";
+           gInspector.sidebar.tabbar.getCurrentTabId() == "animationinspector";
   },
 
   onPanelVisibilityChange: Task.async(function* () {
     if (this.isPanelVisible()) {
       this.onNewNodeFront();
     }
   }),
 
--- a/devtools/client/animationinspector/test/browser_animation_refresh_when_active.js
+++ b/devtools/client/animationinspector/test/browser_animation_refresh_when_active.js
@@ -15,39 +15,39 @@ add_task(function* () {
   yield testRefresh(inspector, panel);
 });
 
 function* testRefresh(inspector, panel) {
   info("Select a non animated node");
   yield selectNodeAndWaitForAnimations(".still", inspector);
 
   info("Switch to the rule-view panel");
-  inspector.sidebar.select("ruleview");
+  inspector.sidebar.tabbar.select("ruleview");
 
   info("Select the animated node now");
   yield selectNodeAndWaitForAnimations(".animated", inspector);
 
   assertAnimationsDisplayed(panel, 0,
     "The panel doesn't show the animation data while inactive");
 
   info("Switch to the animation panel");
-  inspector.sidebar.select("animationinspector");
+  inspector.sidebar.tabbar.select("animationinspector");
   yield panel.once(panel.UI_UPDATED_EVENT);
 
   assertAnimationsDisplayed(panel, 1,
     "The panel shows the animation data after selecting it");
 
   info("Switch again to the rule-view");
-  inspector.sidebar.select("ruleview");
+  inspector.sidebar.tabbar.select("ruleview");
 
   info("Select the non animated node again");
   yield selectNodeAndWaitForAnimations(".still", inspector);
 
   assertAnimationsDisplayed(panel, 1,
     "The panel still shows the previous animation data since it is inactive");
 
   info("Switch to the animation panel again");
-  inspector.sidebar.select("animationinspector");
+  inspector.sidebar.tabbar.select("animationinspector");
   yield panel.once(panel.UI_UPDATED_EVENT);
 
   assertAnimationsDisplayed(panel, 0,
     "The panel is now empty after refreshing");
 }
--- a/devtools/client/animationinspector/test/head.js
+++ b/devtools/client/animationinspector/test/head.js
@@ -6,16 +6,18 @@
 "use strict";
 
 /* import-globals-from ../../inspector/test/head.js */
 // Import the inspector's head.js first (which itself imports shared-head.js).
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
   this);
 
+// Services.prefs.setBoolPref("devtools.dump.emit", true);
+
 const FRAME_SCRIPT_URL = CHROME_URL_ROOT + "doc_frame_script.js";
 const COMMON_FRAME_SCRIPT_URL = "chrome://devtools/content/shared/frame-script-utils.js";
 const TAB_NAME = "animationinspector";
 const ANIMATION_L10N =
   new LocalizationHelper("devtools/locale/animationinspector.properties");
 
 // Auto clean-up when a test ends
 registerCleanupFunction(function* () {
@@ -25,16 +27,17 @@ registerCleanupFunction(function* () {
     gBrowser.removeCurrentTab();
   }
 });
 
 // Clean-up all prefs that might have been changed during a test run
 // (safer here because if the test fails, then the pref is never reverted)
 registerCleanupFunction(() => {
   Services.prefs.clearUserPref("devtools.debugger.log");
+  Services.prefs.clearUserPref("devtools.dump.emit");
 });
 
 // WebAnimations API is not enabled by default in all release channels yet, see
 // Bug 1264101.
 function enableWebAnimationsAPI() {
   return new Promise(resolve => {
     SpecialPowers.pushPrefEnv({"set": [
       ["dom.animations-api.core.enabled", true]
@@ -86,17 +89,17 @@ function* reloadTab(inspector) {
            and animations of its subtree are properly displayed.
  */
 var selectNodeAndWaitForAnimations = Task.async(
   function* (data, inspector, reason = "test") {
     yield selectNode(data, inspector, reason);
 
     // We want to make sure the rest of the test waits for the animations to
     // be properly displayed (wait for all target DOM nodes to be previewed).
-    let {AnimationsPanel} = inspector.sidebar.getWindowForTab(TAB_NAME);
+    let {AnimationsPanel} = inspector.sidebar.tabbar.getWindowForTab(TAB_NAME);
     yield waitForAllAnimationTargets(AnimationsPanel);
   }
 );
 
 /**
  * Check if there are the expected number of animations being displayed in the
  * panel right now.
  * @param {AnimationsPanel} panel
@@ -114,17 +117,17 @@ function assertAnimationsDisplayed(panel
  * Takes an Inspector panel that was just created, and waits
  * for a "inspector-updated" event as well as the animation inspector
  * sidebar to be ready. Returns a promise once these are completed.
  *
  * @param {InspectorPanel} inspector
  * @return {Promise}
  */
 var waitForAnimationInspectorReady = Task.async(function* (inspector) {
-  let win = inspector.sidebar.getWindowForTab(TAB_NAME);
+  let win = inspector.sidebar.tabbar.getWindowForTab(TAB_NAME);
   let updated = inspector.once("inspector-updated");
 
   // In e10s, if we wait for underlying toolbox actors to
   // load (by setting DevToolsUtils.testing to true), we miss the
   // "animationinspector-ready" event on the sidebar, so check to see if the
   // iframe is already loaded.
   let tabReady = win.document.readyState === "complete" ?
                  promise.resolve() :
@@ -139,17 +142,17 @@ var waitForAnimationInspectorReady = Tas
  * @return a promise that resolves when the inspector is ready.
  */
 var openAnimationInspector = Task.async(function* () {
   let {inspector, toolbox} = yield openInspectorSidebarTab(TAB_NAME);
 
   info("Waiting for the inspector and sidebar to be ready");
   yield waitForAnimationInspectorReady(inspector);
 
-  let win = inspector.sidebar.getWindowForTab(TAB_NAME);
+  let win = inspector.sidebar.tabbar.getWindowForTab(TAB_NAME);
   let {AnimationsController, AnimationsPanel} = win;
 
   info("Waiting for the animation controller and panel to be ready");
   if (AnimationsPanel.initialized) {
     yield AnimationsPanel.initialized;
   } else {
     yield AnimationsPanel.once(AnimationsPanel.PANEL_INITIALIZED);
   }
--- a/devtools/client/inspector/components/box-model.js
+++ b/devtools/client/inspector/components/box-model.js
@@ -417,17 +417,17 @@ BoxModelView.prototype = {
   },
 
   /**
    * Is the BoxModelView visible in the sidebar.
    * @return {Boolean}
    */
   isViewVisible: function () {
     return this.inspector &&
-           this.inspector.sidebar.getCurrentTabID() == "computedview";
+           this.inspector.sidebar.tabbar.getCurrentTabId() == "computedview";
   },
 
   /**
    * Is the BoxModelView visible in the sidebar and is the current node valid to
    * be displayed in the view.
    * @return {Boolean}
    */
   isViewVisibleAndNodeValid: function () {
--- a/devtools/client/inspector/components/inspector-tab-panel.js
+++ b/devtools/client/inspector/components/inspector-tab-panel.js
@@ -15,18 +15,16 @@ const { div } = DOM;
  * Helper panel component that is using an existing DOM node
  * as the content. It's used by Sidebar as well as SplitBox
  * components.
  */
 var InspectorTabPanel = createClass({
   displayName: "InspectorTabPanel",
 
   propTypes: {
-    // ID of the node that should be rendered as the content.
-    id: PropTypes.string.isRequired,
     // Optional prefix for panel IDs.
     idPrefix: PropTypes.string,
     // Optional mount callback
     onMount: PropTypes.func,
   },
 
   getDefaultProps: function () {
     return {
--- a/devtools/client/inspector/computed/computed.js
+++ b/devtools/client/inspector/computed/computed.js
@@ -1417,17 +1417,17 @@ function ComputedViewTool(inspector, win
   this.onSelected();
 }
 
 ComputedViewTool.prototype = {
   isSidebarActive: function () {
     if (!this.computedView) {
       return false;
     }
-    return this.inspector.sidebar.getCurrentTabID() == "computedview";
+    return this.inspector.sidebar.tabbar.getCurrentTabId() == "computedview";
   },
 
   onSelected: function (event) {
     // Ignore the event if the view has been destroyed, or if it's inactive.
     // But only if the current selection isn't null. If it's been set to null,
     // let the update go through as this is needed to empty the view on
     // navigation.
     if (!this.computedView) {
--- a/devtools/client/inspector/fonts/fonts.js
+++ b/devtools/client/inspector/fonts/fonts.js
@@ -43,17 +43,17 @@ FontInspector.prototype = {
     this.update();
   },
 
   /**
    * Is the fontinspector visible in the sidebar?
    */
   isActive: function () {
     return this.inspector.sidebar &&
-           this.inspector.sidebar.getCurrentTabID() == "fontinspector";
+           this.inspector.sidebar.tabbar.getCurrentTabId() == "fontinspector";
   },
 
   /**
    * Remove listeners.
    */
   destroy: function () {
     this.chromeDoc = null;
     this.inspector.sidebar.off("fontinspector-selected", this.onNewNode);
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -533,74 +533,74 @@ Inspector.prototype = {
     Services.prefs.setIntPref("devtools.toolsidebar-height.inspector", state.height);
   },
 
   /**
    * Build the sidebar.
    */
   setupSidebar: function () {
     let tabbox = this.panelDoc.querySelector("#inspector-sidebar");
-    this.sidebar = new ToolSidebar(tabbox, this, "inspector", {
+    this.sidebar = new ToolSidebar("sidebar-panel-", tabbox, this, "inspector", {
       showAllTabsMenu: true
     });
 
     let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar");
 
     if (!Services.prefs.getBoolPref("devtools.fontinspector.enabled") &&
        defaultTab == "fontinspector") {
       defaultTab = "ruleview";
     }
 
     // Append all side panels
-    this.sidebar.addExistingTab(
+    this.sidebar.tabbar.addExistingTab(
       "ruleview",
       INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
       defaultTab == "ruleview");
 
-    this.sidebar.addExistingTab(
+    this.sidebar.tabbar.addExistingTab(
       "computedview",
       INSPECTOR_L10N.getStr("inspector.sidebar.computedViewTitle"),
       defaultTab == "computedview");
 
     this._setDefaultSidebar = (event, toolId) => {
       Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId);
     };
 
     this.sidebar.on("select", this._setDefaultSidebar);
 
     this.ruleview = new RuleViewTool(this, this.panelWin);
     this.computedview = new ComputedViewTool(this, this.panelWin);
 
     if (Services.prefs.getBoolPref("devtools.layoutview.enabled")) {
-      this.sidebar.addExistingTab(
+      this.sidebar.tabbar.addExistingTab(
         "layoutview",
         INSPECTOR_L10N.getStr("inspector.sidebar.layoutViewTitle"),
         defaultTab == "layoutview"
       );
 
       this.layoutview = new LayoutViewTool(this, this.panelWin);
     }
 
     if (this.target.form.animationsActor) {
-      this.sidebar.addFrameTab(
+      this.sidebar.tabbar.addFrameTab(
         "animationinspector",
         INSPECTOR_L10N.getStr("inspector.sidebar.animationInspectorTitle"),
         "chrome://devtools/content/animationinspector/animation-inspector.xhtml",
         defaultTab == "animationinspector");
     }
 
     if (Services.prefs.getBoolPref("devtools.fontinspector.enabled") &&
         this.canGetUsedFontFaces) {
-      this.sidebar.addExistingTab(
+      this.sidebar.tabbar.addExistingTab(
         "fontinspector",
         INSPECTOR_L10N.getStr("inspector.sidebar.fontInspectorTitle"),
         defaultTab == "fontinspector");
 
       this.fontInspector = new FontInspector(this, this.panelWin);
-      this.sidebar.toggleTab(true, "fontinspector");
+      this.sidebar.tabbar.toggleTab(true, "fontinspector");
     }
 
     // Setup the splitter before the sidebar is displayed so,
     // we don't miss any events.
     this.setupSplitter();
 
     this.sidebar.show(defaultTab);
   },
--- a/devtools/client/inspector/rules/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -1525,17 +1525,17 @@ function RuleViewTool(inspector, window)
   this.onSelected();
 }
 
 RuleViewTool.prototype = {
   isSidebarActive: function () {
     if (!this.view) {
       return false;
     }
-    return this.inspector.sidebar.getCurrentTabID() == "ruleview";
+    return this.inspector.sidebar.tabbar.getCurrentTabId() == "ruleview";
   },
 
   onSelected: function (event) {
     // Ignore the event if the view has been destroyed, or if it's inactive.
     // But only if the current selection isn't null. If it's been set to null,
     // let the update go through as this is needed to empty the view on
     // navigation.
     if (!this.view) {
--- a/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_context-menu-copy-color_01.js
@@ -18,17 +18,17 @@ add_task(function* () {
   let {inspector} = yield openInspector();
   yield testView("ruleview", inspector);
   yield testView("computedview", inspector);
 });
 
 function* testView(viewId, inspector) {
   info("Testing " + viewId);
 
-  yield inspector.sidebar.select(viewId);
+  yield inspector.sidebar.tabbar.select(viewId);
   let view = inspector[viewId].view || inspector[viewId].computedView;
   yield selectNode("div", inspector);
 
   testIsColorValueNode(view);
   testIsColorPopupOnAllNodes(view);
   yield clearCurrentNodeSelection(inspector);
 }
 
--- a/devtools/client/inspector/shared/test/head.js
+++ b/devtools/client/inspector/shared/test/head.js
@@ -37,17 +37,17 @@ registerCleanupFunction(() => {
  *
  * Most of these functions are async too and return promises.
  *
  * All tests should follow the following pattern:
  *
  * add_task(function*() {
  *   yield addTab(TEST_URI);
  *   let {toolbox, inspector} = yield openInspector();
- *   inspector.sidebar.select(viewId);
+ *   inspector.sidebar.tabbar.select(viewId);
  *   let view = inspector[viewId].view;
  *   yield selectNode("#test", inspector);
  *   yield someAsyncTestFunction(view);
  * });
  *
  * add_task is the way to define the testcase in the test file. It accepts
  * a single generator-function argument.
  * The generator function should yield any async call.
--- a/devtools/client/inspector/test/browser_inspector_addSidebarTab.js
+++ b/devtools/client/inspector/test/browser_inspector_addSidebarTab.js
@@ -30,33 +30,33 @@ add_task(function* () {
         )
       );
     }
   }));
 
   // Append custom panel (tab) into the Inspector panel and
   // make sure it's selected by default (the last arg = true).
   inspector.addSidebarTab("myPanel", "My Panel", tabPanel, true);
-  is(inspector.sidebar.getCurrentTabID(), "myPanel",
+  is(inspector.sidebar.tabbar.getCurrentTabId(), "myPanel",
      "My Panel is selected by default");
 
   // Define another custom side-panel.
   tabPanel = React.createFactory(React.createClass({
     displayName: "myTabPanel2",
     render: function () {
       return (
         div({className: "my-tab-panel2"},
           "Another Content"
         )
       );
     }
   }));
 
   // Append second panel, but don't select it by default.
   inspector.addSidebarTab("myPanel", "My Panel", tabPanel, false);
-  is(inspector.sidebar.getCurrentTabID(), "myPanel",
+  is(inspector.sidebar.tabbar.getCurrentTabId(), "myPanel",
      "My Panel is selected by default");
 
   // Check the the panel content is properly rendered.
   let tabPanelNode = inspector.panelDoc.querySelector(".my-tab-panel");
   is(tabPanelNode.textContent, CONTENT_TEXT,
     "Side panel content has been rendered.");
 });
--- a/devtools/client/inspector/test/browser_inspector_pseudoclass-lock.js
+++ b/devtools/client/inspector/test/browser_inspector_pseudoclass-lock.js
@@ -18,17 +18,17 @@ const TEST_URL = "data:text/html;charset
                  "  </div>" +
                  "</body>";
 
 add_task(function* () {
   info("Creating the test tab and opening the rule-view");
   let {toolbox, inspector, testActor} = yield openInspectorForURL(TEST_URL);
 
   info("Selecting the ruleview sidebar");
-  inspector.sidebar.select("ruleview");
+  inspector.sidebar.tabbar.select("ruleview");
 
   let view = inspector.ruleview.view;
 
   info("Selecting the test node");
   yield selectNode("#div-1", inspector);
 
   yield togglePseudoClass(inspector);
   yield assertPseudoAddedToNode(inspector, testActor, view);
--- a/devtools/client/inspector/test/browser_inspector_sidebarstate.js
+++ b/devtools/client/inspector/test/browser_inspector_sidebarstate.js
@@ -5,34 +5,34 @@
 
 const TEST_URI = "data:text/html;charset=UTF-8," +
   "<h1>browser_inspector_sidebarstate.js</h1>";
 
 add_task(function* () {
   let { inspector, toolbox } = yield openInspectorForURL(TEST_URI);
 
   info("Selecting ruleview.");
-  inspector.sidebar.select("ruleview");
+  inspector.sidebar.tabbar.select("ruleview");
 
-  is(inspector.sidebar.getCurrentTabID(), "ruleview",
+  is(inspector.sidebar.tabbar.getCurrentTabId(), "ruleview",
      "Rule View is selected by default");
 
   info("Selecting computed view.");
-  inspector.sidebar.select("computedview");
+  inspector.sidebar.tabbar.select("computedview");
 
   // Finish initialization of the computed panel before
   // destroying the toolbox.
   yield waitForTick();
 
   info("Closing inspector.");
   yield toolbox.destroy();
 
   info("Re-opening inspector.");
   inspector = (yield openInspector()).inspector;
 
-  if (!inspector.sidebar.getCurrentTabID()) {
+  if (!inspector.sidebar.tabbar.getCurrentTabId()) {
     info("Default sidebar still to be selected, adding select listener.");
     yield inspector.sidebar.once("select");
   }
 
-  is(inspector.sidebar.getCurrentTabID(), "computedview",
+  is(inspector.sidebar.tabbar.getCurrentTabId(), "computedview",
      "Computed view is selected by default.");
 });
--- a/devtools/client/inspector/test/shared-head.js
+++ b/devtools/client/inspector/test/shared-head.js
@@ -44,17 +44,17 @@ var openInspector = Task.async(function*
  *        The ID of the sidebar tab to be opened
  * @return a promise that resolves when the inspector is ready and the tab is
  * visible and ready
  */
 var openInspectorSidebarTab = Task.async(function* (id) {
   let {toolbox, inspector, testActor} = yield openInspector();
 
   info("Selecting the " + id + " sidebar");
-  inspector.sidebar.select(id);
+  inspector.sidebar.tabbar.select(id);
 
   return {
     toolbox,
     inspector,
     testActor
   };
 });
 
@@ -101,29 +101,29 @@ function openComputedView() {
 /**
  * Select the rule view sidebar tab on an already opened inspector panel.
  *
  * @param {InspectorPanel} inspector
  *        The opened inspector panel
  * @return {CssRuleView} the rule view
  */
 function selectRuleView(inspector) {
-  inspector.sidebar.select("ruleview");
+  inspector.sidebar.tabbar.select("ruleview");
   return inspector.ruleview.view;
 }
 
 /**
  * Select the computed view sidebar tab on an already opened inspector panel.
  *
  * @param {InspectorPanel} inspector
  *        The opened inspector panel
  * @return {CssComputedView} the computed view
  */
 function selectComputedView(inspector) {
-  inspector.sidebar.select("computedview");
+  inspector.sidebar.tabbar.select("computedview");
   return inspector.computedview.computedView;
 }
 
 /**
  * Get the NodeFront for a node that matches a given css selector, via the
  * protocol.
  * @param {String|NodeFront} selector
  * @param {InspectorPanel} inspector The instance of InspectorPanel currently
--- a/devtools/client/inspector/toolsidebar.js
+++ b/devtools/client/inspector/toolsidebar.js
@@ -17,45 +17,42 @@ var { Task } = require("devtools/shared/
  * This new component is part of devtools.html aimed at
  * removing XUL and use HTML for entire DevTools UI.
  * There are currently two implementation of the side bar since
  * the `sidebar.js` module (mentioned above) is still used by
  * other panels.
  * As soon as all panels are using this HTML based
  * implementation it can be removed.
  */
-function ToolSidebar(tabbox, panel, uid, options = {}) {
+function ToolSidebar(idPrefix, tabbox, panel, uid, options = {}) {
   EventEmitter.decorate(this);
 
+  this._idPrefix = idPrefix;
   this._tabbox = tabbox;
   this._uid = uid;
   this._panelDoc = this._tabbox.ownerDocument;
   this._toolPanel = panel;
   this._options = options;
 
   if (!options.disableTelemetry) {
     this._telemetry = new Telemetry();
   }
 
-  this._tabs = [];
-
   if (this._options.hideTabstripe) {
     this._tabbox.setAttribute("hidetabs", "true");
   }
 
   this.render();
 
-  this._toolPanel.emit("sidebar-created", this);
+  this._toolPanel.emit(this._idPrefix + "-created", this);
 }
 
 exports.ToolSidebar = ToolSidebar;
 
 ToolSidebar.prototype = {
-  TABPANEL_ID_PREFIX: "sidebar-panel-",
-
   // React
 
   get React() {
     return this._toolPanel.React;
   },
 
   get ReactDOM() {
     return this._toolPanel.ReactDOM;
@@ -69,157 +66,80 @@ ToolSidebar.prototype = {
     return this._toolPanel.InspectorTabPanel;
   },
 
   // Rendering
 
   render: function () {
     let Tabbar = this.React.createFactory(this.browserRequire(
       "devtools/client/shared/components/tabs/tabbar"));
+    let PanelTab = this.browserRequire(
+      "devtools/client/shared/components/tabs/paneltab");
 
-    let sidebar = Tabbar({
-      toolbox: this._toolPanel._toolbox,
+    let tabbar = Tabbar({
+      panel: this._toolPanel,
+      panelDoc: this._panelDoc,
       showAllTabsMenu: true,
+      idPrefix: this._idPrefix,
       onSelect: this.handleSelectionChange.bind(this),
+      onPanelReady: this.handlePanelReady.bind(this),
+      onNewTabRegistered: this.handleNewTabRegistered.bind(this),
+      onToolReady: this.handleToolReady.bind(this),
+      onTabUnregistered: this.handleTabUnregistered.bind(this),
+
+      InspectorTabPanel: this.InspectorTabPanel.bind(this),
+      addTab: PanelTab.addTab,
+      addExistingTab: PanelTab.addExistingTab,
+      addFrameTab: PanelTab.addFrameTab,
+      onPanelMounted: PanelTab.onPanelMounted,
+      getTabPanel: PanelTab.getTabPanel,
     });
 
-    this._tabbar = this.ReactDOM.render(sidebar, this._tabbox);
+    this.tabbar = this.ReactDOM.render(tabbar, this._tabbox);
+
+    return this.tabbar;
   },
 
   /**
    * Register a side-panel tab.
    *
    * @param {string} tab uniq id
    * @param {string} title tab title
    * @param {React.Component} panel component. See `InspectorPanelTab` as an example.
    * @param {boolean} selected true if the panel should be selected
    */
   addTab: function (id, title, panel, selected) {
-    this._tabbar.addTab(id, title, selected, panel);
+    this.tabbar.addTab(id, title, selected, panel);
     this.emit("new-tab-registered", id);
   },
 
   /**
-   * Helper API for adding side-panels that use existing DOM nodes
-   * (defined within inspector.xhtml) as the content.
-   *
-   * @param {string} tab uniq id
-   * @param {string} title tab title
-   * @param {boolean} selected true if the panel should be selected
-   */
-  addExistingTab: function (id, title, selected) {
-    let panel = this.InspectorTabPanel({
-      id: id,
-      idPrefix: this.TABPANEL_ID_PREFIX,
-      key: id,
-      title: title,
-    });
-
-    this.addTab(id, title, panel, selected);
-  },
-
-  /**
-   * Helper API for adding side-panels that use existing <iframe> nodes
-   * (defined within inspector.xhtml) as the content.
-   * The document must have a title, which will be used as the name of the tab.
-   *
-   * @param {string} tab uniq id
-   * @param {string} title tab title
-   * @param {string} url
-   * @param {boolean} selected true if the panel should be selected
-   */
-  addFrameTab: function (id, title, url, selected) {
-    let panel = this.InspectorTabPanel({
-      id: id,
-      idPrefix: this.TABPANEL_ID_PREFIX,
-      key: id,
-      title: title,
-      url: url,
-      onMount: this.onSidePanelMounted.bind(this),
-    });
-
-    this.addTab(id, title, panel, selected);
-  },
-
-  onSidePanelMounted: function (content, props) {
-    let iframe = content.querySelector("iframe");
-    if (!iframe || iframe.getAttribute("src")) {
-      return;
-    }
-
-    let onIFrameLoaded = (event) => {
-      iframe.removeEventListener("load", onIFrameLoaded, true);
-
-      let doc = event.target;
-      let win = doc.defaultView;
-      if ("setPanel" in win) {
-        win.setPanel(this._toolPanel, iframe);
-      }
-      this.emit(props.id + "-ready");
-    };
-
-    iframe.addEventListener("load", onIFrameLoaded, true);
-    iframe.setAttribute("src", props.url);
-  },
-
-  /**
-   * Remove an existing tab.
-   * @param {String} tabId The ID of the tab that was used to register it, or
-   * the tab id attribute value if the tab existed before the sidebar
-   * got created.
-   * @param {String} tabPanelId Optional. If provided, this ID will be used
-   * instead of the tabId to retrieve and remove the corresponding <tabpanel>
-   */
-  removeTab: Task.async(function* (tabId, tabPanelId) {
-    this._tabbar.removeTab(tabId);
-
-    let win = this.getWindowForTab(tabId);
-    if (win && ("destroy" in win)) {
-      yield win.destroy();
-    }
-
-    this.emit("tab-unregistered", tabId);
-  }),
-
-  /**
    * Show or hide a specific tab.
    * @param {Boolean} isVisible True to show the tab/tabpanel, False to hide it.
    * @param {String} id The ID of the tab to be hidden.
    */
   toggleTab: function (isVisible, id) {
-    this._tabbar.toggleTab(id, isVisible);
+    this.tabbar.toggleTab(id, isVisible);
   },
 
   /**
    * Select a specific tab.
    */
   select: function (id) {
-    this._tabbar.select(id);
+    this.tabbar.select(id);
   },
 
   /**
    * Return the id of the selected tab.
    */
   getCurrentTabID: function () {
     return this._currentTool;
   },
 
   /**
-   * Returns the requested tab panel based on the id.
-   * @param {String} id
-   * @return {DOMNode}
-   */
-  getTabPanel: function (id) {
-    // Search with and without the ID prefix as there might have been existing
-    // tabpanels by the time the sidebar got created
-    return this._panelDoc.querySelector("#" +
-      this.TABPANEL_ID_PREFIX + id + ", #" + id);
-  },
-
-  /**
    * Event handler.
    */
   handleSelectionChange: function (id) {
     if (this._destroyed) {
       return;
     }
 
     let previousTool = this._currentTool;
@@ -235,61 +155,68 @@ ToolSidebar.prototype = {
     if (this._telemetry) {
       this._telemetry.toolOpened(this._currentTool);
     }
 
     this.emit(this._currentTool + "-selected");
     this.emit("select", this._currentTool);
   },
 
+  handlePanelReady: function (id) {
+    if (this._destroyed) {
+      return;
+    }
+
+    this.emit(id + "-ready");
+  },
+
+  handleTabUnregistered: function (tabId) {
+    this.emit("tab-unregistered", tabId);
+  },
+
+  handleNewTabRegistered: function (tabId) {
+    this.emit("new-tab-registered", tabId);
+  },
+
+  handleToolReady: function (toolId) {
+    this.emit(toolId + "-ready");
+  },
+
   /**
-   * Show the sidebar.
+   * Show the tabbar.
    *
    * @param  {String} id
-   *         The sidebar tab id to select.
+   *         The tab id to select.
    */
   show: function (id) {
     this._tabbox.removeAttribute("hidden");
 
-    // If an id is given, select the corresponding sidebar tab and record the
-    // tool opened.
+    // If an id is given, select the corresponding tab and record the tool
+    // opened.
     if (id) {
       this._currentTool = id;
 
       if (this._telemetry) {
         this._telemetry.toolOpened(this._currentTool);
       }
     }
 
     this.emit("show");
   },
 
   /**
-   * Show the sidebar.
+   * Hide the tabbar.
    */
   hide: function () {
     this._tabbox.setAttribute("hidden", "true");
 
     this.emit("hide");
   },
 
   /**
-   * Return the window containing the tab content.
-   */
-  getWindowForTab: function (id) {
-    // Get the tabpanel and make sure it contains an iframe
-    let panel = this.getTabPanel(id);
-    if (!panel || !panel.firstElementChild || !panel.firstElementChild.contentWindow) {
-      return null;
-    }
-
-    return panel.firstElementChild.contentWindow;
-  },
-
-  /**
    * Clean-up.
    */
   destroy: Task.async(function* () {
     if (this._destroyed) {
       return;
     }
     this._destroyed = true;
 
@@ -310,16 +237,16 @@ ToolSidebar.prototype = {
       }
       panel.remove();
     }
 
     if (this._currentTool && this._telemetry) {
       this._telemetry.toolClosed(this._currentTool);
     }
 
-    this._toolPanel.emit("sidebar-destroyed", this);
+    this._toolPanel.emit(this._idPrefix + "-destroyed", this);
 
-    this._tabs = null;
+    this._idPrefix = null;
     this._tabbox = null;
     this._panelDoc = null;
     this._toolPanel = null;
   })
 };
--- a/devtools/client/responsivedesign/test/head.js
+++ b/devtools/client/responsivedesign/test/head.js
@@ -140,34 +140,34 @@ function waitForToolboxFrameFocus(toolbo
  * corresponds to the given id selected
  * @return a promise that resolves when the inspector is ready and the sidebar
  * view is visible and ready
  */
 var openInspectorSideBar = Task.async(function* (id) {
   let {toolbox, inspector} = yield openInspector();
 
   info("Selecting the " + id + " sidebar");
-  inspector.sidebar.select(id);
+  inspector.sidebar.tabbar.select(id);
 
   return {
     toolbox: toolbox,
     inspector: inspector,
     view: inspector[id].view || inspector[id].computedView
   };
 });
 
 /**
  * Checks whether the inspector's sidebar corresponding to the given id already
  * exists
  * @param {InspectorPanel}
  * @param {String}
  * @return {Boolean}
  */
 function hasSideBarTab(inspector, id) {
-  return !!inspector.sidebar.getWindowForTab(id);
+  return !!inspector.sidebar.tabbar.getWindowForTab(id);
 }
 
 /**
  * Open the toolbox, with the inspector tool visible, and the computed-view
  * sidebar tab selected.
  * @return a promise that resolves when the inspector is ready and the computed
  * view is visible and ready
  */
--- a/devtools/client/shared/components/tabs/moz.build
+++ b/devtools/client/shared/components/tabs/moz.build
@@ -1,12 +1,13 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
+    'paneltab.js',
     'tabbar.css',
     'tabbar.js',
     'tabs.css',
     'tabs.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/tabs/paneltab.js
@@ -0,0 +1,96 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+module.exports = {
+  addTab(id, title, selected = false, panel, url) {
+    let tabs = this.state.tabs.slice();
+    tabs.push({id, title, panel, url});
+
+    let newState = Object.assign({}, this.state, {
+      tabs: tabs,
+    });
+
+    if (selected) {
+      newState.activeTab = tabs.length - 1;
+    }
+
+    this.setState(newState);
+  },
+
+  addExistingTab(id, title, selected) {
+    this.addTab(id, title, selected, this.InspectorTabPanel);
+
+    if (this.props.onNewTabRegistered) {
+      this.props.onNewTabRegistered(id);
+    }
+  },
+
+  /**
+   * Register a tab. A tab is a document.
+   * The document must have a title, which will be used as the name of the tab.
+   *
+   * @param {string} tab uniq id
+   * @param {string} url
+   */
+  addFrameTab(id, title, url, selected) {
+    let panel = this.InspectorTabPanel({
+      idPrefix: this.props.idPrefix,
+      id: id,
+      key: id,
+      title: title,
+      url: url,
+      onMount: this.onPanelMounted.bind(this),
+    });
+
+    this.addTab(id, title, selected, panel);
+
+    if (this.props.onNewTabRegistered) {
+      this.props.onNewTabRegistered(id);
+    }
+  },
+
+  onPanelMounted(content, props) {
+    let iframe = content.querySelector("iframe");
+    if (!iframe || iframe.getAttribute("src")) {
+      return;
+    }
+
+    let onIFrameLoaded = event => {
+      iframe.removeEventListener("load", onIFrameLoaded, true);
+
+      let doc = event.target;
+      let win = doc.defaultView;
+      if ("setPanel" in win) {
+        win.setPanel(this.props.panel, iframe);
+      }
+
+      if (this.props.onToolReady) {
+        this.props.onToolReady(props.id);
+      }
+
+      if (this.props.onPanelReady) {
+        this.props.onPanelReady(props.id);
+      }
+    };
+
+    iframe.addEventListener("load", onIFrameLoaded, true);
+    iframe.setAttribute("src", props.url);
+  },
+
+  /**
+   * Returns the requested tab panel based on the id.
+   * @param {String} id
+   * @return {DOMNode}
+   */
+  getTabPanel(id) {
+    // Search with and without the ID prefix as there might have been existing
+    // tabpanels by the time the tabbar got created
+    return this.props.panelDoc.querySelector("#" +
+      this.props.idPrefix + id + ", #" + id);
+  }
+};
--- a/devtools/client/shared/components/tabs/tabbar.js
+++ b/devtools/client/shared/components/tabs/tabbar.js
@@ -1,151 +1,190 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { DOM, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
+const { Component, DOM, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
 const Tabs = createFactory(require("devtools/client/shared/components/tabs/tabs").Tabs);
-
 const Menu = require("devtools/client/framework/menu");
 const MenuItem = require("devtools/client/framework/menu-item");
 
 // Shortcuts
 const { div } = DOM;
 
 /**
  * Renders Tabbar component.
  */
-let Tabbar = createClass({
-  displayName: "Tabbar",
-
-  propTypes: {
-    onSelect: PropTypes.func,
-    showAllTabsMenu: PropTypes.bool,
-    toolbox: PropTypes.object,
-  },
+class Tabbar extends Component { //eslint-disable-line
+  constructor(props) {
+    super(props);
 
-  getDefaultProps: function () {
-    return {
-      showAllTabsMenu: false,
-    };
-  },
-
-  getInitialState: function () {
-    return {
+    this.state = {
       tabs: [],
       activeTab: 0
     };
-  },
+
+    this.onTabChanged = this.onTabChanged.bind(this);
+
+    if (typeof this.props.InspectorTabPanel !== "undefined") {
+      this.InspectorTabPanel = this.props.InspectorTabPanel.bind(this);
+    }
+
+    if (typeof this.props.addTab !== "undefined") {
+      this.addTab = this.props.addTab.bind(this);
+    }
+
+    if (typeof this.props.addExistingTab !== "undefined") {
+      this.addExistingTab = this.props.addExistingTab.bind(this);
+    }
+
+    if (typeof this.props.addFrameTab !== "undefined") {
+      this.addFrameTab = this.props.addFrameTab.bind(this);
+    }
+
+    if (typeof this.props.onPanelMounted !== "undefined") {
+      this.onPanelMounted = this.props.onPanelMounted.bind(this);
+    }
+
+    if (typeof this.props.InspectorTabPanel !== "undefined") {
+      this.getTabPanel = this.props.getTabPanel.bind(this);
+    }
+  }
 
   // Public API
 
-  addTab: function (id, title, selected = false, panel, url) {
-    let tabs = this.state.tabs.slice();
-    tabs.push({id, title, panel, url});
+  get browserRequire() {
+    return this.props.panel.browserRequire;
+  }
 
-    let newState = Object.assign({}, this.state, {
-      tabs: tabs,
-    });
+  /**
+   * Remove an existing tab.
+   * @param {String} tabId The ID of the tab that was used to register it, or
+   * the tab id attribute value if the tab existed before the tabbar
+   * got created.
+   * @param {String} tabPanelId Optional. If provided, this ID will be used
+   * instead of the tabId to retrieve and remove the corresponding <tabpanel>
+   */
+  * removeTab(tabId, tabPanelId) {
+    let index = this.getTabIndex(tabId);
 
-    if (selected) {
-      newState.activeTab = tabs.length - 1;
+    if (index < 0) {
+      return;
     }
 
-    this.setState(newState, () => {
-      if (this.props.onSelect && selected) {
-        this.props.onSelect(id);
-      }
-    });
-  },
+    let tabs = this.state.tabs.slice();
+    tabs.splice(index, 1);
+
+    this.setState(Object.assign({}, this.state, {
+      tabs: tabs,
+    }));
 
-  toggleTab: function (tabId, isVisible) {
+    let win = this.getWindowForTab(tabId);
+    if (win && ("destroy" in win)) {
+      yield win.destroy();
+    }
+
+    if (this.props.onTabUnregistered) {
+      this.props.onTabUnregistered(tabId);
+    }
+  }
+
+  toggleTab(tabId, isVisible) {
     let index = this.getTabIndex(tabId);
     if (index < 0) {
       return;
     }
 
     let tabs = this.state.tabs.slice();
     tabs[index] = Object.assign({}, tabs[index], {
       isVisible: isVisible
     });
 
     this.setState(Object.assign({}, this.state, {
       tabs: tabs,
     }));
-  },
-
-  removeTab: function (tabId) {
-    let index = this.getTabIndex(tabId);
-    if (index < 0) {
-      return;
-    }
+  }
 
-    let tabs = this.state.tabs.slice();
-    tabs.splice(index, 1);
-
-    this.setState(Object.assign({}, this.state, {
-      tabs: tabs,
-    }));
-  },
-
-  select: function (tabId) {
+  select(tabId) {
     let index = this.getTabIndex(tabId);
     if (index < 0) {
       return;
     }
 
     let newState = Object.assign({}, this.state, {
       activeTab: index,
     });
 
     this.setState(newState, () => {
       if (this.props.onSelect) {
         this.props.onSelect(tabId);
       }
     });
-  },
+  }
 
   // Helpers
 
-  getTabIndex: function (tabId) {
-    let tabIndex = -1;
-    this.state.tabs.forEach((tab, index) => {
+  getTab(tabId) {
+    for (let tab of this.state.tabs) {
       if (tab.id == tabId) {
-        tabIndex = index;
+        return tab;
       }
-    });
-    return tabIndex;
-  },
+    }
+
+    return null;
+  }
 
-  getTabId: function (index) {
+  getTabIndex(tabId) {
+    for (let [index, tab] of this.state.tabs.entries()) {
+      if (tab.id === tabId) {
+        return index;
+      }
+    }
+
+    return -1;
+  }
+
+  getTabId(index) {
     return this.state.tabs[index].id;
-  },
+  }
 
-  getCurrentTabId: function () {
+  getCurrentTabId() {
     return this.state.tabs[this.state.activeTab].id;
-  },
+  }
 
   // Event Handlers
 
-  onTabChanged: function (index) {
+  onTabChanged(index) {
     this.setState({
       activeTab: index
+    }, () => {
+      if (this.props.onSelect) {
+        this.props.onSelect(this.state.tabs[index].id);
+      }
     });
+  }
 
-    if (this.props.onSelect) {
-      this.props.onSelect(this.state.tabs[index].id);
+  /**
+   * Return the window containing the tab content.
+   */
+  getWindowForTab(id) {
+    // Get the tabpanel and make sure it contains an iframe
+    let panel = this.getTabPanel(id);
+    if (!panel || !panel.firstElementChild || !panel.firstElementChild.contentWindow) {
+      return null;
     }
-  },
 
-  onAllTabsMenuClick: function (event) {
+    return panel.firstElementChild.contentWindow;
+  }
+
+  onAllTabsMenuClick(event) {
     let menu = new Menu();
     let target = event.target;
 
     // Generate list of menu items from the list of tabs.
     this.state.tabs.forEach(tab => {
       menu.append(new MenuItem({
         label: tab.title,
         type: "checkbox",
@@ -160,45 +199,68 @@ let Tabbar = createClass({
     // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Method/openPopup
     // https://bugzilla.mozilla.org/show_bug.cgi?id=1274551
     let rect = target.getBoundingClientRect();
     let screenX = target.ownerDocument.defaultView.mozInnerScreenX;
     let screenY = target.ownerDocument.defaultView.mozInnerScreenY;
     menu.popup(rect.left + screenX, rect.bottom + screenY, this.props.toolbox);
 
     return menu;
-  },
+  }
 
   // Rendering
 
-  renderTab: function (tab) {
+  renderTab(tab) {
     if (typeof tab.panel === "function") {
       return tab.panel({
+        idPrefix: this.props.idPrefix,
         key: tab.id,
         title: tab.title,
         id: tab.id,
         url: tab.url,
       });
     }
 
     return tab.panel;
-  },
+  }
 
-  render: function () {
+  render() {
     let tabs = this.state.tabs.map(tab => {
       return this.renderTab(tab);
     });
 
     return (
       div({className: "devtools-sidebar-tabs"},
         Tabs({
           onAllTabsMenuClick: this.onAllTabsMenuClick,
           showAllTabsMenu: this.props.showAllTabsMenu,
           tabActive: this.state.activeTab,
           onAfterChange: this.onTabChanged},
           tabs
         )
       )
     );
-  },
-});
+  }
+}
+
+Tabbar.displayName = "Tabbar";
+
+Tabbar.propTypes = {
+  idPrefix: PropTypes.string,
+  onSelect: PropTypes.func,
+  onTabUnregistered: PropTypes.func,
+  showAllTabsMenu: PropTypes.bool,
+  toolbox: PropTypes.object,
+
+  InspectorTabPanel: PropTypes.func,
+  addTab: PropTypes.func,
+  addExistingTab: PropTypes.func,
+  addFrameTab: PropTypes.func,
+  onPanelMounted: PropTypes.func,
+  getTabPanel: PropTypes.func,
+};
+
+Tabbar.defaultProps = {
+  idPrefix: "",
+  showAllTabsMenu: false,
+};
 
 module.exports = Tabbar;
--- a/devtools/client/shared/components/test/mochitest/test_tabs_accessibility.html
+++ b/devtools/client/shared/components/test/mochitest/test_tabs_accessibility.html
@@ -15,17 +15,19 @@ Test tabs accessibility.
 <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 tabbar = Tabbar({
+      idPrefix: "sidebar",
+    });
     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);
 
--- a/devtools/client/shared/test/browser_telemetry_sidebar.js
+++ b/devtools/client/shared/test/browser_telemetry_sidebar.js
@@ -33,17 +33,17 @@ function* testSidebar(toolbox) {
   // Concatenate the array with itself so that we can open each tool twice.
   sidebarTools.push.apply(sidebarTools, sidebarTools);
 
   return new Promise(resolve => {
     // See TOOL_DELAY for why we need setTimeout here
     setTimeout(function selectSidebarTab() {
       let tool = sidebarTools.pop();
       if (tool) {
-        inspector.sidebar.select(tool);
+        inspector.sidebar.tabbar.select(tool);
         setTimeout(function () {
           setTimeout(selectSidebarTab, TOOL_DELAY);
         }, TOOL_DELAY);
       } else {
         resolve();
       }
     }, TOOL_DELAY);
   });