Bug 1236991 - part 2: implement a default tooltiptextprovider in toolkit, r?enndeakin draft
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Tue, 12 Apr 2016 21:50:47 +0100
changeset 350954 9d65d38845b32aa36610b6ff17ad2e8f879e0208
parent 350941 3837d6744a109a4ce6e777da30aeff5312a98ea8
child 350955 dbef335e6251ed2e880cb55e6da227683bf8d750
push id15454
push usergijskruitbosch@gmail.com
push dateThu, 14 Apr 2016 14:37:20 +0000
reviewersenndeakin
bugs1236991
milestone48.0a1
Bug 1236991 - part 2: implement a default tooltiptextprovider in toolkit, r?enndeakin MozReview-Commit-ID: LqtibkGoDjQ
b2g/installer/package-manifest.in
browser/installer/package-manifest.in
mobile/android/installer/package-manifest.in
toolkit/components/moz.build
toolkit/components/tooltiptext/TooltipTextProvider.js
toolkit/components/tooltiptext/TooltipTextProvider.manifest
toolkit/components/tooltiptext/moz.build
toolkit/content/widgets/popup.xml
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -390,16 +390,18 @@
 @RESPATH@/components/nsSetDefaultBrowser.js
 @RESPATH@/components/toolkitsearch.manifest
 @RESPATH@/components/nsTryToClose.manifest
 @RESPATH@/components/nsTryToClose.js
 @RESPATH@/components/passwordmgr.manifest
 @RESPATH@/components/nsLoginInfo.js
 @RESPATH@/components/nsLoginManager.js
 @RESPATH@/components/nsLoginManagerPrompter.js
+@RESPATH@/components/TooltipTextProvider.js
+@RESPATH@/components/TooltipTextProvider.manifest
 @RESPATH@/components/NetworkGeolocationProvider.manifest
 @RESPATH@/components/NetworkGeolocationProvider.js
 @RESPATH@/components/TVSimulatorService.js
 @RESPATH@/components/TVSimulatorService.manifest
 #ifdef MOZ_WEBRTC
 @RESPATH@/components/PeerConnection.js
 @RESPATH@/components/PeerConnection.manifest
 #endif
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -382,16 +382,18 @@
 @RESPATH@/components/nsSearchSuggestions.js
 @RESPATH@/components/nsSidebar.js
 @RESPATH@/components/passwordmgr.manifest
 @RESPATH@/components/nsLoginInfo.js
 @RESPATH@/components/nsLoginManager.js
 @RESPATH@/components/nsLoginManagerPrompter.js
 @RESPATH@/components/storage-json.js
 @RESPATH@/components/crypto-SDR.js
+@RESPATH@/components/TooltipTextProvider.js
+@RESPATH@/components/TooltipTextProvider.manifest
 @RESPATH@/components/jsconsole-clhandler.manifest
 @RESPATH@/components/jsconsole-clhandler.js
 @RESPATH@/components/webvtt.xpt
 @RESPATH@/components/WebVTT.manifest
 @RESPATH@/components/WebVTTParserWrapper.js
 #ifdef MOZ_GTK
 @RESPATH@/components/nsFilePicker.manifest
 @RESPATH@/components/nsFilePicker.js
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -301,16 +301,18 @@
 @BINPATH@/components/nsSearchService.js
 @BINPATH@/components/nsSidebar.js
 @BINPATH@/components/passwordmgr.manifest
 @BINPATH@/components/nsLoginInfo.js
 @BINPATH@/components/nsLoginManager.js
 @BINPATH@/components/nsLoginManagerPrompter.js
 @BINPATH@/components/storage-mozStorage.js
 @BINPATH@/components/crypto-SDR.js
+@BINPATH@/components/TooltipTextProvider.js
+@BINPATH@/components/TooltipTextProvider.manifest
 @BINPATH@/components/NetworkGeolocationProvider.manifest
 @BINPATH@/components/NetworkGeolocationProvider.js
 @BINPATH@/components/extensions.manifest
 @BINPATH@/components/utils.manifest
 @BINPATH@/components/simpleServices.js
 @BINPATH@/components/addonManager.js
 @BINPATH@/components/amContentHandler.js
 @BINPATH@/components/amInstallTrigger.js
--- a/toolkit/components/moz.build
+++ b/toolkit/components/moz.build
@@ -53,16 +53,17 @@ DIRS += [
     'reflect',
     'securityreporter',
     'sqlite',
     'startup',
     'statusfilter',
     'telemetry',
     'thumbnails',
     'timermanager',
+    'tooltiptext',
     'typeaheadfind',
     'utils',
     'urlformatter',
     'viewconfig',
     'workerloader',
     'xulstore'
 ]
 
