commit 32de7fcb43cf6614e23873240cfe3cb55e85a784
parent c9fd191de6dfbd1f87e2e3b565cb6df777534a14
Author: Stephen Thompson <sthompson@mozilla.com>
Date: Tue, 6 Jan 2026 01:31:32 +0000
Bug 2000070 - "add note" button on tab hover preview r=dwalker,jswinarton,desktop-theme-reviewers,tabbrowser-reviewers
When hovering over a tab with a loaded web page, the tab hover preview panel will now display an "Add note" button if the tab does not yet have a tab note. Clicking the "Add note" button will open the "Add note" tab note panel and close the tab hover preview panel.
Differential Revision: https://phabricator.services.mozilla.com/D275678
Diffstat:
6 files changed, 133 insertions(+), 12 deletions(-)
diff --git a/browser/base/content/main-popupset.inc.xhtml b/browser/base/content/main-popupset.inc.xhtml
@@ -553,7 +553,7 @@
<hbox id="ctrlTab-showAll-container" pack="center"/>
</panel>
- <!-- TODO: create lazily? -->
+ <!-- TODO Bug 2008627: create lazily? -->
<panel id="tab-preview-panel"
type="arrow"
orient="vertical"
@@ -573,6 +573,19 @@
<html:div class="tab-note-text-container"></html:div>
</panel>
+ <!--
+ TODO Bug 2008627: put all of #tab-preview-panel in here, not just this button.
+ This is being lazy-loaded because otherwise it will eagerly load multiple svg
+ images on startup (see browser_startup_images.js).
+ -->
+ <html:template id="tabPreviewPanelTemplate">
+ <html:moz-box-button
+ class="tab-preview-add-note"
+ data-l10n-id="tab-context-add-note"
+ iconsrc="chrome://global/skin/icons/tab-notes.svg"
+ />
+ </html:template>
+
<panel id="tabgroup-preview-panel"
role="menu"
data-l10n-id="tab-group-preview-name"
diff --git a/browser/components/tabbrowser/content/tab-hover-preview.mjs b/browser/components/tabbrowser/content/tab-hover-preview.mjs
@@ -42,6 +42,9 @@ export default class TabHoverPanelSet {
/** @type {HoverPanel|null} */
#activePanel;
+ /**
+ * @param {Window} win
+ */
constructor(win) {
XPCOMUtils.defineLazyPreferenceGetter(
this,
@@ -59,10 +62,21 @@ export default class TabHoverPanelSet {
this.#win
);
- this.tabPanel = new TabPanel(
- this.#win.document.getElementById("tab-preview-panel"),
- this
+ /** @type {HTMLTemplateElement} */
+ const tabPreviewTemplate = win.document.getElementById(
+ "tabPreviewPanelTemplate"
);
+ const importedFragment = win.document.importNode(
+ tabPreviewTemplate.content,
+ true
+ );
+ // #tabPreviewPanelTemplate is currently just the .tab-preview-add-note
+ // button element, so append it to the tab preview panel body.
+ const addNoteButton = importedFragment.firstElementChild;
+ const tabPreviewPanel =
+ this.#win.document.getElementById("tab-preview-panel");
+ tabPreviewPanel.append(addNoteButton);
+ this.tabPanel = new TabPanel(tabPreviewPanel, this);
this.tabGroupPanel = new TabGroupPanel(
this.#win.document.getElementById("tabgroup-preview-panel"),
this
@@ -274,8 +288,15 @@ class TabPanel extends HoverPanel {
this.#tab = null;
this.#thumbnailElement = null;
+
+ this.panelElement
+ .querySelector(".tab-preview-add-note")
+ .addEventListener("click", () => this.#openTabNotePanel());
}
+ /**
+ * @param {Event} e
+ */
handleEvent(e) {
switch (e.type) {
case "popupshowing":
@@ -334,6 +355,11 @@ class TabPanel extends HoverPanel {
}
}
+ /**
+ * @param {MozTabbrowserTab} [leavingTab]
+ * @param {object} [options]
+ * @param {boolean} [options.force=false]
+ */
deactivate(leavingTab = null, { force = false } = {}) {
if (!this._prefUseTabNotes) {
force = true;
@@ -475,6 +501,16 @@ class TabPanel extends HoverPanel {
: "";
}
+ /**
+ * Opens the tab note menu in the context of the current tab. Since only
+ * one panel should be open at a time, this also closes the tab hover preview
+ * panel.
+ */
+ #openTabNotePanel() {
+ this.win.gBrowser.tabNoteMenu.openPanel(this.#tab);
+ this.deactivate(this.#tab, { force: true });
+ }
+
#updatePreview(tab = null) {
if (tab) {
this.#tab = tab;
@@ -496,10 +532,21 @@ class TabPanel extends HoverPanel {
"";
}
- lazy.TabNotes.get(this.#tab).then(note => {
- this.panelElement.querySelector(".tab-note-text-container").textContent =
- note?.text || "";
- });
+ const noteTextContainer = this.panelElement.querySelector(
+ ".tab-note-text-container"
+ );
+ const addNoteButton = this.panelElement.querySelector(
+ ".tab-preview-add-note"
+ );
+ if (this._prefUseTabNotes && lazy.TabNotes.isEligible(this.#tab)) {
+ lazy.TabNotes.get(this.#tab).then(note => {
+ noteTextContainer.textContent = note?.text || "";
+ addNoteButton.toggleAttribute("hidden", !!note);
+ });
+ } else {
+ noteTextContainer.textContent = "";
+ addNoteButton.setAttribute("hidden", "");
+ }
let thumbnailContainer = this.panelElement.querySelector(
".tab-preview-thumbnail-container"
diff --git a/browser/components/tabbrowser/test/browser/tabs/browser_tab_preview.js b/browser/components/tabbrowser/test/browser/tabs/browser_tab_preview.js
@@ -555,7 +555,8 @@ add_task(async function tabContentChangeTests() {
});
/**
- * Test that if a note is set on a tab, the note appears in the preview panel
+ * Test that tab notes and their UI elements appear correctly in the tab
+ * hover preview panel.
*/
add_task(async function tabNotesTests() {
if (!Services.prefs.getBoolPref("browser.tabs.notes.enabled", false)) {
@@ -571,18 +572,46 @@ add_task(async function tabNotesTests() {
const tab = await addTabTo(gBrowser, "https://example.com/");
+ info("validate the presentation of an eligible tab with no note");
await openTabPreview(tab);
Assert.equal(
previewPanel.querySelector(".tab-note-text-container").innerText,
"",
"Preview panel contains no tab note"
);
- await closeTabPreviews();
+ let addNoteButton = previewPanel.querySelector(".tab-preview-add-note");
+ Assert.ok(
+ !addNoteButton.hasAttribute("hidden"),
+ "add note button should be visible on an eligible tab without a tab note"
+ );
+
+ info("choose to add a note from the tab hover preview panel");
+ let tabNotePanel = document.getElementById("tabNotePanel");
+ let panelShown = BrowserTestUtils.waitForPopupEvent(tabNotePanel, "shown");
+ const previewHidden = BrowserTestUtils.waitForPopupEvent(
+ previewPanel,
+ "hidden"
+ );
+ addNoteButton.click();
+ await Promise.all([panelShown, previewHidden]);
+ info("save a new tab note");
+ Assert.equal(
+ document.activeElement,
+ tabNotePanel.querySelector("textarea"),
+ "tab note textarea should be focused"
+ );
+ const input = BrowserTestUtils.waitForEvent(document.activeElement, "input");
+ EventUtils.sendString(noteText, window);
+ await input;
+ let menuHidden = BrowserTestUtils.waitForPopupEvent(tabNotePanel, "hidden");
const tabNoteCreated = BrowserTestUtils.waitForEvent(tab, "TabNote:Created");
- TabNotes.set(tab, noteText);
- await tabNoteCreated;
+ tabNotePanel.querySelector("#tab-note-editor-button-save").click();
+ await Promise.all([menuHidden, tabNoteCreated]);
+ await closeTabPreviews();
+
+ info("validate the presentation of an eligible tab with a tab note");
await openTabPreview(tab);
Assert.equal(
@@ -590,22 +619,39 @@ add_task(async function tabNotesTests() {
noteText,
"New tab note is visible in preview panel"
);
+ addNoteButton = previewPanel.querySelector(".tab-preview-add-note");
+ Assert.ok(
+ addNoteButton.hasAttribute("hidden"),
+ "add note button should be hidden on an eligible tab with a tab note"
+ );
await closeTabPreviews();
+ info(
+ "delete the tab note to return the tab hover preview to the state with no tab note"
+ );
const tabNoteRemoved = BrowserTestUtils.waitForEvent(tab, "TabNote:Removed");
TabNotes.delete(tab);
await tabNoteRemoved;
+ info(
+ "validate the presentation of an eligible tab after its note has been deleted"
+ );
await openTabPreview(tab);
Assert.equal(
previewPanel.querySelector(".tab-note-text-container").innerText,
"",
"Preview panel contains no tab note after delete"
);
+ addNoteButton = previewPanel.querySelector(".tab-preview-add-note");
+ Assert.ok(
+ !addNoteButton.hasAttribute("hidden"),
+ "add note button should be visible on an eligible tab without a tab note after delete"
+ );
await closeTabPreviews();
BrowserTestUtils.removeTab(tab);
await resetState();
+ await TabNotes.reset();
});
/*
diff --git a/browser/themes/shared/tabbrowser/tab-hover-preview.css b/browser/themes/shared/tabbrowser/tab-hover-preview.css
@@ -64,6 +64,16 @@
-webkit-line-clamp: 10;
}
+.tab-preview-add-note {
+ --box-border-color: transparent;
+ --box-padding: var(--space-medium);
+ margin: 0 var(--space-small) var(--space-small);
+
+ &[hidden] {
+ display: none;
+ }
+}
+
@keyframes tab-hover-preview-fadein {
from {
opacity: 0;
diff --git a/toolkit/themes/shared/desktop-jar.inc.mn b/toolkit/themes/shared/desktop-jar.inc.mn
@@ -116,6 +116,7 @@
skin/classic/global/icons/sort-arrow.svg (../../shared/icons/sort-arrow.svg)
skin/classic/global/icons/trending.svg (../../shared/icons/trending.svg)
skin/classic/global/icons/trophy.svg (../../shared/icons/trophy.svg)
+ skin/classic/global/icons/tab-notes.svg (../../shared/icons/tab-notes.svg)
skin/classic/global/icons/tab-notes-12.svg (../../shared/icons/tab-notes-12.svg)
skin/classic/global/icons/update-icon.svg (../../shared/icons/update-icon.svg)
skin/classic/global/icons/arrow-counterclockwise-16.svg (../../shared/icons/arrow-counterclockwise-16.svg)
diff --git a/toolkit/themes/shared/icons/tab-notes.svg b/toolkit/themes/shared/icons/tab-notes.svg
@@ -0,0 +1,4 @@
+<!-- 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/. -->
+<svg width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity" xmlns="http://www.w3.org/2000/svg"><path d="m14.78 10.78-4 4a.747.747 0 0 1-.53.22H3a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v7.25c0 .199-.079.39-.22.53zm-1.28-.841V3c0-.275-.225-.5-.5-.5H3c-.275 0-.5.225-.5.5v10c0 .275.225.5.5.5h6.939l.061-.061V10.5a.5.5 0 0 1 .5-.5h2.939l.061-.061z"/></svg>