Bug 1283454 - migrate MDN docs tooltip to use HTML;r=ochameau
authorJulian Descottes <jdescottes@mozilla.com>
Tue, 05 Jul 2016 14:39:52 +0200
changeset 304908 395c5a486954a3f747ceb07dfc8f1ebd3d00cf60
parent 304907 9efa66f1c6a15449be7a3c37ade8e104ce2afef8
child 304909 b8b27cb2e148119595af75ff7ee3c2af1b7e833a
push id30446
push usercbook@mozilla.com
push dateThu, 14 Jul 2016 09:44:34 +0000
treeherdermozilla-central@cd9da00ffcc3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau
bugs1283454
milestone50.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1283454 - migrate MDN docs tooltip to use HTML;r=ochameau MozReview-Commit-ID: 50DceNmgGAQ
devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-02.js
devtools/client/inspector/rules/test/browser_rules_css-docs-tooltip_closes-on-escape.js
devtools/client/inspector/shared/style-inspector-overlays.js
devtools/client/jar.mn
devtools/client/shared/test/browser_mdn-docs-01.js
devtools/client/shared/test/browser_mdn-docs-02.js
devtools/client/shared/widgets/MdnDocsWidget.js
devtools/client/shared/widgets/Tooltip.js
devtools/client/shared/widgets/mdn-docs-frame.xhtml
devtools/client/shared/widgets/mdn-docs.css
devtools/client/shared/widgets/tooltip/CssDocsTooltip.js
devtools/client/shared/widgets/tooltip/moz.build
--- a/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-02.js
+++ b/devtools/client/inspector/rules/test/browser_rules_context-menu-show-mdn-docs-02.js
@@ -7,17 +7,17 @@
  * with the MDN docs tooltip.
  *
  * If you display the context click on a property name in the rule view, you
  * should see a menu item "Show MDN Docs". If you click that item, the MDN
  * docs tooltip should be shown, containing docs from MDN for that property.
  *
  * This file tests that:
  * - clicking the context menu item shows the tooltip
- * - pressing "Escape" while the tooltip is showing hides the tooltip
+ * - the tooltip content matches the property name for which the context menu was opened
  */
 
 "use strict";
 
 const {setBaseCssDocsUrl} =
   require("devtools/client/shared/widgets/MdnDocsWidget");
 
 const PROPERTYNAME = "color";
@@ -31,20 +31,17 @@ const TEST_DOC = `
     </body>
   </html>
 `;
 
 add_task(function* () {
   yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_DOC));
   let {inspector, view} = yield openRuleView();
   yield selectNode("div", inspector);
-  yield testShowAndHideMdnTooltip(view);
-});
 
