Bug 1266450 - part7: fix html tooltip autofocus behavior;r=bgrins draft
authorJulian Descottes <jdescottes@mozilla.com>
Wed, 08 Jun 2016 13:32:15 +0200
changeset 376623 26334504e7865e51915d2ec867d5d364b4f00ea7
parent 376622 fcee58928b2f281acc5da3e94cff8f02a1cf83bf
child 523199 5afbd2500330e2838500b25bcd8e1d9eeef7055e
push id20630
push userjdescottes@mozilla.com
push dateWed, 08 Jun 2016 11:49:53 +0000
reviewersbgrins
bugs1266450
milestone50.0a1
Bug 1266450 - part7: fix html tooltip autofocus behavior;r=bgrins For autofocus tooltips, we need to find a focusable item in order to call focus() now that the tooltip content lives in the same document as the toolbox. Updated the corresponding test and made some superficial changes to HTMLTooltip.js. MozReview-Commit-ID: L61eIxgFm3d
devtools/client/shared/test/browser_html_tooltip-03.js
devtools/client/shared/widgets/HTMLTooltip.js
--- a/devtools/client/shared/test/browser_html_tooltip-03.js
+++ b/devtools/client/shared/test/browser_html_tooltip-03.js
@@ -37,83 +37,68 @@ add_task(function* () {
 
   yield testNoAutoFocus(doc);
   yield testAutoFocus(doc);
   yield testAutoFocusPreservesFocusChange(doc);
 });
 
 function* testNoAutoFocus(doc) {
   yield focusNode(doc, "#box4-input");
-  is(getFocusedDocument(doc), doc, "Focus is in the XUL document");
+  ok(doc.activeElement.closest("#box4-input"), "Focus is in the #box4-input");
 
   info("Test a tooltip without autofocus will not take focus");
   let tooltip = yield createTooltip(doc, false);
 
   yield showTooltip(tooltip, doc.getElementById("box1"));
-  is(getFocusedDocument(doc), doc, "Focus is still in the XUL document");
-  ok(doc.activeElement.closest("#box4-input"), "Focus is in the #box4-input");
+  ok(doc.activeElement.closest("#box4-input"), "Focus is still in the #box4-input");
 
   yield hideTooltip(tooltip);
   yield blurNode(doc, "#box4-input");
 }
 
 function* testAutoFocus(doc) {
   yield focusNode(doc, "#box4-input");
-  is(getFocusedDocument(doc), doc, "Focus is in the XUL document");
+  ok(doc.activeElement.closest("#box4-input"), "Focus is in the #box4-input");
 
   info("Test autofocus tooltip takes focus when displayed, " +
     "and restores the focus when hidden");
   let tooltip = yield createTooltip(doc, true);
 
   yield showTooltip(tooltip, doc.getElementById("box1"));
-  is(getFocusedDocument(doc), tooltip.panel.ownerDocument,
-    "Focus is in the tooltip document");
+  ok(doc.activeElement.closest(".tooltip-content"), "Focus is in the tooltip");
 
   yield hideTooltip(tooltip);
-  is(getFocusedDocument(doc), doc, "Focus is back in the XUL document");
   ok(doc.activeElement.closest("#box4-input"), "Focus is in the #box4-input");
 
   info("Blur the textbox before moving to the next test to reset the state.");
   yield blurNode(doc, "#box4-input");
 }
 
 function* testAutoFocusPreservesFocusChange(doc) {
   yield focusNode(doc, "#box4-input");
-  is(getFocusedDocument(doc), doc, "Focus is in the XUL document");
+  ok(doc.activeElement.closest("#box4-input"), "Focus is still in the #box3-input");
 
   info("Test autofocus tooltip takes focus when displayed, " +
     "but does not try to restore the active element if it is not focused when hidden");
   let tooltip = yield createTooltip(doc, true);
 
   yield showTooltip(tooltip, doc.getElementById("box1"));
-  is(getFocusedDocument(doc), tooltip.panel.ownerDocument,
-    "Focus is in the tooltip document");
+  ok(doc.activeElement.closest(".tooltip-content"), "Focus is in the tooltip");
 
   info("Move the focus to #box3-input while the tooltip is displayed");
   yield focusNode(doc, "#box3-input");
-  is(getFocusedDocument(doc), doc, "Focus is back in the XUL document");
-  ok(doc.activeElement.closest("#box3-input"), "Focus is in the #box3-input");
+  ok(doc.activeElement.closest("#box3-input"), "Focus moved to the #box3-input");
 
   yield hideTooltip(tooltip);
-  is(getFocusedDocument(doc), doc, "Focus is still in the XUL document");
-
   ok(doc.activeElement.closest("#box3-input"), "Focus is still in the #box3-input");
 
   info("Blur the textbox before moving to the next test to reset the state.");
   yield blurNode(doc, "#box3-input");
 }
 
