tor-browser

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

sidebar-main.mjs (29490B)


      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 http://mozilla.org/MPL/2.0/. */
      4 
      5 import {
      6  classMap,
      7  html,
      8  ifDefined,
      9  nothing,
     10  repeat,
     11  when,
     12 } from "chrome://global/content/vendor/lit.all.mjs";
     13 import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
     14 
     15 // eslint-disable-next-line import/no-unassigned-import
     16 import "chrome://browser/content/sidebar/sidebar-pins-promo.mjs";
     17 
     18 const lazy = {};
     19 ChromeUtils.defineESModuleGetters(lazy, {
     20  ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs",
     21  ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
     22  // GenAI.sys.mjs is missing. tor-browser#44045.
     23 });
     24 
     25 /**
     26 * Sidebar with expanded and collapsed states that provides entry points
     27 * to various sidebar panels and sidebar extensions.
     28 */
     29 export default class SidebarMain extends MozLitElement {
     30  static properties = {
     31    bottomActions: { type: Array },
     32    expanded: { type: Boolean, reflect: true },
     33    selectedView: { type: String },
     34    sidebarItems: { type: Array },
     35    open: { type: Boolean },
     36    shouldShowOverflowButton: { type: Boolean },
     37    isOverflowMenuOpen: { type: Boolean },
     38  };
     39 
     40  static queries = {
     41    allButtons: { all: "moz-button" },
     42    extensionButtons: { all: ".tools-and-extensions > moz-button[extension]" },
     43    toolButtons: {
     44      all: ".tools-and-extensions > moz-button[view]:not([extension])",
     45    },
     46    customizeButton: ".bottom-actions > moz-button[view=viewCustomizeSidebar]",
     47    buttonGroup: ".actions-list:not(.bottom-actions):not(.overflow-button)",
     48    moreToolsButton: ".more-tools-button",
     49    buttonsWrapper: ".buttons-wrapper",
     50  };
     51 
     52  get fluentStrings() {
     53    if (!this._fluentStrings) {
     54      this._fluentStrings = new Localization(
     55        ["browser/sidebar.ftl", "preview/genai.ftl"],
     56        true
     57      );
     58    }
     59    return this._fluentStrings;
     60  }
     61 
     62  constructor() {
     63    super();
     64    this.bottomActions = [];
     65    this.selectedView = window.SidebarController.currentID;
     66    this.open = window.SidebarController.isOpen;
     67    this.contextMenuTarget = null;
     68    this.expanded = false;
     69    this.clickCounts = {
     70      genai: 0,
     71      totalToolsMinusGenai: 0,
     72    };
     73    this.shouldShowOverflowButton = false;
     74    this.overflowMenuOpen = false;
     75  }
     76 
     77  tooltips = {
     78    viewHistorySidebar: {
     79      shortcutId: "key_gotoHistory",
     80      openl10nId: "sidebar-menu-open-history-tooltip",
     81      close10nId: "sidebar-menu-close-history-tooltip",
     82    },
     83    viewBookmarksSidebar: {
     84      shortcutId: "viewBookmarksSidebarKb",
     85      openl10nId: "sidebar-menu-open-bookmarks-tooltip",
     86      close10nId: "sidebar-menu-close-bookmarks-tooltip",
     87    },
     88    viewGenaiChatSidebar: {
     89      shortcutId: "viewGenaiChatSidebarKb",
     90      openl10nId: "sidebar-menu-open-ai-chatbot-tooltip-generic",
     91      close10nId: "sidebar-menu-close-ai-chatbot-tooltip-generic",
     92      openProviderl10nId: "sidebar-menu-open-ai-chatbot-provider-tooltip",
     93      closeProviderl10nId: "sidebar-menu-close-ai-chatbot-provider-tooltip",
     94    },
     95  };
     96 
     97  connectedCallback() {
     98    super.connectedCallback();
     99    this._sidebarBox = document.getElementById("sidebar-box");
    100    this._sidebarMain = document.getElementById("sidebar-main");
    101    this._contextMenu = document.getElementById("sidebar-context-menu");
    102    this._toolsOverflowMenu = document.getElementById("sidebar-tools-overflow");
    103    this._toolsOverflowButtonGroup =
    104      this._toolsOverflowMenu.querySelector("button-group");
    105    this._manageExtensionMenuItem = document.getElementById(
    106      "sidebar-context-menu-manage-extension"
    107    );
    108    this._removeExtensionMenuItem = document.getElementById(
    109      "sidebar-context-menu-remove-extension"
    110    );
    111    this._reportExtensionMenuItem = document.getElementById(
    112      "sidebar-context-menu-report-extension"
    113    );
    114    this._unpinExtensionMenuItem = document.getElementById(
    115      "sidebar-context-menu-unpin-extension"
    116    );
    117    this._hideSidebarMenuItem = document.getElementById(
    118      "sidebar-context-menu-hide-sidebar"
    119    );
    120    this._enableVerticalTabsMenuItem = document.getElementById(
    121      "sidebar-context-menu-enable-vertical-tabs"
    122    );
    123    this._customizeSidebarMenuItem = document.getElementById(
    124      "sidebar-context-menu-customize-sidebar"
    125    );
    126    this._menuseparator = this._contextMenu.querySelector("menuseparator");
    127 
    128    this._sidebarBox.addEventListener("sidebar-show", this);
    129    this._sidebarBox.addEventListener("sidebar-hide", this);
    130    this._sidebarMain.addEventListener("contextmenu", this);
    131    this._contextMenu.addEventListener("popuphidden", this);
    132    this._contextMenu.addEventListener("command", this);
    133    this._toolsOverflowMenu.addEventListener("popupshown", this);
    134    this._toolsOverflowMenu.addEventListener("popuphidden", this);
    135 
    136    window.addEventListener("SidebarItemAdded", this);
    137    window.addEventListener("SidebarItemChanged", this);
    138    window.addEventListener("SidebarItemRemoved", this);
    139 
    140    this.setCustomize();
    141    this.createSplitter();
    142    this.createToolsObservers();
    143  }
    144 
    145  disconnectedCallback() {
    146    super.disconnectedCallback();
    147    this._sidebarBox.removeEventListener("sidebar-show", this);
    148    this._sidebarBox.removeEventListener("sidebar-hide", this);
    149    this._sidebarMain.removeEventListener("contextmenu", this);
    150    this._contextMenu.removeEventListener("popuphidden", this);
    151    this._contextMenu.removeEventListener("command", this);
    152    this._toolsOverflowMenu.removeEventListener("popupshown", this);
    153    this._toolsOverflowMenu.removeEventListener("popuphidden", this);
    154 
    155    window.removeEventListener("SidebarItemAdded", this);
    156    window.removeEventListener("SidebarItemChanged", this);
    157    window.removeEventListener("SidebarItemRemoved", this);
    158 
    159    this._toolsIntersectionObserver?.disconnect();
    160    this._toolsResizeObserver?.disconnect();
    161    this.ownerDocument
    162      .getElementById("drag-to-pin-promo-card")
    163      ?.disconnectedCallback();
    164  }
    165 
    166  get isToolsDragging() {
    167    return this.toolsSplitter.getAttribute("state") === "dragging";
    168  }
    169 
    170  createToolsObservers() {
    171    this._toolsIntersectionObserver = new IntersectionObserver(
    172      entries => {
    173        this.shouldShowOverflowButton = entries.some(
    174          entry =>
    175            !entry.isIntersecting &&
    176            window.SidebarController.sidebarVerticalTabsEnabled
    177        );
    178        let panelButtonGroup = document.getElementById("tools-overflow-list");
    179        for (const entry of entries) {
    180          let view = entry.target.getAttribute("view");
    181          let copyButton;
    182          copyButton = panelButtonGroup.querySelector(`[view='${view}']`);
    183          let buttonAlreadyAddedToOverflow = !!copyButton;
    184          if (!entry.isIntersecting && !buttonAlreadyAddedToOverflow) {
    185            // We can't move the original tools/extension buttons into the overflow panel
    186            // because Lit will lose the original references to them. We instead create copies of
    187            // these buttons to add to the overflow panel
    188            let newCopyButton = this.createCopyButton(view);
    189            panelButtonGroup.appendChild(newCopyButton);
    190 
    191            // Hide original button
    192            entry.target.style.visibility = "hidden";
    193            for (const button of this.buttonGroup.children) {
    194              const style = window.getComputedStyle(button);
    195              if (style.display !== "none" && style.visibility !== "hidden") {
    196                this.buttonGroup.activeChild = button;
    197                break;
    198              }
    199            }
    200          } else if (entry.isIntersecting && buttonAlreadyAddedToOverflow) {
    201            // Remove copy button from the panel
    202            copyButton.remove();
    203 
    204            // Show original button
    205            entry.target.style.visibility = "visible";
    206          }
    207        }
    208      },
    209      {
    210        root: this.buttonGroup,
    211        rootMargin: "0px",
    212        threshold: 0.5,
    213      }
    214    );
    215 
    216    this._toolsResizeObserver = new ResizeObserver(() => {
    217      if (this.isToolsDragging) {
    218        window.SidebarController._state.toolsDragActive = true;
    219      }
    220    });
    221 
    222    this._toolsDropHandler = () =>
    223      (window.SidebarController._state.toolsDragActive = false);
    224    this.toolsSplitter.addEventListener("command", this._toolsDropHandler);
    225  }
    226 
    227  createSplitter() {
    228    let toolsSplitter = document.createXULElement("splitter");
    229    toolsSplitter.setAttribute("orient", "vertical");
    230    toolsSplitter.setAttribute("id", "sidebar-tools-and-extensions-splitter");
    231    toolsSplitter.setAttribute("resizebefore", "none");
    232    toolsSplitter.setAttribute("resizeafter", "sibling");
    233    this.toolsSplitter = toolsSplitter;
    234  }
    235 
    236  createCopyButton(view) {
    237    let newButtonAction;
    238    if (view === "viewCustomizeSidebar") {
    239      newButtonAction = this.bottomActions[0];
    240    } else {
    241      newButtonAction = this.getToolsAndExtensions().get(view);
    242    }
    243    let newButtonValues = this.getEntrypointValues(newButtonAction);
    244    let newButton = document.createElement("moz-button");
    245    if (newButtonValues) {
    246      newButton.classList.add("expanded-button");
    247      newButton.setAttribute("view", newButtonValues.action.view);
    248      newButton.setAttribute("aria-pressed", newButtonValues.isActiveView);
    249      newButton.setAttribute(
    250        "type",
    251        newButtonValues.isActiveView ? "icon" : "icon ghost"
    252      );
    253      newButton.addEventListener("click", async () => {
    254        await this.showView(newButtonValues.action.view);
    255        this.resetPanelButtonValues();
    256        const isActiveView =
    257          this.open && newButtonValues.action.view === this.selectedView;
    258        newButton.setAttribute("aria-pressed", isActiveView);
    259        newButton.setAttribute("type", isActiveView ? "icon" : "icon ghost");
    260        this._toolsOverflowMenu.hidePopup();
    261      });
    262      newButton.setAttribute("title", newButtonValues.tooltip);
    263      newButton.iconSrc = newButtonValues.action.iconUrl;
    264      newButton.textContent = newButtonValues.actionLabel;
    265      if (newButtonValues.action.view?.includes("-sidebar-action")) {
    266        newButton.setAttribute("extension", "");
    267      }
    268      if (newButtonValues.action.extensionId) {
    269        newButton.setAttribute(
    270          "extensionId",
    271          newButtonValues.action.extensionId
    272        );
    273      }
    274    }
    275    return newButton;
    276  }
    277 
    278  resetPanelButtonValues() {
    279    let panelButtonGroup = document.getElementById("tools-overflow-list");
    280    for (const panelButton of Array.from(panelButtonGroup.children)) {
    281      // reset aria-pressed and button type for all panel buttons
    282      const isActiveView =
    283        this.open && panelButton.getAttribute("view") === this.selectedView;
    284      panelButton.setAttribute("aria-pressed", isActiveView);
    285      panelButton.setAttribute("type", isActiveView ? "icon" : "icon ghost");
    286    }
    287  }
    288 
    289  async onSidebarPopupShowing(event) {
    290    // Store the context menu target which holds the id required for managing sidebar items
    291    let targetHost = event.explicitOriginalTarget.getRootNode().host;
    292    let toolbarContextMenuTarget =
    293      event.explicitOriginalTarget.flattenedTreeParentNode
    294        .flattenedTreeParentNode;
    295    let isToolbarTarget = false;
    296    if (["moz-button", "sidebar-main"].includes(targetHost?.localName)) {
    297      this.contextMenuTarget = targetHost;
    298    } else if (
    299      document
    300        .getElementById("vertical-tabs")
    301        .contains(toolbarContextMenuTarget)
    302    ) {
    303      this.contextMenuTarget = toolbarContextMenuTarget;
    304      isToolbarTarget = true;
    305    }
    306    if (
    307      this.contextMenuTarget?.localName === "sidebar-main" &&
    308      !window.SidebarController.sidebarVerticalTabsEnabled
    309    ) {
    310      this.updateSidebarContextMenuItems();
    311      return;
    312    }
    313    if (
    314      this.contextMenuTarget?.getAttribute("extensionId") ||
    315      this.contextMenuTarget?.className.includes("tab") ||
    316      isToolbarTarget
    317    ) {
    318      this.updateExtensionContextMenuItems();
    319      return;
    320    }
    321 
    322    if (this.contextMenuTarget?.hasAttribute("contextMenu")) {
    323      this.hideExistingMenuItem();
    324 
    325      const toolId = this.contextMenuTarget.getAttribute("contextMenu");
    326      await this.buildToolContextMenuItems(event, toolId);
    327 
    328      const items = this._contextMenu.querySelectorAll(
    329        "[customized-tool='true']"
    330      );
    331 
    332      if (items?.length) {
    333        // Since we are dynamically building/customizing the sidebar context menu for tools
    334        // This ensures that the menu is fully updated before showing.
    335        this._contextMenu.openPopupAtScreen(event.screenX, event.screenY, true);
    336        return;
    337      }
    338    }
    339 
    340    event.preventDefault();
    341  }
    342 
    343  async buildToolContextMenuItems(event, toolId) {
    344    const menu = this._contextMenu;
    345    // Clear previously added custom menuitems
    346    menu
    347      .querySelectorAll("[customized-tool='true']")
    348      .forEach(node => node.remove());
    349 
    350    const menuBuilders = {};
    351 
    352    const builder = menuBuilders[toolId];
    353    if (typeof builder === "function") {
    354      const originalAppendChild = menu.appendChild.bind(menu);
    355 
    356      menu.appendChild = child => {
    357        child.setAttribute("customized-tool", true);
    358        return originalAppendChild(child);
    359      };
    360 
    361      await builder(menu);
    362      menu.appendChild = originalAppendChild;
    363    }
    364 
    365    return menu;
    366  }
    367 
    368  hideToolMenuItems() {
    369    const customMenuItems = this._contextMenu.querySelectorAll(
    370      "[customized-tool='true']"
    371    );
    372    customMenuItems.forEach(item => (item.hidden = true));
    373  }
    374 
    375  hideExistingMenuItem() {
    376    this._customizeSidebarMenuItem.hidden = true;
    377    this._enableVerticalTabsMenuItem.hidden = true;
    378    this._hideSidebarMenuItem.hidden = true;
    379    this._unpinExtensionMenuItem.hidden = true;
    380    this._manageExtensionMenuItem.hidden = true;
    381    this._removeExtensionMenuItem.hidden = true;
    382    this._reportExtensionMenuItem.hidden = true;
    383    // Prevent the menu separator visible in Window and Linux
    384    this._menuseparator.hidden = true;
    385  }
    386 
    387  updateSidebarContextMenuItems() {
    388    this._menuseparator.hidden = true;
    389    this._manageExtensionMenuItem.hidden = true;
    390    this._removeExtensionMenuItem.hidden = true;
    391    this._reportExtensionMenuItem.hidden = true;
    392    this._unpinExtensionMenuItem.hidden = true;
    393    this._customizeSidebarMenuItem.hidden = false;
    394    this._enableVerticalTabsMenuItem.hidden = false;
    395    this._hideSidebarMenuItem.hidden = false;
    396    this.hideToolMenuItems();
    397  }
    398 
    399  async updateExtensionContextMenuItems() {
    400    this._customizeSidebarMenuItem.hidden = true;
    401    this._enableVerticalTabsMenuItem.hidden = true;
    402    this._hideSidebarMenuItem.hidden = true;
    403    this._menuseparator.hidden = false;
    404    this._unpinExtensionMenuItem.hidden = false;
    405    this._manageExtensionMenuItem.hidden = false;
    406    this._removeExtensionMenuItem.hidden = false;
    407    this._reportExtensionMenuItem.hidden = false;
    408    this.hideToolMenuItems();
    409    const extensionId = this.contextMenuTarget.getAttribute("extensionId");
    410    if (!extensionId) {
    411      return;
    412    }
    413    const addon = await window.AddonManager?.getAddonByID(extensionId);
    414    if (!addon) {
    415      // Disable all context menus items if the addon doesn't
    416      // exist anymore from the AddonManager perspective.
    417      this._manageExtensionMenuItem.disabled = true;
    418      this._removeExtensionMenuItem.disabled = true;
    419      this._reportExtensionMenuItem.disabled = true;
    420    } else {
    421      this._manageExtensionMenuItem.disabled = false;
    422      this._removeExtensionMenuItem.disabled = !(
    423        addon.permissions & AddonManager.PERM_CAN_UNINSTALL
    424      );
    425      this._reportExtensionMenuItem.disabled = !window.gAddonAbuseReportEnabled;
    426    }
    427  }
    428 
    429  async manageExtension() {
    430    await window.BrowserAddonUI.manageAddon(
    431      this.contextMenuTarget.getAttribute("extensionId"),
    432      "sidebar-context-menu"
    433    );
    434  }
    435 
    436  async removeExtension() {
    437    await window.BrowserAddonUI.removeAddon(
    438      this.contextMenuTarget.getAttribute("extensionId"),
    439      "sidebar-context-menu"
    440    );
    441  }
    442 
    443  async reportExtension() {
    444    await window.BrowserAddonUI.reportAddon(
    445      this.contextMenuTarget.getAttribute("extensionId"),
    446      "sidebar-context-menu"
    447    );
    448  }
    449 
    450  unpinExtension() {
    451    window.SidebarController.toggleTool(
    452      this.contextMenuTarget.getAttribute("view")
    453    );
    454  }
    455 
    456  getImageUrl(icon, targetURI) {
    457    if (window.IS_STORYBOOK) {
    458      return `chrome://global/skin/icons/defaultFavicon.svg`;
    459    }
    460    if (!icon) {
    461      if (targetURI?.startsWith("moz-extension")) {
    462        return "chrome://mozapps/skin/extensions/extension.svg";
    463      }
    464      return `chrome://global/skin/icons/defaultFavicon.svg`;
    465    }
    466    // If the icon is not for website (doesn't begin with http), we
    467    // display it directly. Otherwise we go through the page-icon
    468    // protocol to try to get a cached version. We don't load
    469    // favicons directly.
    470    if (icon.startsWith("http")) {
    471      return `page-icon:${targetURI}`;
    472    }
    473    return icon;
    474  }
    475 
    476  getToolsAndExtensions() {
    477    return window.SidebarController.toolsAndExtensions;
    478  }
    479 
    480  setCustomize() {
    481    const view = "viewCustomizeSidebar";
    482    const customizeSidebar = window.SidebarController.sidebars.get(view);
    483    this.bottomActions = [
    484      {
    485        l10nId: customizeSidebar.revampL10nId,
    486        iconUrl: customizeSidebar.iconUrl,
    487        view,
    488      },
    489    ];
    490  }
    491 
    492  async handleEvent(e) {
    493    switch (e.type) {
    494      case "command":
    495        switch (e.target.id) {
    496          case "sidebar-context-menu-manage-extension":
    497            await this.manageExtension();
    498            break;
    499          case "sidebar-context-menu-report-extension":
    500            await this.reportExtension();
    501            break;
    502          case "sidebar-context-menu-remove-extension":
    503            await this.removeExtension();
    504            break;
    505          case "sidebar-context-menu-unpin-extension":
    506            this.unpinExtension();
    507            break;
    508          case "sidebar-context-menu-hide-sidebar":
    509            if (
    510              window.SidebarController._animationEnabled &&
    511              !window.gReduceMotion
    512            ) {
    513              window.SidebarController._animateSidebarMain();
    514            }
    515            window.SidebarController.hide({ dismissPanel: false });
    516            window.SidebarController._state.updateVisibility(false);
    517            break;
    518          case "sidebar-context-menu-enable-vertical-tabs":
    519            await window.SidebarController.toggleVerticalTabs();
    520            break;
    521          case "sidebar-context-menu-customize-sidebar":
    522            await window.SidebarController.show("viewCustomizeSidebar");
    523            break;
    524        }
    525        break;
    526      case "contextmenu":
    527        if (
    528          !["tabs-newtab-button", "vertical-tabs-newtab-button"].includes(
    529            e.target.id
    530          )
    531        ) {
    532          this.onSidebarPopupShowing(e).then(() => {
    533            // populating the context menu can be async, so dispatch an event when ready
    534            this.dispatchEvent(new CustomEvent("sidebar-contextmenu-ready"));
    535          });
    536        }
    537        break;
    538      case "popuphidden":
    539        if (e.target === this._contextMenu) {
    540          this.contextMenuTarget = null;
    541        } else if (e.target === this._toolsOverflowMenu) {
    542          this.isOverflowMenuOpen = false;
    543          window.removeEventListener("keydown", this.handleOverflowKeypress);
    544        }
    545        break;
    546      case "popupshown":
    547        if (e.target === this._toolsOverflowMenu) {
    548          this.isOverflowMenuOpen = true;
    549          window.addEventListener("keydown", this.handleOverflowKeypress);
    550        }
    551        break;
    552      case "sidebar-show":
    553        this.selectedView = e.detail.viewId;
    554        this.open = true;
    555        break;
    556      case "sidebar-hide":
    557        this.open = false;
    558        break;
    559      case "SidebarItemAdded":
    560      case "SidebarItemChanged":
    561      case "SidebarItemRemoved":
    562        this.requestUpdate();
    563        break;
    564    }
    565  }
    566 
    567  handleOverflowKeypress = e => {
    568    if (
    569      (e.key == "ArrowDown" || e.key == "ArrowUp") &&
    570      !this._toolsOverflowButtonGroup.matches(":focus-within")
    571    ) {
    572      if (e.key == "ArrowUp") {
    573        this._toolsOverflowButtonGroup.activeChild =
    574          this._toolsOverflowButtonGroup.walker.lastChild();
    575      }
    576      this._toolsOverflowButtonGroup.activeChild.focus();
    577    }
    578  };
    579 
    580  async checkShouldShowCalloutSurveys(view) {
    581    if (view == "viewGenaiChatSidebar") {
    582      this.clickCounts.genai++;
    583    } else {
    584      this.clickCounts.totalToolsMinusGenai++;
    585    }
    586 
    587    await lazy.ASRouter.waitForInitialized;
    588    lazy.ASRouter.sendTriggerMessage({
    589      browser: window.gBrowser.selectedBrowser,
    590      id: "sidebarToolOpened",
    591      context: {
    592        view,
    593        clickCounts: this.clickCounts,
    594      },
    595    });
    596  }
    597 
    598  async showView(view) {
    599    const { currentID, toolsAndExtensions } = window.SidebarController;
    600    let isToolOpening =
    601      (!currentID || (currentID && currentID !== view)) &&
    602      toolsAndExtensions.has(view);
    603    window.SidebarController.recordIconClick(view, this.expanded);
    604    window.SidebarController.toggle(view);
    605    if (view === "viewCustomizeSidebar") {
    606      Glean.sidebarCustomize.iconClick.record();
    607    }
    608    if (isToolOpening) {
    609      await this.checkShouldShowCalloutSurveys(view);
    610    }
    611  }
    612 
    613  enabledToolsAndExtensionsCount() {
    614    let enabledToolsAndExtensionsCount = 0;
    615    for (const tool of window.SidebarController.toolsAndExtensions.values()) {
    616      if (!tool.disabled) {
    617        enabledToolsAndExtensionsCount++;
    618      }
    619    }
    620    return enabledToolsAndExtensionsCount;
    621  }
    622 
    623  isToolsOverflowing() {
    624    return this.expanded && window.SidebarController.sidebarVerticalTabsEnabled;
    625  }
    626 
    627  willUpdate() {
    628    this._toolsIntersectionObserver?.disconnect();
    629    this._toolsResizeObserver?.disconnect();
    630  }
    631 
    632  updated() {
    633    if (
    634      window.SidebarController.sidebarRevampVisibility !== "expand-on-hover"
    635    ) {
    636      for (const buttonEl of this.allButtons) {
    637        if (buttonEl.hasAttribute("view")) {
    638          this._toolsIntersectionObserver.observe(buttonEl);
    639        }
    640      }
    641 
    642      this._toolsResizeObserver.observe(this.buttonGroup);
    643    } else {
    644      this.shouldShowOverflowButton = !this.expanded;
    645      for (const buttonEl of this.allButtons) {
    646        if (buttonEl.style.visibility === "hidden") {
    647          buttonEl.style.visibility = "visible";
    648        }
    649      }
    650      this._toolsIntersectionObserver.disconnect();
    651      this._toolsResizeObserver.disconnect();
    652    }
    653  }
    654 
    655  getEntrypointValues(action) {
    656    let providerInfo;
    657    if (action.view === "viewGenaiChatSidebar") {
    658      // GenAI.sys.mjs is missing. tor-browser#44045.
    659      return null;
    660    }
    661 
    662    if (action.disabled || action.hidden) {
    663      return null;
    664    }
    665    const isActiveView = this.open && action.view === this.selectedView;
    666    let actionLabel = "";
    667    if (action.tooltiptext) {
    668      actionLabel = action.tooltiptext;
    669    } else if (action.l10nId) {
    670      const messages = this.fluentStrings.formatMessagesSync([action.l10nId]);
    671      const attributes = messages?.[0]?.attributes;
    672      actionLabel = attributes?.find(attr => attr.name === "label")?.value;
    673    }
    674 
    675    let tooltip = actionLabel;
    676    const tooltipInfo = this.tooltips[action.view];
    677    if (tooltipInfo) {
    678      const { shortcutId, openl10nId, close10nId } = tooltipInfo;
    679      let l10nId = isActiveView ? close10nId : openl10nId;
    680      let tooltipData = {};
    681 
    682      if (action.view === "viewGenaiChatSidebar") {
    683        const provider = providerInfo?.name;
    684 
    685        if (provider) {
    686          tooltipData.provider = provider;
    687          l10nId = isActiveView
    688            ? tooltipInfo.closeProviderl10nId
    689            : tooltipInfo.openProviderl10nId;
    690        }
    691      }
    692 
    693      if (shortcutId) {
    694        const shortcut = lazy.ShortcutUtils.prettifyShortcut(
    695          document.getElementById(shortcutId)
    696        );
    697        tooltipData.shortcut = shortcut;
    698      }
    699 
    700      tooltip = this.fluentStrings.formatValueSync(l10nId, tooltipData);
    701    }
    702 
    703    let toolsOverflowing = this.isToolsOverflowing();
    704    return { action, isActiveView, toolsOverflowing, tooltip, actionLabel };
    705  }
    706 
    707  entrypointTemplate(action) {
    708    let buttonValues = this.getEntrypointValues(action);
    709    return html`${when(
    710      buttonValues,
    711      () => html`
    712        <moz-button
    713          class=${classMap({
    714            "tools-overflow": buttonValues.toolsOverflowing,
    715          })}
    716          type=${buttonValues.isActiveView ? "icon" : "icon ghost"}
    717          aria-pressed=${buttonValues.isActiveView}
    718          view=${buttonValues.action.view}
    719          @click=${async () => await this.showView(buttonValues.action.view)}
    720          title=${buttonValues.tooltip}
    721          .iconSrc=${buttonValues.action.iconUrl}
    722          ?extension=${buttonValues.action.view?.includes("-sidebar-action")}
    723          extensionId=${ifDefined(buttonValues.action.extensionId)}
    724          ?attention=${!!action?.attention}
    725          contextMenu=${action?.contextMenu || nothing}
    726        >
    727        </moz-button>
    728      `
    729    )}`;
    730  }
    731 
    732  showOverflowMenu(e) {
    733    let panel = document.getElementById("sidebar-tools-overflow");
    734    this.resetPanelButtonValues();
    735    let isKeyboardEvent = e.detail == 0;
    736    panel.addEventListener(
    737      "popupshown",
    738      () => {
    739        let group = panel.querySelector("button-group");
    740        group.activeChild = group.firstElementChild;
    741        if (isKeyboardEvent) {
    742          group.activeChild.focus();
    743        }
    744      },
    745      { once: true }
    746    );
    747 
    748    if (isKeyboardEvent) {
    749      panel.addEventListener(
    750        "popuphidden",
    751        () => {
    752          this.moreToolsButton.focus();
    753        },
    754        { once: true }
    755      );
    756    }
    757 
    758    panel.openPopup(
    759      this.moreToolsButton.shadowRoot.querySelector(".button-background"),
    760      window.SidebarController._positionStart
    761        ? "bottomright bottomleft"
    762        : "bottomleft bottomright"
    763    );
    764  }
    765 
    766  shouldUpdate() {
    767    const container = window.SidebarController.sidebarContainer;
    768    return container && !container.hidden;
    769  }
    770 
    771  render() {
    772    /* Add 1 to tools and extensions count for "Customize sidebar" option */
    773    let enabledToolsAndExtensionsCount =
    774      this.enabledToolsAndExtensionsCount() + 1;
    775    let messages = this.fluentStrings.formatMessagesSync([
    776      "sidebar-menu-more-tools-label",
    777    ]);
    778    const attributes = messages?.[0]?.attributes;
    779    let moreToolsTooltip = attributes?.find(
    780      attr => attr.name === "label"
    781    )?.value;
    782    return html`
    783      <link
    784        rel="stylesheet"
    785        href="chrome://browser/content/sidebar/sidebar.css"
    786      />
    787      <link
    788        rel="stylesheet"
    789        href="chrome://browser/content/sidebar/sidebar-main.css"
    790      />
    791      <div class="wrapper">
    792        <slot name="tabstrip"></slot>
    793        ${when(
    794          ((enabledToolsAndExtensionsCount > 2 && !this.expanded) ||
    795            this.expanded) &&
    796            window.SidebarController.sidebarRevampVisibility !==
    797              "expand-on-hover" &&
    798            window.SidebarController.sidebarVerticalTabsEnabled,
    799          () => html`${this.toolsSplitter}`
    800        )}
    801        <div
    802          class="buttons-wrapper"
    803          ?overflowing=${this.shouldShowOverflowButton}
    804        >
    805          <button-group
    806            class="tools-and-extensions actions-list"
    807            orientation=${this.isToolsOverflowing() ? "horizontal" : "vertical"}
    808            overflowing=${ifDefined(this.shouldShowOverflowButton)}
    809          >
    810            ${when(!this.isToolsOverflowing(), () =>
    811              repeat(
    812                this.getToolsAndExtensions().values(),
    813                action => action.view,
    814                action => this.entrypointTemplate(action)
    815              )
    816            )}
    817            ${when(window.SidebarController.sidebarVerticalTabsEnabled, () =>
    818              repeat(
    819                this.bottomActions,
    820                action => action.view,
    821                action => this.entrypointTemplate(action)
    822              )
    823            )}
    824            ${when(this.isToolsOverflowing(), () =>
    825              repeat(
    826                this.getToolsAndExtensions().values(),
    827                action => action.view,
    828                action => this.entrypointTemplate(action)
    829              )
    830            )}
    831          </button-group>
    832          ${when(
    833            !window.SidebarController.sidebarVerticalTabsEnabled,
    834            () =>
    835              html` <div class="bottom-actions actions-list">
    836                ${repeat(
    837                  this.bottomActions,
    838                  action => action.view,
    839                  action => this.entrypointTemplate(action)
    840                )}
    841              </div>`
    842          )}
    843          <button-group
    844            class="tools-and-extensions actions-list overflow-button"
    845            orientation="vertical"
    846            part="overflow-button"
    847            ?hidden=${!this.shouldShowOverflowButton}
    848          >
    849            <moz-button
    850              class="more-tools-button"
    851              type=${this.isOverflowMenuOpen ? "icon" : "icon ghost"}
    852              aria-pressed=${this.isOverflowMenuOpen}
    853              @click=${window.SidebarController.sidebarRevampVisibility ===
    854              "expand-on-hover"
    855                ? nothing
    856                : this.showOverflowMenu}
    857              title=${moreToolsTooltip}
    858              .iconSrc=${"chrome://global/skin/icons/chevron.svg"}
    859            >
    860            </moz-button>
    861          </button-group>
    862        </div>
    863      </div>
    864    `;
    865  }
    866 }
    867 
    868 customElements.define("sidebar-main", SidebarMain);