commit b0b5d233f78d7599dad7edeccd0bf0157c55ed2c
parent 24f55b8044c6eeee335ee5d738a32af57553dab0
Author: DJ <dj@walker.dev>
Date: Wed, 19 Nov 2025 19:36:42 +0000
Bug 1994525 - Add panel to create/edit Tab Notes. r=sthompson,fluent-reviewers,desktop-theme-reviewers,tabbrowser-reviewers,bolsson,kcochrane
Differential Revision: https://phabricator.services.mozilla.com/D271430
Diffstat:
9 files changed, 371 insertions(+), 0 deletions(-)
diff --git a/browser/base/content/browser-main.js b/browser/base/content/browser-main.js
@@ -26,6 +26,7 @@
Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser/tabbrowser.js", this);
Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser/tabgroup.js", this);
Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser/tabgroup-menu.js", this);
+ Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser/tabnote-menu.js", this);
Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser/tabs.js", this);
Services.scriptloader.loadSubScript("chrome://browser/content/tabbrowser/tabsplitview.js", this);
Services.scriptloader.loadSubScript("chrome://browser/content/places/places-menupopup.js", this);
diff --git a/browser/base/content/main-popupset.inc.xhtml b/browser/base/content/main-popupset.inc.xhtml
@@ -69,6 +69,8 @@
data-lazy-l10n-id="bookmark-selected-tabs"/>
<menuitem id="context_bookmarkTab"
data-lazy-l10n-id="tab-context-bookmark-tab"/>
+ <menuitem id="context_addNote" hidden="true" data-lazy-l10n-id="tab-context-add-note" />
+ <menuitem id="context_editNote" hidden="true" data-lazy-l10n-id="tab-context-edit-note" />
<menu id="context_moveTabOptions"
data-lazy-l10n-id="tab-context-move-tabs"
data-l10n-args='{"tabCount": 1}'>
@@ -148,6 +150,7 @@
<!-- for search and content formfill/pw manager -->
<tabgroup-menu id="tab-group-editor"></tabgroup-menu>
+ <tabnote-menu id="tab-note-menu"></tabnote-menu>
<!-- Starting point for sidebar tools overflow -->
<panel id="sidebar-tools-overflow"
diff --git a/browser/base/content/main-popupset.js b/browser/base/content/main-popupset.js
@@ -84,6 +84,10 @@ document.addEventListener(
case "context_bookmarkTab":
PlacesCommandHook.bookmarkTabs([TabContextMenu.contextTab]);
break;
+ case "context_addNote":
+ case "context_editNote":
+ gBrowser.tabNoteMenu.openPanel(TabContextMenu.contextTab);
+ break;
case "context_moveToStart":
gBrowser.moveTabsToStart(TabContextMenu.contextTab);
break;
diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js
@@ -94,6 +94,7 @@
init() {
this.tabContainer = document.getElementById("tabbrowser-tabs");
this.tabGroupMenu = document.getElementById("tab-group-editor");
+ this.tabNoteMenu = document.getElementById("tab-note-menu");
this.tabbox = document.getElementById("tabbrowser-tabbox");
this.tabpanels = document.getElementById("tabbrowser-tabpanels");
this.pinnedTabsContainer = document.getElementById(
@@ -162,6 +163,12 @@
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
+ "_tabNotesEnabled",
+ "browser.tabs.notes.enabled",
+ false
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
"showPidAndActiveness",
"browser.tabs.tooltipsShowPidAndActiveness",
false
@@ -9683,6 +9690,18 @@ var TabContextMenu = {
contextUngroupSplitView.hidden = true;
}
+ let contextAddNote = document.getElementById("context_addNote");
+ let contextEditNote = document.getElementById("context_editNote");
+ if (gBrowser._tabNotesEnabled) {
+ let noteURL = this.contextTab.linkedBrowser.currentURI.spec;
+ let hasNote = gBrowser.TabNotes.has(noteURL);
+ contextAddNote.hidden = hasNote;
+ contextEditNote.hidden = !hasNote;
+ } else {
+ contextAddNote.hidden = true;
+ contextEditNote.hidden = true;
+ }
+
// Split View
let splitViewEnabled = Services.prefs.getBoolPref(
"browser.tabs.splitView.enabled",
diff --git a/browser/components/tabbrowser/content/tabnote-menu.js b/browser/components/tabbrowser/content/tabnote-menu.js
@@ -0,0 +1,179 @@
+/* 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/. */
+
+"use strict";
+
+// This is loaded into chrome windows with the subscript loader. Wrap in
+// a block to prevent accidentally leaking globals onto `window`.
+{
+ class MozTabbrowserTabNoteMenu extends MozXULElement {
+ static markup = /*html*/ `
+ <panel
+ id="tabNotePanel"
+ type="arrow"
+ titlebar="normal"
+ class="tab-note-editor-panel"
+ orient="vertical"
+ role="dialog"
+ ignorekeys="true"
+ norolluponanchor="true"
+ aria-labelledby="tab-note-editor-title"
+ consumeoutsideclicks="false">
+
+ <html:div class="panel-header" >
+ <html:h1
+ id="tab-note-editor-title">
+ </html:h1>
+ </html:div>
+
+ <toolbarseparator />
+
+ <html:div
+ class="panel-body
+ tab-note-editor-name">
+ <html:textarea
+ id="tab-note-text"
+ name="tab-note-text"
+ rows="3"
+ value=""
+ data-l10n-id="tab-note-editor-text-field"
+ ></html:textarea>
+ </html:div>
+
+ <html:moz-button-group
+ class="tab-note-create-actions tab-note-create-mode-only"
+ id="tab-note-default-actions">
+ <html:moz-button
+ id="tab-note-editor-button-cancel"
+ data-l10n-id="tab-note-editor-button-cancel">
+ </html:moz-button>
+ <html:moz-button
+ type="primary"
+ id="tab-note-editor-button-save"
+ data-l10n-id="tab-note-editor-button-save">
+ </html:moz-button>
+ </html:moz-button-group>
+
+ </panel>
+ `;
+
+ #initialized = false;
+ #panel;
+ #noteField;
+ #titleNode;
+ #currentTab = null;
+ #createMode;
+
+ connectedCallback() {
+ if (this.#initialized) {
+ return;
+ }
+
+ this.textContent = "";
+ this.appendChild(this.constructor.fragment);
+ this.initializeAttributeInheritance();
+
+ this.#panel = this.querySelector("panel");
+ this.#noteField = document.getElementById("tab-note-text");
+ this.#titleNode = document.getElementById("tab-note-editor-title");
+
+ this.querySelector("#tab-note-editor-button-cancel").addEventListener(
+ "click",
+ () => {
+ this.#panel.hidePopup();
+ }
+ );
+ this.querySelector("#tab-note-editor-button-save").addEventListener(
+ "click",
+ () => {
+ this.saveNote();
+ }
+ );
+ this.#panel.addEventListener("keypress", this);
+ this.#panel.addEventListener("popuphidden", this);
+
+ this.#initialized = true;
+ }
+
+ on_keypress(event) {
+ if (event.defaultPrevented) {
+ // The event has already been consumed inside of the panel.
+ return;
+ }
+
+ switch (event.keyCode) {
+ case KeyEvent.DOM_VK_ESCAPE:
+ this.#panel.hidePopup();
+ break;
+ case KeyEvent.DOM_VK_RETURN:
+ this.saveNote();
+ break;
+ }
+ }
+
+ on_popuphidden() {
+ this.#currentTab = null;
+ this.#noteField.value = "";
+ }
+
+ get createMode() {
+ return this.#createMode;
+ }
+
+ set createMode(createModeEnabled) {
+ if (this.#createMode == createModeEnabled) {
+ return;
+ }
+ let headerL10nId = createModeEnabled
+ ? "tab-note-editor-title-create"
+ : "tab-note-editor-title-edit";
+ this.#titleNode.innerText =
+ gBrowser.tabLocalization.formatValueSync(headerL10nId);
+ this.#createMode = createModeEnabled;
+ }
+
+ get #panelPosition() {
+ if (gBrowser.tabContainer.verticalMode) {
+ return SidebarController._positionStart
+ ? "topleft topright"
+ : "topright topleft";
+ }
+ return "bottomleft topleft";
+ }
+
+ openPanel(tab) {
+ this.#currentTab = tab;
+ let url = this.#currentTab.linkedBrowser.currentURI.spec;
+ let note = gBrowser.TabNotes.get(url);
+
+ if (note) {
+ this.createMode = false;
+ this.#noteField.value = note;
+ } else {
+ this.createMode = true;
+ }
+ this.#panel.addEventListener(
+ "popupshown",
+ () => {
+ this.#noteField.focus();
+ },
+ {
+ once: true,
+ }
+ );
+ this.#panel.openPopup(tab, {
+ position: this.#panelPosition,
+ });
+ }
+
+ saveNote() {
+ let url = this.#currentTab.linkedBrowser.currentURI.spec;
+ let note = this.#noteField.value;
+ gBrowser.TabNotes.set(url, note);
+ this.#panel.hidePopup();
+ }
+ }
+
+ customElements.define("tabnote-menu", MozTabbrowserTabNoteMenu);
+}
diff --git a/browser/components/tabbrowser/jar.mn b/browser/components/tabbrowser/jar.mn
@@ -14,5 +14,6 @@ browser.jar:
content/browser/tabbrowser/tabbrowser.js (content/tabbrowser.js)
content/browser/tabbrowser/tabgroup.js (content/tabgroup.js)
content/browser/tabbrowser/tabgroup-menu.js (content/tabgroup-menu.js)
+ content/browser/tabbrowser/tabnote-menu.js (content/tabnote-menu.js)
content/browser/tabbrowser/tabs.js (content/tabs.js)
content/browser/tabbrowser/tabsplitview.js (content/tabsplitview.js)
diff --git a/browser/components/tabbrowser/test/browser/tabs/browser_tab_notes.js b/browser/components/tabbrowser/test/browser/tabs/browser_tab_notes.js
@@ -2,6 +2,8 @@
* 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/. */
+"use strict";
+
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [["browser.tabs.notes.enabled", true]],
@@ -66,3 +68,115 @@ add_task(async function tabNotesSanitizationTests() {
Assert.equal(result, correctValue, "TabNotes.set truncates note length");
});
+
+/**
+ * Tab note menu tests
+ */
+
+async function openTabNoteMenu(tab) {
+ let tabContextMenu = await getContextMenu(tab, "tabContextMenu");
+ let tabNotePanel = document.getElementById("tabNotePanel");
+ let panelShown = BrowserTestUtils.waitForPopupEvent(tabNotePanel, "shown");
+ tabContextMenu.activateItem(document.getElementById("context_addNote"));
+ await panelShown;
+ return tabNotePanel;
+}
+
+async function closeTabNoteMenu() {
+ let tabNotePanel = document.getElementById("tabNotePanel");
+ let menuHidden = BrowserTestUtils.waitForPopupEvent(tabNotePanel, "hidden");
+ tabNotePanel.hidePopup();
+ return menuHidden;
+}
+
+add_task(async function test_openTabNotePanelFromContextMenu() {
+ // open context menu with tab notes disabled
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.notes.enabled", false]],
+ });
+ let tab = await addTab("about:blank");
+ let addNoteElement = document.getElementById("context_addNote");
+ let tabContextMenu = await getContextMenu(tab, "tabContextMenu");
+ Assert.ok(
+ addNoteElement.hidden,
+ "'Add Note' is hidden from context menu when pref disabled"
+ );
+ await closeContextMenu(tabContextMenu);
+ await SpecialPowers.popPrefEnv();
+
+ // open context menu with tab notes enabled
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.notes.enabled", true]],
+ });
+ tabContextMenu = await getContextMenu(tab, "tabContextMenu");
+ Assert.ok(
+ !addNoteElement.hidden,
+ "'Add Note' is visible in context menu when pref enabled"
+ );
+ let tabNotePanel = document.getElementById("tabNotePanel");
+
+ // open panel from context menu
+ let panelShown = BrowserTestUtils.waitForPopupEvent(tabNotePanel, "shown");
+ Assert.equal(tabNotePanel.state, "closed", "Tab note panel starts hidden");
+ tabContextMenu.activateItem(addNoteElement);
+ await panelShown;
+ Assert.equal(
+ tabNotePanel.state,
+ "open",
+ "Tab note panel appears after clicking context menu item"
+ );
+ await closeTabNoteMenu();
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_dismissTabNotePanel() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.notes.enabled", true]],
+ });
+
+ // Dismiss panel by pressing Esc
+ let tab = await addTab("about:blank");
+ let tabNoteMenu = await openTabNoteMenu(tab);
+ Assert.equal(tabNoteMenu.state, "open", "Tab note menu is open");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await BrowserTestUtils.waitForPopupEvent(tabNoteMenu, "hidden");
+ Assert.equal(
+ tabNoteMenu.state,
+ "closed",
+ "Tab note menu closes after pressing Esc"
+ );
+
+ // Dismiss panel by clicking Cancel
+ tabNoteMenu = await openTabNoteMenu(tab);
+ Assert.equal(tabNoteMenu.state, "open", "Tab note menu is open");
+ let menuHidden = BrowserTestUtils.waitForPopupEvent(tabNoteMenu, "hidden");
+ let cancelButton = document.getElementById("tab-note-editor-button-cancel");
+ cancelButton.click();
+ await menuHidden;
+ Assert.equal(
+ tabNoteMenu.state,
+ "closed",
+ "Tab note menu closes after clicking cancel button"
+ );
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_saveTabNote() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.notes.enabled", true]],
+ });
+ let tab = await addTab("about:blank");
+ let tabNoteMenu = await openTabNoteMenu(tab);
+ tabNoteMenu.querySelector("textarea").value = "Lorem ipsum dolor";
+ let menuHidden = BrowserTestUtils.waitForPopupEvent(tabNoteMenu, "hidden");
+ tabNoteMenu.querySelector("#tab-note-editor-button-save").click();
+ await menuHidden;
+
+ Assert.equal(gBrowser.TabNotes.get("about:blank"), "Lorem ipsum dolor");
+
+ gBrowser.TabNotes.delete("about:blank");
+ BrowserTestUtils.removeTab(tab);
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/locales/en-US/browser/tabbrowser.ftl b/browser/locales/en-US/browser/tabbrowser.ftl
@@ -381,6 +381,25 @@ tab-group-context-open-saved-group-in-this-window =
tab-group-context-open-saved-group-in-new-window =
.label = Open Group in New Window
+## Tab Notes
+
+tab-context-add-note =
+ .label = Add Note
+ .accesskey = A
+tab-context-edit-note =
+ .label = Edit Note
+ .accesskey = E
+tab-note-editor-title-create = Add note
+tab-note-editor-title-edit = Edit note
+tab-note-editor-text-field =
+ .placeholder = What do you want to remember about this tab?
+tab-note-editor-button-cancel =
+ .label = Cancel
+ .accesskey = C
+tab-note-editor-button-save =
+ .label = Save
+ .accesskey = S
+
## Split View
# Split view tabs display their respective contents side by side
diff --git a/browser/themes/shared/tabbrowser/tabs.css b/browser/themes/shared/tabbrowser/tabs.css
@@ -1860,6 +1860,37 @@ tab-group {
}
}
+/* Tab Notes */
+.tab-note-editor-panel {
+ --panel-width: 300px;
+ --arrowpanel-header-min-height: auto;
+ padding-inline: var(--space-xsmall);
+ padding-block: 0;
+ font: menu;
+
+ .panel-header {
+ padding-block-start: 0;
+
+ & > h1 {
+ margin: var(--space-small);
+ }
+ }
+
+ toolbarseparator {
+ padding-inline: 0;
+ }
+
+ .panel-body {
+ padding-block: var(--space-medium);
+ }
+
+ #tab-note-text {
+ width: 100%;
+ box-sizing: border-box;
+ padding: var(--space-medium);
+ }
+}
+
/* Tab Overflow */
#tabbrowser-arrowscrollbox,