Bug 1251809 - Add input[type=file] tooltip support for e10s. r=ehsan a=ritu IGNORE IDL
MozReview-Commit-ID: FpwKGrFQNrK
--- a/dom/base/nsIDOMFileList.idl
+++ b/dom/base/nsIDOMFileList.idl
@@ -1,14 +1,14 @@
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/. */
#include "nsISupports.idl"
-[uuid(57128a85-34de-42db-a252-84dd57724a59)]
+[builtinclass, uuid(57128a85-34de-42db-a252-84dd57724a59)]
interface nsIDOMFileList : nsISupports
{
readonly attribute unsigned long length;
// returns a DOM File object
nsISupports item(in unsigned long index);
};
--- a/embedding/browser/nsDocShellTreeOwner.cpp
+++ b/embedding/browser/nsDocShellTreeOwner.cpp
@@ -30,29 +30,31 @@
#include "nsIDOMNodeList.h"
#include "nsIDOMDocument.h"
#include "nsIDOMDocumentType.h"
#include "nsIDOMElement.h"
#include "Link.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/SVGTitleElement.h"
#include "nsIDOMEvent.h"
+#include "nsIDOMFileList.h"
#include "nsIDOMMouseEvent.h"
#include "nsIFormControl.h"
#include "nsIDOMHTMLInputElement.h"
#include "nsIDOMHTMLTextAreaElement.h"
#include "nsIDOMHTMLHtmlElement.h"
#include "nsIDOMHTMLAppletElement.h"
#include "nsIDOMHTMLObjectElement.h"
#include "nsIDOMHTMLEmbedElement.h"
#include "nsIDOMHTMLDocument.h"
#include "nsIImageLoadingContent.h"
#include "nsIWebNavigation.h"
#include "nsIDOMHTMLElement.h"
#include "nsIPresShell.h"
+#include "nsIStringBundle.h"
#include "nsPIDOMWindow.h"
#include "nsPIWindowRoot.h"
#include "nsIDOMWindowCollection.h"
#include "nsIWindowWatcher.h"
#include "nsPIWindowWatcher.h"
#include "nsIPrompt.h"
#include "nsITabParent.h"
#include "nsRect.h"
@@ -63,16 +65,18 @@
#include "nsPresContext.h"
#include "nsViewManager.h"
#include "nsView.h"
#include "nsIDOMDragEvent.h"
#include "nsIConstraintValidation.h"
#include "mozilla/Attributes.h"
#include "mozilla/EventListenerManager.h"
#include "mozilla/dom/Event.h" // for nsIDOMEvent::InternalDOMEvent()
+#include "mozilla/dom/File.h" // for input type=file
+#include "mozilla/dom/FileList.h" // for input type=file
using namespace mozilla;
using namespace mozilla::dom;
// A helper routine that navigates the tricky path from a |nsWebBrowser| to
// a |EventTarget| via the window root and chrome event handler.
static nsresult
GetDOMEventTarget(nsWebBrowser* aInBrowser, EventTarget** aTarget)
@@ -1066,30 +1070,26 @@ public:
DefaultTooltipTextProvider();
NS_DECL_ISUPPORTS
NS_DECL_NSITOOLTIPTEXTPROVIDER
protected:
~DefaultTooltipTextProvider() {}
- nsCOMPtr<nsIAtom> mTag_dialog;
- nsCOMPtr<nsIAtom> mTag_dialogheader;
- nsCOMPtr<nsIAtom> mTag_window;
+ nsCOMPtr<nsIAtom> mTag_dialogHeader;
};
NS_IMPL_ISUPPORTS(DefaultTooltipTextProvider, nsITooltipTextProvider)
DefaultTooltipTextProvider::DefaultTooltipTextProvider()
{
// There are certain element types which we don't want to use
// as tool tip text.
- mTag_dialog = do_GetAtom("dialog");
- mTag_dialogheader = do_GetAtom("dialogheader");
- mTag_window = do_GetAtom("window");
+ mTag_dialogHeader = do_GetAtom("dialogheader");
}
// A helper routine that determines whether we're still interested in SVG
// titles. We need to stop at the SVG root element that has a document node
// parent.
static bool
UseSVGTitle(nsIDOMElement* aCurrElement)
{
@@ -1137,23 +1137,76 @@ DefaultTooltipTextProvider::GetNodeText(
}
}
while (!found && current) {
nsCOMPtr<nsIDOMElement> currElement(do_QueryInterface(current));
if (currElement) {
nsCOMPtr<nsIContent> content(do_QueryInterface(currElement));
if (content) {
- if (!content->IsAnyOfXULElements(mTag_dialog,
- mTag_dialogheader,
- mTag_window)) {
+ if (!content->IsAnyOfXULElements(nsGkAtoms::dialog,
+ mTag_dialogHeader,
+ nsGkAtoms::window)) {
// first try the normal title attribute...
if (!content->IsSVGElement()) {
- currElement->GetAttribute(NS_LITERAL_STRING("title"), outText);
- if (outText.Length()) {
+ // If the element is an <input type="file"> without a title,
+ // we should show the current file selection.
+ if (content->IsHTMLElement(nsGkAtoms::input) &&
+ content->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+ NS_LITERAL_STRING("file"), eIgnoreCase) &&
+ !content->HasAttr(kNameSpaceID_None, nsGkAtoms::title)) {
+ found = true;
+ nsCOMPtr<nsIDOMFileList> fileList;
+ nsCOMPtr<nsIDOMHTMLInputElement> currInputElement(do_QueryInterface(currElement));
+ nsresult rv = currInputElement->GetFiles(getter_AddRefs(fileList));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!fileList) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::services::GetStringBundleService();
+ if (!bundleService) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleService->CreateBundle("chrome://global/locale/layout/HtmlForm.properties",
+ getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t listLength = 0;
+ rv = fileList->GetLength(&listLength);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (listLength == 0) {
+ if (content->HasAttr(kNameSpaceID_None, nsGkAtoms::multiple)) {
+ rv = bundle->GetStringFromName(MOZ_UTF16("NoFilesSelected"),
+ getter_Copies(outText));
+ } else {
+ rv = bundle->GetStringFromName(MOZ_UTF16("NoFileSelected"),
+ getter_Copies(outText));
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ FileList* fl = static_cast<FileList*>(fileList.get());
+ fl->Item(0)->GetName(outText);
+
+ // 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 uint32_t TRUNCATED_FILE_COUNT = 20;
+ uint32_t count = std::min(listLength, TRUNCATED_FILE_COUNT);
+ for (uint32_t i = 1; i < count; ++i) {
+ nsString fileName;
+ fl->Item(i)->GetName(fileName);
+ outText.Append(NS_LITERAL_STRING("\n"));
+ outText.Append(fileName);
+ }
+ }
+ } else if (NS_SUCCEEDED(currElement->GetAttribute(NS_LITERAL_STRING("title"), outText)) &&
+ outText.Length()) {
found = true;
}
}
if (!found) {
// ...ok, that didn't work, try it in the XLink namespace
NS_NAMED_LITERAL_STRING(xlinkNS, "http://www.w3.org/1999/xlink");
nsCOMPtr<mozilla::dom::Link> linkContent(
do_QueryInterface(currElement));
--- a/toolkit/content/tests/browser/browser.ini
+++ b/toolkit/content/tests/browser/browser.ini
@@ -12,17 +12,16 @@ skip-if = buildapp == 'mulet' || e10s #
skip-if = e10s # Bug 1064580
[browser_bug1198465.js]
[browser_contentTitle.js]
[browser_default_image_filename.js]
[browser_f7_caret_browsing.js]
skip-if = e10s
[browser_findbar.js]
[browser_input_file_tooltips.js]
-skip-if = e10s # Bug 1236991 - Update or remove tests that use fillInPageTooltip
[browser_isSynthetic.js]
support-files =
empty.png
[browser_keyevents_during_autoscrolling.js]
[browser_save_resend_postdata.js]
support-files =
common/mockTransfer.js
data/post_form_inner.sjs
--- a/toolkit/content/tests/browser/browser_input_file_tooltips.js
+++ b/toolkit/content/tests/browser/browser_input_file_tooltips.js
@@ -1,38 +1,111 @@
-function test()
-{
- let data = [
- { value: "/tmp", result: "tmp" },
- { title: "foo", result: "foo" },
- { result: "No file selected." },
- { multiple: true, result: "No files selected." },
- { required: true, result: "Please select a file." }
- ];
+
+let tempFile;
+add_task(function* setup() {
+ yield new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({"set": [["ui.tooltipDelay", 0]]}, resolve);
+ });
+ tempFile = createTempFile();
+ registerCleanupFunction(function() {
+ tempFile.remove(true);
+ });
+});
- let doc = gBrowser.contentDocument;
- let tooltip = document.getElementById("aHTMLTooltip");
+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."});
+});
- for (let test of data) {
- let input = doc.createElement('input');
+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);
+
+ yield new Promise(resolve => {
+ EventUtils.synthesizeNativeMouseMove(tab.linkedBrowser, 300, 300, resolve);
+ });
+
+ ContentTask.spawn(tab.linkedBrowser, test, function*(test) {
+ let doc = content.document;
+ let input = doc.createElement("input");
doc.body.appendChild(input);
- input.type = 'file';
+ 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.value) {
- if (test.value == "/tmp" && navigator.platform.indexOf('Win') != -1) {
- test.value = "C:\\Temp";
- test.result = "Temp";
- }
- input.value = test.value;
+ input.setAttribute("title", test.title);
}
if (test.multiple) {
input.multiple = true;
}
if (test.required) {
input.required = true;
}
+ });
- ok(tooltip.fillInPageTooltip(input));
- is(tooltip.getAttribute('label'), test.result);
+ if (test.value) {
+ 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);
+ 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();
+ }
}
+
+ 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);
+ });
+ });
+ yield new Promise(resolve => {
+ EventUtils.synthesizeNativeMouseMove(tab.linkedBrowser, 100, 5, resolve);
+ });
+ yield new Promise(resolve => setTimeout(resolve, 100));
+ yield new Promise(resolve => {
+ EventUtils.synthesizeNativeMouseMove(tab.linkedBrowser, 110, 15, resolve);
+ });
+ let tooltip = yield awaitTooltipOpen;
+
+ is(tooltip.getAttribute("label"), test.result, "tooltip label should match expectation");
+
+ 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;
+}
--- a/toolkit/content/widgets/popup.xml
+++ b/toolkit/content/widgets/popup.xml
@@ -532,18 +532,19 @@
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 or disconnected.
+ // 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)