-function getFocusedDocument(doc) {
-  let activeElement = doc.activeElement;
-  while (activeElement && activeElement.contentDocument) {
-    activeElement = activeElement.contentDocument.activeElement;
-  }
-  return activeElement.ownerDocument;
-}
-
 /**
  * Fpcus the node corresponding to the provided selector in the provided document. Returns
  * a promise that will resolve when receiving the focus event on the node.
  */
 function focusNode(doc, selector) {
   let node = doc.querySelector(selector);
   let onFocus = once(node, "focus");
   node.focus();
@@ -138,12 +123,15 @@ function blurNode(doc, selector) {
  *        Document in which the tooltip should be created
  * @param {Boolean} autofocus
  * @return {Promise} promise that will resolve the HTMLTooltip instance created when the
  *         tooltip content will be ready.
  */
 function* createTooltip(doc, autofocus) {
   let tooltip = new HTMLTooltip({doc}, {autofocus});
   let div = doc.createElementNS(HTML_NS, "div");
+  div.classList.add("tooltip-content");
   div.style.height = "50px";
+  div.innerHTML = '<input type="text"></input>';
+
   yield tooltip.setContent(div, 150, 50);
   return tooltip;
 }
--- a/devtools/client/shared/widgets/HTMLTooltip.js
+++ b/devtools/client/shared/widgets/HTMLTooltip.js
@@ -154,42 +154,44 @@ HTMLTooltip.prototype = {
     this.container.style.left = computedPosition.left + "px";
 
     if (this.type === TYPE.ARROW) {
       this.arrow.style.left = computedPosition.arrowLeft + "px";
     }
 
     this.container.classList.add("tooltip-visible");
 
+    // Keep a pointer on the focused element to refocus it when hiding the tooltip.
+    this._focusedElement = this.doc.activeElement;
+
     this.attachEventsTimer = this.doc.defaultView.setTimeout(() => {
-	  this._focusedElement = this.doc.activeElement;
-      if (this.autofocus) {
-        this.panel.focus();
-      }
+      this._maybeFocusTooltip();
       this.topWindow.addEventListener("click", this._onClick, true);
       this.emit("shown");
     }, 0);
   },
 
   /**
    * Hide the current tooltip. The event "hidden" will be fired when the tooltip
    * is hidden.
    */
   hide: function () {
     this.doc.defaultView.clearTimeout(this.attachEventsTimer);
+    if (!this.isVisible()) {
+      return;
+    }
 
-    if (this.isVisible()) {
-      this.topWindow.removeEventListener("click", this._onClick, true);
-      this.container.classList.remove("tooltip-visible");
-      this.emit("hidden");
+    this.topWindow.removeEventListener("click", this._onClick, true);
+    this.container.classList.remove("tooltip-visible");
+    this.emit("hidden");
 
-      if (this.container.contains(this.doc.activeElement) && this._focusedElement) {
-        this._focusedElement.focus();
-        this._focusedElement = null;
-      }
+    let tooltipHasFocus = this.container.contains(this.doc.activeElement);
+    if (tooltipHasFocus && this._focusedElement) {
+      this._focusedElement.focus();
+      this._focusedElement = null;
     }
   },
 
   /**
    * Check if the tooltip is currently displayed.
    * @return {Boolean} true if the tooltip is visible
    */
   isVisible: function () {
@@ -228,30 +230,30 @@ HTMLTooltip.prototype = {
     this.hide();
     if (this.consumeOutsideClicks) {
       e.preventDefault();
       e.stopPropagation();
     }
   },
 
   _isInTooltipContainer: function (node) {
-    let tooltipWindow = this.panel.ownerDocument.defaultView;
-    let win = node.ownerDocument.defaultView;
-
+    // Check if the target is the tooltip arrow.
     if (this.arrow && this.arrow === node) {
       return true;
     }
 
+    let tooltipWindow = this.panel.ownerDocument.defaultView;
+    let win = node.ownerDocument.defaultView;
+
+    // Check if the tooltip panel contains the node if they live in the same document.
     if (win === tooltipWindow) {
-      // If node is in the same window as the tooltip, check if the tooltip panel
-      // contains node.
       return this.panel.contains(node);
     }
 
-    // Otherwise check if the node window is in the tooltip container.
+    // Check if the node window is in the tooltip container.
     while (win.parent && win.parent != win) {
       win = win.parent;
       if (win === tooltipWindow) {
         return this.panel.contains(win.frameElement);
       }
     }
 
     return false;
@@ -348,12 +350,29 @@ HTMLTooltip.prototype = {
 
     // Compute right and bottom coordinates using the rest of the data.
     let right = left + width;
     let bottom = top + height;
 
     return {top, right, bottom, left, width, height};
   },
 
+  /**
+   * Check if the tooltip's owner document is a XUL document.
+   */
   _isXUL: function () {
     return this.doc.documentElement.namespaceURI === XUL_NS;
   },
+
+  /**
+   * If the tootlip is configured to autofocus and a focusable element can be found,
+   * focus it.
+   */
+  _maybeFocusTooltip: function () {
+    // Simplied selector targetting elements that can receive the focus, full version at
+    // http://stackoverflow.com/questions/1599660/which-html-elements-can-receive-focus .
+    let focusableSelector = "a, button, iframe, input, select, textarea";
+    let focusableElement = this.panel.querySelector(focusableSelector);
+    if (this.autofocus && focusableElement) {
+      focusableElement.focus();
+    }
+  },
 };