forked from mirrors/gecko-dev
Bug 1873833 - Make tooltips work on anonymous content / UA widgets. r=smaug,Gijs
We also simplify the tooltip text provider loop, and make it work better now that e.g. the anonymous button in file inputs can be targeted. Differential Revision: https://phabricator.services.mozilla.com/D203759
This commit is contained in:
parent
18cbc7bb3b
commit
d7f3627404
6 changed files with 205 additions and 153 deletions
|
|
@ -1177,7 +1177,7 @@ nsresult ChromeTooltipListener::MouseMove(Event* aMouseEvent) {
|
|||
}
|
||||
|
||||
if (!mShowingTooltip) {
|
||||
if (nsCOMPtr<EventTarget> eventTarget = aMouseEvent->GetComposedTarget()) {
|
||||
if (nsCOMPtr<EventTarget> eventTarget = aMouseEvent->GetOriginalTarget()) {
|
||||
mPossibleTooltipNode = nsINode::FromEventTarget(eventTarget);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,16 +80,9 @@ void nsXULTooltipListener::MouseOut(Event* aEvent) {
|
|||
// check to see if the mouse left the targetNode, and if so,
|
||||
// hide the tooltip
|
||||
if (currentTooltip) {
|
||||
// which node did the mouse leave?
|
||||
EventTarget* eventTarget = aEvent->GetComposedTarget();
|
||||
nsCOMPtr<nsINode> targetNode = nsINode::FromEventTargetOrNull(eventTarget);
|
||||
if (targetNode && targetNode->IsContent() &&
|
||||
!targetNode->AsContent()->GetContainingShadow()) {
|
||||
eventTarget = aEvent->GetTarget();
|
||||
}
|
||||
|
||||
nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
|
||||
if (pm) {
|
||||
nsCOMPtr<nsINode> targetNode =
|
||||
nsINode::FromEventTargetOrNull(aEvent->GetOriginalTarget());
|
||||
if (nsXULPopupManager* pm = nsXULPopupManager::GetInstance()) {
|
||||
nsCOMPtr<nsINode> tooltipNode =
|
||||
pm->GetLastTriggerTooltipNode(currentTooltip->GetComposedDoc());
|
||||
|
||||
|
|
@ -99,8 +92,7 @@ void nsXULTooltipListener::MouseOut(Event* aEvent) {
|
|||
// tooltip appears positioned near the mouse.
|
||||
nsCOMPtr<EventTarget> relatedTarget =
|
||||
aEvent->AsMouseEvent()->GetRelatedTarget();
|
||||
nsIContent* relatedContent =
|
||||
nsIContent::FromEventTargetOrNull(relatedTarget);
|
||||
auto* relatedContent = nsIContent::FromEventTargetOrNull(relatedTarget);
|
||||
if (tooltipNode == targetNode && relatedContent != currentTooltip) {
|
||||
HideTooltip();
|
||||
// reset special tree tracking
|
||||
|
|
@ -134,12 +126,12 @@ void nsXULTooltipListener::MouseMove(Event* aEvent) {
|
|||
}
|
||||
|
||||
nsCOMPtr<nsIContent> currentTooltip = do_QueryReferent(mCurrentTooltip);
|
||||
nsCOMPtr<EventTarget> eventTarget = aEvent->GetComposedTarget();
|
||||
nsIContent* content = nsIContent::FromEventTargetOrNull(eventTarget);
|
||||
auto* const mouseMoveTarget =
|
||||
nsIContent::FromEventTargetOrNull(aEvent->GetOriginalTarget());
|
||||
|
||||
bool isSameTarget = true;
|
||||
nsCOMPtr<nsIContent> tempContent = do_QueryReferent(mPreviousMouseMoveTarget);
|
||||
if (tempContent && tempContent != content) {
|
||||
if (tempContent && tempContent != mouseMoveTarget) {
|
||||
isSameTarget = false;
|
||||
}
|
||||
|
||||
|
|
@ -154,13 +146,15 @@ void nsXULTooltipListener::MouseMove(Event* aEvent) {
|
|||
return;
|
||||
}
|
||||
mMouseScreenPoint = newMouseScreenPoint;
|
||||
mPreviousMouseMoveTarget = do_GetWeakReference(content);
|
||||
mPreviousMouseMoveTarget = do_GetWeakReference(mouseMoveTarget);
|
||||
|
||||
nsCOMPtr<nsIContent> sourceContent =
|
||||
do_QueryInterface(aEvent->GetCurrentTarget());
|
||||
auto* const sourceContent =
|
||||
nsIContent::FromEventTargetOrNull(aEvent->GetCurrentTarget());
|
||||
mSourceNode = do_GetWeakReference(sourceContent);
|
||||
mIsSourceTree = sourceContent->IsXULElement(nsGkAtoms::treechildren);
|
||||
if (mIsSourceTree) CheckTreeBodyMove(mouseEvent);
|
||||
if (mIsSourceTree) {
|
||||
CheckTreeBodyMove(mouseEvent);
|
||||
}
|
||||
|
||||
// as the mouse moves, we want to make sure we reset the timer to show it,
|
||||
// so that the delay is from when the mouse stops moving, not when it enters
|
||||
|
|
@ -192,8 +186,7 @@ void nsXULTooltipListener::MouseMove(Event* aEvent) {
|
|||
kNameSpaceID_None, nsGkAtoms::popupsinherittooltip,
|
||||
nsGkAtoms::_true, eCaseMatters));
|
||||
if (!allowTooltipCrossingPopup) {
|
||||
for (nsIContent* targetContent =
|
||||
nsIContent::FromEventTargetOrNull(eventTarget);
|
||||
for (auto* targetContent = mouseMoveTarget;
|
||||
targetContent && targetContent != sourceContent;
|
||||
targetContent = targetContent->GetFlattenedTreeParent()) {
|
||||
if (targetContent->IsAnyOfXULElements(
|
||||
|
|
@ -204,7 +197,7 @@ void nsXULTooltipListener::MouseMove(Event* aEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
mTargetNode = do_GetWeakReference(eventTarget);
|
||||
mTargetNode = do_GetWeakReference(mouseMoveTarget);
|
||||
if (mTargetNode) {
|
||||
nsresult rv = NS_NewTimerWithFuncCallback(
|
||||
getter_AddRefs(mTooltipTimer), sTooltipCallback, this,
|
||||
|
|
@ -218,7 +211,9 @@ void nsXULTooltipListener::MouseMove(Event* aEvent) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (mIsSourceTree) return;
|
||||
if (mIsSourceTree) {
|
||||
return;
|
||||
}
|
||||
// Hide the tooltip if it is currently showing.
|
||||
if (currentTooltip) {
|
||||
HideTooltip();
|
||||
|
|
@ -505,7 +500,9 @@ static void GetImmediateChild(nsIContent* aContent, nsAtom* aTag,
|
|||
|
||||
nsresult nsXULTooltipListener::FindTooltip(nsIContent* aTarget,
|
||||
nsIContent** aTooltip) {
|
||||
if (!aTarget) return NS_ERROR_NULL_POINTER;
|
||||
if (!aTarget) {
|
||||
return NS_ERROR_NULL_POINTER;
|
||||
}
|
||||
|
||||
// before we go on, make sure that target node still has a window
|
||||
Document* document = aTarget->GetComposedDoc();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,37 @@
|
|||
|
||||
export function TooltipTextProvider() {}
|
||||
|
||||
function getFileInputTitleText(tipElement) {
|
||||
let files = tipElement.files;
|
||||
let bundle = Services.strings.createBundle(
|
||||
"chrome://global/locale/layout/HtmlForm.properties"
|
||||
);
|
||||
if (!files.length) {
|
||||
return bundle.GetStringFromName(
|
||||
tipElement.multiple ? "NoFilesSelected" : "NoFileSelected"
|
||||
);
|
||||
}
|
||||
let 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) {
|
||||
const l10n = new Localization(["toolkit/global/htmlForm.ftl"], true);
|
||||
const andXMoreStr = l10n.formatValueSync("input-file-and-more-files", {
|
||||
fileCount: files.length - TRUNCATED_FILE_COUNT,
|
||||
});
|
||||
titleText += "\n" + andXMoreStr;
|
||||
}
|
||||
return titleText;
|
||||
}
|
||||
|
||||
TooltipTextProvider.prototype = {
|
||||
getNodeText(tipElement, textOut, directionOut) {
|
||||
// Don't show the tooltip if the tooltip node is a document or browser.
|
||||
|
|
@ -29,89 +60,22 @@ TooltipTextProvider.prototype = {
|
|||
|
||||
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 (
|
||||
(defView.HTMLInputElement.isInstance(tipElement) ||
|
||||
defView.HTMLTextAreaElement.isInstance(tipElement) ||
|
||||
defView.HTMLSelectElement.isInstance(tipElement) ||
|
||||
defView.HTMLButtonElement.isInstance(tipElement)) &&
|
||||
!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;
|
||||
for (; tipElement; tipElement = tipElement.flattenedTreeParentNode) {
|
||||
if (tipElement.nodeType != defView.Node.ELEMENT_NODE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the element is an <input type='file'> without a title, we should show
|
||||
// the current file selection.
|
||||
if (
|
||||
!titleText &&
|
||||
defView.HTMLInputElement.isInstance(tipElement) &&
|
||||
tipElement.type == "file" &&
|
||||
!tipElement.hasAttribute("title")
|
||||
) {
|
||||
let files = tipElement.files;
|
||||
|
||||
try {
|
||||
var bundle = Services.strings.createBundle(
|
||||
"chrome://global/locale/layout/HtmlForm.properties"
|
||||
);
|
||||
if (!files.length) {
|
||||
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) {
|
||||
const l10n = new Localization(
|
||||
["toolkit/global/htmlForm.ftl"],
|
||||
true
|
||||
);
|
||||
const andXMoreStr = l10n.formatValueSync(
|
||||
"input-file-and-more-files",
|
||||
{ fileCount: files.length - TRUNCATED_FILE_COUNT }
|
||||
);
|
||||
titleText += "\n" + andXMoreStr;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Check texts against null so that title="" can be used to undefine a
|
||||
// title on a child element.
|
||||
let usedTipElement = null;
|
||||
while (
|
||||
tipElement &&
|
||||
titleText == null &&
|
||||
XLinkTitleText == null &&
|
||||
SVGTitleText == null &&
|
||||
XULtooltiptextText == null
|
||||
) {
|
||||
if (tipElement.nodeType == defView.Node.ELEMENT_NODE) {
|
||||
if (tipElement.namespaceURI == XUL_NS) {
|
||||
XULtooltiptextText = tipElement.hasAttribute("tooltiptext")
|
||||
lookingForSVGTitle = false;
|
||||
// NOTE: getAttribute behaves differently for XUL so we can't rely on
|
||||
// it returning null, see bug 232598.
|
||||
titleText = tipElement.hasAttribute("tooltiptext")
|
||||
? tipElement.getAttribute("tooltiptext")
|
||||
: null;
|
||||
} else if (!defView.SVGElement.isInstance(tipElement)) {
|
||||
lookingForSVGTitle = false;
|
||||
titleText = tipElement.getAttribute("title");
|
||||
}
|
||||
|
||||
|
|
@ -124,37 +88,61 @@ TooltipTextProvider.prototype = {
|
|||
) {
|
||||
XLinkTitleText = tipElement.getAttributeNS(XLinkNS, "title");
|
||||
}
|
||||
|
||||
// If the element is invalid per HTML5 Forms specifications and has no title,
|
||||
// show the constraint validation error message.
|
||||
if (
|
||||
titleText == null &&
|
||||
(defView.HTMLInputElement.isInstance(tipElement) ||
|
||||
defView.HTMLTextAreaElement.isInstance(tipElement) ||
|
||||
defView.HTMLSelectElement.isInstance(tipElement) ||
|
||||
defView.HTMLButtonElement.isInstance(tipElement)) &&
|
||||
!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 == null &&
|
||||
defView.HTMLInputElement.isInstance(tipElement) &&
|
||||
tipElement.type == "file"
|
||||
) {
|
||||
try {
|
||||
titleText = getFileInputTitleText(tipElement);
|
||||
} catch (ex) {}
|
||||
}
|
||||
|
||||
if (
|
||||
lookingForSVGTitle &&
|
||||
(!defView.SVGElement.isInstance(tipElement) ||
|
||||
tipElement.parentNode.nodeType == defView.Node.DOCUMENT_NODE)
|
||||
tipElement.parentNode.nodeType != defView.Node.DOCUMENT_NODE
|
||||
) {
|
||||
lookingForSVGTitle = false;
|
||||
}
|
||||
if (lookingForSVGTitle) {
|
||||
for (let childNode of tipElement.childNodes) {
|
||||
if (defView.SVGTitleElement.isInstance(childNode)) {
|
||||
SVGTitleText = childNode.textContent;
|
||||
titleText = childNode.textContent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usedTipElement = tipElement;
|
||||
// Check texts against null so that title="" can be used to undefine a
|
||||
// title on a child element.
|
||||
if (titleText != null || XLinkTitleText != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
tipElement = tipElement.flattenedTreeParentNode;
|
||||
}
|
||||
|
||||
return [titleText, XLinkTitleText, SVGTitleText, XULtooltiptextText].some(
|
||||
function (t) {
|
||||
return [titleText, XLinkTitleText].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) {
|
||||
if (tipElement) {
|
||||
direction = defView
|
||||
.getComputedStyle(usedTipElement)
|
||||
.getComputedStyle(tipElement)
|
||||
.getPropertyValue("direction");
|
||||
}
|
||||
|
||||
|
|
@ -163,8 +151,7 @@ TooltipTextProvider.prototype = {
|
|||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
classID: Components.ID("{f376627f-0bbc-47b8-887e-fc92574cc91f}"),
|
||||
|
|
|
|||
|
|
@ -13,4 +13,6 @@ support-files = ["xul_tooltiptext.xhtml"]
|
|||
["browser_input_file_tooltips.js"]
|
||||
skip-if = ["os == 'win' && os_version == '10.0'"] # Permafail on Win 10 (bug 1400368)
|
||||
|
||||
["browser_nac_tooltip.js"]
|
||||
|
||||
["browser_shadow_dom_tooltip.js"]
|
||||
|
|
|
|||
66
toolkit/components/tooltiptext/tests/browser_nac_tooltip.js
Normal file
66
toolkit/components/tooltiptext/tests/browser_nac_tooltip.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
/* eslint-disable mozilla/no-arbitrary-setTimeout */
|
||||
|
||||
"use strict";
|
||||
|
||||
add_setup(async function () {
|
||||
await SpecialPowers.pushPrefEnv({ set: [["ui.tooltip.delay_ms", 0]] });
|
||||
});
|
||||
|
||||
add_task(async function () {
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: "data:text/html,<!DOCTYPE html>",
|
||||
},
|
||||
async function (browser) {
|
||||
info("Moving mouse out of the way.");
|
||||
await EventUtils.synthesizeAndWaitNativeMouseMove(browser, 300, 300);
|
||||
|
||||
await SpecialPowers.spawn(browser, [], function () {
|
||||
let widget = content.document.insertAnonymousContent();
|
||||
widget.root.innerHTML = `<button style="pointer-events: auto; position: absolute; width: 200px; height: 200px;" title="foo">bar</button>`;
|
||||
let tttp = Cc[
|
||||
"@mozilla.org/embedcomp/default-tooltiptextprovider;1"
|
||||
].getService(Ci.nsITooltipTextProvider);
|
||||
|
||||
let text = {};
|
||||
let dir = {};
|
||||
ok(
|
||||
tttp.getNodeText(widget.root.querySelector("button"), text, dir),
|
||||
"A tooltip should be shown for NAC"
|
||||
);
|
||||
is(text.value, "foo", "Tooltip text should be correct");
|
||||
});
|
||||
|
||||
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(browser, 50, 5);
|
||||
info("Waiting");
|
||||
await new Promise(resolve => setTimeout(resolve, 400));
|
||||
info("Second mouse move");
|
||||
await EventUtils.synthesizeAndWaitNativeMouseMove(browser, 70, 5);
|
||||
info("Waiting for tooltip to open");
|
||||
let tooltip = await awaitTooltipOpen;
|
||||
is(
|
||||
tooltip.getAttribute("label"),
|
||||
"foo",
|
||||
"tooltip label should match expectation"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
@ -15,12 +15,12 @@
|
|||
|
||||
<box id="parent" tooltiptext="Box Tooltip" style="margin: 10px">
|
||||
<button id="withtext" label="Tooltip Text" tooltiptext="Button Tooltip"
|
||||
style="-moz-appearance: none; padding: 0;"/>
|
||||
<button id="without" label="No Tooltip" style="-moz-appearance: none; padding: 0;"/>
|
||||
style="appearance: none; padding: 0;"/>
|
||||
<button id="without" label="No Tooltip" style="appearance: none; padding: 0;"/>
|
||||
<!-- remove the native theme and borders to avoid some platform
|
||||
specific sizing differences -->
|
||||
<button id="withtooltip" label="Tooltip Element" tooltip="thetooltip"
|
||||
class="plain" style="-moz-appearance: none; padding: 0;"/>
|
||||
class="plain" style="appearance: none; padding: 0;"/>
|
||||
</box>
|
||||
|
||||
<script class="testbody" type="application/javascript">
|
||||
|
|
|
|||
Loading…
Reference in a new issue