tor-browser

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

commit d31739e1c0e3859874d62909f4bc5314de12ff3a
parent 877fd2e1cbc05facbd0950757494eb3c6f8d904c
Author: Erik Nordin <enordin@mozilla.com>
Date:   Wed,  1 Oct 2025 16:07:08 +0000

Bug 1991224 - Resize about:translations textareas on input r=translations-reviewers,gregtatum

This patch implements the ability for the `about:translations`
<textarea> elements to resize their heights and stay in sync
with each other as the source input or translated content
grow beyond the current bounds.

Differential Revision: https://phabricator.services.mozilla.com/D266488

Diffstat:
Mtoolkit/components/translations/content/about-translations.mjs | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mtoolkit/components/translations/tests/browser/browser.toml | 2++
Atoolkit/components/translations/tests/browser/browser_about_translations_textarea_resize_by_input.js | 226+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/components/translations/tests/browser/shared-head.js | 7+++++++
4 files changed, 289 insertions(+), 2 deletions(-)

diff --git a/toolkit/components/translations/content/about-translations.mjs b/toolkit/components/translations/content/about-translations.mjs @@ -633,6 +633,7 @@ class AboutTranslations { this.#updateSourceScriptDirection(); this.#updateTargetScriptDirection(); + this.#synchronizeTextAreasToMaxContentHeight(); if (sourceTextArea.value) { this.#displayTranslatingPlaceholder(); @@ -692,6 +693,7 @@ class AboutTranslations { sourceTextArea.dispatchEvent(new Event("input")); this.#updateSourceScriptDirection(); + this.#synchronizeTextAreasToMaxContentHeight(); } /** @@ -709,6 +711,7 @@ class AboutTranslations { } this.#updateTargetScriptDirection(); + this.#synchronizeTextAreasToMaxContentHeight(); } /** @@ -952,6 +955,7 @@ class AboutTranslations { onDebounce: async () => { try { this.#updateURLFromUI(); + this.#synchronizeTextAreasToMaxContentHeight(); await this.#maybeUpdateDetectedSourceLanguage(); @@ -1029,12 +1033,60 @@ class AboutTranslations { // Mark the events so that they show up in the Firefox Profiler. This makes it handy // to visualize the debouncing behavior. doEveryTime: () => { - this.#updateSourceScriptDirection(); + const sourceText = this.#getSourceText(); performance.mark( - `Translations: input changed to ${this.#getSourceText().length} code units.` + `Translations: input changed to ${sourceText.length} code units.` ); + + if (!sourceText) { + this.#setTargetText(""); + } + + this.#updateSourceScriptDirection(); }, }); + + /** + * Calculates the heights of the content in both the source and target text areas, + * then syncs them both to the maximum calculated content height among the two. + */ + #synchronizeTextAreasToMaxContentHeight() { + const { sourceTextArea, targetTextArea } = this.elements; + + // This will be the same for both the source and target text areas. + const textAreaRatioBefore = + parseFloat(sourceTextArea.style.height) / sourceTextArea.scrollWidth; + + sourceTextArea.style.height = "auto"; + targetTextArea.style.height = "auto"; + + const maxContentHeight = Math.ceil( + Math.max(sourceTextArea.scrollHeight, targetTextArea.scrollHeight) + ); + const maxContentHeightPixels = `${maxContentHeight}px`; + + sourceTextArea.style.height = maxContentHeightPixels; + targetTextArea.style.height = maxContentHeightPixels; + + const textAreaRatioAfter = maxContentHeight / sourceTextArea.scrollWidth; + const ratioDelta = textAreaRatioAfter - textAreaRatioBefore; + const changeThreshold = 0.001; + + if ( + // The text-area heights were not 0px prior to growing. + textAreaRatioBefore > changeThreshold && + // The text-area aspect ratio changed beyond typical floating-point error. + Math.abs(ratioDelta) > changeThreshold + ) { + document.dispatchEvent( + new CustomEvent("AboutTranslations:TextAreaHeightsChanged", { + detail: { + textAreaHeights: ratioDelta < 0 ? "decreased" : "increased", + }, + }) + ); + } + } } /** diff --git a/toolkit/components/translations/tests/browser/browser.toml b/toolkit/components/translations/tests/browser/browser.toml @@ -41,6 +41,8 @@ skip-if = ["os == 'linux'"] # Bug 1821461 ["browser_about_translations_telemetry_open.js"] +["browser_about_translations_textarea_resize_by_input.js"] + ["browser_about_translations_url_load.js"] ["browser_about_translations_url_update.js"] diff --git a/toolkit/components/translations/tests/browser/browser_about_translations_textarea_resize_by_input.js b/toolkit/components/translations/tests/browser/browser_about_translations_textarea_resize_by_input.js @@ -0,0 +1,226 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// The German lower-case character "ß" expands to two characters "SS" when capitalized. +// Our mock translator deterministically capitalizes text for integration tests. +const largeExpandingInput = `\ +ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß \ +ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß \ +ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß \ +ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß \ +ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß \ +ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß \ +ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß \ +ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß \ +ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß \ +ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß \ +ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß \ +ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß`; + +const halfLargeExpandingInput = `\ +ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß \ +ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß \ +ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß \ +ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß \ +ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß \ +ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß ß`; + +/** + * This test case ensures that translating a small input, one that would not + * cause the text content to exceed the default text-area height, does not + * cause the text area to automatically resize. + */ +add_task(async function test_about_translations_no_resize_for_small_input() { + const { aboutTranslationsTestUtils, cleanup } = await openAboutTranslations({ + languagePairs: [ + { fromLang: "de", toLang: "en" }, + { fromLang: "en", toLang: "de" }, + ], + }); + + await aboutTranslationsTestUtils.assertEvents( + { + expected: [ + [ + AboutTranslationsTestUtils.Events.TranslationRequested, + { translationId: 1 }, + ], + [AboutTranslationsTestUtils.Events.ShowTranslatingPlaceholder], + ], + unexpected: [AboutTranslationsTestUtils.Events.TextAreaHeightsChanged], + }, + async () => { + await aboutTranslationsTestUtils.setSourceLanguageSelectorValue("de"); + await aboutTranslationsTestUtils.setTargetLanguageSelectorValue("en"); + await aboutTranslationsTestUtils.setSourceTextAreaValue("Hello world"); + } + ); + + await aboutTranslationsTestUtils.assertEvents( + { + expected: [ + [ + AboutTranslationsTestUtils.Events.TranslationComplete, + { translationId: 1 }, + ], + ], + }, + async () => { + await aboutTranslationsTestUtils.resolveDownloads(1); + } + ); + + await aboutTranslationsTestUtils.assertTranslatedText({ + sourceLanguage: "de", + targetLanguage: "en", + sourceText: "Hello world", + }); + + await cleanup(); +}); + +/** + * This test case ensures that translating a source text that is larger than the + * default source-text-area size will cause it to resize, that producing a translated + * output that is larger than the target-text-area will cause it to resize, and that + * reducing the size of the source text after it has been expanded will cause it to + * return to the default size. + */ +add_task(async function test_about_translations_resize_by_input() { + const { aboutTranslationsTestUtils, cleanup } = await openAboutTranslations({ + languagePairs: [ + { fromLang: "de", toLang: "en" }, + { fromLang: "en", toLang: "de" }, + ], + }); + + info( + "The text areas should expand when a large input is pasted as the source." + ); + await aboutTranslationsTestUtils.assertEvents( + { + expected: [ + [ + AboutTranslationsTestUtils.Events.TranslationRequested, + { translationId: 1 }, + ], + [AboutTranslationsTestUtils.Events.ShowTranslatingPlaceholder], + [ + AboutTranslationsTestUtils.Events.TextAreaHeightsChanged, + { + textAreaHeights: "increased", + }, + ], + ], + }, + async () => { + await aboutTranslationsTestUtils.setSourceLanguageSelectorValue("de"); + await aboutTranslationsTestUtils.setTargetLanguageSelectorValue("en"); + await aboutTranslationsTestUtils.setSourceTextAreaValue( + largeExpandingInput + ); + } + ); + + info( + "The text areas should expand again if the translated output is taller than the input." + ); + await aboutTranslationsTestUtils.assertEvents( + { + expected: [ + [ + AboutTranslationsTestUtils.Events.TranslationComplete, + { translationId: 1 }, + ], + [ + AboutTranslationsTestUtils.Events.TextAreaHeightsChanged, + { + textAreaHeights: "increased", + }, + ], + ], + }, + async () => { + await aboutTranslationsTestUtils.resolveDownloads(1); + } + ); + + await aboutTranslationsTestUtils.assertTranslatedText({ + sourceLanguage: "de", + targetLanguage: "en", + sourceText: largeExpandingInput, + }); + + info( + "The text areas should reduce their size if the content height is reduced." + ); + await aboutTranslationsTestUtils.assertEvents( + { + expected: [ + [ + AboutTranslationsTestUtils.Events.TranslationRequested, + { translationId: 2 }, + ], + [ + AboutTranslationsTestUtils.Events.TranslationComplete, + { translationId: 2 }, + ], + [ + AboutTranslationsTestUtils.Events.TextAreaHeightsChanged, + { + textAreaHeights: "decreased", + }, + ], + ], + unexpected: [ + AboutTranslationsTestUtils.Events.ShowTranslatingPlaceholder, + ], + }, + async () => { + await aboutTranslationsTestUtils.setSourceTextAreaValue( + halfLargeExpandingInput + ); + } + ); + + await aboutTranslationsTestUtils.assertTranslatedText({ + sourceLanguage: "de", + targetLanguage: "en", + sourceText: halfLargeExpandingInput, + }); + + info( + "The text areas should reset to default height when all content is removed." + ); + await aboutTranslationsTestUtils.assertEvents( + { + expected: [ + [ + AboutTranslationsTestUtils.Events.TextAreaHeightsChanged, + { + textAreaHeights: "decreased", + }, + ], + ], + unexpected: [ + AboutTranslationsTestUtils.Events.TranslationRequested, + AboutTranslationsTestUtils.Events.ShowTranslatingPlaceholder, + ], + }, + async () => { + await aboutTranslationsTestUtils.setSourceTextAreaValue(""); + } + ); + + await aboutTranslationsTestUtils.assertSourceTextArea({ + showsPlaceholder: true, + }); + + await aboutTranslationsTestUtils.assertTargetTextArea({ + showsPlaceholder: true, + }); + + await cleanup(); +}); diff --git a/toolkit/components/translations/tests/browser/shared-head.js b/toolkit/components/translations/tests/browser/shared-head.js @@ -2816,6 +2816,13 @@ class AboutTranslationsTestUtils { static TranslationComplete = "AboutTranslations:TranslationComplete"; /** + * Event fired when the source/target textarea heights change. + * + * @type {string} + */ + static TextAreaHeightsChanged = "AboutTranslations:TextAreaHeightsChanged"; + + /** * Event fired when the target text is cleared programmatically. * * @type {string}