Bug 1379208 - Position the tooltips using only one layout flush and no modifications to the surrounding DOM of the tooltip nodes. r?mconley,rickychien draft
authorJared Wein <jwein@mozilla.com>
Mon, 10 Jul 2017 19:01:39 -0400
changeset 606444 7900c44352d345d95c1209722754c9dee7bcc2ad
parent 606124 91c943f7373722ad4e122d98a2ddd6c79708b732
child 636763 79651ab8cb6ff28b3ad135aa4d51c971b374e372
push id67696
push userbmo:jaws@mozilla.com
push dateMon, 10 Jul 2017 23:02:00 +0000
reviewersmconley, rickychien
bugs1379208
milestone56.0a1
Bug 1379208 - Position the tooltips using only one layout flush and no modifications to the surrounding DOM of the tooltip nodes. r?mconley,rickychien MozReview-Commit-ID: JtFJA6dyNb
browser/components/preferences/in-content-new/findInPage.js
browser/themes/shared/incontentprefs/preferences.inc.css
--- a/browser/components/preferences/in-content-new/findInPage.js
+++ b/browser/components/preferences/in-content-new/findInPage.js
@@ -20,29 +20,16 @@ var gSearchResultsPane = {
     this.searchInput = document.getElementById("searchInput");
     this.searchInput.hidden = !Services.prefs.getBoolPref("browser.preferences.search");
     if (!this.searchInput.hidden) {
       this.searchInput.addEventListener("command", this);
       window.addEventListener("load", () => {
         this.searchInput.focus();
         this.initializeCategories();
       });
-
-      // Throttling the resize event to reduce the callback frequency
-      let callbackId;
-      window.addEventListener("resize", () => {
-        if (!callbackId) {
-          callbackId = window.requestAnimationFrame(() => {
-            this.listSearchTooltips.forEach((anchorNode) => {
-              this.calculateTooltipPosition(anchorNode);
-            });
-            callbackId = null;
-          });
-        }
-      });
     }
   },
 
   handleEvent(event) {
     if (event.type === "command") {
       this.searchFunction(event);
     }
   },
@@ -268,16 +255,27 @@ var gSearchResultsPane = {
         let helpUrl = Services.urlFormatter.formatURLPref("app.support.baseURL") + "preferences";
         let brandName = document.getElementById("bundleBrand").getString("brandShortName");
         // eslint-disable-next-line no-unsanitized/property
         document.getElementById("need-help").innerHTML =
           strings.getFormattedString("searchResults.needHelp2", [helpUrl, brandName]);
       } else {
         // Creating tooltips for all the instances found
         this.listSearchTooltips.forEach((anchorNode) => this.createSearchTooltip(anchorNode, this.query));
+
+        // Find width of search tooltips and adjust offset of arrow accordingly.
+        let oldStyle = document.querySelector("#search-tooltip-style");
+        if (oldStyle) {
+          oldStyle.remove();
+        }
+        // Add 20px to the width for the left and right padding.
+        let tooltipWidth = parseFloat(getComputedStyle(document.querySelector("[search-tooltip]"), ":before").width) + 20 + "px";
+        // This calc() centers the arrow on the tooltip (need to add 13px since the tooltip has a 20px offset and the arrow is 7px wide).
+        let style = document.createProcessingInstruction("xml-stylesheet", `href="data:text/css,[search-tooltip]::after { left: calc(${tooltipWidth} / 2 + 13px); }" id="search-tooltip-style" type="text/css"`);
+        document.insertBefore(style, document.firstChild);
       }
     } else {
       this.searchResultsCategory.hidden = true;
       document.getElementById("sorry-message").textContent = "";
       // Going back to General when cleared
       gotoPref("paneGeneral");
     }
   },
