commit 870365b83c1953a2e3fdab88423b2826fb58e6b7
parent 861a4f1e9468f8a99ad7e4ff2c08cacbec3371e4
Author: kpatenio <kpatenio@mozilla.com>
Date: Fri, 14 Nov 2025 19:21:40 +0000
Bug 1997411 — add a new site settings button in status card r=ip-protection-reviewers,fluent-reviewers,rking,bolsson
Differential Revision: https://phabricator.services.mozilla.com/D271163
Diffstat:
10 files changed, 372 insertions(+), 7 deletions(-)
diff --git a/browser/components/ipprotection/IPProtectionPanel.sys.mjs b/browser/components/ipprotection/IPProtectionPanel.sys.mjs
@@ -321,6 +321,7 @@ export class IPProtectionPanel {
doc.addEventListener("IPProtection:UserDisable", this.handleEvent);
doc.addEventListener("IPProtection:ShowHelpPage", this.handleEvent);
doc.addEventListener("IPProtection:SignIn", this.handleEvent);
+ doc.addEventListener("IPProtection:UserShowSiteSettings", this.handleEvent);
}
#removePanelListeners(doc) {
@@ -331,6 +332,10 @@ export class IPProtectionPanel {
doc.removeEventListener("IPProtection:UserDisable", this.handleEvent);
doc.removeEventListener("IPProtection:ShowHelpPage", this.handleEvent);
doc.removeEventListener("IPProtection:SignIn", this.handleEvent);
+ doc.removeEventListener(
+ "IPProtection:UserShowSiteSettings",
+ this.handleEvent
+ );
}
#addProxyListeners() {
@@ -399,6 +404,8 @@ export class IPProtectionPanel {
hasUpgraded: lazy.IPPEnrollAndEntitleManager.hasUpgraded,
error: hasError ? ERRORS.GENERIC : "",
});
+ } else if (event.type == "IPProtection:UserShowSiteSettings") {
+ // TODO: show subview for site settings (Bug 1997413)
}
}
}
diff --git a/browser/components/ipprotection/content/ipprotection-content.mjs b/browser/components/ipprotection/content/ipprotection-content.mjs
@@ -47,7 +47,7 @@ export default class IPProtectionContentElement extends MozLitElement {
this.keyListener = this.#keyListener.bind(this);
this.messageBarListener = this.#messageBarListener.bind(this);
- this.toggleListener = this.#toggleEventListener.bind(this);
+ this.statusCardListener = this.#statusCardListener.bind(this);
this._showMessageBar = false;
this._messageDismissed = false;
}
@@ -58,11 +58,15 @@ export default class IPProtectionContentElement extends MozLitElement {
this.addEventListener("keydown", this.keyListener, { capture: true });
this.addEventListener(
"ipprotection-status-card:user-toggled-on",
- this.#toggleEventListener
+ this.#statusCardListener
);
this.addEventListener(
"ipprotection-status-card:user-toggled-off",
- this.#toggleEventListener
+ this.#statusCardListener
+ );
+ this.addEventListener(
+ "ipprotection-site-settings-control:click",
+ this.#statusCardListener
);
this.addEventListener(
"ipprotection-message-bar:user-dismissed",
@@ -76,11 +80,15 @@ export default class IPProtectionContentElement extends MozLitElement {
this.removeEventListener("keydown", this.keyListener, { capture: true });
this.removeEventListener(
"ipprotection-status-card:user-toggled-on",
- this.#toggleEventListener
+ this.#statusCardListener
);
this.removeEventListener(
"ipprotection-status-card:user-toggled-off",
- this.#toggleEventListener
+ this.#statusCardListener
+ );
+ this.removeEventListener(
+ "ipprotection-site-settings-control:click",
+ this.#statusCardListener
);
this.removeEventListener(
"ipprotection-message-bar:user-dismissed",
@@ -160,7 +168,7 @@ export default class IPProtectionContentElement extends MozLitElement {
}
}
- #toggleEventListener(event) {
+ #statusCardListener(event) {
if (event.type === "ipprotection-status-card:user-toggled-on") {
this.dispatchEvent(
new CustomEvent("IPProtection:UserEnable", { bubbles: true })
@@ -169,6 +177,10 @@ export default class IPProtectionContentElement extends MozLitElement {
this.dispatchEvent(
new CustomEvent("IPProtection:UserDisable", { bubbles: true })
);
+ } else if (event.type === "ipprotection-site-settings-control:click") {
+ this.dispatchEvent(
+ new CustomEvent("IPProtection:UserShowSiteSettings", { bubbles: true })
+ );
}
}
@@ -225,12 +237,15 @@ export default class IPProtectionContentElement extends MozLitElement {
}
statusCardTemplate() {
+ // TODO: Pass site information to status-card to conditionally
+ // render the site settings control. (Bug 1997412)
return html`
<ipprotection-status-card
.protectionEnabled=${this.canEnableConnection}
.canShowTime=${this.canShowConnectionTime}
.enabledSince=${this.state.protectionEnabledSince}
.location=${this.state.location}
+ .siteData=${ifDefined(this.state.siteData)}
></ipprotection-status-card>
`;
}
diff --git a/browser/components/ipprotection/content/ipprotection-site-settings-control.css b/browser/components/ipprotection/content/ipprotection-site-settings-control.css
@@ -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 https://mozilla.org/MPL/2.0/. */
+
+@import "chrome://global/skin/global.css";
+
+#site-settings-label {
+ --box-border-radius-start: initial;
+}
+
+.site-settings {
+ --box-group-border: none;
+ background-color: var(--background-color-box);
+}
+
+.exception-enabled {
+ --box-icon-fill: var(--icon-color-success);
+}
diff --git a/browser/components/ipprotection/content/ipprotection-site-settings-control.mjs b/browser/components/ipprotection/content/ipprotection-site-settings-control.mjs
@@ -0,0 +1,90 @@
+/* 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 https://mozilla.org/MPL/2.0/. */
+
+import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+import { html, classMap } from "chrome://global/content/vendor/lit.all.mjs";
+
+/**
+ * Custom element that implements a button for site settings in the panel.
+ */
+export default class IPPSiteSettingsControl extends MozLitElement {
+ CLICK_EVENT = "ipprotection-site-settings-control:click";
+
+ static properties = {
+ site: { type: String },
+ exceptionEnabled: { type: Boolean },
+ };
+
+ constructor() {
+ super();
+
+ this.site = null;
+ this.exceptionEnabled = false;
+ }
+
+ get iconsrc() {
+ if (!this.exceptionEnabled) {
+ return "chrome://global/skin/icons/close-fill.svg";
+ }
+ return "chrome://global/skin/icons/check-filled.svg";
+ }
+
+ get descriptionL10n() {
+ if (!this.exceptionEnabled) {
+ return "ipprotection-site-settings-button-vpn-off";
+ }
+ return "ipprotection-site-settings-button-vpn-on";
+ }
+
+ handleClickSettings(event) {
+ event.preventDefault();
+
+ this.dispatchEvent(
+ new CustomEvent(this.CLICK_EVENT, {
+ bubbles: true,
+ composed: true,
+ })
+ );
+ }
+
+ render() {
+ if (!this.site) {
+ return null;
+ }
+
+ let icon = this.iconsrc;
+ let descriptionL10n = this.descriptionL10n;
+ let l10nArgs = JSON.stringify({ sitename: this.site });
+
+ return html`
+ <link
+ rel="stylesheet"
+ href="chrome://browser/content/ipprotection/ipprotection-site-settings-control.css"
+ />
+ <moz-box-group>
+ <moz-box-item
+ slot="header"
+ id="site-settings-label"
+ class="site-settings"
+ data-l10n-id="ipprotection-site-settings-control"
+ ></moz-box-item>
+ <moz-box-button
+ @click=${this.handleClickSettings}
+ class=${classMap({
+ "site-settings": true,
+ "exception-enabled": this.exceptionEnabled,
+ })}
+ data-l10n-id=${descriptionL10n}
+ data-l10n-args=${l10nArgs}
+ iconsrc=${icon}
+ ></moz-box-button>
+ </moz-box-group>
+ `;
+ }
+}
+
+customElements.define(
+ "ipprotection-site-settings-control",
+ IPPSiteSettingsControl
+);
diff --git a/browser/components/ipprotection/content/ipprotection-status-card.css b/browser/components/ipprotection/content/ipprotection-status-card.css
@@ -34,3 +34,7 @@
#connection-toggle {
margin-inline-end: var(--space-small);
}
+
+#site-settings {
+ --box-group-border: none;
+}
diff --git a/browser/components/ipprotection/content/ipprotection-status-card.mjs b/browser/components/ipprotection/content/ipprotection-status-card.mjs
@@ -17,6 +17,8 @@ import {
import "chrome://browser/content/ipprotection/ipprotection-header.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://global/content/elements/moz-toggle.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "chrome://browser/content/ipprotection/ipprotection-site-settings-control.mjs";
/**
* Custom element that implements a status card for IP protection.
@@ -29,6 +31,7 @@ export default class IPProtectionStatusCard extends MozLitElement {
statusGroupEl: "#status-card",
connectionToggleEl: "#connection-toggle",
locationEl: "#location-wrapper",
+ siteSettingsEl: "ipprotection-site-settings-control",
};
static shadowRootOptions = {
@@ -41,6 +44,7 @@ export default class IPProtectionStatusCard extends MozLitElement {
canShowTime: { type: Boolean },
enabledSince: { type: Object },
location: { type: Object },
+ siteData: { type: Object },
// Track toggle state separately so that we can tell when the toggle
// is enabled because of the existing protection state or because of user action.
_toggleEnabled: { type: Boolean, state: true },
@@ -138,7 +142,9 @@ export default class IPProtectionStatusCard extends MozLitElement {
? "ipprotection-toggle-active"
: "ipprotection-toggle-inactive";
- // TODO: add site settings button as a slotted element (class="slotted") in a moz-boz-item (Bug 1997411)
+ const siteSettingsTemplate = this.protectionEnabled
+ ? this.siteSettingsTemplate()
+ : null;
return html` <link
rel="stylesheet"
@@ -162,9 +168,31 @@ export default class IPProtectionStatusCard extends MozLitElement {
slot="actions"
></moz-toggle>
</moz-box-item>
+ ${siteSettingsTemplate}
</moz-box-group>`;
}
+ siteSettingsTemplate() {
+ // TODO: Once we're able to detect the current site and its exception status, show
+ // ipprotection-site-settings-control (Bug 1997412).
+ if (!this.siteData?.siteName) {
+ return null;
+ }
+
+ return html` <moz-box-item
+ id="site-settings"
+ class=${classMap({
+ "is-enabled": this.protectionEnabled,
+ })}
+ >
+ <ipprotection-site-settings-control
+ .site=${this.siteData.siteName}
+ .exceptionEnabled=${this.siteData.isException}
+ class="slotted"
+ ></ipprotection-site-settings-control>
+ </moz-box-item>`;
+ }
+
cardDescriptionTemplate() {
// The template consists of location name and connection time.
let time = this.canShowTime
diff --git a/browser/components/ipprotection/jar.mn b/browser/components/ipprotection/jar.mn
@@ -13,6 +13,8 @@ browser.jar:
content/browser/ipprotection/ipprotection-header.mjs (content/ipprotection-header.mjs)
content/browser/ipprotection/ipprotection-message-bar.mjs (content/ipprotection-message-bar.mjs)
content/browser/ipprotection/ipprotection-signedout.mjs (content/ipprotection-signedout.mjs)
+ content/browser/ipprotection/ipprotection-site-settings-control.css (content/ipprotection-site-settings-control.css)
+ content/browser/ipprotection/ipprotection-site-settings-control.mjs (content/ipprotection-site-settings-control.mjs)
content/browser/ipprotection/ipprotection-status-card.css (content/ipprotection-status-card.css)
content/browser/ipprotection/ipprotection-status-card.mjs (content/ipprotection-status-card.mjs)
content/browser/ipprotection/ipprotection-timer.mjs (content/ipprotection-timer.mjs)
diff --git a/browser/components/ipprotection/tests/browser/browser.toml b/browser/components/ipprotection/tests/browser/browser.toml
@@ -34,6 +34,8 @@ prefs = [
["browser_ipprotection_proxy_errors.js"]
+["browser_ipprotection_site_settings_control.js"]
+
["browser_ipprotection_status_card.js"]
["browser_ipprotection_telemetry.js"]
diff --git a/browser/components/ipprotection/tests/browser/browser_ipprotection_site_settings_control.js b/browser/components/ipprotection/tests/browser/browser_ipprotection_site_settings_control.js
@@ -0,0 +1,184 @@
+/* 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/. */
+
+"use strict";
+
+const MOCK_SITE_NAME = "example.com";
+
+/**
+ * Tests that we can show ipprotection-site-settings-control when VPN is on.
+ */
+add_task(
+ async function test_site_settings_control_visible_with_VPN_on_and_site_data() {
+ let content = await openPanel({
+ isSignedOut: false,
+ isProtectionEnabled: false,
+ siteData: {
+ siteName: MOCK_SITE_NAME,
+ isException: false,
+ },
+ });
+
+ Assert.ok(
+ BrowserTestUtils.isVisible(content),
+ "ipprotection content component should be present"
+ );
+
+ let statusCard = content.statusCardEl;
+ Assert.ok(
+ content.statusCardEl,
+ "ipprotection-status-card should be present"
+ );
+
+ // VPN is off, no site settings
+ Assert.ok(
+ !statusCard.siteSettingsEl,
+ "ipprotection-site-settings-control should not be present with VPN off"
+ );
+
+ let siteSettingsVisiblePromise = BrowserTestUtils.waitForMutationCondition(
+ statusCard.shadowRoot,
+ { childList: true, subtree: true },
+ () => statusCard.siteSettingsEl
+ );
+
+ // Turn VPN on
+ await setPanelState({
+ isSignedOut: false,
+ isProtectionEnabled: true,
+ siteData: {
+ siteName: MOCK_SITE_NAME,
+ isException: false,
+ },
+ });
+
+ await Promise.all([statusCard.updateComplete, siteSettingsVisiblePromise]);
+
+ Assert.ok(
+ statusCard.siteSettingsEl,
+ "Now ipprotection-site-settings-control should be present with VPN on"
+ );
+
+ await closePanel();
+ }
+);
+
+/**
+ * Tests that we don't show ipprotection-site-settings-control when there's no siteData.
+ */
+add_task(
+ async function test_site_settings_control_hidden_with_VPN_on_and_no_site_data() {
+ let content = await openPanel({
+ isSignedOut: false,
+ isProtectionEnabled: true,
+ siteData: undefined,
+ });
+
+ Assert.ok(
+ BrowserTestUtils.isVisible(content),
+ "ipprotection content component should be present"
+ );
+
+ let statusCard = content.statusCardEl;
+ Assert.ok(
+ content.statusCardEl,
+ "ipprotection-status-card should be present"
+ );
+ Assert.ok(
+ !statusCard.siteSettingsEl,
+ "ipprotection-site-settings-control should not be present because there's no site data"
+ );
+
+ await closePanel();
+ }
+);
+
+/**
+ * Tests that we don't show ipprotection-site-settings-control when an error occurs.
+ */
+add_task(async function test_site_settings_control_hidden_with_VPN_error() {
+ let content = await openPanel({
+ isSignedOut: false,
+ isProtectionEnabled: true,
+ siteData: {
+ siteName: MOCK_SITE_NAME,
+ isException: false,
+ },
+ });
+
+ Assert.ok(
+ BrowserTestUtils.isVisible(content),
+ "ipprotection content component should be present"
+ );
+
+ let statusCard = content.statusCardEl;
+ Assert.ok(content.statusCardEl, "ipprotection-status-card should be present");
+ Assert.ok(
+ statusCard.siteSettingsEl,
+ "ipprotection-site-settings-control should be present"
+ );
+
+ let siteSettingsNotVisiblePromise = BrowserTestUtils.waitForMutationCondition(
+ statusCard.shadowRoot,
+ { childList: true, subtree: true },
+ () => !statusCard.siteSettingsEl
+ );
+
+ // Turn VPN on
+ await setPanelState({
+ isSignedOut: false,
+ isProtectionEnabled: true,
+ error: "generic-error",
+ });
+
+ await Promise.all([statusCard.updateComplete, siteSettingsNotVisiblePromise]);
+
+ Assert.ok(
+ !statusCard.siteSettingsEl,
+ "Now ipprotection-site-settings-control should not be present due to an error"
+ );
+
+ await closePanel();
+});
+
+/**
+ * Tests that we dispatch expected events when we select the button
+ * in ipprotection-site-settings-control.
+ */
+add_task(async function test_site_settings_control_click_event() {
+ let content = await openPanel({
+ isSignedOut: false,
+ isProtectionEnabled: true,
+ siteData: {
+ siteName: MOCK_SITE_NAME,
+ isException: false,
+ },
+ });
+
+ Assert.ok(
+ BrowserTestUtils.isVisible(content),
+ "ipprotection content component should be present"
+ );
+
+ let statusCard = content.statusCardEl;
+ Assert.ok(content.statusCardEl, "ipprotection-status-card should be present");
+ Assert.ok(
+ statusCard.siteSettingsEl,
+ "ipprotection-site-settings-control should be present"
+ );
+
+ let button =
+ statusCard.siteSettingsEl.shadowRoot.querySelector("moz-box-button");
+ let eventPromise = BrowserTestUtils.waitForEvent(
+ window,
+ "IPProtection:UserShowSiteSettings"
+ );
+
+ button.click();
+
+ await eventPromise;
+ Assert.ok("Event was dispatched after clicking site settings button");
+
+ await closePanel();
+});
diff --git a/browser/locales-preview/ipProtection.ftl b/browser/locales-preview/ipProtection.ftl
@@ -62,6 +62,21 @@ ipprotection-connection-time = { $time }
ipprotection-location-title =
.title = Location selected based on fastest server
+ipprotection-site-settings-control =
+ .label = Website settings
+
+# Variables:
+# $sitename (String) - The name of the site that we're currently on (eg. example.com)
+ipprotection-site-settings-button-vpn-off =
+ .label = { $sitename }
+ .description = VPN is off
+
+# Variables:
+# $sitename (String) - The name of the site that we're currently on (eg. example.com)
+ipprotection-site-settings-button-vpn-on =
+ .label = { $sitename }
+ .description = VPN is on
+
# When VPN is toggled on
ipprotection-toggle-active =
.aria-label = Turn VPN off