commit dabbf9a4cd16cb65fe410e0cd5e9232330db10f6
parent bd005d4ae09f1e8021b55dec464fec033238d890
Author: Erik Nordin <enordin@mozilla.com>
Date: Tue, 23 Dec 2025 16:05:29 +0000
Bug 1992233 - Part 3/3: Implement Clear Button Functionality r=translations-reviewers,gregtatum
This commit implements the logic for clicking the source-section clear
button on the about:translations page. Invoking the clear button will
clear the source text, resulting in the button now being hidden, and
the source-text section having focus.
Differential Revision: https://phabricator.services.mozilla.com/D277057
Diffstat:
4 files changed, 220 insertions(+), 13 deletions(-)
diff --git a/toolkit/components/translations/content/about-translations.mjs b/toolkit/components/translations/content/about-translations.mjs
@@ -372,6 +372,7 @@ class AboutTranslations {
learnMoreLink,
sourceLanguageSelector,
sourceSectionActionsColumn,
+ sourceSectionClearButton,
sourceSectionTextArea,
swapLanguagesButton,
targetLanguageSelector,
@@ -389,6 +390,14 @@ class AboutTranslations {
"pointerdown",
this.#onSourceSectionActionsPointerDown
);
+ sourceSectionClearButton.addEventListener(
+ "click",
+ this.#onSourceSectionClearButton
+ );
+ sourceSectionClearButton.addEventListener(
+ "mousedown",
+ this.#onSourceSectionClearButtonMouseDown
+ );
sourceSectionTextArea.addEventListener("input", this.#onSourceTextInput);
sourceSectionTextArea.addEventListener(
"focus",
@@ -421,6 +430,32 @@ class AboutTranslations {
};
/**
+ * Handles mousedown on the source section clear button.
+ */
+ #onSourceSectionClearButtonMouseDown = event => {
+ if (this.elements.sourceSection.classList.contains("focus-section")) {
+ // When the source section has a focus outline, clicking the clear button will cause the outline
+ // to disappear and then reappear since clicking the clear button re-focuses the source section.
+ // We should just avoid the outline flash all together when the source section is focused.
+ event.preventDefault();
+ }
+ };
+
+ /**
+ * Handles clicks on the source section clear button.
+ */
+ #onSourceSectionClearButton = event => {
+ if (!this.#sourceTextAreaHasValue()) {
+ return;
+ }
+
+ event.preventDefault();
+ this.#setSourceText("");
+ this.#maybeUpdateDetectedSourceLanguage();
+ this.elements.sourceSectionTextArea.focus();
+ };
+
+ /**
* Handles clicks on the swap-languages button.
*/
#onSwapLanguagesButton = () => {
@@ -1059,12 +1094,20 @@ class AboutTranslations {
*/
#setSourceText(value) {
const { sourceSectionTextArea } = this.elements;
+ const hadValueBefore = Boolean(sourceSectionTextArea.value);
sourceSectionTextArea.value = value;
sourceSectionTextArea.dispatchEvent(new Event("input"));
+ this.#maybeUpdateDetectedSourceLanguage();
this.#updateSourceScriptDirection();
this.#ensureSectionHeightsMatch({ scheduleCallback: false });
+
+ if (!value && hadValueBefore) {
+ document.dispatchEvent(
+ new CustomEvent("AboutTranslationsTest:ClearSourceText")
+ );
+ }
}
/**
diff --git a/toolkit/components/translations/tests/browser/browser.toml b/toolkit/components/translations/tests/browser/browser.toml
@@ -41,6 +41,8 @@ skip-if = [
["browser_about_translations_flip_lexical_shortlist.js"]
+["browser_about_translations_source_clear_button_functionality.js"]
+
["browser_about_translations_source_clear_button_visibility.js"]
["browser_about_translations_swap_languages_button_detected.js"]
diff --git a/toolkit/components/translations/tests/browser/browser_about_translations_source_clear_button_functionality.js b/toolkit/components/translations/tests/browser/browser_about_translations_source_clear_button_functionality.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(
+ async function test_source_clear_button_clears_text_and_target_output() {
+ const { aboutTranslationsTestUtils, cleanup } = await openAboutTranslations(
+ {
+ languagePairs: LANGUAGE_PAIRS,
+ autoDownloadFromRemoteSettings: false,
+ }
+ );
+
+ await aboutTranslationsTestUtils.setSourceLanguageSelectorValue("en");
+ await aboutTranslationsTestUtils.setTargetLanguageSelectorValue("fr");
+
+ await aboutTranslationsTestUtils.setSourceTextAreaValue("Hello world");
+ await aboutTranslationsTestUtils.assertEvents(
+ {
+ expected: [
+ [
+ AboutTranslationsTestUtils.Events.TranslationComplete,
+ AboutTranslationsTestUtils.AnyEventDetail,
+ ],
+ [AboutTranslationsTestUtils.Events.CopyButtonEnabled],
+ ],
+ },
+ async () => {
+ await aboutTranslationsTestUtils.resolveDownloads(1);
+ }
+ );
+
+ await aboutTranslationsTestUtils.assertSourceClearButton({
+ visible: true,
+ });
+
+ await aboutTranslationsTestUtils.assertEvents(
+ {
+ expected: [
+ [AboutTranslationsTestUtils.Events.ClearSourceText],
+ [AboutTranslationsTestUtils.Events.SourceTextClearButtonHidden],
+ [AboutTranslationsTestUtils.Events.ClearTargetText],
+ [AboutTranslationsTestUtils.Events.CopyButtonDisabled],
+ ],
+ unexpected: [
+ AboutTranslationsTestUtils.Events.TranslationRequested,
+ AboutTranslationsTestUtils.Events.CopyButtonEnabled,
+ ],
+ },
+ async () => {
+ await aboutTranslationsTestUtils.clickClearButton();
+ }
+ );
+
+ await aboutTranslationsTestUtils.assertSourceTextArea({
+ showsPlaceholder: true,
+ });
+ await aboutTranslationsTestUtils.assertTargetTextArea({
+ showsPlaceholder: true,
+ });
+ await aboutTranslationsTestUtils.assertCopyButton({ enabled: false });
+ await aboutTranslationsTestUtils.assertSourceClearButton({
+ visible: false,
+ });
+
+ await cleanup();
+ }
+);
+
+add_task(
+ async function test_source_clear_button_cancels_translation_in_progress() {
+ const { aboutTranslationsTestUtils, cleanup } = await openAboutTranslations(
+ {
+ languagePairs: LANGUAGE_PAIRS,
+ autoDownloadFromRemoteSettings: false,
+ }
+ );
+
+ await aboutTranslationsTestUtils.setSourceLanguageSelectorValue("en");
+ await aboutTranslationsTestUtils.setTargetLanguageSelectorValue("fr");
+
+ await aboutTranslationsTestUtils.assertEvents(
+ {
+ expected: [
+ [
+ AboutTranslationsTestUtils.Events.TranslationRequested,
+ { translationId: 1 },
+ ],
+ [AboutTranslationsTestUtils.Events.ShowTranslatingPlaceholder],
+ [AboutTranslationsTestUtils.Events.SourceTextClearButtonShown],
+ ],
+ },
+ async () => {
+ await aboutTranslationsTestUtils.setSourceTextAreaValue(
+ "Clearing in progress"
+ );
+ }
+ );
+
+ await aboutTranslationsTestUtils.assertEvents(
+ {
+ expected: [
+ [AboutTranslationsTestUtils.Events.ClearSourceText],
+ [AboutTranslationsTestUtils.Events.SourceTextClearButtonHidden],
+ [AboutTranslationsTestUtils.Events.ClearTargetText],
+ ],
+ unexpected: [
+ AboutTranslationsTestUtils.Events.CopyButtonEnabled,
+ AboutTranslationsTestUtils.Events.TranslationRequested,
+ AboutTranslationsTestUtils.Events.TranslationComplete,
+ ],
+ },
+ async () => {
+ await aboutTranslationsTestUtils.clickClearButton();
+ }
+ );
+
+ await aboutTranslationsTestUtils.assertSourceTextArea({
+ showsPlaceholder: true,
+ });
+ await aboutTranslationsTestUtils.assertTargetTextArea({
+ showsPlaceholder: true,
+ });
+ await aboutTranslationsTestUtils.assertSourceClearButton({
+ visible: false,
+ });
+
+ await aboutTranslationsTestUtils.assertEvents(
+ {
+ unexpected: [AboutTranslationsTestUtils.Events.TranslationComplete],
+ },
+ async () => {
+ await aboutTranslationsTestUtils.resolveDownloads(1);
+ }
+ );
+
+ await cleanup();
+ }
+);
diff --git a/toolkit/components/translations/tests/browser/shared-head.js b/toolkit/components/translations/tests/browser/shared-head.js
@@ -4188,7 +4188,7 @@ class AboutTranslationsTestUtils {
static CopyButtonReset = "AboutTranslationsTest:CopyButtonReset";
/**
- * Event fired when the source clear button becomes visible.
+ * Event fired when the clear button becomes visible.
*
* @type {string}
*/
@@ -4196,7 +4196,7 @@ class AboutTranslationsTestUtils {
"AboutTranslationsTest:SourceTextClearButtonShown";
/**
- * Event fired when the source clear button becomes hidden.
+ * Event fired when the clear button becomes hidden.
*
* @type {string}
*/
@@ -4220,6 +4220,13 @@ class AboutTranslationsTestUtils {
"AboutTranslationsTest:TextAreaHeightsChanged";
/**
+ * Event fired when the source text is cleared programmatically.
+ *
+ * @type {string}
+ */
+ static ClearSourceText = "AboutTranslationsTest:ClearSourceText";
+
+ /**
* Event fired when the target text is cleared programmatically.
*
* @type {string}
@@ -5196,7 +5203,7 @@ class AboutTranslationsTestUtils {
}
/**
- * Retrieves the state of the source clear button.
+ * Retrieves the state of the clear button.
*
* @returns {Promise<{exists: boolean, hidden: boolean, tabIndex: string | null}>}
*/
@@ -5224,7 +5231,7 @@ class AboutTranslationsTestUtils {
}
/**
- * Asserts properties of the source clear button.
+ * Asserts properties of the clear button.
*
* @param {object} options
* @param {boolean} [options.visible=false]
@@ -5238,24 +5245,43 @@ class AboutTranslationsTestUtils {
tabIndex: actualTabIndex,
} = await this.getSourceClearButtonState();
- ok(exists, "Expected source clear button to be present.");
+ ok(exists, "Expected clear button to be present.");
if (visible) {
- ok(!hidden, "Expected source clear button to be visible.");
+ ok(!hidden, "Expected clear button to be visible.");
} else {
- ok(hidden, "Expected source clear button to be hidden.");
+ ok(hidden, "Expected clear button to be hidden.");
}
if (tabIndex !== undefined) {
is(
actualTabIndex,
tabIndex,
- `Expected source clear button tabindex to be "${tabIndex}".`
+ `Expected clear button tabindex to be "${tabIndex}".`
);
}
}
/**
+ * Clicks the clear button.
+ *
+ * @returns {Promise<void>}
+ */
+ async clickClearButton() {
+ await doubleRaf(document);
+ try {
+ await this.#runInPage(selectors => {
+ const clearButton = content.document.querySelector(
+ selectors.clearButton
+ );
+ clearButton.click();
+ });
+ } catch (error) {
+ AboutTranslationsTestUtils.#reportTestFailure(error);
+ }
+ }
+
+ /**
* Retrieves the current value of the target textarea.
*
* @returns {Promise<string>}
@@ -5575,11 +5601,7 @@ class AboutTranslationsTestUtils {
visibilityMap.sourceSectionTextArea,
"source textarea"
);
- assertVisibility(
- clearButton,
- visibilityMap.clearButton,
- "source clear button"
- );
+ assertVisibility(clearButton, visibilityMap.clearButton, "clear button");
assertVisibility(
targetSectionTextArea,
visibilityMap.targetSectionTextArea,