tor-browser

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

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:
Mtoolkit/components/translations/content/about-translations.css | 16+++++++++++++++-
Mtoolkit/components/translations/content/about-translations.html | 10++++++++++
Mtoolkit/components/translations/content/about-translations.mjs | 45++++++++++++++++++++++++++++++++++++++++++---
Mtoolkit/components/translations/tests/browser/browser_about_translations_dropdowns.js | 1+
Mtoolkit/components/translations/tests/browser/browser_about_translations_enabling.js | 3+++
Mtoolkit/components/translations/tests/browser/shared-head.js | 61++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mtoolkit/locales/en-US/toolkit/about/aboutTranslations.ftl | 5+++++
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