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
--- 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();
+ }
+ },
};