tor-browser

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

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:
Mbrowser/components/preferences/jar.mn | 1+
Mbrowser/components/preferences/preferences.xhtml | 1+
Mbrowser/components/preferences/tests/chrome/chrome.toml | 2++
Abrowser/components/preferences/tests/chrome/test_sync_device_name.html | 264+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abrowser/components/preferences/widgets/sync-device-name/sync-device-name.mjs | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abrowser/components/preferences/widgets/sync-device-name/sync-device-name.stories.mjs | 44++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/locales/en-US/browser/preferences/preferences.ftl | 10++++++++++
Apython/l10n/fluent_migrations/bug_1971841_sync_device_name.py | 46++++++++++++++++++++++++++++++++++++++++++++++
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"), + ), + ], + ), + ], + )