commit 3ccdf3d2bbdfe4e58c6a612b7a7c823cc9bc57ef
parent 64b002161e1dc7e36e55d9f101970d07083aea3c
Author: Anna Yeddi <ayeddi@mozilla.com>
Date: Mon, 27 Oct 2025 15:03:25 +0000
Bug 1976118 - Replacing default color pickers on Colors Dialog with moz-input-color components. r=eeejay,fluent-reviewers,bolsson,desktop-theme-reviewers,hjones
Adding basic test coverage for the Colors sub-dialog in the Settings UI, while relying on the `moz-input-color` reusable component to test the input's functionality.
Differential Revision: https://phabricator.services.mozilla.com/D258062
Diffstat:
9 files changed, 313 insertions(+), 41 deletions(-)
diff --git a/browser/components/preferences/dialogs/colors.css b/browser/components/preferences/dialogs/colors.css
@@ -0,0 +1,12 @@
+/* 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/. */
+
+#custom-colors,
+#custom-colors .color-container {
+ gap: var(--space-small);
+}
+
+#custom-colors .color-picker {
+ width: 100%;
+}
diff --git a/browser/components/preferences/dialogs/colors.xhtml b/browser/components/preferences/dialogs/colors.xhtml
@@ -22,6 +22,10 @@
rel="stylesheet"
href="chrome://browser/skin/preferences/preferences.css"
/>
+ <html:link
+ rel="stylesheet"
+ href="chrome://browser/content/preferences/dialogs/colors.css"
+ />
<html:link rel="localization" href="browser/preferences/colors.ftl" />
</linkset>
@@ -33,63 +37,58 @@
<key id="key_close" data-l10n-id="colors-close-key" modifiers="accel" />
</keyset>
- <hbox>
- <groupbox flex="1">
+ <hbox id="custom-colors">
+ <groupbox flex="1" class="color-container">
<label
><html:h2
class="heading-medium"
data-l10n-id="colors-text-and-background"
/></label>
- <hbox align="center">
- <label
- data-l10n-id="colors-text-header"
+ <hbox align="center" class="color-row">
+ <html:moz-input-color
+ value="CanvasText"
+ data-l10n-id="colors-text"
control="foregroundtextmenu"
- />
- <spacer flex="1" />
- <html:input
- type="color"
id="foregroundtextmenu"
+ class="color-picker"
preference="browser.display.foreground_color"
- />
+ ></html:moz-input-color>
</hbox>
- <hbox align="center" style="margin-top: 5px">
- <label data-l10n-id="colors-background" control="backgroundmenu" />
- <spacer flex="1" />
- <html:input
- type="color"
+ <hbox align="center" class="color-row">
+ <html:moz-input-color
+ value="Canvas"
+ data-l10n-id="colors-text-background"
+ control="backgroundmenu"
id="backgroundmenu"
+ class="color-picker"
preference="browser.display.background_color"
- />
+ ></html:moz-input-color>
</hbox>
</groupbox>
- <groupbox flex="1">
+ <groupbox flex="1" class="color-container">
<label
><html:h2 class="heading-medium" data-l10n-id="colors-links-header"
/></label>
- <hbox align="center">
- <label
- data-l10n-id="colors-unvisited-links"
+ <hbox align="center" class="color-row">
+ <html:moz-input-color
+ value="LinkText"
+ data-l10n-id="colors-links-unvisited"
control="unvisitedlinkmenu"
- />
- <spacer flex="1" />
- <html:input
- type="color"
id="unvisitedlinkmenu"
+ class="color-picker"
preference="browser.anchor_color"
- />
+ ></html:moz-input-color>
</hbox>
- <hbox align="center" style="margin-top: 5px">
- <label
- data-l10n-id="colors-visited-links"
+ <hbox align="center" class="color-row">
+ <html:moz-input-color
+ value="VisitedText"
+ data-l10n-id="colors-links-visited"
control="visitedlinkmenu"
- />
- <spacer flex="1" />
- <html:input
- type="color"
id="visitedlinkmenu"
+ class="color-picker"
preference="browser.visited_color"
- />
+ ></html:moz-input-color>
</hbox>
</groupbox>
</hbox>
diff --git a/browser/components/preferences/dialogs/jar.mn b/browser/components/preferences/dialogs/jar.mn
@@ -10,6 +10,7 @@ browser.jar:
content/browser/preferences/dialogs/clearSiteData.css
content/browser/preferences/dialogs/clearSiteData.js
content/browser/preferences/dialogs/clearSiteData.xhtml
+ content/browser/preferences/dialogs/colors.css
content/browser/preferences/dialogs/colors.xhtml
content/browser/preferences/dialogs/colors.js
content/browser/preferences/dialogs/connection.xhtml
diff --git a/browser/components/preferences/main.inc.xhtml b/browser/components/preferences/main.inc.xhtml
@@ -192,11 +192,11 @@
data-l10n-id="preferences-colors-manage-button"
search-l10n-ids="
colors-text-and-background,
- colors-text-header,
- colors-background,
+ colors-text,
+ colors-text-background,
colors-links-header,
- colors-unvisited-links,
- colors-visited-links
+ colors-links-unvisited,
+ colors-links-visited
"/>
</vbox>
</radiogroup>
diff --git a/browser/components/preferences/preferences.xhtml b/browser/components/preferences/preferences.xhtml
@@ -97,6 +97,7 @@
<script type="module" src="chrome://browser/content/preferences/widgets/setting-group.mjs"></script>
<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>
</head>
<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
diff --git a/browser/components/preferences/tests/browser.toml b/browser/components/preferences/tests/browser.toml
@@ -55,6 +55,8 @@ run-if = ["os == 'win'"] # Windows-specific handler application selection dialog
["browser_checkspelling.js"]
+["browser_colors_dialog.js"]
+
["browser_connection.js"]
["browser_connection_bug388287.js"]
diff --git a/browser/components/preferences/tests/browser_colors_dialog.js b/browser/components/preferences/tests/browser_colors_dialog.js
@@ -0,0 +1,220 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+async function launchPreferences() {
+ let prefs = await openPreferencesViaOpenPreferencesAPI("paneGeneral", {
+ leaveOpen: true,
+ });
+ Assert.equal(prefs.selectedPane, "paneGeneral", "General pane was selected");
+}
+
+async function reset() {
+ // We have to add this in because the initial state of `document_color_use`
+ // affects the initial state of color preferenced which can change the
+ // starting color values.
+ Services.prefs.clearUserPref("browser.anchor_color");
+ Services.prefs.clearUserPref("browser.visited_color");
+ Services.prefs.clearUserPref("browser.display.foreground_color");
+ Services.prefs.clearUserPref("browser.display.background_color");
+}
+
+add_setup(async function () {
+ info("Setting the test up");
+ // Ensure default color values are used:
+ reset();
+ // Ensure the pref is set to "Always" on all platforms (1 is default on Mac,
+ // but on Windows and Linux it's 0, and the colors dialog is available on 2):
+ Services.prefs.setIntPref("browser.display.document_color_use", 1);
+
+ registerCleanupFunction(async function () {
+ reset();
+ Services.prefs.clearUserPref("browser.display.document_color_use");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ });
+});
+
+add_task(async function testColorPicker() {
+ await launchPreferences();
+
+ const button = gBrowser.contentDocument.getElementById("colors");
+
+ const radiogroup = content.document.getElementById("contrastControlSettings");
+ const radioOff = content.document.getElementById("contrastSettingsOff");
+ const radioCustom = content.document.getElementById("contrastSettingsOn");
+
+ // Focus "Off" Contrast radio button:
+ radiogroup.focus();
+ Assert.equal(
+ radiogroup,
+ gBrowser.contentDocument.activeElement,
+ "Radio group for Custom Colors is focused"
+ );
+ Assert.equal(
+ radioOff,
+ radiogroup.querySelector("radio[focused='true']"),
+ "Radio group with option 'Off' is focused"
+ );
+ Assert.ok(button.disabled, "Manage Colors button is disabled");
+
+ // Focus "Custom" Contrast radio button:
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ Assert.equal(
+ radioCustom,
+ radiogroup.querySelector("radio[focused='true']"),
+ "Radio group with option 'Custom' is focused"
+ );
+ Assert.ok(!button.disabled, "Manage Colors button is now enabled");
+
+ // Focus "Manage Colors" button:
+ EventUtils.synthesizeKey("KEY_Tab");
+ Assert.equal(
+ button,
+ gBrowser.contentDocument.activeElement,
+ "Manage Colors button is focused"
+ );
+
+ info("Open Colors dialog");
+ // Open Colors sub-dialog:
+ let promiseSubDialogLoaded = promiseLoadSubDialog(
+ "chrome://browser/content/preferences/dialogs/colors.xhtml"
+ );
+ EventUtils.synthesizeKey(" ");
+ let colorsDialogWindow = await promiseSubDialogLoaded;
+ let colorsDialogDoc = colorsDialogWindow.document;
+ Assert.ok(colorsDialogDoc, "Colors dialog exists");
+ Assert.ok(
+ BrowserTestUtils.isVisible(colorsDialogDoc.getElementById("ColorsDialog")),
+ "Colors dialog is visible"
+ );
+
+ // Pickers and preferences defaults (should be the same length and order):
+ const pickerControlAttrs = [
+ "foregroundtextmenu",
+ "backgroundmenu",
+ "unvisitedlinkmenu",
+ "visitedlinkmenu",
+ ];
+ const prefNames = [
+ "browser.display.foreground_color",
+ "browser.display.background_color",
+ "browser.anchor_color",
+ "browser.visited_color",
+ ];
+ // Values of static preferences (https://searchfox.org/mozilla-central/rev/270c20e4b063d80ce71f029b4adc4ba03a12edc0/modules/libpref/init/StaticPrefList.yaml#1506-1508,1439-1441,1039-1041,2097-2099):
+ const colorValuesHex = ["#000000", "#FFFFFF", "#0000EE", "#551A8B"];
+ const colorValuesSystem = ["CanvasText", "Canvas", "LinkText", "VisitedText"];
+
+ // Checking the defaults are entered correctly:
+ Assert.equal(
+ prefNames.length,
+ pickerControlAttrs.length,
+ "prefNames and pickerControlAttrs arrays have the same length"
+ );
+ Assert.equal(
+ prefNames.length,
+ colorValuesHex.length,
+ "prefNames and colorValuesHex arrays have the same length"
+ );
+ Assert.equal(
+ prefNames.length,
+ colorValuesSystem.length,
+ "prefNames and colorValuesSystem arrays have the same length"
+ );
+
+ const expectedPickersMap = prefNames.map((pref, i) => ({
+ pref,
+ id: pickerControlAttrs[i],
+ control: pickerControlAttrs[i],
+ // Ensure a HEX value and a system color are interchangeable in this
+ // scenario, i.e. CanvasText resolves to #000000 in our default test cases:
+ expectedValues: [colorValuesSystem[i], colorValuesHex[i]],
+ }));
+
+ for (const picker of expectedPickersMap) {
+ // Pref:
+ const prefValue = Services.prefs.getStringPref(picker.pref, "");
+ Assert.ok(prefValue, `Preference with id="${picker.pref}" exists`);
+ const prefValueNormalized = prefValue.startsWith("#")
+ ? prefValue.toUpperCase()
+ : prefValue;
+
+ // Picker:
+ const pickerEl = colorsDialogDoc.getElementById(picker.id);
+ Assert.ok(pickerEl, `Moz-input-color picker with id="${picker.id}" exists`);
+ const pickerValueNormalized = pickerEl.value.startsWith("#")
+ ? pickerEl.value.toUpperCase()
+ : pickerEl.value;
+
+ info("Test markup of a color picker");
+
+ // Checking default picker attributes:
+ Assert.equal(
+ pickerEl.getAttribute("id"),
+ picker.id,
+ `Picker's "id" attribute is as expected`
+ );
+ Assert.equal(
+ pickerEl.getAttribute("control"),
+ picker.control,
+ `Picker's "control" attribute is as expected`
+ );
+ Assert.equal(
+ pickerEl.getAttribute("preference"),
+ picker.pref,
+ `Picker's "preference" attribute is as expected`
+ );
+ Assert.ok(
+ picker.expectedValues.includes(prefValueNormalized),
+ `Pref ${picker.pref} value "${prefValue}" is one of two expected values: ${picker.expectedValues.join(" or ")}`
+ );
+ Assert.ok(
+ picker.expectedValues.includes(pickerValueNormalized),
+ `Picker "${picker.id}" value "${pickerEl.value}" is one of two expected values: ${picker.expectedValues.join(" or ")}`
+ );
+ Assert.ok(
+ pickerEl.hasAttribute("data-l10n-id"),
+ `Picker has a localizable ID`
+ );
+ Assert.equal(
+ pickerEl.getAttribute("data-l10n-attrs"),
+ "label",
+ `Picker has a localizable "label" attribute`
+ );
+ Assert.ok(pickerEl.label, `Picker has a localizable label text`);
+
+ // Checking pref and picker values are referencing the same actual color:
+ const prefValueIsExpected =
+ picker.expectedValues.includes(prefValueNormalized);
+ const pickerValueIsExpected = picker.expectedValues.includes(
+ pickerValueNormalized
+ );
+ const valuesMatchExactly = prefValueNormalized === pickerValueNormalized;
+
+ Assert.ok(
+ valuesMatchExactly || (prefValueIsExpected && pickerValueIsExpected),
+ `Pref and picker values should be using the same color:
+ pref="${prefValue}", pickerEl="${pickerEl.value}", expected=
+ ${picker.expectedValues.join(" or ")}`
+ );
+
+ info("Test a color picker is focusable");
+
+ // Confirming the focus location:
+ Assert.equal(
+ pickerEl,
+ colorsDialogDoc.activeElement,
+ `The ${picker.id} picker is focused`
+ );
+ // Move focus to the next picker:
+ EventUtils.synthesizeKey("KEY_Tab");
+ }
+ info("Close Colors dialog");
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ // Confirm the focus is returned to the "Manage Colors" button:
+ Assert.equal(
+ button,
+ gBrowser.contentDocument.activeElement,
+ "Manage Colors button is focused"
+ );
+});
diff --git a/browser/locales/en-US/browser/preferences/colors.ftl b/browser/locales/en-US/browser/preferences/colors.ftl
@@ -11,16 +11,20 @@ colors-close-key =
colors-text-and-background = Text and Background
-colors-text-header = Text
+colors-text =
+ .label = Text
.accesskey = T
-colors-background = Background
+colors-text-background =
+ .label = Background
.accesskey = B
colors-links-header = Link Colors
-colors-unvisited-links = Unvisited Links
+colors-links-unvisited =
+ .label = Unvisited Links
.accesskey = L
-colors-visited-links = Visited Links
+colors-links-visited =
+ .label = Visited Links
.accesskey = V
diff --git a/python/l10n/fluent_migrations/bug_1976118_colors_dialog_with_moz_input_color_pickers.py b/python/l10n/fluent_migrations/bug_1976118_colors_dialog_with_moz_input_color_pickers.py
@@ -0,0 +1,33 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+from fluent.migrate.helpers import transforms_from
+
+
+def migrate(ctx):
+ """Bug 1976118 - Replacing default color pickers on Colors Dialog with moz-input-color components, part {index}."""
+
+ source = "browser/browser/preferences/colors.ftl"
+ target = source
+
+ ctx.add_transforms(
+ target,
+ target,
+ transforms_from(
+ """
+colors-text =
+ .label = {COPY_PATTERN(from_path, "colors-text-header")}
+ .accesskey = {COPY_PATTERN(from_path, "colors-text-header.accesskey")}
+colors-text-background =
+ .label = {COPY_PATTERN(from_path, "colors-background")}
+ .accesskey = {COPY_PATTERN(from_path, "colors-background.accesskey")}
+colors-links-unvisited =
+ .label = {COPY_PATTERN(from_path, "colors-unvisited-links")}
+ .accesskey = {COPY_PATTERN(from_path, "colors-unvisited-links.accesskey")}
+colors-links-visited =
+ .label = {COPY_PATTERN(from_path, "colors-visited-links")}
+ .accesskey = {COPY_PATTERN(from_path, "colors-visited-links.accesskey")}
+""",
+ from_path=source,
+ ),
+ )