diff --git a/browser/components/tabbrowser/GroupsList.sys.mjs b/browser/components/tabbrowser/GroupsList.sys.mjs
new file mode 100644
index 000000000000..944476e7eca4
--- /dev/null
+++ b/browser/components/tabbrowser/GroupsList.sys.mjs
@@ -0,0 +1,132 @@
+/* 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/. */
+
+export class GroupsPanel {
+ constructor({ view, containerNode }) {
+ this.view = view;
+
+ this.containerNode = containerNode;
+ this.win = containerNode.ownerGlobal;
+ this.doc = containerNode.ownerDocument;
+ this.panelMultiView = null;
+ this.view.addEventListener("ViewShowing", this);
+ }
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "ViewShowing":
+ if (event.target == this.view) {
+ this.panelMultiView = this.view.panelMultiView;
+ this.#populate(event);
+ }
+ break;
+ case "PanelMultiViewHidden":
+ if ((this.panelMultiView = event.target)) {
+ this.#cleanup();
+ this.panelMultiView = null;
+ }
+
+ break;
+ case "command":
+ this.#handleCommand(event);
+ break;
+ }
+ }
+
+ #handleCommand(event) {
+ let groupId = event.target.closest("toolbaritem").groupId;
+
+ switch (event.target.dataset.command) {
+ case "allTabsGroupView_selectGroup":
+ let group = this.win.gBrowser.getTabGroupById(groupId);
+ group.select();
+ group.ownerGlobal.focus();
+
+ break;
+ case "allTabsGroupView_restoreGroup":
+ this.win.SessionStore.openSavedTabGroup(groupId, this.win);
+ break;
+ }
+ }
+
+ #setupListeners() {
+ this.view.addEventListener("command", this);
+ this.view.panelMultiView.addEventListener("PanelMultiViewHidden", this);
+ }
+
+ #cleanup() {
+ this.containerNode.innerHTML = "";
+ this.view.removeEventListener("command", this);
+ }
+
+ #populate() {
+ let fragment = this.doc.createDocumentFragment();
+ let otherWindowGroups = this.win.gBrowser
+ .getAllTabGroups()
+ .filter(group => {
+ return group.ownerGlobal !== this.win;
+ });
+ let savedGroups = this.win.SessionStore.savedGroups;
+
+ if (savedGroups.length + otherWindowGroups.length > 0) {
+ let header = this.doc.createElement("h2");
+ header.setAttribute("class", "subview-subheader");
+ this.doc.l10n.setAttributes(header, "tab-group-menu-header");
+ fragment.appendChild(header);
+ }
+ for (let groupData of otherWindowGroups) {
+ fragment.appendChild(this.#createRow(groupData));
+ }
+ for (let groupData of savedGroups) {
+ fragment.appendChild(this.#createRow(groupData, "closed"));
+ }
+ this.containerNode.appendChild(fragment);
+ this.#setupListeners();
+ }
+
+ #createRow(group, kind = "open") {
+ let { doc } = this;
+ let row = doc.createXULElement("toolbaritem");
+ row.setAttribute("class", "all-tabs-item all-tabs-group-item");
+ row.setAttribute("context", "tabContextMenu");
+ row.style.setProperty(
+ "--tab-group-color",
+ `var(--tab-group-color-${group.color})`
+ );
+ row.style.setProperty(
+ "--tab-group-color-invert",
+ `var(--tab-group-color-${group.color}-invert)`
+ );
+ row.style.setProperty(
+ "--tab-group-color-pale",
+ `var(--tab-group-color-${group.color}-pale)`
+ );
+ row.groupId = group.id;
+ let button = doc.createXULElement("toolbarbutton");
+ button.setAttribute(
+ "class",
+ "all-tabs-button subviewbutton subviewbutton-iconic all-tabs-group-action-button"
+ );
+ if (kind != "open") {
+ button.classList.add("all-tabs-group-saved-group");
+ button.dataset.command = "allTabsGroupView_restoreGroup";
+ } else {
+ button.dataset.command = "allTabsGroupView_selectGroup";
+ }
+ button.setAttribute("flex", "1");
+ button.setAttribute("crop", "end");
+
+ if (group.name) {
+ button.setAttribute("label", group.name);
+ } else {
+ doc.l10n
+ .formatValues([{ id: "tab-group-name-default" }])
+ .then(([msg]) => {
+ button.setAttribute("label", msg);
+ });
+ }
+ row.appendChild(button);
+ return row;
+ }
+}
diff --git a/browser/components/tabbrowser/TabsList.sys.mjs b/browser/components/tabbrowser/TabsList.sys.mjs
index 44878afb8ff5..7c08d02f2b9a 100644
--- a/browser/components/tabbrowser/TabsList.sys.mjs
+++ b/browser/components/tabbrowser/TabsList.sys.mjs
@@ -102,9 +102,14 @@ class TabsListBase {
*/
_populate() {
let fragment = this.doc.createDocumentFragment();
+ let currentGroupId;
for (let tab of this.gBrowser.tabs) {
if (this.filterFn(tab)) {
+ if (tab.group && tab.group.id != currentGroupId) {
+ fragment.appendChild(this._createGroupRow(tab.group));
+ currentGroupId = tab.group.id;
+ }
fragment.appendChild(this._createRow(tab));
}
}
@@ -121,6 +126,10 @@ class TabsListBase {
* Remove the menuitems from the DOM, cleanup internal state and listeners.
*/
_cleanup() {
+ this.doc
+ .querySelectorAll(".all-tabs-group-button")
+ .forEach(node => node.remove());
+
for (let item of this.rows) {
item.remove();
}
@@ -262,6 +271,9 @@ export class TabsPanel extends TabsListBase {
this.gBrowser.removeTab(event.target.tab);
break;
}
+ if (event.target.classList.contains("all-tabs-group-button")) {
+ this.gBrowser.getTabGroupById(event.target.groupId).select();
+ }
// fall through
default:
super.handleEvent(event);
@@ -275,7 +287,10 @@ export class TabsPanel extends TabsListBase {
// The loading throbber can't be set until the toolbarbutton is rendered,
// so set the image attributes again now that the elements are in the DOM.
for (let row of this.rows) {
- this._setImageAttributes(row, row.tab);
+ // Ensure this isn't a group label
+ if (row.tab) {
+ this._setImageAttributes(row, row.tab);
+ }
}
}
@@ -324,6 +339,10 @@ export class TabsPanel extends TabsListBase {
});
}
+ if (tab.group) {
+ row.classList.add("grouped");
+ }
+
row.appendChild(button);
let muteButton = doc.createXULElement("toolbarbutton");
@@ -352,6 +371,49 @@ export class TabsPanel extends TabsListBase {
return row;
}
+ _createGroupRow(group) {
+ let { doc } = this;
+ let row = doc.createXULElement("toolbaritem");
+ row.setAttribute("class", "all-tabs-item all-tabs-group-item");
+ row.setAttribute("context", "none");
+ row.style.setProperty(
+ "--tab-group-color",
+ `var(--tab-group-color-${group.color})`
+ );
+ row.style.setProperty(
+ "--tab-group-color-invert",
+ `var(--tab-group-color-${group.color}-invert)`
+ );
+ row.style.setProperty(
+ "--tab-group-color-pale",
+ `var(--tab-group-color-${group.color}-pale)`
+ );
+ row.addEventListener("command", this);
+ let button = doc.createXULElement("toolbarbutton");
+ button.setAttribute(
+ "class",
+ "all-tabs-button all-tabs-group-button subviewbutton subviewbutton-iconic"
+ );
+ button.setAttribute("flex", "1");
+ button.setAttribute("crop", "end");
+ button.group = group;
+ button.groupId = group.id;
+
+ if (group.label) {
+ button.label = group.label;
+ } else {
+ doc.l10n
+ .formatValues([{ id: "tab-group-name-default" }])
+ .then(([msg]) => {
+ button.label = msg;
+ });
+ }
+
+ button.image = "chrome://browser/skin/tabbrowser/tab-group-chicklet.svg";
+ row.appendChild(button);
+ return row;
+ }
+
_setRowAttributes(row, tab) {
setAttributes(row, { selected: tab.selected });
diff --git a/browser/components/tabbrowser/content/browser-allTabsMenu.inc.xhtml b/browser/components/tabbrowser/content/browser-allTabsMenu.inc.xhtml
index 61d48237a63b..6349ed3a784f 100644
--- a/browser/components/tabbrowser/content/browser-allTabsMenu.inc.xhtml
+++ b/browser/components/tabbrowser/content/browser-allTabsMenu.inc.xhtml
@@ -23,6 +23,9 @@
class="subviewbutton subviewbutton-nav"
closemenu="none"
data-l10n-id="all-tabs-menu-hidden-tabs"/>
+
+
+