Bug 1286523 - fix autocomplete popup position in RTL locales;r=bgrins
authorJulian Descottes <jdescottes@mozilla.com>
Wed, 13 Jul 2016 18:52:37 +0200
changeset 305310 bc547f508f58108db8b9ad9c2b94f130c07de194
parent 305309 124641c7e85d0090330c6732098fe65427073fcb
child 305311 9d95b275e2cf97f87d313ec24cf0e52409d6fc46
child 305482 59b689bc8972abc6692edeecd4dba8e536f18cc7
push id30459
push usercbook@mozilla.com
push dateMon, 18 Jul 2016 15:07:13 +0000
treeherdermozilla-central@9d95b275e2cf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1286523
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 1286523 - fix autocomplete popup position in RTL locales;r=bgrins MozReview-Commit-ID: HRC1ialFBVj
devtools/client/shared/test/browser.ini
devtools/client/shared/test/browser_html_tooltip_rtl.js
devtools/client/shared/widgets/HTMLTooltip.js
--- a/devtools/client/shared/test/browser.ini
+++ b/devtools/client/shared/test/browser.ini
@@ -117,16 +117,17 @@ skip-if = e10s # Bug 1221911, bug 122228
 [browser_html_tooltip-02.js]
 [browser_html_tooltip-03.js]
 [browser_html_tooltip-04.js]
 [browser_html_tooltip-05.js]
 [browser_html_tooltip_arrow-01.js]
 [browser_html_tooltip_arrow-02.js]
 [browser_html_tooltip_consecutive-show.js]
 [browser_html_tooltip_offset.js]