-function* testShowMdnTooltip(view) {
   setBaseCssDocsUrl(URL_ROOT);
 
   info("Setting the popupNode for the MDN docs tooltip");
 
   let {nameSpan} = getRuleViewProperty(view, "element", PROPERTYNAME);
 
   let allMenuItems = openStyleContextMenuAndGetAllItems(view, nameSpan.firstChild);
   let menuitemShowMdnDocs = allMenuItems.find(item => item.label ===
@@ -52,35 +49,13 @@ function* testShowMdnTooltip(view) {
 
   let cssDocs = view.tooltips.cssDocs;
 
   info("Showing the MDN docs tooltip");
   let onShown = cssDocs.tooltip.once("shown");
   menuitemShowMdnDocs.click();
   yield onShown;
   ok(true, "The MDN docs tooltip was shown");
-}
-
-/**
- * Test that:
- *  - the MDN tooltip is shown when we click the context menu item
- *  - the tooltip's contents have been initialized (we don't fully
- *  test this here, as it's fully tested with the tooltip test code)
- *  - the tooltip is hidden when we press Escape
- */
-function* testShowAndHideMdnTooltip(view) {
-  yield testShowMdnTooltip(view);
 
   info("Quick check that the tooltip contents are set");
-  let cssDocs = view.tooltips.cssDocs;
-
-  // FIXME: Remove the comment below when bug 1246896 is fixed.
-  /* eslint-disable mozilla/no-cpows-in-tests */
-  let tooltipDocument = cssDocs.tooltip.content.contentDocument;
-  let h1 = tooltipDocument.getElementById("property-name");
+  let h1 = cssDocs.tooltip.container.querySelector(".mdn-property-name");
   is(h1.textContent, PROPERTYNAME, "The MDN docs tooltip h1 is correct");
-
-  info("Simulate pressing the 'Escape' key");
-  let onHidden = cssDocs.tooltip.once("hidden");
-  EventUtils.sendKey("escape");
-  yield onHidden;
-  ok(true, "The MDN docs tooltip was hidden on pressing 'escape'");
-}
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_css-docs-tooltip_closes-on-escape.js
@@ -0,0 +1,51 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that the CssDocs tooltip of the ruleview can be closed when pressing the Escape
+ * key.
+ */
+
+"use strict";
+
+const {setBaseCssDocsUrl} =
+  require("devtools/client/shared/widgets/MdnDocsWidget");
+
+const PROPERTYNAME = "color";
+
+const TEST_URI = `
+  <html>
+    <body>
+      <div style="color: red">
+        Test "Show MDN Docs" closes on escape
+      </div>
+    </body>
+  </html>
+`;
+
+/**
+ * Test that the tooltip is hidden when we press Escape
+ */
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openRuleView();
+  yield selectNode("div", inspector);
+
+  setBaseCssDocsUrl(URL_ROOT);
+
+  info("Retrieve a valid anchor for the CssDocs tooltip");
+  let {nameSpan} = getRuleViewProperty(view, "element", PROPERTYNAME);
+
+  info("Showing the MDN docs tooltip");
+  let onShown = view.tooltips.cssDocs.tooltip.once("shown");
+  view.tooltips.cssDocs.show(nameSpan, PROPERTYNAME);
+  yield onShown;
+  ok(true, "The MDN docs tooltip was shown");
+
+  info("Simulate pressing the 'Escape' key");
+  let onHidden = view.tooltips.cssDocs.tooltip.once("hidden");
+  EventUtils.sendKey("escape");
+  yield onHidden;
+  ok(true, "The MDN docs tooltip was hidden on pressing 'escape'");
+});
--- a/devtools/client/inspector/shared/style-inspector-overlays.js
+++ b/devtools/client/inspector/shared/style-inspector-overlays.js
@@ -15,19 +15,21 @@
 const {getColor} = require("devtools/client/shared/theme");
 const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
 const {
   getImageDimensions,
   setImageTooltip,
   setBrokenImageTooltip,
 } = require("devtools/client/shared/widgets/tooltip/ImageTooltipHelper");
 const {
+  CssDocsTooltip,
+} = require("devtools/client/shared/widgets/tooltip/CssDocsTooltip");
+const {
   SwatchColorPickerTooltip,
   SwatchCubicBezierTooltip,
-  CssDocsTooltip,
   SwatchFilterTooltip
 } = require("devtools/client/shared/widgets/Tooltip");
 const EventEmitter = require("devtools/shared/event-emitter");
 const promise = require("promise");
 const {Task} = require("devtools/shared/task");
 const Services = require("Services");
 
 const PREF_IMAGE_TOOLTIP_SIZE = "devtools.inspector.imagePreviewTooltipSize";