copy from toolkit/content/widgets/popup.xml
copy to toolkit/components/tooltiptext/TooltipTextProvider.js
--- a/toolkit/content/widgets/popup.xml
+++ b/toolkit/components/tooltiptext/TooltipTextProvider.js
@@ -1,743 +1,148 @@
-<?xml version="1.0"?>
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
-
-<bindings id="popupBindings"
-   xmlns="http://www.mozilla.org/xbl"
-   xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-   xmlns:xbl="http://www.mozilla.org/xbl">
-
-  <binding id="popup-base">
-    <resources>
-      <stylesheet src="chrome://global/skin/popup.css"/>
-    </resources>
-
-    <implementation implements="nsIDOMXULPopupElement">
-      <property name="label" onget="return this.getAttribute('label');"
-                             onset="this.setAttribute('label', val); return val;"/>
-      <property name="position" onget="return this.getAttribute('position');"
-                                onset="this.setAttribute('position', val); return val;"/>
-      <property name="popupBoxObject">
-        <getter>
-          return this.boxObject;
-        </getter>
-      </property>
-
-      <property name="state" readonly="true"
-                onget="return this.popupBoxObject.popupState"/>
-
-      <property name="triggerNode" readonly="true"
-                onget="return this.popupBoxObject.triggerNode"/>
-
-      <property name="anchorNode" readonly="true"
-                onget="return this.popupBoxObject.anchorNode"/>
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-      <method name="openPopup">
-        <parameter name="aAnchorElement"/>
-        <parameter name="aPosition"/>
-        <parameter name="aX"/>
-        <parameter name="aY"/>
-        <parameter name="aIsContextMenu"/>
-        <parameter name="aAttributesOverride"/>
-        <parameter name="aTriggerEvent"/>
-        <body>
-        <![CDATA[
-          try {
-            var popupBox = this.popupBoxObject;
-            if (popupBox)
-              popupBox.openPopup(aAnchorElement, aPosition, aX, aY,
-                                 aIsContextMenu, aAttributesOverride, aTriggerEvent);
-          } catch(e) {}
-        ]]>
-        </body>
-      </method>
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
-      <method name="openPopupAtScreen">
-        <parameter name="aX"/>
-        <parameter name="aY"/>
-        <parameter name="aIsContextMenu"/>
-        <parameter name="aTriggerEvent"/>
-        <body>
-        <![CDATA[
-          try {
-            var popupBox = this.popupBoxObject;
-            if (popupBox)
-              popupBox.openPopupAtScreen(aX, aY, aIsContextMenu, aTriggerEvent);
-          } catch(e) {}
-        ]]>
-        </body>
-      </method>
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function TooltipTextProvider() {}
 
