Bug 1686316 - Show the allowFocusCheckbox for dialogs opened from TabDialogBox. r=Gijs

This revision introduces helpers for determining whether or not dialogs opened with TabDialogBox show the checkbox for allowing focus (tab switching). The approach for showing the checkbox follows the pattern similar to how its handled for TabModalPromptBox:

First, when a prompt is opened, the "DOMWillOpenModalDialog" event is fired from `PromptParent.jsm` on the browser tab. The browser then determines if the tab the event is dispatched on is the current selected tab. If the dialog was opened from another tab, then we check if the content prompt principal permission "focus-tab-by-prompt" is allowed for the URI the dialog was opened for and store its prompt principal on the tab prompt's `_onNextPromptShowAllowFocusCheckboxFor` property.  This presence for this value is ultimately what determines whether or not the checkbox is shown. Everything after that, the prompt's UI component is responsible for handling the checkbox's state and setting a handler for setting the permission when it's checked.

Implementing this for TabDialogBox makes it so we also store the prompt principal on the dialog box. We then process this value and send some information (such as explicitly setting a `checkLabel` value) via the `args` object for common dialog to process. And finally, we set the "focus-tab-by-prompt" permission for that URI via a closing callback for the dialog.

Differential Revision: https://phabricator.services.mozilla.com/D102076
This commit is contained in:
Micah Tigley 2021-01-31 03:05:00 +00:00
parent cf8a197c9f
commit d245fddb87
4 changed files with 170 additions and 9 deletions

View File

@ -36,6 +36,12 @@ XPCOMUtils.defineLazyPreferenceGetter(
false
);
XPCOMUtils.defineLazyGetter(this, "gTabBrowserBundle", () => {
return Services.strings.createBundle(
"chrome://browser/locale/tabbrowser.properties"
);
});
/**
* @typedef {Object} Prompt
* @property {Function} resolver
@ -281,7 +287,8 @@ class PromptParent extends JSWindowActorParent {
args.promptAborted = false;
args.openedWithTabDialog = true;
let bag = PromptUtils.objectToPropBag(args);
// Convert args object to a prop bag for the dialog to consume.
let bag;
if (
args.modalType === Services.prompt.MODAL_TYPE_TAB ||
@ -296,16 +303,24 @@ class PromptParent extends JSWindowActorParent {
}
// Tab or content level prompt
let dialogBox = win.gBrowser.getTabDialogBox(browser);
if (dialogBox._allowTabFocusByPromptPrincipal) {
this.addTabSwitchCheckboxToArgs(dialogBox, args);
}
bag = PromptUtils.objectToPropBag(args);
await dialogBox.open(
uri,
{
features: "resizable=no",
modalType: args.modalType,
allowFocusCheckbox: args.allowFocusCheckbox,
},
bag
);
} else {
// Window prompt
bag = PromptUtils.objectToPropBag(args);
Services.ww.openWindow(
win,
uri,
@ -354,4 +369,31 @@ class PromptParent extends JSWindowActorParent {
return details;
}
/**
* Set properties on `args` needed by the dialog to allow tab switching for the
* page that opened the prompt.
*
* @param {TabDialogBox} dialogBox
* The dialog to show the tab-switch checkbox for.
* @param {Object} args
* The `args` object to set tab switching permission info on.
*/
addTabSwitchCheckboxToArgs(dialogBox, args) {
let allowTabFocusByPromptPrincipal =
dialogBox._allowTabFocusByPromptPrincipal;
if (
allowTabFocusByPromptPrincipal &&
args.modalType === Services.prompt.MODAL_TYPE_CONTENT
) {
let allowTabswitchCheckboxLabel = gTabBrowserBundle.formatStringFromName(
"tabs.allowTabFocusByPromptForSite",
[allowTabFocusByPromptPrincipal.URI.host]
);
args.allowFocusCheckbox = true;
args.checkLabel = allowTabswitchCheckboxLabel;
}
}
}

View File

