Update On Fri Apr 4 20:23:47 CEST 2025

This commit is contained in:
github-action[bot] 2025-04-04 20:23:48 +02:00
parent 3bebcd781f
commit c3221dad80
1087 changed files with 31424 additions and 50762 deletions

View file

@ -2093,6 +2093,8 @@ pref("pdfjs.handleOctetStream", true);
// Is the sidebar positioned ahead of the content browser
pref("sidebar.position_start", true);
pref("sidebar.revamp", false);
// Should the sidebar launcher default to visible or not with horizontal tabs
pref("sidebar.revamp.defaultLauncherVisible", true);
// This is nightly only for now, as we need to address bug 1933527 and bug 1934039.
#ifdef NIGHTLY_BUILD
pref("sidebar.revamp.round-content-area", true);

View file

@ -702,11 +702,9 @@ var gXPInstallObserver = {
}
installInfo = null;
Services.telemetry
.getHistogramById("SECURITY_UI")
.add(
Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL_CLICK_THROUGH
);
Glean.securityUi.events.accumulateSingleSample(
Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL_CLICK_THROUGH
);
};
let cancelInstallation = () => {
@ -829,9 +827,9 @@ var gXPInstallObserver = {
removeNotificationOnEnd(popup, installInfo.installs);
Services.telemetry
.getHistogramById("SECURITY_UI")
.add(Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL);
Glean.securityUi.events.accumulateSingleSample(
Ci.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL
);
},
// IDs of addon install related notifications
@ -958,9 +956,9 @@ var gXPInstallObserver = {
options.removeOnDismissal = true;
options.persistent = false;
Services.telemetry
.getHistogramById("SECURITY_UI")
.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED);
Glean.securityUi.events.accumulateSingleSample(
Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED
);
let popup = PopupNotifications.show(
browser,
aTopic,
@ -1051,9 +1049,9 @@ var gXPInstallObserver = {
let learnMore = doc.getElementById("addon-install-blocked-info");
learnMore.setAttribute("support-page", article);
};
Services.telemetry
.getHistogramById("SECURITY_UI")
.add(Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED);
Glean.securityUi.events.accumulateSingleSample(
Ci.nsISecurityUITelemetry.WARNING_ADDON_ASKING_PREVENTED
);
const [
installMsg,
@ -1068,12 +1066,10 @@ var gXPInstallObserver = {
]);
const action = buildNotificationAction(installMsg, () => {
Services.telemetry
.getHistogramById("SECURITY_UI")
.add(
Ci.nsISecurityUITelemetry
.WARNING_ADDON_ASKING_PREVENTED_CLICK_THROUGH
);
Glean.securityUi.events.accumulateSingleSample(
Ci.nsISecurityUITelemetry
.WARNING_ADDON_ASKING_PREVENTED_CLICK_THROUGH
);
installInfo.install();
});

View file

@ -216,11 +216,9 @@
<menuitem id="context-saveaudio"
data-l10n-id="main-context-menu-audio-save-as"
/>
#ifdef CONTEXT_COPY_IMAGE_CONTENTS
<menuitem id="context-copyimage-contents"
data-l10n-id="main-context-menu-image-copy"
/>
#endif
<menuitem id="context-copyimage"
data-l10n-id="main-context-menu-image-copy-link"
/>

View file

@ -89,7 +89,7 @@ var gBrowserInit = {
document.documentElement.setAttribute("sizemode", "maximized");
}
}
if (AppConstants.MENUBAR_CAN_AUTOHIDE) {
if (!Services.appinfo.nativeMenubar) {
const toolbarMenubar = document.getElementById("toolbar-menubar");
// set a default value
if (!toolbarMenubar.hasAttribute("autohide")) {

View file

@ -274,6 +274,7 @@ var gProfiles = {
"aria-label",
this.bundle.GetStringFromName("panel.back")
);
backButton.style.fill = "var(--appmenu-profiles-theme-fg)";
let currentProfileCard = PanelMultiView.getViewNode(
document,
@ -296,6 +297,7 @@ var gProfiles = {
editButton.hidden = true;
} else {
profilesHeader.style.backgroundColor = "var(--appmenu-profiles-theme-bg)";
profilesHeader.style.color = "var(--appmenu-profiles-theme-fg)";
editButton.hidden = false;
}

View file

@ -6,7 +6,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#define HIDDEN_WINDOW
<?csp script-src-attr 'none'; ?>
<?csp script-src chrome: moz-src: resource:; ?>
<window id="main-window"
xmlns:html="http://www.w3.org/1999/xhtml"

View file

@ -8,6 +8,11 @@
document.addEventListener(
"DOMContentLoaded",
() => {
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
TabGroupMetrics:
"moz-src:///browser/components/tabbrowser/TabGroupMetrics.sys.mjs",
});
let mainPopupSet = document.getElementById("mainPopupSet");
// eslint-disable-next-line complexity
mainPopupSet.addEventListener("command", event => {
@ -130,7 +135,11 @@ document.addEventListener(
let tabGroup = gBrowser.getTabGroupById(tabGroupId);
// Tabs need to be removed by their owning `Tabbrowser` or else
// there are errors.
tabGroup.ownerGlobal.gBrowser.removeTabGroup(tabGroup);
tabGroup.ownerGlobal.gBrowser.removeTabGroup(tabGroup, {
isUserTriggered: true,
telemetrySource:
lazy.TabGroupMetrics.METRIC_SOURCE.TAB_OVERFLOW_MENU,
});
}
break;

View file

@ -24,7 +24,7 @@ async function testContextMenu() {
await BrowserTestUtils.withNewTab("about:blank", async () => {
let panelUIMenuButton = document.getElementById("PanelUI-menu-button");
let contextMenu = await openContextMenu(panelUIMenuButton);
let array1 = AppConstants.MENUBAR_CAN_AUTOHIDE
let array1 = !Services.appinfo.nativeMenubar
? [
".customize-context-moveToPanel",
".customize-context-removeFromToolbar",
@ -65,7 +65,7 @@ async function testContextMenu() {
info("trigger the context menu");
let contextMenu2 = await openContextMenu(panelUIMenuButton);
info("context menu should be open, verify its menu items");
let array2 = AppConstants.MENUBAR_CAN_AUTOHIDE
let array2 = !Services.appinfo.nativeMenubar
? [
".customize-context-moveToPanel",
".customize-context-removeFromToolbar",

View file

@ -80,10 +80,4 @@ DEFINES["MOZ_APP_VERSION_DISPLAY"] = CONFIG["MOZ_APP_VERSION_DISPLAY"]
DEFINES["APP_LICENSE_BLOCK"] = "%s/content/overrides/app-license.html" % SRCDIR
if CONFIG["MOZ_WIDGET_TOOLKIT"] in ("windows", "gtk", "cocoa"):
DEFINES["CONTEXT_COPY_IMAGE_CONTENTS"] = 1
if CONFIG["MOZ_WIDGET_TOOLKIT"] in ("windows", "gtk"):
DEFINES["MENUBAR_CAN_AUTOHIDE"] = 1
JAR_MANIFESTS += ["jar.mn"]

View file

@ -1849,9 +1849,7 @@ BrowserGlue.prototype = {
_firstWindowTelemetry(aWindow) {
let scaling = aWindow.devicePixelRatio * 100;
try {
Services.telemetry.getHistogramById("DISPLAY_SCALING").add(scaling);
} catch (ex) {}
Glean.gfxDisplay.scaling.accumulateSingleSample(scaling);
},
_collectStartupConditionsTelemetry() {

View file

@ -96,6 +96,14 @@ html {
--mr-screen-background-color: #F8F6F4;
--single-select-border-color: #8F8F9D;
--single-select-hover-color: #DEDEDF;
--picker-background-color: color-mix(in srgb, transparent 98%, black 2%);
--picker-hover-background-color: color-mix(in srgb, transparent 95%, black 5%);
--picker-border-color: var(--in-content-border-color);
--picker-checked-border-color: var(--in-content-item-selected);
--picker-hover-border-color: var(--picker-border-color);
--picker-focus-ring-color: var(--in-content-item-selected);
--picker-checkbox-color: var(--in-content-item-selected);
--picker-checkbox-hover-color: var(--picker-checkbox-color);
@media (prefers-color-scheme: dark) {
--grey-subtitle-1: #FFF;
@ -103,6 +111,19 @@ html {
--mr-welcome-background-gradient: linear-gradient(0deg, rgba(144, 89, 255, 30%) 0%, rgba(2, 144, 238, 30%) 100%);
--mr-screen-background-color: #62697A;
--single-select-hover-color: #52525E;
--picker-background-color: color-mix(in srgb, transparent 98%, white 2%);
--picker-hover-background-color: color-mix(in srgb, transparent 95%, white 5%);
}
@media (forced-colors: active) {
--picker-background-color: ButtonFace;
--picker-hover-background-color: SelectedItemText;
--picker-border-color: ButtonText;
--picker-checked-border-color: var(--picker-border-color);
--picker-hover-border-color: SelectedItem;
--picker-focus-ring-color: CanvasText;
--picker-checkbox-color: ButtonText;
--picker-checkbox-hover-color: SelectedItem;
}
font-family: system-ui;
@ -1028,6 +1049,11 @@ html {
@media only screen and (width <= 800px) {
padding-block: 20px;
}
.steps:not(.progress-bar) {
justify-content: start;
padding-top: 24px;
}
}
.action-buttons {
@ -2155,6 +2181,77 @@ html {
padding: 24px;
margin: 0;
&.picker {
display: flex;
flex-flow: row wrap;
box-sizing: border-box;
input[type='checkbox'] {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.checkbox-container {
display: flex;
flex-direction: row;
align-items: center;
background-color: var(--picker-background-color);
border: 1px solid var(--picker-border-color);
padding: 8px 12px;
border-radius: 100px;
user-select: none;
&:has(input:checked) {
border-color: var(--picker-checked-border-color);
border-width: 3px;
margin: -2px;
&:hover {
border-color: var(--picker-hover-border-color);
}
}
&:hover {
background-color: var(--picker-hover-background-color);
border-color: var(--picker-hover-border-color);
.picker-icon.picker-checked {
background-color: var(--picker-checkbox-hover-color);
}
}
&:focus-visible {
outline: 2px solid var(--picker-focus-ring-color);
outline-offset: 6px;
&:has(input:checked) {
outline-offset: 4px;
}
}
.picker-icon {
border-radius: 100%;
width: 27px;
height: 27px;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
margin-inline-end: 8px;
font-size: 12px;
forced-color-adjust: none;
&.picker-checked {
forced-color-adjust: auto;
background-color: var(--picker-checkbox-color);
mask: url('chrome://global/skin/icons/check.svg') center / 15px no-repeat exclude, linear-gradient(#000 0 0);
}
}
}
}
.checkbox-container {
display: grid;

View file

@ -62,8 +62,9 @@ export const MultiSelect = ({
setActiveMultiSelect,
multiSelectId,
}) => {
const { data } = content.tiles;
const { data, multiSelectItemDesign } = content.tiles;
const isPicker = multiSelectItemDesign === "picker";
const refs = useRef({});
const handleChange = useCallback(() => {
@ -107,6 +108,48 @@ export const MultiSelect = ({
[content.tiles.style]
);
const PickerIcon = ({ emoji, bgColor, isChecked }) => {
return (
<span
className={`picker-icon ${isChecked ? "picker-checked" : ""}`}
style={{
...(!isChecked && bgColor && { backgroundColor: bgColor }),
}}
>
{!isChecked && emoji ? emoji : ""}
</span>
);
};
// This handles interaction for when the user is clicking on or keyboard-interacting
// with the container element when using the picker design. It is required
// for appropriate accessibility.
const handleCheckboxContainerInteraction = e => {
if (!isPicker) {
return;
}
if (e.type === "keydown") {
// Prevent scroll on space presses
if (e.key === " ") {
e.preventDefault();
}
// Only handle space and enter keypresses
if (e.key !== " " && e.key !== "Enter") {
return;
}
}
const container = e.currentTarget;
// Manually flip the hidden checkbox since handleChange relies on it
const checkbox = container.querySelector('input[type="checkbox"]');
checkbox.checked = !checkbox.checked;
// Manually call handleChange to update the multiselect state
handleChange();
};
// When screen renders for first time, update state
// with checkbox ids that has defaultvalue true
useEffect(() => {
@ -123,7 +166,7 @@ export const MultiSelect = ({
return (
<div
className="multi-select-container"
className={`multi-select-container ${multiSelectItemDesign || ""}`}
style={containerStyle}
role={
items.some(({ type, group }) => type === "radio" && group)
@ -138,11 +181,26 @@ export const MultiSelect = ({
</Localized>
) : null}
{items.map(
({ id, label, description, icon, type = "checkbox", group, style }) => (
({
id,
label,
description,
icon,
type = "checkbox",
group,
style,
pickerEmoji,
pickerEmojiBackgroundColor,
}) => (
<div
key={id + label}
className="checkbox-container multi-select-item"
style={AboutWelcomeUtils.getValidStyle(style, MULTI_SELECT_STYLES)}
tabIndex={isPicker ? "0" : null}
onClick={isPicker ? handleCheckboxContainerInteraction : null}
onKeyDown={isPicker ? handleCheckboxContainerInteraction : null}
role={isPicker ? "checkbox" : null}
aria-checked={isPicker ? activeMultiSelect?.includes(id) : null}
>
<input
type={type} // checkbox or radio
@ -157,7 +215,15 @@ export const MultiSelect = ({
onChange={handleChange}
ref={el => (refs.current[id] = el)}
aria-describedby={description ? `${id}-description` : null}
tabIndex={isPicker ? "-1" : "0"}
/>
{isPicker && (
<PickerIcon
emoji={pickerEmoji}
bgColor={pickerEmojiBackgroundColor}
isChecked={activeMultiSelect?.includes(id)}
/>
)}
{label ? (
<Localized text={label}>
<label htmlFor={id}></label>

View file

@ -510,15 +510,12 @@ export class WelcomeScreen extends React.PureComponent {
let actionResult;
if (["OPEN_URL", "SHOW_FIREFOX_ACCOUNTS"].includes(action.type)) {
actionResult = this.handleOpenURL(
action,
props.flowParams,
props.UTMTerm
);
this.handleOpenURL(action, props.flowParams, props.UTMTerm);
} else if (action.type) {
actionResult = action.needsAwait
? await AboutWelcomeUtils.handleUserAction(action)
: AboutWelcomeUtils.handleUserAction(action);
let actionPromise = AboutWelcomeUtils.handleUserAction(action);
if (action.needsAwait) {
actionResult = await actionPromise;
}
if (action.type === "FXA_SIGNIN_FLOW") {
AboutWelcomeUtils.sendActionTelemetry(
props.messageId,
@ -583,6 +580,15 @@ export class WelcomeScreen extends React.PureComponent {
props.navigate();
}
// Used by FeatureCallout to advance screens by re-rendering the whole
// wrapper, updating anchor, page_event_listeners, etc. `navigate` only
// updates the inner content. Only implemented by FeatureCallout.
if (action.advance_screens) {
if (shouldDoBehavior(action.advance_screens.behavior ?? true)) {
window.AWAdvanceScreens?.(action.advance_screens);
}
}
if (shouldDoBehavior(action.dismiss)) {
window.AWFinish();
}

View file

@ -582,7 +582,10 @@ export class ProtonScreen extends React.PureComponent {
? this.renderPicture(content.logo)
: null}
{content.title || content.subtitle ? (
<div className={`welcome-text ${content.title_style || ""}`}>
<div
id="multi-stage-message-welcome-text"
className={`welcome-text ${content.title_style || ""}`}
>
{content.title ? this.renderTitle(content) : null}
{content.subtitle ? (
@ -639,8 +642,21 @@ export class ProtonScreen extends React.PureComponent {
activeMultiSelect={this.props.activeMultiSelect}
/>
)}
{
/* Fullscreen dot-style step indicator should sit inside the
main inner content to share its padding, which will be
configurable with Bug 1956042 */
!hideStepsIndicator &&
!aboveButtonStepsIndicator &&
!content.progress_bar &&
content.fullscreen
? this.renderStepsIndicator()
: null
}
</div>
{!hideStepsIndicator && !aboveButtonStepsIndicator
{!hideStepsIndicator &&
!aboveButtonStepsIndicator &&
!(content.fullscreen && !content.progress_bar)
? this.renderStepsIndicator()
: null}
</div>

View file

@ -627,9 +627,12 @@ class WelcomeScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo
}
let actionResult;
if (["OPEN_URL", "SHOW_FIREFOX_ACCOUNTS"].includes(action.type)) {
actionResult = this.handleOpenURL(action, props.flowParams, props.UTMTerm);
this.handleOpenURL(action, props.flowParams, props.UTMTerm);
} else if (action.type) {
actionResult = action.needsAwait ? await _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.handleUserAction(action) : _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.handleUserAction(action);
let actionPromise = _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.handleUserAction(action);
if (action.needsAwait) {
actionResult = await actionPromise;
}
if (action.type === "FXA_SIGNIN_FLOW") {
_lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.sendActionTelemetry(props.messageId, actionResult ? "sign_in" : "sign_in_cancel", "FXA_SIGNIN_FLOW");
}
@ -678,6 +681,15 @@ class WelcomeScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCo
if (shouldDoBehavior(action.navigate)) {
props.navigate();
}
// Used by FeatureCallout to advance screens by re-rendering the whole
// wrapper, updating anchor, page_event_listeners, etc. `navigate` only
// updates the inner content. Only implemented by FeatureCallout.
if (action.advance_screens) {
if (shouldDoBehavior(action.advance_screens.behavior ?? true)) {
window.AWAdvanceScreens?.(action.advance_screens);
}
}
if (shouldDoBehavior(action.dismiss)) {
window.AWFinish();
}
@ -1350,6 +1362,7 @@ class ProtonScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
justifyContent: content.split_content_justify_content
}
}, content.logo && content.fullscreen ? this.renderPicture(content.logo) : null, content.title || content.subtitle ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
id: "multi-stage-message-welcome-text",
className: `welcome-text ${content.title_style || ""}`
}, content.title ? this.renderTitle(content) : null, content.subtitle ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
text: content.subtitle
@ -1376,7 +1389,11 @@ class ProtonScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
addonName: this.props.addonName,
handleAction: this.props.handleAction,
activeMultiSelect: this.props.activeMultiSelect
})), !hideStepsIndicator && !aboveButtonStepsIndicator ? this.renderStepsIndicator() : null)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
}),
/* Fullscreen dot-style step indicator should sit inside the
main inner content to share its padding, which will be
configurable with Bug 1956042 */
!hideStepsIndicator && !aboveButtonStepsIndicator && !content.progress_bar && content.fullscreen ? this.renderStepsIndicator() : null), !hideStepsIndicator && !aboveButtonStepsIndicator && !(content.fullscreen && !content.progress_bar) ? this.renderStepsIndicator() : null)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
text: content.info_text
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {
className: "info-text"
@ -2661,8 +2678,10 @@ const MultiSelect = ({
multiSelectId
}) => {
const {
data
data,
multiSelectItemDesign
} = content.tiles;
const isPicker = multiSelectItemDesign === "picker";
const refs = (0,react__WEBPACK_IMPORTED_MODULE_0__.useRef)({});
const handleChange = (0,react__WEBPACK_IMPORTED_MODULE_0__.useCallback)(() => {
const newActiveMultiSelect = [];
@ -2691,6 +2710,47 @@ const MultiSelect = ({
}, [] // eslint-disable-line react-hooks/exhaustive-deps
);
const containerStyle = (0,react__WEBPACK_IMPORTED_MODULE_0__.useMemo)(() => _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.getValidStyle(content.tiles.style, MULTI_SELECT_STYLES, true), [content.tiles.style]);
const PickerIcon = ({
emoji,
bgColor,
isChecked
}) => {
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {
className: `picker-icon ${isChecked ? "picker-checked" : ""}`,
style: {
...(!isChecked && bgColor && {
backgroundColor: bgColor
})
}
}, !isChecked && emoji ? emoji : "");
};
// This handles interaction for when the user is clicking on or keyboard-interacting
// with the container element when using the picker design. It is required
// for appropriate accessibility.
const handleCheckboxContainerInteraction = e => {
if (!isPicker) {
return;
}
if (e.type === "keydown") {
// Prevent scroll on space presses
if (e.key === " ") {
e.preventDefault();
}
// Only handle space and enter keypresses
if (e.key !== " " && e.key !== "Enter") {
return;
}
}
const container = e.currentTarget;
// Manually flip the hidden checkbox since handleChange relies on it
const checkbox = container.querySelector('input[type="checkbox"]');
checkbox.checked = !checkbox.checked;
// Manually call handleChange to update the multiselect state
handleChange();
};
// When screen renders for first time, update state
// with checkbox ids that has defaultvalue true
@ -2710,7 +2770,7 @@ const MultiSelect = ({
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "multi-select-container",
className: `multi-select-container ${multiSelectItemDesign || ""}`,
style: containerStyle,
role: items.some(({
type,
@ -2728,11 +2788,18 @@ const MultiSelect = ({
icon,
type = "checkbox",
group,
style
style,
pickerEmoji,
pickerEmojiBackgroundColor
}) => /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
key: id + label,
className: "checkbox-container multi-select-item",
style: _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.getValidStyle(style, MULTI_SELECT_STYLES)
style: _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.getValidStyle(style, MULTI_SELECT_STYLES),
tabIndex: isPicker ? "0" : null,
onClick: isPicker ? handleCheckboxContainerInteraction : null,
onKeyDown: isPicker ? handleCheckboxContainerInteraction : null,
role: isPicker ? "checkbox" : null,
"aria-checked": isPicker ? activeMultiSelect?.includes(id) : null
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", {
type: type // checkbox or radio
,
@ -2743,7 +2810,12 @@ const MultiSelect = ({
style: _lib_aboutwelcome_utils_mjs__WEBPACK_IMPORTED_MODULE_2__.AboutWelcomeUtils.getValidStyle(icon?.style, MULTI_SELECT_ICON_STYLES),
onChange: handleChange,
ref: el => refs.current[id] = el,
"aria-describedby": description ? `${id}-description` : null
"aria-describedby": description ? `${id}-description` : null,
tabIndex: isPicker ? "-1" : "0"
}), isPicker && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(PickerIcon, {
emoji: pickerEmoji,
bgColor: pickerEmojiBackgroundColor,
isChecked: activeMultiSelect?.includes(id)
}), label ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
text: label
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", {

View file

@ -1260,6 +1260,14 @@ html {
--mr-screen-background-color: #F8F6F4;
--single-select-border-color: #8F8F9D;
--single-select-hover-color: #DEDEDF;
--picker-background-color: color-mix(in srgb, transparent 98%, black 2%);
--picker-hover-background-color: color-mix(in srgb, transparent 95%, black 5%);
--picker-border-color: var(--in-content-border-color);
--picker-checked-border-color: var(--in-content-item-selected);
--picker-hover-border-color: var(--picker-border-color);
--picker-focus-ring-color: var(--in-content-item-selected);
--picker-checkbox-color: var(--in-content-item-selected);
--picker-checkbox-hover-color: var(--picker-checkbox-color);
font-family: system-ui;
font-size: 16px;
position: relative;
@ -1274,6 +1282,20 @@ html {
--mr-welcome-background-gradient: linear-gradient(0deg, rgba(144, 89, 255, 30%) 0%, rgba(2, 144, 238, 30%) 100%);
--mr-screen-background-color: #62697A;
--single-select-hover-color: #52525E;
--picker-background-color: color-mix(in srgb, transparent 98%, white 2%);
--picker-hover-background-color: color-mix(in srgb, transparent 95%, white 5%);
}
}
@media (forced-colors: active) {
.onboardingContainer {
--picker-background-color: ButtonFace;
--picker-hover-background-color: SelectedItemText;
--picker-border-color: ButtonText;
--picker-checked-border-color: var(--picker-border-color);
--picker-hover-border-color: SelectedItem;
--picker-focus-ring-color: CanvasText;
--picker-checkbox-color: ButtonText;
--picker-checkbox-hover-color: SelectedItem;
}
}
@media (prefers-contrast) {
@ -2077,6 +2099,10 @@ html {
padding-block: 20px;
}
}
.onboardingContainer .screen[pos=split][fullscreen] .section-main .main-content .main-content-inner .steps:not(.progress-bar) {
justify-content: start;
padding-top: 24px;
}
.onboardingContainer .screen[pos=split][fullscreen] .section-main .main-content .action-buttons {
position: static;
height: auto;
@ -3210,6 +3236,66 @@ html {
padding: 24px;
margin: 0;
}
.onboardingContainer #content-tiles-container .content-tile .multi-select-container.picker {
display: flex;
flex-flow: row wrap;
box-sizing: border-box;
}
.onboardingContainer #content-tiles-container .content-tile .multi-select-container.picker input[type=checkbox] {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.onboardingContainer #content-tiles-container .content-tile .multi-select-container.picker .checkbox-container {
display: flex;
flex-direction: row;
align-items: center;
background-color: var(--picker-background-color);
border: 1px solid var(--picker-border-color);
padding: 8px 12px;
border-radius: 100px;
user-select: none;
}
.onboardingContainer #content-tiles-container .content-tile .multi-select-container.picker .checkbox-container:has(input:checked) {
border-color: var(--picker-checked-border-color);
border-width: 3px;
margin: -2px;
}
.onboardingContainer #content-tiles-container .content-tile .multi-select-container.picker .checkbox-container:has(input:checked):hover {
border-color: var(--picker-hover-border-color);
}
.onboardingContainer #content-tiles-container .content-tile .multi-select-container.picker .checkbox-container:hover {
background-color: var(--picker-hover-background-color);
border-color: var(--picker-hover-border-color);
}
.onboardingContainer #content-tiles-container .content-tile .multi-select-container.picker .checkbox-container:hover .picker-icon.picker-checked {
background-color: var(--picker-checkbox-hover-color);
}
.onboardingContainer #content-tiles-container .content-tile .multi-select-container.picker .checkbox-container:focus-visible {
outline: 2px solid var(--picker-focus-ring-color);
outline-offset: 6px;
}
.onboardingContainer #content-tiles-container .content-tile .multi-select-container.picker .checkbox-container:focus-visible:has(input:checked) {
outline-offset: 4px;
}
.onboardingContainer #content-tiles-container .content-tile .multi-select-container.picker .checkbox-container .picker-icon {
border-radius: 100%;
width: 27px;
height: 27px;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
margin-inline-end: 8px;
font-size: 12px;
forced-color-adjust: none;
}
.onboardingContainer #content-tiles-container .content-tile .multi-select-container.picker .checkbox-container .picker-icon.picker-checked {
forced-color-adjust: auto;
background-color: var(--picker-checkbox-color);
mask: url("chrome://global/skin/icons/check.svg") center/15px no-repeat exclude, linear-gradient(#000 0 0);
}
.onboardingContainer #content-tiles-container .content-tile .multi-select-container .checkbox-container {
display: grid;
}

View file

@ -42,6 +42,53 @@ const BASE_CONTENT = {
},
};
const PICKER_CONTENT = {
id: "MULTI_SELECT_TEST",
targeting: "true",
content: {
fullscreen: true,
position: "split",
progress_bar: true,
logo: {},
tiles: [
{
type: "multiselect",
multiSelectItemDesign: "picker",
subtitle: { raw: "What are you using Firefox for?" },
data: [
{
id: "checkbox-school",
defaultValue: false,
pickerEmoji: "🎓",
pickerEmojiBackgroundColor: "#c3e0ff",
label: {
raw: "School",
},
checkedAction: {
type: "SET_PREF",
data: {
pref: {
name: "onboarding-personalization.school",
value: true,
},
},
},
uncheckedAction: {
type: "SET_PREF",
data: {
pref: {
name: "onboarding-personalization.school",
value: false,
},
},
},
},
],
},
],
},
};
/**
* Core multiselect functionality is covered in
* browser_aboutwelcome_multistage_mr.js
@ -75,3 +122,42 @@ add_task(async function test_multiselect_with_item_description() {
]
);
});
/**
* Test multiselect styles with picker configuration
*/
add_task(async function test_picker_multiselect_styles() {
const TEST_JSON = JSON.stringify([PICKER_CONTENT]);
let browser = await openAboutWelcome(TEST_JSON);
await test_screen_content(
browser,
"renders screen with a picker checklist item",
// Expected selectors:
[
// multiselect container has picker class
`.multi-select-container.picker`,
// Checkbox container should have role, tabindex, aria-checked properties
`.checkbox-container[role="checkbox"]`,
`.checkbox-container[tabIndex="0"]`,
`.checkbox-container[aria-checked="false"]`,
],
// Unexpected selectors
[
// Hidden input should be unchecked
`input[type="checkbox"]:checked`,
]
);
// Hidden input should indeed be hidden
await test_element_styles(browser, ".checkbox-container input", {
width: "0px",
height: "0px",
opacity: "0",
});
// Picker icon background color should match passed value
await test_element_styles(browser, ".picker-icon", {
backgroundColor: "rgb(195, 224, 255)",
});
});

View file

@ -218,4 +218,166 @@ describe("MultiSelect component", () => {
assert.strictEqual(checks.first().prop("style").color, "blue");
assert.strictEqual(checks.at(1).prop("style").color, "yellow");
});
it("should render picker elements when multiSelectItemDesign is 'picker'", () => {
const PICKER_PROPS = { ...MULTISELECT_SCREEN_PROPS };
PICKER_PROPS.content.tiles.multiSelectItemDesign = "picker";
PICKER_PROPS.content.tiles.data = [
{
id: "picker-option-1",
defaultValue: true,
label: "Picker Option 1",
pickerEmoji: "🙃",
pickerEmojiBackgroundColor: "#c3e0ff",
},
{
id: "picker-option-2",
defaultValue: false,
label: "Picker Option 2",
pickerEmoji: "✨",
pickerEmojiBackgroundColor: "#ffebcc",
},
];
const wrapper = mount(<MultiSelect {...PICKER_PROPS} />);
wrapper.setProps({ activeMultiSelect: ["picker-option-1"] });
// Container should have picker class
const container = wrapper.find(".multi-select-container");
assert.strictEqual(container.hasClass("picker"), true);
const pickerIcons = wrapper.find(".picker-icon");
assert.strictEqual(pickerIcons.length, 2);
// First icon should be checked (no emoji, no background color)
const firstIcon = pickerIcons.at(0);
assert.strictEqual(firstIcon.hasClass("picker-checked"), true);
assert.strictEqual(firstIcon.text(), "");
assert.strictEqual(firstIcon.prop("style").backgroundColor, undefined);
// Second icon should not be checked (should have emoji and background color)
const secondIcon = pickerIcons.at(1);
assert.strictEqual(secondIcon.hasClass("picker-checked"), false);
assert.strictEqual(secondIcon.text(), "✨");
assert.strictEqual(secondIcon.prop("style").backgroundColor, "#ffebcc");
});
// The picker design adds functionality for checkbox to be checked
// even when click events occur on the container itself, instead of just
// the label or input
it("should handle click events for picker design", () => {
const PICKER_PROPS = { ...MULTISELECT_SCREEN_PROPS };
PICKER_PROPS.content.tiles.multiSelectItemDesign = "picker";
PICKER_PROPS.content.tiles.data = [
{
id: "picker-option-1",
defaultValue: true,
label: "Picker Option 1",
pickerEmoji: "🙃",
},
{
id: "picker-option-2",
defaultValue: false,
label: "Picker Option 2",
pickerEmoji: "✨",
},
];
const wrapper = mount(<MultiSelect {...PICKER_PROPS} />);
wrapper.setProps({ activeMultiSelect: ["picker-option-1"] });
// check the container of the second item
const checkboxContainers = wrapper.find(".checkbox-container");
const secondContainer = checkboxContainers.at(1);
secondContainer.simulate("click");
// setActiveMultiSelect should be called with both ids
assert.calledWith(setActiveMultiSelect, [
"picker-option-1",
"picker-option-2",
]);
// uncheck the first item
const firstContainer = checkboxContainers.at(0);
firstContainer.simulate("click");
// setActiveMultiSelect should be called with just the second id
assert.calledWith(setActiveMultiSelect, ["picker-option-2"]);
});
it("should handle keyboard events for picker design", () => {
const PICKER_PROPS = { ...MULTISELECT_SCREEN_PROPS };
PICKER_PROPS.content.tiles.multiSelectItemDesign = "picker";
PICKER_PROPS.content.tiles.data = [
{
id: "picker-option-1",
defaultValue: false,
label: "Picker Option 1",
},
];
const wrapper = mount(<MultiSelect {...PICKER_PROPS} />);
wrapper.setProps({ activeMultiSelect: [] });
const checkboxContainer = wrapper.find(".checkbox-container").first();
// Test spacebar press
checkboxContainer.simulate("keydown", {
key: " ",
});
assert.calledWith(setActiveMultiSelect, ["picker-option-1"]);
// Test Enter press
checkboxContainer.simulate("keydown", {
key: "Enter",
});
assert.calledWith(setActiveMultiSelect, []);
// Test other key press
setActiveMultiSelect.reset();
checkboxContainer.simulate("keydown", {
key: "Tab",
});
assert.notCalled(setActiveMultiSelect);
});
it("should not use handleCheckboxContainerInteraction when multiSelectItemDesign is not 'picker'", () => {
const wrapper = mount(<MultiSelect {...MULTISELECT_SCREEN_PROPS} />);
wrapper.setProps({ activeMultiSelect: ["checkbox-1"] });
const checkboxContainer = wrapper.find(".checkbox-container").first();
assert.strictEqual(checkboxContainer.prop("tabIndex"), null);
assert.strictEqual(checkboxContainer.prop("onClick"), null);
assert.strictEqual(checkboxContainer.prop("onKeyDown"), null);
// Likewise, the extra accessibility attributes should not be present on the container
assert.strictEqual(checkboxContainer.prop("role"), null);
assert.strictEqual(checkboxContainer.prop("aria-checked"), null);
});
it("should set proper accessibility attributes for picker design when multiSelectItemDesign is 'picker' ", () => {
const PICKER_PROPS = { ...MULTISELECT_SCREEN_PROPS };
PICKER_PROPS.content.tiles.multiSelectItemDesign = "picker";
PICKER_PROPS.content.tiles.data = [
{
id: "picker-option-1",
defaultValue: true,
label: "Picker Option 1",
},
];
const wrapper = mount(<MultiSelect {...PICKER_PROPS} />);
wrapper.setProps({ activeMultiSelect: ["picker-option-1"] });
const checkboxContainer = wrapper.find(".checkbox-container").first();
// the checkbox-container should have appropriate accessibility attributes
assert.strictEqual(checkboxContainer.prop("tabIndex"), "0");
assert.strictEqual(checkboxContainer.prop("role"), "checkbox");
assert.strictEqual(checkboxContainer.prop("aria-checked"), true);
// the actual (hidden) checkbox should have tabIndex="-1" (to avoid double focus)
const checkbox = wrapper.find("input[type='checkbox']").first();
assert.strictEqual(checkbox.prop("tabIndex"), "-1");
});
});

View file

@ -421,6 +421,37 @@ describe("MultiStageAboutWelcomeProton module", () => {
assert.equal(siblingElement.classList.contains("action-buttons"), true);
});
it("should render the steps indicator in main inner content if fullscreen and not progress bar style", () => {
const SCREEN_PROPS = {
content: {
title: "Test Fullscreen Dot Steps",
fullscreen: true,
position: "split",
progress_bar: false,
totalNumberOfScreens: 2,
},
};
const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />);
const stepsIndicators = wrapper.find(".steps");
assert.equal(
stepsIndicators.length,
1,
"Only one steps indicator should be rendered"
);
assert.isTrue(
wrapper.find(".main-content-inner .steps").exists(),
"Steps indicator is inside main-content-inner"
);
assert.isFalse(
stepsIndicators.first().hasClass("progress-bar"),
"Steps indicator should not have progress-bar class"
);
});
it("should render a progress bar if there are 2 steps", () => {
const SCREEN_PROPS = {
content: {

View file

@ -135,7 +135,7 @@ interface FeatureCallout {
// messages in Nimbus experiments. It's for local messages only.
skip_in_tests?: string;
content: {
// The same as the id above
// Must match the top-level id above.
id: string;
template: "multistage";
backdrop: "transparent";
@ -144,7 +144,13 @@ interface FeatureCallout {
// The name of a preference that will be used to store screen progress. Only
// relevant if your callout has multiple screens and serves as a tour. This
// allows tour progress to persist across sessions and even devices, if the
// pref is synced via FxA. In most cases, this will not be needed.
// pref is synced via FxA. In most cases, this will not be needed. A tour
// pref name allows a callout's SET_PREF actions to advance screens or
// dismiss the callout. Optional, as `advance_screens` and `dismiss` handle
// progress too. Tour prefs allow resuming from the same screen after
// dismissal or restart, or even syncing progress across devices. Pref names
// must be in `SpecialMessageActions.sys.mjs#allowedPrefs` or start with
// "messaging-system-action." (e.g., "messaging-system-action.tour1").
tour_pref_name?: string;
// A default value for the pref. Can be used if the pref is not set in
// Firefox's default prefs. This is the default value that will be used
@ -163,248 +169,249 @@ interface FeatureCallout {
// `primary_button.action`) with `navigate: true`, the user can advance to
// the next screen, causing the first screen to fade out and the next screen
// to fade in.
screens: [
{
id: string;
// Feature callouts with multiple screens show a series of dots at the
// bottom, indicating which screen the user is on. This property allows
// you to hide those dots. The steps indicator is already hidden if
// there's only one screen, since it's unnecessary. Defaults to false.
force_hide_steps_indicator?: boolean;
// An array of anchor objects. Each anchor object represents a single
// element on the page that the callout should be anchored to. The
// callout will be anchored to the first visible element in the array.
anchors: [
{
// A CSS selector for the element to anchor to. The callout will be
// anchored to the first visible element that matches this selector.
// This supports a special token %triggerTab% that functions as a
// selector for the tab that triggered the callout, usually (but not
// always) the selected tab. It can be placed at any position in the
// selector, like other tokens. For example:
// "#tabbrowser-tabs %triggerTab%[visuallyselected] .tab-icon-image"
selector: string;
// An object representing how the callout should be positioned
// relative to the anchor element.
panel_position: {
// The point on the anchor that the callout should be tied to. See
// PopupAttachmentPoint below for the possible values. These are
// the same values used by XULPopupElements.
anchor_attachment: PopupAttachmentPoint;
// The point on the callout that should be tied to the anchor.
callout_attachment: PopupAttachmentPoint;
// Offsets in pixels to apply to the callout position in the
// horizontal and vertical directions. Generally not needed.
offset_x?: number;
offset_y?: number;
};
// Hide the arrow that points from the callout to the anchor?
hide_arrow?: boolean;
// Whether to set the [open] style on the anchor element while the
// callout is showing. False to set it, true to not set it. Not all
// elements have an [open] style. Buttons do, for example. It's
// usually similar to :active.
no_open_on_anchor?: boolean;
// The desired width of the arrow in a number of pixels. 33.94113 by
// default (this corresponds to a triangle with 24px edges). This
// also affects the height of the arrow.
arrow_width?: number;
}
];
content: {
position: "callout";
// By default, callouts don't hide if the user clicks outside of them.
// Set this to true to make the callout hide on outside clicks.
autohide?: boolean;
// By default, hitting Escape will dismiss the callout, whether it is
// focused or not. Setting this to true will stop keypresses from
// dispatching up to the callout from outside it, though they will
// still work when the callout is focused. Best to leave this as-is.
ignorekeys?: boolean;
// Callout card width as a CSS value, e.g. "400px" or "min-content".
// Defaults to "400px".
width?: string;
// Callout card padding as a CSS value, e.g. "12px 16px" or "1em".
// Defaults to "16px".
padding?: number;
// Callouts normally have a vertical layout, with rows of content. If
// you want a single row with a more inline layout, you can use this
// property, which works well in tandem with title_logo.
layout?: "inline";
// An optional object representing a large illustration to show above
// other content. See Logo below for the possible properties.
logo?: Logo;
// The callout's headline. This is optional but commonly used. Can be
// a raw string or a LocalizableThing (see interface below).
title?: Label;
// An optional object representing an icon to show next to the title.
// See TitleLogo below for the possible properties.
title_logo?: TitleLogo;
// A subtitle to show below the title. Typically a longer paragraph.
subtitle?: Label;
primary_button?: {
// Text to show inside the button.
label: Label;
// Buttons can optionally show an arrow icon, indicating that
// clicking the button will advance to the next screen.
has_arrow_icon?: boolean;
// Buttons can be disabled. The boolean option isn't really useful,
// since there's no logic to enable the button. However, if your
// screen uses the "multiselect" tile (see tiles), you can use
// "hasActiveMultiSelect" to disable the button until the user
// selects something.
disabled?: boolean | "hasActiveMultiSelect";
// Primary buttons can have a "primary" or "secondary" style. This
// is useful because you can't change the order of the buttons, but
// you can swap the primary and secondary buttons' styles.
style?: "primary" | "secondary";
// The action to take when the button is clicked. See Action below.
action: Action;
screens: Array<{
// A unique screen ID recorded in impression telemetry. Each screen in a
// message should have a different ID, which can be referenced in actions
// to update the tour pref and advance screens.
id: string;
// Feature callouts with multiple screens show a series of dots at the
// bottom, indicating which screen the user is on. This property allows
// you to hide those dots. The steps indicator is already hidden if
// there's only one screen, since it's unnecessary. Defaults to false.
force_hide_steps_indicator?: boolean;
// An array of anchor objects. Each anchor object represents a single
// element on the page that the callout should be anchored to. The
// callout will be anchored to the first visible element in the array.
anchors: [
{
// A CSS selector for the element to anchor to. The callout will be
// anchored to the first visible element that matches this selector.
// This supports a special token %triggerTab% that functions as a
// selector for the tab that triggered the callout, usually (but not
// always) the selected tab. It can be placed at any position in the
// selector, like other tokens. For example:
// "#tabbrowser-tabs %triggerTab%[visuallyselected] .tab-icon-image"
selector: string;
// An object representing how the callout should be positioned
// relative to the anchor element.
panel_position: {
// The point on the anchor that the callout should be tied to. See
// PopupAttachmentPoint below for the possible values. These are
// the same values used by XULPopupElements.
anchor_attachment: PopupAttachmentPoint;
// The point on the callout that should be tied to the anchor.
callout_attachment: PopupAttachmentPoint;
// Offsets in pixels to apply to the callout position in the
// horizontal and vertical directions. Generally not needed.
offset_x?: number;
offset_y?: number;
};
secondary_button?: {
label: Label;
// Extra text to show before the button.
text: Label;
has_arrow_icon?: boolean;
disabled?: boolean | "hasActiveMultiSelect";
style?: "primary" | "secondary";
action: Action;
};
additional_button?: {
label: Label;
// If you have several buttons, you can use this property to control
// the orientation of the buttons. By default, buttons are laid out
// in a complex way. Use row or column to override this.
flow?: "row" | "column";
disabled?: boolean;
// The additional button can also be styled as a link.
style?: "primary" | "secondary" | "link";
action: Action;
// Justification/alignment of the buttons row/column. Defaults to
// "end" (right-justified buttons). You can use space-between if,
// for example, you have 2 buttons and you want one on the left and
// one on the right.
alignment?: "start" | "end" | "space-between";
};
dismiss_button?: {
// This can be used to control the ARIA attributes and tooltip.
// Usually it's omitted, since it has a correct default value.
label?: Label;
// The button can be 32px or 24px. Defaults to 32px.
size?: "small" | "large";
action: Action;
// CSS overrides.
// Hide the arrow that points from the callout to the anchor?
hide_arrow?: boolean;
// Whether to apply the [open] style to the anchor element when the
// callout is shown. Relevant for elements like buttons with an [open]
// style that adds shading, similar to :active. False to apply the
// style, true to skip it.
no_open_on_anchor?: boolean;
// The desired width of the arrow in a number of pixels. 33.94113 by
// default (this corresponds to a triangle with 24px edges). This
// also affects the height of the arrow.
arrow_width?: number;
}
];
content: {
position: "callout";
// By default, callouts don't hide if the user clicks outside of them.
// Set this to true to make the callout hide on outside clicks.
autohide?: boolean;
// By default, hitting Escape will dismiss the callout, whether it is
// focused or not. Setting this to true will stop keypresses from
// dispatching up to the callout from outside it, though they will
// still work when the callout is focused. Best to leave this as-is.
ignorekeys?: boolean;
// Callout card width as a CSS value, e.g. "400px" or "min-content".
// Defaults to "400px".
width?: string;
// Callout card padding as a CSS value, e.g. "12px 16px" or "1em".
// Defaults to "16px".
padding?: number;
// Callouts normally have a vertical layout, with rows of content. If
// you want a single row with a more inline layout, you can use this
// property, which works well in tandem with title_logo.
layout?: "inline";
// An optional object representing a large illustration to show above
// other content. See Logo below for the possible properties.
logo?: Logo;
// The callout's headline. This is optional but commonly used. Can be
// a raw string or a LocalizableThing (see interface below).
title?: Label;
// An optional object representing an icon to show next to the title.
// See TitleLogo below for the possible properties.
title_logo?: TitleLogo;
// A subtitle to show below the title. Typically a longer paragraph.
subtitle?: Label;
primary_button?: {
// Text to show inside the button.
label: Label;
// Buttons can optionally show an arrow icon, indicating that
// clicking the button will advance to the next screen.
has_arrow_icon?: boolean;
// Buttons can be disabled. The boolean option isn't really useful,
// since there's no logic to enable the button. However, if your
// screen uses the "multiselect" tile (see tiles), you can use
// "hasActiveMultiSelect" to disable the button until the user
// selects something.
disabled?: boolean | "hasActiveMultiSelect";
// Primary buttons can have a "primary" or "secondary" style. This
// is useful because you can't change the order of the buttons, but
// you can swap the primary and secondary buttons' styles.
style?: "primary" | "secondary";
// The action to take when the button is clicked. See Action below.
action: Action;
};
secondary_button?: {
label: Label;
// Extra text to show before the button.
text: Label;
has_arrow_icon?: boolean;
disabled?: boolean | "hasActiveMultiSelect";
style?: "primary" | "secondary";
action: Action;
};
additional_button?: {
label: Label;
// If you have several buttons, you can use this property to control
// the orientation of the buttons. By default, buttons are laid out
// in a complex way. Use row or column to override this.
flow?: "row" | "column";
disabled?: boolean;
// The additional button can also be styled as a link.
style?: "primary" | "secondary" | "link";
action: Action;
// Justification/alignment of the buttons row/column. Defaults to
// "end" (right-justified buttons). You can use space-between if,
// for example, you have 2 buttons and you want one on the left and
// one on the right.
alignment?: "start" | "end" | "space-between";
};
dismiss_button?: {
// This can be used to control the ARIA attributes and tooltip.
// Usually it's omitted, since it has a correct default value.
label?: Label;
// The button can be 32px or 24px. Defaults to 32px.
size?: "small" | "large";
action: Action;
// CSS overrides.
marginBlock?: string;
marginInline?: string;
};
// A split button is an additional_button or secondary_button split
// into 2 buttons: one that performs the main action, and one with an
// arrow that opens a dropdown submenu (which this property controls).
submenu_button?: {
// This defines the dropdown menu that appears when the user clicks
// the split button.
submenu: SubmenuItem[];
// The submenu button can only be a split button, so a secondary or
// additional button needs to exist for it to attach to.
attached_to: "secondary_button" | "additional_button";
// Used mainly to control the ARIA label and tooltip (tooltips are
// currently broken), but can also be used to override CSS styles.
label?: Label;
// Whether the split button should follow the primary or secondary
// button style. Set this to the same style you specified for the
// button it's attached to. Defaults to "secondary".
style?: "primary" | "secondary";
};
// Predefined content modules. The only one currently supported in
// feature callout is "multiselect", which allows you to show a series
// of checkboxes and/or radio buttons.
tiles?: {
type: "multiselect";
// Depends on the type, but we only support "multiselect" currently.
data: MultiSelectItem[];
// Allows CSS overrides of the multiselect container.
style?: {
color?: string;
fontSize?: string;
fontWeight?: string;
letterSpacing?: string;
lineHeight?: string;
marginBlock?: string;
marginInline?: string;
paddingBlock?: string;
paddingInline?: string;
whiteSpace?: string;
flexDirection?: string;
flexWrap?: string;
flexFlow?: string;
flexGrow?: string;
flexShrink?: string;
justifyContent?: string;
alignItems?: string;
gap?: string;
// Any CSS properties starting with "--" are also allowed, to
// override CSS variables used in _feature-callout.scss.
"--some-variable"?: string;
};
// A split button is an additional_button or secondary_button split
// into 2 buttons: one that performs the main action, and one with an
// arrow that opens a dropdown submenu (which this property controls).
submenu_button?: {
// This defines the dropdown menu that appears when the user clicks
// the split button.
submenu: SubmenuItem[];
// The submenu button can only be a split button, so a secondary or
// additional button needs to exist for it to attach to.
attached_to: "secondary_button" | "additional_button";
// Used mainly to control the ARIA label and tooltip (tooltips are
// currently broken), but can also be used to override CSS styles.
label?: Label;
// Whether the split button should follow the primary or secondary
// button style. Set this to the same style you specified for the
// button it's attached to. Defaults to "secondary".
style?: "primary" | "secondary";
};
// Predefined content modules. The only one currently supported in
// feature callout is "multiselect", which allows you to show a series
// of checkboxes and/or radio buttons.
tiles?: {
type: "multiselect";
// Depends on the type, but we only support "multiselect" currently.
data: MultiSelectItem[];
// Allows CSS overrides of the multiselect container.
style?: {
color?: string;
fontSize?: string;
fontWeight?: string;
letterSpacing?: string;
lineHeight?: string;
marginBlock?: string;
marginInline?: string;
paddingBlock?: string;
paddingInline?: string;
whiteSpace?: string;
flexDirection?: string;
flexWrap?: string;
flexFlow?: string;
flexGrow?: string;
flexShrink?: string;
justifyContent?: string;
alignItems?: string;
gap?: string;
// Any CSS properties starting with "--" are also allowed, to
// override CSS variables used in _feature-callout.scss.
"--some-variable"?: string;
};
};
// The dots in the corner that show what screen you're on and how many
// screens there are in total. This property is only used to override
// the ARIA attributes or tooltip. Not recommended.
steps_indicator?: {
string_id: string;
};
// An extra block of configurable content below the title/subtitle but
// above the optional `tiles` section and the main buttons. Styles not
// yet implemented; not recommended.
above_button_content?: LinkParagraphOrImage[];
// An optional array of event listeners to add to the page where the
// feature callout is shown. This can be used to perform actions in
// response to interactions and other events outside of the feature
// callout itself. The prototypical use case is dismissing the feature
// callout when the user clicks the button the callout is anchored to.
// It also supports performing actions on a timeout/interval.
page_event_listeners?: Array<{
params: {
// Event type string, e.g. "click". This supports:
// 1. Any DOM event type
// 2. "timeout" and "interval" for timers
// 3. Internal feature callout events: "touradvance" and
// "tourend". This can be used to perform actions when the user
// advances to the next screen or finishes the callout tour.
type: string;
// Target selector, e.g. `tag.class, #id[attr]` - Not needed for
// all types.
selectors?: string;
// addEventListener options
options: {
// Handle events in capturing phase?
capture?: boolean;
// Remove listener after first event?
once?: boolean;
// Prevent default action in event handler?
preventDefault?: boolean;
// Used only for `timeout` and `interval` event types. These
// don't set up real event listeners, but instead invoke the
// action on a timer.
interval?: number;
// Extend addEventListener to all windows? Not compatible with
// `interval`.
every_window: boolean;
};
};
action: {
// One of the special message action ids.
type?: "string";
// Data to pass to the action. Depends on the action.
data?: any;
// Dismiss screen after performing action? If there's no type, the
// action will *only_ dismiss the callout.
dismiss?: boolean;
};
}>;
};
}
];
// The dots in the corner that show what screen you're on and how many
// screens there are in total. This property is only used to override
// the ARIA attributes or tooltip. Not recommended.
steps_indicator?: {
string_id: string;
};
// An extra block of configurable content below the title/subtitle but
// above the optional `tiles` section and the main buttons. Styles not
// yet implemented; not recommended.
above_button_content?: LinkParagraphOrImage[];
// An optional array of event listeners to add to the page where the
// feature callout is shown. This can be used to perform actions in
// response to interactions and other events outside of the feature
// callout itself. The prototypical use case is dismissing the feature
// callout when the user clicks the button the callout is anchored to.
// It also supports performing actions on a timeout/interval.
page_event_listeners?: Array<{
params: {
// Event type string, e.g. "click". This supports:
// 1. Any DOM event type
// 2. "timeout" and "interval" for timers
// 3. Internal feature callout events: "touradvance" and
// "tourend". This can be used to perform actions when the user
// advances to the next screen or finishes the callout tour.
type: string;
// Target selector, e.g. `tag.class, #id[attr]` - Not needed for
// all types.
selectors?: string;
// addEventListener options
options: {
// Handle events in capturing phase?
capture?: boolean;
// Remove listener after first event?
once?: boolean;
// Prevent default action in event handler?
preventDefault?: boolean;
// Used only for `timeout` and `interval` event types. These
// don't set up real event listeners, but instead invoke the
// action on a timer.
interval?: number;
// Extend addEventListener to all windows? Not compatible with
// `interval`.
every_window: boolean;
};
};
action: {
// One of the special message action ids.
type?: "string";
// Data to pass to the action. Depends on the action.
data?: any;
// Dismiss screen after performing action? If there's no type, the
// action will *only_ dismiss the callout.
dismiss?: boolean;
};
}>;
};
}>;
// Specify the index of the screen to start on. Generally unused.
startScreen?: number;
};
@ -472,19 +479,42 @@ interface Action {
type?: "string";
// Data to pass to the action. Depends on the action.
data?: any;
// Set to true if you want the action to advance to the next screen or hide
// the callout if it's the last screen. Can be used in lieu of "type" and
// "data" to create a button that just advances the screen.
navigate?: boolean;
// Same as "navigate" but dismisses the callout instead of advancing to the
// next screen.
dismiss?: boolean;
// Set to true if you want the action to dismiss the callout/tour. Can be used
// in addition to, or instead of, a special message action type. Set to
// "actionResult" if you want the callout to only be dismissed after the
// special message action has resolved successfully. "actionResult" will only
// take effect for certain special message action ids, and it requires setting
// `needsAwait` to true. It is rarely used in the feature callout surface.
dismiss?: boolean | "actionResult";
// Indicates that the action should navigate to a different screen.
advance_screens?: {
// As with dismiss, this can be set to true to take effect immediately, or
// set to "actionResult" to only advance screens after the special message
// action has resolved successfully. Defaults to true.
behavior?: boolean | "actionResult";
// How many screens, and in which direction, to advance. Positive integers
// advance forward, negative integers advance backward. Must be an integer.
// If advancing by the specified number of screens would take you beyond the
// last screen, it will end the tour, just like if you used `dismiss: true`.
// If it's a negative integer that advances beyond the first screen, it will
// stop at the first screen.
direction?: number;
// The id of the screen to advance to. If both id and direction are provided
// (which they shouldn't be), the id takes priority. Either id or direction
// is required. Passing the special token `%end%` ends the tour.
id?: string;
};
// Set to true if this action is for the primary button and you're using the
// "multiselect" tile. This is what allows the primary button to perform the
// actions specified by the user's checkbox/radio selections. It will combine
// all the actions for all the selected checkboxes/radios into this action's
// data.actions array, and perform them in series.
collectSelect?: boolean;
// Setting this to true will require the special message action (given by the
// type property above) to successfully resolve before dismissing the callout
// or advancing screens. This requires dismiss or advance_screens.behavior to
// be "actionResult", or it will have no effect.
needsAwait?: boolean;
}
// Either an image or a paragraph that supports inline links. Currently requires

View file

@ -2,8 +2,6 @@
* 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/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
@ -88,12 +86,6 @@ export class FeatureCallout {
this._handlePrefChange = this._handlePrefChange.bind(this);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"cfrFeaturesUserPref",
"browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features",
true
);
this.setupFeatureTourProgress();
// When the window is focused, ensure tour is synced with tours in any other
@ -192,13 +184,138 @@ export class FeatureCallout {
this._featureTourProgress = null;
}
if (topic === "nsPref:changed") {
this._maybeAdvanceScreens();
this._advanceOnTourPrefChange();
}
break;
}
}
_maybeAdvanceScreens() {
/**
* @typedef {Object} AdvanceScreensOptions
* @property {Boolean|"actionResult"} [behavior] Set to true to take effect
* immediately, or set to "actionResult" to only advance screens after the
* special message action has resolved successfully. "actionResult" requires
* `action.needsAwait` to be true. Defaults to true.
* @property {String} [id] The id of the screen to advance to. If both id and
* direction are provided (which they shouldn't be), the id takes priority.
* Either `id` or `direction` is required. Passing `%end%` ends the tour.
* @property {Number} [direction] How many screens, and in which direction, to
* advance. Positive integers advance forward, negative integers advance
* backward. Must be an integer. If advancing by the specified number of
* screens would take you beyond the last screen, it will end the tour, just
* like if you used `dismiss: true`. If it's a negative integer that
* advances beyond the first screen, it will stop at the first screen.
*/
/** @param {AdvanceScreensOptions} options */
_advanceScreens({ id, direction } = {}) {
if (!this.currentScreen) {
lazy.log.error(
`In ${this.location}: Cannot advance screens without a current screen.`
);
return;
}
if ((!direction || !Number.isInteger(direction)) && !id) {
lazy.log.debug(
`In ${this.location}: Cannot advance screens without a valid direction or id.`
);
return;
}
if (id === "%end%") {
// Special case for ending the tour. When `id` is `%end%`, we should end
// the tour and clear the current screen.
this.endTour();
return;
}
let nextIndex = -1; // Default to -1 to indicate an invalid index.
let currentIndex = this.config.screens.findIndex(
screen => screen.id === this.currentScreen.id
);
if (id) {
nextIndex = this.config.screens.findIndex(screen => screen.id === id);
if (nextIndex === -1) {
lazy.log.debug(
`In ${this.location}: Unable to find screen with id: ${id}`
);
return;
}
if (nextIndex === currentIndex) {
lazy.log.debug(
`In ${this.location}: Already on screen with id: ${id}. Not advancing.`
);
return;
}
} else {
// Calculate the next index based on the current screen and direction.
nextIndex = Math.max(0, currentIndex + direction);
}
if (nextIndex < 0) {
// Don't allow going before the first screen.
lazy.log.debug(
`In ${this.location}: Cannot advance before the first screen.`
);
return;
}
if (nextIndex >= this.config.screens.length) {
// Allow ending the tour if we would go beyond the last screen.
this.endTour();
return;
}
this.ready = false;
this._container?.classList.toggle(
"hidden",
this._container?.localName !== "panel"
);
this._pageEventManager?.emit({
type: "touradvance",
target: this._container,
});
const onFadeOut = async () => {
this._container?.remove();
this.renderObserver?.disconnect();
this._removePositionListeners();
this._removePanelConflictListeners();
this.doc.querySelector(`[src="${BUNDLE_SRC}"]`)?.remove();
if (this.message) {
const isMessageUnblocked = await lazy.ASRouter.isUnblockedMessage(
this.message
);
if (!isMessageUnblocked) {
this.endTour();
return;
}
}
let updated = await this._updateConfig(this.message, nextIndex);
if (!updated && !this.currentScreen) {
this.endTour();
return;
}
let rendering = await this._renderCallout();
if (!rendering) {
this.endTour();
}
};
if (this._container?.localName === "panel") {
this._container.removeEventListener("popuphiding", this);
const controller = new AbortController();
this._container.addEventListener(
"popuphidden",
event => {
if (event.target === this._container) {
controller.abort();
onFadeOut();
}
},
{ signal: controller.signal }
);
this._container.hidePopup(true);
} else {
this.win.setTimeout(onFadeOut, TRANSITION_MS);
}
}
_advanceOnTourPrefChange() {
if (this.doc.visibilityState === "hidden" || !this.featureTourProgress) {
return;
}
@ -219,7 +336,7 @@ export class FeatureCallout {
let prefVal = this.featureTourProgress;
// End the tour according to the tour progress pref or if the user disabled
// contextual feature recommendations.
if (prefVal.complete || !this.cfrFeaturesUserPref) {
if (prefVal.complete) {
this.endTour();
} else if (prefVal.screen !== this.currentScreen?.id) {
// Pref changes only matter to us insofar as they let us advance an
@ -282,9 +399,17 @@ export class FeatureCallout {
};
if (this._container?.localName === "panel") {
this._container.removeEventListener("popuphiding", this);
this._container.addEventListener("popuphidden", onFadeOut, {
once: true,
});
const controller = new AbortController();
this._container.addEventListener(
"popuphidden",
event => {
if (event.target === this._container) {
controller.abort();
onFadeOut();
}
},
{ signal: controller.signal }
);
this._container.hidePopup(true);
} else {
this.win.setTimeout(onFadeOut, TRANSITION_MS);
@ -356,7 +481,7 @@ export class FeatureCallout {
}
case "visibilitychange":
this._maybeAdvanceScreens();
this._advanceOnTourPrefChange();
break;
case "resize":
@ -826,7 +951,7 @@ export class FeatureCallout {
this._container.id = CONTAINER_ID;
this._container.setAttribute(
"aria-describedby",
`#${CONTAINER_ID} .welcome-text`
"multi-stage-message-welcome-text"
);
if (arrow_width) {
this._container.style.setProperty("--arrow-width", `${arrow_width}px`);
@ -1358,6 +1483,7 @@ export class FeatureCallout {
),
AWSendToParent: (name, data) => getActionHandler(name)(data),
AWFinish: () => this.endTour(),
AWAdvanceScreens: options => this._advanceScreens(options),
AWEvaluateScreenTargeting: getActionHandler("EVALUATE_SCREEN_TARGETING"),
AWEvaluateAttributeTargeting: getActionHandler(
"EVALUATE_ATTRIBUTE_TARGETING"
@ -1435,9 +1561,17 @@ export class FeatureCallout {
this._emitEvent("end");
};
if (this._container?.localName === "panel") {
this._container.addEventListener("popuphidden", onFadeOut, {
once: true,
});
const controller = new AbortController();
this._container.addEventListener(
"popuphidden",
event => {
if (event.target === this._container) {
controller.abort();
onFadeOut();
}
},
{ signal: controller.signal }
);
this._container.hidePopup(!skipFadeOut);
} else if (this._container) {
this.win.setTimeout(onFadeOut, skipFadeOut ? 0 : TRANSITION_MS);
@ -1502,21 +1636,22 @@ export class FeatureCallout {
* in this.config, which is returned by AWGetFeatureConfig. The aboutwelcome
* bundle will use that function to get the content when it executes.
* @param {Object} [message] ASRouter message. Omit to request a new one.
* @param {Number} [screenIndex] Index of the screen to render.
* @returns {Promise<boolean>} true if a message is loaded, false if not.
*/
async _updateConfig(message) {
async _updateConfig(message, screenIndex) {
if (this.loadingConfig) {
return false;
}
this.message = message || (await this._loadConfig());
this.message = structuredClone(message || (await this._loadConfig()));
switch (this.message.template) {
case "feature_callout":
break;
case "spotlight":
// Special handling for spotlight messages, which can be configured as a
// kind of introduction to a feature tour.
// Deprecated: Special handling for spotlight messages, used as an
// introduction to feature tours.
this.currentScreen = "spotlight";
// fall through
default:
@ -1525,12 +1660,29 @@ export class FeatureCallout {
this.config = this.message.content;
// Set the default start screen.
let newScreen = this.config?.screens?.[this.config?.startScreen || 0];
if (!this.config.screens) {
lazy.log.error(
`In ${
this.location
}: Expected a message object with content.screens property, got: ${JSON.stringify(
this.message
)}`
);
return false;
}
// Set or override the default start screen.
let overrideScreen = Number.isInteger(screenIndex);
if (overrideScreen) {
this.config.startScreen = screenIndex;
}
let newScreen = this.config?.screens?.[this.config?.startScreen ?? 0];
// If we have a feature tour in progress, try to set the start screen to
// whichever screen is configured in the feature tour pref.
if (
this.config.screens &&
!overrideScreen &&
this.config?.tour_pref_name &&
this.config.tour_pref_name === this.pref?.name &&
this.featureTourProgress
@ -1606,20 +1758,25 @@ export class FeatureCallout {
* @property {PageEventListenerOptions} [options] addEventListener options
*
* @typedef {Object} PageEventListenerOptions
* @property {Boolean} [capture] Use event capturing phase?
* @property {Boolean} [once] Remove listener after first event?
* @property {Boolean} [preventDefault] Prevent default action?
* @property {Boolean} [capture] Use event capturing phase
* @property {Boolean} [once] Remove listener after first event
* @property {Boolean} [preventDefault] Prevent default action
* @property {Number} [interval] Used only for `timeout` and `interval` event
* types. These don't set up real event listeners, but instead invoke the
* action on a timer.
* @property {Boolean} [every_window] Extend addEventListener to all windows?
* @property {Boolean} [every_window] Extend addEventListener to all windows.
* Not compatible with `interval`.
*
* @typedef {Object} PageEventListenerAction Action sent to AboutWelcomeParent
* @property {String} [type] Action type, e.g. `OPEN_URL`
* @property {Object} [data] Extra data, properties depend on action type
* @property {Boolean} [dismiss] Dismiss screen after performing action?
* @property {Boolean} [reposition] Reposition screen after performing action?
* @property {AdvanceScreensOptions} [advance_screens] Jump to a new screen
* @property {Boolean|"actionResult"} [dismiss] Dismiss callout
* @property {Boolean|"actionResult"} [reposition] Reposition callout
* @property {Boolean} [needsAwait] Wait for any special message actions
* (given by the type property above) to resolve before advancing screens,
* dismissing, or repositioning the callout, if those actions are set to
* "actionResult".
*/
_attachPageEventListeners(listeners) {
listeners?.forEach(({ params, action }) =>
@ -1640,13 +1797,14 @@ export class FeatureCallout {
* @param {PageEventListenerAction} action
* @param {Event} event Triggering event
*/
_handlePageEventAction(action, event) {
async _handlePageEventAction(action, event) {
const page = this.location;
const message_id = this.config?.id.toUpperCase();
const source =
typeof event.target === "string"
? event.target
: this._getUniqueElementIdentifier(event.target);
let actionResult;
if (action.type) {
this.win.AWSendEventTelemetry?.({
event: "PAGE_EVENT",
@ -1658,9 +1816,36 @@ export class FeatureCallout {
},
message_id,
});
this.win.AWSendToParent("SPECIAL_ACTION", action);
let actionPromise = this.win.AWSendToParent("SPECIAL_ACTION", action);
if (action.needsAwait) {
actionResult = await actionPromise;
}
}
if (action.dismiss) {
// `navigate` and `dismiss` can be true/false/undefined, or they can be a
// string "actionResult" in which case we should use the actionResult
// (boolean resolved by handleUserAction)
const shouldDoBehavior = behavior => {
if (behavior !== "actionResult") {
return behavior;
}
if (action.needsAwait) {
return actionResult;
}
lazy.log.warn(
`In ${
this.location
}: "actionResult" is only supported for actions with needsAwait, got: ${JSON.stringify(action)}`
);
return false;
};
if (action.advance_screens) {
if (shouldDoBehavior(action.advance_screens.behavior ?? true)) {
this._advanceScreens?.(action.advance_screens);
}
}
if (shouldDoBehavior(action.dismiss)) {
this.win.AWSendEventTelemetry?.({
event: "DISMISS",
event_context: { source: `PAGE_EVENT:${source}`, page },
@ -1668,7 +1853,7 @@ export class FeatureCallout {
});
this._dismiss();
}
if (action.reposition) {
if (shouldDoBehavior(action.reposition)) {
this.win.requestAnimationFrame(() => this._positionCallout());
}
}
@ -1823,11 +2008,6 @@ export class FeatureCallout {
this._container?.remove();
this.renderObserver?.disconnect();
if (!this.cfrFeaturesUserPref) {
this.endTour();
return false;
}
let rendering = (await this._renderCallout()) && !!this.currentScreen;
if (!rendering) {
this.endTour();

View file

@ -1156,20 +1156,20 @@ const MESSAGES = () => [
subtitle: { raw: "Hello!" },
secondary_button: {
label: { raw: "Advance" },
action: { navigate: true },
action: { advance_screens: { direction: 1 } },
},
submenu_button: {
submenu: [
{
type: "action",
label: { raw: "Item 1" },
action: { navigate: true },
action: { advance_screens: { direction: 1 } },
id: "item1",
},
{
type: "action",
label: { raw: "Item 2" },
action: { navigate: true },
action: { advance_screens: { direction: 1 } },
id: "item2",
},
{
@ -1179,13 +1179,17 @@ const MESSAGES = () => [
{
type: "action",
label: { raw: "Item 3" },
action: { navigate: true },
action: {
advance_screens: { direction: 1 },
},
id: "item3",
},
{
type: "action",
label: { raw: "Item 4" },
action: { navigate: true },
action: {
advance_screens: { direction: 1 },
},
id: "item4",
},
],
@ -1194,6 +1198,194 @@ const MESSAGES = () => [
],
attached_to: "secondary_button",
},
dismiss_button: { action: { dismiss: true } },
},
},
{
id: "FEATURE_CALLOUT_2",
anchors: [
{
selector: "#PanelUI-menu-button",
panel_position: {
anchor_attachment: "bottomcenter",
callout_attachment: "topright",
},
},
],
content: {
position: "callout",
title: { raw: "Panel Feature Callout 2" },
subtitle: { raw: "Hellossss!" },
secondary_button: {
label: { raw: "Advance" },
action: { advance_screens: { direction: 1 } },
},
primary_button: {
label: { raw: "Go back" },
style: "secondary",
action: { advance_screens: { direction: -1 } },
},
submenu_button: {
submenu: [
{
type: "action",
label: { raw: "Item 1" },
action: { advance_screens: { direction: 1 } },
id: "item1",
},
{
type: "action",
label: { raw: "Item 2" },
action: { advance_screens: { direction: 1 } },
id: "item2",
},
{
type: "menu",
label: { raw: "Menu 1" },
submenu: [
{
type: "action",
label: { raw: "Item 3" },
action: {
advance_screens: { direction: 1 },
},
id: "item3",
},
{
type: "action",
label: { raw: "Item 4" },
action: {
advance_screens: { direction: 1 },
},
id: "item4",
},
],
id: "menu1",
},
],
attached_to: "secondary_button",
},
dismiss_button: { action: { dismiss: true } },
},
},
{
id: "FEATURE_CALLOUT_3",
anchors: [
{
selector: "#stop-reload-button",
panel_position: {
anchor_attachment: "bottomcenter",
callout_attachment: "topleft",
},
},
],
content: {
position: "callout",
title: { raw: "Panel Feature Callout" },
subtitle: { raw: "Screen 2!" },
secondary_button: {
label: { raw: "Finish" },
style: "primary",
action: {
type: "MULTI_ACTION",
collectSelect: true,
dismiss: true,
data: { actions: [] },
},
disabled: "hasActiveMultiSelect",
},
primary_button: {
label: { raw: "Go back" },
style: "secondary",
action: { advance_screens: { direction: -1 } },
},
tiles: {
type: "multiselect",
style: {
flexDirection: "column",
alignItems: "flex-start",
},
data: [
{
id: "radio-choice-1",
type: "radio",
group: "radios",
icon: {
style: {
width: "14px",
height: "14px",
marginInline: "0 0.5em",
},
},
label: { raw: "Choice 1" },
},
{
id: "radio-choice-2",
type: "radio",
group: "radios",
icon: {
style: {
width: "14px",
height: "14px",
marginInline: "0 0.5em",
},
},
label: { raw: "Choice 2" },
},
{
id: "radio-choice-3",
type: "radio",
group: "radios",
icon: {
style: {
width: "14px",
height: "14px",
marginInline: "0 0.5em",
},
},
label: { raw: "Choice 3" },
},
{
id: "radio-choice-4",
type: "radio",
group: "radios",
icon: {
style: {
width: "14px",
height: "14px",
marginInline: "0 0.5em",
},
},
label: { raw: "Choice 4" },
},
{
id: "radio-choice-5",
type: "radio",
group: "radios",
icon: {
style: {
width: "14px",
height: "14px",
marginInline: "0 0.5em",
},
},
label: { raw: "Choice 5" },
},
{
id: "radio-choice-6",
type: "radio",
group: "radios",
icon: {
style: {
width: "14px",
height: "14px",
marginInline: "0 0.5em",
},
},
label: { raw: "Choice 6" },
},
],
},
dismiss_button: {
action: { dismiss: true },
},

View file

@ -136,32 +136,6 @@ add_task(async function feature_callout_closes_on_dismiss() {
await BrowserTestUtils.closeWindow(win);
});
add_task(async function feature_callout_respects_cfr_features_pref() {
await SpecialPowers.pushPrefEnv({
set: [
[
"browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features",
false,
],
],
});
const testMessage = getTestMessage();
const win = await BrowserTestUtils.openNewBrowserWindow();
const doc = win.document;
const browser = win.gBrowser.selectedBrowser;
await showFeatureCallout(browser, testMessage);
ok(
!doc.querySelector(calloutSelector),
"Feature Callout element was not created because CFR pref was disabled"
);
await SpecialPowers.popPrefEnv();
await BrowserTestUtils.closeWindow(win);
});
add_task(async function feature_callout_dismiss_on_timeout() {
const testMessage = getTestMessage();
const sandbox = sinon.createSandbox();

View file

@ -462,6 +462,150 @@ add_task(async function triggered_feature_tour_with_custom_pref() {
);
});
// Test that a feature callout message can be loaded into ASRouter and displayed
// via a standard trigger. Also test that the callout can be a feature tour
// without requiring a tour pref to be used.
add_task(async function triggered_feature_tour_with_advance_screens() {
let sandbox = sinon.createSandbox();
const TEST_MESSAGES = [
{
id: "TEST_FEATURE_TOUR",
template: "feature_callout",
content: {
id: "TEST_FEATURE_TOUR",
template: "multistage",
backdrop: "transparent",
transitions: false,
disableHistoryUpdates: true,
screens: [
{
id: "FEATURE_CALLOUT_1",
anchors: [
{
selector: "#PanelUI-menu-button",
arrow_position: "top-center-arrow-end",
},
],
content: {
position: "callout",
title: { string_id: "callout-pdfjs-edit-title" },
subtitle: { string_id: "callout-pdfjs-edit-body-b" },
primary_button: {
label: { string_id: "callout-pdfjs-edit-button" },
action: { advance_screens: { direction: 1 } },
},
},
},
{
id: "FEATURE_CALLOUT_2",
anchors: [
{
selector: "#back-button",
arrow_position: "top-center-arrow-start",
},
],
content: {
position: "callout",
title: { string_id: "callout-pdfjs-draw-title" },
subtitle: { string_id: "callout-pdfjs-draw-body-b" },
primary_button: {
label: { raw: "Go forward" },
action: {
advance_screens: { direction: 1 },
},
},
secondary_button: {
label: { raw: "Go back" },
action: {
advance_screens: { direction: -1 },
},
},
},
},
],
},
priority: 2,
targeting: "true",
trigger: { id: "nthTabClosed" },
},
];
const getMessagesStub = sandbox.stub(FeatureCalloutMessages, "getMessages");
getMessagesStub.returns(TEST_MESSAGES);
await ASRouter._updateMessageProviders();
await ASRouter.loadMessagesFromAllProviders(
ASRouter.state.providers.filter(p => p.id === "onboarding")
);
// Test that callout is triggered and shown in browser chrome
const win1 = await BrowserTestUtils.openNewBrowserWindow();
win1.focus();
const tab1 = await BrowserTestUtils.openNewForegroundTab(win1.gBrowser);
await TestUtils.waitForTick();
win1.gBrowser.removeTab(tab1);
await waitForCalloutScreen(
win1.document,
TEST_MESSAGES[0].content.screens[0].id
);
ok(
win1.document.querySelector(calloutSelector),
"Feature Callout is rendered in the browser chrome when a message is available"
);
// Test that the callout advances screen
win1.document.querySelector(`#${calloutId} .primary`).click();
await waitForCalloutScreen(
win1.document,
TEST_MESSAGES[0].content.screens[1].id
);
ok(
win1.document.querySelector(calloutSelector),
"Feature Callout screen 2 is rendered"
);
// Test that the callout goes backward
win1.document.querySelector(`#${calloutId} .secondary`).click();
await waitForCalloutScreen(
win1.document,
TEST_MESSAGES[0].content.screens[0].id
);
ok(
win1.document.querySelector(calloutSelector),
"Feature Callout screen 1 is rendered again"
);
// Go forward again
win1.document.querySelector(`#${calloutId} .primary`).click();
await waitForCalloutScreen(
win1.document,
TEST_MESSAGES[0].content.screens[1].id
);
ok(
win1.document.querySelector(calloutSelector),
"Feature Callout screen 2 is rendered again"
);
// Test that the tour ends when advancing past the last screen
win1.document.querySelector(`#${calloutId} .primary`).click();
await waitForCalloutRemoved(win1.document);
ok(
!win1.document.querySelector(calloutSelector),
"Feature Callout is not rendered after the tour ends"
);
await BrowserTestUtils.waitForCondition(
() => !FeatureCalloutBroker.isCalloutShowing,
"Waiting for all callouts to empty from the callout broker"
);
BrowserTestUtils.closeWindow(win1);
sandbox.restore();
await ASRouter.resetMessageState();
await ASRouter._updateMessageProviders();
await ASRouter.loadMessagesFromAllProviders(
ASRouter.state.providers.filter(p => p.id === "onboarding")
);
});
add_task(async function callout_not_shown_if_dialog_open() {
const win = await BrowserTestUtils.openNewBrowserWindow();
let dialogPromise = BrowserTestUtils.promiseAlertDialog(null, undefined, {

View file

@ -360,7 +360,7 @@ var CustomizableUIInternal = {
true
);
if (AppConstants.MENUBAR_CAN_AUTOHIDE) {
if (!Services.appinfo.nativeMenubar) {
this.registerArea(
CustomizableUI.AREA_MENUBAR,
{

View file

@ -118,7 +118,7 @@ add_task(function test_doorhanger_keep() {
});
add_task(function test_doorhanger_alltabs_button_in_menubar() {
if (!AppConstants.MENUBAR_CAN_AUTOHIDE) {
if (Services.appinfo.nativeMenubar) {
info("skipping test because the menubar is not customizable");
return;
}

View file

@ -906,6 +906,102 @@ newtab:
expires: never
telemetry_mirror: FX_ABOUTHOME_CACHE_CONSTRUCTION
report_content_open:
type: event
description: >
Recorded when content reporting is opened from context menu
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1954656
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1954656
data_sensitivity:
- interaction
notification_emails:
- rhamoui@mozilla.com
expires: never
extra_keys:
newtab_visit_id: *newtab_visit_id
send_in_pings:
- newtab
report_content_submit:
type: event
description: >
Recorded when content reporting has been submitted
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1954656
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1954656
data_sensitivity:
- interaction
notification_emails:
- rhamoui@mozilla.com
expires: never
extra_keys:
card_type:
description: >
The type of the content card (e.g., "spoc", "organic")
type: string
corpus_item_id:
description: >
A content identifier.
For organic Newtab recommendations it is an opaque id produced by
Newtab's recommendation systems that corresponds uniquely to the URL.
This is the replacement for tile_id and scheduled_corpus_item_id.
type: string
is_section_followed:
description: >
If click belongs in a section, if that section is followed
type: boolean
newtab_visit_id:
description: >
The id of this newtab visit.
Allows you to separate multiple simultaneous newtabs and
build an event timeline of actions taken from this newtab.
type: string
received_rank:
description: >
The rank or order of the recommendation at the time it was sent to the client.
type: quantity
recommended_at:
description: >
The time in milliseconds the recommendation was recommended at.
type: quantity
report_reason:
description: >
The reason selected by the user when reporting the content
type: string
scheduled_corpus_item_id:
description: >
A content identifier.
For organic Newtab recommendations it is an opaque id produced by
Newtab's recommendation systems that corresponds uniquely to
a piece of content scheduled for a specific day on a specific surface.
This is the replacement for tile_id.
type: string
section:
description: >
If click belongs in a section, the name of the section
type: string
section_position:
description: >
If click belongs in a section, the numeric position of the section
type: string
title:
description: >
Title of the recommendation.
type: string
topic:
description: >
The topic of the recommendation. Like "entertainment".
type: string
url:
description: >
URL of the recommendation.
type: string
send_in_pings:
- newtab
newtab.search:
enabled:
lifetime: application

View file

@ -445,33 +445,14 @@ function scrollAndHighlight(subcategory) {
if (!element) {
return;
}
let header = getClosestDisplayedHeader(element);
header.scrollIntoView({
element.scrollIntoView({
behavior: "smooth",
block: "center",
});
element.classList.add("spotlight");
}
/**
* If there is no visible second level header it will return first level header,
* otherwise return second level header.
* @returns {Element} - The closest displayed header.
*/
function getClosestDisplayedHeader(element) {
let header = element.closest("groupbox");
let searchHeader = header.querySelector(".search-header");
if (
searchHeader &&
searchHeader.hidden &&
header.previousElementSibling.classList.contains("subcategory")
) {
header = header.previousElementSibling;
}
return header;
}
function friendlyPrefCategoryNameToInternalName(aName) {
if (aName.startsWith("pane")) {
return aName;

View file

@ -29,13 +29,10 @@ export class SettingGroup extends MozLitElement {
}
xulCheckboxTemplate(item, setting) {
let result = document.createDocumentFragment();
let result;
let checkbox = document.createXULElement("checkbox");
checkbox.id = item.id;
document.l10n.setAttributes(checkbox, item.l10nId);
if (item.subcategory) {
checkbox.setAttribute("subcategory", item.subcategory);
}
checkbox.addEventListener("command", e =>
setting.userChange(e.target.checked)
);
@ -47,9 +44,12 @@ export class SettingGroup extends MozLitElement {
supportLink.supportPage = item.supportPage;
checkbox.classList.add("tail-with-learn-more");
container.append(checkbox, supportLink);
result.append(container);
result = container;
} else {
result.append(checkbox);
result = checkbox;
}
if (item.subcategory) {
result.dataset.subcategory = item.subcategory;
}
return result;
}

View file

@ -68,16 +68,17 @@ moz-card {
}
.theme-name {
display: flex;
align-items: center;
padding-inline-start: var(--space-medium);
padding-block: var(--space-small);
display: block;
padding: var(--space-small) var(--space-medium);
border-inline-width: var(--theme-card-border-width);
border-block-end-width: var(--theme-card-border-width);
border-block-start: var(--card-border);
font-size: var(--font-size-small);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@media (forced-colors) {
color: ButtonText;

View file

@ -41,6 +41,14 @@ fail-if = ["a11y_checks"] # Bug 1955503
["browser_test_db_lazily_created.js"]
["browser_test_last_tab.js"]
skip-if = [
"os == 'win' && os_version == '11.26100' && processor == 'x86_64' && asan", # Bug 1950795
"os == 'win' && os_version == '11.26100' && debug", # Bug 1950795
"os == 'mac' && os_version == '11.20' && arch == 'aarch64' && opt", # Bug 1950795
"os == 'mac' && os_version == '10.15' && processor == 'x86_64'", # Bug 1950795
"os == 'linux' && os_version == '18.04' && processor == 'x86_64' && socketprocess_networking", # Bug 1950795
"os == 'linux' && os_version == '18.04' && processor == 'x86_64' && swgl", # Bug 1950795
]
["browser_test_nimbus_feature.js"]

View file

@ -265,9 +265,9 @@ export var SessionStartup = {
// Report shutdown success via telemetry. Shortcoming here are
// being-killed-by-OS-shutdown-logic, shutdown freezing after
// session restore was written, etc.
Services.telemetry
.getHistogramById("SHUTDOWN_OK")
.add(!this._previousSessionCrashed);
Glean.sessionRestore.shutdownOk[
this._previousSessionCrashed ? "false" : "true"
].add();
Glean.sessionRestore.shutdownSuccessSessionStartup.record({
shutdown_ok: this._previousSessionCrashed.toString(),
shutdown_reason: previousSessionCrashedReason,

View file

@ -2598,71 +2598,78 @@ var SessionStoreInternal = {
if (!syncShutdown) {
// We've got some time to shut down, so let's do this properly that there
// will be a complete session available upon next startup.
// To prevent a blocker from taking longer than the DELAY_CRASH_MS limit
// (which will cause a crash) of AsyncShutdown whilst flushing all windows,
// we resolve the Promise blocker once:
// We use our own timer and spin the event loop ourselves, as we do not
// want to crash on timeout and as we need to run in response to
// "quit-application-granted", which is not yet a real shutdown phase.
//
// We end spinning once:
// 1. the flush duration exceeds 10 seconds before DELAY_CRASH_MS, or
// 2. 'oop-frameloader-crashed', or
// 3. 'ipc:content-shutdown' is observed.
lazy.AsyncShutdown.quitApplicationGranted.addBlocker(
"SessionStore: flushing all windows",
() => {
// Set up the list of promises that will signal a complete sessionstore
// shutdown: either all data is saved, or we crashed or the message IPC
// channel went away in the meantime.
let promises = [this.flushAllWindowsAsync(progress)];
// 2. 'oop-frameloader-crashed' (issued by BrowserParent::ActorDestroy
// on abnormal frame shutdown) is observed, or
// 3. 'ipc:content-shutdown' (issued by ContentParent::ActorDestroy on
// abnormal shutdown) is observed, or
// 4. flushAllWindowsAsync completes (hopefully the normal case).
const observeTopic = topic => {
let deferred = Promise.withResolvers();
const observer = subject => {
// Skip abort on ipc:content-shutdown if not abnormal/crashed
subject.QueryInterface(Ci.nsIPropertyBag2);
if (
!(topic == "ipc:content-shutdown" && !subject.get("abnormal"))
) {
deferred.resolve();
}
};
const cleanup = () => {
try {
Services.obs.removeObserver(observer, topic);
} catch (ex) {
console.error(
"SessionStore: exception whilst flushing all windows: ",
ex
);
}
};
Services.obs.addObserver(observer, topic);
deferred.promise.then(cleanup, cleanup);
return deferred;
};
// Set up the list of promises that will signal a complete sessionstore
// shutdown: either all data is saved, or we crashed or the message IPC
// channel went away in the meantime.
let promises = [this.flushAllWindowsAsync(progress)];
// Build a list of deferred executions that require cleanup once the
// Promise race is won.
// Ensure that the timer fires earlier than the AsyncShutdown crash timer.
let waitTimeMaxMs = Math.max(
0,
lazy.AsyncShutdown.DELAY_CRASH_MS - 10000
);
let defers = [
this.looseTimer(waitTimeMaxMs),
const observeTopic = topic => {
let deferred = Promise.withResolvers();
const observer = subject => {
// Skip abort on ipc:content-shutdown if not abnormal/crashed
subject.QueryInterface(Ci.nsIPropertyBag2);
if (!(topic == "ipc:content-shutdown" && !subject.get("abnormal"))) {
deferred.resolve();
}
};
const cleanup = () => {
try {
Services.obs.removeObserver(observer, topic);
} catch (ex) {
console.error(
"SessionStore: exception whilst flushing all windows: ",
ex
);
}
};
Services.obs.addObserver(observer, topic);
deferred.promise.then(cleanup, cleanup);
return deferred;
};
// FIXME: We should not be aborting *all* flushes when a single
// content process crashes here.
observeTopic("oop-frameloader-crashed"),
observeTopic("ipc:content-shutdown"),
];
// Add these monitors to the list of Promises to start the race.
promises.push(...defers.map(deferred => deferred.promise));
// Build a list of deferred executions that require cleanup once the
// Promise race is won.
// Ensure that the timer fires earlier than the AsyncShutdown crash timer.
let waitTimeMaxMs = Math.max(
0,
lazy.AsyncShutdown.DELAY_CRASH_MS - 10000
);
let defers = [
this.looseTimer(waitTimeMaxMs),
return Promise.race(promises).then(() => {
// When a Promise won the race, make sure we clean up the running
// monitors.
defers.forEach(deferred => deferred.reject());
});
},
() => progress
// FIXME: We should not be aborting *all* flushes when a single
// content process crashes here.
observeTopic("oop-frameloader-crashed"),
observeTopic("ipc:content-shutdown"),
];
// Add these monitors to the list of Promises to start the race.
promises.push(...defers.map(deferred => deferred.promise));
let isDone = false;
Promise.race(promises)
.then(() => {
// When a Promise won the race, make sure we clean up the running
// monitors.
defers.forEach(deferred => deferred.reject());
})
.finally(() => {
isDone = true;
});
Services.tm.spinEventLoopUntil(
"Wait until SessionStoreInternal.flushAllWindowsAsync finishes.",
() => isDone
);
} else {
// We have to shut down NOW, which means we only get to save whatever

View file

@ -367,6 +367,25 @@ session_restore:
expires: never
telemetry_mirror: FX_SESSION_RESTORE_NUMBER_OF_EAGER_TABS_RESTORED
shutdown_ok:
type: labeled_counter
description: >
Did the browser start after a successful shutdown
This metric was generated to correspond to the Legacy Telemetry boolean
histogram SHUTDOWN_OK.
labels:
- "false"
- "true"
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1421688
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1421688
notification_emails:
- chutten@mozilla.com
expires: never
telemetry_mirror: h#SHUTDOWN_OK
browser.engagement:
sessionrestore_interstitial:
type: labeled_counter

View file

@ -100,10 +100,10 @@ async function takeScreenshot(
let browser = doc.createXULElement("browser");
browser.setAttribute("remote", "true");
browser.setAttribute("type", "content");
browser.setAttribute(
"style",
`width: ${contentWidth}px; min-width: ${contentWidth}px; height: ${contentHeight}px; min-height: ${contentHeight}px;`
);
browser.style.width = `${contentWidth}px`;
browser.style.minWidth = `${contentWidth}px`;
browser.style.height = `${contentHeight}px`;
browser.style.minHeight = `${contentHeight}px`;
browser.setAttribute("maychangeremoteness", "true");
doc.documentElement.appendChild(browser);

View file

@ -44,6 +44,7 @@ const SIDEBAR_MAXIMUM_WIDTH = "75vw";
const LEGACY_USED_PREF = "sidebar.old-sidebar.has-used";
const REVAMP_USED_PREF = "sidebar.new-sidebar.has-used";
const DEFAULT_LAUNCHER_VISIBLE_PREF = "sidebar.revamp.defaultLauncherVisible";
/**
* A reactive data store for the sidebar's UI state. Similar to Lit's
@ -54,16 +55,15 @@ export class SidebarState {
#controller = null;
/** @type {SidebarStateProps} */
#props = {
panelOpen: false,
launcherVisible: true,
launcherExpanded: false,
launcherDragActive: false,
launcherHoverActive: false,
...SidebarState.defaultProperties,
};
/** @type {SidebarStateProps} */
static defaultProperties = Object.freeze({
command: "",
launcherDragActive: false,
launcherExpanded: false,
launcherHoverActive: false,
launcherVisible: false,
panelOpen: false,
});
@ -80,6 +80,10 @@ export class SidebarState {
this.#controller = controller;
this.revampEnabled = controller.sidebarRevampEnabled;
this.revampVisibility = controller.sidebarRevampVisibility;
if (this.revampEnabled) {
this.#props.launcherVisible = this.defaultLauncherVisible;
}
}
/**
@ -135,7 +139,7 @@ export class SidebarState {
// Don't show launcher if we're in a popup window.
this.launcherVisible = false;
} else {
this.launcherVisible = true;
this.launcherVisible = this.defaultLauncherVisible;
}
// Explicitly trigger effects to ensure that the UI is kept up to date.
@ -153,6 +157,11 @@ export class SidebarState {
* New properties to overwrite the default state with.
*/
loadInitialState(props) {
// Override any initial launcher visible state when the pref is defined
if (Services.prefs.prefHasUserValue(DEFAULT_LAUNCHER_VISIBLE_PREF)) {
props.launcherVisible = this.defaultLauncherVisible;
delete props.hidden;
}
for (const [key, value] of Object.entries(props)) {
if (value === undefined) {
// `undefined` means we should use the default value.
@ -267,6 +276,18 @@ export class SidebarState {
this.#launcherContainerEl.style.maxWidth = `calc(${SIDEBAR_MAXIMUM_WIDTH} - ${width}px)`;
}
get defaultLauncherVisible() {
if (!this.revampEnabled) {
return false;
}
// default/fallback value for vertical tabs is to always be visible initially
if (lazy.verticalTabsEnabled) {
return true;
}
return this.#controller.revampDefaultLauncherVisible;
}
get launcherVisible() {
return this.#props.launcherVisible;
}
@ -275,9 +296,13 @@ export class SidebarState {
* Update the launcher `visible` and `expanded` states
*
* @param {boolean} visible
* Show or hide the launcher. Defaults to the value returned by the defaultLauncherVisible getter
* @param {boolean} forceExpandValue
*/
updateVisibility(visible, forceExpandValue = null) {
updateVisibility(
visible = this.defaultLauncherVisible,
forceExpandValue = null
) {
switch (this.revampVisibility) {
case "hide-sidebar":
if (lazy.verticalTabsEnabled) {

View file

@ -570,6 +570,9 @@ var SidebarController = {
await this._state.loadInitialState(state);
await this.waitUntilStable(); // Finish newly scheduled tasks.
this.updateToolbarButton();
if (this.sidebarRevampVisibility === "expand-on-hover") {
await this.toggleExpandOnHover(true);
}
this.uiStateInitialized = true;
},
@ -792,7 +795,9 @@ var SidebarController = {
Services.prefs.setBoolPref("sidebar.verticalTabs", false);
}
} else {
this._state.launcherVisible = true;
// initial launcher visibleness with sidebar.revamp is is one of the
// default properties managed by SidebarState
this._state.launcherVisible = this._state.defaultLauncherVisible;
}
if (!this._sidebars.get(this.lastOpenedId)) {
this.lastOpenedId = this.DEFAULT_SIDEBAR_ID;
@ -1939,6 +1944,10 @@ var SidebarController = {
async setLauncherCollapsedWidth() {
let browserEl = document.getElementById("browser");
if (this.getUIState().launcherExpanded) {
this._state.launcherExpanded = false;
}
await this.waitUntilStable();
let collapsedWidth = await new Promise(resolve => {
requestAnimationFrame(() => {
resolve(this._getRects([this.sidebarMain])[0][1].width);
@ -1976,9 +1985,6 @@ var SidebarController = {
if (!this._state) {
this._state = new this.SidebarState(this);
}
if (this.getUIState().launcherExpanded && !isDragEnded) {
this._state.launcherExpanded = false;
}
await this.waitUntilStable();
MousePosTracker.addListener(this);
if (!isDragEnded) {
@ -2156,3 +2162,18 @@ XPCOMUtils.defineLazyPreferenceGetter(
}
}
);
XPCOMUtils.defineLazyPreferenceGetter(
SidebarController,
"revampDefaultLauncherVisible",
"sidebar.revamp.defaultLauncherVisible",
false,
(_aPreference, _previousValue, _newValue) => {
if (
!SidebarController.uninitializing &&
!SidebarController.inSingleTabWindow
) {
SidebarController._state.updateVisibility();
}
}
);

View file

@ -1,4 +1,5 @@
[DEFAULT]
tags = "local"
["test_default_launcher_visible.py"]
["test_initialize_vertical_tabs.py"]

View file

@ -0,0 +1,118 @@
# 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/.
from marionette_driver import Wait
from marionette_harness import MarionetteTestCase
default_visible_pref = "sidebar.revamp.defaultLauncherVisible"
class TestDefaultLauncherVisible(MarionetteTestCase):
def setUp(self):
super().setUp()
self.marionette.set_pref("sidebar.revamp", True)
self.marionette.set_context("chrome")
def tearDown(self):
try:
# Make sure subsequent tests get a clean profile
self.marionette.restart(in_app=False, clean=True)
finally:
super().tearDown()
def restart_with_prefs(self, prefs):
# We need to quit the browser and restart with the prefs already set
# in order to examine the startup behavior
for name, value in prefs.items():
if value is None:
self.marionette.clear_pref(name)
else:
self.marionette.set_pref(name, value)
self.marionette.restart(clean=False, in_app=True)
self.marionette.set_context("chrome")
def is_launcher_visible(self):
hidden = self.marionette.execute_script(
"""
const window = BrowserWindowTracker.getTopWindow();
return window.SidebarController.sidebarContainer.hidden;
"""
)
return not hidden
def is_launcher_hidden(self):
hidden = self.marionette.execute_script(
"""
const window = BrowserWindowTracker.getTopWindow();
return window.SidebarController.sidebarContainer.hidden;
"""
)
return hidden
def test_default_visible_pref(self):
# By default when sidebar.revamp is enabled, the launcher should be initially visible
Wait(self.marionette).until(
lambda _: self.is_launcher_visible(),
message="Sidebar launcher should be initially visible",
)
# Flip the default and make the launcher initially hidden
self.restart_with_prefs(
{
default_visible_pref: False,
"sidebar.backupState": None,
}
)
Wait(self.marionette).until(
lambda _: self.is_launcher_hidden(),
message="Launcher should be hidden after restart",
)
# Ensure user actions override the default - click the toolbar button to show the launcher
self.marionette.execute_script(
"""
const window = BrowserWindowTracker.getTopWindow();
return window.document.getElementById("sidebar-button").click()
"""
)
self.assertTrue(
self.is_launcher_visible(),
"Sidebar launcher is visible again",
)
self.marionette.restart(clean=False, in_app=True)
# Check the default pref is overriden and the launcher remains visible
self.assertFalse(
self.is_launcher_visible(),
"Sidebar launcher is still visible after restart",
)
def test_vertical_tabs_default_hidden(self):
# Verify that starting with verticalTabs enabled and default visibility false results in a visible
# launcher with the vertical tabstrip
self.marionette.quit()
self.marionette.start_session()
self.marionette.set_pref("sidebar.revamp", True)
self.marionette.set_pref("sidebar.verticalTabs", True)
self.marionette.set_pref(default_visible_pref, False)
self.marionette.set_context("chrome")
Wait(self.marionette).until(
lambda _: self.is_launcher_visible(),
message="Sidebar launcher should be initially visible",
)
tabsWidth = self.marionette.execute_script(
"""
const window = BrowserWindowTracker.getTopWindow();
return document.getElementById("vertical-tabs").getBoundingClientRect().width;
"""
)
self.assertGreater(tabsWidth, 0, "#vertical-tabs element has width")
# switch to 'hide-sidebar' visibility mode and confirm the launcher becomes hidden
self.marionette.set_pref("sidebar.visibility", "hide-sidebar")
Wait(self.marionette).until(
lambda _: self.is_launcher_hidden(),
message="Sidebar launcher should become hidden when hide-sidebar visibility is set and defaultLauncherVisible is false",
)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,18 @@
/* 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/. */
/**
* A common list of systems, surfaces, controls, etc. from which user
* interactions with tab groups could originate. These "source" values
* should be sent as extra data with tab group-related metrics events.
*/
const METRIC_SOURCE = Object.freeze({
TAB_OVERFLOW_MENU: "tab_overflow",
TAB_GROUP_MENU: "tab_group",
UNKNOWN: "unknown",
});
export const TabGroupMetrics = {
METRIC_SOURCE,
};

View file

@ -379,9 +379,9 @@
}
const diff_in_msec = Date.now() - this._lastUnloaded;
Services.telemetry
.getHistogramById("TAB_UNLOAD_TO_RELOAD")
.add(diff_in_msec / 1000);
Glean.browserEngagement.tabUnloadToReload.accumulateSingleSample(
diff_in_msec / 1000
);
Glean.browserEngagement.tabReloadCount.add(1);
delete this._lastUnloaded;
}

View file

@ -110,6 +110,8 @@
PictureInPicture: "resource://gre/modules/PictureInPicture.sys.mjs",
SmartTabGroupingManager:
"moz-src:///browser/components/tabbrowser/SmartTabGrouping.sys.mjs",
TabGroupMetrics:
"moz-src:///browser/components/tabbrowser/TabGroupMetrics.sys.mjs",
TabStateFlusher:
"resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
UrlbarProviderOpenTabs:
@ -2218,20 +2220,6 @@
browserSidebarContainer.className = "browserSidebarContainer";
browserSidebarContainer.appendChild(browserContainer);
if (!isPreloadBrowser) {
let visibility = Services.prefs.getStringPref(
"sidebar.visibility",
"always-show"
);
let expandOnHover = Services.prefs.getBoolPref(
"sidebar.expandOnHover",
false
);
if (visibility === "expand-on-hover" && expandOnHover) {
SidebarController.toggleExpandOnHover(true);
}
}
// Prevent the superfluous initial load of a blank document
// if we're going to load something other than about:blank.
if (!uriIsAboutBlank || skipLoad) {
@ -2969,7 +2957,9 @@
* switches windows).
* Causes the group create UI to be displayed and telemetry events to be fired.
* @param {string} [options.telemetryUserCreateSource]
* The means by which the tab group was created. Defaults to "unknown".
* The means by which the tab group was created.
* @see TabGroupMetrics.METRIC_SOURCE for possible values.
* Defaults to "unknown".
*/
addTabGroup(
tabs,
@ -3038,8 +3028,23 @@
* The tab group to remove.
* @param {object} [options]
* Options to use when removing tabs. @see removeTabs for more info.
* @param {boolean} [options.isUserTriggered=false]
* Should be true if this group is being removed by an explicit
* request from the user (as opposed to a group being removed
* for technical reasons, such as when an already existing group
* switches windows). This causes telemetry events to fire.
* @param {string} [options.telemetrySource="unknown"]
* The means by which the tab group was removed.
* @see TabGroupMetrics.METRIC_SOURCE for possible values.
* Defaults to "unknown".
*/
async removeTabGroup(group, options = {}) {
async removeTabGroup(
group,
options = {
isUserTriggered: false,
telemetrySource: this.TabGroupMetrics.METRIC_SOURCE.UNKNOWN,
}
) {
if (this.tabGroupMenu.panel.state != "closed") {
this.tabGroupMenu.panel.hidePopup(options.animate);
}
@ -3073,6 +3078,8 @@
bubbles: true,
detail: {
skipSessionStore: options.skipSessionStore,
isUserTriggered: options.isUserTriggered,
telemetrySource: options.telemetrySource,
},
})
);

View file

@ -7,6 +7,9 @@
// This is loaded into chrome windows with the subscript loader. Wrap in
// a block to prevent accidentally leaking globals onto `window`.
{
const { TabGroupMetrics } = ChromeUtils.importESModule(
"moz-src:///browser/components/tabbrowser/TabGroupMetrics.sys.mjs"
);
const { TabStateFlusher } = ChromeUtils.importESModule(
"resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
);
@ -445,7 +448,10 @@
document
.getElementById("tabGroupEditor_deleteGroup")
.addEventListener("command", () => {
gBrowser.removeTabGroup(this.activeGroup);
gBrowser.removeTabGroup(this.activeGroup, {
isUserTriggered: true,
telemetrySource: TabGroupMetrics.METRIC_SOURCE.TAB_GROUP_MENU,
});
});
this.panel.addEventListener("popupshown", this);

View file

@ -79,6 +79,23 @@ browser.engagement:
type: quantity
expires: never
tab_unload_to_reload:
type: timing_distribution
description: >
How long (sec) a tab had been unloaded until it was reloaded.
This metric was generated to correspond to the Legacy Telemetry
exponential histogram TAB_UNLOAD_TO_RELOAD.
time_unit: second
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1715858
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1715858
notification_emails:
- tkikuchi@mozilla.com
expires: never
telemetry_mirror: TAB_UNLOAD_TO_RELOAD
browser.ui.interaction:
all_tabs_panel_dragstart_tab_event_count:
type: counter
@ -233,6 +250,29 @@ tabgroup:
description: The ID of the saved tab group
type: string
delete:
type: event
description: >
Recorded when the user deletes a tab group
notification_emails:
- dao@mozilla.com
- jswinarton@mozilla.com
- sthompson@mozilla.com
bugs:
- https://bugzil.la/1938430
data_reviews:
- https://bugzil.la/1938430
data_sensitivity:
- interaction
extra_keys:
source:
description: The system, surface, or control the user used to delete the tab group
type: string
id:
description: Tab group ID of the tab group being deleted. Tab group IDs are derived from their creation timestamps and have no other relationship to any tab group metadata.
type: string
expires: never
smart_tab_optin:
type: event
description: >

View file

@ -13,6 +13,7 @@ MOZ_SRC_FILES += [
"NewTabPagePreloading.sys.mjs",
"OpenInTabsUtils.sys.mjs",
"SmartTabGrouping.sys.mjs",
"TabGroupMetrics.sys.mjs",
"TabsList.sys.mjs",
"TabUnloader.sys.mjs",
]

View file

@ -2,24 +2,39 @@
* 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 { TabStateFlusher } = ChromeUtils.importESModule(
"resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
);
let resetTelemetry = async () => {
await Services.fog.testFlushAllChildren();
Services.fog.testResetFOG();
};
/** @type {Window} */
let win;
add_setup(async () => {
win = await BrowserTestUtils.openNewBrowserWindow();
win.gTabsPanel.init();
registerCleanupFunction(async () => {
await BrowserTestUtils.closeWindow(win);
});
});
add_task(async function test_tabGroupTelemetry() {
await resetTelemetry();
let tabGroupCreateTelemetry;
let group1tab = BrowserTestUtils.addTab(gBrowser, "https://example.com");
let group1tab = BrowserTestUtils.addTab(win.gBrowser, "https://example.com");
await BrowserTestUtils.browserLoaded(group1tab.linkedBrowser);
let group1 = gBrowser.addTabGroup([group1tab], {
let group1 = win.gBrowser.addTabGroup([group1tab], {
isUserTriggered: true,
telemetryUserCreateSource: "test-source",
});
gBrowser.tabGroupMenu.close();
win.gBrowser.tabGroupMenu.close();
await BrowserTestUtils.waitForCondition(() => {
tabGroupCreateTelemetry = Glean.tabgroup.createGroup.testGetValue();
@ -48,7 +63,7 @@ add_task(async function test_tabGroupTelemetry() {
);
Assert.equal(
Glean.tabgroup.tabCountInGroups.outside.testGetValue(),
1,
2,
"tabCountInGroups.outside has correct value"
);
Assert.equal(
@ -75,19 +90,19 @@ add_task(async function test_tabGroupTelemetry() {
await resetTelemetry();
let group2Tabs = [
BrowserTestUtils.addTab(gBrowser, "https://example.com"),
BrowserTestUtils.addTab(gBrowser, "https://example.com"),
BrowserTestUtils.addTab(gBrowser, "https://example.com"),
BrowserTestUtils.addTab(win.gBrowser, "https://example.com"),
BrowserTestUtils.addTab(win.gBrowser, "https://example.com"),
BrowserTestUtils.addTab(win.gBrowser, "https://example.com"),
];
await Promise.all(
group2Tabs.map(t => BrowserTestUtils.browserLoaded(t.linkedBrowser))
);
let group2 = gBrowser.addTabGroup(group2Tabs, {
let group2 = win.gBrowser.addTabGroup(group2Tabs, {
isUserTriggered: true,
telemetryUserCreateSource: "test-source",
});
gBrowser.tabGroupMenu.close();
win.gBrowser.tabGroupMenu.close();
await BrowserTestUtils.waitForCondition(() => {
return (
@ -103,7 +118,7 @@ add_task(async function test_tabGroupTelemetry() {
);
Assert.equal(
Glean.tabgroup.tabCountInGroups.outside.testGetValue(),
1,
2,
"tabCountInGroups.outside has correct value after adding a new tab group"
);
Assert.equal(
@ -129,7 +144,10 @@ add_task(async function test_tabGroupTelemetry() {
await resetTelemetry();
let newTabInGroup2 = BrowserTestUtils.addTab(gBrowser, "https://example.com");
let newTabInGroup2 = BrowserTestUtils.addTab(
win.gBrowser,
"https://example.com"
);
await BrowserTestUtils.browserLoaded(newTabInGroup2.linkedBrowser);
group2.addTabs([newTabInGroup2]);
@ -148,7 +166,7 @@ add_task(async function test_tabGroupTelemetry() {
);
Assert.equal(
Glean.tabgroup.tabCountInGroups.outside.testGetValue(),
1,
2,
"tabCountInGroups.outside has correct value after modifying a tab group"
);
Assert.equal(
@ -202,9 +220,9 @@ add_task(async function test_tabGroupTelemetrySaveGroup() {
await resetTelemetry();
let group1tab = BrowserTestUtils.addTab(gBrowser, "https://example.com");
let group1tab = BrowserTestUtils.addTab(win.gBrowser, "https://example.com");
await BrowserTestUtils.browserLoaded(group1tab.linkedBrowser);
let group1 = gBrowser.addTabGroup([group1tab]);
let group1 = win.gBrowser.addTabGroup([group1tab]);
group1.saveAndClose();
await BrowserTestUtils.waitForCondition(() => {
@ -223,7 +241,7 @@ add_task(async function test_tabGroupTelemetrySaveGroup() {
await resetTelemetry();
let group2tab = BrowserTestUtils.addTab(gBrowser, "https://example.com");
let group2tab = BrowserTestUtils.addTab(win.gBrowser, "https://example.com");
await BrowserTestUtils.browserLoaded(group2tab.linkedBrowser);
let group2 = gBrowser.addTabGroup([group2tab]);
group2.saveAndClose({ isUserTriggered: true });
@ -242,3 +260,189 @@ add_task(async function test_tabGroupTelemetrySaveGroup() {
"tabgroup.save event extra_keys has correct values after tab group save by explicit user event"
);
});
/**
* @param {MozTabbrowserTabGroup} tabGroup
* @returns {Promise<MozPanel>}
* Panel holding the tab group context menu for the requested tab group.
*/
async function openTabGroupContextMenu(tabGroup) {
let tabgroupEditor =
tabGroup.ownerDocument.getElementById("tab-group-editor");
let tabgroupPanel = tabgroupEditor.panel;
let panelShown = BrowserTestUtils.waitForPopupEvent(tabgroupPanel, "shown");
EventUtils.synthesizeMouseAtCenter(
tabGroup.querySelector(".tab-group-label"),
{ type: "contextmenu", button: 2 },
tabGroup.ownerGlobal
);
await panelShown;
return tabgroupPanel;
}
add_task(async function test_tabGroupContextMenu_deleteTabGroup() {
await resetTelemetry();
let tab = BrowserTestUtils.addTab(win.gBrowser, "https://example.com");
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
let group = win.gBrowser.addTabGroup([tab]);
// Close the automatically-opened "create tab group" menu.
win.gBrowser.tabGroupMenu.close();
let groupId = group.id;
let menu = await openTabGroupContextMenu(group);
let deleteTabGroupButton = menu.querySelector("#tabGroupEditor_deleteGroup");
deleteTabGroupButton.click();
await TestUtils.waitForCondition(
() => !win.gBrowser.tabGroups.includes(group),
"wait for group to be deleted"
);
let tabGroupDeleteEvents = Glean.tabgroup.delete.testGetValue();
Assert.equal(
tabGroupDeleteEvents.length,
1,
"should have recorded a tabgroup.delete event"
);
let [tabGroupDeleteEvent] = tabGroupDeleteEvents;
Assert.deepEqual(
tabGroupDeleteEvent.extra,
{
source: "tab_group",
id: groupId,
},
"should have recorded the correct source and ID"
);
await resetTelemetry();
});
/**
* @returns {Promise<PanelView>}
*/
async function openTabsMenu() {
let viewShown = BrowserTestUtils.waitForEvent(
win.document.getElementById("allTabsMenu-allTabsView"),
"ViewShown"
);
win.document.getElementById("alltabs-button").click();
return (await viewShown).target;
}
/**
* @returns {Promise<void>}
*/
async function closeTabsMenu() {
let panel = win.document
.getElementById("allTabsMenu-allTabsView")
.closest("panel");
if (!panel) {
return;
}
let hidden = BrowserTestUtils.waitForPopupEvent(panel, "hidden");
panel.hidePopup();
await hidden;
}
/**
* @param {XULToolbarButton} triggerNode
* @param {string} contextMenuId
* @returns {Promise<XULMenuElement|XULPopupElement>}
*/
async function getContextMenu(triggerNode, contextMenuId) {
let nodeWindow = triggerNode.ownerGlobal;
triggerNode.scrollIntoView();
const contextMenu = nodeWindow.document.getElementById(contextMenuId);
const contextMenuShown = BrowserTestUtils.waitForPopupEvent(
contextMenu,
"shown"
);
EventUtils.synthesizeMouseAtCenter(
triggerNode,
{ type: "contextmenu", button: 2 },
nodeWindow
);
await contextMenuShown;
return contextMenu;
}
/**
* @param {XULMenuElement|XULPopupElement} contextMenu
* @returns {Promise<void>}
*/
async function closeContextMenu(contextMenu) {
let menuHidden = BrowserTestUtils.waitForPopupEvent(contextMenu, "hidden");
contextMenu.hidePopup();
await menuHidden;
}
/**
* Returns a new basic, unnamed tab group that is fully loaded in the browser
* and in session state.
*
* @returns {Promise<MozTabbrowserTabGroup>}
*/
async function makeTabGroup() {
let tab = BrowserTestUtils.addTab(win.gBrowser, "https://example.com");
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
await TabStateFlusher.flush(tab.linkedBrowser);
let group = win.gBrowser.addTabGroup([tab]);
// Close the automatically-opened "create tab group" menu.
win.gBrowser.tabGroupMenu.close();
return group;
}
add_task(async function test_tabOverflowContextMenu_deleteOpenTabGroup() {
await resetTelemetry();
info("set up an open tab group to be deleted");
let openGroup = await makeTabGroup();
let openGroupId = openGroup.id;
info("delete the open tab group");
let allTabsMenu = await openTabsMenu();
let tabGroupButton = allTabsMenu.querySelector(
`#allTabsMenu-groupsView [data-tab-group-id="${openGroupId}"]`
);
let menu = await getContextMenu(
tabGroupButton,
"open-tab-group-context-menu"
);
menu.querySelector("#open-tab-group-context-menu_delete").click();
await closeContextMenu(menu);
await closeTabsMenu();
await TestUtils.waitForCondition(
() => !win.gBrowser.tabGroups.includes(openGroup),
"wait for group to be deleted"
);
let tabGroupDeleteEvents = Glean.tabgroup.delete.testGetValue();
Assert.equal(
tabGroupDeleteEvents.length,
1,
"should have recorded one tabgroup.delete event"
);
let [openTabGroupDeleteEvent] = tabGroupDeleteEvents;
Assert.deepEqual(
openTabGroupDeleteEvent.extra,
{
source: "tab_overflow",
id: openGroupId,
},
"should have recorded the correct source and ID for the open tab group"
);
await resetTelemetry();
});

View file

@ -11,11 +11,6 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
const kWebAppWindowFeatures =
"chrome,dialog=no,titlebar,close,toolbar,location,personalbar=no,status,menubar=no,resizable,minimizable,scrollbars";
let lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
});
export let TaskbarTabs = {
async init(window) {
if (
@ -35,10 +30,18 @@ export let TaskbarTabs = {
async handleEvent(event) {
let gBrowser = event.view.gBrowser;
await lazy.BrowserWindowTracker.promiseOpenWindow({
features: kWebAppWindowFeatures,
args: this._generateArgs(gBrowser.selectedTab),
let win = Services.ww.openWindow(
null,
AppConstants.BROWSER_CHROME_URL,
"_blank",
kWebAppWindowFeatures,
this._generateArgs(gBrowser.selectedTab)
);
await new Promise(resolve => {
win.addEventListener("load", resolve, { once: true });
});
await win.delayedStartupPromise;
},
/**

View file

@ -137,8 +137,11 @@ for (const type of [
"PREVIEW_REQUEST_CANCEL",
"PREVIEW_RESPONSE",
"REMOVE_DOWNLOAD_FILE",
"REPORT_AD_OPEN",
"REPORT_AD_SUBMIT",
"REPORT_CLOSE",
"REPORT_OPEN",
"REPORT_CONTENT_OPEN",
"REPORT_CONTENT_SUBMIT",
"RICH_ICON_MISSING",
"SAVE_SESSION_PERF_DATA",
"SAVE_TO_POCKET",

View file

@ -896,15 +896,41 @@ function DiscoveryStream(prevState = INITIAL_STATE.DiscoveryStream, action) {
showBlockSectionConfirmation: true,
sectionData: action.data,
};
case at.REPORT_OPEN:
case at.REPORT_AD_OPEN:
return {
...prevState,
report: {
...prevState.report,
card_type: action.data?.card_type,
position: action.data?.position,
placement_id: action.data?.placement_id,
reporting_url: action.data?.reporting_url,
url: action.data?.url,
visible: true,
},
};
case at.REPORT_CONTENT_OPEN:
return {
...prevState,
report: {
...prevState.report,
card_type: action.data?.card_type,
corpus_item_id: action.data?.corpus_item_id,
is_section_followed: action.data?.is_section_followed,
received_rank: action.data?.received_rank,
recommended_at: action.data?.recommended_at,
scheduled_corpus_item_id: action.data?.scheduled_corpus_item_id,
section_position: action.data?.section_position,
section: action.data?.section,
title: action.data?.title,
topic: action.data?.topic,
url: action.data?.url,
visible: true,
},
};
case at.REPORT_CLOSE:
case at.REPORT_AD_SUBMIT:
case at.REPORT_CONTENT_SUBMIT:
return {
...prevState,
report: {

View file

@ -320,14 +320,15 @@ export class _DiscoveryStreamBase extends React.PureComponent {
}
// Render a DS-style TopSites then the rest if any in a collapsible section
const { DiscoveryStream } = this.props;
return (
<React.Fragment>
{this.props.DiscoveryStream.isPrivacyInfoModalVisible && (
<DSPrivacyModal dispatch={this.props.dispatch} />
)}
{reportContentEnabled && <ReportContent />}
{reportContentEnabled && (
<ReportContent spocs={DiscoveryStream.spocs} />
)}
{topSites &&
this.renderLayout([
{

View file

@ -923,6 +923,8 @@ export class _DSCard extends React.PureComponent {
is_section_followed={this.props.sectionFollowed}
format={format}
isSectionsCard={this.props.mayHaveSectionsCards}
topic={this.props.topic}
selected_topics={this.props.selected_topics}
/>
)}
</div>

View file

@ -31,7 +31,7 @@ export class _DSLinkMenu extends React.PureComponent {
TOP_STORIES_CONTEXT_MENU_OPTIONS = [
"CheckBookmark",
...(showReporting ? ["ReportContent"] : []),
...(showReporting && this.props.section ? ["ReportContent"] : []),
...saveToPocketOptions,
"Separator",
"OpenInNewWindow",
@ -41,6 +41,9 @@ export class _DSLinkMenu extends React.PureComponent {
];
}
// eslint-disable-next-line no-console
console.log("dslinkmenu prop", this.props);
const type = this.props.type || "DISCOVERY_STREAM";
const title = this.props.title || this.props.source;
@ -76,7 +79,9 @@ export class _DSLinkMenu extends React.PureComponent {
scheduled_corpus_item_id: this.props.scheduled_corpus_item_id,
recommended_at: this.props.recommended_at,
received_rank: this.props.received_rank,
topic: this.props.topic,
is_list_card: this.props.is_list_card,
position: index,
...(this.props.format ? { format: this.props.format } : {}),
...(this.props.section
? {

View file

@ -5,26 +5,102 @@ import React, { useRef, useEffect, useCallback, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { actionTypes as at, actionCreators as ac } from "common/Actions.mjs";
export const ReportContent = () => {
export const ReportContent = spocs => {
const dispatch = useDispatch();
const modal = useRef(null);
const radioGroupRef = useRef(null);
const submitButtonRef = useRef(null);
const report = useSelector(state => state.DiscoveryStream.report);
const [valueSelected, setValueSelected] = useState(false);
const [selectedReason, setSelectedReason] = useState(null);
const spocData = spocs.spocs.data;
// Sends a dispatch to update the redux store when modal is cancelled
const handleCancel = useCallback(() => {
const handleCancel = () => {
dispatch(
ac.BroadcastToContent({
ac.AlsoToMain({
type: at.REPORT_CLOSE,
})
);
}, [dispatch]);
};
const handleSubmit = useCallback(e => {
e.preventDefault();
}, []);
const handleSubmit = useCallback(() => {
const {
card_type,
corpus_item_id,
is_section_followed,
position,
received_rank,
recommended_at,
reporting_url,
scheduled_corpus_item_id,
section_position,
section,
title,
topic,
url,
} = report;
if (card_type === "organic") {
dispatch(
ac.AlsoToMain({
type: at.REPORT_CONTENT_SUBMIT,
data: {
card_type,
corpus_item_id,
is_section_followed,
received_rank,
recommended_at,
report_reason: selectedReason,
scheduled_corpus_item_id,
section_position,
section,
title,
topic,
url,
},
})
);
} else if (card_type === "spoc") {
// Retrieve placement_id by comparing spocData with the ad that was reported
const getPlacementId = () => {
if (!spocData || !report.url) {
return null;
}
for (const [placementId, spocList] of Object.entries(spocData)) {
for (const spoc of Object.values(spocList)) {
if (spoc?.url === report.url) {
return placementId;
}
}
}
return null;
};
const placement_id = getPlacementId();
dispatch(
ac.AlsoToMain({
type: at.REPORT_AD_SUBMIT,
data: {
report_reason: selectedReason,
placement_id,
position,
reporting_url,
url,
},
})
);
}
dispatch(
ac.AlsoToMain({
type: at.BLOCK_URL,
data: [{ ...report }],
})
);
}, [dispatch, selectedReason, report, spocData]);
// Opens and closes the modal based on user interaction
useEffect(() => {
@ -40,7 +116,14 @@ export const ReportContent = () => {
const radioGroup = radioGroupRef.current;
const submitButton = submitButtonRef.current;
const handleRadioChange = () => setValueSelected(true);
const handleRadioChange = e => {
const reasonValue = e?.target?.value;
if (reasonValue) {
setValueSelected(true);
setSelectedReason(reasonValue);
}
};
if (radioGroup) {
radioGroup.addEventListener("change", handleRadioChange);
@ -62,7 +145,7 @@ export const ReportContent = () => {
radioGroup.removeEventListener("change", handleRadioChange);
}
};
}, [valueSelected]);
}, [valueSelected, selectedReason]);
return (
<dialog
@ -72,25 +155,53 @@ export const ReportContent = () => {
onClose={() => dispatch({ type: at.REPORT_CLOSE })}
>
<form action="">
<moz-radio-group
name="report"
ref={radioGroupRef}
id="report-group"
data-l10n-id="newtab-report-ads-why-reporting"
>
<moz-radio
value="unsafe"
data-l10n-id="newtab-report-ads-reason-unsafe"
></moz-radio>
<moz-radio
data-l10n-id="newtab-report-ads-reason-inappropriate"
value="inappropriate"
></moz-radio>
<moz-radio
data-l10n-id="newtab-report-ads-reason-seen-it-too-many-times"
value="too-many"
></moz-radio>
</moz-radio-group>
{/* spocs and stories are going to have different reporting
options, so placed a conditional to render the different reasons */}
{report.card_type === "spoc" ? (
<>
<moz-radio-group
name="report"
ref={radioGroupRef}
id="report-group"
data-l10n-id="newtab-report-ads-why-reporting"
>
<moz-radio
data-l10n-id="newtab-report-ads-reason-not-interested"
value="not_interested"
></moz-radio>
<moz-radio
data-l10n-id="newtab-report-ads-reason-inappropriate"
value="inappropriate"
></moz-radio>
<moz-radio
data-l10n-id="newtab-report-ads-reason-seen-it-too-many-times"
value="seen_too_many_times"
></moz-radio>
</moz-radio-group>
</>
) : (
<>
<moz-radio-group
name="report"
ref={radioGroupRef}
id="report-group"
data-l10n-id="newtab-report-content-why-reporting"
>
<moz-radio
value="Unsafe content"
data-l10n-id="newtab-report-ads-reason-unsafe"
></moz-radio>
<moz-radio
data-l10n-id="newtab-report-ads-reason-inappropriate"
value="Inappropriate content"
></moz-radio>
<moz-radio
data-l10n-id="newtab-report-ads-reason-seen-it-too-many-times"
value="Seen too many times"
></moz-radio>
</moz-radio-group>
</>
)}
<moz-button-group>
<moz-button

View file

@ -93,7 +93,7 @@ export const LinkMenuOptions = {
BlockUrl: (site, index, eventSource) => {
return LinkMenuOptions.BlockUrls([site], index, eventSource);
},
// Same as BlockUrl, cept can work on an array of sites.
// Same as BlockUrl, except can work on an array of sites.
BlockUrls: (tiles, pos, eventSource) => ({
id: "newtab-menu-dismiss",
icon: "dismiss",
@ -525,12 +525,40 @@ export const LinkMenuOptions = {
},
}),
}),
ReportAd: () => ({
id: "newtab-menu-report-this-ad",
action: ac.BroadcastToContent({ type: at.REPORT_OPEN }),
}),
ReportContent: () => ({
id: "newtab-menu-report-content",
action: ac.BroadcastToContent({ type: at.REPORT_OPEN }),
}),
ReportAd: site => {
return {
id: "newtab-menu-report-this-ad",
action: ac.AlsoToMain({
type: at.REPORT_AD_OPEN,
data: {
card_type: site.card_type,
position: site.position,
reporting_url: site.shim.report,
url: site.url,
},
}),
};
},
ReportContent: site => {
return {
id: "newtab-menu-report-content",
action: ac.AlsoToMain({
type: at.REPORT_CONTENT_OPEN,
data: {
card_type: site.card_type,
corpus_item_id: site.corpus_item_id,
is_section_followed: site.is_section_followed,
received_rank: site.received_rank,
recommended_at: site.recommended_at,
scheduled_corpus_item_id: site.scheduled_corpus_item_id,
section_position: site.section_position,
section: site.section,
title: site.title,
topic: site.topic,
url: site.url,
},
}),
};
},
};

View file

@ -210,8 +210,11 @@ for (const type of [
"PREVIEW_REQUEST_CANCEL",
"PREVIEW_RESPONSE",
"REMOVE_DOWNLOAD_FILE",
"REPORT_AD_OPEN",
"REPORT_AD_SUBMIT",
"REPORT_CLOSE",
"REPORT_OPEN",
"REPORT_CONTENT_OPEN",
"REPORT_CONTENT_SUBMIT",
"RICH_ICON_MISSING",
"SAVE_SESSION_PERF_DATA",
"SAVE_TO_POCKET",
@ -1777,7 +1780,7 @@ const LinkMenuOptions = {
BlockUrl: (site, index, eventSource) => {
return LinkMenuOptions.BlockUrls([site], index, eventSource);
},
// Same as BlockUrl, cept can work on an array of sites.
// Same as BlockUrl, except can work on an array of sites.
BlockUrls: (tiles, pos, eventSource) => ({
id: "newtab-menu-dismiss",
icon: "dismiss",
@ -2209,14 +2212,42 @@ const LinkMenuOptions = {
},
}),
}),
ReportAd: () => ({
id: "newtab-menu-report-this-ad",
action: actionCreators.BroadcastToContent({ type: actionTypes.REPORT_OPEN }),
}),
ReportContent: () => ({
id: "newtab-menu-report-content",
action: actionCreators.BroadcastToContent({ type: actionTypes.REPORT_OPEN }),
}),
ReportAd: site => {
return {
id: "newtab-menu-report-this-ad",
action: actionCreators.AlsoToMain({
type: actionTypes.REPORT_AD_OPEN,
data: {
card_type: site.card_type,
position: site.position,
reporting_url: site.shim.report,
url: site.url,
},
}),
};
},
ReportContent: site => {
return {
id: "newtab-menu-report-content",
action: actionCreators.AlsoToMain({
type: actionTypes.REPORT_CONTENT_OPEN,
data: {
card_type: site.card_type,
corpus_item_id: site.corpus_item_id,
is_section_followed: site.is_section_followed,
received_rank: site.received_rank,
recommended_at: site.recommended_at,
scheduled_corpus_item_id: site.scheduled_corpus_item_id,
section_position: site.section_position,
section: site.section,
title: site.title,
topic: site.topic,
url: site.url,
},
}),
};
},
};
;// CONCATENATED MODULE: ./content-src/components/LinkMenu/LinkMenu.jsx
@ -2414,8 +2445,11 @@ class _DSLinkMenu extends (external_React_default()).PureComponent {
TOP_STORIES_CONTEXT_MENU_OPTIONS = ["BlockUrl", ...(showReporting ? ["ReportAd"] : []), "ManageSponsoredContent", "OurSponsorsAndYourPrivacy"];
} else {
const saveToPocketOptions = this.props.pocket_button_enabled ? ["CheckArchiveFromPocket", "CheckSavedToPocket"] : [];
TOP_STORIES_CONTEXT_MENU_OPTIONS = ["CheckBookmark", ...(showReporting ? ["ReportContent"] : []), ...saveToPocketOptions, "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"];
TOP_STORIES_CONTEXT_MENU_OPTIONS = ["CheckBookmark", ...(showReporting && this.props.section ? ["ReportContent"] : []), ...saveToPocketOptions, "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl"];
}
// eslint-disable-next-line no-console
console.log("dslinkmenu prop", this.props);
const type = this.props.type || "DISCOVERY_STREAM";
const title = this.props.title || this.props.source;
return /*#__PURE__*/external_React_default().createElement("div", {
@ -2451,7 +2485,9 @@ class _DSLinkMenu extends (external_React_default()).PureComponent {
scheduled_corpus_item_id: this.props.scheduled_corpus_item_id,
recommended_at: this.props.recommended_at,
received_rank: this.props.received_rank,
topic: this.props.topic,
is_list_card: this.props.is_list_card,
position: index,
...(this.props.format ? {
format: this.props.format
} : {}),
@ -4047,7 +4083,9 @@ class _DSCard extends (external_React_default()).PureComponent {
section_position: this.props.sectionPosition,
is_section_followed: this.props.sectionFollowed,
format: format,
isSectionsCard: this.props.mayHaveSectionsCards
isSectionsCard: this.props.mayHaveSectionsCards,
topic: this.props.topic,
selected_topics: this.props.selected_topics
}))));
}
}
@ -5770,23 +5808,90 @@ class DSPrivacyModal extends (external_React_default()).PureComponent {
const ReportContent = () => {
const ReportContent = spocs => {
const dispatch = (0,external_ReactRedux_namespaceObject.useDispatch)();
const modal = (0,external_React_namespaceObject.useRef)(null);
const radioGroupRef = (0,external_React_namespaceObject.useRef)(null);
const submitButtonRef = (0,external_React_namespaceObject.useRef)(null);
const report = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.DiscoveryStream.report);
const [valueSelected, setValueSelected] = (0,external_React_namespaceObject.useState)(false);
const [selectedReason, setSelectedReason] = (0,external_React_namespaceObject.useState)(null);
const spocData = spocs.spocs.data;
// Sends a dispatch to update the redux store when modal is cancelled
const handleCancel = (0,external_React_namespaceObject.useCallback)(() => {
dispatch(actionCreators.BroadcastToContent({
const handleCancel = () => {
dispatch(actionCreators.AlsoToMain({
type: actionTypes.REPORT_CLOSE
}));
}, [dispatch]);
const handleSubmit = (0,external_React_namespaceObject.useCallback)(e => {
e.preventDefault();
}, []);
};
const handleSubmit = (0,external_React_namespaceObject.useCallback)(() => {
const {
card_type,
corpus_item_id,
is_section_followed,
position,
received_rank,
recommended_at,
reporting_url,
scheduled_corpus_item_id,
section_position,
section,
title,
topic,
url
} = report;
if (card_type === "organic") {
dispatch(actionCreators.AlsoToMain({
type: actionTypes.REPORT_CONTENT_SUBMIT,
data: {
card_type,
corpus_item_id,
is_section_followed,
received_rank,
recommended_at,
report_reason: selectedReason,
scheduled_corpus_item_id,
section_position,
section,
title,
topic,
url
}
}));
} else if (card_type === "spoc") {
// Retrieve placement_id by comparing spocData with the ad that was reported
const getPlacementId = () => {
if (!spocData || !report.url) {
return null;
}
for (const [placementId, spocList] of Object.entries(spocData)) {
for (const spoc of Object.values(spocList)) {
if (spoc?.url === report.url) {
return placementId;
}
}
}
return null;
};
const placement_id = getPlacementId();
dispatch(actionCreators.AlsoToMain({
type: actionTypes.REPORT_AD_SUBMIT,
data: {
report_reason: selectedReason,
placement_id,
position,
reporting_url,
url
}
}));
}
dispatch(actionCreators.AlsoToMain({
type: actionTypes.BLOCK_URL,
data: [{
...report
}]
}));
}, [dispatch, selectedReason, report, spocData]);
// Opens and closes the modal based on user interaction
(0,external_React_namespaceObject.useEffect)(() => {
@ -5801,7 +5906,13 @@ const ReportContent = () => {
(0,external_React_namespaceObject.useEffect)(() => {
const radioGroup = radioGroupRef.current;
const submitButton = submitButtonRef.current;
const handleRadioChange = () => setValueSelected(true);
const handleRadioChange = e => {
const reasonValue = e?.target?.value;
if (reasonValue) {
setValueSelected(true);
setSelectedReason(reasonValue);
}
};
if (radioGroup) {
radioGroup.addEventListener("change", handleRadioChange);
}
@ -5820,7 +5931,7 @@ const ReportContent = () => {
radioGroup.removeEventListener("change", handleRadioChange);
}
};
}, [valueSelected]);
}, [valueSelected, selectedReason]);
return /*#__PURE__*/external_React_default().createElement("dialog", {
className: "report-content-form",
id: "dialog-report",
@ -5830,21 +5941,35 @@ const ReportContent = () => {
})
}, /*#__PURE__*/external_React_default().createElement("form", {
action: ""
}, /*#__PURE__*/external_React_default().createElement("moz-radio-group", {
}, report.card_type === "spoc" ? /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("moz-radio-group", {
name: "report",
ref: radioGroupRef,
id: "report-group",
"data-l10n-id": "newtab-report-ads-why-reporting"
}, /*#__PURE__*/external_React_default().createElement("moz-radio", {
value: "unsafe",
"data-l10n-id": "newtab-report-ads-reason-unsafe"
"data-l10n-id": "newtab-report-ads-reason-not-interested",
value: "not_interested"
}), /*#__PURE__*/external_React_default().createElement("moz-radio", {
"data-l10n-id": "newtab-report-ads-reason-inappropriate",
value: "inappropriate"
}), /*#__PURE__*/external_React_default().createElement("moz-radio", {
"data-l10n-id": "newtab-report-ads-reason-seen-it-too-many-times",
value: "too-many"
})), /*#__PURE__*/external_React_default().createElement("moz-button-group", null, /*#__PURE__*/external_React_default().createElement("moz-button", {
value: "seen_too_many_times"
}))) : /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, /*#__PURE__*/external_React_default().createElement("moz-radio-group", {
name: "report",
ref: radioGroupRef,
id: "report-group",
"data-l10n-id": "newtab-report-content-why-reporting"
}, /*#__PURE__*/external_React_default().createElement("moz-radio", {
value: "Unsafe content",
"data-l10n-id": "newtab-report-ads-reason-unsafe"
}), /*#__PURE__*/external_React_default().createElement("moz-radio", {
"data-l10n-id": "newtab-report-ads-reason-inappropriate",
value: "Inappropriate content"
}), /*#__PURE__*/external_React_default().createElement("moz-radio", {
"data-l10n-id": "newtab-report-ads-reason-seen-it-too-many-times",
value: "Seen too many times"
}))), /*#__PURE__*/external_React_default().createElement("moz-button-group", null, /*#__PURE__*/external_React_default().createElement("moz-button", {
"data-l10n-id": "newtab-topic-selection-cancel-button",
onClick: handleCancel
}), /*#__PURE__*/external_React_default().createElement("moz-button", {
@ -7941,15 +8066,41 @@ function DiscoveryStream(prevState = INITIAL_STATE.DiscoveryStream, action) {
showBlockSectionConfirmation: true,
sectionData: action.data,
};
case actionTypes.REPORT_OPEN:
case actionTypes.REPORT_AD_OPEN:
return {
...prevState,
report: {
...prevState.report,
card_type: action.data?.card_type,
position: action.data?.position,
placement_id: action.data?.placement_id,
reporting_url: action.data?.reporting_url,
url: action.data?.url,
visible: true,
},
};
case actionTypes.REPORT_CONTENT_OPEN:
return {
...prevState,
report: {
...prevState.report,
card_type: action.data?.card_type,
corpus_item_id: action.data?.corpus_item_id,
is_section_followed: action.data?.is_section_followed,
received_rank: action.data?.received_rank,
recommended_at: action.data?.recommended_at,
scheduled_corpus_item_id: action.data?.scheduled_corpus_item_id,
section_position: action.data?.section_position,
section: action.data?.section,
title: action.data?.title,
topic: action.data?.topic,
url: action.data?.url,
visible: true,
},
};
case actionTypes.REPORT_CLOSE:
case actionTypes.REPORT_AD_SUBMIT:
case actionTypes.REPORT_CONTENT_SUBMIT:
return {
...prevState,
report: {
@ -11325,9 +11476,14 @@ class _DiscoveryStreamBase extends (external_React_default()).PureComponent {
}
// Render a DS-style TopSites then the rest if any in a collapsible section
const {
DiscoveryStream
} = this.props;
return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, this.props.DiscoveryStream.isPrivacyInfoModalVisible && /*#__PURE__*/external_React_default().createElement(DSPrivacyModal, {
dispatch: this.props.dispatch
}), reportContentEnabled && /*#__PURE__*/external_React_default().createElement(ReportContent, null), topSites && this.renderLayout([{
}), reportContentEnabled && /*#__PURE__*/external_React_default().createElement(ReportContent, {
spocs: DiscoveryStream.spocs
}), topSites && this.renderLayout([{
width: 12,
components: [topSites],
sectionType: "topsites"

View file

@ -1,3 +1,6 @@
// We're using console.error() to debug, so we'll be keeping this rule handy
/* eslint no-console: ["error", { allow: ["error"] }] */
/* 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/. */
@ -553,7 +556,6 @@ export class TelemetryFeed {
},
});
const session = this.sessions.get(au.getPortIdOfSender(action));
switch (action.data?.event) {
case "CLICK": {
const {
@ -984,6 +986,89 @@ export class TelemetryFeed {
case at.INLINE_SELECTION_IMPRESSION:
this.handleInlineSelectionUserEvent(action);
break;
case at.REPORT_AD_OPEN:
case at.REPORT_AD_SUBMIT:
this.handleReportAdUserEvent(action);
break;
case at.REPORT_CONTENT_OPEN:
case at.REPORT_CONTENT_SUBMIT:
this.handleReportContentUserEvent(action);
break;
}
}
async handleReportAdUserEvent(action) {
const { placement_id, position, report_reason, reporting_url } =
action.data || {};
const url = new URL(reporting_url);
url.searchParams.append("placement_id", placement_id);
url.searchParams.append("reason", report_reason);
url.searchParams.append("position", position);
const adResponse = url.toString();
const allowed =
this._prefs
.get(PREF_ENDPOINTS)
.split(",")
.map(item => item.trim())
.filter(item => item) || [];
if (!allowed.some(prefix => adResponse.startsWith(prefix))) {
throw new Error(
`[Unified ads callback] Not one of allowed prefixes (${allowed})`
);
}
try {
await fetch(adResponse);
} catch (error) {
console.error("Error:", error);
}
}
handleReportContentUserEvent(action) {
const session = this.sessions.get(au.getPortIdOfSender(action));
const {
card_type,
corpus_item_id,
is_section_followed,
received_rank,
recommended_at,
report_reason,
scheduled_corpus_item_id,
section_position,
section,
title,
topic,
url,
} = action.data || {};
if (session) {
switch (action.type) {
case "REPORT_CONTENT_OPEN":
Glean.newtab.reportContentOpen.record({
newtab_visit_id: session.session_id,
});
break;
case "REPORT_CONTENT_SUBMIT":
Glean.newtab.reportContentSubmit.record({
card_type,
corpus_item_id,
is_section_followed,
newtab_visit_id: session.session_id,
received_rank,
recommended_at,
report_reason,
scheduled_corpus_item_id,
section_position,
section,
title,
topic,
url,
});
break;
}
}
}

View file

@ -61,9 +61,6 @@
#ifdef MOZ_UPDATER
@RESPATH@/updater.ini
#endif
#if defined(MOZ_UPDATE_AGENT)
@RESPATH@/locale.ini
#endif
[xpcom]
@RESPATH@/dependentlibs.list

View file

@ -6,10 +6,14 @@
newtab-report-ads-why-reporting =
.label = Why are you reporting this ad?
newtab-report-content-why-reporting =
.label = Why are you reporting this story?
newtab-report-ads-reason-unsafe =
.label = Its unsafe
newtab-report-ads-reason-inappropriate =
.label = Its inappropriate
newtab-report-ads-reason-seen-it-too-many-times =
.label = Ive seen it too many times
newtab-report-ads-reason-not-interested =
.label = Not interested
newtab-report-submit = Submit

View file

@ -17,7 +17,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"af": {
"pin": false,
@ -37,7 +37,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"an": {
"pin": false,
@ -57,7 +57,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"ar": {
"pin": false,
@ -77,7 +77,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"ast": {
"pin": false,
@ -97,7 +97,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"az": {
"pin": false,
@ -117,7 +117,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"be": {
"pin": false,
@ -137,7 +137,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"bg": {
"pin": false,
@ -157,7 +157,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"bn": {
"pin": false,
@ -177,7 +177,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"bo": {
"pin": false,
@ -197,7 +197,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"br": {
"pin": false,
@ -217,7 +217,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"brx": {
"pin": false,
@ -237,7 +237,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"bs": {
"pin": false,
@ -257,7 +257,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"ca": {
"pin": false,
@ -277,7 +277,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"ca-valencia": {
"pin": false,
@ -297,7 +297,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"cak": {
"pin": false,
@ -317,7 +317,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"ckb": {
"pin": false,
@ -337,7 +337,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"cs": {
"pin": false,
@ -357,7 +357,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"cy": {
"pin": false,
@ -377,7 +377,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"da": {
"pin": false,
@ -397,7 +397,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"de": {
"pin": false,
@ -417,7 +417,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"dsb": {
"pin": false,
@ -437,7 +437,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"el": {
"pin": false,
@ -457,7 +457,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"en-CA": {
"pin": false,
@ -477,7 +477,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"en-GB": {
"pin": false,
@ -497,7 +497,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"eo": {
"pin": false,
@ -517,7 +517,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"es-AR": {
"pin": false,
@ -537,7 +537,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"es-CL": {
"pin": false,
@ -557,7 +557,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"es-ES": {
"pin": false,
@ -577,7 +577,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"es-MX": {
"pin": false,
@ -597,7 +597,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"et": {
"pin": false,
@ -617,7 +617,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"eu": {
"pin": false,
@ -637,7 +637,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"fa": {
"pin": false,
@ -657,7 +657,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"ff": {
"pin": false,
@ -677,7 +677,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"fi": {
"pin": false,
@ -697,7 +697,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"fr": {
"pin": false,
@ -717,7 +717,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"fur": {
"pin": false,
@ -737,7 +737,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"fy-NL": {
"pin": false,
@ -757,7 +757,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"ga-IE": {
"pin": false,
@ -777,7 +777,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"gd": {
"pin": false,
@ -797,7 +797,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"gl": {
"pin": false,
@ -817,7 +817,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"gn": {
"pin": false,
@ -837,7 +837,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"gu-IN": {
"pin": false,
@ -857,7 +857,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"he": {
"pin": false,
@ -877,7 +877,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"hi-IN": {
"pin": false,
@ -897,7 +897,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"hr": {
"pin": false,
@ -917,7 +917,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"hsb": {
"pin": false,
@ -937,7 +937,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"hu": {
"pin": false,
@ -957,7 +957,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"hy-AM": {
"pin": false,
@ -977,7 +977,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"hye": {
"pin": false,
@ -997,7 +997,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"ia": {
"pin": false,
@ -1017,7 +1017,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"id": {
"pin": false,
@ -1037,7 +1037,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"is": {
"pin": false,
@ -1057,7 +1057,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"it": {
"pin": false,
@ -1077,7 +1077,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"ja": {
"pin": false,
@ -1095,7 +1095,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"ja-JP-mac": {
"pin": false,
@ -1103,7 +1103,7 @@
"macosx64",
"macosx64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"ka": {
"pin": false,
@ -1123,7 +1123,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"kab": {
"pin": false,
@ -1143,7 +1143,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"kk": {
"pin": false,
@ -1163,7 +1163,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"km": {
"pin": false,
@ -1183,7 +1183,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"kn": {
"pin": false,
@ -1203,7 +1203,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"ko": {
"pin": false,
@ -1223,7 +1223,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"lij": {
"pin": false,
@ -1243,7 +1243,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"lo": {
"pin": false,
@ -1263,7 +1263,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"lt": {
"pin": false,
@ -1283,7 +1283,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"ltg": {
"pin": false,
@ -1303,7 +1303,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"lv": {
"pin": false,
@ -1323,7 +1323,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"meh": {
"pin": false,
@ -1343,7 +1343,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"mk": {
"pin": false,
@ -1363,7 +1363,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"ml": {
"pin": false,
@ -1383,7 +1383,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"mr": {
"pin": false,
@ -1403,7 +1403,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"ms": {
"pin": false,
@ -1423,7 +1423,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"my": {
"pin": false,
@ -1443,7 +1443,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"nb-NO": {
"pin": false,
@ -1463,7 +1463,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"ne-NP": {
"pin": false,
@ -1483,7 +1483,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"nl": {
"pin": false,
@ -1503,7 +1503,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"nn-NO": {
"pin": false,
@ -1523,7 +1523,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"oc": {
"pin": false,
@ -1543,7 +1543,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"pa-IN": {
"pin": false,
@ -1563,7 +1563,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"pl": {
"pin": false,
@ -1583,7 +1583,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"pt-BR": {
"pin": false,
@ -1603,7 +1603,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"pt-PT": {
"pin": false,
@ -1623,7 +1623,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"rm": {
"pin": false,
@ -1643,7 +1643,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"ro": {
"pin": false,
@ -1663,7 +1663,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"ru": {
"pin": false,
@ -1683,7 +1683,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"sat": {
"pin": false,
@ -1703,7 +1703,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"sc": {
"pin": false,
@ -1723,7 +1723,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"scn": {
"pin": false,
@ -1743,7 +1743,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"sco": {
"pin": false,
@ -1763,7 +1763,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"si": {
"pin": false,
@ -1783,7 +1783,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"sk": {
"pin": false,
@ -1803,7 +1803,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"skr": {
"pin": false,
@ -1823,7 +1823,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"sl": {
"pin": false,
@ -1843,7 +1843,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"son": {
"pin": false,
@ -1863,7 +1863,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"sq": {
"pin": false,
@ -1883,7 +1883,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"sr": {
"pin": false,
@ -1903,7 +1903,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"sv-SE": {
"pin": false,
@ -1923,7 +1923,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"szl": {
"pin": false,
@ -1943,7 +1943,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"ta": {
"pin": false,
@ -1963,7 +1963,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"te": {
"pin": false,
@ -1983,7 +1983,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"tg": {
"pin": false,
@ -2003,7 +2003,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"th": {
"pin": false,
@ -2023,7 +2023,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"tl": {
"pin": false,
@ -2043,7 +2043,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"tr": {
"pin": false,
@ -2063,7 +2063,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"trs": {
"pin": false,
@ -2083,7 +2083,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"uk": {
"pin": false,
@ -2103,7 +2103,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"ur": {
"pin": false,
@ -2123,7 +2123,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"uz": {
"pin": false,
@ -2143,7 +2143,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"vi": {
"pin": false,
@ -2163,7 +2163,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"wo": {
"pin": false,
@ -2183,7 +2183,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"xh": {
"pin": false,
@ -2203,7 +2203,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"zh-CN": {
"pin": false,
@ -2223,7 +2223,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
},
"zh-TW": {
"pin": false,
@ -2243,6 +2243,6 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "68cde7a1ed4d73d438cb6bd224f43a2854703219"
"revision": "1509ebc4af08bf8cc5019c99404117eadc9eca0f"
}
}

View file

@ -18,7 +18,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
"moz-src:///browser/components/search/SearchSERPTelemetry.sys.mjs",
SearchSERPTelemetryUtils:
"moz-src:///browser/components/search/SearchSERPTelemetry.sys.mjs",
TabGroupMetrics:
"moz-src:///browser/components/tabbrowser/TabGroupMetrics.sys.mjs",
WindowsInstallsInfo:
"resource://gre/modules/components-utils/WindowsInstallsInfo.sys.mjs",
@ -597,6 +598,9 @@ export let BrowserUsageTelemetry = {
case "TabGroupExpand":
this._onTabGroupExpandOrCollapse();
break;
case "TabGroupRemoveRequested":
this._onTabGroupRemoveRequested(event);
break;
case "TabGroupSaved":
this._onTabGroupSave(event);
break;
@ -1147,7 +1151,7 @@ export let BrowserUsageTelemetry = {
win.addEventListener("TabOpen", this, true);
win.addEventListener("TabPinned", this, true);
win.addEventListener("TabGroupCreate", this);
win.addEventListener("TabGroupRemoved", this);
win.addEventListener("TabGroupRemoveRequested", this);
win.addEventListener("TabGrouped", this);
win.addEventListener("TabUngrouped", this);
win.addEventListener("TabGroupCollapse", this);
@ -1166,7 +1170,7 @@ export let BrowserUsageTelemetry = {
win.removeEventListener("TabOpen", this, true);
win.removeEventListener("TabPinned", this, true);
win.removeEventListener("TabGroupCreate", this);
win.removeEventListener("TabGroupRemoved", this);
win.removeEventListener("TabGroupRemoveRequested", this);
win.removeEventListener("TabGrouped", this);
win.removeEventListener("TabUngrouped", this);
win.removeEventListener("TabGroupCollapse", this);
@ -1315,6 +1319,23 @@ export let BrowserUsageTelemetry = {
Glean.tabgroup.activeGroups.expanded.set(expanded);
},
/**
* @param {CustomEvent} event
*/
_onTabGroupRemoveRequested(event) {
let {
isUserTriggered = false,
telemetrySource = lazy.TabGroupMetrics.METRIC_SOURCE.UNKNOWN,
} = event.detail;
if (isUserTriggered) {
Glean.tabgroup.delete.record({
id: event.target.id,
source: telemetrySource,
});
}
},
/**
* Tracks the window count and registers the listeners for the tab count.
* @param{Object} win The window object.
@ -1370,7 +1391,7 @@ export let BrowserUsageTelemetry = {
tabCount !== undefined &&
currentTime > this._lastRecordTabCount + MINIMUM_TAB_COUNT_INTERVAL_MS
) {
Services.telemetry.getHistogramById("TAB_COUNT").add(tabCount);
Glean.browserEngagement.tabCount.accumulateSingleSample(tabCount);
this._lastRecordTabCount = currentTime;
}
@ -1379,9 +1400,9 @@ export let BrowserUsageTelemetry = {
currentTime >
this._lastRecordLoadedTabCount + MINIMUM_TAB_COUNT_INTERVAL_MS
) {
Services.telemetry
.getHistogramById("LOADED_TAB_COUNT")
.add(loadedTabCount);
Glean.browserEngagement.loadedTabCount.accumulateSingleSample(
loadedTabCount
);
this._lastRecordLoadedTabCount = currentTime;
}
},

View file

@ -385,13 +385,9 @@ function extractIconSize(aSizes) {
// Telemetry probes for measuring the sizes attribute
// usage and available dimensions.
Services.telemetry
.getHistogramById("LINK_ICON_SIZES_ATTR_USAGE")
.add(sizesType);
Glean.linkIconSizesAttr.usage.accumulateSingleSample(sizesType);
if (width > 0) {
Services.telemetry
.getHistogramById("LINK_ICON_SIZES_ATTR_DIMENSION")
.add(width);
Glean.linkIconSizesAttr.dimension.accumulateSingleSample(width);
}
return width;

View file

@ -666,7 +666,7 @@ export var ProcessHangMonitor = {
// On e10s this counts slow-script notice only once.
// This code is not reached on non-e10s.
Services.telemetry.getHistogramById("SLOW_SCRIPT_NOTICE_COUNT").add();
Glean.dom.slowScriptNoticeCount.add(1);
this._activeReports.set(report, {
deselectCount: 0,

View file

@ -338,6 +338,56 @@ browser.engagement:
unit: domains
telemetry_mirror: BROWSER_ENGAGEMENT_UNIQUE_DOMAINS_COUNT
tab_count:
type: custom_distribution
description: >
Number of tabs opened across all windows, collected at most every 5
minutes whenever the user interacts with the browser in the following
ways: open tab/window, page load.
This metric was generated to correspond to the Legacy Telemetry
exponential histogram TAB_COUNT.
range_min: 1
range_max: 1000
bucket_count: 100
histogram_type: exponential
unit: tabs
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1361855
- https://bugzilla.mozilla.org/show_bug.cgi?id=1488945
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1361855
- https://bugzilla.mozilla.org/show_bug.cgi?id=1488945
notification_emails:
- gijs@mozilla.com
expires: never
telemetry_mirror: TAB_COUNT
loaded_tab_count:
type: custom_distribution
description: >
Number of fully loaded (i.e., not pending from session restore) tabs
opened across all windows, collected at most every 5 minutes whenever the
user interacts with the browser in the following ways: open tab/window,
page load, restoring a pending tab.
This metric was generated to correspond to the Legacy Telemetry
exponential histogram LOADED_TAB_COUNT.
range_min: 1
range_max: 1000
bucket_count: 100
histogram_type: exponential
unit: tabs
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1634508
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1634508
notification_emails:
- beth@mozilla.com
- perf-telemetry-alerts@mozilla.com
expires: never
telemetry_mirror: LOADED_TAB_COUNT
installation.first_seen:
failure_reason:
type: string
@ -1567,3 +1617,47 @@ browser.content_crash:
- wmccloskey@mozilla.com
expires: never
telemetry_mirror: h#FX_CONTENT_CRASH_NOT_SUBMITTED
link_icon_sizes_attr:
usage:
type: custom_distribution
description: >
The possible types of the 'sizes' attribute for <link rel=icon>. 0:
Attribute not specified, 1: 'any', 2: Integer dimensions, 3: Invalid
value.
This metric was generated to correspond to the Legacy Telemetry enumerated
histogram LINK_ICON_SIZES_ATTR_USAGE.
range_min: 0
range_max: 4
bucket_count: 5
histogram_type: linear
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1053467
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1053467
notification_emails:
- fx-search-telemetry@mozilla.com
expires: never
telemetry_mirror: LINK_ICON_SIZES_ATTR_USAGE
dimension:
type: custom_distribution
description: >
The width dimension of the 'sizes' attribute for <link rel=icon>.
This metric was generated to correspond to the Legacy Telemetry linear
histogram LINK_ICON_SIZES_ATTR_DIMENSION.
range_min: 1
range_max: 513
bucket_count: 64
histogram_type: linear
unit: pixel
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1053467
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1053467
notification_emails:
- fx-search-telemetry@mozilla.com
expires: never
telemetry_mirror: LINK_ICON_SIZES_ATTR_DIMENSION

View file

@ -131,7 +131,7 @@ function assertVisibilityScalars(expected) {
] ?? {};
// Only some platforms have the menubar items.
if (AppConstants.MENUBAR_CAN_AUTOHIDE) {
if (!Services.appinfo.nativeMenubar) {
expected.push("menubar-items_pinned_menu-bar");
}

View file

@ -5,5 +5,3 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
JAR_MANIFESTS += ["jar.mn"]
DEFINES["MENUBAR_CAN_AUTOHIDE"] = 1

View file

@ -953,9 +953,14 @@
#tabbrowser-tabs[orient="vertical"] & {
width: var(--tab-group-line-thickness);
inset: -3px 0 -2px;
inset-block: -3px -2px;
inset-inline: 2px 0;
position: absolute;
#tabbrowser-tabs[expanded] & {
inset-inline-start: 0;
}
.tabbrowser-tab:last-of-type > .tab-stack > .tab-background > & {
inset-block-end: 0;
border-end-start-radius: calc(var(--tab-group-line-thickness) / 2);
@ -1056,8 +1061,8 @@ tab-group {
#tabbrowser-tabs[orient="vertical"] tab-group:not([collapsed]) > &::after {
width: var(--tab-group-line-thickness);
inset: 0;
inset-inline-end: auto;
inset-block: 0;
inset-inline: 2px auto;
border-start-start-radius: 1px;
border-start-end-radius: 1px;
}
@ -1569,10 +1574,6 @@ tab-group {
margin-inline: var(--tab-inner-inline-margin);
}
.tab-close-button {
margin-inline-end: calc(-1 * var(--tab-close-button-padding));
}
&:not([expanded]) {
.tabbrowser-tab[pinned] {
width: var(--tab-collapsed-width);
@ -1664,7 +1665,7 @@ tab-group {
max-width: none;
.tab-close-button {
margin-inline-end: calc(-1 * var(--tab-close-button-padding));
margin-inline-end: calc(var(--tab-close-button-padding) / -2);
}
&:not(:hover) .tab-close-button:not([selected]) {

View file

@ -25,43 +25,31 @@
}
}
@media (-moz-windows-accent-color-in-titlebar) or (-moz-windows-mica) {
:root[customtitlebar] {
@media (-moz-windows-mica) {
&:not([lwtheme]) {
background-color: transparent;
/* stylelint-disable-next-line media-query-no-invalid */
@media -moz-pref("widget.windows.mica.toplevel-backdrop", 2) {
/* For acrylic, do the same we do for popups to guarantee some contrast */
background-color: light-dark(rgba(255, 255, 255, .6), rgba(0, 0, 0, .6));
}
}
}
:root[customtitlebar]:not([lwtheme]) {
@media (-moz-windows-mica) {
background-color: transparent;
/* stylelint-disable-next-line media-query-no-invalid */
@media -moz-pref("browser.theme.windows.accent-color-in-tabs.enabled") {
&:not([lwtheme]) .browser-toolbox-background {
/* These colors match the Linux/HCM default button colors. We need to
* override these on the toolbox-like elements because the accent color
* is arbitrary, so the hardcoded brand colors from browser-colors.css
* might not provide sufficient contrast. */
--toolbarbutton-icon-fill: currentColor;
--toolbarbutton-hover-background: color-mix(in srgb, currentColor 17%, transparent);
--toolbarbutton-active-background: color-mix(in srgb, currentColor 30%, transparent);
--toolbar-field-color: currentColor;
--urlbar-box-bgcolor: var(--button-background-color-hover);
--urlbar-box-focus-bgcolor: var(--button-background-color);
--urlbar-box-hover-bgcolor: var(--button-background-color-hover);
--urlbar-box-active-bgcolor: var(--button-background-color-active);
}
@media -moz-pref("widget.windows.mica.toplevel-backdrop", 2) {
/* For acrylic, do the same we do for popups to guarantee some contrast */
background-color: light-dark(rgba(255, 255, 255, .6), rgba(0, 0, 0, .6));
}
}
&[sizemode="normal"] #navigator-toolbox {
border-top: .5px solid ActiveBorder;
&:-moz-window-inactive {
border-top-color: InactiveBorder;
}
/* stylelint-disable-next-line media-query-no-invalid */
@media -moz-pref("browser.theme.windows.accent-color-in-tabs.enabled") {
.browser-toolbox-background {
/* These colors match the Linux/HCM default button colors. We need to
* override these on the toolbox-like elements because the accent color
* is arbitrary, so the hardcoded brand colors from browser-colors.css
* might not provide sufficient contrast. */
--toolbarbutton-icon-fill: currentColor;
--toolbarbutton-hover-background: color-mix(in srgb, currentColor 17%, transparent);
--toolbarbutton-active-background: color-mix(in srgb, currentColor 30%, transparent);
--toolbar-field-color: currentColor;
--urlbar-box-bgcolor: var(--button-background-color-hover);
--urlbar-box-focus-bgcolor: var(--button-background-color);
--urlbar-box-hover-bgcolor: var(--button-background-color-hover);
--urlbar-box-active-bgcolor: var(--button-background-color-active);
}
}
}

View file

@ -5,5 +5,3 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
JAR_MANIFESTS += ["jar.mn"]
DEFINES["MENUBAR_CAN_AUTOHIDE"] = 1

View file

@ -185,6 +185,8 @@ fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and
["browser_dbg-editor-scroll.js"]
skip-if = ["cm5"]
["browser_dbg-editor-horizontal-scroll.js"]
["browser_dbg-editor-select.js"]
["browser_dbg-ember-original-variable-mapping-notifications.js"]

View file

@ -0,0 +1,180 @@
/* 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/>. */
// Tests that the editor scrolls correctly when pausing on location that
// requires horizontal scrolling.
"use strict";
add_task(async function testHorizontalScrolling() {
if (!isCm6Enabled) {
ok(true, "This test is disabled on CM5");
return;
}
// Ensure having the default fixed height, as it can impact the number of displayed lines
await pushPref("devtools.toolbox.footer.height", 250);
// Also set a precise size for side panels, as it can impact the number of displayed columns
await pushPref("devtools.debugger.start-panel-size", 300);
await pushPref("devtools.debugger.end-panel-size", 300);
// Strengthen the test by ensuring we always use the same Firefox window size.
// Note that the inner size is the important one as that's the final space available for DevTools.
// The outer size will be different based on OS/Environment.
const expectedWidth = 1280;
const expectedHeight = 1040;
if (
window.innerWidth != expectedWidth ||
window.innerHeight != expectedHeight
) {
info("Resize the top level window to match the expected size");
const onResize = once(window, "resize");
const deltaW = window.outerWidth - window.innerWidth;
const deltaH = window.outerHeight - window.innerHeight;
const originalWidth = window.outerWidth;
const originalHeight = window.outerHeight;
window.resizeTo(expectedWidth + deltaW, expectedHeight + deltaH);
await onResize;
registerCleanupFunction(() => {
window.resizeTo(originalWidth, originalHeight);
});
}
is(window.innerWidth, expectedWidth);
const dbg = await initDebugger(
"doc-editor-scroll.html",
"scroll.js",
"long.js"
);
await selectSource(dbg, "horizontal-scroll.js");
const editor = getCMEditor(dbg);
const global = editor.codeMirror.contentDOM.ownerGlobal;
const font = new global.FontFace(
"Ahem",
"url(chrome://mochitests/content/browser/devtools/client/debugger/test/mochitest/examples/Ahem.ttf)"
);
const loadedFont = await font.load();
global.document.fonts.add(loadedFont);
is(global.devicePixelRatio, 1);
is(global.browsingContext.top.window.devicePixelRatio, 1);
global.browsingContext.top.overrideDPPX = 1;
is(global.browsingContext.fullZoom, 1);
is(global.browsingContext.textZoom, 1);
// /!\ Change the Codemirror font to use a fixed font across all OSes
// and always have the same number of characters displayed.
// Note that this devtools mono makes the "o" characters almost invisible.
editor.codeMirror.contentDOM.style.fontFamily = "Ahem";
editor.codeMirror.contentDOM.style.fontSize = "10px";
editor.codeMirror.contentDOM.style.lineHeight = "15px";
editor.codeMirror.contentDOM.style.fontWeight = "normal";
editor.codeMirror.contentDOM.style.fontStyle = "normal";
editor.codeMirror.contentDOM.style.fontStretch = "normal";
is(global.getComputedStyle(editor.codeMirror.contentDOM).fontFamily, "Ahem");
await wait(1000);
is(
Math.round(editor.codeMirror.dom.getBoundingClientRect().width),
679,
"Sanity check to ensure we have a fixed editor width, so that we have the expected displayed columns"
);
// All the following methods lookup for first/last visible position in the current viewport.
// Also note that the element at the returned position may only be partially visible.
function getFirstVisibleColumn() {
const { x, y } = editor.codeMirror.dom.getBoundingClientRect();
const gutterWidth =
editor.codeMirror.dom.querySelector(".cm-gutters").clientWidth;
// This is hardcoded to match the second line, which is around 20px from the top.
// Also append the gutter width as it would pick hidden columns displayed behind it
const pos = editor.codeMirror.posAtCoords({
x: x + gutterWidth + 2,
y: y + 20,
});
// /!\ the column is 0-based while lines are 1-based
return pos - editor.codeMirror.state.doc.lineAt(pos).from;
}
function getLastVisibleColumn() {
const { x, y, width } = editor.codeMirror.dom.getBoundingClientRect();
// This is hardcoded to match the second line, which is around 20px from the top
const pos = editor.codeMirror.posAtCoords({ x: x + width, y: y + 20 });
// /!\ the column is 0-based while lines are 1-based
return pos - editor.codeMirror.state.doc.lineAt(pos).from;
}
info("Pause in middle of the screen, we should not scroll on pause");
await addBreakpoint(dbg, "horizontal-scroll.js", 2, 25);
invokeInTab("horizontal");
await waitForPaused(dbg);
const lastColumn = getLastVisibleColumn();
is(lastColumn, 55);
ok(
isScrolledPositionVisible(dbg, 2, 1),
"The 2nd line, first column is visible"
);
ok(
!isScrolledPositionVisible(dbg, 2, lastColumn),
"The 2nd line, last column is partially visible and considered hidden"
);
ok(
isScrolledPositionVisible(dbg, 2, lastColumn - 1),
"The column before the last column is visible"
);
info("Step to the last visible column, the editor shouldn't scroll");
// This breakpoint location is on the last visible column and would not cause a scroll.
await addBreakpoint(dbg, "horizontal-scroll.js", 2, lastColumn);
await resume(dbg);
await waitForPaused(dbg);
is(getLastVisibleColumn(), lastColumn, "We did not scroll horizontaly");
ok(
!isScrolledPositionVisible(dbg, 2, lastColumn),
"The last column is still considered hidden"
);
ok(
isScrolledPositionVisible(dbg, 2, lastColumn - 1),
"The column before the last colunm is still visible"
);
info(
"Step to the next column, and the editor should scroll it into the center"
);
info("Step into the next breakable column, the editor should now scroll");
// Set a breakpoint to the next breakable position (there is one every two columns, and lastColumn was breakable)
await addBreakpoint(dbg, "horizontal-scroll.js", 2, lastColumn + 2);
await resume(dbg);
await waitForPaused(dbg);
const lastColumn2 = getLastVisibleColumn();
is(lastColumn2, 74);
ok(
isScrolledPositionVisible(dbg, 2, lastColumn2),
"The new last column is visible"
);
ok(
!isScrolledPositionVisible(dbg, 2, lastColumn2 + 1),
"The column after the last is hidden"
);
const firstColumn = getFirstVisibleColumn();
is(firstColumn, 30);
ok(
!isScrolledPositionVisible(dbg, 2, firstColumn),
"The new first column is partially visible and considered hidden"
);
ok(
isScrolledPositionVisible(dbg, 2, firstColumn + 1),
"The column after the first visible is visible"
);
await resume(dbg);
});

View file

@ -89,6 +89,33 @@ add_task(async function testIsPositionVisible() {
// Ensure having the default fixed height, as it can impact the number of displayed lines
await pushPref("devtools.toolbox.footer.height", 250);
// Also set a precise size for side panels, as it can impact the number of displayed columns
await pushPref("devtools.debugger.start-panel-size", 300);
await pushPref("devtools.debugger.end-panel-size", 300);
// Strengthen the test by ensuring we always use the same Firefox window size.
// Note that the inner size is the important one as that's the final space available for DevTools.
// The outer size will be different based on OS/Environment.
const expectedWidth = 1280;
const expectedHeight = 1040;
if (
window.innerWidth != expectedWidth ||
window.innerHeight != expectedHeight
) {
info("Resize the top level window to match the expected size");
const onResize = once(window, "resize");
const deltaW = window.outerWidth - window.innerWidth;
const deltaH = window.outerHeight - window.innerHeight;
const originalWidth = window.outerWidth;
const originalHeight = window.outerHeight;
window.resizeTo(expectedWidth + deltaW, expectedHeight + deltaH);
await onResize;
registerCleanupFunction(() => {
window.resizeTo(originalWidth, originalHeight);
});
}
is(window.innerWidth, expectedWidth);
const dbg = await initDebugger(
"doc-editor-scroll.html",
"scroll.js",
@ -98,18 +125,20 @@ add_task(async function testIsPositionVisible() {
await selectSource(dbg, "scroll.js");
const editor = getCMEditor(dbg);
function getFirstLine() {
// All the following methods lookup for first/last visible position in the current viewport.
// Also note that the element at the returned position may only be partially visible.
function getFirstVisibleLine() {
const { x, y } = editor.codeMirror.dom.getBoundingClientRect();
// Add a pixel as we may be on the edge of the previous line which is hidden
const pos = editor.codeMirror.posAtCoords({ x, y: y + 1 });
return editor.codeMirror.state.doc.lineAt(pos).number;
}
function getLastLine() {
function getLastVisibleLine() {
const { x, y, height } = editor.codeMirror.dom.getBoundingClientRect();
const pos = editor.codeMirror.posAtCoords({ x, y: y + height });
return editor.codeMirror.state.doc.lineAt(pos).number;
}
const lastLine = getLastLine();
const lastLine = getLastVisibleLine();
is(
lastLine,
@ -145,13 +174,13 @@ add_task(async function testIsPositionVisible() {
await resume(dbg);
info(
"Set a breakpoint on the last partially visibible line, it should scroll that line in the middle of the viewport"
"Set a breakpoint on the last partially visible line, it should scroll that line in the middle of the viewport"
);
await addBreakpoint(dbg, "scroll.js", lastLine);
invokeInTab("line" + lastLine);
await waitForPaused(dbg);
const newLastLine = getLastLine();
const newLastLine = getLastVisibleLine();
is(newLastLine, 16, "The new last line is the 16th");
ok(
!isScrolledPositionVisible(dbg, newLastLine),
@ -161,7 +190,7 @@ add_task(async function testIsPositionVisible() {
isScrolledPositionVisible(dbg, newLastLine - 1),
"The line before is reported as visible"
);
const firstLine = getFirstLine();
const firstLine = getFirstVisibleLine();
is(firstLine, 6);
ok(
isScrolledPositionVisible(dbg, firstLine),
@ -181,7 +210,7 @@ add_task(async function testIsPositionVisible() {
invokeInTab("line50");
await waitForPaused(dbg);
const newLastLine2 = getLastLine();
const newLastLine2 = getLastVisibleLine();
is(newLastLine2, 55);
ok(
!isScrolledPositionVisible(dbg, newLastLine2),
@ -191,7 +220,7 @@ add_task(async function testIsPositionVisible() {
isScrolledPositionVisible(dbg, newLastLine2 - 1),
"The line before is visible"
);
const firstLine2 = getFirstLine();
const firstLine2 = getFirstVisibleLine();
is(firstLine2, 45);
ok(
isScrolledPositionVisible(dbg, firstLine2),

View file

@ -10,6 +10,7 @@
<body>
<script src="scroll.js"></script>
<script src="horizontal-scroll.js"></script>
<script src="long.js"></script>
<script src="frames.js"></script>
</body>

View file

@ -0,0 +1,2 @@
let a="a",b="b",c="c",d="d",e="e",f="f",g="g",h="h",i="i",j="j",k="k",l="l",m="m",n="n",o="o",p="p",q="q",r="r",s="s",t="t",u="u",v="v",x="x",y="y",z="z";
function horizontal() { console.log("horizontal");a;b;c;d;e;f;g;h;i;j;k;l;m;n;o;p;q;r;s;t;u;v;x;y;z;a;b;c;d;e;f;g;h;i;j;k;l;m;n;o;p;q;r;s;t;u;v;x;y;z;a;b;c;d;e;f;g;h;i;j;k;l;m;n;o;p;q;r;s;t;u;v;x;y;z;a;b;c;d;e;f;g;h;i;j;k;l;m;n;o;p;q;r;s;t;u;v;x;y;z }

View file

@ -86,7 +86,7 @@ const PowerShell = {
parameters.push(`-Uri ${escapeStr(url)}`);
if (method !== "GET") {
parameters.push(`-Method ${method}`);
parameters.push(`-Method ${escapeStr(method)}`);
}
if (session.length) {

View file

@ -62,7 +62,7 @@ Invoke-WebRequest -UseBasicParsing -Uri "https://example.com/browser/devtools/cl
$session.Cookies.Add((New-Object System.Net.Cookie("bob", "true", "/", "example.com")))
$session.Cookies.Add((New-Object System.Net.Cookie("tom", "cool", "/", "example.com")))
Invoke-WebRequest -UseBasicParsing -Uri "https://example.com/browser/devtools/client/netmonitor/test/sjs_simple-test-server.sjs" \`
-Method POST \`
-Method "POST" \`
-WebSession $session \`
-UserAgent "${navigator.userAgent}" \`
-Headers @{
@ -91,7 +91,7 @@ Invoke-WebRequest -UseBasicParsing -Uri "https://example.com/browser/devtools/cl
$session.Cookies.Add((New-Object System.Net.Cookie("bob", "true", "/", "example.com")))
$session.Cookies.Add((New-Object System.Net.Cookie("tom", "cool", "/", "example.com")))
Invoke-WebRequest -UseBasicParsing -Uri "https://example.com/browser/devtools/client/netmonitor/test/sjs_simple-test-server.sjs" \`
-Method POST \`
-Method "POST" \`
-WebSession $session \`
-UserAgent "${navigator.userAgent}" \`
-Headers @{

View file

@ -3458,13 +3458,18 @@ class Editor extends EventEmitter {
// `coordsAtPos` returns the absolute position of the line/column location
// so that we have to ensure comparing with same absolute position for
// CodeMirror DOM Element.
//
// Note that it may return the coordinates for a column breakpoint marker
// so it may still report as visible, if the marker is on the edge of the viewport
// and the displayed character at line/column is actually hidden after the scrollable area.
const coords = cm.coordsAtPos(pos);
if (!coords) {
return false;
}
const { x, y, width, height } = cm.dom.getBoundingClientRect();
const gutterWidth = cm.dom.querySelector(".cm-gutters").clientWidth;
inXView = withinBounds(coords.left - x, 0, width);
inXView = coords.left > x + gutterWidth && coords.right < x + width;
inYView = coords.top > y && coords.bottom < y + height;
} else {
const { top, left } = cm.charCoords({ line, ch: column }, "local");
@ -3548,6 +3553,7 @@ class Editor extends EventEmitter {
* @param {Number} line - The line in the source
* @param {Number} column - The column in the source
* @param {String|null} yAlign - Optional value for position of the line after the line is scrolled.
* (Used by `scrollEditorIntoView` test helper)
*/
async scrollTo(line, column, yAlign) {
if (this.isDestroyed()) {
@ -3566,7 +3572,7 @@ class Editor extends EventEmitter {
}
return cm.dispatch({
effects: EditorView.scrollIntoView(offset, {
x: "nearest",
x: "center",
y: yAlign || "center",
}),
});

View file

@ -1,10 +1,6 @@
How to submit a patch
=====================
+--------------------------------------------------------------------+
| This page is an import from MDN and the contents might be outdated |
+--------------------------------------------------------------------+
Submitting a patch, getting it reviewed, and committed to the Firefox
source tree involves several steps. This article explains how.
@ -72,9 +68,9 @@ the proposed change.
If module ownership is not clear, ask on the newsgroups or `on
Matrix <https://chat.mozilla.org>`__. The revision log for the relevant
file might also be helpful. For example, see the change log for
``browser/base/content/browser.js``, by clicking the "Hg Log"
``browser/base/content/browser.js``, by clicking the "Git Log"
link at the top of `Searchfox <https://searchfox.org/mozilla-central/source/>`__, or
by running ``hg log browser/base/content/browser.js``. The corresponding
by running ``git log browser/base/content/browser.js``. The corresponding
checkin message will contain something like "r=nickname", identifying
active code submissions, and potential code reviewers.

View file

@ -10,13 +10,13 @@ assembly views) by running the following commands:
- Firefox:
```
samply record PERF_SPEW_DIR=/tmp IONPERF=src MOZ_DISABLE_CONTENT_SANDBOX=1 MOZ_USE_PERFORMANCE_MARKER_FILE=1 JIT_OPTION_onlyInlineSelfHosted=true python3 ./mach run`
samply record PERF_SPEW_DIR=/tmp IONPERF=src MOZ_DISABLE_CONTENT_SANDBOX=1 MOZ_USE_PERFORMANCE_MARKER_FILE=1 JIT_OPTION_enableICFramePointers=true JIT_OPTION_onlyInlineSelfHosted=true JIT_OPTION_emitInterpreterEntryTrampoline=true python3 ./mach run
```
- JS shell:
```
samply record PERF_SPEW_DIR=/tmp IONPERF=src ~/code/obj-shell/dist/bin/js --enable-ic-frame-pointers --only-inline-selfhosted index.js`
samply record PERF_SPEW_DIR=/tmp IONPERF=src ~/code/obj-shell/dist/bin/js --enable-ic-frame-pointers --only-inline-selfhosted index.js
```
## Motivation

View file

@ -838,7 +838,10 @@ void Animation::CommitStyles(ErrorResult& aRv) {
UniquePtr<StyleAnimationValueMap> animationValues(
Servo_AnimationValueMap_Create());
if (!presContext->EffectCompositor()->ComposeServoAnimationRuleForEffect(
*keyframeEffect, CascadeLevel(), animationValues.get())) {
*keyframeEffect, CascadeLevel(), animationValues.get(),
StaticPrefs::dom_animations_commit_styles_endpoint_inclusive()
? EndpointBehavior::Inclusive
: EndpointBehavior::Exclusive)) {
NS_WARNING("Failed to compose animation style to commit");
return;
}
@ -1277,7 +1280,8 @@ void Animation::WillComposeStyle() {
void Animation::ComposeStyle(
StyleAnimationValueMap& aComposeResult,
const InvertibleAnimatedPropertyIDSet& aPropertiesToSkip) {
const InvertibleAnimatedPropertyIDSet& aPropertiesToSkip,
EndpointBehavior aEndpointBehavior) {
if (!mEffect) {
return;
}
@ -1331,7 +1335,8 @@ void Animation::ComposeStyle(
KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect();
if (keyframeEffect) {
keyframeEffect->ComposeStyle(aComposeResult, aPropertiesToSkip);
keyframeEffect->ComposeStyle(aComposeResult, aPropertiesToSkip,
aEndpointBehavior);
}
}

View file

@ -343,8 +343,10 @@ class Animation : public DOMEventTargetHelper,
* Any properties contained in |aPropertiesToSkip| will not be added or
* updated in |aComposeResult|.
*/
void ComposeStyle(StyleAnimationValueMap& aComposeResult,
const InvertibleAnimatedPropertyIDSet& aPropertiesToSkip);
void ComposeStyle(
StyleAnimationValueMap& aComposeResult,
const InvertibleAnimatedPropertyIDSet& aPropertiesToSkip,
EndpointBehavior aEndpointBehavior = EndpointBehavior::Exclusive);
void NotifyEffectTimingUpdated();
void NotifyEffectPropertiesUpdated();

View file

@ -102,7 +102,8 @@ void AnimationEffect::SetSpecifiedTiming(TimingParams&& aTiming) {
ComputedTiming AnimationEffect::GetComputedTimingAt(
const Nullable<TimeDuration>& aLocalTime, const TimingParams& aTiming,
double aPlaybackRate,
Animation::ProgressTimelinePosition aProgressTimelinePosition) {
Animation::ProgressTimelinePosition aProgressTimelinePosition,
EndpointBehavior aEndpointBehavior) {
static const StickyTimeDuration zeroDuration;
// Always return the same object to benefit from return-value optimization.
@ -143,8 +144,8 @@ ComputedTiming AnimationEffect::GetComputedTimingAt(
StickyTimeDuration activeAfterBoundary = aTiming.CalcActiveAfterBoundary();
if (localTime > activeAfterBoundary ||
(aPlaybackRate >= 0 && localTime == activeAfterBoundary &&
!atProgressTimelineBoundary)) {
(aEndpointBehavior == EndpointBehavior::Exclusive && aPlaybackRate >= 0 &&
localTime == activeAfterBoundary && !atProgressTimelineBoundary)) {
result.mPhase = ComputedTiming::AnimationPhase::After;
if (!result.FillsForwards()) {
// The animation isn't active or filling at this time.
@ -155,7 +156,8 @@ ComputedTiming AnimationEffect::GetComputedTimingAt(
result.mActiveDuration),
zeroDuration);
} else if (localTime < beforeActiveBoundary ||
(aPlaybackRate < 0 && localTime == beforeActiveBoundary &&
(aEndpointBehavior == EndpointBehavior::Exclusive &&
aPlaybackRate < 0 && localTime == beforeActiveBoundary &&
!atProgressTimelineBoundary)) {
result.mPhase = ComputedTiming::AnimationPhase::Before;
if (!result.FillsBackwards()) {
@ -266,14 +268,14 @@ ComputedTiming AnimationEffect::GetComputedTimingAt(
}
ComputedTiming AnimationEffect::GetComputedTiming(
const TimingParams* aTiming) const {
const TimingParams* aTiming, EndpointBehavior aEndpointBehavior) const {
const double playbackRate = mAnimation ? mAnimation->PlaybackRate() : 1;
const auto progressTimelinePosition =
mAnimation ? mAnimation->AtProgressTimelineBoundary()
: Animation::ProgressTimelinePosition::NotBoundary;
return GetComputedTimingAt(GetLocalTime(),
aTiming ? *aTiming : NormalizedTiming(),
playbackRate, progressTimelinePosition);
return GetComputedTimingAt(
GetLocalTime(), aTiming ? *aTiming : NormalizedTiming(), playbackRate,
progressTimelinePosition, aEndpointBehavior);
}
// Helper function for generating an (Computed)EffectTiming dictionary

View file

@ -80,10 +80,13 @@ class AnimationEffect : public nsISupports, public nsWrapperCache {
static ComputedTiming GetComputedTimingAt(
const Nullable<TimeDuration>& aLocalTime, const TimingParams& aTiming,
double aPlaybackRate,
Animation::ProgressTimelinePosition aProgressTimelinePosition);
Animation::ProgressTimelinePosition aProgressTimelinePosition,
EndpointBehavior aEndpointBehavior = EndpointBehavior::Exclusive);
// Shortcut that gets the computed timing using the current local time as
// calculated from the timeline time.
ComputedTiming GetComputedTiming(const TimingParams* aTiming = nullptr) const;
ComputedTiming GetComputedTiming(
const TimingParams* aTiming = nullptr,
EndpointBehavior aEndpointBehavior = EndpointBehavior::Exclusive) const;
virtual void SetAnimation(Animation* aAnimation) = 0;
Animation* GetAnimation() const { return mAnimation; };

View file

@ -25,6 +25,7 @@
#include "mozilla/StaticPrefs_layers.h"
#include "mozilla/StyleAnimationValue.h"
#include "mozilla/SVGObserverUtils.h"
#include "nsComputedDOMStyle.h"
#include "nsContentUtils.h"
#include "nsCSSPropertyIDSet.h"
#include "nsCSSProps.h"
@ -339,7 +340,9 @@ class EffectCompositeOrderComparator {
static void ComposeSortedEffects(
const nsTArray<KeyframeEffect*>& aSortedEffects,
const EffectSet* aEffectSet, EffectCompositor::CascadeLevel aCascadeLevel,
StyleAnimationValueMap* aAnimationValues) {
StyleAnimationValueMap* aAnimationValues,
dom::EndpointBehavior aEndpointBehavior =
dom::EndpointBehavior::Exclusive) {
const bool isTransition =
aCascadeLevel == EffectCompositor::CascadeLevel::Transitions;
InvertibleAnimatedPropertyIDSet propertiesToSkip;
@ -366,7 +369,8 @@ static void ComposeSortedEffects(
for (KeyframeEffect* effect : aSortedEffects) {
auto* animation = effect->GetAnimation();
MOZ_ASSERT(!isTransition || animation->CascadeLevel() == aCascadeLevel);
animation->ComposeStyle(*aAnimationValues, propertiesToSkip);
animation->ComposeStyle(*aAnimationValues, propertiesToSkip,
aEndpointBehavior);
}
}
@ -418,7 +422,8 @@ bool EffectCompositor::GetServoAnimationRule(
bool EffectCompositor::ComposeServoAnimationRuleForEffect(
KeyframeEffect& aEffect, CascadeLevel aCascadeLevel,
StyleAnimationValueMap* aAnimationValues) {
StyleAnimationValueMap* aAnimationValues,
dom::EndpointBehavior aEndpointBehavior) {
MOZ_ASSERT(aAnimationValues);
MOZ_ASSERT(mPresContext && mPresContext->IsDynamic(),
"Should not be in print preview");
@ -440,6 +445,16 @@ bool EffectCompositor::ComposeServoAnimationRuleForEffect(
// need to ensure the cascade results are up-to-date manually.
MaybeUpdateCascadeResults(target.mElement, target.mPseudoRequest);
// We may need to update the base styles cached on the keyframes for |aEffect|
// since they won't be updated as part of the regular animation processing if
// |aEffect| has finished but doesn't have an appropriate fill mode.
// We can get computed style without flush, because |CommitStyles| should have
// already flushed styles.
RefPtr<const ComputedStyle> style =
nsComputedDOMStyle::GetComputedStyleNoFlush(target.mElement,
target.mPseudoRequest);
aEffect.UpdateBaseStyle(style);
EffectSet* effectSet = EffectSet::Get(target);
// Get a list of effects sorted by composite order up to and including
@ -458,7 +473,7 @@ bool EffectCompositor::ComposeServoAnimationRuleForEffect(
sortedEffectList.AppendElement(&aEffect);
ComposeSortedEffects(sortedEffectList, effectSet, aCascadeLevel,
aAnimationValues);
aAnimationValues, aEndpointBehavior);
MOZ_ASSERT(effectSet == EffectSet::Get(target),
"EffectSet should not change while composing style");

View file

@ -16,6 +16,7 @@
#include "mozilla/PseudoElementHashEntry.h"
#include "mozilla/RefPtr.h"
#include "mozilla/ServoTypes.h"
#include "mozilla/dom/EndpointBehavior.h"
#include "nsCSSPropertyID.h"
#include "nsCycleCollectionParticipant.h"
#include "nsTHashMap.h"
@ -133,7 +134,9 @@ class EffectCompositor {
// committing the computed style of a removed Animation.
bool ComposeServoAnimationRuleForEffect(
dom::KeyframeEffect& aEffect, CascadeLevel aCascadeLevel,
StyleAnimationValueMap* aAnimationValues);
StyleAnimationValueMap* aAnimationValues,
dom::EndpointBehavior aEndpointBehavior =
dom::EndpointBehavior::Exclusive);
bool HasPendingStyleUpdates() const;

View file

@ -0,0 +1,16 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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/. */
#ifndef mozilla_dom_EndpointBehavior_h
#define mozilla_dom_EndpointBehavior_h
namespace mozilla::dom {
enum class EndpointBehavior : bool { Exclusive, Inclusive };
} // namespace mozilla::dom
#endif // mozilla_dom_EndpointBehavior_h

View file

@ -477,6 +477,10 @@ void KeyframeEffect::UpdateProperties(const ComputedStyle* aStyle,
RequestRestyle(EffectCompositor::RestyleType::Layer);
}
void KeyframeEffect::UpdateBaseStyle(const ComputedStyle* aStyle) {
EnsureBaseStyles(aStyle, BuildProperties(aStyle), nullptr, nullptr);
}
void KeyframeEffect::EnsureBaseStyles(
const ComputedStyle* aComputedValues,
const nsTArray<AnimationProperty>& aProperties,
@ -609,8 +613,9 @@ void KeyframeEffect::ComposeStyleRule(StyleAnimationValueMap& aAnimationValues,
void KeyframeEffect::ComposeStyle(
StyleAnimationValueMap& aComposeResult,
const InvertibleAnimatedPropertyIDSet& aPropertiesToSkip) {
ComputedTiming computedTiming = GetComputedTiming();
const InvertibleAnimatedPropertyIDSet& aPropertiesToSkip,
EndpointBehavior aEndpointBehavior) {
ComputedTiming computedTiming = GetComputedTiming(nullptr, aEndpointBehavior);
// If the progress is null, we don't have fill data for the current
// time so we shouldn't animate.

View file

@ -275,8 +275,10 @@ class KeyframeEffect : public AnimationEffect {
// Updates |aComposeResult| with the animation values produced by this
// AnimationEffect for the current time except any properties contained
// in |aPropertiesToSkip|.
void ComposeStyle(StyleAnimationValueMap& aComposeResult,
const InvertibleAnimatedPropertyIDSet& aPropertiesToSkip);
void ComposeStyle(
StyleAnimationValueMap& aComposeResult,
const InvertibleAnimatedPropertyIDSet& aPropertiesToSkip,
EndpointBehavior aEndpointBehavior = EndpointBehavior::Exclusive);
// Returns true if at least one property is being animated on compositor.
bool IsRunningOnCompositor() const;
@ -339,6 +341,8 @@ class KeyframeEffect : public AnimationEffect {
return result;
}
void UpdateBaseStyle(const ComputedStyle* aStyle);
enum class MatchForCompositor {
// This animation matches and should run on the compositor if possible.
Yes,

View file

@ -18,6 +18,7 @@ EXPORTS.mozilla.dom += [
"CSSPseudoElement.h",
"CSSTransition.h",
"DocumentTimeline.h",
"EndpointBehavior.h",
"KeyframeEffect.h",
"ScrollTimeline.h",
"ViewTimeline.h",

View file

@ -9,6 +9,7 @@
#include "mozilla/CycleCollectedJSRuntime.h"
#include "mozilla/ProfilerMarkers.h"
#include "mozilla/dom/ScriptSettings.h"
#include "mozilla/glean/DomMetrics.h"
#include "mozilla/PerfStats.h"
#include "nsRefreshDriver.h"
@ -227,7 +228,7 @@ void CCGCScheduler::NoteGCSliceEnd(TimeStamp aStart, TimeStamp aEnd) {
TimeDuration idleDuration = sliceDuration - nonIdleDuration;
uint32_t percent =
uint32_t(idleDuration.ToSeconds() / sliceDuration.ToSeconds() * 100);
Telemetry::Accumulate(Telemetry::GC_SLICE_DURING_IDLE, percent);
glean::dom::gc_slice_during_idle.AccumulateSingleSample(percent);
mTriggeredGCDeadline.reset();
}
@ -1024,8 +1025,8 @@ JS::SliceBudget CCGCScheduler::ComputeForgetSkippableBudget(
double duration =
(endPoint - mForgetSkippableFrequencyStartTime).ToSeconds() / 60;
uint32_t frequencyPerMinute = uint32_t(mForgetSkippableCounter / duration);
Telemetry::Accumulate(Telemetry::FORGET_SKIPPABLE_FREQUENCY,
frequencyPerMinute);
glean::dom::forget_skippable_frequency.AccumulateSingleSample(
frequencyPerMinute);
mForgetSkippableCounter = 0;
mForgetSkippableFrequencyStartTime = aStartTimeStamp;
}

View file

@ -130,7 +130,6 @@
#include "mozilla/StorageAccess.h"
#include "mozilla/StoragePrincipalHelper.h"
#include "mozilla/StyleSheet.h"
#include "mozilla/Telemetry.h"
#include "mozilla/TelemetryScalarEnums.h"
#include "mozilla/TextControlElement.h"
#include "mozilla/TextEditor.h"
@ -2277,8 +2276,8 @@ void Document::AccumulatePageLoadTelemetry(
TimeStamp asyncOpen;
timedChannel->GetAsyncOpen(&asyncOpen);
if (asyncOpen) {
Telemetry::AccumulateTimeDelta(Telemetry::DNS_PERF_FIRST_BYTE_MS, dnsKey,
asyncOpen, responseStart);
glean::perf::dns_first_byte.Get(dnsKey).AccumulateRawDuration(
responseStart - asyncOpen);
}
// First Contentful Composite
@ -2288,9 +2287,8 @@ void Document::AccumulatePageLoadTelemetry(
firstContentfulComposite - navigationStart);
if (!http3Key.IsEmpty()) {
Telemetry::AccumulateTimeDelta(
Telemetry::HTTP3_PERF_FIRST_CONTENTFUL_PAINT_MS, http3Key,
navigationStart, firstContentfulComposite);
glean::perf::http3_first_contentful_paint.Get(http3Key)
.AccumulateRawDuration(firstContentfulComposite - navigationStart);
#ifndef ANDROID
AccumulateHttp3FcpGleanPref(http3Key,
firstContentfulComposite - navigationStart);
@ -2298,18 +2296,16 @@ void Document::AccumulatePageLoadTelemetry(
}
if (!http3WithPriorityKey.IsEmpty()) {
Telemetry::AccumulateTimeDelta(
Telemetry::H3P_PERF_FIRST_CONTENTFUL_PAINT_MS, http3WithPriorityKey,
navigationStart, firstContentfulComposite);
glean::perf::h3p_first_contentful_paint.Get(http3WithPriorityKey)
.AccumulateRawDuration(firstContentfulComposite - navigationStart);
#ifndef ANDROID
AccumulatePriorityFcpGleanPref(
http3WithPriorityKey, firstContentfulComposite - navigationStart);
#endif
}
Telemetry::AccumulateTimeDelta(
Telemetry::DNS_PERF_FIRST_CONTENTFUL_PAINT_MS, dnsKey, navigationStart,
firstContentfulComposite);
glean::perf::dns_first_contentful_paint.Get(dnsKey).AccumulateRawDuration(
firstContentfulComposite - navigationStart);
glean::performance_pageload::fcp_responsestart.AccumulateRawDuration(
firstContentfulComposite - responseStart);
@ -2335,14 +2331,13 @@ void Document::AccumulatePageLoadTelemetry(
glean::performance_pageload::load_time.AccumulateRawDuration(
loadEventStart - navigationStart);
if (!http3Key.IsEmpty()) {
Telemetry::AccumulateTimeDelta(Telemetry::HTTP3_PERF_PAGE_LOAD_TIME_MS,
http3Key, navigationStart, loadEventStart);
glean::perf::http3_page_load_time.Get(http3Key).AccumulateRawDuration(
loadEventStart - navigationStart);
}
if (!http3WithPriorityKey.IsEmpty()) {
Telemetry::AccumulateTimeDelta(Telemetry::H3P_PERF_PAGE_LOAD_TIME_MS,
http3WithPriorityKey, navigationStart,
loadEventStart);
glean::perf::h3p_page_load_time.Get(http3WithPriorityKey)
.AccumulateRawDuration(loadEventStart - navigationStart);
}
glean::performance_pageload::load_time_responsestart.AccumulateRawDuration(
@ -16936,7 +16931,7 @@ void Document::MaybeRecomputePartitionKey() {
->SetPartitionKey(originURI, false);
}
bool Document::RecomputeResistFingerprinting() {
bool Document::RecomputeResistFingerprinting(bool aForceRefreshRTPCallerType) {
mOverriddenFingerprintingSettings.reset();
const bool previous = mShouldResistFingerprinting;
@ -17009,7 +17004,7 @@ bool Document::RecomputeResistFingerprinting() {
mShouldResistFingerprinting));
bool changed = previous != mShouldResistFingerprinting;
if (changed) {
if (changed || aForceRefreshRTPCallerType) {
if (auto win = nsGlobalWindowInner::Cast(GetInnerWindow())) {
win->RefreshReduceTimerPrecisionCallerType();
}
@ -18448,14 +18443,16 @@ Document::CreatePermissionGrantPromise(
inner, principal, aTopLevelBaseDomain, aFrameOnly,
// Allow
[p] {
Telemetry::AccumulateCategorical(
Telemetry::LABELS_STORAGE_ACCESS_API_UI::Allow);
glean::dom::storage_access_api_ui
.EnumGet(glean::dom::StorageAccessApiUiLabel::eAllow)
.Add();
p->Resolve(StorageAccessAPIHelper::eAllow, __func__);
},
// Block
[p] {
Telemetry::AccumulateCategorical(
Telemetry::LABELS_STORAGE_ACCESS_API_UI::Deny);
glean::dom::storage_access_api_ui
.EnumGet(glean::dom::StorageAccessApiUiLabel::eDeny)
.Add();
p->Reject(false, __func__);
});
@ -18464,8 +18461,9 @@ Document::CreatePermissionGrantPromise(
if (pr == PromptResult::Pending) {
// We're about to show a prompt, record the request attempt
Telemetry::AccumulateCategorical(
Telemetry::LABELS_STORAGE_ACCESS_API_UI::Request);
glean::dom::storage_access_api_ui
.EnumGet(glean::dom::StorageAccessApiUiLabel::eRequest)
.Add();
}
bool isThirdPartyTracker =
@ -18496,9 +18494,10 @@ Document::CreatePermissionGrantPromise(
pr2 = PromptResult::Granted;
autoGrant = true;
Telemetry::AccumulateCategorical(
Telemetry::LABELS_STORAGE_ACCESS_API_UI::
AllowAutomatically);
glean::dom::storage_access_api_ui
.EnumGet(glean::dom::StorageAccessApiUiLabel::
eAllowautomatically)
.Add();
}
// If we can complete the permission request, do so.
@ -19369,8 +19368,8 @@ void Document::RecordNavigationTiming(ReadyState aReadyState) {
switch (aReadyState) {
case READYSTATE_LOADING:
if (!mDOMLoadingSet) {
Telemetry::AccumulateTimeDelta(Telemetry::TIME_TO_DOM_LOADING_MS,
startTime);
glean::performance_time::to_dom_loading.AccumulateRawDuration(
TimeStamp::Now() - startTime);
mDOMLoadingSet = true;
}
break;

View file

@ -4240,7 +4240,7 @@ class Document : public nsINode,
// Recompute the current resist fingerprinting state. Returns true when
// the state was changed.
bool RecomputeResistFingerprinting();
bool RecomputeResistFingerprinting(bool aForceRefreshRTPCallerType = false);
// Recompute the partitionKey for this document if needed. This is for
// handling the case where the principal of the document is changed during the

View file

@ -89,7 +89,7 @@ CallbackTimeoutHandler::CallbackTimeoutHandler(
JSContext* aCx, nsIGlobalObject* aGlobal, Function* aFunction,
nsTArray<JS::Heap<JS::Value>>&& aArguments)
: TimeoutHandler(aCx), mGlobal(aGlobal), mFunction(aFunction) {
mozilla::HoldJSObjects(this);
mozilla::HoldJSObjectsWithKey(this);
mArgs = std::move(aArguments);
}
@ -149,7 +149,7 @@ NS_IMPL_CYCLE_COLLECTING_RELEASE(CallbackTimeoutHandler)
void CallbackTimeoutHandler::ReleaseJSObjects() {
mArgs.Clear();
mozilla::DropJSObjects(this);
mozilla::DropJSObjectsWithKey(this);
}
bool CallbackTimeoutHandler::Call(const char* aExecutionReason) {

View file

@ -13,6 +13,7 @@
#include "nsCycleCollectionParticipant.h"
#include "nsString.h"
#include "mozilla/Attributes.h"
#include "mozilla/HoldDropJSObjects.h"
#include "mozilla/SourceLocation.h"
#include "mozilla/dom/FunctionBinding.h"
@ -21,7 +22,7 @@ namespace mozilla::dom {
/**
* Utility class for implementing nsITimeoutHandlers, designed to be subclassed.
*/
class TimeoutHandler : public nsISupports {
class TimeoutHandler : public nsISupports, public JSHolderBase {
public:
MOZ_CAN_RUN_SCRIPT virtual bool Call(const char* /* unused */);
// Append a UTF-8 string to aOutString that describes the callback function,

View file

@ -265,7 +265,7 @@ VisualViewport::VisualViewportScrollEvent::VisualViewportScrollEvent(
mPrevLayoutOffset(aPrevLayoutOffset) {
VVP_LOG("%p: Registering PostScroll on %p %p\n", aViewport, aPresContext,
aPresContext->RefreshDriver());
aPresContext->RefreshDriver()->PostVisualViewportScrollEvent(this);
aPresContext->RefreshDriver()->PostScrollEvent(this);
}
bool VisualViewport::VisualViewportScrollEvent::HasPresContext(

View file

@ -942,18 +942,20 @@ static constexpr nsLiteralCString kRfpPrefs[] = {
"privacy.fingerprintingProtection"_ns,
"privacy.fingerprintingProtection.pbmode"_ns,
"privacy.fingerprintingProtection.overrides"_ns,
"privacy.baselineFingerprintingProtection"_ns,
"privacy.baselineFingerprintingProtection.overrides"_ns,
};
static void RecomputeResistFingerprintingAllDocs(const char*, void*) {
AutoTArray<RefPtr<Document>, 64> allDocuments;
Document::GetAllInProcessDocuments(allDocuments);
for (auto& doc : allDocuments) {
if (doc->RecomputeResistFingerprinting()) {
if (auto* pc = doc->GetPresContext()) {
pc->MediaFeatureValuesChanged(
{MediaFeatureChangeReason::PreferenceChange},
MediaFeatureChangePropagation::JustThisDocument);
}
doc->RecomputeResistFingerprinting(
/* aForceRefreshRTPCallerType= */ true);
if (auto* pc = doc->GetPresContext()) {
pc->MediaFeatureValuesChanged(
{MediaFeatureChangeReason::PreferenceChange},
MediaFeatureChangePropagation::JustThisDocument);
}
}
}
@ -2399,11 +2401,8 @@ bool nsContentUtils::ShouldResistFingerprinting(nsIGlobalObject* aGlobalObject,
// Newer Should RFP Functions ----------------------------------
// Utilities ---------------------------------------------------
inline void LogDomainAndPrefList(const char* urlType,
const char* exemptedDomainsPrefName,
nsAutoCString& url, bool isExemptDomain) {
nsAutoCString list;
Preferences::GetCString(exemptedDomainsPrefName, list);
inline void LogDomainAndList(const char* urlType, nsAutoCString& list,
nsAutoCString& url, bool isExemptDomain) {
MOZ_LOG(nsContentUtils::ResistFingerprintingLog(), LogLevel::Debug,
("%s \"%s\" is %s the exempt list \"%s\"", urlType,
PromiseFlatCString(url).get(), isExemptDomain ? "in" : "NOT in",
@ -2437,23 +2436,8 @@ bool nsContentUtils::ETPSaysShouldNotResistFingerprinting(
// A positive return from this function should always be obeyed.
// A negative return means we should keep checking things.
// We do not want this check to apply to RFP, only to FPP
// There is one problematic combination of prefs; however:
// If RFP is enabled in PBMode only and FPP is enabled globally
// (so, in non-PBM mode) - we need to know if we're in PBMode or not.
// But that's kind of expensive and we'd like to avoid it if we
// don't have to, so special-case that scenario
if (StaticPrefs::privacy_fingerprintingProtection_DoNotUseDirectly() &&
!StaticPrefs::privacy_resistFingerprinting_DoNotUseDirectly() &&
StaticPrefs::privacy_resistFingerprinting_pbmode_DoNotUseDirectly()) {
if (aIsPBM) {
// In PBM (where RFP is enabled) do not exempt based on the ETP toggle
return false;
}
} else if (StaticPrefs::privacy_resistFingerprinting_DoNotUseDirectly() ||
(aIsPBM &&
StaticPrefs::
privacy_resistFingerprinting_pbmode_DoNotUseDirectly())) {
// We do not want this check to apply to RFP, only to FPP.
if (nsRFPService::IsRFPPrefEnabled(aIsPBM)) {
// In RFP, never use the ETP toggle to exempt.
// We can safely return false here even if we are not in PBM mode
// and RFP_pbmode is enabled because we will later see that and
@ -2513,9 +2497,6 @@ inline bool SchemeSaysShouldNotResistFingerprinting(nsIPrincipal* aPrincipal) {
return !isContentAccessibleAboutURI;
}
const char* kExemptedDomainsPrefName =
"privacy.resistFingerprinting.exemptedDomains";
inline bool PartionKeyIsAlsoExempted(
const mozilla::OriginAttributes& aOriginAttributes) {
// If we've gotten here we have (probably) passed the CookieJarSettings
@ -2538,14 +2519,14 @@ inline bool PartionKeyIsAlsoExempted(
}
if (!NS_FAILED(rv)) {
bool isExemptPartitionKey =
nsContentUtils::IsURIInPrefList(uri, kExemptedDomainsPrefName);
nsAutoCString list;
nsRFPService::GetExemptedDomainsLowercase(list);
bool isExemptPartitionKey = nsContentUtils::IsURIInList(uri, list);
if (MOZ_LOG_TEST(nsContentUtils::ResistFingerprintingLog(),
mozilla::LogLevel::Debug)) {
nsAutoCString url;
uri->GetHost(url);
LogDomainAndPrefList("Partition Key", kExemptedDomainsPrefName, url,
isExemptPartitionKey);
LogDomainAndList("Partition Key", list, url, isExemptPartitionKey);
}
return isExemptPartitionKey;
}
@ -2723,19 +2704,6 @@ bool nsContentUtils::ShouldResistFingerprinting_dangerous(
" OriginAttributes) and the URI is %s",
aURI->GetSpecOrDefault().get()));
if (!StaticPrefs::privacy_resistFingerprinting_DoNotUseDirectly() &&
!StaticPrefs::privacy_fingerprintingProtection_DoNotUseDirectly()) {
// If neither of the 'regular' RFP prefs are set, then one (or both)
// of the PBM-Only prefs are set (or we would have failed the
// Positive return check.) Therefore, if we are not in PBM, return false
if (!aOriginAttributes.IsPrivateBrowsing()) {
MOZ_LOG(nsContentUtils::ResistFingerprintingLog(), LogLevel::Debug,
("Inside ShouldResistFingerprinting_dangerous(nsIURI*,"
" OriginAttributes) OA PBM Check said false"));
return false;
}
}
// Exclude internal schemes and web extensions
if (SchemeSaysShouldNotResistFingerprinting(aURI)) {
MOZ_LOG(nsContentUtils::ResistFingerprintingLog(), LogLevel::Debug,
@ -2746,15 +2714,14 @@ bool nsContentUtils::ShouldResistFingerprinting_dangerous(
bool isExemptDomain = false;
nsAutoCString list;
Preferences::GetCString(kExemptedDomainsPrefName, list);
ToLowerCase(list);
nsRFPService::GetExemptedDomainsLowercase(list);
isExemptDomain = IsURIInList(aURI, list);
if (MOZ_LOG_TEST(nsContentUtils::ResistFingerprintingLog(),
mozilla::LogLevel::Debug)) {
nsAutoCString url;
aURI->GetHost(url);
LogDomainAndPrefList("URI", kExemptedDomainsPrefName, url, isExemptDomain);
LogDomainAndList("URI", list, url, isExemptDomain);
}
if (isExemptDomain) {
@ -2811,14 +2778,15 @@ bool nsContentUtils::ShouldResistFingerprinting_dangerous(
}
bool isExemptDomain = false;
aPrincipal->IsURIInPrefList(kExemptedDomainsPrefName, &isExemptDomain);
nsAutoCString list;
nsRFPService::GetExemptedDomainsLowercase(list);
aPrincipal->IsURIInList(list, &isExemptDomain);
if (MOZ_LOG_TEST(nsContentUtils::ResistFingerprintingLog(),
mozilla::LogLevel::Debug)) {
nsAutoCString origin;
aPrincipal->GetOrigin(origin);
LogDomainAndPrefList("URI", kExemptedDomainsPrefName, origin,
isExemptDomain);
LogDomainAndList("URI", list, origin, isExemptDomain);
}
if (isExemptDomain) {

View file

@ -8,7 +8,6 @@
#include "GeckoProfiler.h"
#include "mozilla/ProfilerMarkers.h"
#include "mozilla/Telemetry.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/glean/DomMetrics.h"
#include "mozilla/dom/Document.h"
@ -437,8 +436,8 @@ void nsDOMNavigationTiming::NotifyContentfulCompositeForRootContentDocument(
"nsDOMNavigationTiming::TTITimeout");
if (mDocShellHasBeenActiveSinceNavigationStart) {
Telemetry::AccumulateTimeDelta(Telemetry::TIME_TO_FIRST_CONTENTFUL_PAINT_MS,
mNavigationStart, mContentfulComposite);
glean::performance_time::to_first_contentful_paint.AccumulateRawDuration(
mContentfulComposite - mNavigationStart);
}
}

View file

@ -87,7 +87,7 @@
#include "mozilla/StaticPrefs_privacy.h"
#include "mozilla/StorageAccess.h"
#include "mozilla/StoragePrincipalHelper.h"
#include "mozilla/Telemetry.h"
#include "mozilla/glean/DomMetrics.h"
#include "mozilla/TelemetryHistogramEnums.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/UniquePtr.h"
@ -1088,8 +1088,10 @@ nsGlobalWindowInner::~nsGlobalWindowInner() {
MOZ_LOG(gDOMLeakPRLogInner, LogLevel::Debug,
("DOMWINDOW %p destroyed", this));
Telemetry::Accumulate(Telemetry::INNERWINDOWS_WITH_MUTATION_LISTENERS,
mMutationBits ? 1 : 0);
glean::dom::innerwindows_with_mutation_listeners
.EnumGet(static_cast<glean::dom::InnerwindowsWithMutationListenersLabel>(
mMutationBits ? 1 : 0))
.Add();
// An inner window is destroyed, pull it out of the outer window's
// list if inner windows.
@ -1871,8 +1873,10 @@ void nsGlobalWindowInner::InitDocumentDependentState(JSContext* aCx) {
mLastOpenedURI = mDoc->GetDocumentURI();
#endif
Telemetry::Accumulate(Telemetry::INNERWINDOWS_WITH_MUTATION_LISTENERS,
mMutationBits ? 1 : 0);
glean::dom::innerwindows_with_mutation_listeners
.EnumGet(static_cast<glean::dom::InnerwindowsWithMutationListenersLabel>(
mMutationBits ? 1 : 0))
.Add();
// Clear our mutation bitfield.
mMutationBits = 0;
@ -3343,15 +3347,6 @@ bool nsGlobalWindowInner::DeviceSensorsEnabled(JSContext*, JSObject*) {
return Preferences::GetBool("device.sensors.enabled");
}
/* static */
bool nsGlobalWindowInner::CachesEnabled(JSContext* aCx, JSObject* aObj) {
if (!IsSecureContextOrObjectIsFromSecureContext(aCx, aObj)) {
return StaticPrefs::dom_caches_testing_enabled() ||
ServiceWorkersEnabled(aCx, aObj);
}
return true;
}
/* static */
bool nsGlobalWindowInner::IsGleanNeeded(JSContext* aCx, JSObject* aObj) {
// Glean is needed in ChromeOnly contexts and also in privileged about pages.
@ -5045,7 +5040,7 @@ nsGlobalWindowInner::ShowSlowScriptDialog(JSContext* aCx,
// Record the slow script event if we haven't done so already for this inner
// window (which represents a particular page to the user).
if (!mHasHadSlowScript) {
Telemetry::Accumulate(Telemetry::SLOW_SCRIPT_PAGE_COUNT, 1);
glean::dom::slow_script_page_count.Add(1);
}
mHasHadSlowScript = true;
@ -5082,7 +5077,7 @@ nsGlobalWindowInner::ShowSlowScriptDialog(JSContext* aCx,
// Reached only on non-e10s - once per slow script dialog.
// On e10s - we probe once at ProcessHangsMonitor.sys.mjs
Telemetry::Accumulate(Telemetry::SLOW_SCRIPT_NOTICE_COUNT, 1);
glean::dom::slow_script_notice_count.Add(1);
// Get the nsIPrompt interface from the docshell
nsCOMPtr<nsIDocShell> ds = GetDocShell();

View file

@ -425,8 +425,6 @@ class nsGlobalWindowInner final : public mozilla::dom::EventTarget,
static bool DeviceSensorsEnabled(JSContext*, JSObject*);
static bool CachesEnabled(JSContext* aCx, JSObject*);
// WebIDL permission Func for whether Glean APIs are permitted.
static bool IsGleanNeeded(JSContext*, JSObject*);
@ -1302,7 +1300,7 @@ class nsGlobalWindowInner final : public mozilla::dom::EventTarget,
// Represents whether the inner window's page has had a slow script notice.
// Only used by inner windows; will always be false for outer windows.
// This is used to implement Telemetry measures such as
// SLOW_SCRIPT_PAGE_COUNT.
// SLOW_SCRIPT_PAGE_COUNT (glean::dom::slow_script_page_count).
bool mHasHadSlowScript : 1;
// Fast way to tell if this is a chrome window (without having to QI).

Some files were not shown because too many files have changed in this diff Show more