fune/browser/base/content/upgradeDialog.js
Mike Conley 02db702d76 Bug 1709050 - Suppress the focus ring in the upgrade dialog and some about: pages when they first load. r=Mardak,Gijs
Using autofocus="true" or programmatically calling .focus() when there is no pre-existing focused
element causes the :focus-visible to match.

This patch avoids using autofocus="true" on some of our about: pages, and uses
Element.focus({ preventFocusRing: true }) to avoid that when these pages and dialog
first load. Note that this doesn't prevent the focus ring from _ever_ appear, it just
stops it from appearing on first load.

Differential Revision: https://phabricator.services.mozilla.com/D115062
2021-05-21 18:13:29 +00:00

328 lines
11 KiB
JavaScript

/* 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 {
AddonManager,
AppConstants,
document: gDoc,
getShellService,
Services,
} = window.docShell.chromeEventHandler.ownerGlobal;
// Number of height pixels to switch to compact mode to avoid showing scrollbars
// on the non-compact mode. This is based on the natural height of the full-size
// "accented" content while also accounting for the dialog container margins.
const COMPACT_MODE_HEIGHT = 679;
const SHELL = getShellService();
const IS_DEFAULT = SHELL.isDefaultBrowser();
const NEED_PIN = SHELL.doesAppNeedPin();
// Strings for various elements with matching ids on each screen.
const PIN_OR_ALT_STRING = document.l10n.ready.then(async () => {
const ids = [
"upgrade-dialog-new-primary-pin-alt-button",
"upgrade-dialog-new-primary-pin-button",
];
const [preferred, fallback] = await document.l10n.formatValues(ids);
// Use the former preferred string id (for "Pin to taskbar") if it's
// translated OR if the fallback id (for "Pin Firefox to my taskbar") isn't.
// This handles en-* case where neither appear translated and non-en-* too.
return preferred !== "Pin to taskbar" ||
fallback.match(/^Pin .+ to my taskbar$/)
? ids[0]
: ids[1];
});
const THEME_OR_DEFAULT_STRING = IS_DEFAULT
? "upgrade-dialog-new-primary-theme-button"
: "upgrade-dialog-new-primary-default-button";
const SCREEN_STRINGS = [
{
title: "upgrade-dialog-new-title",
subtitle: NEED_PIN.then(pin =>
pin ? "upgrade-dialog-new-alt-subtitle" : "upgrade-dialog-new-subtitle"
),
primary: NEED_PIN.then(pin =>
pin ? PIN_OR_ALT_STRING : THEME_OR_DEFAULT_STRING
),
secondary: "upgrade-dialog-new-secondary-button",
},
{
title: "upgrade-dialog-default-title",
subtitle: "upgrade-dialog-default-subtitle",
primary: "upgrade-dialog-default-primary-button",
secondary: "upgrade-dialog-default-secondary-button",
},
{
title: "upgrade-dialog-theme-title",
primary: "upgrade-dialog-theme-primary-button",
secondary: "upgrade-dialog-theme-secondary-button",
},
];
// Themes that can be selected by the button with matching index.
const THEME_IDS = [
"default-theme@mozilla.org",
"firefox-compact-light@mozilla.org",
"firefox-compact-dark@mozilla.org",
"firefox-alpenglow@mozilla.org",
];
// Callbacks to run when the dialog closes (both from this file or externally).
const CLEANUP = [];
addEventListener("pagehide", () => CLEANUP.forEach(f => f()), { once: true });
// Save the previous theme to revert to it.
let gPrevTheme = AddonManager.getAddonsByTypes(["theme"]).then(addons => {
for (const { id, isActive, screenshots } of addons) {
if (isActive) {
// Only show the "keep" theme option for "other" themes.
if (!THEME_IDS.includes(id)) {
THEME_IDS.push(id);
}
// Assume we need to revert the theme unless cleared.
CLEANUP.push(() => gPrevTheme && enableTheme(id));
return {
id,
swatch: screenshots?.[0].url,
};
}
}
// If there were no active themes, the default will be selected.
return { id: THEME_IDS[0] };
});
// Helper to switch themes.
async function enableTheme(id) {
(await AddonManager.getAddonByID(id)).enable();
}
// Helper to show the theme in chrome with an adjusted modal backdrop.
function adjustModalBackdrop() {
const { classList } = gDoc.getElementById("window-modal-dialog");
classList.add("showToolbar");
CLEANUP.push(() => classList.remove("showToolbar"));
}
// Helper to record various events from the dialog content.
function recordEvent(obj, val) {
Services.telemetry.recordEvent("upgrade_dialog", "content", obj, `${val}`);
}
// Assume the dialog closes from an external trigger unless this helper is used.
let gCloseReason = "external";
CLEANUP.push(() => recordEvent("close", gCloseReason));
function closeDialog(reason) {
gCloseReason = reason;
close();
}
// Detect quit requests to proactively dismiss to allow the quit prompt to show
// as otherwise gDialogBox queues the prompt as these share the same display.
const QUIT_TOPIC = "quit-application-requested";
const QUIT_OBSERVER = () => closeDialog(QUIT_TOPIC);
Services.obs.addObserver(QUIT_OBSERVER, QUIT_TOPIC);
CLEANUP.push(() => Services.obs.removeObserver(QUIT_OBSERVER, QUIT_TOPIC));
// Hook up dynamic behaviors of the dialog.
function onLoad(ready) {
// Change content for Windows 7 because non-light themes aren't quite right.
const win7Content = AppConstants.isPlatformAndVersionAtMost("win", "6.1");
const { body } = document;
const title = document.getElementById("title");
const subtitle = document.getElementById("subtitle");
const image = document.querySelector(".image");
const items = document.querySelector(".items");
const themes = document.querySelector(".themes");
const primary = document.getElementById("primary");
const secondary = document.getElementById("secondary");
const steps = document.querySelector(".steps");
// Update the screen content and handle actions.
let current = -1;
(async function advance({ target } = {}) {
// Record which button was clicked.
if (target) {
recordEvent("button", target.dataset.l10nId);
}
// Move to the next screen and perform screen-specific behavior.
switch (++current) {
// Handle initial / first screen setup.
case 0:
// Wait for main button clicks on each screen.
primary.addEventListener("click", advance);
secondary.addEventListener("click", advance);
// Check parent window's height to determine if we should be compact.
if (gDoc.ownerGlobal.outerHeight < COMPACT_MODE_HEIGHT) {
body.classList.add("compact");
recordEvent("show", "compact");
}
// Windows 7 has a single screen so hide steps.
if (win7Content) {
steps.style.visibility = "hidden";
if (IS_DEFAULT) {
SCREEN_STRINGS[current].primary =
"upgrade-dialog-new-primary-win7-button";
secondary.style.display = "none";
}
}
// Show images instead of items for "pin" content.
let removeDefaultScreen = true;
if (await NEED_PIN) {
items.remove();
removeDefaultScreen = IS_DEFAULT;
} else {
image.remove();
}
// Keep the second screen only if we need to both pin and set default.
if (removeDefaultScreen) {
SCREEN_STRINGS.splice(1, 1);
}
// NB: We keep the screen count for win7 at 2, so actions are handled in
// "default" case. Send telemetry to be able to identify what users see.
recordEvent("show", `${SCREEN_STRINGS.length}-screens`);
// Copy the initial step indicator enough times for each screen.
for (let i = SCREEN_STRINGS.length; i > 1; i--) {
steps.append(steps.lastChild.cloneNode(true));
}
steps.lastChild.classList.add("current");
break;
// Handle actions and setup for not-first and not-last screens.
default:
const { l10nId } = primary.dataset;
if (target === primary) {
switch (l10nId) {
case "upgrade-dialog-new-primary-default-button":
case "upgrade-dialog-default-primary-button":
SHELL.setAsDefault();
break;
case "upgrade-dialog-new-primary-pin-button":
case "upgrade-dialog-new-primary-pin-alt-button":
SHELL.pinToTaskbar();
break;
}
} else if (
target === secondary &&
l10nId === "upgrade-dialog-new-primary-theme-button"
) {
closeDialog("early");
return;
}
// First screen is the only screen for Windows 7.
if (win7Content) {
closeDialog("win7");
return;
}
// Prepare theme screen content only when we're moving to the last one.
if (current !== SCREEN_STRINGS.length - 1) {
break;
}
// Prepare the initial theme selection and wait for theme button clicks.
const { id, swatch } = await gPrevTheme;
themes.children[THEME_IDS.indexOf(id)].checked = true;
themes.addEventListener("click", ({ target: button }) => {
// Ignore clicks on whitespace of the container around theme buttons.
if (button.parentNode === themes) {
// Enable the theme of the corresponding button position.
const index = [...themes.children].indexOf(button);
enableTheme(THEME_IDS[index]);
recordEvent("theme", index);
}
});
// Remove the last "keep" theme option if the user didn't customize.
if (themes.childElementCount > THEME_IDS.length) {
themes.removeChild(themes.lastElementChild);
} else if (swatch) {
themes.lastElementChild.style.setProperty(
"--theme-swatch",
`url("${swatch}")`
);
}
// Load resource: theme swatches with permission.
[...themes.children].forEach(input => {
new Image().src = getComputedStyle(
input,
"::before"
).backgroundImage.match(/resource:[^"]+/)?.[0];
});
// Update content and backdrop for theme screen.
subtitle.remove();
image.remove();
items.remove();
themes.classList.remove("hidden");
adjustModalBackdrop();
break;
// Handle the last (theme) screen actions.
case SCREEN_STRINGS.length:
// New theme is confirmed, so don't revert to previous.
if (target === primary) {
gPrevTheme = null;
}
closeDialog("complete");
return;
}
// Update various elements reused across screens.
image.setAttribute("screen", current);
steps.prepend(steps.lastChild);
// Update strings for reused elements that change between screens.
await document.l10n.ready;
const translatedElements = [];
const strings = SCREEN_STRINGS[current];
for (let el of [title, subtitle, primary, secondary]) {
const stringId = await strings[el.id];
if (stringId) {
document.l10n.setAttributes(el, stringId);
translatedElements.push(el);
}
}
// Wait for initial translations to load before getting sizing information.
await document.l10n.translateElements(translatedElements);
requestAnimationFrame(() => {
// Ensure the primary button is focused on each screen.
primary.focus({ preventFocusRing: true });
// Save first screen height, so later screens can flex and anchor content.
if (current === 0) {
body.style.minHeight = getComputedStyle(body).height;
// Indicate to SubDialog that we're done sizing the first screen.
ready();
}
// Let testing know the screen is ready to continue.
dispatchEvent(new CustomEvent("ready"));
});
// Record which screen was shown identified by the primary button.
recordEvent("show", primary.dataset.l10nId);
})();
}
document.mozSubdialogReady = new Promise(resolve =>
document.addEventListener("DOMContentLoaded", () => onLoad(resolve), {
once: true,
})
);