@@ -271,32 +273,31 @@ TooltipsOverlay.prototype = {
    * Add the tooltips overlay to the view. This will start tracking mouse
    * movements and display tooltips when needed
    */
   addToView: function () {
     if (this._isStarted || this._isDestroyed) {
       return;
     }
 
-    let panelDoc = this.view.inspector.panelDoc;
+    let { toolbox } = this.view.inspector;
 
     // Image, fonts, ... preview tooltip
-    this.previewTooltip = new HTMLTooltip(this.view.inspector.toolbox, {
+    this.previewTooltip = new HTMLTooltip(toolbox, {
       type: "arrow",
       useXulWrapper: true
     });
     this.previewTooltip.startTogglingOnHover(this.view.element,
       this._onPreviewTooltipTargetHover.bind(this));
 
     // MDN CSS help tooltip
-    this.cssDocs = new CssDocsTooltip(panelDoc);
+    this.cssDocs = new CssDocsTooltip(toolbox);
 
     if (this.isRuleView) {
       // Color picker tooltip
-      let { toolbox } = this.view.inspector;
       this.colorPicker = new SwatchColorPickerTooltip(toolbox);
       // Cubic bezier tooltip
       this.cubicBezier = new SwatchCubicBezierTooltip(toolbox);
       // Filter editor tooltip
       this.filterEditor = new SwatchFilterTooltip(toolbox);
     }
 
     this._isStarted = true;
@@ -388,17 +389,17 @@ TooltipsOverlay.prototype = {
       this.colorPicker.hide();
     }
 
     if (this.isRuleView && this.cubicBezier.tooltip.isVisible()) {
       this.cubicBezier.revert();
       this.cubicBezier.hide();
     }
 
-    if (this.isRuleView && this.cssDocs.tooltip.isShown()) {
+    if (this.isRuleView && this.cssDocs.tooltip.isVisible()) {
       this.cssDocs.hide();
     }
 
     if (this.isRuleView && this.filterEditor.tooltip.isVisible()) {
       this.filterEditor.revert();
       this.filterEdtior.hide();
     }
 
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -123,17 +123,16 @@ devtools.jar:
     content/framework/dev-edition-promo/dev-edition-logo.png (framework/dev-edition-promo/dev-edition-logo.png)
     content/inspector/inspector.xul (inspector/inspector.xul)
     content/inspector/inspector.css (inspector/inspector.css)
     content/framework/connect/connect.xhtml (framework/connect/connect.xhtml)
     content/framework/connect/connect.css (framework/connect/connect.css)
     content/framework/connect/connect.js (framework/connect/connect.js)
     content/shared/widgets/graphs-frame.xhtml (shared/widgets/graphs-frame.xhtml)
     content/shared/widgets/cubic-bezier.css (shared/widgets/cubic-bezier.css)
-    content/shared/widgets/mdn-docs-frame.xhtml (shared/widgets/mdn-docs-frame.xhtml)
     content/shared/widgets/mdn-docs.css (shared/widgets/mdn-docs.css)
     content/shared/widgets/filter-widget.css (shared/widgets/filter-widget.css)
     content/shared/widgets/spectrum.css (shared/widgets/spectrum.css)
     content/eyedropper/eyedropper.xul (eyedropper/eyedropper.xul)
     content/eyedropper/crosshairs.css (eyedropper/crosshairs.css)
     content/eyedropper/nocursor.css (eyedropper/nocursor.css)
     content/aboutdebugging/aboutdebugging.xhtml (aboutdebugging/aboutdebugging.xhtml)
     content/aboutdebugging/aboutdebugging.css (aboutdebugging/aboutdebugging.css)
--- a/devtools/client/shared/test/browser_mdn-docs-01.js
+++ b/devtools/client/shared/test/browser_mdn-docs-01.js
@@ -15,22 +15,18 @@
  *
  * In this file we test:
  * - the initial state of the document before the docs have loaded
  * - the state of the document after the docs have loaded
  */
 
 "use strict";
 
-const {CssDocsTooltip} = require("devtools/client/shared/widgets/Tooltip");
 const {setBaseCssDocsUrl, MdnDocsWidget} = require("devtools/client/shared/widgets/MdnDocsWidget");
 
-// frame to load the tooltip into
-const MDN_DOCS_TOOLTIP_FRAME = "chrome://devtools/content/shared/widgets/mdn-docs-frame.xhtml";
-
 /**
  * Test properties
  *
  * In the real tooltip, a CSS property name is used to look up an MDN page
  * for that property.
  * In the test code, the names defined here is used to look up a page
  * served by the test server.
  */
@@ -40,24 +36,26 @@ const BASIC_EXPECTED_SUMMARY = "A summar
 const BASIC_EXPECTED_SYNTAX = [{type: "comment", text: "/* The part we want   */"},
                                {type: "text", text: "\n"},
                                {type: "property-name", text: "this"},
                                {type: "text", text: ":"},
                                {type: "text", text: " "},
                                {type: "property-value", text: "is-the-part-we-want"},
                                {type: "text", text: ";"}];
 
-const URI_PARAMS = "?utm_source=mozilla&utm_medium=firefox-inspector&utm_campaign=default";
+const URI_PARAMS =
+  "?utm_source=mozilla&utm_medium=firefox-inspector&utm_campaign=default";
 
 add_task(function* () {
   setBaseCssDocsUrl(TEST_URI_ROOT);
 
   yield addTab("about:blank");
-  let [host, win, doc] = yield createHost("bottom", MDN_DOCS_TOOLTIP_FRAME);
-  let widget = new MdnDocsWidget(win.document);
+  let [host, win] = yield createHost("bottom", "data:text/html," +
+    "<div class='mdn-container'></div>");
+  let widget = new MdnDocsWidget(win.document.querySelector("div"));
 
   yield testTheBasics(widget);
 
   host.destroy();
   gBrowser.removeCurrentTab();
 });
 
 /**
--- a/devtools/client/shared/test/browser_mdn-docs-02.js
+++ b/devtools/client/shared/test/browser_mdn-docs-02.js
@@ -16,21 +16,20 @@
  * error conditions like parts of the document being missing.
  *
  * We also test that the tooltip properly handles the case where the page
  * doesn't exist at all.
  */
 
 "use strict";
 
-const {CssDocsTooltip} = require("devtools/client/shared/widgets/Tooltip");
-const {setBaseCssDocsUrl, MdnDocsWidget} = require("devtools/client/shared/widgets/MdnDocsWidget");
-
-// frame to load the tooltip into
-const MDN_DOCS_TOOLTIP_FRAME = "chrome://devtools/content/shared/widgets/mdn-docs-frame.xhtml";
+const {
+  setBaseCssDocsUrl,
+  MdnDocsWidget
+} = require("devtools/client/shared/widgets/MdnDocsWidget");
 
 const BASIC_EXPECTED_SUMMARY = "A summary of the property.";
 const BASIC_EXPECTED_SYNTAX = [{type: "comment", text: "/* The part we want   */"},
                                {type: "text", text: "\n"},
                                {type: "property-name", text: "this"},
                                {type: "text", text: ":"},
                                {type: "text", text: " "},
                                {type: "property-value", text: "is-the-part-we-want"},
@@ -95,43 +94,33 @@ const TEST_DATA = [{
   }
 }
 ];
 
 add_task(function* () {
   setBaseCssDocsUrl(TEST_URI_ROOT);
 
   yield addTab("about:blank");
-  let [host, win, doc] = yield createHost("bottom", MDN_DOCS_TOOLTIP_FRAME);
-  let widget = new MdnDocsWidget(win.document);
+  let [host, win] = yield createHost("bottom", "data:text/html," +
+    "<div class='mdn-container'></div>");
+  let widget = new MdnDocsWidget(win.document.querySelector("div"));
 
   for (let {desc, docsPageUrl, expectedContents} of TEST_DATA) {
     info(desc);
     yield widget.loadCssDocs(docsPageUrl);
     checkTooltipContents(widget.elements, expectedContents);
   }
   host.destroy();
   gBrowser.removeCurrentTab();
 });
 
-function* testNonExistentPage(widget) {
-  info("Test a property for which we don't have a page");
-  yield widget.loadCssDocs("i-dont-exist.html");
-  checkTooltipContents(widget.elements, {
-    propertyName: "i-dont-exist.html",
-    summary: ERROR_MESSAGE,
-    syntax: ""
-  });
-}
-
 /*
  * Utility function to check content of the tooltip.
  */
 function checkTooltipContents(doc, expected) {
-
   is(doc.heading.textContent,
      expected.propertyName,
      "Property name is correct");
 
   is(doc.summary.textContent,
      expected.summary,
      "Summary is correct");
 
--- a/devtools/client/shared/widgets/MdnDocsWidget.js
+++ b/devtools/client/shared/widgets/MdnDocsWidget.js
@@ -24,16 +24,18 @@
 
 "use strict";
 
 const Services = require("Services");
 const defer = require("devtools/shared/defer");
 const {getCSSLexer} = require("devtools/shared/css-lexer");
 const {gDevTools} = require("devtools/client/framework/devtools");
 
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
 // Parameters for the XHR request
 // see https://developer.mozilla.org/en-US/docs/MDN/Kuma/API#Document_parameters
 const XHR_PARAMS = "?raw&macros";
 // URL for the XHR request
 var XHR_CSS_URL = "https://developer.mozilla.org/en-US/docs/Web/CSS/";
 
 // Parameters for the link to MDN in the tooltip, so
 // so we know which MDN visits come from this feature
@@ -82,17 +84,17 @@ function appendSyntaxHighlightedCSS(cssT
   let doc = parentElement.ownerDocument;
   let identClass = PROPERTY_NAME_COLOR;
   let lexer = getCSSLexer(cssText);
 
   /**
    * Create a SPAN node with the given text content and class.
    */
   function createStyledNode(textContent, className) {
-    let newNode = doc.createElement("span");
+    let newNode = doc.createElementNS(XHTML_NS, "span");
     newNode.classList.add(className);
     newNode.textContent = textContent;
     return newNode;
   }
 
   /**
    * If the symbol is ":", we will expect the next
    * "ident" token to be part of a property value.
@@ -219,42 +221,49 @@ function getCssDocs(cssProperty) {
 
   return deferred.promise;
 }
 
 exports.getCssDocs = getCssDocs;
 
 /**
  * The MdnDocsWidget is used by tooltip code that needs to display docs
- * from MDN in a tooltip. The tooltip code loads a document that contains the
- * basic structure of a docs tooltip (loaded from mdn-docs-frame.xhtml),
- * and passes this document into the widget's constructor.
+ * from MDN in a tooltip.
  *
  * In the constructor, the widget does some general setup that's not
  * dependent on the particular item we need docs for.
  *
  * After that, when the tooltip code needs to display docs for an item, it
  * asks the widget to retrieve the docs and update the document with them.
  *
- * @param {Document} tooltipDocument
- * A DOM document. The widget expects the document to have a particular
- * structure.
+ * @param {Element} tooltipContainer
+ * A DOM element where the MdnDocs widget markup should be created.
  */
-function MdnDocsWidget(tooltipDocument) {
+function MdnDocsWidget(tooltipContainer) {
+  tooltipContainer.innerHTML =
+    `<header>
+       <h1 class="mdn-property-name theme-fg-color5"></h1>
+     </header>
+     <div class="mdn-property-info">
+       <div class="mdn-summary"></div>
+       <pre class="mdn-syntax devtools-monospace"></pre>
+     </div>
+     <footer>
+       <a class="mdn-visit-page theme-link" href="#">Visit MDN (placeholder)</a>
+     </footer>`;
+
   // fetch all the bits of the document that we will manipulate later
   this.elements = {
-    heading: tooltipDocument.getElementById("property-name"),
-    summary: tooltipDocument.getElementById("summary"),
-    syntax: tooltipDocument.getElementById("syntax"),
-    info: tooltipDocument.getElementById("property-info"),
-    linkToMdn: tooltipDocument.getElementById("visit-mdn-page")
+    heading: tooltipContainer.querySelector(".mdn-property-name"),
+    summary: tooltipContainer.querySelector(".mdn-summary"),
+    syntax: tooltipContainer.querySelector(".mdn-syntax"),
+    info: tooltipContainer.querySelector(".mdn-property-info"),
+    linkToMdn: tooltipContainer.querySelector(".mdn-visit-page")
   };
 
-  this.doc = tooltipDocument;
-
   // get the localized string for the link text
   this.elements.linkToMdn.textContent =
     l10n.strings.GetStringFromName("docsTooltip.visitMDN");
 
   // listen for clicks and open in the browser window instead
   let mainWindow = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
   this.elements.linkToMdn.addEventListener("click", function (e) {
     e.stopPropagation();
@@ -352,17 +361,16 @@ MdnDocsWidget.prototype = {
     initializeDocument(propertyName);
     getCssDocs(propertyName).then(finalizeDocument, gotError);
 
     return deferred.promise;
   },
 
   destroy: function () {
     this.elements = null;
-    this.doc = null;
   }
 };
 
 /**
  * L10N utility class
  */
 function L10N() {}
 L10N.prototype = {};
--- a/devtools/client/shared/widgets/Tooltip.js
+++ b/devtools/client/shared/widgets/Tooltip.js
@@ -4,17 +4,16 @@
 
 "use strict";
 
 const {Ci} = require("chrome");
 const defer = require("devtools/shared/defer");
 const {Spectrum} = require("devtools/client/shared/widgets/Spectrum");
 const {CubicBezierWidget} =
       require("devtools/client/shared/widgets/CubicBezierWidget");
-const {MdnDocsWidget} = require("devtools/client/shared/widgets/MdnDocsWidget");
 const {CSSFilterEditorWidget} = require("devtools/client/shared/widgets/FilterWidget");
 const {TooltipToggle} = require("devtools/client/shared/widgets/tooltip/TooltipToggle");
 const EventEmitter = require("devtools/shared/event-emitter");
 const {colorUtils} = require("devtools/client/shared/css-color");
 const Heritage = require("sdk/core/heritage");
 const {Eyedropper} = require("devtools/client/eyedropper/eyedropper");
 const {gDevTools} = require("devtools/client/framework/devtools");
 const Services = require("Services");
@@ -28,19 +27,17 @@ loader.lazyRequireGetter(this, "clearNam
 loader.lazyRequireGetter(this, "setNamedTimeout", "devtools/client/shared/widgets/view-helpers", true);
 
 XPCOMUtils.defineLazyModuleGetter(this, "VariablesView",
   "resource://devtools/client/shared/widgets/VariablesView.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "VariablesViewController",
   "resource://devtools/client/shared/widgets/VariablesViewController.jsm");
 
 const XHTML_NS = "http://www.w3.org/1999/xhtml";
-const MDN_DOCS_FRAME = "chrome://devtools/content/shared/widgets/mdn-docs-frame.xhtml";
 const ESCAPE_KEYCODE = Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE;
-const RETURN_KEYCODE = Ci.nsIDOMKeyEvent.DOM_VK_RETURN;
 const POPUP_EVENTS = ["shown", "hidden", "showing", "hiding"];
 
 /**
  * Tooltip widget.
  *
  * This widget is intended at any tool that may need to show rich content in the
  * form of floating panels.
  * A common use case is image previewing in the CSS rule view, but more complex
@@ -548,42 +545,16 @@ Tooltip.prototype = {
 
     // load the document from url into the iframe
     iframe.setAttribute("src", url);
 
     // Put the iframe in the tooltip
     this.content = iframe;
 
     return def.promise;
-  },
-
-  /**
-   * Set the content of this tooltip to the MDN docs widget.
-   *
-   * This is called when the tooltip is first constructed.
-   *
-   * @return {promise} A promise which is resolved with an MdnDocsWidget.
-   *
-   * It loads the tooltip's structure from a separate XHTML file
-   * into an iframe. When the iframe is loaded it constructs
-   * an MdnDocsWidget and passes that into resolve.
-   *
-   * The caller can use the MdnDocsWidget to update the tooltip's
-   * UI with new content each time the tooltip is shown.
-   */
-  setMdnDocsContent: function () {
-    let dimensions = {width: "410", height: "300"};
-    return this.setIFrameContent(dimensions, MDN_DOCS_FRAME).then(onLoaded);
-
-    function onLoaded(iframe) {
-      let win = iframe.contentWindow.wrappedJSObject;
-      // create an MdnDocsWidget, initializing it with the content document
-      let widget = new MdnDocsWidget(win.document);
-      return widget;
-    }
   }
 };
 
 /**
  * Base class for all (color, gradient, ...)-swatch based value editors inside
  * tooltips
  *
  * @param {Toolbox} toolbox
@@ -1000,55 +971,16 @@ Heritage.extend(SwatchBasedEditorTooltip
     this.widget.then(widget => {
       widget.off("updated", this._onUpdate);
       widget.destroy();
     });
   }
 });
 
 /**
- * Tooltip for displaying docs for CSS properties from MDN.
- *
- * @param {XULDocument} doc
- */
-function CssDocsTooltip(doc) {
-  this.tooltip = new Tooltip(doc, {
-    consumeOutsideClick: true,
-    closeOnKeys: [ESCAPE_KEYCODE, RETURN_KEYCODE],
-    noAutoFocus: false
-  });
-  this.widget = this.tooltip.setMdnDocsContent();
-}
-
-module.exports.CssDocsTooltip = CssDocsTooltip;
-
-CssDocsTooltip.prototype = {
-  /**
-   * Load CSS docs for the given property,
-   * then display the tooltip.
-   */
-  show: function (anchor, propertyName) {
-    function loadCssDocs(widget) {
-      return widget.loadCssDocs(propertyName);
-    }
-
-    this.widget.then(loadCssDocs);
-    this.tooltip.show(anchor, "topcenter bottomleft");
-  },
-
-  hide: function () {
-    this.tooltip.hide();
-  },
-
-  destroy: function () {
-    this.tooltip.destroy();
-  }
-};
-
-/**
  * The swatch-based css filter tooltip class is a specific class meant to be
  * used along with rule-view's generated css filter swatches.
  * It extends the parent SwatchBasedEditorTooltip class.
  * It just wraps a standard Tooltip and sets its content with an instance of a
  * CSSFilterEditorWidget.
  *
  * @param {Toolbox} toolbox
  *        The devtools toolbox, needed to get the devtools main window.
deleted file mode 100644
--- a/devtools/client/shared/widgets/mdn-docs-frame.xhtml
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 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/. -->
-<!DOCTYPE html>
-
-<html xmlns="http://www.w3.org/1999/xhtml">
-<head>
-  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
-  <link rel="stylesheet" href="chrome://devtools/content/shared/widgets/mdn-docs.css" type="text/css"/>
-  <script type="application/javascript;version=1.8" src="chrome://devtools/content/shared/theme-switching.js"/>
-</head>
-<body class="theme-body">
-
-  <div id = "container">
-
-    <header>
-      <h1 id="property-name" class="theme-fg-color5"></h1>
-    </header>
-
-    <div id="property-info">
-      <div id="summary"></div>
-      <pre id="syntax" class="devtools-monospace"></pre>
-    </div>
-
-    <footer>
-      <a id="visit-mdn-page" class="theme-link" href="#">Visit MDN (placeholder)</a>
-    </footer>
-
-  </div>
-
-</body>
-</html>
\ No newline at end of file
--- a/devtools/client/shared/widgets/mdn-docs.css
+++ b/devtools/client/shared/widgets/mdn-docs.css
@@ -1,41 +1,39 @@
 /* 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/. */
 
-#visit-mdn-page {
-  display: inline-block;
-  padding: 1em 0;
-}
-
-html, body, #container {
-  height: 100%;
-  width: 100%;
-  margin: 0;
-  padding: 0;
-}
-
-#container {
+.mdn-container {
+  height: 300px;
+  margin: 4px;
+  overflow: auto;
+  box-sizing: border-box;
   display: flex;
   flex-direction: column;
 }
 
-header, footer {
+.mdn-container header,
+.mdn-container footer {
   flex: 1;
   padding: 0 1em;
 }
 
-#property-info {
+.mdn-property-info {
   flex: 10;
   padding: 0 1em;
   overflow: auto;
   transition: opacity 400ms ease-in;
 }
 
-#syntax {
+.mdn-syntax {
   margin-top: 1em;
 }
 
 .devtools-throbber {
+  align-self: center;
   opacity: 0;
-  align-self: center;
 }
+
+.mdn-visit-page {
+  display: inline-block;
+  padding: 1em 0;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/widgets/tooltip/CssDocsTooltip.js
@@ -0,0 +1,95 @@
+/* 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 {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
+const {MdnDocsWidget} = require("devtools/client/shared/widgets/MdnDocsWidget");
+const XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+loader.lazyRequireGetter(this, "KeyShortcuts",
+  "devtools/client/shared/key-shortcuts", true);
+
+const TOOLTIP_WIDTH = 418;
+const TOOLTIP_HEIGHT = 308;
+
+/**
+ * Tooltip for displaying docs for CSS properties from MDN.
+ *
+ * @param {Toolbox} toolbox
+ *        Toolbox used to create the tooltip.
+ */
+function CssDocsTooltip(toolbox) {
+  this.tooltip = new HTMLTooltip(toolbox, {
+    type: "arrow",
+    consumeOutsideClicks: true,
+    autofocus: true,
+    useXulWrapper: true,
+    stylesheet: "chrome://devtools/content/shared/widgets/mdn-docs.css",
+  });
+  this.widget = this.setMdnDocsContent();
+
+  // Initialize keyboard shortcuts
+  this.shortcuts = new KeyShortcuts({ window: toolbox.doc.defaultView });
+  this._onShortcut = this._onShortcut.bind(this);
+
+  this.shortcuts.on("Escape", this._onShortcut);
+  this.shortcuts.on("Return", this._onShortcut);
+}
+
+module.exports.CssDocsTooltip = CssDocsTooltip;
+
+CssDocsTooltip.prototype = {
+  /**
+   * Load CSS docs for the given property,
+   * then display the tooltip.
+   */
+  show: function (anchor, propertyName) {
+    this.tooltip.once("shown", () => {
+      this.widget.loadCssDocs(propertyName);
+    });
+    this.tooltip.show(anchor);
+  },
+
+  hide: function () {
+    this.tooltip.hide();
+  },
+
+  _onShortcut: function (shortcut, event) {
+    if (!this.tooltip.isVisible()) {
+      return;
+    }
+
+    event.stopPropagation();
+    if (shortcut === "Return") {
+      // If user is pressing return, do not prevent default and delay hiding the tooltip
+      // in case the focus is on the "Visit MDN page" link.
+      this.tooltip.doc.defaultView.setTimeout(this.hide.bind(this), 0);
+    } else {
+      // For any other key, preventDefault() and hide straight away.
+      event.preventDefault();
+      this.hide();
+    }
+  },
+
+  /**
+   * Set the content of this tooltip to the MDN docs widget. This is called when the
+   * tooltip is first constructed.
+   * The caller can use the MdnDocsWidget to update the tooltip's  UI with new content
+   * each time the tooltip is shown.
+   *
+   * @return {MdnDocsWidget} the created MdnDocsWidget instance.
+   */
+  setMdnDocsContent: function () {
+    let container = this.tooltip.doc.createElementNS(XHTML_NS, "div");
+    container.setAttribute("class", "mdn-container theme-body");
+    this.tooltip.setContent(container, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT});
+    return new MdnDocsWidget(container);
+  },
+
+  destroy: function () {
+    this.shortcuts.destroy();
+    this.tooltip.destroy();
+  }
+};
--- a/devtools/client/shared/widgets/tooltip/moz.build
+++ b/devtools/client/shared/widgets/tooltip/moz.build
@@ -1,11 +1,12 @@
 # -*- Mode: python; c-basic-offset: 4; 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(
+    'CssDocsTooltip.js',
     'EventTooltipHelper.js',
     'ImageTooltipHelper.js',
     'TooltipToggle.js',
 )