Bug 1512048 - Convert tabmodalprompt binding to JSM module r=Gijs
authorTimothy Guan-tin Chien <timdream@gmail.com>
Fri, 04 Jan 2019 19:29:34 +0000
changeset 509682 92f9e0296e6270f507f6b36aa7038b6d03ba2a04
parent 509681 606aa2a9d0b28592538bbf77405ac10dda993df0
child 509683 fc381b6584997e41a989228472d1afe802bbb067
push id10547
push userffxbld-merge
push dateMon, 21 Jan 2019 13:03:58 +0000
treeherdermozilla-beta@24ec1916bffe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersGijs
bugs1512048
milestone66.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 1512048 - Convert tabmodalprompt binding to JSM module r=Gijs This converts the tabmodalprompt binding to a class, to be constructed along side with the element by TabModalPromptBox. TabModalPromptBox will keep the instances in a map and pass it to the callers, instead of the element. The tests and callers can access the class instance by passing the element reference to the map. Differential Revision: https://phabricator.services.mozilla.com/D15505
accessible/tests/mochitest/relations/test_ui_modalprompt.html
browser/base/content/browser.js
browser/base/content/tabbrowser.js
browser/base/content/test/general/browser_beforeunload_duplicate_dialogs.js
browser/base/content/test/general/browser_double_close_tab.js
browser/base/content/test/tabPrompts/browser_multiplePrompts.js
browser/base/content/test/tabPrompts/browser_openPromptInBackgroundTab.js
browser/components/extensions/test/browser/browser_ext_optionsPage_modals.js
browser/components/places/tests/browser/browser_sidebarpanels_click.js
devtools/server/tests/browser/browser_navigateEvents.js
docshell/test/browser/browser_bug1415918_beforeunload_options.js
docshell/test/browser/browser_onbeforeunload_navigation.js
dom/tests/browser/browser_beforeunload_between_chrome_content.js
dom/tests/browser/browser_cancel_keydown_keypress_event.js
dom/tests/browser/browser_test_focus_after_modal_state.js
editor/libeditor/tests/test_bug569988.html
layout/base/tests/browser_disableDialogs_onbeforeunload.js
layout/base/tests/browser_onbeforeunload_only_after_interaction.js
layout/base/tests/browser_onbeforeunload_only_after_interaction_in_frame.js
testing/marionette/browser.js
toolkit/components/prompts/content/tabprompts.jsm
toolkit/components/prompts/content/tabprompts.xml
toolkit/components/prompts/jar.mn
toolkit/components/prompts/src/CommonDialog.jsm
toolkit/components/prompts/test/chromeScript.js
toolkit/components/startup/tests/browser/head.js
toolkit/content/xul.css
--- a/accessible/tests/mochitest/relations/test_ui_modalprompt.html
+++ b/accessible/tests/mochitest/relations/test_ui_modalprompt.html
@@ -44,17 +44,17 @@
         window.setTimeout(
           function() {
             currentTabDocument().defaultView.alert("hello");
           }, 0);
       };
 
       this.check = function showAlert_finalCheck(aEvent) {
         var dialog = aEvent.accessible.DOMNode;
-        var info = dialog.ui.infoBody;
+        var info = dialog.querySelector(".tabmodalprompt-infoBody");
         testRelation(info, RELATION_DESCRIPTION_FOR, dialog);
         testRelation(dialog, RELATION_DESCRIBED_BY, info);
       };
 
       this.getID = function showAlert_getID() {
         return "show alert";
       };
     }
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -52,16 +52,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   SafeBrowsing: "resource://gre/modules/SafeBrowsing.jsm",
   Sanitizer: "resource:///modules/Sanitizer.jsm",
   SessionStartup: "resource:///modules/sessionstore/SessionStartup.jsm",
   SessionStore: "resource:///modules/sessionstore/SessionStore.jsm",
   ShortcutUtils: "resource://gre/modules/ShortcutUtils.jsm",
   SimpleServiceDiscovery: "resource://gre/modules/SimpleServiceDiscovery.jsm",
   SiteDataManager: "resource:///modules/SiteDataManager.jsm",
   SitePermissions: "resource:///modules/SitePermissions.jsm",
+  TabModalPrompt: "chrome://global/content/tabprompts.jsm",
   TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm",
   TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.jsm",
   Translation: "resource:///modules/translation/Translation.jsm",
   UITour: "resource:///modules/UITour.jsm",
   UpdateUtils: "resource://gre/modules/UpdateUtils.jsm",
   UrlbarInput: "resource:///modules/UrlbarInput.jsm",
   UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
   UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm",
