commit d8f323be8e2a8035212c65d116e80d233285c657
parent 38b06ed3a40cdff7461de3598d8146be22f533e1
Author: Anna Kulyk <akulyk@mozilla.com>
Date: Thu, 30 Oct 2025 15:41:06 +0000
Bug 1971841 - Part 1: Create sync-device-name component r=hjones,fluent-reviewers,desktop-theme-reviewers,bolsson
Differential Revision: https://phabricator.services.mozilla.com/D269994
Diffstat:
8 files changed, 506 insertions(+), 0 deletions(-)
diff --git a/browser/components/preferences/jar.mn b/browser/components/preferences/jar.mn
@@ -36,3 +36,4 @@ browser.jar:
content/browser/preferences/widgets/setting-pane.mjs (widgets/setting-pane/setting-pane.mjs)
content/browser/preferences/widgets/security-privacy-card.mjs (widgets/security-privacy/security-privacy-card/security-privacy-card.mjs)
content/browser/preferences/widgets/security-privacy-card.css (widgets/security-privacy/security-privacy-card/security-privacy-card.css)
+ content/browser/preferences/widgets/sync-device-name.mjs (widgets/sync-device-name/sync-device-name.mjs)
diff --git a/browser/components/preferences/preferences.xhtml b/browser/components/preferences/preferences.xhtml
@@ -98,6 +98,7 @@
<script type="module" src="chrome://browser/content/preferences/widgets/setting-control.mjs"></script>
<script type="module" src="chrome://browser/content/preferences/widgets/security-privacy-card.mjs"></script>
<script type="module" src="chrome://global/content/elements/moz-input-color.mjs"></script>
+ <script type="module" src="chrome://browser/content/preferences/widgets/sync-device-name.mjs"></script>
</head>
<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
diff --git a/browser/components/preferences/tests/chrome/chrome.toml b/browser/components/preferences/tests/chrome/chrome.toml
@@ -15,3 +15,5 @@ support-files = [
["test_setting_control_options.html"]
["test_setting_group.html"]
+
+["test_sync_device_name.html"]
diff --git a/browser/components/preferences/tests/chrome/test_sync_device_name.html b/browser/components/preferences/tests/chrome/test_sync_device_name.html
@@ -0,0 +1,264 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>sync-device-name test</title>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <link
+ rel="stylesheet"
+ href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ />
+ <link rel="stylesheet" href="chrome://global/skin/global.css" />
+ <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <script src="../../../../../toolkit/content/tests/widgets/lit-test-helpers.js"></script>
+ <script
+ type="module"
+ src="chrome://browser/content/preferences/widgets/sync-device-name.mjs"
+ ></script>
+ <script>
+ let html, testHelpers;
+
+ const TEST_DEVICE_NAME = "My Device";
+
+ add_setup(async function setup() {
+ testHelpers = new LitTestHelpers();
+ ({ html } = await testHelpers.setupLit());
+ testHelpers.setupTests({
+ templateFn: () =>
+ html`<sync-device-name
+ value=${TEST_DEVICE_NAME}
+ ></sync-device-name>`,
+ });
+
+ MozXULElement.insertFTLIfNeeded("browser/preferences/preferences.ftl");
+ });
+
+ add_task(async function testSyncDeviceName() {
+ let {
+ children: [syncDeviceName],
+ } = await testHelpers.renderTemplate();
+
+ ok(syncDeviceName, "sync-device-name element renders.");
+ ok(
+ !syncDeviceName._isInEditMode,
+ "sync-device-name initially renders in display mode."
+ );
+ let deviceNameLabel =
+ syncDeviceName.shadowRoot.querySelector("moz-box-item").label;
+ is(deviceNameLabel, TEST_DEVICE_NAME, "Device name is displayed.");
+ ok(syncDeviceName.changeBtnEl, "Change device name button renders.");
+
+ synthesizeMouseAtCenter(syncDeviceName.changeBtnEl, {});
+ await syncDeviceName.updateComplete;
+
+ ok(
+ syncDeviceName._isInEditMode,
+ "sync-device-name switches to edit mode."
+ );
+ is(
+ syncDeviceName.shadowRoot.activeElement,
+ syncDeviceName.inputTextEl,
+ "The input element is in focus."
+ );
+ is(
+ syncDeviceName.inputTextEl.inputEl.value,
+ TEST_DEVICE_NAME,
+ "Device name text is displayed in input element."
+ );
+
+ let cancelBtn = syncDeviceName.shadowRoot.querySelector(
+ "#fxaCancelChangeDeviceName"
+ );
+ let saveBtn = syncDeviceName.shadowRoot.querySelector(
+ "#fxaSaveChangeDeviceName"
+ );
+ ok(cancelBtn && saveBtn, "Cancel and Save buttons render.");
+
+ synthesizeMouseAtCenter(cancelBtn, {});
+ await syncDeviceName.updateComplete;
+
+ ok(
+ !syncDeviceName._isInEditMode,
+ "sync-device-name switches back to display mode."
+ );
+ is(
+ syncDeviceName.shadowRoot.activeElement,
+ syncDeviceName.changeBtnEl,
+ "The Change device name button element is in focus."
+ );
+ });
+
+ add_task(async function testDisabledStateOfSyncDeviceName() {
+ let {
+ children: [syncDeviceName],
+ } = await testHelpers.renderTemplate();
+
+ ok(!syncDeviceName.disabled, "sync-device-name is enabled by default.");
+ ok(
+ !syncDeviceName.changeBtnEl.disabled,
+ "Change device name button is enabled by default."
+ );
+
+ syncDeviceName.disabled = true;
+ await syncDeviceName.updateComplete;
+
+ ok(
+ syncDeviceName.changeBtnEl.disabled,
+ "Change device name button is disabled when sync-device-name is disabled."
+ );
+ });
+
+ add_task(async function testEditModeOfSyncDeviceName() {
+ let {
+ children: [syncDeviceName],
+ } = await testHelpers.renderTemplate();
+
+ const TEST_STRING = " Test";
+
+ ok(syncDeviceName, "sync-device-name element renders.");
+
+ synthesizeMouseAtCenter(syncDeviceName.changeBtnEl, {});
+ await syncDeviceName.updateComplete;
+
+ is(
+ syncDeviceName.shadowRoot.activeElement,
+ syncDeviceName.inputTextEl,
+ "The input element is in focus."
+ );
+
+ sendString(TEST_STRING);
+
+ is(
+ syncDeviceName.inputTextEl.value,
+ `${TEST_DEVICE_NAME}${TEST_STRING}`,
+ "The input element has updated value."
+ );
+
+ let cancelBtn = syncDeviceName.shadowRoot.querySelector(
+ "#fxaCancelChangeDeviceName"
+ );
+
+ synthesizeMouseAtCenter(cancelBtn, {});
+ await syncDeviceName.updateComplete;
+
+ is(
+ syncDeviceName.value,
+ TEST_DEVICE_NAME,
+ "Device name value hasn't changed."
+ );
+
+ synthesizeMouseAtCenter(syncDeviceName.changeBtnEl, {});
+ await syncDeviceName.updateComplete;
+ sendString(TEST_STRING);
+
+ is(
+ syncDeviceName.inputTextEl.value,
+ `${TEST_DEVICE_NAME}${TEST_STRING}`,
+ "The input element has updated value."
+ );
+
+ let saveBtn = syncDeviceName.shadowRoot.querySelector(
+ "#fxaSaveChangeDeviceName"
+ );
+ synthesizeMouseAtCenter(saveBtn, {});
+ await syncDeviceName.updateComplete;
+
+ is(
+ syncDeviceName.value,
+ `${TEST_DEVICE_NAME}${TEST_STRING}`,
+ "Device name value was updated after clicking the Save button."
+ );
+ });
+
+ add_task(async function testSyncDeviceNameKeyboardInteraction() {
+ let {
+ children: [syncDeviceName],
+ } = await testHelpers.renderTemplate();
+
+ const TEST_STRING = " Test";
+
+ ok(syncDeviceName, "sync-device-name element renders.");
+
+ synthesizeKey("KEY_Tab", {});
+
+ is(
+ syncDeviceName.shadowRoot.activeElement,
+ syncDeviceName.changeBtnEl,
+ "Change device name button element is in focus."
+ );
+
+ synthesizeKey("KEY_Enter", {});
+ await syncDeviceName.updateComplete;
+
+ is(
+ syncDeviceName.shadowRoot.activeElement,
+ syncDeviceName.inputTextEl,
+ "The input element is in focus."
+ );
+
+ sendString(TEST_STRING);
+
+ is(
+ syncDeviceName.inputTextEl.value,
+ `${TEST_DEVICE_NAME}${TEST_STRING}`,
+ "The input element has updated value."
+ );
+
+ synthesizeKey("KEY_Escape", {});
+ await syncDeviceName.updateComplete;
+
+ ok(
+ !syncDeviceName._isInEditMode,
+ "sync-device-name switched to display mode after pressing Escape key."
+ );
+ is(
+ syncDeviceName.shadowRoot.activeElement,
+ syncDeviceName.changeBtnEl,
+ "Change device name button element is in focus."
+ );
+ is(
+ syncDeviceName.value,
+ TEST_DEVICE_NAME,
+ "Device name value hasn't changed."
+ );
+
+ synthesizeKey("KEY_Enter", {});
+ await syncDeviceName.updateComplete;
+
+ ok(
+ syncDeviceName._isInEditMode,
+ "sync-device-name is back in edit mode."
+ );
+ is(
+ syncDeviceName.shadowRoot.activeElement,
+ syncDeviceName.inputTextEl,
+ "The input element is in focus."
+ );
+
+ sendString(TEST_STRING);
+ synthesizeKey("KEY_Enter", {});
+ await syncDeviceName.updateComplete;
+
+ ok(
+ !syncDeviceName._isInEditMode,
+ "sync-device-name switched to display mode after pressing Enter key."
+ );
+ is(
+ syncDeviceName.shadowRoot.activeElement,
+ syncDeviceName.changeBtnEl,
+ "Change device name button element is in focus."
+ );
+ is(
+ syncDeviceName.value,
+ `${TEST_DEVICE_NAME}${TEST_STRING}`,
+ "Device name value was updated."
+ );
+ });
+ </script>
+ </head>
+ <body>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+ </body>
+</html>
diff --git a/browser/components/preferences/widgets/sync-device-name/sync-device-name.mjs b/browser/components/preferences/widgets/sync-device-name/sync-device-name.mjs
@@ -0,0 +1,138 @@
+/* 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/. */
+
+import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+import { html } from "chrome://global/content/vendor/lit.all.mjs";
+
+/**
+ * A custom element that manages the display and editing of a Device name
+ * in Firefox Sync settings section.
+ *
+ * @tagname sync-device-name
+ * @property {string} value - The current value of the device name.
+ * @property {string} defaultValue - Default device name shown in the input field when empty.
+ * @property {boolean} disabled - The disabled state of the device name component.
+ * @property {boolean} _isInEditMode - Whether the component is currently in edit mode.
+ */
+class SyncDeviceName extends MozLitElement {
+ static properties = {
+ value: { type: String },
+ defaultValue: { type: String },
+ disabled: { type: Boolean },
+ _isInEditMode: { type: Boolean, state: true },
+ };
+
+ static queries = {
+ inputTextEl: "#fxaSyncComputerName",
+ changeBtnEl: "#fxaChangeDeviceName",
+ };
+
+ constructor() {
+ super();
+
+ /** @type {string} */
+ this.value = "";
+
+ /** @type {string} */
+ this.defaultValue = "";
+
+ /** @type {boolean} */
+ this.disabled = false;
+
+ /** @type {boolean} */
+ this._isInEditMode = false;
+ }
+
+ setFocus() {
+ this.updateComplete.then(() => {
+ const targetEl = this._isInEditMode ? this.inputTextEl : this.changeBtnEl;
+ targetEl?.focus();
+ });
+ }
+
+ onDeviceNameChange() {
+ this._isInEditMode = true;
+ this.setFocus();
+ }
+
+ onDeviceNameCancel() {
+ this._isInEditMode = false;
+ this.setFocus();
+ }
+
+ onDeviceNameSave() {
+ const inputVal = this.inputTextEl.value?.trim();
+ this.value = inputVal === "" ? this.defaultValue : inputVal;
+ this._isInEditMode = false;
+ this.setFocus();
+ }
+
+ /**
+ * Handles key presses in the device name input.
+ * Pressing Enter saves the name, pressing Escape cancels editing.
+ * @param {KeyboardEvent} event
+ */
+ onDeviceNameKeyDown(event) {
+ switch (event.key) {
+ case "Enter":
+ event.preventDefault();
+ this.onDeviceNameSave();
+ break;
+ case "Escape":
+ event.preventDefault();
+ this.onDeviceNameCancel();
+ break;
+ }
+ }
+
+ displayDeviceNameTemplate() {
+ return html`<moz-button
+ id="fxaChangeDeviceName"
+ data-l10n-id="sync-device-name-change-2"
+ data-l10n-attrs="accesskey"
+ slot="actions"
+ @click=${this.onDeviceNameChange}
+ ?disabled=${this.disabled}
+ ></moz-button>`;
+ }
+
+ editDeviceNameTemplate() {
+ return html`<moz-input-text
+ id="fxaSyncComputerName"
+ data-l10n-id="sync-device-name-input"
+ data-l10n-args=${JSON.stringify({ placeholder: this.defaultValue })}
+ .value=${this.value}
+ @keydown=${this.onDeviceNameKeyDown}
+ ></moz-input-text>
+ <moz-button
+ id="fxaCancelChangeDeviceName"
+ data-l10n-id="sync-device-name-cancel"
+ data-l10n-attrs="accesskey"
+ slot="actions"
+ @click=${this.onDeviceNameCancel}
+ ></moz-button>
+ <moz-button
+ id="fxaSaveChangeDeviceName"
+ data-l10n-id="sync-device-name-save"
+ data-l10n-attrs="accesskey"
+ slot="actions"
+ @click=${this.onDeviceNameSave}
+ ></moz-button>`;
+ }
+
+ render() {
+ let label = "";
+ if (!this._isInEditMode) {
+ label = this.value == "" ? this.defaultValue : this.value;
+ }
+ return html`
+ <moz-box-item label=${label}>
+ ${this._isInEditMode
+ ? this.editDeviceNameTemplate()
+ : this.displayDeviceNameTemplate()}
+ </moz-box-item>
+ `;
+ }
+}
+customElements.define("sync-device-name", SyncDeviceName);
diff --git a/browser/components/preferences/widgets/sync-device-name/sync-device-name.stories.mjs b/browser/components/preferences/widgets/sync-device-name/sync-device-name.stories.mjs
@@ -0,0 +1,44 @@
+/* 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/. */
+
+import { html } from "chrome://global/content/vendor/lit.all.mjs";
+import "chrome://browser/content/preferences/widgets/sync-device-name.mjs";
+
+export default {
+ title: "Domain-specific UI Widgets/Settings/Sync Device Name",
+ component: "sync-device-name",
+ parameters: {
+ status: "in-development",
+ },
+};
+
+window.MozXULElement.insertFTLIfNeeded("browser/preferences/preferences.ftl");
+
+const Template = ({ value = "", defaultValue = "", disabled = false }) => html`
+ <div style="max-width: 500px">
+ <sync-device-name
+ value=${value}
+ defaultvalue=${defaultValue}
+ ?disabled=${disabled}
+ ></sync-device-name>
+ </div>
+`;
+
+export const Default = Template.bind({});
+Default.args = {
+ value: "My Device Name",
+ disabled: false,
+};
+
+export const WithDefaultValue = Template.bind({});
+WithDefaultValue.args = {
+ ...Default.args,
+ defaultValue: "My Default Device Name",
+};
+
+export const Disabled = Template.bind({});
+Disabled.args = {
+ ...Default.args,
+ disabled: true,
+};
diff --git a/browser/locales/en-US/browser/preferences/preferences.ftl b/browser/locales/en-US/browser/preferences/preferences.ftl
@@ -1058,6 +1058,16 @@ sync-engine-settings =
sync-device-name-header = Device Name
+# Variables:
+# $placeholder (string) - The placeholder text of the input
+sync-device-name-input =
+ .aria-label = Device Name
+ .placeholder = { $placeholder }
+
+sync-device-name-change-2 =
+ .label = Change Device Name
+ .accesskey = h
+
sync-device-name-change =
.label = Change Device Name…
.accesskey = h
diff --git a/python/l10n/fluent_migrations/bug_1971841_sync_device_name.py b/python/l10n/fluent_migrations/bug_1971841_sync_device_name.py
@@ -0,0 +1,46 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+import re
+from fluent.migrate.transforms import TransformPattern, COPY_PATTERN
+import fluent.syntax.ast as FTL
+
+
+class STRIP_ELLIPSIS(TransformPattern):
+ def visit_TextElement(self, node):
+ node.value = re.sub(r"(?:…|\.\.\.)$", "", node.value)
+ return node
+
+
+def migrate(ctx):
+ """Bug 1971841 - Convert Sync section to config-based prefs, part {index}"""
+ path = "browser/browser/preferences/preferences.ftl"
+
+ ctx.add_transforms(
+ path,
+ path,
+ [
+ FTL.Message(
+ id=FTL.Identifier("sync-device-name-input"),
+ attributes=[
+ FTL.Attribute(
+ id=FTL.Identifier("aria-label"),
+ value=COPY_PATTERN(path, "sync-device-name-header"),
+ ),
+ ],
+ ),
+ FTL.Message(
+ id=FTL.Identifier("sync-device-name-change-2"),
+ attributes=[
+ FTL.Attribute(
+ id=FTL.Identifier("label"),
+ value=STRIP_ELLIPSIS(path, "sync-device-name-change.label"),
+ ),
+ FTL.Attribute(
+ id=FTL.Identifier("accesskey"),
+ value=COPY_PATTERN(path, "sync-device-name-change.accesskey"),
+ ),
+ ],
+ ),
+ ],
+ )