tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mbrowser/base/content/browser-main.js | 1+
Mbrowser/base/content/main-popupset.inc.xhtml | 3+++
Mbrowser/base/content/main-popupset.js | 4++++
Mbrowser/components/tabbrowser/content/tabbrowser.js | 19+++++++++++++++++++
Abrowser/components/tabbrowser/content/tabnote-menu.js | 179+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/components/tabbrowser/jar.mn | 1+
Mbrowser/components/tabbrowser/test/browser/tabs/browser_tab_notes.js | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/locales/en-US/browser/tabbrowser.ftl | 19+++++++++++++++++++
Mbrowser/themes/shared/tabbrowser/tabs.css | 31+++++++++++++++++++++++++++++++
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,