tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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);