Bug 1475485, @title tooltips should work also inside ShadowDOM, r=mrbkap
authorOlli Pettay <Olli.Pettay@helsinki.fi>
Sat, 14 Jul 2018 04:48:19 +0300
changeset 484401 015a00cd67680e33731ef61931b87afb6c66d9bb
parent 484400 4afbbc6154255f6f5dc1151a5a2cd1ea674e3915
child 484402 50a16618593df5b2dd4a3cceaedc9472741b91ef
push id1815
push userffxbld-merge
push dateMon, 15 Oct 2018 10:40:45 +0000
treeherdermozilla-release@18d4c09e9378 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmrbkap
bugs1475485
milestone63.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 1475485, @title tooltips should work also inside ShadowDOM, r=mrbkap
docshell/base/nsDocShellTreeOwner.cpp
dom/webidl/Element.webidl
dom/webidl/Text.webidl
layout/xul/nsXULTooltipListener.cpp
toolkit/components/tooltiptext/TooltipTextProvider.js
toolkit/components/tooltiptext/tests/browser.ini
toolkit/components/tooltiptext/tests/browser_shadow_dom_tooltip.js
--- a/docshell/base/nsDocShellTreeOwner.cpp
+++ b/docshell/base/nsDocShellTreeOwner.cpp
@@ -1213,17 +1213,17 @@ ChromeTooltipListener::MouseMove(Event* 
 
   if (mTooltipTimer) {
     mTooltipTimer->Cancel();
   }
 
   if (!mShowingTooltip && !mTooltipShownOnce) {
     nsIEventTarget* target = nullptr;
 
-    nsCOMPtr<EventTarget> eventTarget = aMouseEvent->GetTarget();
+    nsCOMPtr<EventTarget> eventTarget = aMouseEvent->GetComposedTarget();
     if (eventTarget) {
       mPossibleTooltipNode = do_QueryInterface(eventTarget);
       nsCOMPtr<nsIGlobalObject> global(eventTarget->GetOwnerGlobal());
       if (global) {
         target = global->EventTargetFor(TaskCategory::UI);
       }
     }
 
@@ -1312,16 +1312,22 @@ ChromeTooltipListener::HideTooltip()
 //   -- the x/y coordinates of the mouse      (mMouseClientY, mMouseClientX)
 //   -- the dom node the user hovered over    (mPossibleTooltipNode)
 void
 ChromeTooltipListener::sTooltipCallback(nsITimer* aTimer,
                                         void* aChromeTooltipListener)
 {
   auto self = static_cast<ChromeTooltipListener*>(aChromeTooltipListener);
   if (self && self->mPossibleTooltipNode) {
+    if (!self->mPossibleTooltipNode->IsInComposedDoc()) {
+      // release tooltip target if there is one, NO MATTER WHAT
+      self->mPossibleTooltipNode = nullptr;
+      return;
+    }
+
     // The actual coordinates we want to put the tooltip at are relative to the
     // toplevel docshell of our mWebBrowser.  We know what the screen
     // coordinates of the mouse event were, which means we just need the screen
     // coordinates of the docshell.  Unfortunately, there is no good way to
     // find those short of groveling for the presentation in that docshell and
     // finding the screen coords of its toplevel widget...
     nsCOMPtr<nsIDocShell> docShell =
       do_GetInterface(static_cast<nsIWebBrowser*>(self->mWebBrowser));
--- a/dom/webidl/Element.webidl
+++ b/dom/webidl/Element.webidl
@@ -261,16 +261,20 @@ partial interface Element {
   [BinaryName="shadowRootByMode", Func="nsDocument::IsShadowDOMEnabled"]
   readonly attribute ShadowRoot? shadowRoot;
 
   [ChromeOnly, Func="nsDocument::IsShadowDOMEnabled", BinaryName="shadowRoot"]
   readonly attribute ShadowRoot? openOrClosedShadowRoot;
 
   [BinaryName="assignedSlotByMode", Func="nsDocument::IsShadowDOMEnabled"]
   readonly attribute HTMLSlotElement? assignedSlot;
+
+  [ChromeOnly, BinaryName="assignedSlot", Func="nsDocument::IsShadowDOMEnabled"]
+  readonly attribute HTMLSlotElement? openOrClosedAssignedSlot;
+
   [CEReactions, Unscopable, SetterThrows, Func="nsDocument::IsShadowDOMEnabled"]
            attribute DOMString slot;
 };
 
 Element implements ChildNode;
 Element implements NonDocumentTypeChildNode;
 Element implements ParentNode;
 Element implements Animatable;
--- a/dom/webidl/Text.webidl
+++ b/dom/webidl/Text.webidl
@@ -16,11 +16,14 @@ interface Text : CharacterData {
   Text splitText(unsigned long offset);
   [Throws]
   readonly attribute DOMString wholeText;
 };
 
 partial interface Text {
   [BinaryName="assignedSlotByMode", Func="nsTextNode::IsShadowDOMEnabled"]
   readonly attribute HTMLSlotElement? assignedSlot;
+
+  [ChromeOnly, BinaryName="assignedSlot", Func="nsTextNode::IsShadowDOMEnabled"]
+  readonly attribute HTMLSlotElement? openOrClosedAssignedSlot;
 };
 
 Text implements GeometryUtils;
--- a/layout/xul/nsXULTooltipListener.cpp
+++ b/layout/xul/nsXULTooltipListener.cpp
@@ -97,17 +97,23 @@ nsXULTooltipListener::MouseOut(Event* aE
     return;
 #endif
 
 #ifdef MOZ_XUL
   // check to see if the mouse left the targetNode, and if so,
   // hide the tooltip
   if (currentTooltip) {
     // which node did the mouse leave?
-    nsCOMPtr<nsINode> targetNode = do_QueryInterface(aEvent->GetTarget());
+    EventTarget* eventTarget = aEvent->GetComposedTarget();
+    nsCOMPtr<nsIContent> content = do_QueryInterface(eventTarget);
+    if (content && !content->GetContainingShadow()) {
+      eventTarget = aEvent->GetTarget();
+    }
+
+    nsCOMPtr<nsINode> targetNode = do_QueryInterface(eventTarget);
 
     nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
     if (pm) {
       nsCOMPtr<nsINode> tooltipNode =
         pm->GetLastTriggerTooltipNode(currentTooltip->GetComposedDoc());
       if (tooltipNode == targetNode) {
         // if the target node is the current tooltip target node, the mouse
         // left the node the tooltip appeared on, so close the tooltip.
@@ -168,17 +174,21 @@ nsXULTooltipListener::MouseMove(Event* a
   // so that the delay is from when the mouse stops moving, not when it enters
   // the node.
   KillTooltipTimer();
 
   // If the mouse moves while the tooltip is up, hide it. If nothing is
   // showing and the tooltip hasn't been displayed since the mouse entered
   // the node, then start the timer to show the tooltip.
   if (!currentTooltip && !mTooltipShownOnce) {
-    nsCOMPtr<EventTarget> eventTarget = aEvent->GetTarget();
+    nsCOMPtr<EventTarget> eventTarget = aEvent->GetComposedTarget();
+    nsCOMPtr<nsIContent> content = do_QueryInterface(eventTarget);
+    if (content && !content->GetContainingShadow()) {
+      eventTarget = aEvent->GetTarget();
+    }
 
     // don't show tooltips attached to elements outside of a menu popup
     // when hovering over an element inside it. The popupsinherittooltip
     // attribute may be used to disable this behaviour, which is useful for
     // large menu hierarchies such as bookmarks.
     if (!sourceContent->IsElement() ||
         !sourceContent->AsElement()->AttrValueIs(kNameSpaceID_None,
                                                  nsGkAtoms::popupsinherittooltip,
--- a/toolkit/components/tooltiptext/TooltipTextProvider.js
+++ b/toolkit/components/tooltiptext/TooltipTextProvider.js
@@ -4,21 +4,20 @@
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 function TooltipTextProvider() {}
 
 TooltipTextProvider.prototype = {
   getNodeText(tipElement, textOut, directionOut) {
-    // Don't show the tooltip if the tooltip node is a document, browser, or disconnected.
+    // Don't show the tooltip if the tooltip node is a document or browser.
+    // Caller should ensure the node is in (composed) document.
     if (!tipElement || !tipElement.ownerDocument ||
-        tipElement.localName == "browser" ||
-        (tipElement.ownerDocument.compareDocumentPosition(tipElement) &
-         tipElement.ownerDocument.DOCUMENT_POSITION_DISCONNECTED)) {
+        tipElement.localName == "browser") {
       return false;
     }
 
     var defView = tipElement.ownerGlobal;
     // XXX Work around bug 350679:
     // "Tooltips can be fired in documents with no view".
     if (!defView)
       return false;
@@ -118,17 +117,28 @@ TooltipTextProvider.prototype = {
               break;
             }
           }
         }
 
         usedTipElement = tipElement;
       }
 
-      tipElement = tipElement.parentNode;
+      let parent = tipElement.parentNode;
+      if (defView.ShadowRoot &&
+          parent instanceof defView.ShadowRoot) {
+        tipElement = parent.host;
+      } else {
+        let slot = tipElement.openOrClosedAssignedSlot;
+        if (slot) {
+          tipElement = slot;
+        } else {
+          tipElement = parent;
+        }
+      }
     }
 
     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");
 
         if (usedTipElement) {
--- a/toolkit/components/tooltiptext/tests/browser.ini
+++ b/toolkit/components/tooltiptext/tests/browser.ini
@@ -1,8 +1,9 @@
 [browser_bug329212.js]
 support-files = title_test.svg
 [browser_bug331772_xul_tooltiptext_in_html.js]
 support-files = xul_tooltiptext.xhtml
 [browser_bug561623.js]
 [browser_bug581947.js]
 [browser_input_file_tooltips.js]
 skip-if = os == 'win' && os_version == '10.0'  # Permafail on Win 10 (bug 1400368)
+[browser_shadow_dom_tooltip.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/tooltiptext/tests/browser_shadow_dom_tooltip.js
@@ -0,0 +1,131 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+add_task(async function setup() {
+  await SpecialPowers.pushPrefEnv(
+    {"set": [["ui.tooltipDelay", 0],
+             ["dom.webcomponents.shadowdom.enabled", true]]});
+});
+
+add_task(async function test_title_in_shadow_dom() {
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+  info("Moving mouse out of the way.");
+  await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 300, 300);
+
+  info("creating host");
+  await ContentTask.spawn(tab.linkedBrowser, {}, async function() {
+    let doc = content.document;
+    let host = doc.createElement("div");
+    doc.body.appendChild(host);
+    host.setAttribute("style", "position: absolute; top: 0; left: 0;");
+    var sr = host.attachShadow({ mode: "closed" });
+    sr.innerHTML = "<div title='shadow' style='width: 200px; height: 200px;'>shadow</div>";
+  });
+
+  let awaitTooltipOpen = new Promise(resolve => {
+    let tooltipId = Services.appinfo.browserTabsRemoteAutostart ?
+                      "remoteBrowserTooltip" :
+                      "aHTMLTooltip";
+    let tooltip = document.getElementById(tooltipId);
+    tooltip.addEventListener("popupshown", function(event) {
+      resolve(event.target);
+    }, {once: true});
+  });
+  info("Initial mouse move");
+  await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 50, 5);
+  info("Waiting");
+  await new Promise(resolve => setTimeout(resolve, 400));
+  info("Second mouse move");
+  await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 70, 5);
+  info("Waiting for tooltip to open");
+  let tooltip = await awaitTooltipOpen;
+
+  is(tooltip.getAttribute("label"), "shadow", "tooltip label should match expectation");
+
+  info("Closing tab");
+  BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_title_in_light_dom() {
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+  info("Moving mouse out of the way.");
+  await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 300, 300);
+
+  info("creating host");
+  await ContentTask.spawn(tab.linkedBrowser, {}, async function() {
+    let doc = content.document;
+    let host = doc.createElement("div");
+    host.title = "light";
+    doc.body.appendChild(host);
+    host.setAttribute("style", "position: absolute; top: 0; left: 0;");
+    var sr = host.attachShadow({ mode: "closed" });
+    sr.innerHTML = "<div style='width: 200px; height: 200px;'>shadow</div>";
+  });
+
+  let awaitTooltipOpen = new Promise(resolve => {
+    let tooltipId = Services.appinfo.browserTabsRemoteAutostart ?
+                      "remoteBrowserTooltip" :
+                      "aHTMLTooltip";
+    let tooltip = document.getElementById(tooltipId);
+    tooltip.addEventListener("popupshown", function(event) {
+      resolve(event.target);
+    }, {once: true});
+  });
+  info("Initial mouse move");
+  await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 50, 5);
+  info("Waiting");
+  await new Promise(resolve => setTimeout(resolve, 400));
+  info("Second mouse move");
+  await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 70, 5);
+  info("Waiting for tooltip to open");
+  let tooltip = await awaitTooltipOpen;
+
+  is(tooltip.getAttribute("label"), "light", "tooltip label should match expectation");
+
+  info("Closing tab");
+  BrowserTestUtils.removeTab(tab);
+});
+
+
+add_task(async function test_title_through_slot() {
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+  info("Moving mouse out of the way.");
+  await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 300, 300);
+
+  info("creating host");
+  await ContentTask.spawn(tab.linkedBrowser, {}, async function() {
+    let doc = content.document;
+    let host = doc.createElement("div");
+    host.title = "light";
+    host.innerHTML = "<div style='width: 200px; height: 200px;'>light</div>"
+    doc.body.appendChild(host);
+    host.setAttribute("style", "position: absolute; top: 0; left: 0;");
+    var sr = host.attachShadow({ mode: "closed" });
+    sr.innerHTML = "<div title='shadow' style='width: 200px; height: 200px;'><slot></slot></div>";
+  });
+
+  let awaitTooltipOpen = new Promise(resolve => {
+    let tooltipId = Services.appinfo.browserTabsRemoteAutostart ?
+                      "remoteBrowserTooltip" :
+                      "aHTMLTooltip";
+    let tooltip = document.getElementById(tooltipId);
+    tooltip.addEventListener("popupshown", function(event) {
+      resolve(event.target);
+    }, {once: true});
+  });
+  info("Initial mouse move");
+  await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 50, 5);
+  info("Waiting");
+  await new Promise(resolve => setTimeout(resolve, 400));
+  info("Second mouse move");
+  await EventUtils.synthesizeAndWaitNativeMouseMove(tab.linkedBrowser, 70, 5);
+  info("Waiting for tooltip to open");
+  let tooltip = await awaitTooltipOpen;
+
+  is(tooltip.getAttribute("label"), "shadow", "tooltip label should match expectation");
+
+  info("Closing tab");
+  BrowserTestUtils.removeTab(tab);
+});