@@ -8009,40 +8010,48 @@ const SafeBrowsingNotificationBox = {
 
       this._currentURIBaseDomain = null;
     }
   },
 };
 
 function TabModalPromptBox(browser) {
   this._weakBrowserRef = Cu.getWeakReference(browser);
+  /*
+   * This WeakMap holds the TabModalPrompt instances, key to the <tabmodalprompt> prompt
+   * in the DOM. We don't want to hold the instances directly to avoid leaking.
+   *
+   * WeakMap also prevents us from reading back its insertion order.
+   * Order of the elements in the DOM should be the only order to consider.
+   */
+  this.prompts = new WeakMap();
 }
 
 TabModalPromptBox.prototype = {
   _promptCloseCallback(onCloseCallback, principalToAllowFocusFor, allowFocusCheckbox, ...args) {
     if (principalToAllowFocusFor && allowFocusCheckbox &&
         allowFocusCheckbox.checked) {
       Services.perms.addFromPrincipal(principalToAllowFocusFor, "focus-tab-by-prompt",
                                       Services.perms.ALLOW_ACTION);
     }
     onCloseCallback.apply(this, args);
   },
 
   appendPrompt(args, onCloseCallback) {
-    let newPrompt = document.createXULElement("tabmodalprompt");
+    let newPrompt = new TabModalPrompt(window);
+    this.prompts.set(newPrompt.element, newPrompt);
+
     let browser = this.browser;
-    browser.parentNode.insertBefore(newPrompt, browser.nextElementSibling);
+    browser.parentNode.insertBefore(newPrompt.element, browser.nextElementSibling);
     browser.setAttribute("tabmodalPromptShowing", true);
 
-    newPrompt.clientTop; // style flush to assure binding is attached
-
     let prompts = this.listPrompts();
     if (prompts.length > 1) {
       // Let's hide ourself behind the current prompt.
-      newPrompt.hidden = true;
+      newPrompt.element.hidden = true;
     }
 
     let principalToAllowFocusFor = this._allowTabFocusByPromptPrincipal;
     delete this._allowTabFocusByPromptPrincipal;
 
     let allowFocusCheckbox; // Define outside the if block so we can bind it into the callback.
     let hostForAllowFocusCheckbox = "";
     try {
@@ -8052,46 +8061,47 @@ TabModalPromptBox.prototype = {
       let allowFocusRow = document.createXULElement("row");
       allowFocusCheckbox = document.createXULElement("checkbox");
       let spacer = document.createXULElement("spacer");
       allowFocusRow.appendChild(spacer);
       let label = gTabBrowserBundle.formatStringFromName("tabs.allowTabFocusByPromptForSite",
                                                       [hostForAllowFocusCheckbox], 1);
       allowFocusCheckbox.setAttribute("label", label);
       allowFocusRow.appendChild(allowFocusCheckbox);
-      newPrompt.appendChild(allowFocusRow);
+      newPrompt.ui.rows.append(allowFocusRow);
     }
 
     let tab = gBrowser.getTabForBrowser(browser);
     let closeCB = this._promptCloseCallback.bind(null, onCloseCallback, principalToAllowFocusFor,
                                                  allowFocusCheckbox);
     newPrompt.init(args, tab, closeCB);
     return newPrompt;
   },
 
   removePrompt(aPrompt) {
+    this.prompts.delete(aPrompt.element);
     let browser = this.browser;
-    browser.parentNode.removeChild(aPrompt);
+    aPrompt.element.remove();
 
     let prompts = this.listPrompts();
     if (prompts.length) {
       let prompt = prompts[prompts.length - 1];
-      prompt.hidden = false;
+      prompt.element.hidden = false;
       prompt.Dialog.setDefaultFocus();
     } else {
       browser.removeAttribute("tabmodalPromptShowing");
       browser.focus();
     }
   },
 
   listPrompts(aPrompt) {
-    // Get the nodelist, then return as an array
+    // Get the nodelist, then return the TabModalPrompt instances as an array
     const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
     let els = this.browser.parentNode.getElementsByTagNameNS(XUL_NS, "tabmodalprompt");
-    return Array.from(els);
+    return Array.from(els).map(el => this.prompts.get(el));
   },
 
   onNextPromptShowAllowFocusCheckboxFor(principal) {
     this._allowTabFocusByPromptPrincipal = principal;
   },
 
   get browser() {
     let browser = this._weakBrowserRef.get();
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -1144,17 +1144,17 @@ window._gBrowser = {
     // Don't steal focus from the tab bar.
     if (document.activeElement == newTab)
       return;
 
     let newBrowser = this.getBrowserForTab(newTab);
 
     // If there's a tabmodal prompt showing, focus it.
     if (newBrowser.hasAttribute("tabmodalPromptShowing")) {
-      let prompts = newBrowser.parentNode.getElementsByTagNameNS(this._XUL_NS, "tabmodalprompt");
+      let prompts = newBrowser.tabModalPromptBox.listPrompts();
       let prompt = prompts[prompts.length - 1];
       // @tabmodalPromptShowing is also set for other tab modal prompts
       // (e.g. the Payment Request dialog) so there may not be a <tabmodalprompt>.
       // Bug 1492814 will implement this for the Payment Request dialog.
       if (prompt) {
         prompt.Dialog.setDefaultFocus();
         return;
       }
--- a/browser/base/content/test/general/browser_beforeunload_duplicate_dialogs.js
+++ b/browser/base/content/test/general/browser_beforeunload_duplicate_dialogs.js
@@ -3,20 +3,20 @@ const TEST_PAGE = "http://mochi.test:888
 var expectingDialog = false;
 var wantToClose = true;
 var resolveDialogPromise;
 function onTabModalDialogLoaded(node) {
   ok(expectingDialog, "Should be expecting this dialog.");
   expectingDialog = false;
   if (wantToClose) {
     // This accepts the dialog, closing it
-    node.Dialog.ui.button0.click();
+    node.querySelector(".tabmodalprompt-button0").click();
   } else {
     // This keeps the page open
-    node.Dialog.ui.button1.click();
+    node.querySelector(".tabmodalprompt-button1").click();
   }
   if (resolveDialogPromise) {
     resolveDialogPromise();
   }
 }
 
 SpecialPowers.pushPrefEnv({"set": [["dom.require_user_interaction_for_beforeunload", false]]});
 
--- a/browser/base/content/test/general/browser_double_close_tab.js
+++ b/browser/base/content/test/general/browser_double_close_tab.js
@@ -48,17 +48,17 @@ add_task(async function() {
   await new Promise(resolveOuter => {
     waitForDialog(dialogNode => {
       waitForDialogDestroyed(dialogNode, () => {
         let doCompletion = () => setTimeout(resolveOuter, 0);
         info("Now checking if dialog is destroyed");
         ok(!dialogNode.parentNode, "onbeforeunload dialog should be gone.");
         if (dialogNode.parentNode) {
           // Failed to remove onbeforeunload dialog, so do it ourselves:
-          let leaveBtn = dialogNode.ui.button0;
+          let leaveBtn = dialogNode.querySelector(".tabmodalprompt-button0");
           waitForDialogDestroyed(dialogNode, doCompletion);
           EventUtils.synthesizeMouseAtCenter(leaveBtn, {});
           return;
         }
         doCompletion();
       });
       // Click again:
       document.getAnonymousElementByAttribute(testTab, "anonid", "close-button").click();
--- a/browser/base/content/test/tabPrompts/browser_multiplePrompts.js
+++ b/browser/base/content/test/tabPrompts/browser_multiplePrompts.js
@@ -35,38 +35,39 @@ add_task(async function() {
     }, "tabmodal-dialog-loaded");
   });
 
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url, true);
   info("Tab loaded");
 
   await promptsOpenedPromise;
 
-  let promptsCount = PROMPTCOUNT;
-  while (promptsCount--) {
-    let prompts = tab.linkedBrowser.parentNode.querySelectorAll("tabmodalprompt");
-    is(prompts.length, promptsCount + 1, "There should be " + (promptsCount + 1) + " prompt(s).");
+  let promptElementsCount = PROMPTCOUNT;
+  while (promptElementsCount--) {
+    let promptElements = tab.linkedBrowser.parentNode.querySelectorAll("tabmodalprompt");
+    is(promptElements.length, promptElementsCount + 1, "There should be " + (promptElementsCount + 1) + " prompt(s).");
     // The oldest should be the first.
     let i = 0;
-    for (let prompt of prompts) {
+    for (let promptElement of promptElements) {
+      let prompt = tab.linkedBrowser.tabModalPromptBox.prompts.get(promptElement);
       is(prompt.Dialog.args.text, "Alert countdown #" + i, "The #" + i + " alert should be labelled as such.");
-      if (i !== promptsCount) {
-        is(prompt.hidden, true, "This prompt should be hidden.");
+      if (i !== promptElementsCount) {
+        is(prompt.element.hidden, true, "This prompt should be hidden.");
         i++;
         continue;
       }
 
-      is(prompt.hidden, false, "The last prompt should not be hidden.");
+      is(prompt.element.hidden, false, "The last prompt should not be hidden.");
       prompt.onButtonClick(0);
 
       // The click is handled async; wait for an event loop turn for that to
       // happen.
       await new Promise(function(resolve) {
         Services.tm.dispatchToMainThread(resolve);
       });
     }
   }
 
-  let prompts = tab.linkedBrowser.parentNode.querySelectorAll("tabmodalprompt");
-  is(prompts.length, 0, "Prompts should all be dismissed.");
+  let promptElements = tab.linkedBrowser.parentNode.querySelectorAll("tabmodalprompt");
+  is(promptElements.length, 0, "Prompts should all be dismissed.");
 
   BrowserTestUtils.removeTab(tab);
 });
--- a/browser/base/content/test/tabPrompts/browser_openPromptInBackgroundTab.js
+++ b/browser/base/content/test/tabPrompts/browser_openPromptInBackgroundTab.js
@@ -25,26 +25,25 @@ add_task(async function() {
   await openedTabGotAttentionPromise;
   // check for attention attribute
   is(openedTab.getAttribute("attention"), "true", "Tab with alert should have 'attention' attribute.");
   ok(!openedTab.selected, "Tab with alert should not be selected");
 
   // switch tab back, and check the checkbox is displayed:
   await BrowserTestUtils.switchTab(gBrowser, openedTab);
   // check the prompt is there, and the extra row is present
-  let prompts = openedTab.linkedBrowser.parentNode.querySelectorAll("tabmodalprompt");
-  is(prompts.length, 1, "There should be 1 prompt");
-  let ourPrompt = prompts[0];
-  let row = ourPrompt.querySelector("row");
-  ok(row, "Should have found the row with our checkbox");
-  let checkbox = row.querySelector("checkbox[label*='example.com']");
+  let promptElements = openedTab.linkedBrowser.parentNode.querySelectorAll("tabmodalprompt");
+  is(promptElements.length, 1, "There should be 1 prompt");
+  let ourPromptElement = promptElements[0];
+  let checkbox = ourPromptElement.querySelector("checkbox[label*='example.com']");
   ok(checkbox, "The checkbox should be there");
   ok(!checkbox.checked, "Checkbox shouldn't be checked");
   // tick box and accept dialog
   checkbox.checked = true;
+  let ourPrompt = openedTab.linkedBrowser.tabModalPromptBox.prompts.get(ourPromptElement);
   ourPrompt.onButtonClick(0);
   // Wait for that click to actually be handled completely.
   await new Promise(function(resolve) {
     Services.tm.dispatchToMainThread(resolve);
   });
   // check permission is set
   let ps = Services.perms;
   is(ps.ALLOW_ACTION, ps.testPermission(makeURI(pageWithAlert), "focus-tab-by-prompt"),
--- a/browser/components/extensions/test/browser/browser_ext_optionsPage_modals.js
+++ b/browser/components/extensions/test/browser/browser_ext_optionsPage_modals.js
@@ -78,17 +78,17 @@ add_task(async function test_tab_options
     stack = gBrowser.selectedBrowser.parentNode;
   }
 
   let dialogs = stack.querySelectorAll("tabmodalprompt");
 
   Assert.equal(dialogs.length, 1, "Expect a tab modal opened for the about addons tab");
 
   info("Close the tab modal prompt");
-  dialogs[0].onButtonClick(0);
+  dialogs[0].querySelector(".tabmodalprompt-button0").click();
 
   await extension.awaitFinish("options-ui-modals");
 
   Assert.equal(stack.querySelectorAll("tabmodalprompt").length, 0,
                "Expect the tab modal to be closed");
 
   BrowserTestUtils.removeTab(gBrowser.selectedTab);
 
--- a/browser/components/places/tests/browser/browser_sidebarpanels_click.js
+++ b/browser/components/places/tests/browser/browser_sidebarpanels_click.js
@@ -139,17 +139,21 @@ async function testPlacesPanel(testInfo,
 
 function promiseAlertDialogObserved() {
   return new Promise(resolve => {
     function observer(subject) {
       info("alert dialog observed as expected");
       Services.obs.removeObserver(observer, "common-dialog-loaded");
       Services.obs.removeObserver(observer, "tabmodal-dialog-loaded");
 
-      subject.Dialog.ui.button0.click();
+      if (subject.Dialog) {
+        subject.Dialog.ui.button0.click();
+      } else {
+        subject.querySelector(".tabmodalprompt-button0").click();
+      }
       resolve();
     }
     Services.obs.addObserver(observer, "common-dialog-loaded");
     Services.obs.addObserver(observer, "tabmodal-dialog-loaded");
   });
 }
 
 function changeSidebarDirection(aDirection) {
--- a/devtools/server/tests/browser/browser_navigateEvents.js
+++ b/devtools/server/tests/browser/browser_navigateEvents.js
@@ -71,17 +71,17 @@ function assertEvent(event, data) {
   }
 }
 
 function waitForOnBeforeUnloadDialog(browser, callback) {
   browser.addEventListener("DOMWillOpenModalDialog", async function(event) {
     const stack = browser.parentNode;
     const dialogs = stack.getElementsByTagName("tabmodalprompt");
     await waitUntil(() => dialogs[0]);
-    const {button0, button1} = dialogs[0].ui;
+    const {button0, button1} = browser.tabModalPromptBox.prompts.get(dialogs[0]).ui;
     callback(button0, button1);
   }, {capture: true, once: true});
 }
 
 var httpObserver = function(subject, topic, state) {
   const channel = subject.QueryInterface(Ci.nsIHttpChannel);
   const url = channel.URI.spec;
   // Only listen for our document request, as many other requests can happen
--- a/docshell/test/browser/browser_bug1415918_beforeunload_options.js
+++ b/docshell/test/browser/browser_bug1415918_beforeunload_options.js
@@ -18,17 +18,17 @@ add_task(async function test() {
   let stack = browser.parentNode;
   let buttonId;
   let promptShown = false;
 
   let observer = new MutationObserver(function(mutations) {
     mutations.forEach(function(mutation) {
       if (buttonId && mutation.type == "attributes" && browser.hasAttribute("tabmodalPromptShowing")) {
         let prompt = stack.getElementsByTagNameNS(XUL_NS, "tabmodalprompt")[0];
-        document.getAnonymousElementByAttribute(prompt, "anonid", buttonId).click();
+        prompt.querySelector(`.tabmodalprompt-${buttonId}`).click();
         promptShown = true;
       }
     });
   });
   observer.observe(browser, { attributes: true });
 
   /*
    * Check condition where beforeunload handlers request a prompt.
--- a/docshell/test/browser/browser_onbeforeunload_navigation.js
+++ b/docshell/test/browser/browser_onbeforeunload_navigation.js
@@ -180,17 +180,17 @@ function onTabModalDialogLoaded(node) {
       Services.tm.dispatchToMainThread(() => {
         mm.sendAsyncMessage("test-beforeunload:dialog-gone");
       });
     }
   });
   observer.observe(node.parentNode, {childList: true});
 
   BrowserTestUtils.waitForMessage(mm, "test-beforeunload:dialog-response").then((stayingOnPage) => {
-    let button = stayingOnPage ? node.ui.button1 : node.ui.button0;
+    let button = node.querySelector(stayingOnPage ? ".tabmodalprompt-button1" : ".tabmodalprompt-button0");
     // ... and then actually make the dialog go away
     info("Clicking button: " + button.label);
     EventUtils.synthesizeMouseAtCenter(button, {});
   });
 }
 
 // Listen for the dialog being created
 Services.obs.addObserver(onTabModalDialogLoaded, "tabmodal-dialog-loaded");
--- a/dom/tests/browser/browser_beforeunload_between_chrome_content.js
+++ b/dom/tests/browser/browser_beforeunload_between_chrome_content.js
@@ -19,17 +19,18 @@ function injectBeforeUnload(browser) {
   });
 }
 
 // Wait for onbeforeunload dialog, and dismiss it immediately.
 function awaitAndCloseBeforeUnloadDialog(doStayOnPage) {
   return new Promise(resolve => {
     function onDialogShown(node) {
       Services.obs.removeObserver(onDialogShown, "tabmodal-dialog-loaded");
-      let button = doStayOnPage ? node.ui.button1 : node.ui.button0;
+      let button =
+        node.querySelector(doStayOnPage ? ".tabmodalprompt-button1" : ".tabmodalprompt-button0");
       button.click();
       resolve();
     }
 
     Services.obs.addObserver(onDialogShown, "tabmodal-dialog-loaded");
   });
 }
 
--- a/dom/tests/browser/browser_cancel_keydown_keypress_event.js
+++ b/dom/tests/browser/browser_cancel_keydown_keypress_event.js
@@ -1,17 +1,17 @@
 const URL =
   "https://example.com/browser/dom/tests/browser/prevent_return_key.html";
 
 // Wait for alert dialog and dismiss it immediately.
 function awaitAndCloseAlertDialog() {
   return new Promise(resolve => {
     function onDialogShown(node) {
       Services.obs.removeObserver(onDialogShown, "tabmodal-dialog-loaded");
-      let button = node.ui.button0;
+      let button = node.querySelector(".tabmodalprompt-button0");
       button.click();
       resolve();
     }
     Services.obs.addObserver(onDialogShown, "tabmodal-dialog-loaded");
   });
 }
 
 add_task(async function() {
--- a/dom/tests/browser/browser_test_focus_after_modal_state.js
+++ b/dom/tests/browser/browser_test_focus_after_modal_state.js
@@ -1,16 +1,16 @@
 const TEST_URL =
   "https://example.com/browser/dom/tests/browser/focus_after_prompt.html";
 
 function awaitAndClosePrompt() {
   return new Promise(resolve => {
     function onDialogShown(node) {
       Services.obs.removeObserver(onDialogShown, "tabmodal-dialog-loaded");
-      let button = node.ui.button0;
+      let button = node.querySelector(".tabmodalprompt-button0");
       button.click();
       resolve();
     }
     Services.obs.addObserver(onDialogShown, "tabmodal-dialog-loaded");
   });
 }
 
 let lastMessageReceived = "";
--- a/editor/libeditor/tests/test_bug569988.html
+++ b/editor/libeditor/tests/test_bug569988.html
@@ -30,21 +30,29 @@ function runTest() {
     const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm", null);
     var gPromptInput = null;
     var os = Services.obs;
 
     os.addObserver(onPromptLoad, "common-dialog-loaded");
     os.addObserver(onPromptLoad, "tabmodal-dialog-loaded");
 
     function onPromptLoad(subject, topic, data) {
+      let ui = subject.Dialog ? subject.Dialog.ui : undefined;
+      if (!ui) {
+        // subject is an tab prompt, find the elements ourselves
+        ui = {
+          loginTextbox: subject.querySelector(".tabmodalprompt-loginTextbox"),
+          button0: subject.querySelector(".tabmodalprompt-button0"),
+        };
+      }
       sendAsyncMessage("ok", [true, "onPromptLoad is called"]);
-      gPromptInput = subject.Dialog.ui.loginTextbox;
+      gPromptInput = ui.loginTextbox;
       gPromptInput.addEventListener("focus", onPromptFocus);
       // shift focus to ensure it fires.
-      subject.Dialog.ui.button0.focus();
+      ui.button0.focus();
       gPromptInput.focus();
     }
 
     function onPromptFocus() {
       sendAsyncMessage("ok", [true, "onPromptFocus is called"]);
       gPromptInput.removeEventListener("focus", onPromptFocus);
 
       var listenerService = Services.els;
--- a/layout/base/tests/browser_disableDialogs_onbeforeunload.js
+++ b/layout/base/tests/browser_disableDialogs_onbeforeunload.js
@@ -11,17 +11,17 @@ SpecialPowers.pushPrefEnv({"set": [["dom
 const PAGE_URL =
   "data:text/html," + encodeURIComponent("<script>(" + pageScript.toSource() + ")();</script>");
 
 add_task(async function enableDialogs() {
   // The onbeforeunload dialog should appear.
   let dialogShown = false;
   function onDialogShown(node) {
     dialogShown = true;
-    let dismissButton = node.ui.button0;
+    let dismissButton = node.querySelector(".tabmodalprompt-button0");
     dismissButton.click();
   }
   let obsName = "tabmodal-dialog-loaded";
   Services.obs.addObserver(onDialogShown, obsName);
   await openPage(true);
   Services.obs.removeObserver(onDialogShown, obsName);
   Assert.ok(dialogShown);
 });
--- a/layout/base/tests/browser_onbeforeunload_only_after_interaction.js
+++ b/layout/base/tests/browser_onbeforeunload_only_after_interaction.js
@@ -11,17 +11,17 @@ SpecialPowers.pushPrefEnv({"set": [["dom
 const PAGE_URL =
   "data:text/html," + encodeURIComponent("<script>(" + pageScript.toSource() + ")();</script>");
 
 add_task(async function doClick() {
   // The onbeforeunload dialog should appear.
   let dialogShown = false;
   function onDialogShown(node) {
     dialogShown = true;
-    let dismissButton = node.ui.button0;
+    let dismissButton = node.querySelector(".tabmodalprompt-button0");
     dismissButton.click();
   }
   let obsName = "tabmodal-dialog-loaded";
   Services.obs.addObserver(onDialogShown, obsName);
   await openPage(true);
   Services.obs.removeObserver(onDialogShown, obsName);
   Assert.ok(dialogShown, "Should have shown dialog.");
 });
--- a/layout/base/tests/browser_onbeforeunload_only_after_interaction_in_frame.js
+++ b/layout/base/tests/browser_onbeforeunload_only_after_interaction_in_frame.js
@@ -15,17 +15,17 @@ const FRAME_URL =
 const PAGE_URL =
   "data:text/html," + encodeURIComponent("<iframe src='" + FRAME_URL + "'></iframe><script>(" + pageScript.toSource() + ")();</script>");
 
 add_task(async function doClick() {
   // The onbeforeunload dialog should appear.
   let dialogShown = false;
   function onDialogShown(node) {
     dialogShown = true;
-    let dismissButton = node.ui.button0;
+    let dismissButton = node.querySelector(".tabmodalprompt-button0");
     dismissButton.click();
   }
   let obsName = "tabmodal-dialog-loaded";
   Services.obs.addObserver(onDialogShown, obsName);
   await openPage(true);
   Services.obs.removeObserver(onDialogShown, obsName);
   Assert.ok(dialogShown, "Should have shown dialog.");
 });
--- a/testing/marionette/browser.js
+++ b/testing/marionette/browser.js
@@ -269,19 +269,20 @@ browser.Context = class {
   getTabModalUI() {
     let br = this.contentBrowser;
     if (!br.hasAttribute("tabmodalPromptShowing")) {
       return null;
     }
 
     // The modal is a direct sibling of the browser element.
     // See tabbrowser.xml's getTabModalPromptBox.
-    let modals = br.parentNode.getElementsByTagNameNS(
+    let modalElements = br.parentNode.getElementsByTagNameNS(
         XUL_NS, "tabmodalprompt");
-    return modals[0].ui;
+
+    return br.tabModalPromptBox.prompts.get(modalElements[0]).ui;
   }
 
   /**
    * Close the current window.
    *
    * @return {Promise}
    *     A promise which is resolved when the current window has been closed.
    */
rename from toolkit/components/prompts/content/tabprompts.xml
rename to toolkit/components/prompts/content/tabprompts.jsm
--- a/toolkit/components/prompts/content/tabprompts.xml
+++ b/toolkit/components/prompts/content/tabprompts.jsm
@@ -1,353 +1,311 @@
-<?xml version="1.0"?>
-<!-- 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/. -->
+/* 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/. */
 
-<!-- This file is imported into the browser window, and expects various variables,
-     e.g. Ci, Services, to be available. -->
+"use strict";
 
-<!DOCTYPE bindings [
-<!ENTITY % commonDialogDTD  SYSTEM "chrome://global/locale/commonDialog.dtd">
-<!ENTITY % dialogOverlayDTD SYSTEM "chrome://global/locale/dialogOverlay.dtd">
-%commonDialogDTD;
-%dialogOverlayDTD;
-]>
+var EXPORTED_SYMBOLS = [
+  "TabModalPrompt",
+];
 
-<bindings id="tabPrompts"
-   xmlns="http://www.mozilla.org/xbl"
-   xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-   xmlns:xbl="http://www.mozilla.org/xbl">
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 
-  <binding id="tabmodalprompt">
-    <xbl:content xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-                 role="dialog"
-                 aria-describedby="infoBody">
-
-        <!-- This is based on the guts of commonDialog.xul -->
-        <spacer flex="1"/>
+var TabModalPrompt = class {
+  constructor(win) {
+    this.win = win;
+    let newPrompt = this.element = win.document.createXULElement("tabmodalprompt");
+    newPrompt.setAttribute("role", "dialog");
+    let randomIdSuffix = Math.random().toString(32).substr(2);
+    newPrompt.setAttribute("aria-describedby", `infoBody-${randomIdSuffix}`);
+    newPrompt.appendChild(win.MozXULElement.parseXULToFragment(`
+      <spacer flex="1"/>
         <hbox pack="center">
-            <vbox anonid="mainContainer" class="tabmodalprompt-mainContainer">
-                <grid class="tabmodalprompt-topContainer" flex="1">
-                    <columns>
-                        <column/>
-                        <column flex="1"/>
-                    </columns>
-
-                    <rows>
-                        <vbox anonid="infoContainer" align="center" pack="center" flex="1">
-                            <description anonid="infoTitle" class="infoTitle" hidden="true" />
-                            <description anonid="infoBody" class="infoBody"/>
-                        </vbox>
+          <vbox class="tabmodalprompt-mainContainer">
+            <grid class="tabmodalprompt-topContainer" flex="1">
+              <columns>
+                <column/>
+                <column flex="1"/>
+              </columns>
 
-                        <row anonid="loginContainer" hidden="true" align="center">
-                            <label anonid="loginLabel" value="&editfield0.label;" control="loginTextbox"/>
-                            <textbox anonid="loginTextbox"/>
-                        </row>
+              <rows class="tabmodalprompt-rows">
+                <vbox class="tabmodalprompt-infoContainer" align="center" pack="center" flex="1">
+                  <description class="tabmodalprompt-infoTitle infoTitle" hidden="true" />
+                  <description class="tabmodalprompt-infoBody infoBody" id="infoBody-${randomIdSuffix}"/>
+                </vbox>
 
-                        <row anonid="password1Container" hidden="true" align="center">
-                            <label anonid="password1Label" value="&editfield1.label;" control="password1Textbox"/>
-                            <textbox anonid="password1Textbox" type="password"/>
-                        </row>
+                <row class="tabmodalprompt-loginContainer" hidden="true" align="center">
+                  <label class="tabmodalprompt-loginLabel" value="&editfield0.label;" control="loginTextbox-${randomIdSuffix}"/>
+                  <textbox class="tabmodalprompt-loginTextbox" id="loginTextbox-${randomIdSuffix}"/>
+                </row>
 
-                        <row anonid="checkboxContainer" hidden="true">
-                            <spacer/>
-                            <checkbox anonid="checkbox"/>
-                        </row>
+                <row class="tabmodalprompt-password1Container" hidden="true" align="center">
+                  <label class="tabmodalprompt-password1Label" value="&editfield1.label;" control="password1Textbox-${randomIdSuffix}"/>
+                  <textbox class="tabmodalprompt-password1Textbox" type="password" id="password1Textbox-${randomIdSuffix}"/>
+                </row>
 
-                        <xbl:children includes="row"/>
-                    </rows>
-                </grid>
-                <xbl:children/>
-                <hbox class="tabmodalprompt-buttonContainer">
-#ifdef XP_UNIX
-                    <button anonid="button3" hidden="true"/>
-                    <button anonid="button2" hidden="true"/>
-                    <spacer anonid="buttonSpacer" flex="1"/>
-                    <button anonid="button1" label="&cancelButton.label;"/>
-                    <button anonid="button0" label="&okButton.label;"/>
-#else
-                    <button anonid="button3" hidden="true"/>
-                    <spacer anonid="buttonSpacer" flex="1"/>
-                    <button anonid="button0" label="&okButton.label;"/>
-                    <button anonid="button2" hidden="true"/>
-                    <button anonid="button1" label="&cancelButton.label;"/>
-#endif
-                </hbox>
-            </vbox>
-        </hbox>
-        <spacer flex="2"/>
-    </xbl:content>
+                <row class="tabmodalprompt-checkboxContainer" hidden="true">
+                  <spacer/>
+                  <checkbox class="tabmodalprompt-checkbox"/>
+                </row>
 
-    <implementation>
-        <constructor>
-        <![CDATA[
-            let self = this;
-            function getElement(anonid) {
-                return document.getAnonymousElementByAttribute(self, "anonid", anonid);
-            }
+                <!-- content goes here -->
+              </rows>
+            </grid>
+            <hbox class="tabmodalprompt-buttonContainer">
+              <button class="tabmodalprompt-button3" hidden="true"/>
+              <spacer class="tabmodalprompt-buttonSpacer" flex="1"/>
+              <button class="tabmodalprompt-button0" label="&okButton.label;"/>
+              <button class="tabmodalprompt-button2" hidden="true"/>
+              <button class="tabmodalprompt-button1" label="&cancelButton.label;"/>
+            </hbox>
+          </vbox>
+      </hbox>
+      <spacer flex="2"/>
+    `, ["chrome://global/locale/commonDialog.dtd", "chrome://global/locale/dialogOverlay.dtd"]));
 
-            this.ui = {
-                prompt: this,
-                loginContainer: getElement("loginContainer"),
-                loginTextbox: getElement("loginTextbox"),
-                loginLabel: getElement("loginLabel"),
-                password1Container: getElement("password1Container"),
-                password1Textbox: getElement("password1Textbox"),
-                password1Label: getElement("password1Label"),
-                infoBody: getElement("infoBody"),
-                infoTitle: getElement("infoTitle"),
-                infoIcon: null,
-                checkbox: getElement("checkbox"),
-                checkboxContainer: getElement("checkboxContainer"),
-                button3: getElement("button3"),
-                button2: getElement("button2"),
-                button1: getElement("button1"),
-                button0: getElement("button0"),
-                // focusTarget (for BUTTON_DELAY_ENABLE) not yet supported
-            };
+    this.ui = {
+      prompt: this,
+      promptContainer: this.element,
+      mainContainer: newPrompt.querySelector(".tabmodalprompt-mainContainer"),
+      loginContainer: newPrompt.querySelector(".tabmodalprompt-loginContainer"),
+      loginTextbox: newPrompt.querySelector(".tabmodalprompt-loginTextbox"),
+      loginLabel: newPrompt.querySelector(".tabmodalprompt-loginLabel"),
+      password1Container: newPrompt.querySelector(".tabmodalprompt-password1Container"),
+      password1Textbox: newPrompt.querySelector(".tabmodalprompt-password1Textbox"),
+      password1Label: newPrompt.querySelector(".tabmodalprompt-password1Label"),
+      infoContainer: newPrompt.querySelector(".tabmodalprompt-infoContainer"),
+      infoBody: newPrompt.querySelector(".tabmodalprompt-infoBody"),
+      infoTitle: newPrompt.querySelector(".tabmodalprompt-infoTitle"),
+      infoIcon: null,
+      rows: newPrompt.querySelector(".tabmodalprompt-rows"),
+      checkbox: newPrompt.querySelector(".tabmodalprompt-checkbox"),
+      checkboxContainer: newPrompt.querySelector(".tabmodalprompt-checkboxContainer"),
+      button3: newPrompt.querySelector(".tabmodalprompt-button3"),
+      button2: newPrompt.querySelector(".tabmodalprompt-button2"),
+      button1: newPrompt.querySelector(".tabmodalprompt-button1"),
+      button0: newPrompt.querySelector(".tabmodalprompt-button0"),
+      // focusTarget (for BUTTON_DELAY_ENABLE) not yet supported
+    };
 
-            this.ui.button0.addEventListener("command", this.onButtonClick.bind(this, 0));
-            this.ui.button1.addEventListener("command", this.onButtonClick.bind(this, 1));
-            this.ui.button2.addEventListener("command", this.onButtonClick.bind(this, 2));
-            this.ui.button3.addEventListener("command", this.onButtonClick.bind(this, 3));
-            // Anonymous wrapper used here because |Dialog| doesn't exist until init() is called!
-            this.ui.checkbox.addEventListener("command", function() { self.Dialog.onCheckbox(); });
-            this.isLive = false;
-        ]]>
-        </constructor>
-        <destructor>
-        <![CDATA[
-            if (this.isLive) {
-                this.abortPrompt();
-            }
-        ]]>
-        </destructor>
+    if (AppConstants.XP_UNIX) {
+      // Reorder buttons on Linux
+      let buttonContainer = newPrompt.querySelector(".tabmodalprompt-buttonContainer");
+      buttonContainer.appendChild(this.ui.button3);
+      buttonContainer.appendChild(this.ui.button2);
+      buttonContainer.appendChild(newPrompt.querySelector(".tabmodalprompt-buttonSpacer"));
+      buttonContainer.appendChild(this.ui.button1);
+      buttonContainer.appendChild(this.ui.button0);
+    }
+
+    this.ui.button0.addEventListener("command", this.onButtonClick.bind(this, 0));
+    this.ui.button1.addEventListener("command", this.onButtonClick.bind(this, 1));
+    this.ui.button2.addEventListener("command", this.onButtonClick.bind(this, 2));
+    this.ui.button3.addEventListener("command", this.onButtonClick.bind(this, 3));
+    // Anonymous wrapper used here because |Dialog| doesn't exist until init() is called!
+    this.ui.checkbox.addEventListener("command", () => { this.Dialog.onCheckbox(); });
 
-        <field name="ui"/>
-        <field name="args"/>
-        <field name="linkedTab"/>
-        <field name="onCloseCallback"/>
-        <field name="Dialog"/>
-        <field name="isLive"/>
-        <field name="availWidth"/>
-        <field name="availHeight"/>
-        <field name="minWidth"/>
-        <field name="minHeight"/>
+    /**
+     * Based on dialog.xml handlers
+     */
+    this.element.addEventListener("keypress", (event) => {
+      switch (event.keyCode) {
+        case KeyEvent.DOM_VK_RETURN:
+          this.onKeyAction("default", event);
+          break;
 
-        <method name="init">
-            <parameter name="args"/>
-            <parameter name="linkedTab"/>
-            <parameter name="onCloseCallback"/>
-            <body>
-            <![CDATA[
-                this.args = args;
-                this.linkedTab = linkedTab;
-                this.onCloseCallback = onCloseCallback;
+        case KeyEvent.DOM_VK_ESCAPE:
+          this.onKeyAction("cancel", event);
+          break;
 
-                if (args.enableDelay)
-                    throw "BUTTON_DELAY_ENABLE not yet supported for tab-modal prompts";
+        default:
+          if (AppConstants.platform == "macosx" && event.key == "." && event.metaKey) {
+            this.onKeyAction("cancel", event);
+          }
+          break;
+      }
+    }, { mozSystemGroup: true });
 
-                // We need to remove the prompt when the tab or browser window is closed or
-                // the page navigates, else we never unwind the event loop and that's sad times.
-                // Remember to cleanup in shutdownPrompt()!
-                this.isLive = true;
-                window.addEventListener("resize", this);
-                window.addEventListener("unload", this);
-                if (linkedTab) {
-                  linkedTab.addEventListener("TabClose", this);
-                }
-                // Note:
-                // nsPrompter.js or in e10s mode browser-parent.js call abortPrompt,
-                // when the domWindow, for which the prompt was created, generates
-                // a "pagehide" event.
+    this.element.addEventListener("focus", (event) => {
+      let bnum = this.args.defaultButtonNum || 0;
+      let defaultButton = this.ui["button" + bnum];
 
-                let tmp = {};
-                ChromeUtils.import("resource://gre/modules/CommonDialog.jsm", tmp);
-                this.Dialog = new tmp.CommonDialog(args, this.ui);
-                this.Dialog.onLoad(null);
-
-                // Display the tabprompt title that shows the prompt origin when
-                // the prompt origin is not the same as that of the top window.
-                if (!args.showAlertOrigin)
-                    this.ui.infoTitle.removeAttribute("hidden");
-
-                // TODO: should unhide buttonSpacer on Windows when there are 4 buttons.
-                //       Better yet, just drop support for 4-button dialogs. (bug 609510)
+      if (AppConstants.platform == "macosx") {
+        // On OS X, the default button always stays marked as such (until
+        // the entire prompt blurs).
+        defaultButton.setAttribute("default", "true");
+      } else {
+        // On other platforms, the default button is only marked as such
+        // when no other button has focus. XUL buttons on not-OSX will
+        // react to pressing enter as a command, so you can't trigger the
+        // default without tabbing to it or something that isn't a button.
+        let focusedDefault = (event.originalTarget == defaultButton);
+        let someButtonFocused = event.originalTarget.localName == "button" ||
+          event.originalTarget.localName == "toolbarbutton";
+        if (focusedDefault || !someButtonFocused) {
+          defaultButton.setAttribute("default", "true");
+        }
+      }
+    }, true);
 
-                this.onResize();
-            ]]>
-            </body>
-        </method>
+    this.element.addEventListener("blur", () => {
+      // If focus shifted to somewhere else in the browser, don't make
+      // the default button look active.
+      let bnum = this.args.defaultButtonNum || 0;
+      let button = this.ui["button" + bnum];
+      button.removeAttribute("default");
+    });
+  }
+
+  init(args, linkedTab, onCloseCallback) {
+    this.args = args;
+    this.linkedTab = linkedTab;
+    this.onCloseCallback = onCloseCallback;
+
+    if (args.enableDelay)
+      throw "BUTTON_DELAY_ENABLE not yet supported for tab-modal prompts";
 
-        <method name="shutdownPrompt">
-            <body>
-            <![CDATA[
-                // remove our event listeners
-                try {
-                    window.removeEventListener("resize", this);
-                    window.removeEventListener("unload", this);
-                    if (this.linkedTab) {
-                      this.linkedTab.removeEventListener("TabClose", this);
-                    }
-                } catch (e) { }
-                this.isLive = false;
-                // invoke callback
-                this.onCloseCallback();
-            ]]>
-            </body>
-        </method>
+    // We need to remove the prompt when the tab or browser window is closed or
+    // the page navigates, else we never unwind the event loop and that's sad times.
+    // Remember to cleanup in shutdownPrompt()!
+    this.win.addEventListener("resize", this);
+    this.win.addEventListener("unload", this);
+    if (linkedTab) {
+      linkedTab.addEventListener("TabClose", this);
+    }
+    // Note:
+    // nsPrompter.js or in e10s mode browser-parent.js call abortPrompt,
+    // when the domWindow, for which the prompt was created, generates
+    // a "pagehide" event.
 
-        <method name="abortPrompt">
-            <body>
-            <![CDATA[
-                // Called from other code when the page changes.
-                this.Dialog.abortPrompt();
-                this.shutdownPrompt();
-            ]]>
-            </body>
-        </method>
+    let tmp = {};
+    ChromeUtils.import("resource://gre/modules/CommonDialog.jsm", tmp);
+    this.Dialog = new tmp.CommonDialog(args, this.ui);
+    this.Dialog.onLoad(null);
 
-        <method name="handleEvent">
-            <parameter name="aEvent"/>
-            <body>
-            <![CDATA[
-                switch (aEvent.type) {
-                  case "resize":
-                    this.onResize();
-                    break;
-                  case "unload":
-                  case "TabClose":
-                    this.abortPrompt();
-                    break;
-                }
-            ]]>
-            </body>
-        </method>
+    // Display the tabprompt title that shows the prompt origin when
+    // the prompt origin is not the same as that of the top window.
+    if (!args.showAlertOrigin)
+      this.ui.infoTitle.removeAttribute("hidden");
+
+    // TODO: should unhide buttonSpacer on Windows when there are 4 buttons.
+    //       Better yet, just drop support for 4-button dialogs. (bug 609510)
+
+    this.onResize();
+  }
 
-        <method name="onResize">
-            <body>
-            <![CDATA[
-                let availWidth = this.clientWidth;
-                let availHeight = this.clientHeight;
-                if (availWidth == this.availWidth && availHeight == this.availHeight)
-                    return;
-                this.availWidth = availWidth;
-                this.availHeight = availHeight;
+  // Sadly this is needed to ensure all the bindings inside the <tabmodalprompt>
+  // are attached. This method had to be called by CommonDialog.jsm after
+  // it had set the visibility of each of the elements.
+  ensureXBLBindingAttached() {
+    for (let key in this.ui) {
+      if (this.ui[key] instanceof this.win.XULElement) {
+        if (this.ui[key].hidden) {
+          continue;
+        }
+        this.ui[key].clientTop;
+      }
+    }
+  }
 
-                let self = this;
-                function getElement(anonid) {
-                    return document.getAnonymousElementByAttribute(self, "anonid", anonid);
-                }
-                let main = getElement("mainContainer");
-                let info = getElement("infoContainer");
-                let body = this.ui.infoBody;
-
-                // cap prompt dimensions at 60% width and 60% height of content area
-                if (!this.minWidth)
-                  this.minWidth = parseInt(window.getComputedStyle(main).minWidth);
-                if (!this.minHeight)
-                  this.minHeight = parseInt(window.getComputedStyle(main).minHeight);
-                let maxWidth = Math.max(Math.floor(availWidth * 0.6), this.minWidth) +
-                               info.clientWidth - main.clientWidth;
-                let maxHeight = Math.max(Math.floor(availHeight * 0.6), this.minHeight) +
-                                info.clientHeight - main.clientHeight;
-                body.style.maxWidth = maxWidth + "px";
-                info.style.overflow = info.style.width = info.style.height = "";
+  shutdownPrompt() {
+    // remove our event listeners
+    try {
+      this.win.removeEventListener("resize", this);
+      this.win.removeEventListener("unload", this);
+      if (this.linkedTab) {
+        this.linkedTab.removeEventListener("TabClose", this);
+      }
+    } catch (e) {}
+    // invoke callback
+    this.onCloseCallback();
+    this.win = null;
+    this.ui = null;
+    // Intentionally not cleaning up |this.element| here --
+    // TabModalPromptBox.removePrompt() would need it and it might not
+    // be called yet -- see browser_double_close_tabs.js.
+  }
 
-                // when prompt text is too long, use scrollbars
-                if (info.clientWidth > maxWidth) {
-                    info.style.overflow = "auto";
-                    info.style.width = maxWidth + "px";
-                }
-                if (info.clientHeight > maxHeight) {
-                    info.style.overflow = "auto";
-                    info.style.height = maxHeight + "px";
-                }
-            ]]>
-            </body>
-        </method>
+  abortPrompt() {
+    // Called from other code when the page changes.
+    this.Dialog.abortPrompt();
+    this.shutdownPrompt();
+  }
+
+  handleEvent(aEvent) {
+    switch (aEvent.type) {
+      case "resize":
+        this.onResize();
+        break;
+      case "unload":
+      case "TabClose":
+        this.abortPrompt();
+        break;
+    }
+  }
 
-        <method name="onButtonClick">
-            <parameter name="buttonNum"/>
-            <body>
-            <![CDATA[
-                // We want to do all the work her asynchronously off a Gecko
-                // runnable, because of situations like the one described in
-                // https://bugzilla.mozilla.org/show_bug.cgi?id=1167575#c35 : we
-                // get here off processing of an OS event and will also process
-                // one more Gecko runnable before we break out of the event loop
-                // spin whoever posted the prompt is doing.  If we do all our
-                // work sync, we will exit modal state _before_ processing that
-                // runnable, and if exiting moral state posts a runnable we will
-                // incorrectly process that runnable before leaving our event
-                // loop spin.
-                Services.tm.dispatchToMainThread(() => {
-                    this.Dialog["onButton" + buttonNum]();
-                    this.shutdownPrompt();
-                  });
-            ]]>
-            </body>
-        </method>
+  onResize() {
+    let availWidth = this.clientWidth;
+    let availHeight = this.clientHeight;
+    if (availWidth == this.availWidth && availHeight == this.availHeight)
+      return;
+    this.availWidth = availWidth;
+    this.availHeight = availHeight;
+
+    let main = this.ui.mainContainer;
+    let info = this.ui.infoContainer;
+    let body = this.ui.infoBody;
 
-        <method name="onKeyAction">
-            <parameter name="action"/>
-            <parameter name="event"/>
-            <body>
-            <![CDATA[
-                if (event.defaultPrevented)
-                    return;
+    // cap prompt dimensions at 60% width and 60% height of content area
+    if (!this.minWidth)
+      this.minWidth = parseInt(this.win.getComputedStyle(main).minWidth);
+    if (!this.minHeight)
+      this.minHeight = parseInt(this.win.getComputedStyle(main).minHeight);
+    let maxWidth = Math.max(Math.floor(availWidth * 0.6), this.minWidth) +
+      info.clientWidth - main.clientWidth;
+    let maxHeight = Math.max(Math.floor(availHeight * 0.6), this.minHeight) +
+      info.clientHeight - main.clientHeight;
+    body.style.maxWidth = maxWidth + "px";
+    info.style.overflow = info.style.width = info.style.height = "";
 
-                event.stopPropagation();
-                if (action == "default") {
-                    let bnum = this.args.defaultButtonNum || 0;
-                    this.onButtonClick(bnum);
-                } else { // action == "cancel"
-                    this.onButtonClick(1); // Cancel button
-                }
-            ]]>
-            </body>
-        </method>
-    </implementation>
-
-    <handlers>
-        <!-- Based on dialog.xml handlers -->
-        <handler event="keypress" keycode="VK_RETURN"
-                 group="system" action="this.onKeyAction('default', event);"/>
-        <handler event="keypress" keycode="VK_ESCAPE"
-                 group="system" action="this.onKeyAction('cancel', event);"/>
-#ifdef XP_MACOSX
-        <handler event="keypress" key="." modifiers="meta"
-                 group="system" action="this.onKeyAction('cancel', event);"/>
-#endif
-        <handler event="focus" phase="capturing">
-            let bnum = this.args.defaultButtonNum || 0;
-            let defaultButton = this.ui["button" + bnum];
+    // when prompt text is too long, use scrollbars
+    if (info.clientWidth > maxWidth) {
+      info.style.overflow = "auto";
+      info.style.width = maxWidth + "px";
+    }
+    if (info.clientHeight > maxHeight) {
+      info.style.overflow = "auto";
+      info.style.height = maxHeight + "px";
+    }
+  }
 
-            let { AppConstants } =
-                ChromeUtils.import("resource://gre/modules/AppConstants.jsm", {});
-            if (AppConstants.platform == "macosx") {
-              // On OS X, the default button always stays marked as such (until
-              // the entire prompt blurs).
-              defaultButton.setAttribute("default", true);
-            } else {
-              // On other platforms, the default button is only marked as such
-              // when no other button has focus. XUL buttons on not-OSX will
-              // react to pressing enter as a command, so you can't trigger the
-              // default without tabbing to it or something that isn't a button.
-              let focusedDefault = (event.originalTarget == defaultButton);
-              let someButtonFocused = event.originalTarget.localName == "button" ||
-                                      event.originalTarget.localName == "toolbarbutton";
-              defaultButton.setAttribute("default", focusedDefault || !someButtonFocused);
-            }
-        </handler>
-        <handler event="blur">
-            // If focus shifted to somewhere else in the browser, don't make
-            // the default button look active.
-            let bnum = this.args.defaultButtonNum || 0;
-            let button = this.ui["button" + bnum];
-            button.setAttribute("default", false);
-        </handler>
-    </handlers>
+  onButtonClick(buttonNum) {
+    // We want to do all the work her asynchronously off a Gecko
+    // runnable, because of situations like the one described in
+    // https://bugzilla.mozilla.org/show_bug.cgi?id=1167575#c35 : we
+    // get here off processing of an OS event and will also process
+    // one more Gecko runnable before we break out of the event loop
+    // spin whoever posted the prompt is doing.  If we do all our
+    // work sync, we will exit modal state _before_ processing that
+    // runnable, and if exiting moral state posts a runnable we will
+    // incorrectly process that runnable before leaving our event
+    // loop spin.
+    Services.tm.dispatchToMainThread(() => {
+      this.Dialog["onButton" + buttonNum]();
+      this.shutdownPrompt();
+    });
+  }
 
-  </binding>
-</bindings>
+  onKeyAction(action, event) {
+    if (event.defaultPrevented)
+      return;
+
+    event.stopPropagation();
+    if (action == "default") {
+      let bnum = this.args.defaultButtonNum || 0;
+      this.onButtonClick(bnum);
+    } else { // action == "cancel"
+      this.onButtonClick(1); // Cancel button
+    }
+  }
+};
--- a/toolkit/components/prompts/jar.mn
+++ b/toolkit/components/prompts/jar.mn
@@ -4,9 +4,9 @@
 
 toolkit.jar:
    content/global/commonDialog.js             (content/commonDialog.js)
 *  content/global/commonDialog.xul            (content/commonDialog.xul)
    content/global/commonDialog.css            (content/commonDialog.css)
    content/global/selectDialog.js             (content/selectDialog.js)
    content/global/selectDialog.xul            (content/selectDialog.xul)
    content/global/tabprompts.css              (content/tabprompts.css)
-*  content/global/tabprompts.xml              (content/tabprompts.xml)
+   content/global/tabprompts.jsm              (content/tabprompts.jsm)
--- a/toolkit/components/prompts/src/CommonDialog.jsm
+++ b/toolkit/components/prompts/src/CommonDialog.jsm
@@ -133,16 +133,17 @@ CommonDialog.prototype = {
         }
         let infoBody = this.ui.infoBody;
         infoBody.appendChild(infoBody.ownerDocument.createTextNode(croppedMessage));
 
         let label = this.args.checkLabel;
         if (label) {
             // Only show the checkbox if label has a value.
             this.ui.checkboxContainer.hidden = false;
+            this.ui.checkboxContainer.clientTop; // style flush to assure binding is attached
             this.setLabelForNode(this.ui.checkbox, label);
             this.ui.checkbox.checked = this.args.checked;
         }
 
         // set the icon
         let icon = this.ui.infoIcon;
         if (icon)
             this.iconClass.forEach((el, idx, arr) => icon.classList.add(el));
@@ -156,16 +157,21 @@ CommonDialog.prototype = {
         let b = (this.args.defaultButtonNum || 0);
         let button = this.ui["button" + b];
 
         if (xulDialog)
             xulDialog.defaultButton = ["accept", "cancel", "extra1", "extra2"][b];
         else
             button.setAttribute("default", "true");
 
+        // For tab prompts, we will need to ensure its content bindings are attached.
+        if (!xulDialog) {
+            this.ui.prompt.ensureXBLBindingAttached();
+        }
+
         // Set default focus / selection.
         this.setDefaultFocus(true);
 
         if (this.args.enableDelay) {
             this.delayHelper = new EnableDelayHelper({
                 disableDialog: () => this.setButtonsEnabledState(false),
                 enableDialog: () => this.setButtonsEnabledState(true),
                 focusTarget: this.ui.focusTarget,
@@ -178,20 +184,23 @@ CommonDialog.prototype = {
                 Cc["@mozilla.org/sound;1"].
                 createInstance(Ci.nsISound).
                 playEventSound(this.soundID);
             }
         } catch (e) {
             Cu.reportError("Couldn't play common dialog event sound: " + e);
         }
 
-        let topic = "common-dialog-loaded";
-        if (!xulDialog)
-            topic = "tabmodal-dialog-loaded";
-        Services.obs.notifyObservers(this.ui.prompt, topic);
+        if (xulDialog) {
+            // ui.prompt is the window object of the dialog.
+            Services.obs.notifyObservers(this.ui.prompt, "common-dialog-loaded");
+        } else {
+            // ui.promptContainer is the <tabmodalprompt> element.
+            Services.obs.notifyObservers(this.ui.promptContainer, "tabmodal-dialog-loaded");
+        }
     },
 
     setLabelForNode(aNode, aLabel) {
         // This is for labels which may contain embedded access keys.
         // If we end in (&X) where X represents the access key, optionally preceded
         // by spaces and/or followed by the ':' character, store the access key and
         // remove the access key placeholder + leading spaces from the label.
         // Otherwise a character preceded by one but not two &s is the access key.
--- a/toolkit/components/prompts/test/chromeScript.js
+++ b/toolkit/components/prompts/test/chromeScript.js
@@ -81,18 +81,18 @@ function getSelectState(ui) {
 
 function getPromptState(ui) {
   let state = {};
   state.msg         = ui.infoBody.textContent;
   state.titleHidden = ui.infoTitle.getAttribute("hidden") == "true";
   state.textHidden  = ui.loginContainer.hidden;
   state.passHidden  = ui.password1Container.hidden;
   state.checkHidden = ui.checkboxContainer.hidden;
-  state.checkMsg    = ui.checkbox.label;
-  state.checked     = ui.checkbox.checked;
+  state.checkMsg    = state.checkHidden ? "" : ui.checkbox.label;
+  state.checked     = state.checkHidden ? false : ui.checkbox.checked;
   // tab-modal prompts don't have an infoIcon
   state.iconClass   = ui.infoIcon ? ui.infoIcon.className : null;
   state.textValue   = ui.loginTextbox.getAttribute("value");
   state.passValue   = ui.password1Textbox.getAttribute("value");
 
   state.butt0Label  = ui.button0.label;
   state.butt1Label  = ui.button1.label;
   state.butt2Label  = ui.button2.label;
@@ -114,19 +114,19 @@ function getPromptState(ui) {
   if (e == null) {
     state.focused = null;
   } else if (ui.button0.isSameNode(e)) {
     state.focused = "button0";
   } else if (ui.button1.isSameNode(e)) {
     state.focused = "button1";
   } else if (ui.button2.isSameNode(e)) {
     state.focused = "button2";
-  } else if (ui.loginTextbox.inputField.isSameNode(e)) {
+  } else if (e.isSameNode(ui.loginTextbox.inputField)) {
     state.focused = "textField";
-  } else if (ui.password1Textbox.inputField.isSameNode(e)) {
+  } else if (e.isSameNode(ui.password1Textbox.inputField)) {
     state.focused = "passField";
   } else if (ui.infoBody.isSameNode(e)) {
     state.focused = "infoBody";
   } else {
     state.focused = "ERROR: unexpected element focused: " + (e ? e.localName : "<null>");
   }
 
   return state;
--- a/toolkit/components/startup/tests/browser/head.js
+++ b/toolkit/components/startup/tests/browser/head.js
@@ -17,13 +17,13 @@ function waitForOnBeforeUnloadDialog(bro
       return;
     }
 
     browser.removeEventListener("DOMWillOpenModalDialog", onModalDialog, true);
 
     SimpleTest.waitForCondition(() => Services.focus.activeWindow == browser.ownerGlobal, function() {
       let stack = browser.parentNode;
       let dialogs = stack.getElementsByTagNameNS(XUL_NS, "tabmodalprompt");
-      let {button0, button1} = dialogs[0].ui;
+      let {button0, button1} = browser.tabModalPromptBox.prompts.get(dialogs[0]).ui;
       callback(button0, button1);
     }, "Waited too long for window with dialog to focus");
   }, true);
 }
--- a/toolkit/content/xul.css
+++ b/toolkit/content/xul.css
@@ -733,17 +733,16 @@ richlistitem {
 
 /*********** findbar ************/
 findbar {
   overflow-x: hidden;
 }
 
 /*********** tabmodalprompt ************/
 tabmodalprompt {
-  -moz-binding: url("chrome://global/content/tabprompts.xml#tabmodalprompt");
   overflow: hidden;
   text-shadow: none;
 }
 
 .button-highlightable-text:not([highlightable="true"]),
 .button-text[highlightable="true"],
 .menulist-highlightable-label:not([highlightable="true"]),
 .menulist-label[highlightable="true"],