commit 861a4f1e9468f8a99ad7e4ff2c08cacbec3371e4 parent 7bd13722128e1a5348212e5ddcad9ae6dc0afe26 Author: kpatenio <kpatenio@mozilla.com> Date: Fri, 14 Nov 2025 19:21:40 +0000 Bug 1997406 — implement new status card designs for the ip protection panel r=fluent-reviewers,ip-protection-reviewers,bolsson,rking Differential Revision: https://phabricator.services.mozilla.com/D271156 Diffstat:
10 files changed, 124 insertions(+), 378 deletions(-)
diff --git a/browser/components/ipprotection/content/ipprotection-content.mjs b/browser/components/ipprotection/content/ipprotection-content.mjs @@ -12,8 +12,6 @@ import { // eslint-disable-next-line import/no-unassigned-import import "chrome://browser/content/ipprotection/ipprotection-header.mjs"; // eslint-disable-next-line import/no-unassigned-import -import "chrome://browser/content/ipprotection/ipprotection-flag.mjs"; -// eslint-disable-next-line import/no-unassigned-import import "chrome://browser/content/ipprotection/ipprotection-message-bar.mjs"; // eslint-disable-next-line import/no-unassigned-import import "chrome://browser/content/ipprotection/ipprotection-signedout.mjs"; @@ -226,16 +224,6 @@ export default class IPProtectionContentElement extends MozLitElement { `; } - descriptionTemplate() { - return this.state.location - ? html` - <ipprotection-flag - .location=${this.state.location} - ></ipprotection-flag> - ` - : null; - } - statusCardTemplate() { return html` <ipprotection-status-card diff --git a/browser/components/ipprotection/content/ipprotection-flag.css b/browser/components/ipprotection/content/ipprotection-flag.css @@ -1,17 +0,0 @@ -/* 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"; - -#flag-wrapper { - display: flex; - align-items: center; - gap: var(--space-small); -} - -#location-icon { - height: var(--icon-size); - width: var(--icon-size); - pointer-events: none; -} diff --git a/browser/components/ipprotection/content/ipprotection-flag.mjs b/browser/components/ipprotection/content/ipprotection-flag.mjs @@ -1,66 +0,0 @@ -/* 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 } from "chrome://global/content/vendor/lit.all.mjs"; -import { FLAGS } from "chrome://browser/content/ipprotection/ipprotection-constants.mjs"; - -/** - * A custom element that handles the display of flag icons. - */ -export default class IPProtectionFlagElement extends MozLitElement { - static properties = { - location: { type: Object }, - _iconSrc: { type: String, state: true }, - _name: { type: String, state: true }, - }; - - constructor() { - super(); - } - - get #hasValidLocation() { - return this.location && this.location.name && this.location.code; - } - - #getFlagIcon() { - const iconName = this.location.code; - - if (!Object.hasOwn(FLAGS, iconName)) { - return null; - } - - return FLAGS[iconName]; - } - - locationDescriptionTemplate() { - return html` - <img id="location-icon" src=${this._iconSrc} /> - <span id="location-name">${this._name}</span> - `; - } - - render() { - if (!this.#hasValidLocation) { - return null; - } - - this._name = this.location.name; - this._iconSrc = this.#getFlagIcon(); - - let locationDescription = this._iconSrc - ? this.locationDescriptionTemplate() - : null; - - return html` - <link - rel="stylesheet" - href="chrome://browser/content/ipprotection/ipprotection-flag.css" - /> - <div id="flag-wrapper">${locationDescription}</div> - `; - } -} - -customElements.define("ipprotection-flag", IPProtectionFlagElement); diff --git a/browser/components/ipprotection/content/ipprotection-status-card.css b/browser/components/ipprotection/content/ipprotection-status-card.css @@ -5,14 +5,7 @@ @import "chrome://global/skin/global.css"; :host { - --status-card-connected-background-color: var(--color-violet-90); - --status-card-connected-text-color: var(--color-white); - --connection-globe-icon-size: 48px; - --connection-rings-extra-space-x: 23px; - --connection-rings-extra-space-y: 23px; - --connection-shield-color: var(--panel-background); - --connection-shield-off-background-color: var(--icon-color-critical); - --connection-shield-on-background-color: var(--color-accent-primary); + --border-color: var(--border-color-card); } #ipprotection-content-wrapper { @@ -22,11 +15,6 @@ padding-block: var(--panel-subview-body-padding-block); } -.vpn-top-content { - margin-inline: var(--space-large); - margin-block-start: var(--space-small); -} - .vpn-status-group { display: block; position: relative; @@ -35,76 +23,14 @@ } .is-enabled { - color: var(--status-card-connected-text-color); - background-color: var(--status-card-connected-background-color); + background-color: var(--background-color-success); } #status-card { - --shield-bgcolor: var(--connection-shield-off-background-color); - --box-icon-size: var(--connection-globe-icon-size); - --box-icon-fill: var(--connection-shield-color); - --box-icon-stroke: var(--shield-bgcolor); --box-label-font-weight: var(--font-weight-semibold); -moz-context-properties: fill, stroke; - /** - * moz-box-group changes the border of items based on its position in the group. - * Undo it to preserve the border radius even though the animation rings are - * loaded first in the group. - */ - --box-border-radius-start: initial; - fill: var(--connection-shield-color); - stroke: var(--shield-bgcolor); - - &.is-enabled { - --shield-bgcolor: var(--connection-shield-on-background-color); - } } #connection-toggle { - --toggle-width: var(--size-item-xlarge); - --toggle-height: var(--size-item-medium); - --toggle-dot-margin: 2px; margin-inline-end: var(--space-small); } - -#status-card-animation { - display: block; - position: absolute; - width: 100%; - height: 100%; - pointer-events: none; - z-index: 1; -} - -#animation-rings { - display: block; - width: 100%; - height: 100%; - contain: strict; - /** - * A hacky attempt at centering the rings with the globe icon. - * It makes some assumptions about the icon size and padding pixels used in moz-box-item. - * The rings are 200px in height and width, so they have a radius of 100px. - * Additional px gap is merely for alignment and not tied to any element. - * TODO: (Bug 1981251) See if we can better calculate the center coords. - */ - background-position-x: calc(-100px + var(--connection-globe-icon-size) + var(--space-large) - var(--connection-rings-extra-space-x)); - background-position-y: calc(-100px + var(--connection-globe-icon-size) + var(--space-large) - var(--connection-rings-extra-space-y)); - background-image: url("chrome://browser/content/ipprotection/assets/rings.svg"); - background-repeat: no-repeat; - - &:dir(rtl) { - --connection-rings-extra-space-x: 4.5px; - /* Shift by approximately 100% of the element's width to move the rings to the opposite side. */ - background-position-x: calc(100% + var(--connection-globe-icon-size) + var(--space-large) + var(--connection-rings-extra-space-x)); - } - - @media (prefers-reduced-motion: reduce) { - display: none; - } -} - -#location-wrapper { - --box-icon-fill: currentColor; - --box-label-font-weight: var(--font-weight-semibold); -} diff --git a/browser/components/ipprotection/content/ipprotection-status-card.mjs b/browser/components/ipprotection/content/ipprotection-status-card.mjs @@ -3,7 +3,11 @@ * 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"; +import { + html, + classMap, + styleMap, +} from "chrome://global/content/vendor/lit.all.mjs"; import { connectionTimer, defaultTimeValue, @@ -12,17 +16,17 @@ import { // eslint-disable-next-line import/no-unassigned-import import "chrome://browser/content/ipprotection/ipprotection-header.mjs"; // eslint-disable-next-line import/no-unassigned-import -import "chrome://browser/content/ipprotection/ipprotection-flag.mjs"; -// eslint-disable-next-line import/no-unassigned-import import "chrome://global/content/elements/moz-toggle.mjs"; /** * Custom element that implements a status card for IP protection. */ export default class IPProtectionStatusCard extends MozLitElement { + TOGGLE_ON_EVENT = "ipprotection-status-card:user-toggled-on"; + TOGGLE_OFF_EVENT = "ipprotection-status-card:user-toggled-off"; + static queries = { statusGroupEl: "#status-card", - animationEl: "#status-card-animation", connectionToggleEl: "#connection-toggle", locationEl: "#location-wrapper", }; @@ -37,9 +41,8 @@ export default class IPProtectionStatusCard extends MozLitElement { canShowTime: { type: Boolean }, enabledSince: { type: Object }, location: { type: Object }, - _showAnimation: { type: Boolean, state: true }, // Track toggle state separately so that we can tell when the toggle - // is enabled because of existing protection state or because of user action. + // is enabled because of the existing protection state or because of user action. _toggleEnabled: { type: Boolean, state: true }, }; @@ -47,7 +50,6 @@ export default class IPProtectionStatusCard extends MozLitElement { super(); this.keyListener = this.#keyListener.bind(this); - this._showAnimation = false; } connectedCallback() { @@ -67,14 +69,14 @@ export default class IPProtectionStatusCard extends MozLitElement { if (isEnabled) { this.dispatchEvent( - new CustomEvent("ipprotection-status-card:user-toggled-on", { + new CustomEvent(this.TOGGLE_ON_EVENT, { bubbles: true, composed: true, }) ); } else { this.dispatchEvent( - new CustomEvent("ipprotection-status-card:user-toggled-off", { + new CustomEvent(this.TOGGLE_OFF_EVENT, { bubbles: true, composed: true, }) @@ -126,89 +128,87 @@ export default class IPProtectionStatusCard extends MozLitElement { // (eg. error thrown), unset the toggle. this._toggleEnabled = false; } - - /** - * Don't show animations until all elements are connected and layout is fully drawn. - * This will allow us to best position our animation component with the globe icon - * based on the most up to date status card dimensions. - */ - if (this.protectionEnabled) { - this._showAnimation = true; - } else { - this._showAnimation = false; - } - } - - descriptionTemplate() { - return this.location - ? html` - <ipprotection-flag .location=${this.location}></ipprotection-flag> - ` - : null; - } - - animationRingsTemplate() { - return html` <div id="status-card-animation"> - <div id="animation-rings"></div> - </div>`; } - alphaCardTemplate() { + cardContentTemplate() { const statusCardL10nId = this.protectionEnabled ? "ipprotection-connection-status-on" : "ipprotection-connection-status-off"; const toggleL10nId = this.protectionEnabled ? "ipprotection-toggle-active" : "ipprotection-toggle-inactive"; - const statusIcon = this.protectionEnabled - ? "chrome://browser/content/ipprotection/assets/ipprotection-connection-on.svg" - : "chrome://browser/content/ipprotection/assets/ipprotection-connection-off.svg"; + // TODO: add site settings button as a slotted element (class="slotted") in a moz-boz-item (Bug 1997411) + + return html` <link + rel="stylesheet" + href="chrome://browser/content/ipprotection/ipprotection-status-card.css" + /> + <moz-box-group class="vpn-status-group"> + <moz-box-item + id="status-card" + class=${classMap({ + "is-enabled": this.protectionEnabled, + })} + layout="default" + data-l10n-id=${statusCardL10nId} + .description=${this.cardDescriptionTemplate()} + > + <moz-toggle + id="connection-toggle" + data-l10n-id=${toggleL10nId} + @click=${this.handleToggleConnect} + ?pressed=${this._toggleEnabled} + slot="actions" + ></moz-toggle> + </moz-box-item> + </moz-box-group>`; + } + + cardDescriptionTemplate() { + // The template consists of location name and connection time. let time = this.canShowTime ? connectionTimer(this.enabledSince) : defaultTimeValue; - return html` <moz-box-group class="vpn-status-group"> - ${this._showAnimation ? this.animationRingsTemplate() : null} - <moz-box-item - id="status-card" - class=${classMap({ - "is-enabled": this.protectionEnabled, - })} - layout="large-icon" - iconsrc=${statusIcon} - data-l10n-id=${statusCardL10nId} - data-l10n-args=${time} - > - <moz-toggle - id="connection-toggle" - data-l10n-id=${toggleL10nId} - @click=${this.handleToggleConnect} - ?pressed=${this._toggleEnabled} - slot="actions" - ></moz-toggle> - </moz-box-item> - <moz-box-item - id="location-wrapper" - class=${classMap({ - "is-enabled": this.protectionEnabled, - })} - iconsrc="chrome://global/skin/icons/info.svg" - data-l10n-id="ipprotection-location-title" - .description=${this.descriptionTemplate()} - > - </moz-box-item> - </moz-box-group>`; + // To work around mox-box-item description elements being hard to reach because of the shadowDOM, + // let's use a lit stylemap to apply style changes directly. + let labelStyles = styleMap({ + display: "flex", + gap: "var(--space-small)", + }); + let imgStyles = styleMap({ + "-moz-context-properties": "fill", + fill: "currentColor", + }); + + return this.location + ? html` + <div id="vpn-details"> + <div + id="location-label" + data-l10n-id="ipprotection-location-title" + style=${labelStyles} + > + <span>${this.location.name}</span> + <img + src="chrome://global/skin/icons/info.svg" + style=${imgStyles} + /> + </div> + <span + id="time" + data-l10n-id="ipprotection-connection-time" + data-l10n-args=${time} + ></span> + </div> + ` + : null; } render() { - return html` - <link - rel="stylesheet" - href="chrome://browser/content/ipprotection/ipprotection-status-card.css" - /> - ${this.alphaCardTemplate()} - `; + let content = this.cardContentTemplate(); + return html`${content}`; } } diff --git a/browser/components/ipprotection/jar.mn b/browser/components/ipprotection/jar.mn @@ -11,8 +11,6 @@ browser.jar: content/browser/ipprotection/ipprotection-customelements.js (content/ipprotection-customelements.js) content/browser/ipprotection/ipprotection-header.css (content/ipprotection-header.css) content/browser/ipprotection/ipprotection-header.mjs (content/ipprotection-header.mjs) - content/browser/ipprotection/ipprotection-flag.css (content/ipprotection-flag.css) - content/browser/ipprotection/ipprotection-flag.mjs (content/ipprotection-flag.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-status-card.css (content/ipprotection-status-card.css) diff --git a/browser/components/ipprotection/tests/browser/browser.toml b/browser/components/ipprotection/tests/browser/browser.toml @@ -22,8 +22,6 @@ prefs = [ ["browser_ipprotection_content_signedout.js"] -["browser_ipprotection_flag.js"] - ["browser_ipprotection_header.js"] ["browser_ipprotection_keyboard_navigation.js"] diff --git a/browser/components/ipprotection/tests/browser/browser_ipprotection_flag.js b/browser/components/ipprotection/tests/browser/browser_ipprotection_flag.js @@ -1,50 +0,0 @@ -/* 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"; - -/** - * Tests that ipprotection-flag has the correct content. - */ -add_task(async function test_flags_content() { - const mockLocation = { - name: "United States", - code: "us", - }; - - let content = await openPanel({ isSignedOut: false }); - - let cardLoadedPromise = BrowserTestUtils.waitForMutationCondition( - content.shadowRoot, - { childList: true, subtree: true }, - () => content.statusCardEl - ); - - await setPanelState({ - isSignedOut: false, - location: mockLocation, - }); - await cardLoadedPromise; - - let statusCard = content.statusCardEl; - - Assert.ok(statusCard.locationEl, "Location details should be present"); - - let flag = - statusCard.locationEl?.shadowRoot.querySelector("ipprotection-flag"); - - Assert.ok(flag, "Flag component should be present"); - - let icon = flag.shadowRoot.getElementById("location-icon"); - let name = flag.shadowRoot.getElementById("location-name"); - - Assert.ok(icon, "Location flag icon should be present"); - Assert.equal( - name.textContent, - mockLocation.name, - "Location name should be correct" - ); - - await closePanel(); -}); diff --git a/browser/components/ipprotection/tests/browser/browser_ipprotection_status_card.js b/browser/components/ipprotection/tests/browser/browser_ipprotection_status_card.js @@ -16,7 +16,7 @@ ChromeUtils.defineESModuleGetters(lazy, { }); /** - * Tests UI updates to the status card in the panel after enable/disable. + * Tests UI updates to the status card in the panel after enable/disables. */ add_task(async function test_status_card_in_panel() { const l10nIdOn = "ipprotection-connection-status-on"; @@ -47,27 +47,27 @@ add_task(async function test_status_card_in_panel() { l10nIdOff, "Status card connection toggle data-l10n-id should be correct by default" ); - Assert.equal( - statusCard?.statusGroupEl.description, - "", - "Time string should be empty" - ); - Assert.ok(statusCard.locationEl, "Location details should be present"); - let flag = - statusCard.locationEl?.shadowRoot.querySelector("ipprotection-flag"); + let descriptionMetadata = statusCard?.statusGroupEl.description; - Assert.ok(flag, "Flag component should be present"); + Assert.ok( + descriptionMetadata.values.length, + "Ensure there are elements loaded in the description slot" + ); - let animationLoadedPromise = BrowserTestUtils.waitForMutationCondition( - statusCard.shadowRoot, - { childList: true, subtree: true }, - () => statusCard.animationEl + let locationNameFilter = descriptionMetadata.values.filter( + locationName => locationName === mockLocation.name ); - let timerUpdatedPromise = BrowserTestUtils.waitForMutationCondition( - statusCard.shadowRoot, - { childList: true, subtree: true }, - () => JSON.parse(statusCard.statusGroupEl.dataset.l10nArgs).time != "" + Assert.ok(locationNameFilter.length, "Found location in status card"); + + // We can't check the time value directly, so instead see if the lit timerDirective is loaded in the component. + // Assert that there's no timerDirective, so that we know the timer is not running. + let timerDirectiveFilter = descriptionMetadata.values.filter( + value => value._$litDirective$?.name == "TimerDirective" + ); + Assert.ok( + !timerDirectiveFilter.length, + "Timer should not be loaded in description meta data" ); // Set state as if protection is enabled @@ -80,54 +80,22 @@ add_task(async function test_status_card_in_panel() { content.requestUpdate(); - await Promise.all([ - content.updateComplete, - timerUpdatedPromise, - animationLoadedPromise, - ]); + await content.updateComplete; Assert.equal( statusCard?.statusGroupEl.getAttribute("data-l10n-id"), l10nIdOn, "Status card connection toggle data-l10n-id should be correct when protection is enabled" ); - Assert.ok(statusCard.animationEl, "Status card animation should be present"); - - let animationUnloadedPromise = BrowserTestUtils.waitForMutationCondition( - statusCard.shadowRoot, - { childList: true, subtree: true }, - () => !statusCard.animationEl - ); - let timerStoppedPromise = BrowserTestUtils.waitForMutationCondition( - statusCard.shadowRoot, - { childList: true, subtree: true }, - () => JSON.parse(statusCard?.statusGroupEl.dataset.l10nArgs).time === "" - ); - - // // Set state as if protection is disabled - await setPanelState({ - isSignedOut: false, - protectionEnabledSince: enabledSince, - location: mockLocation, - isProtectionEnabled: false, - }); - content.requestUpdate(); - - await Promise.all([ - content.updateComplete, - animationUnloadedPromise, - timerStoppedPromise, - ]); - - Assert.equal( - statusCard?.statusGroupEl.getAttribute("data-l10n-id"), - l10nIdOff, - "Status card connection toggle data-l10n-id should be correct when protection is disabled" + // Now check the timerDirective again and see if it's loaded. If found, then the timer is running. + descriptionMetadata = statusCard?.statusGroupEl.description; + timerDirectiveFilter = descriptionMetadata.values.filter( + value => value._$litDirective$?.name == "TimerDirective" ); Assert.ok( - !statusCard.animationEl, - "Status card animation should not be present" + timerDirectiveFilter.length, + "Timer should be loaded now in description meta data" ); await closePanel(); diff --git a/browser/locales-preview/ipProtection.ftl b/browser/locales-preview/ipProtection.ftl @@ -36,17 +36,31 @@ ipprotection-feature-introduction-button-primary = Next ipprotection-feature-introduction-button-secondary-not-now = Not now ipprotection-feature-introduction-button-secondary-no-thanks = No thanks -## +## Panel + +upgrade-vpn-title = Get peace of mind with full-device protection +upgrade-vpn-paragraph = Protect yourself beyond the browser with <a data-l10n-name="learn-more-vpn">{ -mozilla-vpn-brand-name }</a>. Customize your VPN location, set site-specific locations, and enjoy enhanced security whether you’re at home or on public Wi-Fi. +upgrade-vpn-button = Upgrade -# The panel status card has a header and a connection time displayed under it when the VPN is on. +signed-out-vpn-title = Sign in to boost your browser’s privacy with free { -firefox-vpn-brand-name } +signed-out-vpn-message = You’ve been selected for early access to our new, <a data-l10n-name="learn-more-vpn-signed-out">built-in VPN</a>. Enhance your browser’s protection by hiding your location and encrypting your traffic. +sign-in-vpn = Next + +## Status card + +ipprotection-connection-status-on = + .label = VPN is on +ipprotection-connection-status-off = + .label = VPN is off + +# The panel status card has a header, as well as VPN server location name and connection time displayed under it when the VPN is on. # Variables: # $time (String) - The amount of time connected to the proxy as HH:MM:SS (hours, minutes, seconds). -ipprotection-connection-status-on = - .label = VPN on - .description = { $time } +ipprotection-connection-time = { $time } -ipprotection-connection-status-off = - .label = VPN off +# Location refers to the VPN server geographical position. +ipprotection-location-title = + .title = Location selected based on fastest server # When VPN is toggled on ipprotection-toggle-active = @@ -55,19 +69,6 @@ ipprotection-toggle-active = ipprotection-toggle-inactive = .aria-label = Turn VPN on -# Location refers to the VPN server geographical position. -ipprotection-location-title = - .label = Location - .title = Location selected based on fastest server - -upgrade-vpn-title = Get peace of mind with full-device protection -upgrade-vpn-paragraph = Protect yourself beyond the browser with <a data-l10n-name="learn-more-vpn">{ -mozilla-vpn-brand-name }</a>. Customize your VPN location, set site-specific locations, and enjoy enhanced security whether you’re at home or on public Wi-Fi. -upgrade-vpn-button = Upgrade - -signed-out-vpn-title = Sign in to boost your browser’s privacy with free { -firefox-vpn-brand-name } -signed-out-vpn-message = You’ve been selected for early access to our new, <a data-l10n-name="learn-more-vpn-signed-out">built-in VPN</a>. Enhance your browser’s protection by hiding your location and encrypting your traffic. -sign-in-vpn = Next - ## Messages and errors ipprotection-message-generic-error =