Bug 1286523 - fix autocomplete popup position in RTL locales;r=bgrins draft
authorJulian Descottes <jdescottes@mozilla.com>
Wed, 13 Jul 2016 18:52:37 +0200
changeset 387961 f5cd74d94ca82ed7bc2bcb93eacf279c4385ba00
parent 387960 9329892b3d387739ffc9c53d3c89b3a30e8833ff
child 525496 9aa35141eca0fcd1bebd0f00ebbfdb9b871a7812
push id23119
push userjdescottes@mozilla.com
push dateFri, 15 Jul 2016 07:42:46 +0000
reviewersbgrins
bugs1286523
milestone50.0a1
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) {