-      <method name="openPopupAtScreenRect">
-        <parameter name="aPosition"/>
-        <parameter name="aX"/>
-        <parameter name="aY"/>
-        <parameter name="aWidth"/>
-        <parameter name="aHeight"/>
-        <parameter name="aIsContextMenu"/>
-        <parameter name="aAttributesOverride"/>
-        <parameter name="aTriggerEvent"/>
-        <body>
-        <![CDATA[
-          try {
-            var popupBox = this.popupBoxObject;
-            if (popupBox)
-              popupBox.openPopupAtScreenRect(aPosition, aX, aY, aWidth, aHeight,
-                                             aIsContextMenu, aAttributesOverride, aTriggerEvent);
-          } catch(e) {}
-        ]]>
-        </body>
-      </method>
-
-      <method name="showPopup">
-        <parameter name="element"/>
-        <parameter name="xpos"/>
-        <parameter name="ypos"/>
-        <parameter name="popuptype"/>
-        <parameter name="anchoralignment"/>
-        <parameter name="popupalignment"/>
-        <body>
-        <![CDATA[
-          var popupBox = null;
-          var menuBox = null;
-          try {
-            popupBox = this.popupBoxObject;
-          } catch(e) {}
-          try {
-            menuBox = this.parentNode.boxObject;
-          } catch(e) {}
-          if (menuBox instanceof MenuBoxObject)
-            menuBox.openMenu(true);
-          else if (popupBox)
-            popupBox.showPopup(element, this, xpos, ypos, popuptype, anchoralignment, popupalignment);
-        ]]>
-        </body>
-      </method>
+TooltipTextProvider.prototype = {
+  getNodeText(tipElement, textOut, directionOut) {
+    // Don't show the tooltip if the tooltip node is a document, browser, or disconnected.
+    if (!tipElement || !tipElement.ownerDocument ||
+        tipElement.localName == "browser" ||
+        (tipElement.ownerDocument.compareDocumentPosition(tipElement) &
+         tipElement.ownerDocument.DOCUMENT_POSITION_DISCONNECTED)) {
+      return false;
+    }
 
-      <method name="hidePopup">
-        <parameter name="cancel"/>
-        <body>
-        <![CDATA[
-          var popupBox = null;
-          var menuBox = null;
-          try {
-            popupBox = this.popupBoxObject;
-          } catch(e) {}
-          try {
-            menuBox = this.parentNode.boxObject;
-          } catch(e) {}
-          if (menuBox instanceof MenuBoxObject)
-            menuBox.openMenu(false);
-          else if (popupBox instanceof PopupBoxObject)
-            popupBox.hidePopup(cancel);
-        ]]>
-        </body>
-      </method>
+    var defView = tipElement.ownerDocument.defaultView;
+    // XXX Work around bug 350679:
+    // "Tooltips can be fired in documents with no view".
+    if (!defView)
+      return false;
 
-      <property name="autoPosition">
-        <getter>
-        <![CDATA[
-          return this.popupBoxObject.autoPosition;
-        ]]>
-        </getter>
-        <setter>
-        <![CDATA[
-          return this.popupBoxObject.autoPosition = val;
-        ]]>
-        </setter>
-      </property>
+    const XLinkNS = "http://www.w3.org/1999/xlink";
+    const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
-      <property name="alignmentPosition" readonly="true">
-        <getter>
-        <![CDATA[
-          return this.popupBoxObject.alignmentPosition;
-        ]]>
-        </getter>
-      </property>
-
-      <property name="alignmentOffset" readonly="true">
-        <getter>
-        <![CDATA[
-          return this.popupBoxObject.alignmentOffset;
-        ]]>
-        </getter>
-      </property>
+    var titleText = null;
+    var XLinkTitleText = null;
+    var SVGTitleText = null;
+    var XULtooltiptextText = null;
+    var lookingForSVGTitle = true;
+    var direction = tipElement.ownerDocument.dir;
 
-      <method name="enableKeyboardNavigator">
-        <parameter name="aEnableKeyboardNavigator"/>
-        <body>
-        <![CDATA[
-          this.popupBoxObject.enableKeyboardNavigator(aEnableKeyboardNavigator);
-        ]]>
-        </body>
-      </method>
-
-      <method name="enableRollup">
-        <parameter name="aEnableRollup"/>
-        <body>
-        <![CDATA[
-          this.popupBoxObject.enableRollup(aEnableRollup);
-        ]]>
-        </body>
-      </method>
-
-      <method name="sizeTo">
-        <parameter name="aWidth"/>
-        <parameter name="aHeight"/>
-        <body>
-        <![CDATA[
-          this.popupBoxObject.sizeTo(aWidth, aHeight);
-        ]]>
-        </body>
-      </method>
+    // If the element is invalid per HTML5 Forms specifications and has no title,
+    // show the constraint validation error message.
+    if ((tipElement instanceof defView.HTMLInputElement ||
+         tipElement instanceof defView.HTMLTextAreaElement ||
+         tipElement instanceof defView.HTMLSelectElement ||
+         tipElement instanceof defView.HTMLButtonElement) &&
+        !tipElement.hasAttribute('title') &&
+        (!tipElement.form || !tipElement.form.noValidate)) {
+      // If the element is barred from constraint validation or valid,
+      // the validation message will be the empty string.
+      titleText = tipElement.validationMessage || null;
+    }
 
-      <method name="moveTo">
-        <parameter name="aLeft"/>
-        <parameter name="aTop"/>
-        <body>
-        <![CDATA[
-          this.popupBoxObject.moveTo(aLeft, aTop);
-        ]]>
-        </body>
-      </method>
-
-      <method name="moveToAnchor">
-        <parameter name="aAnchorElement"/>
-        <parameter name="aPosition"/>
-        <parameter name="aX"/>
-        <parameter name="aY"/>
-        <parameter name="aAttributesOverride"/>
-        <body>
-        <![CDATA[
-          this.popupBoxObject.moveToAnchor(aAnchorElement, aPosition, aX, aY, aAttributesOverride);
-        ]]>
-        </body>
-      </method>
-
-      <method name="getOuterScreenRect">
-        <body>
-        <![CDATA[
-          return this.popupBoxObject.getOuterScreenRect();
-        ]]>
-        </body>
-      </method>
-    </implementation>
-
-  </binding>
-
-  <binding id="popup" role="xul:menupopup"
-           extends="chrome://global/content/bindings/popup.xml#popup-base">
-
-    <content>
-      <xul:arrowscrollbox class="popup-internal-box" flex="1" orient="vertical"
-                          smoothscroll="false">
-        <children/>
-      </xul:arrowscrollbox>
-    </content>
+    // If the element is an <input type='file'> without a title, we should show
+    // the current file selection.
+    if (!titleText &&
+        tipElement instanceof defView.HTMLInputElement &&
+        tipElement.type == 'file' &&
+        !tipElement.hasAttribute('title')) {
+      let files = tipElement.files;
 
-    <handlers>
-      <handler event="popupshowing" phase="target">
-        <![CDATA[
-          var array = [];
-          var width = 0;
-          for (var menuitem = this.firstChild; menuitem; menuitem = menuitem.nextSibling) {
-            if (menuitem.localName == "menuitem" && menuitem.hasAttribute("acceltext")) {
-              var accel = document.getAnonymousElementByAttribute(menuitem, "anonid", "accel");
-              if (accel && accel.boxObject) {
-                array.push(accel);
-                if (accel.boxObject.width > width)
-                  width = accel.boxObject.width;
-              }
-            }
+      try {
+        var bundle =
+          Services.strings.createBundle("chrome://global/locale/layout/HtmlForm.properties");
+        if (files.length == 0) {
+          if (tipElement.multiple) {
+            titleText = bundle.GetStringFromName("NoFilesSelected");
+          } else {
+            titleText = bundle.GetStringFromName("NoFileSelected");
           }
-          for (var i = 0; i < array.length; i++)
-            array[i].width = width;
-        ]]>
-      </handler>
-    </handlers>
-  </binding>
-
-  <binding id="panel" role="xul:panel"
-           extends="chrome://global/content/bindings/popup.xml#popup-base">
-    <implementation implements="nsIDOMXULPopupElement">
-      <field name="_prevFocus">0</field>
-      <field name="_dragBindingAlive">true</field>
-      <constructor>
-      <![CDATA[
-        if (this.getAttribute("backdrag") == "true" && !this._draggableStarted) {
-          this._draggableStarted = true;
-          try {
+        } else {
+          titleText = files[0].name;
+          // For UX and performance (jank) reasons we cap the number of
+          // files that we list in the tooltip to 20 plus a "and xxx more"
+          // line, or to 21 if exactly 21 files were picked.
+          const TRUNCATED_FILE_COUNT = 20;
+          let count = Math.min(files.length, TRUNCATED_FILE_COUNT);
+          for (let i = 1; i < count; ++i) {
+            titleText += "\n" + files[i].name;
+          }
+          if (files.length == TRUNCATED_FILE_COUNT + 1) {
+            titleText += "\n" + files[TRUNCATED_FILE_COUNT].name;
+          } else if (files.length > TRUNCATED_FILE_COUNT + 1) {
+            let xmoreStr = bundle.GetStringFromName("AndNMoreFiles");
+            let xmoreNum = files.length - TRUNCATED_FILE_COUNT;
             let tmp = {};
-            Components.utils.import("resource://gre/modules/WindowDraggingUtils.jsm", tmp);
-            let draghandle = new tmp.WindowDraggingElement(this);
-            draghandle.mouseDownCheck = function () {
-              return this._dragBindingAlive;
-            }
-          } catch (e) {}
-        }
-      ]]>
-      </constructor>
-    </implementation>
-
-    <handlers>
-      <handler event="popupshowing"><![CDATA[
-        // Capture the previous focus before has a chance to get set inside the panel
-        try {
-          this._prevFocus = Components.utils
-                            .getWeakReference(document.commandDispatcher.focusedElement);
-          if (this._prevFocus.get())
-            return;
-        } catch (ex) { }
-
-        this._prevFocus = Components.utils.getWeakReference(document.activeElement);
-      ]]></handler>
-      <handler event="popupshown"><![CDATA[
-        // Fire event for accessibility APIs
-        var alertEvent = document.createEvent("Events");
-        alertEvent.initEvent("AlertActive", true, true);
-        this.dispatchEvent(alertEvent);
-       ]]></handler>
-      <handler event="popuphiding"><![CDATA[
-        try {
-          this._currentFocus = document.commandDispatcher.focusedElement;
-        } catch (e) {
-          this._currentFocus = document.activeElement;
-        }
-      ]]></handler>
-      <handler event="popuphidden"><![CDATA[
-        var currentFocus = this._currentFocus;
-        var prevFocus = this._prevFocus ? this._prevFocus.get() : null;
-        this._currentFocus = null;
-        this._prevFocus = null;
-        if (prevFocus && currentFocus && this.getAttribute("norestorefocus") != "true") {
-          // Try to restore focus
-          try {
-            if (document.commandDispatcher.focusedWindow != window)
-              return; // Focus has already been set to a window outside of this panel
-          } catch(ex) {}
-          while (currentFocus) {
-            if (currentFocus == this) {
-              // Focus was set on an element inside this panel,
-              // so we need to move it back to where it was previously
-              try {
-                let fm = Components.classes["@mozilla.org/focus-manager;1"]
-                                   .getService(Components.interfaces.nsIFocusManager);
-                fm.setFocus(prevFocus, fm.FLAG_NOSCROLL);
-              } catch(e) {
-                prevFocus.focus();
-              }
-              return;
-            }
-            currentFocus = currentFocus.parentNode;
+            Cu.import("resource://gre/modules/PluralForm.jsm", tmp);
+            let andXMoreStr = tmp.PluralForm.get(xmoreNum, xmoreStr).replace("#1", xmoreNum);
+            titleText += "\n" + andXMoreStr;
           }
         }
-      ]]></handler>
-    </handlers>
-  </binding>
+      } catch(e) {}
+    }
+
+    // Check texts against null so that title="" can be used to undefine a
+    // title on a child element.
+    while (tipElement &&
+           (titleText == null) && (XLinkTitleText == null) &&
+           (SVGTitleText == null) && (XULtooltiptextText == null)) {
+
+      if (tipElement.nodeType == defView.Node.ELEMENT_NODE) {
+        if (tipElement.namespaceURI == XULNS)
+          XULtooltiptextText = tipElement.getAttribute("tooltiptext");
+        else if (!(tipElement instanceof defView.SVGElement))
+          titleText = tipElement.getAttribute("title");
 
-  <binding id="arrowpanel" extends="chrome://global/content/bindings/popup.xml#panel">
-    <content flip="both" side="top" position="bottomcenter topleft" consumeoutsideclicks="false">
-      <xul:vbox anonid="container" class="panel-arrowcontainer" flex="1"
-               xbl:inherits="side,panelopen">
-        <xul:box anonid="arrowbox" class="panel-arrowbox">
-          <xul:image anonid="arrow" class="panel-arrow" xbl:inherits="side"/>
-        </xul:box>
-        <xul:box class="panel-arrowcontent" xbl:inherits="side,align,dir,orient,pack" flex="1">
-          <children/>
-          <xul:box class="panel-inner-arrowcontentfooter" xbl:inherits="footertype" hidden="true"/>
-        </xul:box>
-      </xul:vbox>
-    </content>
-    <implementation>
-      <field name="_fadeTimer">null</field>
-      <method name="sizeTo">
-        <parameter name="aWidth"/>
-        <parameter name="aHeight"/>
-        <body>
-        <![CDATA[
-          this.popupBoxObject.sizeTo(aWidth, aHeight);
-          if (this.state == "open")
-            this.adjustArrowPosition();
-        ]]>
-        </body>
-      </method>
-      <method name="moveTo">
-        <parameter name="aLeft"/>
-        <parameter name="aTop"/>
-        <body>
-        <![CDATA[
-          this.popupBoxObject.moveTo(aLeft, aTop);
-          if (this.state == "open")
-            this.adjustArrowPosition();
-        ]]>
-        </body>
-      </method>
-      <method name="moveToAnchor">
-        <parameter name="aAnchorElement"/>
-        <parameter name="aPosition"/>
-        <parameter name="aX"/>
-        <parameter name="aY"/>
-        <parameter name="aAttributesOverride"/>
-        <body>
-        <![CDATA[
-          this.popupBoxObject.moveToAnchor(aAnchorElement, aPosition, aX, aY, aAttributesOverride);
-          if (this.state == "open")
-            this.adjustArrowPosition();
-        ]]>
-        </body>
-      </method>
-      <method name="adjustArrowPosition">
-        <body>
-        <![CDATA[
-        var arrow = document.getAnonymousElementByAttribute(this, "anonid", "arrow");
-
-        var anchor = this.anchorNode;
-        if (!anchor) {
-          arrow.hidden = true;
-          return;
+        if ((tipElement instanceof defView.HTMLAnchorElement ||
+             tipElement instanceof defView.HTMLAreaElement ||
+             tipElement instanceof defView.HTMLLinkElement ||
+             tipElement instanceof defView.SVGAElement) && tipElement.href) {
+          XLinkTitleText = tipElement.getAttributeNS(XLinkNS, "title");
         }
-
-        var container = document.getAnonymousElementByAttribute(this, "anonid", "container");
-        var arrowbox = document.getAnonymousElementByAttribute(this, "anonid", "arrowbox");
-
-        var position = this.alignmentPosition;
-        var offset = this.alignmentOffset;
-
-        this.setAttribute("arrowposition", position);
-
-        // if this panel has a "sliding" arrow, we may have previously set margins...
-        arrowbox.style.removeProperty("transform");
-        if (position.indexOf("start_") == 0 || position.indexOf("end_") == 0) {
-          container.orient = "horizontal";
-          arrowbox.orient = "vertical";
-          if (position.indexOf("_after") > 0) {
-            arrowbox.pack = "end";
-          } else {
-            arrowbox.pack = "start";
-          }
-          arrowbox.style.transform = "translate(0, " + -offset + "px)";
-
-          // The assigned side stays the same regardless of direction.
-          var isRTL = (window.getComputedStyle(this).direction == "rtl");
-
-          if (position.indexOf("start_") == 0) {
-            container.dir = "reverse";
-            this.setAttribute("side", isRTL ? "left" : "right");
-          }
-          else {
-            container.dir = "";
-            this.setAttribute("side", isRTL ? "right" : "left");
-          }
+        if (lookingForSVGTitle &&
+            (!(tipElement instanceof defView.SVGElement) ||
+             tipElement.parentNode.nodeType == defView.Node.DOCUMENT_NODE)) {
+          lookingForSVGTitle = false;
         }
-        else if (position.indexOf("before_") == 0 || position.indexOf("after_") == 0) {
-          container.orient = "";
-          arrowbox.orient = "";
-          if (position.indexOf("_end") > 0) {
-            arrowbox.pack = "end";
-          } else {
-            arrowbox.pack = "start";
-          }
-          arrowbox.style.transform = "translate(" + -offset + "px, 0)";
-
-          if (position.indexOf("before_") == 0) {
-            container.dir = "reverse";
-            this.setAttribute("side", "bottom");
-          }
-          else {
-            container.dir = "";
-            this.setAttribute("side", "top");
+        if (lookingForSVGTitle) {
+          for (let childNode of tipElement.childNodes) {
+            if (childNode instanceof defView.SVGTitleElement) {
+              SVGTitleText = childNode.textContent;
+              break;
+            }
           }
         }
 
-        arrow.hidden = false;
-        ]]>
-        </body>
-      </method>
-    </implementation>
-    <handlers>
-      <handler event="popupshowing" phase="target">
-      <![CDATA[
-        this.adjustArrowPosition();
-        if (this.getAttribute("animate") != "false") {
-          this.setAttribute("animate", "open");
-        }
-
-        // set fading
-        var fade = this.getAttribute("fade");
-        var fadeDelay = 0;
-        if (fade == "fast") {
-          fadeDelay = 1;
-        }
-        else if (fade == "slow") {
-          fadeDelay = 4000;
-        }
-        else {
-          return;
-        }
-
-        this._fadeTimer = setTimeout(() => this.hidePopup(true), fadeDelay, this);
-      ]]>
-      </handler>
-      <handler event="popuphiding" phase="target">
-        let animate = (this.getAttribute("animate") != "false");
-
-        if (this._fadeTimer) {
-          clearTimeout(this._fadeTimer);
-          if (animate) {
-            this.setAttribute("animate", "fade");
-          }
-        }
-        else if (animate) {
-          this.setAttribute("animate", "cancel");
-        }
-      </handler>
-      <handler event="popupshown" phase="target">
-        this.setAttribute("panelopen", "true");
-      </handler>
-      <handler event="popuphidden" phase="target">
-        this.removeAttribute("panelopen");
-        if (this.getAttribute("animate") != "false") {
-          this.removeAttribute("animate");
-        }
-      </handler>
-    </handlers>
-  </binding>
-
-  <binding id="tooltip" role="xul:tooltip"
-           extends="chrome://global/content/bindings/popup.xml#popup-base">
-    <content>
-      <children>
-        <xul:label class="tooltip-label" xbl:inherits="xbl:text=label" flex="1"/>
-      </children>
-    </content>
+        direction = defView.getComputedStyle(tipElement, "")
+                           .getPropertyValue("direction");
+      }
 
-    <implementation>
-      <field name="_mouseOutCount">0</field>
-      <field name="_isMouseOver">false</field>
-
-      <property name="label"
-                onget="return this.getAttribute('label');"
-                onset="this.setAttribute('label', val); return val;"/>
-
-      <property name="page" onset="if (val) this.setAttribute('page', 'true');
-                                   else this.removeAttribute('page');
-                                   return val;"
-                            onget="return this.getAttribute('page') == 'true';"/>
-
-      <!-- Given the supplied element within a page, set the tooltip's text to the text
-           for that element. Returns true if text was assigned, and false if the no text
-           is set, which normally would be used to cancel tooltip display.
-
-           Note that DefaultTooltipTextProvider::GetNodeText() from nsDocShellTreeOwner.cpp
-           also performs the same function, but for embedded clients that don't use a XUL/JS
-           layer. These two should be kept synchronized.
-        -->
-      <method name="fillInPageTooltip">
-        <parameter name="tipElement"/>
-        <body>
-        <![CDATA[
-          // Don't show the tooltip if the tooltip node is a document, browser, or disconnected.
-          if (!tipElement || !tipElement.ownerDocument ||
-              tipElement.localName == "browser" ||
-              (tipElement.ownerDocument.compareDocumentPosition(tipElement) & document.DOCUMENT_POSITION_DISCONNECTED)) {
-            return false;
-          }
-
-          var defView = tipElement.ownerDocument.defaultView;
-          // XXX Work around bug 350679:
-          // "Tooltips can be fired in documents with no view".
-          if (!defView)
-            return false;
-
-          const XLinkNS = "http://www.w3.org/1999/xlink";
-          const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-
-          var titleText = null;
-          var XLinkTitleText = null;
-          var SVGTitleText = null;
-          var XULtooltiptextText = null;
-          var lookingForSVGTitle = true;
-          var direction = tipElement.ownerDocument.dir;
-
-          // If the element is invalid per HTML5 Forms specifications and has no title,
-          // show the constraint validation error message.
-          if ((tipElement instanceof HTMLInputElement ||
-               tipElement instanceof HTMLTextAreaElement ||
-               tipElement instanceof HTMLSelectElement ||
-               tipElement instanceof HTMLButtonElement) &&
-              !tipElement.hasAttribute('title') &&
-              (!tipElement.form || !tipElement.form.noValidate)) {
-            // If the element is barred from constraint validation or valid,
-            // the validation message will be the empty string.
-            titleText = tipElement.validationMessage || null;
-          }
-
-          // If the element is an <input type='file'> without a title, we should show
-          // the current file selection.
-          if (!titleText &&
-              tipElement instanceof HTMLInputElement &&
-              tipElement.type == 'file' &&
-              !tipElement.hasAttribute('title')) {
-            let files = tipElement.files;
+      tipElement = tipElement.parentNode;
+    }
 
-            try {
-              var bundle = Components.classes['@mozilla.org/intl/stringbundle;1']
-                                     .getService(Components.interfaces.nsIStringBundleService)
-                                     .createBundle("chrome://global/locale/layout/HtmlForm.properties");
-              if (files.length == 0) {
-                if (tipElement.multiple) {
-                  titleText = bundle.GetStringFromName("NoFilesSelected");
-                } else {
-                  titleText = bundle.GetStringFromName("NoFileSelected");
-                }
-              } else {
-                titleText = files[0].name;
-                // For UX and performance (jank) reasons we cap the number of
-                // files that we list in the tooltip to 20 plus a "and xxx more"
-                // line, or to 21 if exactly 21 files were picked.
-                const TRUNCATED_FILE_COUNT = 20;
-                let count = Math.min(files.length, TRUNCATED_FILE_COUNT);
-                for (let i = 1; i < count; ++i) {
-                  titleText += "\n" + files[i].name;
-                }
-                if (files.length == TRUNCATED_FILE_COUNT + 1) {
-                  titleText += "\n" + files[TRUNCATED_FILE_COUNT].name;
-                } else if (files.length > TRUNCATED_FILE_COUNT + 1) {
-                  let xmoreStr = bundle.GetStringFromName("AndNMoreFiles");
-                  let xmoreNum = files.length - TRUNCATED_FILE_COUNT;
-                  let tmp = {};
-                  Components.utils.import("resource://gre/modules/PluralForm.jsm", tmp);
-                  let andXMoreStr = tmp.PluralForm.get(xmoreNum, xmoreStr).replace("#1", xmoreNum);
-                  titleText += "\n" + andXMoreStr;
-                }
-              }
-            } catch(e) {}
-          }
-
-          // Check texts against null so that title="" can be used to undefine a
-          // title on a child element.
-          while (tipElement &&
-                 (titleText == null) && (XLinkTitleText == null) &&
-                 (SVGTitleText == null) && (XULtooltiptextText == null)) {
-
-            if (tipElement.nodeType == Node.ELEMENT_NODE) {
-              if (tipElement.namespaceURI == XULNS)
-                XULtooltiptextText = tipElement.getAttribute("tooltiptext");
-              else if (!(tipElement instanceof SVGElement))
-                titleText = tipElement.getAttribute("title");
-
-              if ((tipElement instanceof HTMLAnchorElement ||
-                   tipElement instanceof HTMLAreaElement ||
-                   tipElement instanceof HTMLLinkElement ||
-                   tipElement instanceof SVGAElement) && tipElement.href) {
-                XLinkTitleText = tipElement.getAttributeNS(XLinkNS, "title");
-              }
-              if (lookingForSVGTitle &&
-                  (!(tipElement instanceof SVGElement) ||
-                   tipElement.parentNode.nodeType == Node.DOCUMENT_NODE)) {
-                lookingForSVGTitle = false;
-              }
-              if (lookingForSVGTitle) {
-                for (let childNode of tipElement.childNodes) {
-                  if (childNode instanceof SVGTitleElement) {
-                    SVGTitleText = childNode.textContent;
-                    break;
-                  }
-                }
-              }
-
-              direction = defView.getComputedStyle(tipElement, "")
-                                 .getPropertyValue("direction");
-            }
-
-            tipElement = tipElement.parentNode;
-          }
-
-          this.style.direction = direction;
+    return [titleText, XLinkTitleText, SVGTitleText, XULtooltiptextText].some(function (t) {
+      if (t && /\S/.test(t)) {
+        // Make CRLF and CR render one line break each.
+        textOut.value = t.replace(/\r\n?/g, '\n');
+        directionOut.value = direction;
+        return true;
+      }
 
-          return [titleText, XLinkTitleText, SVGTitleText, XULtooltiptextText].some(function (t) {
-            if (t && /\S/.test(t)) {
-              // Make CRLF and CR render one line break each.
-              this.label = t.replace(/\r\n?/g, '\n');
-              return true;
-            }
-
-            return false;
-          }, this);
-        ]]>
-        </body>
-      </method>
-    </implementation>
-
-    <handlers>
-      <handler event="mouseover"><![CDATA[
-        var rel = event.relatedTarget;
-        //dump("ENTERING " + (rel ? rel.localName : "null") + "\n");
-        if (!rel)
-          return;
-
-        // find out if the node we entered from is one of our anonymous children
-        while (rel) {
-          if (rel == this)
-            break;
-          rel = rel.parentNode;
-        }
-
-        // if the exited node is not a descendant of ours, we are entering for the first time
-        if (rel != this)
-          this._isMouseOver = true;
-      ]]></handler>
-
-      <handler event="mouseout"><![CDATA[
-        var rel = event.relatedTarget;
-        //dump("LEAVING " + (rel ? rel.localName : "null") + "\n");
+      return false;
+    });
+  },
 
-        // relatedTarget is null when the titletip is first shown: a mouseout event fires
-        // because the mouse is exiting the main window and entering the titletip "window".
-        // relatedTarget is also null when the mouse exits the main window completely,
-        // so count how many times relatedTarget was null after titletip is first shown
-        // and hide popup the 2nd time
-        if (!rel) {
-          ++this._mouseOutCount;
-          if (this._mouseOutCount > 1)
-            this.hidePopup();
-          return;
-        }
-
-        // find out if the node we are entering is one of our anonymous children
-        while (rel) {
-          if (rel == this)
-            break;
-          rel = rel.parentNode;
-        }
+  classID : Components.ID("{f376627f-0bbc-47b8-887e-fc92574cc91f}"),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsITooltipTextProvider]),
+};
 
-        // if the entered node is not a descendant of ours, hide the tooltip
-        if (rel != this && this._isMouseOver) {
-          this.hidePopup();
-        }
-      ]]></handler>
-
-      <handler event="popupshowing"><![CDATA[
-        if (this.page && !this.fillInPageTooltip(this.triggerNode)) {
-          event.preventDefault();
-        }
-      ]]></handler>
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TooltipTextProvider]);
 
-      <handler event="popuphiding"><![CDATA[
-        this._isMouseOver = false;
-        this._mouseOutCount = 0;
-      ]]></handler>
-    </handlers>
-  </binding>
-
-  <binding id="popup-scrollbars" extends="chrome://global/content/bindings/popup.xml#popup">
-    <content>
-      <xul:hbox class="popup-internal-box" flex="1" orient="vertical" style="overflow: auto;">
-        <children/>
-      </xul:hbox>
-    </content>
-  </binding>
-
-</bindings>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/tooltiptext/TooltipTextProvider.manifest
@@ -0,0 +1,2 @@
+component {f376627f-0bbc-47b8-887e-fc92574cc91f} TooltipTextProvider.js
+contract @mozilla.org/embedcomp/default-tooltiptextprovider;1 {f376627f-0bbc-47b8-887e-fc92574cc91f}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/tooltiptext/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
+
+EXTRA_COMPONENTS += [
+    'TooltipTextProvider.js',
+    'TooltipTextProvider.manifest',
+]
+
+with Files('**'):
+    BUG_COMPONENT = ('Toolkit', 'General')
--- a/toolkit/content/widgets/popup.xml
+++ b/toolkit/content/widgets/popup.xml
@@ -520,157 +520,45 @@
       <property name="label"
                 onget="return this.getAttribute('label');"
                 onset="this.setAttribute('label', val); return val;"/>
 
       <property name="page" onset="if (val) this.setAttribute('page', 'true');
                                    else this.removeAttribute('page');
                                    return val;"
                             onget="return this.getAttribute('page') == 'true';"/>
+      <property name="textProvider"
+                readonly="true">
+        <getter>
+        <![CDATA[
+          if (!this._textProvider) {
+            this._textProvider = Components.classes["@mozilla.org/embedcomp/default-tooltiptextprovider;1"]
+                                 .getService(Components.interfaces.nsITooltipTextProvider);
+          }
+          return this._textProvider;
+        ]]>
+        </getter>
+      </property>
 
       <!-- Given the supplied element within a page, set the tooltip's text to the text
            for that element. Returns true if text was assigned, and false if the no text
            is set, which normally would be used to cancel tooltip display.
-
-           Note that DefaultTooltipTextProvider::GetNodeText() from nsDocShellTreeOwner.cpp
-           also performs the same function, but for embedded clients that don't use a XUL/JS
-           layer. These two should be kept synchronized.
         -->
       <method name="fillInPageTooltip">
         <parameter name="tipElement"/>
         <body>
         <![CDATA[
-          // Don't show the tooltip if the tooltip node is a document, browser, or disconnected.
-          if (!tipElement || !tipElement.ownerDocument ||
-              tipElement.localName == "browser" ||
-              (tipElement.ownerDocument.compareDocumentPosition(tipElement) & document.DOCUMENT_POSITION_DISCONNECTED)) {
-            return false;
-          }
-
-          var defView = tipElement.ownerDocument.defaultView;
-          // XXX Work around bug 350679:
-          // "Tooltips can be fired in documents with no view".
-          if (!defView)
-            return false;
-
-          const XLinkNS = "http://www.w3.org/1999/xlink";
-          const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-
-          var titleText = null;
-          var XLinkTitleText = null;
-          var SVGTitleText = null;
-          var XULtooltiptextText = null;
-          var lookingForSVGTitle = true;
-          var direction = tipElement.ownerDocument.dir;
-
-          // If the element is invalid per HTML5 Forms specifications and has no title,
-          // show the constraint validation error message.
-          if ((tipElement instanceof HTMLInputElement ||
-               tipElement instanceof HTMLTextAreaElement ||
-               tipElement instanceof HTMLSelectElement ||
-               tipElement instanceof HTMLButtonElement) &&
-              !tipElement.hasAttribute('title') &&
-              (!tipElement.form || !tipElement.form.noValidate)) {
-            // If the element is barred from constraint validation or valid,
-            // the validation message will be the empty string.
-            titleText = tipElement.validationMessage || null;
+          let tttp = this.textProvider;
+          let textObj = {}, dirObj = {};
+          let shouldChangeText = tttp.getNodeText(tipElement, textObj, dirObj);
+          if (shouldChangeText) {
+            this.style.direction = dirObj.value;
+            this.label = textObj.value;
           }
-
-          // If the element is an <input type='file'> without a title, we should show
-          // the current file selection.
-          if (!titleText &&
-              tipElement instanceof HTMLInputElement &&
-              tipElement.type == 'file' &&
-              !tipElement.hasAttribute('title')) {
-            let files = tipElement.files;
-
-            try {
-              var bundle = Components.classes['@mozilla.org/intl/stringbundle;1']
-                                     .getService(Components.interfaces.nsIStringBundleService)
-                                     .createBundle("chrome://global/locale/layout/HtmlForm.properties");
-              if (files.length == 0) {
-                if (tipElement.multiple) {
-                  titleText = bundle.GetStringFromName("NoFilesSelected");
-                } else {
-                  titleText = bundle.GetStringFromName("NoFileSelected");
-                }
-              } else {
-                titleText = files[0].name;
-                // For UX and performance (jank) reasons we cap the number of
-                // files that we list in the tooltip to 20 plus a "and xxx more"
-                // line, or to 21 if exactly 21 files were picked.
-                const TRUNCATED_FILE_COUNT = 20;
-                let count = Math.min(files.length, TRUNCATED_FILE_COUNT);
-                for (let i = 1; i < count; ++i) {
-                  titleText += "\n" + files[i].name;
-                }
-                if (files.length == TRUNCATED_FILE_COUNT + 1) {
-                  titleText += "\n" + files[TRUNCATED_FILE_COUNT].name;
-                } else if (files.length > TRUNCATED_FILE_COUNT + 1) {
-                  let xmoreStr = bundle.GetStringFromName("AndNMoreFiles");
-                  let xmoreNum = files.length - TRUNCATED_FILE_COUNT;
-                  let tmp = {};
-                  Components.utils.import("resource://gre/modules/PluralForm.jsm", tmp);
-                  let andXMoreStr = tmp.PluralForm.get(xmoreNum, xmoreStr).replace("#1", xmoreNum);
-                  titleText += "\n" + andXMoreStr;
-                }
-              }
-            } catch(e) {}
-          }
-
-          // Check texts against null so that title="" can be used to undefine a
-          // title on a child element.
-          while (tipElement &&
-                 (titleText == null) && (XLinkTitleText == null) &&
-                 (SVGTitleText == null) && (XULtooltiptextText == null)) {
-
-            if (tipElement.nodeType == Node.ELEMENT_NODE) {
-              if (tipElement.namespaceURI == XULNS)
-                XULtooltiptextText = tipElement.getAttribute("tooltiptext");
-              else if (!(tipElement instanceof SVGElement))
-                titleText = tipElement.getAttribute("title");
-
-              if ((tipElement instanceof HTMLAnchorElement ||
-                   tipElement instanceof HTMLAreaElement ||
-                   tipElement instanceof HTMLLinkElement ||
-                   tipElement instanceof SVGAElement) && tipElement.href) {
-                XLinkTitleText = tipElement.getAttributeNS(XLinkNS, "title");
-              }
-              if (lookingForSVGTitle &&
-                  (!(tipElement instanceof SVGElement) ||
-                   tipElement.parentNode.nodeType == Node.DOCUMENT_NODE)) {
-                lookingForSVGTitle = false;
-              }
-              if (lookingForSVGTitle) {
-                for (let childNode of tipElement.childNodes) {
-                  if (childNode instanceof SVGTitleElement) {
-                    SVGTitleText = childNode.textContent;
-                    break;
-                  }
-                }
-              }
-
-              direction = defView.getComputedStyle(tipElement, "")
-                                 .getPropertyValue("direction");
-            }
-
-            tipElement = tipElement.parentNode;
-          }
-
-          this.style.direction = direction;
-
-          return [titleText, XLinkTitleText, SVGTitleText, XULtooltiptextText].some(function (t) {
-            if (t && /\S/.test(t)) {
-              // Make CRLF and CR render one line break each.
-              this.label = t.replace(/\r\n?/g, '\n');
-              return true;
-            }
-
-            return false;
-          }, this);
+          return shouldChangeText;
         ]]>
         </body>
       </method>
     </implementation>
 
     <handlers>
       <handler event="mouseover"><![CDATA[
         var rel = event.relatedTarget;