ipprotection-status-card.mjs (7401B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 5 import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; 6 import { 7 html, 8 classMap, 9 styleMap, 10 } from "chrome://global/content/vendor/lit.all.mjs"; 11 12 // eslint-disable-next-line import/no-unassigned-import 13 import "chrome://global/content/elements/moz-toggle.mjs"; 14 // eslint-disable-next-line import/no-unassigned-import 15 import "chrome://browser/content/ipprotection/ipprotection-site-settings-control.mjs"; 16 17 /** 18 * Custom element that implements a status card for IP protection. 19 */ 20 export default class IPProtectionStatusCard extends MozLitElement { 21 TOGGLE_ON_EVENT = "ipprotection-status-card:user-toggled-on"; 22 TOGGLE_OFF_EVENT = "ipprotection-status-card:user-toggled-off"; 23 24 static queries = { 25 statusGroupEl: "#status-card", 26 connectionToggleEl: "#connection-toggle", 27 connectionButtonEl: "#connection-toggle-button", 28 locationEl: "#location-wrapper", 29 siteSettingsEl: "ipprotection-site-settings-control", 30 }; 31 32 static shadowRootOptions = { 33 ...MozLitElement.shadowRootOptions, 34 delegatesFocus: true, 35 }; 36 37 static properties = { 38 protectionEnabled: { type: Boolean }, 39 canShowTime: { type: Boolean }, 40 enabledSince: { type: Object }, 41 location: { type: Object }, 42 siteData: { type: Object }, 43 // Track toggle state separately so that we can tell when the toggle 44 // is enabled because of the existing protection state or because of user action. 45 _toggleEnabled: { type: Boolean, state: true }, 46 }; 47 48 constructor() { 49 super(); 50 51 this.keyListener = this.#keyListener.bind(this); 52 } 53 54 connectedCallback() { 55 super.connectedCallback(); 56 this.addEventListener("keydown", this.keyListener, { capture: true }); 57 } 58 59 disconnectedCallback() { 60 super.disconnectedCallback(); 61 this.removeEventListener("keydown", this.keyListener, { capture: true }); 62 } 63 64 handleToggleConnect(event) { 65 let isEnabled = event.target.pressed; 66 67 if (isEnabled) { 68 this.dispatchEvent( 69 new CustomEvent(this.TOGGLE_ON_EVENT, { 70 bubbles: true, 71 composed: true, 72 }) 73 ); 74 } else { 75 this.dispatchEvent( 76 new CustomEvent(this.TOGGLE_OFF_EVENT, { 77 bubbles: true, 78 composed: true, 79 }) 80 ); 81 } 82 83 this._toggleEnabled = isEnabled; 84 } 85 86 // TODO: Move button handling logic and button to new ipprotection-status-box component in Bug 2008854 87 handleOnOffButtonClick() { 88 let isEnabled = !this._toggleEnabled; 89 90 if (isEnabled) { 91 this.dispatchEvent( 92 new CustomEvent(this.TOGGLE_ON_EVENT, { 93 bubbles: true, 94 composed: true, 95 }) 96 ); 97 } else { 98 this.dispatchEvent( 99 new CustomEvent(this.TOGGLE_OFF_EVENT, { 100 bubbles: true, 101 composed: true, 102 }) 103 ); 104 } 105 106 this._toggleEnabled = isEnabled; 107 } 108 109 focus() { 110 this.connectionToggleEl?.focus(); 111 } 112 113 #keyListener(event) { 114 let keyCode = event.code; 115 switch (keyCode) { 116 case "ArrowUp": 117 // Intentional fall-through 118 case "ArrowDown": { 119 event.stopPropagation(); 120 event.preventDefault(); 121 122 let direction = 123 keyCode == "ArrowDown" 124 ? Services.focus.MOVEFOCUS_FORWARD 125 : Services.focus.MOVEFOCUS_BACKWARD; 126 Services.focus.moveFocus( 127 window, 128 null, 129 direction, 130 Services.focus.FLAG_BYKEY 131 ); 132 break; 133 } 134 } 135 } 136 137 updated(changedProperties) { 138 super.updated(changedProperties); 139 140 // If the toggle state isn't set, do so now and let it 141 // match the protection state. 142 if (!changedProperties.has("_toggleEnabled")) { 143 this._toggleEnabled = this.protectionEnabled; 144 } 145 146 if (!this.protectionEnabled && this._toggleEnabled) { 147 // After pressing the toggle, if somehow protection was turned off 148 // (eg. error thrown), unset the toggle. 149 this._toggleEnabled = false; 150 } 151 } 152 153 cardContentTemplate() { 154 const statusCardL10nId = this.protectionEnabled 155 ? "ipprotection-connection-status-on" 156 : "ipprotection-connection-status-off"; 157 const toggleL10nId = this.protectionEnabled 158 ? "ipprotection-toggle-active" 159 : "ipprotection-toggle-inactive"; 160 const toggleButtonType = this.protectionEnabled ? "secondary" : "primary"; 161 const toggleButtonL10nId = this.protectionEnabled 162 ? "ipprotection-button-turn-vpn-off" 163 : "ipprotection-button-turn-vpn-on"; 164 165 const siteSettingsTemplate = this.protectionEnabled 166 ? this.siteSettingsTemplate() 167 : null; 168 169 return html` <link 170 rel="stylesheet" 171 href="chrome://browser/content/ipprotection/ipprotection-status-card.css" 172 /> 173 <moz-box-group class="vpn-status-group"> 174 <moz-box-item 175 id="status-card" 176 class=${classMap({ 177 "is-enabled": this.protectionEnabled, 178 })} 179 layout="default" 180 data-l10n-id=${statusCardL10nId} 181 .description=${this.cardDescriptionTemplate()} 182 > 183 <moz-toggle 184 id="connection-toggle" 185 data-l10n-id=${toggleL10nId} 186 @click=${this.handleToggleConnect} 187 ?pressed=${this._toggleEnabled} 188 slot="actions" 189 ></moz-toggle> 190 </moz-box-item> 191 ${siteSettingsTemplate} 192 </moz-box-group> 193 <moz-button 194 type=${toggleButtonType} 195 id="connection-toggle-button" 196 data-l10n-id=${toggleButtonL10nId} 197 @click=${this.handleOnOffButtonClick} 198 hidden 199 > 200 </moz-button>`; 201 } 202 203 siteSettingsTemplate() { 204 // TODO: Once we're able to detect the current site and its exception status, show 205 // ipprotection-site-settings-control (Bug 1997412). 206 if (!this.siteData?.siteName) { 207 return null; 208 } 209 210 return html` <moz-box-item 211 id="site-settings" 212 class=${classMap({ 213 "is-enabled": this.protectionEnabled, 214 })} 215 > 216 <ipprotection-site-settings-control 217 .site=${this.siteData.siteName} 218 .exceptionEnabled=${this.siteData.isException} 219 class="slotted" 220 ></ipprotection-site-settings-control> 221 </moz-box-item>`; 222 } 223 224 cardDescriptionTemplate() { 225 // To work around mox-box-item description elements being hard to reach because of the shadowDOM, 226 // let's use a lit stylemap to apply style changes directly. 227 let labelStyles = styleMap({ 228 display: "flex", 229 gap: "var(--space-small)", 230 }); 231 let imgStyles = styleMap({ 232 "-moz-context-properties": "fill", 233 fill: "currentColor", 234 }); 235 236 return this.location 237 ? html` 238 <div id="vpn-details"> 239 <div id="location-label" style=${labelStyles}> 240 <span>${this.location.name}</span> 241 <img 242 src="chrome://global/skin/icons/info.svg" 243 data-l10n-id="ipprotection-location-title" 244 style=${imgStyles} 245 /> 246 </div> 247 </div> 248 ` 249 : null; 250 } 251 252 render() { 253 let content = this.cardContentTemplate(); 254 return html`${content}`; 255 } 256 } 257 258 customElements.define("ipprotection-status-card", IPProtectionStatusCard);