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