Source code
Revision control
Copy as Markdown
Other Tools
/* 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
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
import {
html,
classMap,
styleMap,
} from "chrome://global/content/vendor/lit.all.mjs";
import {
connectionTimer,
defaultTimeValue,
} from "chrome://browser/content/ipprotection/ipprotection-timer.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.
*/
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",
connectionToggleEl: "#connection-toggle",
locationEl: "#location-wrapper",
siteSettingsEl: "ipprotection-site-settings-control",
};
static shadowRootOptions = {
...MozLitElement.shadowRootOptions,
delegatesFocus: true,
};
static properties = {
protectionEnabled: { type: Boolean },
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 },
};
constructor() {
super();
this.keyListener = this.#keyListener.bind(this);
}
connectedCallback() {
super.connectedCallback();
this.dispatchEvent(new CustomEvent("IPProtection:Init", { bubbles: true }));
this.addEventListener("keydown", this.keyListener, { capture: true });
}
disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener("keydown", this.keyListener, { capture: true });
}
handleToggleConnect(event) {
let isEnabled = event.target.pressed;
if (isEnabled) {
this.dispatchEvent(
new CustomEvent(this.TOGGLE_ON_EVENT, {
bubbles: true,
composed: true,
})
);
} else {
this.dispatchEvent(
new CustomEvent(this.TOGGLE_OFF_EVENT, {
bubbles: true,
composed: true,
})
);
}
this._toggleEnabled = isEnabled;
}
focus() {
this.connectionToggleEl?.focus();
}
#keyListener(event) {
let keyCode = event.code;
switch (keyCode) {
case "ArrowUp":
// Intentional fall-through
case "ArrowDown": {
event.stopPropagation();
event.preventDefault();
let direction =
keyCode == "ArrowDown"
? Services.focus.MOVEFOCUS_FORWARD
: Services.focus.MOVEFOCUS_BACKWARD;
Services.focus.moveFocus(
window,
null,
direction,
Services.focus.FLAG_BYKEY
);
break;
}
}
}
updated(changedProperties) {
super.updated(changedProperties);
// If the toggle state isn't set, do so now and let it
// match the protection state.
if (!changedProperties.has("_toggleEnabled")) {
this._toggleEnabled = this.protectionEnabled;
}
if (!this.protectionEnabled && this._toggleEnabled) {
// After pressing the toggle, if somehow protection was turned off
// (eg. error thrown), unset the toggle.
this._toggleEnabled = false;
}
}
cardContentTemplate() {
const statusCardL10nId = this.protectionEnabled
? "ipprotection-connection-status-on"
: "ipprotection-connection-status-off";
const toggleL10nId = this.protectionEnabled
? "ipprotection-toggle-active"
: "ipprotection-toggle-inactive";
const siteSettingsTemplate = this.protectionEnabled
? this.siteSettingsTemplate()
: null;
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>
${siteSettingsTemplate}
</moz-box-group>`;
}
siteSettingsTemplate() {
// TODO: Once we're able to detect the current site and its exception status, show
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
? connectionTimer(this.enabledSince)
: defaultTimeValue;
// 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" style=${labelStyles}>
<span>${this.location.name}</span>
<img
src="chrome://global/skin/icons/info.svg"
data-l10n-id="ipprotection-location-title"
style=${imgStyles}
/>
</div>
<span
id="time"
data-l10n-id="ipprotection-connection-time"
data-l10n-args=${time}
></span>
</div>
`
: null;
}
render() {
let content = this.cardContentTemplate();
return html`${content}`;
}
}
customElements.define("ipprotection-status-card", IPProtectionStatusCard);