summaryrefslogtreecommitdiffstats
path: root/toolkit/components/tooltiptext
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/tooltiptext')
-rw-r--r--toolkit/components/tooltiptext/TooltipTextProvider.sys.mjs227
-rw-r--r--toolkit/components/tooltiptext/tests/browser.toml2
-rw-r--r--toolkit/components/tooltiptext/tests/browser_input_file_tooltips.js4
-rw-r--r--toolkit/components/tooltiptext/tests/browser_nac_tooltip.js66
-rw-r--r--toolkit/components/tooltiptext/tests/browser_shadow_dom_tooltip.js2
5 files changed, 178 insertions, 123 deletions
diff --git a/toolkit/components/tooltiptext/TooltipTextProvider.sys.mjs b/toolkit/components/tooltiptext/TooltipTextProvider.sys.mjs
index 0ddd594cb6..96a3e8dd5f 100644
--- a/toolkit/components/tooltiptext/TooltipTextProvider.sys.mjs
+++ b/toolkit/components/tooltiptext/TooltipTextProvider.sys.mjs
@@ -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,142 +60,98 @@ 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 (tipElement.namespaceURI == XUL_NS) {
+ 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");
+ }
- // 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) {}
- }
+ if (
+ (defView.HTMLAnchorElement.isInstance(tipElement) ||
+ defView.HTMLAreaElement.isInstance(tipElement) ||
+ defView.HTMLLinkElement.isInstance(tipElement) ||
+ defView.SVGAElement.isInstance(tipElement)) &&
+ tipElement.href
+ ) {
+ XLinkTitleText = tipElement.getAttributeNS(XLinkNS, "title");
+ }
- // 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")
- ? tipElement.getAttribute("tooltiptext")
- : null;
- } else if (!defView.SVGElement.isInstance(tipElement)) {
- titleText = tipElement.getAttribute("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 (
- (defView.HTMLAnchorElement.isInstance(tipElement) ||
- defView.HTMLAreaElement.isInstance(tipElement) ||
- defView.HTMLLinkElement.isInstance(tipElement) ||
- defView.SVGAElement.isInstance(tipElement)) &&
- tipElement.href
- ) {
- XLinkTitleText = tipElement.getAttributeNS(XLinkNS, "title");
- }
- if (
- lookingForSVGTitle &&
- (!defView.SVGElement.isInstance(tipElement) ||
- 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;
- break;
- }
+ // 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 &&
+ tipElement.parentNode.nodeType != defView.Node.DOCUMENT_NODE
+ ) {
+ for (let childNode of tipElement.childNodes) {
+ if (defView.SVGTitleElement.isInstance(childNode)) {
+ titleText = childNode.textContent;
+ break;
}
}
-
- usedTipElement = tipElement;
}
- tipElement = tipElement.flattenedTreeParentNode;
+ // Check texts against null so that title="" can be used to undefine a
+ // title on a child element.
+ if (titleText != null || XLinkTitleText != null) {
+ break;
+ }
}
- 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) {
- direction = defView
- .getComputedStyle(usedTipElement)
- .getPropertyValue("direction");
- }
+ 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");
- directionOut.value = direction;
- return true;
+ if (tipElement) {
+ direction = defView
+ .getComputedStyle(tipElement)
+ .getPropertyValue("direction");
}
- return false;
+ directionOut.value = direction;
+ return true;
}
- );
+
+ return false;
+ });
},
classID: Components.ID("{f376627f-0bbc-47b8-887e-fc92574cc91f}"),
diff --git a/toolkit/components/tooltiptext/tests/browser.toml b/toolkit/components/tooltiptext/tests/browser.toml
index 189f880be2..d03716e683 100644
--- a/toolkit/components/tooltiptext/tests/browser.toml
+++ b/toolkit/components/tooltiptext/tests/browser.toml
@@ -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"]
diff --git a/toolkit/components/tooltiptext/tests/browser_input_file_tooltips.js b/toolkit/components/tooltiptext/tests/browser_input_file_tooltips.js
index 7d6c5043c4..2f1385f37f 100644
--- a/toolkit/components/tooltiptext/tests/browser_input_file_tooltips.js
+++ b/toolkit/components/tooltiptext/tests/browser_input_file_tooltips.js
@@ -2,7 +2,7 @@
let tempFile;
add_setup(async function () {
- await SpecialPowers.pushPrefEnv({ set: [["ui.tooltipDelay", 0]] });
+ await SpecialPowers.pushPrefEnv({ set: [["ui.tooltip.delay_ms", 0]] });
tempFile = createTempFile();
registerCleanupFunction(function () {
tempFile.remove(true);
@@ -63,7 +63,7 @@ async function do_test(test) {
if (test.value) {
info("Creating mock filepicker to select files");
let MockFilePicker = SpecialPowers.MockFilePicker;
- MockFilePicker.init(window);
+ MockFilePicker.init(window.browsingContext);
MockFilePicker.returnValue = MockFilePicker.returnOK;
MockFilePicker.displayDirectory = FileUtils.getDir("TmpD", []);
MockFilePicker.setFiles([tempFile]);
diff --git a/toolkit/components/tooltiptext/tests/browser_nac_tooltip.js b/toolkit/components/tooltiptext/tests/browser_nac_tooltip.js
new file mode 100644
index 0000000000..449c8b9da7
--- /dev/null
+++ b/toolkit/components/tooltiptext/tests/browser_nac_tooltip.js
@@ -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"
+ );
+ }
+ );
+});
diff --git a/toolkit/components/tooltiptext/tests/browser_shadow_dom_tooltip.js b/toolkit/components/tooltiptext/tests/browser_shadow_dom_tooltip.js
index 50386e07e2..4ceb918da1 100644
--- a/toolkit/components/tooltiptext/tests/browser_shadow_dom_tooltip.js
+++ b/toolkit/components/tooltiptext/tests/browser_shadow_dom_tooltip.js
@@ -1,7 +1,7 @@
/* eslint-disable mozilla/no-arbitrary-setTimeout */
add_setup(async function () {
- await SpecialPowers.pushPrefEnv({ set: [["ui.tooltipDelay", 0]] });
+ await SpecialPowers.pushPrefEnv({ set: [["ui.tooltip.delay_ms", 0]] });
});
add_task(async function test_title_in_shadow_dom() {