commit ffc2589ee135246568c4fb813952977f50cad773
parent 04ab8d5c8c523a1fe0645a98d5352d59eb502663
Author: Erik Nordin <enordin@mozilla.com>
Date: Tue, 23 Dec 2025 16:05:28 +0000
Bug 1992232 - Part 4/6: Add Target Section Copy Button r=translations-reviewers,fluent-reviewers,bolsson,gregtatum
This commit adds the actions row and copy button to the target section
of the about:translations page. This introduces the new UI elements only,
ensuring that the pre-existing logic is consistent, especially resizing.
At this point the button is always disabled and has no functionality.
Subsequent commits will add new functionality and tests.
Differential Revision: https://phabricator.services.mozilla.com/D276113
Diffstat:
7 files changed, 136 insertions(+), 5 deletions(-)
diff --git a/toolkit/components/translations/content/about-translations.css b/toolkit/components/translations/content/about-translations.css
@@ -18,7 +18,7 @@
color: var(--text-color, #15141a);
/* Variables used in the page layout */
- --AT-input-padding: 20px;
+ --AT-input-padding: 16px;
/* This is somewhat arbitrary, but works well for the current design. If the computed
header height changes, this will need to be adjusted. */
--AT-header-height: 156px;
@@ -122,12 +122,26 @@ body {
border: none;
border-radius: 0;
background-color: transparent;
+ padding-block-end: var(--space-xsmall);
&:focus-visible {
outline: none;
}
}
+#about-translations-target-actions {
+ padding: var(--space-xsmall);
+ background-color: transparent;
+
+ &:focus-visible {
+ outline: none;
+ }
+}
+
+#about-translations-copy-button::part(button) {
+ padding-inline: calc(var(--space-small) + var(--space-xxsmall));
+}
+
#about-translations-empty-section {
width: auto;
height: auto;
diff --git a/toolkit/components/translations/content/about-translations.html b/toolkit/components/translations/content/about-translations.html
@@ -129,6 +129,16 @@
data-l10n-id="about-translations-output-placeholder"
readonly
></textarea>
+ <div id="about-translations-target-actions">
+ <moz-button
+ id="about-translations-copy-button"
+ type="ghost"
+ iconSrc="chrome://global/skin/icons/edit-copy.svg"
+ data-l10n-id="about-translations-copy-button-default"
+ data-l10n-attrs="label"
+ disabled
+ ></moz-button>
+ </div>
</div>
</section>
</body>
diff --git a/toolkit/components/translations/content/about-translations.mjs b/toolkit/components/translations/content/about-translations.mjs
@@ -165,6 +165,7 @@ class AboutTranslations {
* Instantiates and returns the elements that comprise the UI.
*
* @returns {{
+ * copyButton: HTMLElement,
* detectLanguageOption: HTMLOptionElement,
* languageLoadErrorMessage: HTMLElement,
* learnMoreLink: HTMLAnchorElement,
@@ -175,6 +176,7 @@ class AboutTranslations {
* swapLanguagesButton: HTMLElement,
* targetLanguageSelector: HTMLSelectElement,
* targetSection: HTMLElement,
+ * targetSectionActionsRow: HTMLElement,
* targetSectionTextArea: HTMLTextAreaElement,
* unsupportedInfoMessage: HTMLElement,
* }}
@@ -185,6 +187,9 @@ class AboutTranslations {
}
this.#lazyElements = {
+ copyButton: /** @type {HTMLElement} */ (
+ document.getElementById("about-translations-copy-button")
+ ),
detectLanguageOption: /** @type {HTMLOptionElement} */ (
document.getElementById("about-translations-detect-language-option")
),
@@ -217,6 +222,9 @@ class AboutTranslations {
targetSection: /** @type {HTMLElement} */ (
document.getElementById("about-translations-target-section")
),
+ targetSectionActionsRow: /** @type {HTMLElement} */ (
+ document.getElementById("about-translations-target-actions")
+ ),
targetSectionTextArea: /** @type {HTMLTextAreaElement} */ (
document.getElementById("about-translations-target-textarea")
),
@@ -332,6 +340,7 @@ class AboutTranslations {
sourceSectionTextArea,
swapLanguagesButton,
targetLanguageSelector,
+ targetSectionActionsRow,
targetSectionTextArea,
} = this.elements;
@@ -351,6 +360,10 @@ class AboutTranslations {
this.#onTargetTextAreaFocus
);
targetSectionTextArea.addEventListener("blur", this.#onTargetTextAreaBlur);
+ targetSectionActionsRow.addEventListener(
+ "pointerdown",
+ this.#onTargetSectionActionsPointerDown
+ );
window.addEventListener("resize", this.#onResize);
window.visualViewport.addEventListener("resize", this.#onResize);
}
@@ -404,6 +417,24 @@ class AboutTranslations {
};
/**
+ * Handles pointerdown events within the target section's actions row.
+ *
+ * Clicking empty space within the actions row should behave as though
+ * the textarea was clicked, but clicking a specific action, such as the
+ * copy button, should have the default behavior for that element.
+ */
+ #onTargetSectionActionsPointerDown = event => {
+ if (event.target?.closest?.("#about-translations-copy-button")) {
+ // The copy button was clicked: preserve the default behavior.
+ return;
+ }
+
+ // Empty space within the actions row was clicked: focus the text area.
+ event.preventDefault();
+ this.elements.targetSectionTextArea.focus();
+ };
+
+ /**
* Handles the custom effects for focusing the target section's text area,
* which should outline the entire section, instead of only the text area.
*/
@@ -1257,6 +1288,7 @@ class AboutTranslations {
sourceSection,
sourceSectionTextArea,
targetSection,
+ targetSectionActionsRow,
targetSectionTextArea,
} = this.elements;
@@ -1274,15 +1306,18 @@ class AboutTranslations {
sourceSectionTextArea.style.height = "auto";
targetSectionTextArea.style.height = "auto";
+ const targetActionsHeight =
+ targetSectionActionsRow.getBoundingClientRect().height;
const minSectionHeight = Math.max(
this.#getMinHeight(sourceSection),
this.#getMinHeight(targetSection)
);
-
+ const targetSectionContentHeight =
+ targetSectionTextArea.scrollHeight + targetActionsHeight;
const maxContentHeight = Math.ceil(
Math.max(
sourceSectionTextArea.scrollHeight,
- targetSectionTextArea.scrollHeight,
+ targetSectionContentHeight,
minSectionHeight
)
);
@@ -1292,12 +1327,16 @@ class AboutTranslations {
);
const maxSectionHeight = maxContentHeight + sectionBorderHeight;
const maxSectionHeightPixels = `${maxSectionHeight}px`;
+ const targetSectionTextAreaHeightPixels = `${Math.max(
+ maxContentHeight - targetActionsHeight,
+ 0
+ )}px`;
sourceSection.style.height = maxSectionHeightPixels;
targetSection.style.height = maxSectionHeightPixels;
- targetSectionTextArea.style.height = "100%";
sourceSectionTextArea.style.height = "100%";
+ targetSectionTextArea.style.height = targetSectionTextAreaHeightPixels;
const textAreaRatioAfter = maxSectionHeight / sectionWidth;
const ratioDelta = textAreaRatioAfter - textAreaRatioBefore;
diff --git a/toolkit/components/translations/tests/browser/browser_about_translations_dropdowns.js b/toolkit/components/translations/tests/browser/browser_about_translations_dropdowns.js
@@ -84,6 +84,7 @@ add_task(
mainUserInterface: false,
sourceLanguageSelector: false,
targetLanguageSelector: false,
+ copyButton: false,
swapLanguagesButton: false,
sourceSectionTextArea: false,
targetSectionTextArea: false,
diff --git a/toolkit/components/translations/tests/browser/browser_about_translations_enabling.js b/toolkit/components/translations/tests/browser/browser_about_translations_enabling.js
@@ -17,6 +17,7 @@ add_task(async function test_about_translations_disabled() {
mainUserInterface: false,
sourceLanguageSelector: false,
targetLanguageSelector: false,
+ copyButton: false,
swapLanguagesButton: false,
sourceSectionTextArea: false,
targetSectionTextArea: false,
@@ -41,6 +42,7 @@ add_task(async function test_about_translations_enabled() {
mainUserInterface: true,
sourceLanguageSelector: true,
targetLanguageSelector: true,
+ copyButton: true,
swapLanguagesButton: true,
sourceSectionTextArea: true,
targetSectionTextArea: true,
@@ -66,6 +68,7 @@ add_task(async function test_about_translations_engine_unsupported() {
mainUserInterface: false,
sourceLanguageSelector: false,
targetLanguageSelector: false,
+ copyButton: false,
swapLanguagesButton: false,
sourceSectionTextArea: false,
targetSectionTextArea: false,
diff --git a/toolkit/components/translations/tests/browser/shared-head.js b/toolkit/components/translations/tests/browser/shared-head.js
@@ -181,6 +181,7 @@ async function openAboutTranslations({
swapLanguagesButton: "moz-button#about-translations-swap-languages-button",
sourceSectionTextArea: "textarea#about-translations-source-textarea",
targetSectionTextArea: "textarea#about-translations-target-textarea",
+ copyButton: "moz-button#about-translations-copy-button",
unsupportedInfoMessage:
"moz-message-bar#about-translations-unsupported-info-message",
languageLoadErrorMessage:
@@ -4910,6 +4911,55 @@ class AboutTranslationsTestUtils {
}
/**
+ * Asserts properties of the copy button.
+ *
+ * @param {object} options
+ * @param {boolean} [options.visible=true]
+ * @param {boolean} [options.enabled=false]
+ * @returns {Promise<void>}
+ */
+ async assertCopyButton({ visible = true, enabled = false } = {}) {
+ await doubleRaf(document);
+
+ let pageResult = {};
+ try {
+ pageResult = await this.#runInPage(selectors => {
+ const { document } = content;
+ const button = document.querySelector(selectors.copyButton);
+ return {
+ exists: !!button,
+ isDisabled: button?.hasAttribute("disabled") ?? true,
+ };
+ });
+ } catch (error) {
+ AboutTranslationsTestUtils.#reportTestFailure(error);
+ }
+
+ const { exists, isDisabled } = pageResult;
+
+ ok(exists, "Expected copy button to be present.");
+
+ await this.assertIsVisible({
+ pageHeader: true,
+ mainUserInterface: true,
+ sourceLanguageSelector: true,
+ targetLanguageSelector: true,
+ copyButton: visible,
+ swapLanguagesButton: true,
+ sourceSectionTextArea: true,
+ targetSectionTextArea: true,
+ });
+
+ if (enabled !== undefined) {
+ if (enabled) {
+ ok(!isDisabled, "Expected copy button to be enabled.");
+ } else {
+ ok(isDisabled, "Expected copy button to be disabled.");
+ }
+ }
+ }
+
+ /**
* Asserts that the target textarea shows the translating placeholder.
*
* @returns {Promise<void>}
@@ -5091,6 +5141,7 @@ class AboutTranslationsTestUtils {
* @param {boolean} [options.mainUserInterface=false]
* @param {boolean} [options.sourceLanguageSelector=false]
* @param {boolean} [options.targetLanguageSelector=false]
+ * @param {boolean} [options.copyButton=false]
* @param {boolean} [options.swapLanguagesButton=false]
* @param {boolean} [options.sourceSectionTextArea=false]
* @param {boolean} [options.targetSectionTextArea=false]
@@ -5103,6 +5154,7 @@ class AboutTranslationsTestUtils {
mainUserInterface = false,
sourceLanguageSelector = false,
targetLanguageSelector = false,
+ copyButton = false,
swapLanguagesButton = false,
sourceSectionTextArea = false,
targetSectionTextArea = false,
@@ -5138,6 +5190,7 @@ class AboutTranslationsTestUtils {
targetLanguageSelector: isElementVisible(
selectors.targetLanguageSelector
),
+ copyButton: isElementVisible(selectors.copyButton),
swapLanguagesButton: isElementVisible(selectors.swapLanguagesButton),
sourceSectionTextArea: isElementVisible(
selectors.sourceSectionTextArea
@@ -5154,10 +5207,15 @@ class AboutTranslationsTestUtils {
};
});
- const assertVisibility = (expectedVisibility, actualVisibility, label) =>
+ const assertVisibility = (
+ expectedVisibility,
+ actualVisibility,
+ label
+ ) => {
expectedVisibility
? ok(actualVisibility, `Expected ${label} to be visible.`)
: ok(!actualVisibility, `Expected ${label} to be hidden.`);
+ };
assertVisibility(pageHeader, visibilityMap.pageHeader, "page header");
assertVisibility(
@@ -5175,6 +5233,7 @@ class AboutTranslationsTestUtils {
visibilityMap.targetLanguageSelector,
"target-language selector"
);
+ assertVisibility(copyButton, visibilityMap.copyButton, "copy button");
assertVisibility(
swapLanguagesButton,
visibilityMap.swapLanguagesButton,
diff --git a/toolkit/locales/en-US/toolkit/about/aboutTranslations.ftl b/toolkit/locales/en-US/toolkit/about/aboutTranslations.ftl
@@ -39,6 +39,11 @@ about-translations-detect-language = { $language } (detected)
about-translations-output-placeholder =
.placeholder = Translation
+# Button label for copying the translated output to the clipboard.
+about-translations-copy-button-default =
+ .label = Copy
+ .title = Copy translation
+
# Text displayed on target-language selector when no language option is selected.
about-translations-select = Select language