tor-browser

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

commit 2ff5a963f6bf2766ec04778adb367fc5e5d7b9ad
parent 20dcf0f1e43bff3123a2ea14a8d824954d46356c
Author: Jason Prickett <jprickett@mozilla.com>
Date:   Fri,  3 Oct 2025 20:06:08 +0000

Bug 1987992 - Add auto-resizing textarea to backup restore component r=omc-reviewers,kpatenio,mimi

Differential Revision: https://phabricator.services.mozilla.com/D266494

Diffstat:
Mbrowser/components/backup/content/restore-from-backup.css | 19+++++++++++++++++++
Mbrowser/components/backup/content/restore-from-backup.mjs | 113++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mbrowser/components/backup/tests/browser/browser_settings_restore_from_backup.js | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 201 insertions(+), 24 deletions(-)

diff --git a/browser/components/backup/content/restore-from-backup.css b/browser/components/backup/content/restore-from-backup.css @@ -47,6 +47,10 @@ display: flex; column-gap: var(--space-small); align-items: center; + + &.aw-embedded-filepicker { + align-items: flex-end; + } } #backup-filepicker-label { @@ -74,6 +78,21 @@ &:dir(rtl) { background-position: calc(100% - var(--space-small)) 50%; } + + &:is(textarea) { + background-position: var(--space-small) var(--space-small); + background-size: var(--icon-size); + resize: none; + overflow: hidden; + } + + &:is(textarea):dir(rtl) { + background-position-x: var(100% - var(--space-small)); + } + } + + #backup-filepicker-button { + margin-block: var(--space-xsmall); } } diff --git a/browser/components/backup/content/restore-from-backup.mjs b/browser/components/backup/content/restore-from-backup.mjs @@ -81,17 +81,31 @@ export default class RestoreFromBackup extends MozLitElement { } this.addEventListener("BackupUI:SelectNewFilepickerPath", this); + + // Resize the textarea when the window is resized + if (this.aboutWelcomeEmbedded) { + this._handleWindowResize = () => this.resizeTextarea(); + window.addEventListener("resize", this._handleWindowResize); + } } - handleEvent(event) { - if (event.type == "BackupUI:SelectNewFilepickerPath") { - let { path, iconURL } = event.detail; - this._fileIconURL = iconURL; - this.getBackupFileInfo(path); + disconnectedCallback() { + super.disconnectedCallback(); + if (this._handleWindowResize) { + window.removeEventListener("resize", this._handleWindowResize); + this._handleWindowResize = null; } } updated(changedProperties) { + super.updated(changedProperties); + + // Resize the textarea. This only runs once on initial render, + // and once each time one of our reactive properties is changed. + if (this.aboutWelcomeEmbedded) { + this.resizeTextarea(); + } + if (changedProperties.has("backupServiceState")) { // If we got a recovery error, recoveryInProgress should be false const inProgress = @@ -108,6 +122,14 @@ export default class RestoreFromBackup extends MozLitElement { } } + handleEvent(event) { + if (event.type == "BackupUI:SelectNewFilepickerPath") { + let { path, iconURL } = event.detail; + this._fileIconURL = iconURL; + this.getBackupFileInfo(path); + } + } + async handleChooseBackupFile() { this.dispatchEvent( new CustomEvent("BackupUI:ShowFilepicker", { @@ -165,6 +187,27 @@ export default class RestoreFromBackup extends MozLitElement { ); } + handleTextareaResize() { + this.resizeTextarea(); + } + + /** + * Resizes the textarea to adjust to the size of the content within + */ + resizeTextarea() { + const target = this.filePicker; + if (!target) { + return; + } + + const hasValue = target.value && !!target.value.trim().length; + + target.style.height = "auto"; + if (hasValue) { + target.style.height = target.scrollHeight + "px"; + } + } + applyContentCustomizations() { if (this.aboutWelcomeEmbedded) { this.style.setProperty("--button-group-justify-content", "flex-start"); @@ -173,13 +216,12 @@ export default class RestoreFromBackup extends MozLitElement { } controlsTemplate() { - let iconURL = null; - if (this.backupServiceState?.backupFileToRestore) { - if (this.aboutWelcomeEmbedded) { - iconURL = this.#placeholderFileIconURL; - } else { - iconURL = this._fileIconURL || this.#placeholderFileIconURL; - } + let iconURL = this.#placeholderFileIconURL; + if ( + this.backupServiceState?.backupFileToRestore && + !this.aboutWelcomeEmbedded + ) { + iconURL = this._fileIconURL || this.#placeholderFileIconURL; } return html` <fieldset id="backup-restore-controls"> @@ -189,18 +231,11 @@ export default class RestoreFromBackup extends MozLitElement { for="backup-filepicker-input" data-l10n-id="restore-from-backup-filepicker-label" ></label> - <div id="backup-filepicker"> - <input - id="backup-filepicker-input" - type="text" - readonly - .value=${this.backupServiceState?.backupFileToRestore - ? this.backupServiceState?.backupFileToRestore - : ""} - style=${styleMap( - iconURL ? { backgroundImage: `url(${iconURL})` } : {} - )} - /> + <div + id="backup-filepicker" + class=${this.aboutWelcomeEmbedded ? "aw-embedded-filepicker" : ""} + > + ${this.inputTemplate(iconURL)} <moz-button id="backup-filepicker-button" @click=${this.handleChooseBackupFile} @@ -229,6 +264,36 @@ export default class RestoreFromBackup extends MozLitElement { `; } + inputTemplate(iconURL) { + const styles = styleMap( + iconURL ? { backgroundImage: `url(${iconURL})` } : {} + ); + const backupFileName = this.backupServiceState?.backupFileToRestore || ""; + + if (this.aboutWelcomeEmbedded) { + return html` + <textarea + id="backup-filepicker-input" + rows="1" + readonly + .value=${backupFileName} + style=${styles} + @input=${this.handleTextareaResize} + ></textarea> + `; + } + + return html` + <input + id="backup-filepicker-input" + type="text" + readonly + .value=${backupFileName} + style=${styles} + /> + `; + } + passwordEntryTemplate() { const isInvalid = this.isIncorrectPassword; const describedBy = isInvalid diff --git a/browser/components/backup/tests/browser/browser_settings_restore_from_backup.js b/browser/components/backup/tests/browser/browser_settings_restore_from_backup.js @@ -190,6 +190,12 @@ add_task(async function test_restore_in_progress() { "File picker has no value assigned automatically" ); + Assert.equal( + restoreFromBackup.filePicker.tagName.toLowerCase(), + "input", + "File picker should be an input when aboutWelcomeEmbedded is false" + ); + // There is a backup file, but it is not a valid one // we don't automatically pick it Assert.ok( @@ -275,3 +281,90 @@ add_task(async function test_restore_in_progress() { sandbox.restore(); }); }); + +/** + * Tests that the restore component uses a textarea when aboutWelcomeEmbedded is true + * as well as the associated functionality for said textarea + */ +add_task( + async function test_restore_from_backup_aboutwelcome_embedded_textarea() { + await BrowserTestUtils.withNewTab( + "about:preferences#sync", + async browser => { + let sandbox = sinon.createSandbox(); + let settings = browser.contentDocument.querySelector("backup-settings"); + await settings.updateComplete; + + Assert.ok( + settings.restoreFromBackupButtonEl, + "Restore button should exist" + ); + + settings.restoreFromBackupButtonEl.click(); + await settings.updateComplete; + let restoreFromBackup = settings.restoreFromBackupEl; + Assert.ok(restoreFromBackup, "restore-from-backup should be found"); + + // When aboutWelcomeEmbedded is false, the file picker should be an input + Assert.equal( + restoreFromBackup.filePicker.tagName.toLowerCase(), + "input", + "File picker should be an input when aboutWelcomeEmbedded is false" + ); + + restoreFromBackup.aboutWelcomeEmbedded = true; + await restoreFromBackup.updateComplete; + let resizeTextareaSpy = sandbox.spy( + restoreFromBackup, + "resizeTextarea" + ); + + const textarea = restoreFromBackup.shadowRoot.querySelector( + "#backup-filepicker-input" + ); + + Assert.ok( + textarea, + "textarea should be present after setting aboutWelcomeEmbedded to true" + ); + Assert.equal( + textarea.tagName.toLowerCase(), + "textarea", + "File picker should be a textarea when aboutWelcomeEmbedded is true" + ); + Assert.equal( + textarea.getAttribute("rows"), + "1", + "Textarea should have rows=1" + ); + + // Test resize functionality when content changes + const initialHeight = textarea.style.height; + Assert.ok(initialHeight, "Textarea should have an initial height set"); + + const longPath = + "/a/very/long/path/to/a/backup/file/that/would/wrap/multiple/lines.html"; + textarea.value = longPath; + restoreFromBackup.resizeTextarea(); + + const newHeight = textarea.style.height; + Assert.notEqual( + newHeight, + initialHeight, + "Textarea height should change when content is added" + ); + + // The text area resize function should also be called + // when the resize event occurs on the window + window.dispatchEvent(new Event("resize")); + + Assert.ok( + resizeTextareaSpy.calledOnce, + "resizeTextarea should be called when window resize event is fired" + ); + + sandbox.restore(); + } + ); + } +);