+[browser_html_tooltip_rtl.js]
 [browser_html_tooltip_variable-height.js]
 [browser_html_tooltip_width-auto.js]
 [browser_html_tooltip_xul-wrapper.js]
 [browser_inplace-editor-01.js]
 [browser_inplace-editor-02.js]
 [browser_inplace-editor_autocomplete_01.js]
 [browser_inplace-editor_autocomplete_02.js]
 [browser_inplace-editor_autocomplete_offset.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/test/browser_html_tooltip_rtl.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_html_tooltip.js */
+"use strict";
+
+/**
+ * Test the HTMLTooltip anchor alignment changes with the anchor direction.
+ * - should be aligned to the right of RTL anchors
+ * - should be aligned to the left of LTR anchors
+ */
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const TEST_URI = `data:text/xml;charset=UTF-8,<?xml version="1.0"?>
+  <?xml-stylesheet href="chrome://global/skin/global.css"?>
+  <?xml-stylesheet href="chrome://devtools/skin/tooltips.css"?>
+  <window
+    xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+    htmlns="http://www.w3.org/1999/xhtml"
+    title="Tooltip test">
+    <hbox style="padding: 90px 0;" flex="1">
+      <hbox id="box1" flex="1" style="background:red; direction: rtl;">test1</hbox>
+      <hbox id="box2" flex="1" style="background:blue; direction: rtl;">test2</hbox>
+      <hbox id="box3" flex="1" style="background:red; direction: ltr;">test3</hbox>
+      <hbox id="box4" flex="1" style="background:blue; direction: ltr;">test4</hbox>
+    </hbox>
+  </window>`;
+
+const {HTMLTooltip} = require("devtools/client/shared/widgets/HTMLTooltip");
+loadHelperScript("helper_html_tooltip.js");
+
+const TOOLBOX_WIDTH = 500;
+const TOOLTIP_WIDTH = 150;
+const TOOLTIP_HEIGHT = 30;
+
+add_task(function* () {
+  // Force the toolbox to be 500px wide (min width is 465px);
+  yield pushPref("devtools.toolbox.sidebar.width", TOOLBOX_WIDTH);
+
+  let [,, doc] = yield createHost("side", TEST_URI);
+
+  info("Test a tooltip is not closed when clicking inside itself");
+
+  let tooltip = new HTMLTooltip({doc}, {useXulWrapper: false});
+  let div = doc.createElementNS(HTML_NS, "div");
+  div.textContent = "tooltip";
+  div.style.cssText = "box-sizing: border-box; border: 1px solid black";
+  tooltip.setContent(div, {width: TOOLTIP_WIDTH, height: TOOLTIP_HEIGHT});
+
+  yield testRtlAnchors(doc, tooltip);
+  yield testLtrAnchors(doc, tooltip);
+  yield hideTooltip(tooltip);
+
+  tooltip.destroy();
+});
+
+function* testRtlAnchors(doc, tooltip) {
+  /*
+   * The layout of the test page is as follows:
+   *   _______________________________
+   *  | toolbox                       |
+   *  | _____   _____   _____   _____ |
+   *  ||     | |     | |     | |     ||
+   *  || box1| | box2| | box3| | box4||
+   *  ||_____| |_____| |_____| |_____||
+   *  |_______________________________|
+   *
+   * - box1 is aligned with the left edge of the toolbox
+   * - box2 is displayed right after box1
+   * - total toolbox width is 500px so each box is 125px wide
+  */
+
+  let box1 = doc.getElementById("box1");
+  let box2 = doc.getElementById("box2");
+
+  info("Display the tooltip on box1.");
+  yield showTooltip(tooltip, box1, {position: "bottom"});
+
+  let panelRect = tooltip.container.getBoundingClientRect();
+  let anchorRect = box1.getBoundingClientRect();
+
+  // box1 uses RTL direction, so the tooltip should be aligned with the right edge of the
+  // anchor, but it is shifted to the right to fit in the toolbox.
+  is(panelRect.left, 0, "Tooltip is aligned with left edge of the toolbox");
+  is(panelRect.top, anchorRect.bottom, "Tooltip aligned with the anchor bottom edge");
+  is(panelRect.height, TOOLTIP_HEIGHT, "Tooltip height is at 100px as expected");
+
+  info("Display the tooltip on box2.");
+  yield showTooltip(tooltip, box2, {position: "bottom"});
+
+  panelRect = tooltip.container.getBoundingClientRect();
+  anchorRect = box2.getBoundingClientRect();
+
+  // box2 uses RTL direction, so the tooltip is aligned with the right edge of the anchor
+  is(panelRect.right, anchorRect.right, "Tooltip is aligned with right edge of anchor");
+  is(panelRect.top, anchorRect.bottom, "Tooltip aligned with the anchor bottom edge");
+  is(panelRect.height, TOOLTIP_HEIGHT, "Tooltip height is at 100px as expected");
+}
+
+function* testLtrAnchors(doc, tooltip) {
+    /*
+   * The layout of the test page is as follows:
+   *   _______________________________
+   *  | toolbox                       |
+   *  | _____   _____   _____   _____ |
+   *  ||     | |     | |     | |     ||
+   *  || box1| | box2| | box3| | box4||
+   *  ||_____| |_____| |_____| |_____||
+   *  |_______________________________|
+   *
+   * - box3 is is displayed right after box2
+   * - box4 is aligned with the right edge of the toolbox
+   * - total toolbox width is 500px so each box is 125px wide
+  */
+
+  let box3 = doc.getElementById("box3");
+  let box4 = doc.getElementById("box4");
+
+  info("Display the tooltip on box3.");
+  yield showTooltip(tooltip, box3, {position: "bottom"});
+
+  let panelRect = tooltip.container.getBoundingClientRect();
+  let anchorRect = box3.getBoundingClientRect();
+
+  // box3 uses LTR direction, so the tooltip is aligned with the left edge of the anchor.
+  is(panelRect.left, anchorRect.left, "Tooltip is aligned with left edge of anchor");
+  is(panelRect.top, anchorRect.bottom, "Tooltip aligned with the anchor bottom edge");
+  is(panelRect.height, TOOLTIP_HEIGHT, "Tooltip height is at 100px as expected");
+
+  info("Display the tooltip on box4.");
+  yield showTooltip(tooltip, box4, {position: "bottom"});
+
+  panelRect = tooltip.container.getBoundingClientRect();
+  anchorRect = box4.getBoundingClientRect();
+
+  // box4 uses LTR direction, so the tooltip should be aligned with the left edge of the
+  // anchor, but it is shifted to the left to fit in the toolbox.
+  is(panelRect.right, TOOLBOX_WIDTH, "Tooltip is aligned with right edge of toolbox");
+  is(panelRect.top, anchorRect.bottom, "Tooltip aligned with the anchor bottom edge");
+  is(panelRect.height, TOOLTIP_HEIGHT, "Tooltip height is at 100px as expected");
+}
--- a/devtools/client/shared/widgets/HTMLTooltip.js
+++ b/devtools/client/shared/widgets/HTMLTooltip.js
@@ -109,50 +109,59 @@ function (anchorRect, viewportRect, heig
  *
  * @param {DOMRect} anchorRect
  *        Bounding rectangle for the anchor, relative to the tooltip document.
  * @param {DOMRect} viewportRect
  *        Bounding rectangle for the viewport. top/left can be different from 0 if some
  *        space should not be used by tooltips (for instance OS toolbars, taskbars etc.).
  * @param {Number} width
  *        Preferred width for the tooltip.
+ * @param {String} type
+ *        The tooltip type (e.g. "arrow").
+ * @param {Number} offset
+ *        Horizontal offset in pixels.
+ * @param {Boolean} isRtl
+ *        If the anchor is in RTL, the tooltip should be aligned to the right.
  * @return {Object}
  *         - {Number} left: the left offset for the tooltip.
  *         - {Number} width: the width to use for the tooltip container.
  *         - {Number} arrowLeft: the left offset to use for the arrow element.
  */
 const calculateHorizontalPosition =
-function (anchorRect, viewportRect, width, type, offset) {
-  let {left: anchorLeft, width: anchorWidth} = anchorRect;
+function (anchorRect, viewportRect, width, type, offset, isRtl) {
+  let anchorWidth = anchorRect.width;
+  let anchorStart = isRtl ? anchorRect.right : anchorRect.left;
 
   // Translate to the available viewport space before calculating dimensions and position.
-  anchorLeft -= viewportRect.left;
+  anchorStart -= viewportRect.left;
 
   // Calculate WIDTH.
   width = Math.min(width, viewportRect.width);
 
   // Calculate LEFT.
   // By default the tooltip is aligned with the anchor left edge. Unless this
   // makes it overflow the viewport, in which case is shifts to the left.
-  let left = Math.min(anchorLeft + offset, viewportRect.width - width);
+  let left = anchorStart + offset - (isRtl ? width : 0);
+  left = Math.min(left, viewportRect.width - width);
+  left = Math.max(0, left);
 
   // Calculate ARROW LEFT (tooltip's LEFT might be updated)
   let arrowLeft;
   // Arrow style tooltips may need to be shifted to the left
   if (type === TYPE.ARROW) {
     let arrowCenter = left + ARROW_OFFSET + ARROW_WIDTH / 2;
-    let anchorCenter = anchorLeft + anchorWidth / 2;
+    let anchorCenter = anchorStart + anchorWidth / 2;
     // If the anchor is too narrow, align the arrow and the anchor center.
     if (arrowCenter > anchorCenter) {
       left = Math.max(0, left - (arrowCenter - anchorCenter));
     }
     // Arrow's left offset relative to the anchor.
     arrowLeft = Math.min(ARROW_OFFSET, (anchorWidth - ARROW_WIDTH) / 2) | 0;
     // Translate the coordinate to tooltip container
-    arrowLeft += anchorLeft - left;
+    arrowLeft += anchorStart - left;
     // Make sure the arrow remains in the tooltip container.
     arrowLeft = Math.min(arrowLeft, width - ARROW_WIDTH);
     arrowLeft = Math.max(arrowLeft, 0);
   }
 
   // Translate back to absolute coordinates by re-including viewport left margin.
   left += viewportRect.left;
 
@@ -342,18 +351,20 @@ HTMLTooltip.prototype = {
     let preferredWidth;
     if (this.preferredWidth === "auto") {
       preferredWidth = this._measureContainerWidth();
     } else {
       let themeWidth = 2 * EXTRA_BORDER[this.type];
       preferredWidth = this.preferredWidth + themeWidth;
     }
 
-    let {left, width, arrowLeft} =
-      calculateHorizontalPosition(anchorRect, viewportRect, preferredWidth, this.type, x);
+    let anchorWin = anchor.ownerDocument.defaultView;
+    let isRtl = anchorWin.getComputedStyle(anchor).direction === "rtl";
+    let {left, width, arrowLeft} = calculateHorizontalPosition(
+      anchorRect, viewportRect, preferredWidth, this.type, x, isRtl);
 
     this.container.style.width = width + "px";
 
     if (this.type === TYPE.ARROW) {
       this.arrow.style.left = arrowLeft + "px";
     }
 
     if (this.useXulWrapper) {