tor-browser

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

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:
Mbrowser/components/tabbrowser/content/tabnote-menu.js | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mbrowser/components/tabnotes/test/browser/browser_tab_notes_menu.js | 69++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mbrowser/locales/en-US/browser/tabbrowser.ftl | 10++++++++++
Mbrowser/themes/shared/tabbrowser/tabs.css | 36++++++++++++++++++++++++++++++++----
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); } }