summaryrefslogtreecommitdiffstats
path: root/browser/components/sidebar
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/components/sidebar/browser-sidebar.js424
-rw-r--r--browser/components/sidebar/jar.mn7
-rw-r--r--browser/components/sidebar/moz.build2
-rw-r--r--browser/components/sidebar/sidebar-customize.css57
-rw-r--r--browser/components/sidebar/sidebar-customize.html31
-rw-r--r--browser/components/sidebar/sidebar-customize.mjs116
-rw-r--r--browser/components/sidebar/sidebar-history.html13
-rw-r--r--browser/components/sidebar/sidebar-history.mjs85
-rw-r--r--browser/components/sidebar/sidebar-launcher.css34
-rw-r--r--browser/components/sidebar/sidebar-launcher.mjs169
-rw-r--r--browser/components/sidebar/sidebar-main.css29
-rw-r--r--browser/components/sidebar/sidebar-main.mjs163
-rw-r--r--browser/components/sidebar/sidebar-syncedtabs.html1
-rw-r--r--browser/components/sidebar/sidebar.ftl9
-rw-r--r--browser/components/sidebar/tests/browser/browser.toml7
-rw-r--r--browser/components/sidebar/tests/browser/browser_customize_sidebar.js64
-rw-r--r--browser/components/sidebar/tests/browser/browser_extensions_sidebar.js222
-rw-r--r--browser/components/sidebar/tests/browser/browser_history_sidebar.js97
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();
+});