@ -8853,6 +8853,7 @@ class TabDialogBox {
sizeTo,
keepOpenSameOriginNav,
modalType = null,
allowFocusCheckbox = false,
} = {},
...aParams
) {
@ -8870,10 +8871,14 @@ class TabDialogBox {
this._onFirstDialogOpen();
}
let closingCallback = () => {
let closingCallback = event => {
if (!hasDialogs) {
this._onLastDialogClose();
}
if (allowFocusCheckbox) {
this.maybeSetAllowTabSwitchPermission(event.target);
}
};
// Open dialog and resolve once it has been closed
@ -9019,6 +9024,28 @@ class TabDialogBox {
}
return this._contentDialogManager;
}
onNextPromptShowAllowFocusCheckboxFor(principal) {
this._allowTabFocusByPromptPrincipal = principal;
}
/**
* Sets the "focus-tab-by-prompt" permission for the dialog.
*/
maybeSetAllowTabSwitchPermission(dialog) {
let checkbox = dialog.querySelector("checkbox");
if (checkbox.checked) {
Services.perms.addFromPrincipal(
this._allowTabFocusByPromptPrincipal,
"focus-tab-by-prompt",
Services.perms.ALLOW_ACTION
);
}
// Don't show the "allow tab switch checkbox" for subsequent prompts.
this._allowTabFocusByPromptPrincipal = null;
}
}
TabDialogBox.prototype.QueryInterface = ChromeUtils.generateQI([

View File

@ -5451,7 +5451,7 @@
return;
}
// For non-system/expanded principals, we bail and show the checkbox
// For non-system/expanded principals without permission, we bail and show the checkbox.
if (promptPrincipal.URI && !promptPrincipal.isSystemPrincipal) {
let permission = Services.perms.testPermissionFromPrincipal(
promptPrincipal,
@ -5459,9 +5459,12 @@
);
if (permission != Services.perms.ALLOW_ACTION) {
// Tell the prompt box we want to show the user a checkbox:
let tabPrompt = this.getTabModalPromptBox(
tabForEvent.linkedBrowser
);
let tabPrompt = Services.prefs.getBoolPref(
"prompts.contentPromptSubDialog"
)
? this.getTabDialogBox(tabForEvent.linkedBrowser)
: this.getTabModalPromptBox(tabForEvent.linkedBrowser);
tabPrompt.onNextPromptShowAllowFocusCheckboxFor(
promptPrincipal
);

View File

@ -131,6 +131,7 @@ add_task(async function test_new_modal_ui() {
await SpecialPowers.pushPrefEnv({
set: [["prompts.contentPromptSubDialog", true]],
});
// Make sure we clear the focus tab permission set in the previous test
PermissionTestUtils.remove(pageWithAlert, "focus-tab-by-prompt");
@ -160,11 +161,99 @@ add_task(async function test_new_modal_ui() {
// 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
// check the prompt is there
let promptElements = openedTab.linkedBrowser.parentNode.querySelectorAll(
".content-prompt-dialog"
);
is(promptElements.length, 1, "There should be 1 prompt");
BrowserTestUtils.removeTab(openedTab);
let dialogBox = gBrowser.getTabDialogBox(openedTab.linkedBrowser);
let contentPromptManager = dialogBox.getContentDialogManager();
is(promptElements.length, 1, "There should be 1 prompt");
is(
contentPromptManager._dialogs.length,
1,
"Content prompt manager should have 1 dialog box."
);
// make sure the checkbox appears and that the permission for allowing tab switching
// is set when the checkbox is tickted and the dialog is accepted
let dialog = contentPromptManager._dialogs[0];
await dialog._dialogReady;
let dialogDoc = dialog._frame.contentWindow.document;
let checkbox = dialogDoc.querySelector("checkbox[label*='example.com']");
let button = dialogDoc.querySelector("#commonDialog").getButton("accept");
ok(checkbox, "The checkbox should be there");
ok(!checkbox.checked, "Checkbox shouldn't be checked");
// tick box and accept dialog
checkbox.checked = true;
button.click();
// Wait for that click to actually be handled completely.
await new Promise(function(resolve) {
Services.tm.dispatchToMainThread(resolve);
});
ok(!contentPromptManager._dialogs.length, "Dialog should be closed");
// check permission is set
is(
Services.perms.ALLOW_ACTION,
PermissionTestUtils.testPermission(pageWithAlert, "focus-tab-by-prompt"),
"Tab switching should now be allowed"
);
// Check if the control center shows the correct permission.
let shown = BrowserTestUtils.waitForEvent(
window,
"popupshown",
true,
event => event.target == gIdentityHandler._identityPopup
);
gIdentityHandler._identityBox.click();
await shown;
let labelText = SitePermissions.getPermissionLabel("focus-tab-by-prompt");
let permissionsList = document.getElementById(
"identity-popup-permission-list"
);
let label = permissionsList.querySelector(".identity-popup-permission-label");
is(label.textContent, labelText);
gIdentityHandler._identityPopup.hidePopup();
// Check if the identity icon signals granted permission.
ok(
gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
"identity-box signals granted permissions"
);
let openedTabSelectedPromise = BrowserTestUtils.waitForAttribute(
"selected",
openedTab,
"true"
);
// switch to other tab again
await BrowserTestUtils.switchTab(gBrowser, firstTab);
// This is sync in non-e10s, but in e10s we need to wait for this, so yield anyway.
// Note that the switchTab promise doesn't actually guarantee anything about *which*
// tab ends up as selected when its event fires, so using that here wouldn't work.
await openedTabSelectedPromise;
ok(contentPromptManager._dialogs.length === 1, "Dialog opened.");
dialog = contentPromptManager._dialogs[0];
await dialog._dialogReady;
// should be switched back
ok(openedTab.selected, "Ta-dah, the other tab should now be selected again!");
// In e10s, with the conformant promise scheduling, we have to wait for next tick
// to ensure that the prompt is open before removing the opened tab, because the
// promise callback of 'openedTabSelectedPromise' could be done at the middle of
// RemotePrompt.openTabPrompt() while 'DOMModalDialogClosed' event is fired.
// await TestUtils.waitForTick();
await BrowserTestUtils.removeTab(openedTab);
});