commit c894e84f05523a6d68b3a9861502cca5a5b8fe0f
parent cee2cda814055cab8c561349fc734bfb3366d2b6
Author: Jeremy Swinarton <jswinarton@mozilla.com>
Date: Tue, 30 Dec 2025 22:03:09 +0000
Bug 2000061: Tab note content textarea aligns with spec r=sthompson,fluent-reviewers,desktop-theme-reviewers,tabbrowser-reviewers,bolsson,emilio
Differential Revision: https://phabricator.services.mozilla.com/D275984
Diffstat:
4 files changed, 194 insertions(+), 35 deletions(-)
diff --git a/browser/components/tabbrowser/content/tabnote-menu.js b/browser/components/tabbrowser/content/tabnote-menu.js
@@ -11,6 +11,15 @@
"moz-src:///browser/components/tabnotes/TabNotes.sys.mjs"
);
+ const OVERFLOW_WARNING_THRESHOLD = 980;
+ const OVERFLOW_MAX_THRESHOLD = 1000;
+
+ const OverflowState = {
+ NONE: "none",
+ WARN: "warn",
+ OVERFLOW: "overflow",
+ };
+
class MozTabbrowserTabNoteMenu extends MozXULElement {
static markup = /*html*/ `
<panel
@@ -45,19 +54,25 @@
></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>
+ <html:div
+ class="panel-action-row">
+ <html:div
+ id="tab-note-overflow-indicator">
+ </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>
+ </html:div>
</panel>
`;
@@ -70,6 +85,9 @@
#currentTab = null;
/** @type {boolean} */
#createMode;
+ #cancelButton;
+ #saveButton;
+ #overflowIndicator;
connectedCallback() {
if (this.#initialized) {
@@ -83,21 +101,21 @@
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.#cancelButton = this.querySelector("#tab-note-editor-button-cancel");
+ this.#saveButton = this.querySelector("#tab-note-editor-button-save");
+ this.#overflowIndicator = this.querySelector(
+ "#tab-note-overflow-indicator"
);
+
+ this.#cancelButton.addEventListener("click", () => {
+ this.#panel.hidePopup();
+ });
+ this.#saveButton.addEventListener("click", () => {
+ this.saveNote();
+ });
this.#panel.addEventListener("keypress", this);
this.#panel.addEventListener("popuphidden", this);
+ this.#noteField.addEventListener("input", this);
this.#initialized = true;
}
@@ -113,11 +131,17 @@
this.#panel.hidePopup();
break;
case KeyEvent.DOM_VK_RETURN:
- this.saveNote();
+ if (!event.shiftKey) {
+ this.saveNote();
+ }
break;
}
}
+ on_input() {
+ this.#updatePanel();
+ }
+
on_popuphidden() {
this.#currentTab = null;
this.#noteField.value = "";
@@ -148,6 +172,42 @@
return "bottomleft topleft";
}
+ #updatePanel() {
+ const inputLength = this.#noteField.value.length;
+ let overflow;
+ if (inputLength > OVERFLOW_MAX_THRESHOLD) {
+ overflow = OverflowState.OVERFLOW;
+ } else if (inputLength > OVERFLOW_WARNING_THRESHOLD) {
+ overflow = OverflowState.WARN;
+ } else {
+ overflow = OverflowState.NONE;
+ }
+
+ this.#saveButton.disabled =
+ overflow == OverflowState.OVERFLOW || !inputLength;
+
+ if (overflow != OverflowState.NONE) {
+ this.#panel.setAttribute("overflow", overflow);
+ this.#overflowIndicator.innerText =
+ gBrowser.tabLocalization.formatValueSync(
+ "tab-note-editor-character-limit",
+ {
+ totalCharacters: inputLength,
+ maxAllowedCharacters: OVERFLOW_MAX_THRESHOLD,
+ }
+ );
+ } else {
+ this.#panel.removeAttribute("overflow");
+ }
+
+ // Manually adjust panel height and scroll behaviour to compensate for input size
+ // CSS has a `field-sizing` attribute that does this automatically,
+ // but it is not yet supported.
+ // TODO bug2006439: Replace this with `field-sizing` after the implementation of bug1832409
+ this.#noteField.style.height = "auto";
+ this.#noteField.style.height = `${this.#noteField.scrollHeight}px`;
+ }
+
/**
* @param {MozTabbrowserTab} tab
*/
@@ -157,6 +217,8 @@
}
this.#currentTab = tab;
+ this.#updatePanel();
+
TabNotes.get(tab).then(note => {
if (note) {
this.createMode = false;
diff --git a/browser/components/tabnotes/test/browser/browser_tab_notes_menu.js b/browser/components/tabnotes/test/browser/browser_tab_notes_menu.js
@@ -213,10 +213,18 @@ add_task(async function test_saveTabNote() {
let tab = BrowserTestUtils.addTab(gBrowser, "https://www.example.com");
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
let tabNoteMenu = await openTabNoteMenuByAddNote(tab);
- tabNoteMenu.querySelector("textarea").value = "Lorem ipsum dolor";
+ let tabNoteInput = tabNoteMenu.querySelector("textarea");
+ tabNoteInput.focus();
+ EventUtils.sendString("Lorem ipsum dolor", window);
+
+ let saveButton = tabNoteMenu.querySelector("#tab-note-editor-button-save");
+ await BrowserTestUtils.waitForCondition(() => {
+ return !saveButton.disabled;
+ });
+
let menuHidden = BrowserTestUtils.waitForPopupEvent(tabNoteMenu, "hidden");
let tabNoteCreated = BrowserTestUtils.waitForEvent(tab, "TabNote:Created");
- tabNoteMenu.querySelector("#tab-note-editor-button-save").click();
+ saveButton.click();
await Promise.all([menuHidden, tabNoteCreated]);
const tabNote = await TabNotes.get(tab);
@@ -252,9 +260,17 @@ add_task(async function test_editTabNote() {
"Tab note panel has initial note value in textarea"
);
- let updatedNoteValue = initialNoteValue + " sit amet";
+ let updatedNoteValue = " sit amet";
+
+ let tabNoteInput = tabNoteMenu.querySelector("textarea");
+ tabNoteInput.focus();
+ EventUtils.sendString(updatedNoteValue, window);
+
+ let saveButton = tabNoteMenu.querySelector("#tab-note-editor-button-save");
+ await BrowserTestUtils.waitForCondition(() => {
+ return !saveButton.disabled;
+ });
- tabNoteMenu.querySelector("textarea").value = updatedNoteValue;
let menuHidden = BrowserTestUtils.waitForPopupEvent(tabNoteMenu, "hidden");
let tabNoteEdited = BrowserTestUtils.waitForEvent(tab, "TabNote:Edited");
tabNoteMenu.querySelector("#tab-note-editor-button-save").click();
@@ -263,7 +279,7 @@ add_task(async function test_editTabNote() {
const tabNote = await TabNotes.get(tab);
Assert.equal(
tabNote.text,
- updatedNoteValue,
+ initialNoteValue + updatedNoteValue,
"The updated text entered into the textarea was saved as a note"
);
@@ -294,6 +310,49 @@ add_task(async function test_deleteTabNote() {
Assert.ok(!result, "Tab note was deleted");
BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_tabNoteOverflow() {
+ let tab = BrowserTestUtils.addTab(gBrowser, "https://www.example.com");
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ let tabNoteMenu = await openTabNoteMenuByAddNote(tab);
+ let saveButton = tabNoteMenu.querySelector("#tab-note-editor-button-save");
+
+ Assert.ok(
+ !tabNoteMenu.hasAttribute("overflow"),
+ "Sanity check: tab note menu overflow is false"
+ );
+
+ let textarea = tabNoteMenu.querySelector("textarea");
+ textarea.focus();
+ EventUtils.sendString("x".repeat(990));
+
+ Assert.equal(
+ tabNoteMenu.getAttribute("overflow"),
+ "warn",
+ "Tab note overflow warning indicator is set"
+ );
+ Assert.ok(
+ !saveButton.disabled,
+ "Save button is not disabled when warning indicator is active"
+ );
+
+ textarea.focus();
+ EventUtils.sendString("x".repeat(100));
+
+ Assert.equal(
+ tabNoteMenu.getAttribute("overflow"),
+ "overflow",
+ "Tab note overflow indicator is set"
+ );
+ Assert.ok(
+ saveButton.disabled,
+ "Save button is disabled when overflow indicator is active"
+ );
+
+ await closeTabNoteMenu();
+ BrowserTestUtils.removeTab(tab);
+
await SpecialPowers.popPrefEnv();
});
diff --git a/browser/locales/en-US/browser/tabbrowser.ftl b/browser/locales/en-US/browser/tabbrowser.ftl
@@ -406,6 +406,16 @@ tab-note-editor-button-save =
.label = Save
.accesskey = S
+# Displayed within the tab note edit dialog box when the user has entered more
+# characters than are allowed.
+# Variables:
+# $totalCharacters (Number): the number of characters the user has entered.
+# $maxAllowedCharacters (Number): the maximum number of characters allowed for a tab note.
+tab-note-editor-character-limit =
+ { $maxAllowedCharacters ->
+ *[other] { NUMBER($totalCharacters, useGrouping: "false") }/{ NUMBER($maxAllowedCharacters, useGrouping: "false") } characters
+ }
+
## 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
@@ -1880,13 +1880,41 @@ tab-group {
}
.panel-body {
+ display: flex;
padding-block: var(--space-medium);
}
- #tab-note-text {
- width: 100%;
- box-sizing: border-box;
- padding: var(--space-medium);
+ .panel-action-row {
+ display: flex;
+ justify-content: space-between;
+ gap: var(--space-large);
+ }
+}
+
+#tab-note-text {
+ width: 100%;
+ padding: var(--space-medium);
+ resize: none;
+ min-height: 3lh;
+ max-height: 352px;
+ overflow-y: auto;
+}
+
+#tab-note-overflow-indicator {
+ font-size: 0.85em;
+ visibility: hidden;
+ color: var(--text-color-deemphasized);
+
+ /* This will apply to both the overflow=warn state (the user has almost
+ * overflowed the limit) and the overflow=overflow state (the user actually
+ * has overflowed the limit).
+ */
+ .tab-note-editor-panel[overflow] & {
+ visibility: inherit;
+ }
+
+ .tab-note-editor-panel[overflow="overflow"] & {
+ color: var(--text-color-error);
}
}