diff options
Diffstat (limited to 'toolkit/components/tooltiptext')
11 files changed, 532 insertions, 0 deletions
diff --git a/toolkit/components/tooltiptext/TooltipTextProvider.js b/toolkit/components/tooltiptext/TooltipTextProvider.js new file mode 100644 index 000000000..a63ab83ad --- /dev/null +++ b/toolkit/components/tooltiptext/TooltipTextProvider.js @@ -0,0 +1,148 @@ +/* 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/. */ + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.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. + if (!tipElement || !tipElement.ownerDocument || + tipElement.localName == "browser" || + (tipElement.ownerDocument.compareDocumentPosition(tipElement) & + tipElement.ownerDocument.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 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; + } + + // 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; + + 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"); + } + } 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 = {}; + Cu.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 == defView.Node.ELEMENT_NODE) { + if (tipElement.namespaceURI == XULNS) + XULtooltiptextText = tipElement.getAttribute("tooltiptext"); + else if (!(tipElement instanceof defView.SVGElement)) + titleText = tipElement.getAttribute("title"); + + if ((tipElement instanceof defView.HTMLAnchorElement || + tipElement instanceof defView.HTMLAreaElement || + tipElement instanceof defView.HTMLLinkElement || + tipElement instanceof defView.SVGAElement) && tipElement.href) { + XLinkTitleText = tipElement.getAttributeNS(XLinkNS, "title"); + } + if (lookingForSVGTitle && + (!(tipElement instanceof defView.SVGElement) || + tipElement.parentNode.nodeType == defView.Node.DOCUMENT_NODE)) { + lookingForSVGTitle = false; + } + if (lookingForSVGTitle) { + for (let childNode of tipElement.childNodes) { + if (childNode instanceof defView.SVGTitleElement) { + SVGTitleText = childNode.textContent; + break; + } + } + } + + direction = defView.getComputedStyle(tipElement, "") + .getPropertyValue("direction"); + } + + tipElement = tipElement.parentNode; + } + + 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 false; + }); + }, + + classID : Components.ID("{f376627f-0bbc-47b8-887e-fc92574cc91f}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsITooltipTextProvider]), +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TooltipTextProvider]); + diff --git a/toolkit/components/tooltiptext/TooltipTextProvider.manifest b/toolkit/components/tooltiptext/TooltipTextProvider.manifest new file mode 100644 index 000000000..a7dac6cd9 --- /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} diff --git a/toolkit/components/tooltiptext/moz.build b/toolkit/components/tooltiptext/moz.build new file mode 100644 index 000000000..c75e6b7a4 --- /dev/null +++ b/toolkit/components/tooltiptext/moz.build @@ -0,0 +1,15 @@ +# -*- Mode: python; 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 += ['tests/browser.ini'] + +EXTRA_COMPONENTS += [ + 'TooltipTextProvider.js', + 'TooltipTextProvider.manifest', +] + +with Files('**'): + BUG_COMPONENT = ('Toolkit', 'General') diff --git a/toolkit/components/tooltiptext/tests/browser.ini b/toolkit/components/tooltiptext/tests/browser.ini new file mode 100644 index 000000000..9896fcd2c --- /dev/null +++ b/toolkit/components/tooltiptext/tests/browser.ini @@ -0,0 +1,7 @@ +[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] diff --git a/toolkit/components/tooltiptext/tests/browser_bug329212.js b/toolkit/components/tooltiptext/tests/browser_bug329212.js new file mode 100644 index 000000000..b3434eff6 --- /dev/null +++ b/toolkit/components/tooltiptext/tests/browser_bug329212.js @@ -0,0 +1,35 @@ +"use strict"; + +add_task(function*() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "http://mochi.test:8888/browser/toolkit/components/tooltiptext/tests/title_test.svg", + }, function*(browser) { + yield ContentTask.spawn(browser, "", function() { + let tttp = Cc["@mozilla.org/embedcomp/default-tooltiptextprovider;1"] + .getService(Ci.nsITooltipTextProvider); + function checkElement(id, expectedTooltipText) { + let el = content.document.getElementById(id); + let textObj = {}; + let shouldHaveTooltip = expectedTooltipText !== null; + is(tttp.getNodeText(el, textObj, {}), shouldHaveTooltip, + "element " + id + " should " + (shouldHaveTooltip ? "" : "not ") + "have a tooltip"); + if (shouldHaveTooltip) { + is(textObj.value, expectedTooltipText, + "element " + id + " should have the right tooltip text"); + } + } + checkElement("svg1", "This is a non-root SVG element title"); + checkElement("text1", "\n\n\n This is a title\n\n "); + checkElement("text2", null); + checkElement("text3", null); + checkElement("link1", "\n This is a title\n "); + checkElement("text4", "\n This is a title\n "); + checkElement("link2", null); + checkElement("link3", "This is an xlink:title attribute"); + checkElement("link4", "This is an xlink:title attribute"); + checkElement("text5", null); + }); + }); +}); + diff --git a/toolkit/components/tooltiptext/tests/browser_bug331772_xul_tooltiptext_in_html.js b/toolkit/components/tooltiptext/tests/browser_bug331772_xul_tooltiptext_in_html.js new file mode 100644 index 000000000..23d8c4a6e --- /dev/null +++ b/toolkit/components/tooltiptext/tests/browser_bug331772_xul_tooltiptext_in_html.js @@ -0,0 +1,19 @@ +/** + * Tests that the tooltiptext attribute is used for XUL elements in an HTML doc. + */ +add_task(function*() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "http://mochi.test:8888/browser/toolkit/components/tooltiptext/tests/xul_tooltiptext.xhtml", + }, function*(browser) { + yield ContentTask.spawn(browser, "", function() { + let textObj = {}; + let tttp = Cc["@mozilla.org/embedcomp/default-tooltiptextprovider;1"] + .getService(Ci.nsITooltipTextProvider); + let xulToolbarButton = content.document.getElementById("xulToolbarButton"); + ok(tttp.getNodeText(xulToolbarButton, textObj, {}), "should get tooltiptext"); + is(textObj.value, "XUL tooltiptext"); + }); + }); +}); + diff --git a/toolkit/components/tooltiptext/tests/browser_bug561623.js b/toolkit/components/tooltiptext/tests/browser_bug561623.js new file mode 100644 index 000000000..49c51c4ba --- /dev/null +++ b/toolkit/components/tooltiptext/tests/browser_bug561623.js @@ -0,0 +1,24 @@ +add_task(function*() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "data:text/html,<!DOCTYPE html><html><body><input id='i'></body></html>", + }, function*(browser) { + yield ContentTask.spawn(browser, "", function() { + let tttp = Cc["@mozilla.org/embedcomp/default-tooltiptextprovider;1"] + .getService(Ci.nsITooltipTextProvider); + let i = content.document.getElementById("i"); + + ok(!tttp.getNodeText(i, {}, {}), + "No tooltip should be shown when @title is null"); + + i.title = "foo"; + ok(tttp.getNodeText(i, {}, {}), + "A tooltip should be shown when @title is not the empty string"); + + i.pattern = "bar"; + ok(tttp.getNodeText(i, {}, {}), + "A tooltip should be shown when @title is not the empty string"); + }); + }); +}); + diff --git a/toolkit/components/tooltiptext/tests/browser_bug581947.js b/toolkit/components/tooltiptext/tests/browser_bug581947.js new file mode 100644 index 000000000..034e0a4d1 --- /dev/null +++ b/toolkit/components/tooltiptext/tests/browser_bug581947.js @@ -0,0 +1,87 @@ +function check(aBrowser, aElementName, aBarred, aType) { + return ContentTask.spawn(aBrowser, [aElementName, aBarred, aType], function*([aElementName, aBarred, aType]) { + let e = content.document.createElement(aElementName); + let contentElement = content.document.getElementById('content'); + contentElement.appendChild(e); + + if (aType) { + e.type = aType; + } + + let tttp = Cc["@mozilla.org/embedcomp/default-tooltiptextprovider;1"] + .getService(Ci.nsITooltipTextProvider); + ok(!tttp.getNodeText(e, {}, {}), + "No tooltip should be shown when the element is valid"); + + e.setCustomValidity('foo'); + if (aBarred) { + ok(!tttp.getNodeText(e, {}, {}), + "No tooltip should be shown when the element is barred from constraint validation"); + } else { + ok(tttp.getNodeText(e, {}, {}), + e.tagName + " " +"A tooltip should be shown when the element isn't valid"); + } + + e.setAttribute('title', ''); + ok (!tttp.getNodeText(e, {}, {}), + "No tooltip should be shown if the title attribute is set"); + + e.removeAttribute('title'); + contentElement.setAttribute('novalidate', ''); + ok (!tttp.getNodeText(e, {}, {}), + "No tooltip should be shown if the novalidate attribute is set on the form owner"); + contentElement.removeAttribute('novalidate'); + + e.remove(); + }); +} + +function todo_check(aBrowser, aElementName, aBarred) { + return ContentTask.spawn(aBrowser, [aElementName, aBarred], function*([aElementName, aBarred]) { + let e = content.document.createElement(aElementName); + let contentElement = content.document.getElementById('content'); + contentElement.appendChild(e); + + let caught = false; + try { + e.setCustomValidity('foo'); + } catch (e) { + caught = true; + } + + todo(!caught, "setCustomValidity should exist for " + aElementName); + + e.remove(); + }); +} + +add_task(function*() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "data:text/html,<!DOCTYPE html><html><body><form id='content'></form></body></html>", + }, function*(browser) { + let testData = [ + /* element name, barred */ + [ 'input', false, null], + [ 'textarea', false, null], + [ 'button', true, 'button'], + [ 'button', false, 'submit'], + [ 'select', false, null], + [ 'output', true, null], + [ 'fieldset', true, null], + [ 'object', true, null], + ]; + + for (let data of testData) { + yield check(browser, data[0], data[1], data[2]); + } + + let todo_testData = [ + [ 'keygen', 'false' ], + ]; + + for (let data of todo_testData) { + yield todo_check(browser, data[0], data[1]); + } + }); +}); diff --git a/toolkit/components/tooltiptext/tests/browser_input_file_tooltips.js b/toolkit/components/tooltiptext/tests/browser_input_file_tooltips.js new file mode 100644 index 000000000..a1323095d --- /dev/null +++ b/toolkit/components/tooltiptext/tests/browser_input_file_tooltips.js @@ -0,0 +1,122 @@ + +let tempFile; +add_task(function* setup() { + yield new Promise(resolve => { + SpecialPowers.pushPrefEnv({"set": [["ui.tooltipDelay", 0]]}, resolve); + }); + tempFile = createTempFile(); + registerCleanupFunction(function() { + tempFile.remove(true); + }); +}); + +add_task(function* test_singlefile_selected() { + yield do_test({value: true, result: "testfile_bug1251809"}); +}); + +add_task(function* test_title_set() { + yield do_test({title: "foo", result: "foo"}); +}); + +add_task(function* test_nofile_selected() { + yield do_test({result: "No file selected."}); +}); + +add_task(function* test_multipleset_nofile_selected() { + yield do_test({multiple: true, result: "No files selected."}); +}); + +add_task(function* test_requiredset() { + yield do_test({required: true, result: "Please select a file."}); +}); + +function* do_test(test) { + info(`starting test ${JSON.stringify(test)}`); + + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser); + + info("Moving mouse out of the way."); + yield new Promise(resolve => { + EventUtils.synthesizeNativeMouseMove(tab.linkedBrowser, 300, 300, resolve); + }); + + info("creating input field"); + yield ContentTask.spawn(tab.linkedBrowser, test, function*(test) { + let doc = content.document; + let input = doc.createElement("input"); + doc.body.appendChild(input); + input.id = "test_input"; + input.setAttribute("style", "position: absolute; top: 0; left: 0;"); + input.type = "file"; + if (test.title) { + input.setAttribute("title", test.title); + } + if (test.multiple) { + input.multiple = true; + } + if (test.required) { + input.required = true; + } + }); + + if (test.value) { + info("Creating mock filepicker to select files"); + let MockFilePicker = SpecialPowers.MockFilePicker; + MockFilePicker.init(window); + MockFilePicker.returnValue = MockFilePicker.returnOK; + MockFilePicker.displayDirectory = FileUtils.getDir("TmpD", [], false); + MockFilePicker.returnFiles = [tempFile]; + + try { + // Open the File Picker dialog (MockFilePicker) to select + // the files for the test. + yield BrowserTestUtils.synthesizeMouseAtCenter("#test_input", {}, tab.linkedBrowser); + info("Waiting for the input to have the requisite files"); + yield ContentTask.spawn(tab.linkedBrowser, {}, function*() { + let input = content.document.querySelector("#test_input"); + yield ContentTaskUtils.waitForCondition(() => input.files.length, + "The input should have at least one file selected"); + info(`The input has ${input.files.length} file(s) selected.`); + }); + } finally { + MockFilePicker.cleanup(); + } + } else { + info("No real file selection required."); + } + + let awaitTooltipOpen = new Promise(resolve => { + let tooltipId = Services.appinfo.browserTabsRemoteAutostart ? + "remoteBrowserTooltip" : + "aHTMLTooltip"; + let tooltip = document.getElementById(tooltipId); + tooltip.addEventListener("popupshown", function onpopupshown(event) { + tooltip.removeEventListener("popupshown", onpopupshown); + resolve(event.target); + }); + }); + info("Initial mouse move"); + yield new Promise(resolve => { + EventUtils.synthesizeNativeMouseMove(tab.linkedBrowser, 50, 5, resolve); + }); + info("Waiting"); + yield new Promise(resolve => setTimeout(resolve, 400)); + info("Second mouse move"); + yield new Promise(resolve => { + EventUtils.synthesizeNativeMouseMove(tab.linkedBrowser, 70, 5, resolve); + }); + info("Waiting for tooltip to open"); + let tooltip = yield awaitTooltipOpen; + + is(tooltip.getAttribute("label"), test.result, "tooltip label should match expectation"); + + info("Closing tab"); + yield BrowserTestUtils.removeTab(tab); +} + +function createTempFile() { + let file = FileUtils.getDir("TmpD", [], false); + file.append("testfile_bug1251809"); + file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644); + return file; +} diff --git a/toolkit/components/tooltiptext/tests/title_test.svg b/toolkit/components/tooltiptext/tests/title_test.svg new file mode 100644 index 000000000..7638fd5cc --- /dev/null +++ b/toolkit/components/tooltiptext/tests/title_test.svg @@ -0,0 +1,59 @@ +<svg width="640px" height="480px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <title>This is a root SVG element's title</title> + <foreignObject> + <html xmlns="http://www.w3.org/1999/xhtml"> + <body> + <svg xmlns="http://www.w3.org/2000/svg" id="svg1"> + <title>This is a non-root SVG element title</title> + </svg> + </body> + </html> + </foreignObject> + <text id="text1" x="10px" y="32px" font-size="24px"> + This contains only <title> + <title> + + + This is a title + + </title> + </text> + <text id="text2" x="10px" y="96px" font-size="24px"> + This contains only <desc> + <desc>This is a desc</desc> + </text> + <text id="text3" x="10px" y="128px" font-size="24px" title="ignored for SVG"> + This contains nothing. + </text> + <a id="link1" xlink:href="#"> + This link contains <title> + <title> + This is a title + </title> + <text id="text4" x="10px" y="192px" font-size="24px"> + </text> + </a> + <a id="link2" xlink:href="#"> + <text x="10px" y="192px" font-size="24px"> + This text contains <title> + <title> + This is a title + </title> + </text> + </a> + <a id="link3" xlink:href="#" xlink:title="This is an xlink:title attribute"> + <text x="10px" y="224px" font-size="24px"> + This link contains <title> & xlink:title attr. + <title>This is a title</title> + </text> + </a> + <a id="link4" xlink:href="#" xlink:title="This is an xlink:title attribute"> + <text x="10px" y="256px" font-size="24px"> + This link contains xlink:title attr. + </text> + </a> + <text id="text5" x="10px" y="160px" font-size="24px" + xlink:title="This is an xlink:title attribute but it isn't on a link" > + This contains nothing. + </text> +</svg> diff --git a/toolkit/components/tooltiptext/tests/xul_tooltiptext.xhtml b/toolkit/components/tooltiptext/tests/xul_tooltiptext.xhtml new file mode 100644 index 000000000..4a80864dd --- /dev/null +++ b/toolkit/components/tooltiptext/tests/xul_tooltiptext.xhtml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <xul:toolbox xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <toolbar> + <toolbarbutton id="xulToolbarButton" + tooltiptext="XUL tooltiptext" + title="XUL title"/> + </toolbar> + </xul:toolbox> +</html> + + |