@@ -404,57 +402,27 @@ var gSearchResultsPane = {
    * Then calculation the offsets to position the tooltip in the correct place.
    *
    * @param Node anchorNode
    *    DOM Element
    * @param String query
    *    Word or words that are being searched for
    */
   createSearchTooltip(anchorNode, query) {
-    let searchTooltip = anchorNode.ownerDocument.createElement("span");
-    searchTooltip.setAttribute("class", "search-tooltip");
-    searchTooltip.textContent = query;
-
-    // Set tooltipNode property to track corresponded tooltip node.
-    anchorNode.tooltipNode = searchTooltip;
-    anchorNode.parentElement.classList.add("search-tooltip-parent");
-    anchorNode.parentElement.appendChild(searchTooltip);
-
-    this.calculateTooltipPosition(anchorNode);
-  },
-
-  calculateTooltipPosition(anchorNode) {
-    let searchTooltip = anchorNode.tooltipNode;
-    // In order to get the up-to-date position of each of the nodes that we're
-    // putting tooltips on, we have to flush layout intentionally, and that
-    // this is the result of a XUL limitation (bug 1363730).
-    let anchorRect = anchorNode.getBoundingClientRect();
-    let tooltipRect = searchTooltip.getBoundingClientRect();
-    let parentRect = anchorNode.parentElement.getBoundingClientRect();
-
-    let offSetLeft = (anchorRect.width / 2) - (tooltipRect.width / 2);
-    let relativeOffset = anchorRect.left - parentRect.left;
-    offSetLeft += relativeOffset > 0 ? relativeOffset : 0;
-    // 20.5 is reserved for tooltip position
-    let offSetTop = anchorRect.top - parentRect.top - 20.5;
-
-    searchTooltip.style.setProperty("left", `${offSetLeft}px`);
-    searchTooltip.style.setProperty("top", `${offSetTop}px`);
+    anchorNode.setAttribute("search-tooltip", query);
   },
 
   /**
    * Remove all search tooltips that were created.
    */
   removeAllSearchTooltips() {
-    let searchTooltips = Array.from(document.querySelectorAll(".search-tooltip"));
+    let searchTooltips = Array.from(document.querySelectorAll("[search-tooltip]"));
     for (let searchTooltip of searchTooltips) {
-      searchTooltip.parentElement.classList.remove("search-tooltip-parent");
-      searchTooltip.remove();
+      searchTooltip.removeAttribute("search-tooltip");
     }
-    this.listSearchTooltips.forEach((anchorNode) => anchorNode.tooltipNode.remove());
     this.listSearchTooltips.clear();
   },
 
   /**
    * Remove all indicators on menuitem.
    */
   removeAllSearchMenuitemIndicators() {
     this.listSearchMenuitemIndicators.forEach((node) => node.removeAttribute("indicator"));
--- a/browser/themes/shared/incontentprefs/preferences.inc.css
+++ b/browser/themes/shared/incontentprefs/preferences.inc.css
@@ -614,50 +614,53 @@ groupbox {
 }
 
 .help-button:link,
 .help-button:visited {
   color: var(--in-content-category-text);
   text-decoration: none;
 }
 
-.search-tooltip {
+[search-tooltip] {
+  /* Setting a transform creates a stacking context and containing
+     block for fixed position descendants. */
+  transform: translateZ(0);
+}
+
+[search-tooltip]::before {
+  content: attr(search-tooltip);
   font-size: 1.25rem;
-  position: absolute;
+  position: fixed;
   padding: 0 10px;
   background-color: #ffe900;
-  border: 1px solid #d7b600;
   -moz-user-select: none;
+  display: block;
+  /* Showing the tooltip above (in the Y context) the node is
+     causing it to get clipped. The tooltip doesn't clip below
+     the element, so this patch positions them below the anchor. */
+  top: -45px;
+  left: 20px;
 }
 
-.search-tooltip:hover,
-.search-tooltip:hover::before {
+[search-tooltip]:hover::before,
+[search-tooltip]:hover::after {
   opacity: .1;
 }
 
-.search-tooltip::before {
-  position: absolute;
+/* Note, since ::before is used for the tooltip text, we only
+   have one more pseudo-element that can be used for creating the
+   tooltip arrow, thus we cannot show a border on the tooltip. */
+[search-tooltip]::after {
+  position: fixed;
   content: "";
   border: 7px solid transparent;
-  border-top-color: #d7b600;
-  top: 100%;
-  offset-inline-start: calc(50% - 7px);
-}
-
-.search-tooltip::after {
-  position: absolute;
-  content: "";
-  border: 6px solid transparent;
-  border-top-color: #ffe900;
-  top: 100%;
-  offset-inline-start: calc(50% - 6px);
-}
-
-.search-tooltip-parent {
-  position: relative;
+  border-bottom-color: #ffe900;
+  /* The offset needs to be one pixel less than the tooltip text
+     to properly overlap the text and prevent a 1 pixel gap. */
+  top: calc(-44px - 1em);
 }
 
 menulist[indicator=true] > menupopup menuitem:not([image]) > .menu-iconic-left {
   display: -moz-box;
   width: 8px;
   min-width: auto; /* Override the min-width defined in menu.css */
   height: 10px;
   margin-inline-end: 6px;