commit 6a460c8f12bdf65f6948e2b83c2a10d734e26de4
parent b52edf65bf2f037cac81f56bd949f10b7f2f5eaf
Author: Jason Prickett <jprickett@mozilla.com>
Date: Tue, 7 Oct 2025 00:02:52 +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:
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: calc(100% - var(--space-small)) 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();
+ }
+ );
+ }
+);