diff options
Diffstat (limited to '')
18 files changed, 1190 insertions, 340 deletions
diff --git a/browser/components/sidebar/browser-sidebar.js b/browser/components/sidebar/browser-sidebar.js index 55664f8cfc..6cbac7c082 100644 --- a/browser/components/sidebar/browser-sidebar.js +++ b/browser/components/sidebar/browser-sidebar.js @@ -3,65 +3,127 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /** - * SidebarUI controls showing and hiding the browser sidebar. + * SidebarController handles logic such as toggling sidebar panels, + * dynamically adding menubar menu items for the View -> Sidebar menu, + * and provides APIs for sidebar extensions, etc. */ -var SidebarUI = { +var SidebarController = { + makeSidebar({ elementId, ...rest }) { + return { + get sourceL10nEl() { + return document.getElementById(elementId); + }, + get title() { + return document.getElementById(elementId).getAttribute("label"); + }, + ...rest, + }; + }, + get sidebars() { if (this._sidebars) { return this._sidebars; } - function makeSidebar({ elementId, ...rest }) { - return { - get sourceL10nEl() { - return document.getElementById(elementId); - }, - get title() { - return document.getElementById(elementId).getAttribute("label"); - }, - ...rest, - }; - } - - return (this._sidebars = new Map([ - [ - "viewBookmarksSidebar", - makeSidebar({ - elementId: "sidebar-switcher-bookmarks", - url: "chrome://browser/content/places/bookmarksSidebar.xhtml", - menuId: "menu_bookmarksSidebar", - }), - ], + this._sidebars = new Map([ [ "viewHistorySidebar", - makeSidebar({ + this.makeSidebar({ elementId: "sidebar-switcher-history", url: this.sidebarRevampEnabled ? "chrome://browser/content/sidebar/sidebar-history.html" : "chrome://browser/content/places/historySidebar.xhtml", menuId: "menu_historySidebar", triggerButtonId: "appMenuViewHistorySidebar", + keyId: "key_gotoHistory", + menuL10nId: "menu-view-history-button", + revampL10nId: "sidebar-menu-history", + icon: `url("chrome://browser/content/firefoxview/view-history.svg")`, }), ], [ "viewTabsSidebar", - makeSidebar({ + this.makeSidebar({ elementId: "sidebar-switcher-tabs", url: this.sidebarRevampEnabled ? "chrome://browser/content/sidebar/sidebar-syncedtabs.html" : "chrome://browser/content/syncedtabs/sidebar.xhtml", menuId: "menu_tabsSidebar", + classAttribute: "sync-ui-item", + menuL10nId: "menu-view-synced-tabs-sidebar", + revampL10nId: "sidebar-menu-synced-tabs", + icon: `url("chrome://browser/content/firefoxview/view-syncedtabs.svg")`, }), ], - [ - "viewMegalistSidebar", - makeSidebar({ - elementId: "sidebar-switcher-megalist", - url: "chrome://global/content/megalist/megalist.html", - menuId: "menu_megalistSidebar", - }), - ], - ])); + ]); + + if (!this.sidebarRevampEnabled) { + this._sidebars.set( + "viewBookmarksSidebar", + this.makeSidebar({ + elementId: "sidebar-switcher-bookmarks", + url: "chrome://browser/content/places/bookmarksSidebar.xhtml", + menuId: "menu_bookmarksSidebar", + keyId: "viewBookmarksSidebarKb", + menuL10nId: "menu-view-bookmarks", + revampL10nId: "sidebar-menu-bookmarks", + }) + ); + if (this.megalistEnabled) { + this._sidebars.set( + "viewMegalistSidebar", + this.makeSidebar({ + elementId: "sidebar-switcher-megalist", + url: "chrome://global/content/megalist/megalist.html", + menuId: "menu_megalistSidebar", + menuL10nId: "menu-view-megalist-sidebar", + revampL10nId: "sidebar-menu-megalist", + }) + ); + } + } else { + this._sidebars.set( + "viewCustomizeSidebar", + this.makeSidebar({ + url: "chrome://browser/content/sidebar/sidebar-customize.html", + revampL10nId: "sidebar-menu-customize", + icon: `url("chrome://browser/skin/preferences/category-general.svg")`, + }) + ); + } + + return this._sidebars; + }, + + /** + * Returns a map of tools and extensions for use in the sidebar + */ + get toolsAndExtensions() { + if (this._toolsAndExtensions) { + return this._toolsAndExtensions; + } + + this._toolsAndExtensions = new Map(); + this.getSidebarPanels(["viewHistorySidebar", "viewTabsSidebar"]).forEach( + tool => { + this._toolsAndExtensions.set(tool.commandID, { + view: tool.commandID, + icon: tool.icon, + l10nId: tool.revampL10nId, + disabled: false, + }); + } + ); + this.getExtensions().forEach(extension => { + this._toolsAndExtensions.set(extension.commandID, { + view: extension.commandID, + extensionId: extension.extensionId, + icon: extension.icon, + tooltiptext: extension.label, + disabled: false, + }); + }); + return this._toolsAndExtensions; }, // Avoid getting the browser element from init() to avoid triggering the @@ -120,9 +182,18 @@ var SidebarUI = { this._switcherTarget = document.getElementById("sidebar-switcher-target"); this._switcherArrow = document.getElementById("sidebar-switcher-arrow"); + const menubar = document.getElementById("viewSidebarMenu"); + for (const [commandID, sidebar] of this.sidebars.entries()) { + if (!Object.hasOwn(sidebar, "extensionId")) { + // registerExtension() already creates menu items for extensions. + const menuitem = this.createMenuItem(commandID, sidebar); + menubar.appendChild(menuitem); + } + } + if (this.sidebarRevampEnabled) { - await import("chrome://browser/content/sidebar/sidebar-launcher.mjs"); - document.getElementById("sidebar-launcher").hidden = false; + await import("chrome://browser/content/sidebar/sidebar-main.mjs"); + document.getElementById("sidebar-main").hidden = false; document.getElementById("sidebar-header").hidden = true; } else { this._switcherTarget.addEventListener("command", () => { @@ -144,12 +215,7 @@ var SidebarUI = { const sideMenuPopupItem = document.getElementById( "sidebar-switcher-megalist" ); - sideMenuPopupItem.style.display = Services.prefs.getBoolPref( - "browser.megalist.enabled", - false - ) - ? "" - : "none"; + sideMenuPopupItem.style.display = this.megalistEnabled ? "" : "none"; }, setMegalistMenubarVisibility(aEvent) { @@ -160,10 +226,7 @@ var SidebarUI = { // Show the megalist item if enabled const megalistItem = popup.querySelector("#menu_megalistSidebar"); - megalistItem.hidden = !Services.prefs.getBoolPref( - "browser.megalist.enabled", - false - ); + megalistItem.hidden = !this.megalistEnabled; }, uninit() { @@ -172,22 +235,6 @@ var SidebarUI = { let enumerator = Services.wm.getEnumerator("navigator:browser"); if (!enumerator.hasMoreElements()) { let xulStore = Services.xulStore; - xulStore.persist(this._box, "sidebarcommand"); - - if (this._box.hasAttribute("positionend")) { - xulStore.persist(this._box, "positionend"); - } else { - xulStore.removeValue( - document.documentURI, - "sidebar-box", - "positionend" - ); - } - if (this._box.hasAttribute("checked")) { - xulStore.persist(this._box, "checked"); - } else { - xulStore.removeValue(document.documentURI, "sidebar-box", "checked"); - } xulStore.persist(this._box, "style"); xulStore.persist(this._title, "value"); @@ -338,30 +385,30 @@ var SidebarUI = { [...browser.children].forEach((node, i) => { node.style.order = i + 1; }); - let sidebarLauncher = document.querySelector("sidebar-launcher"); + let sidebarMain = document.querySelector("sidebar-main"); if (!this._positionStart) { - // DOM ordering is: sidebar-launcher | sidebar-box | splitter | appcontent | - // Want to display as: | appcontent | splitter | sidebar-box | sidebar-launcher - // So we just swap box and appcontent ordering and move sidebar-launcher to the end + // DOM ordering is: sidebar-main | sidebar-box | splitter | appcontent | + // Want to display as: | appcontent | splitter | sidebar-box | sidebar-main + // So we just swap box and appcontent ordering and move sidebar-main to the end let appcontent = document.getElementById("appcontent"); let boxOrdinal = this._box.style.order; this._box.style.order = appcontent.style.order; appcontent.style.order = boxOrdinal; // the launcher should be on the right of the sidebar-box - sidebarLauncher.style.order = parseInt(this._box.style.order) + 1; + sidebarMain.style.order = parseInt(this._box.style.order) + 1; // Indicate we've switched ordering to the box this._box.setAttribute("positionend", true); - sidebarLauncher.setAttribute("positionend", true); + sidebarMain.setAttribute("positionend", true); } else { this._box.removeAttribute("positionend"); - sidebarLauncher.removeAttribute("positionend"); + sidebarMain.removeAttribute("positionend"); } this.hideSwitcherPanel(); - let content = SidebarUI.browser.contentWindow; + let content = SidebarController.browser.contentWindow; if (content && content.updatePosition) { content.updatePosition(); } @@ -378,7 +425,7 @@ var SidebarUI = { // If the opener had a sidebar, open the same sidebar in our window. // The opener can be the hidden window too, if we're coming from the state // where no windows are open, and the hidden window has no sidebar box. - let sourceUI = sourceWindow.SidebarUI; + let sourceUI = sourceWindow.SidebarController; if (!sourceUI || !sourceUI._box) { // no source UI or no _box means we also can't adopt the state. return false; @@ -543,17 +590,212 @@ var SidebarUI = { _loadSidebarExtension(commandID) { let sidebar = this.sidebars.get(commandID); - let { extensionId } = sidebar; - if (extensionId) { - SidebarUI.browser.contentWindow.loadPanel( - extensionId, - sidebar.panel, - sidebar.browserStyle - ); + if (typeof sidebar.onload === "function") { + sidebar.onload(); } }, /** + * Sets the disabled property for a tool when customizing sidebar options + * + * @param {string} commandID + */ + toggleTool(commandID) { + let toggledTool = this.toolsAndExtensions.get(commandID); + toggledTool.disabled = !toggledTool.disabled; + if (!toggledTool.disabled) { + // If re-enabling tool, remove from the map and add it to the end + this.toolsAndExtensions.delete(commandID); + this.toolsAndExtensions.set(commandID, toggledTool); + } + window.dispatchEvent(new CustomEvent("SidebarItemChanged")); + }, + + addOrUpdateExtension(commandID, extension) { + if (this.toolsAndExtensions.has(commandID)) { + // Update existing extension + let extensionToUpdate = this.toolsAndExtensions.get(commandID); + extensionToUpdate.icon = extension.icon; + extensionToUpdate.tooltiptext = extension.label; + window.dispatchEvent(new CustomEvent("SidebarItemChanged")); + } else { + // Add new extension + this.toolsAndExtensions.set(commandID, { + view: commandID, + extensionId: extension.extensionId, + icon: extension.icon, + tooltiptext: extension.label, + disabled: false, + }); + window.dispatchEvent(new CustomEvent("SidebarItemAdded")); + } + }, + + /** + * Add menu items for a browser extension. Add the extension to the + * `sidebars` map. + * + * @param {string} commandID + * @param {object} props + */ + registerExtension(commandID, props) { + const sidebar = { + title: props.title, + url: "chrome://browser/content/webext-panels.xhtml", + menuId: props.menuId, + switcherMenuId: `sidebarswitcher_menu_${commandID}`, + keyId: `ext-key-id-${commandID}`, + label: props.title, + icon: props.icon, + classAttribute: "menuitem-iconic webextension-menuitem", + // The following properties are specific to extensions + extensionId: props.extensionId, + onload: props.onload, + }; + this.sidebars.set(commandID, sidebar); + + // Insert a menuitem for View->Show Sidebars. + const menuitem = this.createMenuItem(commandID, sidebar); + document.getElementById("viewSidebarMenu").appendChild(menuitem); + this.addOrUpdateExtension(commandID, sidebar); + + if (!this.sidebarRevampEnabled) { + // Insert a toolbarbutton for the sidebar dropdown selector. + let switcherMenuitem = this.createMenuItem(commandID, sidebar); + switcherMenuitem.setAttribute("id", sidebar.switcherMenuId); + switcherMenuitem.removeAttribute("type"); + + let separator = document.getElementById("sidebar-extensions-separator"); + separator.parentNode.insertBefore(switcherMenuitem, separator); + } + this._setExtensionAttributes( + commandID, + { icon: props.icon, label: props.title }, + sidebar + ); + }, + + /** + * Create a menu item for the View>Sidebars submenu in the menubar. + * + * @param {string} commandID + * @param {object} sidebar + * @returns {Element} + */ + createMenuItem(commandID, sidebar) { + const menuitem = document.createXULElement("menuitem"); + menuitem.setAttribute("id", sidebar.menuId); + menuitem.setAttribute("type", "checkbox"); + menuitem.addEventListener("command", () => this.toggle(commandID)); + if (sidebar.classAttribute) { + menuitem.setAttribute("class", sidebar.classAttribute); + } + if (sidebar.keyId) { + menuitem.setAttribute("key", sidebar.keyId); + } + if (sidebar.menuL10nId) { + menuitem.dataset.l10nId = sidebar.menuL10nId; + } + return menuitem; + }, + + /** + * Update attributes on all existing menu items for a browser extension. + * + * @param {string} commandID + * @param {object} attributes + * @param {string} attributes.icon + * @param {string} attributes.label + * @param {boolean} needsRefresh + */ + setExtensionAttributes(commandID, attributes, needsRefresh) { + const sidebar = this.sidebars.get(commandID); + this._setExtensionAttributes(commandID, attributes, sidebar, needsRefresh); + this.addOrUpdateExtension(commandID, sidebar); + }, + + _setExtensionAttributes( + commandID, + { icon, label }, + sidebar, + needsRefresh = false + ) { + sidebar.icon = icon; + sidebar.label = label; + + const updateAttributes = el => { + el.style.setProperty("--webextension-menuitem-image", sidebar.icon); + el.setAttribute("label", sidebar.label); + }; + + updateAttributes(document.getElementById(sidebar.menuId), sidebar); + const switcherMenu = document.getElementById(sidebar.switcherMenuId); + if (switcherMenu) { + updateAttributes(switcherMenu, sidebar); + } + if (this.initialized && this.currentID === commandID) { + // Update the sidebar if this extension is the current sidebar. + updateAttributes(this._switcherTarget, sidebar); + this.title = label; + if (this.isOpen && needsRefresh) { + this.show(commandID); + } + } + }, + + /** + * Retrieve the list of registered browser extensions. + * + * @returns {Array} + */ + getExtensions() { + const extensions = []; + for (const [commandID, sidebar] of this.sidebars.entries()) { + if (Object.hasOwn(sidebar, "extensionId")) { + extensions.push({ commandID, ...sidebar }); + } + } + return extensions; + }, + + /** + * Retrieve the list of sidebar panels + * + * @param {Array} commandIds + * @returns {Array} + */ + getSidebarPanels(commandIds) { + const tools = []; + for (const commandID of commandIds) { + const sidebar = this.sidebars.get(commandID); + if (sidebar) { + tools.push({ commandID, ...sidebar }); + } + } + return tools; + }, + + /** + * Remove a browser extension. + * + * @param {string} commandID + */ + removeExtension(commandID) { + const sidebar = this.sidebars.get(commandID); + if (!sidebar) { + return; + } + if (this.currentID === commandID) { + this.hide(); + } + document.getElementById(sidebar.menuId)?.remove(); + document.getElementById(sidebar.switcherMenuId)?.remove(); + this.sidebars.delete(commandID); + this.toolsAndExtensions.delete(commandID); + window.dispatchEvent(new CustomEvent("SidebarItemRemoved")); + }, + + /** * Show the sidebar. * * This wraps the internal method, including a ping to telemetry. @@ -632,7 +874,17 @@ var SidebarUI = { this._box.setAttribute("checked", "true"); this._box.setAttribute("sidebarcommand", commandID); - let { url, title, sourceL10nEl } = this.sidebars.get(commandID); + let { icon, url, title, sourceL10nEl } = this.sidebars.get(commandID); + if (icon) { + this._switcherTarget.style.setProperty( + "--webextension-menuitem-image", + icon + ); + } else { + this._switcherTarget.style.removeProperty( + "--webextension-menuitem-image" + ); + } // use to live update <tree> elements if the locale changes this.lastOpenedId = commandID; @@ -730,15 +982,21 @@ var SidebarUI = { // Add getters related to the position here, since we will want them // available for both startDelayedLoad and init. XPCOMUtils.defineLazyPreferenceGetter( - SidebarUI, + SidebarController, "_positionStart", - SidebarUI.POSITION_START_PREF, + SidebarController.POSITION_START_PREF, true, - SidebarUI.setPosition.bind(SidebarUI) + SidebarController.setPosition.bind(SidebarController) ); XPCOMUtils.defineLazyPreferenceGetter( - SidebarUI, + SidebarController, "sidebarRevampEnabled", "sidebar.revamp", false ); +XPCOMUtils.defineLazyPreferenceGetter( + SidebarController, + "megalistEnabled", + "browser.megalist.enabled", + false +); diff --git a/browser/components/sidebar/jar.mn b/browser/components/sidebar/jar.mn index 8a7071ca72..f9624b2a55 100644 --- a/browser/components/sidebar/jar.mn +++ b/browser/components/sidebar/jar.mn @@ -4,8 +4,11 @@ browser.jar: content/browser/sidebar/browser-sidebar.js - content/browser/sidebar/sidebar-launcher.css - content/browser/sidebar/sidebar-launcher.mjs + content/browser/sidebar/sidebar-customize.css + content/browser/sidebar/sidebar-customize.html + content/browser/sidebar/sidebar-customize.mjs + content/browser/sidebar/sidebar-main.css + content/browser/sidebar/sidebar-main.mjs content/browser/sidebar/sidebar-history.html content/browser/sidebar/sidebar-history.mjs content/browser/sidebar/sidebar-page.mjs diff --git a/browser/components/sidebar/moz.build b/browser/components/sidebar/moz.build index d988c0ff9b..6310d973e0 100644 --- a/browser/components/sidebar/moz.build +++ b/browser/components/sidebar/moz.build @@ -5,3 +5,5 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. JAR_MANIFESTS += ["jar.mn"] + +BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"] diff --git a/browser/components/sidebar/sidebar-customize.css b/browser/components/sidebar/sidebar-customize.css new file mode 100644 index 0000000000..d78477e8ba --- /dev/null +++ b/browser/components/sidebar/sidebar-customize.css @@ -0,0 +1,57 @@ +/* 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 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +.container { + padding-inline: var(--space-small); + font-size: 15px; +} + +.customize-header { + display: flex; + justify-content: space-between; + align-items: center; + -moz-context-properties: fill; + fill: currentColor; + color: currentColor; + + .customize-close-button::part(button) { + background-image: url("chrome://global/skin/icons/close-12.svg"); + } +} + +.customize-firefox-tools { + .inputs { + display: flex; + flex-direction: column; + gap: var(--space-medium); + } + + .input-wrapper { + display: flex; + align-items: center; + gap: var(--space-small); + + > input { + margin-inline-start: 0; + } + + > label { + display: inline-flex; + align-items: center; + gap: var(--space-small); + font-size: 0.9em; + } + + .icon { + -moz-context-properties: fill; + fill: currentColor; + background-image: var(--tool-icon); + background-size: var(--icon-size-default); + width: var(--icon-size-default); + height: var(--icon-size-default); + background-position: center; + background-repeat: no-repeat; + } + } +} diff --git a/browser/components/sidebar/sidebar-customize.html b/browser/components/sidebar/sidebar-customize.html new file mode 100644 index 0000000000..24ba42c210 --- /dev/null +++ b/browser/components/sidebar/sidebar-customize.html @@ -0,0 +1,31 @@ +<!-- 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 + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <meta + http-equiv="Content-Security-Policy" + content="default-src resource: chrome:; object-src 'none'; img-src data: chrome:;" + /> + <meta name="color-scheme" content="light dark" /> + <title data-l10n-id="sidebar-customize-header"></title> + <link rel="localization" href="branding/brand.ftl" /> + <link rel="localization" href="preview/sidebar.ftl" /> + <link + rel="stylesheet" + href="chrome://browser/content/sidebar/sidebar.css" + /> + <script + type="module" + src="chrome://browser/content/sidebar/sidebar-customize.mjs" + ></script> + <script src="chrome://browser/content/contentTheme.js"></script> + </head> + + <body> + <sidebar-customize /> + </body> +</html> diff --git a/browser/components/sidebar/sidebar-customize.mjs b/browser/components/sidebar/sidebar-customize.mjs new file mode 100644 index 0000000000..e4ad5fe5dd --- /dev/null +++ b/browser/components/sidebar/sidebar-customize.mjs @@ -0,0 +1,116 @@ +/* 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { html, styleMap } from "chrome://global/content/vendor/lit.all.mjs"; + +import { SidebarPage } from "./sidebar-page.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://global/content/elements/moz-button.mjs"; + +const l10nMap = new Map([ + ["viewHistorySidebar", "sidebar-customize-history"], + ["viewTabsSidebar", "sidebar-customize-synced-tabs"], +]); + +export class SidebarCustomize extends SidebarPage { + static queries = { + toolInputs: { all: ".customize-firefox-tools input" }, + }; + + connectedCallback() { + super.connectedCallback(); + window.addEventListener("SidebarItemChanged", this); + } + disconnectedCallback() { + super.disconnectedCallback(); + window.removeEventListener("SidebarItemChanged", this); + } + + get sidebarLauncher() { + return this.getWindow().document.querySelector("sidebar-launcher"); + } + + getWindow() { + return window.browsingContext.embedderWindowGlobal.browsingContext.window; + } + + closeCustomizeView(e) { + e.preventDefault(); + let view = e.target.getAttribute("view"); + this.getWindow().SidebarController.toggle(view); + } + + getTools() { + const toolsMap = new Map( + [...this.getWindow().SidebarController.toolsAndExtensions] + // eslint-disable-next-line no-unused-vars + .filter(([key, val]) => !val.extensionId) + ); + return toolsMap; + } + + handleEvent(e) { + switch (e.type) { + case "SidebarItemChanged": + this.requestUpdate(); + break; + } + } + + async onToggleInput(e) { + e.preventDefault(); + this.getWindow().SidebarController.toggleTool(e.target.id); + } + + getInputL10nId(view) { + return l10nMap.get(view); + } + + inputTemplate(tool) { + return html`<div class="input-wrapper"> + <input + type="checkbox" + id=${tool.view} + name=${tool.view} + @change=${this.onToggleInput} + ?checked=${!tool.disabled} + /> + <label for=${tool.view} + ><span class="icon ghost-icon" style=${styleMap({ + "--tool-icon": tool.icon, + })} role="presentation"/></span><span + data-l10n-id=${this.getInputL10nId(tool.view)} + ></span + ></label> + </div>`; + } + + render() { + return html` + ${this.stylesheet()} + <link rel="stylesheet" href="chrome://browser/content/sidebar/sidebar-customize.css"></link> + <div class="container"> + <div class="customize-header"> + <h2 data-l10n-id="sidebar-customize-header"></h2> + <moz-button + class="customize-close-button" + @click=${this.closeCustomizeView} + view="viewCustomizeSidebar" + size="default" + type="icon ghost" + > + </moz-button> + </div> + <div class="customize-firefox-tools"> + <h5 data-l10n-id="sidebar-customize-firefox-tools"></h5> + <div class="inputs"> + ${[...this.getTools().values()].map(tool => this.inputTemplate(tool))} + </div> + </div> + </div> + `; + } +} + +customElements.define("sidebar-customize", SidebarCustomize); diff --git a/browser/components/sidebar/sidebar-history.html b/browser/components/sidebar/sidebar-history.html index f1df5c507a..9544aa58d6 100644 --- a/browser/components/sidebar/sidebar-history.html +++ b/browser/components/sidebar/sidebar-history.html @@ -13,7 +13,6 @@ <meta name="color-scheme" content="light dark" /> <title data-l10n-id="firefoxview-page-title"></title> <link rel="localization" href="branding/brand.ftl" /> - <link rel="localization" href="toolkit/branding/accounts.ftl" /> <link rel="localization" href="browser/firefoxView.ftl" /> <link rel="localization" href="preview/sidebar.ftl" /> <link rel="localization" href="toolkit/branding/brandings.ftl" /> @@ -23,6 +22,18 @@ /> <script type="module" + src="chrome://browser/content/firefoxview/fxview-search-textbox.mjs" + ></script> + <script + type="module" + src="chrome://browser/content/firefoxview/fxview-tab-list.mjs" + ></script> + <script + type="module" + src="chrome://global/content/elements/moz-card.mjs" + ></script> + <script + type="module" src="chrome://browser/content/sidebar/sidebar-history.mjs" ></script> <script src="chrome://browser/content/contentTheme.js"></script> diff --git a/browser/components/sidebar/sidebar-history.mjs b/browser/components/sidebar/sidebar-history.mjs index 6c662b2c6f..c381d48eed 100644 --- a/browser/components/sidebar/sidebar-history.mjs +++ b/browser/components/sidebar/sidebar-history.mjs @@ -2,22 +2,25 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +const lazy = {}; + import { html, when } from "chrome://global/content/vendor/lit.all.mjs"; +import { navigateToLink } from "chrome://browser/content/firefoxview/helpers.mjs"; import { SidebarPage } from "./sidebar-page.mjs"; -// eslint-disable-next-line import/no-unassigned-import -import "chrome://browser/content/firefoxview/fxview-search-textbox.mjs"; -// eslint-disable-next-line import/no-unassigned-import -import "chrome://browser/content/firefoxview/fxview-tab-list.mjs"; -// eslint-disable-next-line import/no-unassigned-import -import "chrome://global/content/elements/moz-card.mjs"; -import { HistoryController } from "chrome://browser/content/firefoxview/HistoryController.mjs"; -import { navigateToLink } from "chrome://browser/content/firefoxview/helpers.mjs"; +ChromeUtils.defineESModuleGetters(lazy, { + HistoryController: "resource:///modules/HistoryController.sys.mjs", +}); const NEVER_REMEMBER_HISTORY_PREF = "browser.privatebrowsing.autostart"; export class SidebarHistory extends SidebarPage { + static queries = { + lists: { all: "fxview-tab-list" }, + searchTextbox: "fxview-search-textbox", + }; + constructor() { super(); this._started = false; @@ -25,13 +28,13 @@ export class SidebarHistory extends SidebarPage { this.maxTabsLength = -1; } - controller = new HistoryController(this, { + controller = new lazy.HistoryController(this, { component: "sidebar", }); connectedCallback() { super.connectedCallback(); - this.controller.updateAllHistoryItems(); + this.controller.updateCache(); } onPrimaryAction(e) { @@ -48,38 +51,34 @@ export class SidebarHistory extends SidebarPage { get cardsTemplate() { if (this.controller.searchResults) { return this.#searchResultsTemplate(); - } else if (this.controller.allHistoryItems.size) { + } else if (!this.controller.isHistoryEmpty) { return this.#historyCardsTemplate(); } return this.#emptyMessageTemplate(); } #historyCardsTemplate() { - let cardsTemplate = []; - this.controller.historyMapByDate.forEach(historyItem => { - if (historyItem.items.length) { - let dateArg = JSON.stringify({ date: historyItem.items[0].time }); - cardsTemplate.push(html`<moz-card - type="accordion" - data-l10n-attrs="heading" - data-l10n-id=${historyItem.l10nId} - data-l10n-args=${dateArg} - > - <div> - <fxview-tab-list - compactRows - class="with-context-menu" - maxTabsLength=${this.maxTabsLength} - .tabItems=${this.getTabItems(historyItem.items)} - @fxview-tab-list-primary-action=${this.onPrimaryAction} - .updatesPaused=${false} - > - </fxview-tab-list> - </div> - </moz-card>`); - } + return this.controller.historyVisits.map(historyItem => { + let dateArg = JSON.stringify({ date: historyItem.items[0].time }); + return html`<moz-card + type="accordion" + data-l10n-attrs="heading" + data-l10n-id=${historyItem.l10nId} + data-l10n-args=${dateArg} + > + <div> + <fxview-tab-list + compactRows + class="with-context-menu" + maxTabsLength=${this.maxTabsLength} + .tabItems=${this.getTabItems(historyItem.items)} + @fxview-tab-list-primary-action=${this.onPrimaryAction} + .updatesPaused=${false} + > + </fxview-tab-list> + </div> + </moz-card>`; }); - return cardsTemplate; } #emptyMessageTemplate() { @@ -154,12 +153,8 @@ export class SidebarHistory extends SidebarPage { </moz-card>`; } - async onChangeSortOption(e) { - await this.controller.onChangeSortOption(e); - } - - async onSearchQuery(e) { - await this.controller.onSearchQuery(e); + onSearchQuery(e) { + this.controller.onSearchQuery(e); } getTabItems(items) { @@ -188,14 +183,6 @@ export class SidebarHistory extends SidebarPage { </div> `; } - - willUpdate() { - if (this.controller.allHistoryItems.size) { - // onChangeSortOption() will update history data once it has been fetched - // from the API. - this.controller.createHistoryMaps(); - } - } } customElements.define("sidebar-history", SidebarHistory); diff --git a/browser/components/sidebar/sidebar-launcher.css b/browser/components/sidebar/sidebar-launcher.css deleted file mode 100644 index b033a650b3..0000000000 --- a/browser/components/sidebar/sidebar-launcher.css +++ /dev/null @@ -1,34 +0,0 @@ -/* 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 - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -.wrapper { - display: grid; - grid-template-rows: auto 1fr; - box-sizing: border-box; - height: 100%; - padding: var(--space-medium); - border-inline-end: 1px solid var(--chrome-content-separator-color); - background-color: var(--sidebar-background-color); - color: var(--sidebar-text-color); - :host([positionend]) & { - border-inline-start: 1px solid var(--chrome-content-separator-color); - border-inline-end: none;; - } -} - -:host([positionend]) { - .wrapper { - border-inline-start: 1px solid var(--chrome-content-separator-color); - } -} - -.actions-list { - display: flex; - flex-direction: column; - justify-content: end; -} - -.icon-button::part(button) { - background-image: var(--action-icon); -} diff --git a/browser/components/sidebar/sidebar-launcher.mjs b/browser/components/sidebar/sidebar-launcher.mjs deleted file mode 100644 index 85eb94b6ca..0000000000 --- a/browser/components/sidebar/sidebar-launcher.mjs +++ /dev/null @@ -1,169 +0,0 @@ -/* 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 - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -import { - html, - ifDefined, - styleMap, -} from "chrome://global/content/vendor/lit.all.mjs"; -import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; - -// eslint-disable-next-line import/no-unassigned-import -import "chrome://global/content/elements/moz-button.mjs"; - -/** - * Vertical strip attached to the launcher that provides an entry point - * to various sidebar panels. - * - */ -export default class SidebarLauncher extends MozLitElement { - static properties = { - topActions: { type: Array }, - bottomActions: { type: Array }, - selectedView: { type: String }, - open: { type: Boolean }, - }; - - constructor() { - super(); - this.topActions = [ - { - icon: `url("chrome://browser/skin/insights.svg")`, - view: null, - l10nId: "sidebar-launcher-insights", - }, - ]; - - this.bottomActions = [ - { - l10nId: "sidebar-menu-history", - icon: `url("chrome://browser/content/firefoxview/view-history.svg")`, - view: "viewHistorySidebar", - }, - { - l10nId: "sidebar-menu-bookmarks", - icon: `url("chrome://browser/skin/bookmark-hollow.svg")`, - view: "viewBookmarksSidebar", - }, - { - l10nId: "sidebar-menu-synced-tabs", - icon: `url("chrome://browser/skin/device-phone.svg")`, - view: "viewTabsSidebar", - }, - ]; - - this.selectedView = window.SidebarUI.currentID; - this.open = window.SidebarUI.isOpen; - this.menuMutationObserver = new MutationObserver(() => - this.#setExtensionItems() - ); - } - - connectedCallback() { - super.connectedCallback(); - this._sidebarBox = document.getElementById("sidebar-box"); - this._sidebarBox.addEventListener("sidebar-show", this); - this._sidebarBox.addEventListener("sidebar-hide", this); - this._sidebarMenu = document.getElementById("viewSidebarMenu"); - - this.menuMutationObserver.observe(this._sidebarMenu, { - childList: true, - subtree: true, - }); - this.#setExtensionItems(); - } - - disconnectedCallback() { - super.disconnectedCallback(); - this._sidebarBox.removeEventListener("sidebar-show", this); - this._sidebarBox.removeEventListener("sidebar-hide", this); - this.menuMutationObserver.disconnect(); - } - - getImageUrl(icon, targetURI) { - if (window.IS_STORYBOOK) { - return `chrome://global/skin/icons/defaultFavicon.svg`; - } - if (!icon) { - if (targetURI?.startsWith("moz-extension")) { - return "chrome://mozapps/skin/extensions/extension.svg"; - } - return `chrome://global/skin/icons/defaultFavicon.svg`; - } - // If the icon is not for website (doesn't begin with http), we - // display it directly. Otherwise we go through the page-icon - // protocol to try to get a cached version. We don't load - // favicons directly. - if (icon.startsWith("http")) { - return `page-icon:${targetURI}`; - } - return icon; - } - - #setExtensionItems() { - for (let item of this._sidebarMenu.children) { - if (item.id.endsWith("-sidebar-action")) { - this.topActions.push({ - tooltiptext: item.label, - icon: item.style.getPropertyValue("--webextension-menuitem-image"), - view: item.id.slice("menubar_menu_".length), - }); - } - } - } - - handleEvent(e) { - switch (e.type) { - case "sidebar-show": - this.selectedView = e.detail.viewId; - this.open = true; - break; - case "sidebar-hide": - this.open = false; - break; - } - } - - showView(e) { - let view = e.target.getAttribute("view"); - window.SidebarUI.toggle(view); - } - - buttonType(action) { - return this.open && action.view == this.selectedView - ? "icon" - : "icon ghost"; - } - - entrypointTemplate(action) { - return html`<moz-button - class="icon-button" - type=${this.buttonType(action)} - view=${action.view} - @click=${action.view ? this.showView : null} - title=${ifDefined(action.tooltiptext)} - data-l10n-id=${ifDefined(action.l10nId)} - style=${styleMap({ "--action-icon": action.icon })} - > - </moz-button>`; - } - - render() { - return html` - <link - rel="stylesheet" - href="chrome://browser/content/sidebar/sidebar-launcher.css" - /> - <div class="wrapper"> - <div class="top-actions actions-list"> - ${this.topActions.map(action => this.entrypointTemplate(action))} - </div> - <div class="bottom-actions actions-list"> - ${this.bottomActions.map(action => this.entrypointTemplate(action))} - </div> - </div> - `; - } -} -customElements.define("sidebar-launcher", SidebarLauncher); diff --git a/browser/components/sidebar/sidebar-main.css b/browser/components/sidebar/sidebar-main.css new file mode 100644 index 0000000000..14fac4d773 --- /dev/null +++ b/browser/components/sidebar/sidebar-main.css @@ -0,0 +1,29 @@ +/* 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 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +.wrapper { + display: grid; + grid-template-rows: auto 1fr; + box-sizing: border-box; + height: 100%; + padding: var(--space-medium); + border-inline-end: 1px solid var(--chrome-content-separator-color); + background-color: var(--sidebar-background-color); + color: var(--sidebar-text-color); + :host([positionend]) & { + border-inline-start: 1px solid var(--chrome-content-separator-color); + border-inline-end: none; + } +} + +.actions-list { + display: flex; + flex-direction: column; + justify-content: end; + gap: var(--space-xsmall); +} + +.icon-button::part(button) { + background-image: var(--action-icon); +} diff --git a/browser/components/sidebar/sidebar-main.mjs b/browser/components/sidebar/sidebar-main.mjs new file mode 100644 index 0000000000..c7c65d18e8 --- /dev/null +++ b/browser/components/sidebar/sidebar-main.mjs @@ -0,0 +1,163 @@ +/* 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { + html, + ifDefined, + styleMap, +} from "chrome://global/content/vendor/lit.all.mjs"; +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; + +// eslint-disable-next-line import/no-unassigned-import +import "chrome://global/content/elements/moz-button.mjs"; + +/** + * Sidebar with expanded and collapsed states that provides entry points + * to various sidebar panels and sidebar extensions. + */ +export default class SidebarMain extends MozLitElement { + static properties = { + bottomActions: { type: Array }, + selectedView: { type: String }, + sidebarItems: { type: Array }, + open: { type: Boolean }, + }; + + static queries = { + extensionButtons: { all: ".tools-and-extensions > moz-button[extension]" }, + toolButtons: { all: ".tools-and-extensions > moz-button:not([extension])" }, + customizeButton: ".bottom-actions > moz-button[view=viewCustomizeSidebar]", + }; + + constructor() { + super(); + this.bottomActions = []; + this.selectedView = window.SidebarController.currentID; + this.open = window.SidebarController.isOpen; + } + + connectedCallback() { + super.connectedCallback(); + this._sidebarBox = document.getElementById("sidebar-box"); + this._sidebarBox.addEventListener("sidebar-show", this); + this._sidebarBox.addEventListener("sidebar-hide", this); + + window.addEventListener("SidebarItemAdded", this); + window.addEventListener("SidebarItemChanged", this); + window.addEventListener("SidebarItemRemoved", this); + + this.setCustomize(); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this._sidebarBox.removeEventListener("sidebar-show", this); + this._sidebarBox.removeEventListener("sidebar-hide", this); + + window.removeEventListener("SidebarItemAdded", this); + window.removeEventListener("SidebarItemChanged", this); + window.removeEventListener("SidebarItemRemoved", this); + } + + getImageUrl(icon, targetURI) { + if (window.IS_STORYBOOK) { + return `chrome://global/skin/icons/defaultFavicon.svg`; + } + if (!icon) { + if (targetURI?.startsWith("moz-extension")) { + return "chrome://mozapps/skin/extensions/extension.svg"; + } + return `chrome://global/skin/icons/defaultFavicon.svg`; + } + // If the icon is not for website (doesn't begin with http), we + // display it directly. Otherwise we go through the page-icon + // protocol to try to get a cached version. We don't load + // favicons directly. + if (icon.startsWith("http")) { + return `page-icon:${targetURI}`; + } + return icon; + } + + getToolsAndExtensions() { + return window.SidebarController.toolsAndExtensions; + } + + setCustomize() { + this.bottomActions.push( + ...window.SidebarController.getSidebarPanels([ + "viewCustomizeSidebar", + ]).map(({ commandID, icon, revampL10nId }) => ({ + l10nId: revampL10nId, + icon, + view: commandID, + })) + ); + } + + handleEvent(e) { + switch (e.type) { + case "sidebar-show": + this.selectedView = e.detail.viewId; + this.open = true; + break; + case "sidebar-hide": + this.open = false; + break; + case "SidebarItemAdded": + case "SidebarItemChanged": + case "SidebarItemRemoved": + this.requestUpdate(); + break; + } + } + + showView(e) { + let view = e.target.getAttribute("view"); + window.SidebarController.toggle(view); + } + + buttonType(action) { + return this.open && action.view == this.selectedView + ? "icon" + : "icon ghost"; + } + + entrypointTemplate(action) { + return html`<moz-button + class="icon-button" + type=${this.buttonType(action)} + view=${action.view} + @click=${action.view ? this.showView : null} + title=${ifDefined(action.tooltiptext)} + data-l10n-id=${ifDefined(action.l10nId)} + style=${styleMap({ "--action-icon": action.icon })} + ?extension=${action.view?.includes("-sidebar-action")} + > + </moz-button>`; + } + + render() { + let toolsAndExtensions = this.getToolsAndExtensions() + ? this.getToolsAndExtensions() + : new Map(); + return html` + <link + rel="stylesheet" + href="chrome://browser/content/sidebar/sidebar-main.css" + /> + <div class="wrapper"> + <div class="tools-and-extensions actions-list"> + ${[...toolsAndExtensions.values()] + .filter(toolOrExtension => !toolOrExtension.disabled) + .map(action => this.entrypointTemplate(action))} + </div> + <div class="bottom-actions actions-list"> + ${this.bottomActions.map(action => this.entrypointTemplate(action))} + </div> + </div> + `; + } +} +customElements.define("sidebar-main", SidebarMain); diff --git a/browser/components/sidebar/sidebar-syncedtabs.html b/browser/components/sidebar/sidebar-syncedtabs.html index 6c4874b9ea..aa1ae7e2e2 100644 --- a/browser/components/sidebar/sidebar-syncedtabs.html +++ b/browser/components/sidebar/sidebar-syncedtabs.html @@ -13,7 +13,6 @@ <meta name="color-scheme" content="light dark" /> <link rel="localization" href="branding/brand.ftl" /> <link rel="localization" href="browser/firefoxView.ftl" /> - <link rel="localization" href="toolkit/branding/accounts.ftl" /> <link rel="localization" href="toolkit/branding/brandings.ftl" /> <link rel="stylesheet" href="chrome://global/skin/global.css" /> <script diff --git a/browser/components/sidebar/sidebar.ftl b/browser/components/sidebar/sidebar.ftl index 2a5ef75d83..e76fda863e 100644 --- a/browser/components/sidebar/sidebar.ftl +++ b/browser/components/sidebar/sidebar.ftl @@ -2,7 +2,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -sidebar-launcher-insights = +sidebar-main-insights = .title = Insights ## Variables: @@ -24,3 +24,10 @@ sidebar-history-date-prev-month = # $query (String) - The search query used for searching through browser history. sidebar-search-results-header = .heading = Search results for “{ $query }” + +sidebar-menu-customize = + .title = Customize sidebar +sidebar-customize-header = Customize sidebar +sidebar-customize-firefox-tools = { -brand-product-name } tools +sidebar-customize-history = History +sidebar-customize-synced-tabs = Tabs from other devices diff --git a/browser/components/sidebar/tests/browser/browser.toml b/browser/components/sidebar/tests/browser/browser.toml new file mode 100644 index 0000000000..5df032495c --- /dev/null +++ b/browser/components/sidebar/tests/browser/browser.toml @@ -0,0 +1,7 @@ +[DEFAULT] + +["browser_customize_sidebar.js"] + +["browser_extensions_sidebar.js"] + +["browser_history_sidebar.js"] diff --git a/browser/components/sidebar/tests/browser/browser_customize_sidebar.js b/browser/components/sidebar/tests/browser/browser_customize_sidebar.js new file mode 100644 index 0000000000..ab26823d08 --- /dev/null +++ b/browser/components/sidebar/tests/browser/browser_customize_sidebar.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(() => SpecialPowers.pushPrefEnv({ set: [["sidebar.revamp", true]] })); +registerCleanupFunction(() => SpecialPowers.popPrefEnv()); + +add_task(async function test_customize_sidebar_actions() { + const win = await BrowserTestUtils.openNewBrowserWindow(); + const { document } = win; + const sidebar = document.getElementById("sidebar-main"); + ok(sidebar, "Sidebar is shown."); + + const button = sidebar.customizeButton; + const promiseFocused = BrowserTestUtils.waitForEvent(win, "SidebarFocused"); + button.click(); + await promiseFocused; + let customizeDocument = win.SidebarController.browser.contentDocument; + const customizeComponent = + customizeDocument.querySelector("sidebar-customize"); + let toolEntrypointsCount = sidebar.toolButtons.length; + is( + customizeComponent.toolInputs.length, + toolEntrypointsCount, + `${toolEntrypointsCount} inputs to toggle Firefox Tools are shown in the Customize Menu.` + ); + for (const toolInput of customizeComponent.toolInputs) { + toolInput.click(); + await BrowserTestUtils.waitForCondition(() => { + let toggledTool = win.SidebarController.toolsAndExtensions.get( + toolInput.name + ); + return toggledTool.disabled; + }, `The entrypoint for ${toolInput.name} has been disabled in the sidebar.`); + toolEntrypointsCount = sidebar.toolButtons.length; + is( + toolEntrypointsCount, + 1, + `The button for the ${toolInput.name} entrypoint has been removed.` + ); + toolInput.click(); + await BrowserTestUtils.waitForCondition(() => { + let toggledTool = win.SidebarController.toolsAndExtensions.get( + toolInput.name + ); + return !toggledTool.disabled; + }, `The entrypoint for ${toolInput.name} has been re-enabled in the sidebar.`); + toolEntrypointsCount = sidebar.toolButtons.length; + is( + toolEntrypointsCount, + 2, + `The button for the ${toolInput.name} entrypoint has been added back.` + ); + // Check ordering + is( + sidebar.toolButtons[1].getAttribute("view"), + toolInput.name, + `The button for the ${toolInput.name} entrypoint has been added back to the end of the list of tools/extensions entrypoints` + ); + } + + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/sidebar/tests/browser/browser_extensions_sidebar.js b/browser/components/sidebar/tests/browser/browser_extensions_sidebar.js new file mode 100644 index 0000000000..0413849fd2 --- /dev/null +++ b/browser/components/sidebar/tests/browser/browser_extensions_sidebar.js @@ -0,0 +1,222 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_setup(() => SpecialPowers.pushPrefEnv({ set: [["sidebar.revamp", true]] })); +registerCleanupFunction(() => SpecialPowers.popPrefEnv()); + +const imageBuffer = imageBufferFromDataURI( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==" +); + +function imageBufferFromDataURI(encodedImageData) { + const decodedImageData = atob(encodedImageData); + return Uint8Array.from(decodedImageData, byte => byte.charCodeAt(0)).buffer; +} + +/* global browser */ +const extData = { + manifest: { + sidebar_action: { + default_icon: "default.png", + default_panel: "default.html", + default_title: "Default Title", + }, + }, + useAddonManager: "temporary", + + files: { + "default.html": ` + <!DOCTYPE html> + <html> + <head><meta charset="utf-8"/> + <script src="sidebar.js"></script> + </head> + <body> + A Test Sidebar + </body></html> + `, + "sidebar.js": function () { + window.onload = () => { + browser.test.sendMessage("sidebar"); + }; + }, + "1.html": ` + <!DOCTYPE html> + <html> + <head><meta charset="utf-8"/></head> + <body> + A Test Sidebar + </body></html> + `, + "default.png": imageBuffer, + "1.png": imageBuffer, + }, + + background() { + browser.test.onMessage.addListener(async ({ msg, data }) => { + switch (msg) { + case "set-icon": + await browser.sidebarAction.setIcon({ path: data }); + break; + case "set-panel": + await browser.sidebarAction.setPanel({ panel: data }); + break; + case "set-title": + await browser.sidebarAction.setTitle({ title: data }); + break; + } + browser.test.sendMessage("done"); + }); + }, +}; + +async function sendMessage(extension, msg, data) { + extension.sendMessage({ msg, data }); + await extension.awaitMessage("done"); +} + +add_task(async function test_extension_sidebar_actions() { + // TODO: Once `sidebar.revamp` is either enabled by default, or removed + // entirely, this test should run in the current window, and it should only + // await one "sidebar" message. + const win = await BrowserTestUtils.openNewBrowserWindow(); + const { document } = win; + const sidebar = document.getElementById("sidebar-main"); + ok(sidebar, "Sidebar is shown."); + + const extension = ExtensionTestUtils.loadExtension({ ...extData }); + await extension.startup(); + await extension.awaitMessage("sidebar"); + await extension.awaitMessage("sidebar"); + is(sidebar.extensionButtons.length, 1, "Extension is shown in the sidebar."); + + // Default icon and title matches. + const button = sidebar.extensionButtons[0]; + let iconUrl = `moz-extension://${extension.uuid}/default.png`; + is( + button.style.getPropertyValue("--action-icon"), + `image-set(url("${iconUrl}"), url("${iconUrl}") 2x)`, + "Extension has the correct icon." + ); + is(button.title, "Default Title", "Extension has the correct title."); + + // Icon can be updated. + await sendMessage(extension, "set-icon", "1.png"); + await sidebar.updateComplete; + iconUrl = `moz-extension://${extension.uuid}/1.png`; + is( + button.style.getPropertyValue("--action-icon"), + `image-set(url("${iconUrl}"), url("${iconUrl}") 2x)`, + "Extension has updated icon." + ); + + // Title can be updated. + await sendMessage(extension, "set-title", "Updated Title"); + await sidebar.updateComplete; + is(button.title, "Updated Title", "Extension has updated title."); + + // Panel can be updated. + await sendMessage(extension, "set-panel", "1.html"); + const panelUrl = `moz-extension://${extension.uuid}/1.html`; + await TestUtils.waitForCondition(() => { + const browser = SidebarController.browser.contentDocument.getElementById( + "webext-panels-browser" + ); + return browser.currentURI.spec === panelUrl; + }, "The new panel is visible."); + + await extension.unload(); + await sidebar.updateComplete; + is( + sidebar.extensionButtons.length, + 0, + "Extension is removed from the sidebar." + ); + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_open_new_window_after_install() { + const extension = ExtensionTestUtils.loadExtension({ ...extData }); + await extension.startup(); + await extension.awaitMessage("sidebar"); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + const { document } = win; + const sidebar = document.getElementById("sidebar-main"); + ok(sidebar, "Sidebar is shown."); + await extension.awaitMessage("sidebar"); + is( + sidebar.extensionButtons.length, + 1, + "Extension is shown in new browser window." + ); + + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url: "about:addons" }, + async browser => { + await BrowserTestUtils.synthesizeMouseAtCenter( + "categories-box button[name=extension]", + {}, + browser + ); + const extensionToggle = await TestUtils.waitForCondition( + () => + browser.contentDocument.querySelector( + `addon-card[addon-id="${extension.id}"] moz-toggle` + ), + "Toggle button for extension is shown." + ); + + let promiseEvent = BrowserTestUtils.waitForEvent( + win, + "SidebarItemRemoved" + ); + extensionToggle.click(); + await promiseEvent; + await sidebar.updateComplete; + is(sidebar.extensionButtons.length, 0, "The extension is disabled."); + + promiseEvent = BrowserTestUtils.waitForEvent(win, "SidebarItemAdded"); + extensionToggle.click(); + await promiseEvent; + await sidebar.updateComplete; + is(sidebar.extensionButtons.length, 1, "The extension is enabled."); + } + ); + + await extension.unload(); + await sidebar.updateComplete; + is( + sidebar.extensionButtons.length, + 0, + "Extension is removed from the sidebar." + ); + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_open_new_private_window_after_install() { + const extension = ExtensionTestUtils.loadExtension({ ...extData }); + await extension.startup(); + await extension.awaitMessage("sidebar"); + + const privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + const { document } = privateWin; + const sidebar = document.getElementById("sidebar-main"); + ok(sidebar, "Sidebar is shown."); + await TestUtils.waitForCondition( + () => sidebar.extensionButtons, + "Extensions container is shown." + ); + is( + sidebar.extensionButtons.length, + 0, + "Extension is hidden in private browser window." + ); + + await extension.unload(); + await BrowserTestUtils.closeWindow(privateWin); +}); diff --git a/browser/components/sidebar/tests/browser/browser_history_sidebar.js b/browser/components/sidebar/tests/browser/browser_history_sidebar.js new file mode 100644 index 0000000000..a498eb4b8e --- /dev/null +++ b/browser/components/sidebar/tests/browser/browser_history_sidebar.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const URLs = [ + "http://mochi.test:8888/browser/", + "https://www.example.com/", + "https://example.net/", + "https://example.org/", +]; + +const today = new Date(); +const yesterday = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate() - 1 +); +const dates = [today, yesterday]; + +let win; + +add_setup(async () => { + await SpecialPowers.pushPrefEnv({ set: [["sidebar.revamp", true]] }); + const pageInfos = URLs.flatMap((url, i) => + dates.map(date => ({ + url, + title: `Example Domain ${i}`, + visits: [{ date }], + })) + ); + await PlacesUtils.history.insertMany(pageInfos); + win = await BrowserTestUtils.openNewBrowserWindow(); +}); + +registerCleanupFunction(async () => { + await SpecialPowers.popPrefEnv(); + await PlacesUtils.history.clear(); + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_history_cards_created() { + const { SidebarController } = win; + await SidebarController.show("viewHistorySidebar"); + const document = SidebarController.browser.contentDocument; + const component = document.querySelector("sidebar-history"); + await component.updateComplete; + const { lists } = component; + + Assert.equal(lists.length, dates.length, "There is a card for each day."); + for (const list of lists) { + Assert.equal( + list.tabItems.length, + URLs.length, + "Card shows the correct number of visits." + ); + } + + SidebarController.hide(); +}); + +add_task(async function test_history_search() { + const { SidebarController } = win; + await SidebarController.show("viewHistorySidebar"); + const { contentDocument: document, contentWindow } = + SidebarController.browser; + const component = document.querySelector("sidebar-history"); + await component.updateComplete; + const { searchTextbox } = component; + + info("Input a search query."); + EventUtils.synthesizeMouseAtCenter(searchTextbox, {}, contentWindow); + EventUtils.sendString("Example Domain 1", contentWindow); + await BrowserTestUtils.waitForMutationCondition( + component.shadowRoot, + { childList: true, subtree: true }, + () => + component.lists.length === 1 && + component.shadowRoot.querySelector( + "moz-card[data-l10n-id=sidebar-search-results-header]" + ) + ); + await TestUtils.waitForCondition(() => { + const { rowEls } = component.lists[0]; + return rowEls.length === 1 && rowEls[0].mainEl.href === URLs[1]; + }, "There is one matching search result."); + + info("Input a bogus search query."); + EventUtils.synthesizeMouseAtCenter(searchTextbox, {}, contentWindow); + EventUtils.sendString("Bogus Query", contentWindow); + await TestUtils.waitForCondition(() => { + const tabList = component.lists[0]; + return tabList?.emptyState; + }, "There are no matching search results."); + + SidebarController.hide(); +}); |