Update On Fri Apr 4 20:23:47 CEST 2025
This commit is contained in:
parent
3bebcd781f
commit
c3221dad80
1087 changed files with 31424 additions and 50762 deletions
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
@ -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")) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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", {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)",
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 },
|
||||
},
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -360,7 +360,7 @@ var CustomizableUIInternal = {
|
|||
true
|
||||
);
|
||||
|
||||
if (AppConstants.MENUBAR_CAN_AUTOHIDE) {
|
||||
if (!Services.appinfo.nativeMenubar) {
|
||||
this.registerArea(
|
||||
CustomizableUI.AREA_MENUBAR,
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"]
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
[DEFAULT]
|
||||
tags = "local"
|
||||
|
||||
["test_default_launcher_visible.py"]
|
||||
["test_initialize_vertical_tabs.py"]
|
||||
|
|
|
@ -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",
|
||||
)
|
17000
browser/components/storybook/package-lock.json
generated
17000
browser/components/storybook/package-lock.json
generated
File diff suppressed because it is too large
Load diff
18
browser/components/tabbrowser/TabGroupMetrics.sys.mjs
Normal file
18
browser/components/tabbrowser/TabGroupMetrics.sys.mjs
Normal 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,
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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: >
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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([
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
? {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -61,9 +61,6 @@
|
|||
#ifdef MOZ_UPDATER
|
||||
@RESPATH@/updater.ini
|
||||
#endif
|
||||
#if defined(MOZ_UPDATE_AGENT)
|
||||
@RESPATH@/locale.ini
|
||||
#endif
|
||||
|
||||
[xpcom]
|
||||
@RESPATH@/dependentlibs.list
|
||||
|
|
|
@ -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 = It’s unsafe
|
||||
newtab-report-ads-reason-inappropriate =
|
||||
.label = It’s inappropriate
|
||||
newtab-report-ads-reason-seen-it-too-many-times =
|
||||
.label = I’ve seen it too many times
|
||||
newtab-report-ads-reason-not-interested =
|
||||
.label = Not interested
|
||||
newtab-report-submit = Submit
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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);
|
||||
});
|
|
@ -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),
|
||||
|
|
BIN
devtools/client/debugger/test/mochitest/examples/Ahem.ttf
Normal file
BIN
devtools/client/debugger/test/mochitest/examples/Ahem.ttf
Normal file
Binary file not shown.
|
@ -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>
|
||||
|
|
|
@ -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 }
|
|
@ -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) {
|
||||
|
|
|
@ -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 @{
|
||||
|
|
|
@ -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",
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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; };
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
16
dom/animation/EndpointBehavior.h
Normal file
16
dom/animation/EndpointBehavior.h
Normal 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
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -18,6 +18,7 @@ EXPORTS.mozilla.dom += [
|
|||
"CSSPseudoElement.h",
|
||||
"CSSTransition.h",
|
||||
"DocumentTimeline.h",
|
||||
"EndpointBehavior.h",
|
||||
"KeyframeEffect.h",
|
||||
"ScrollTimeline.h",
|
||||
"ViewTimeline.h",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue