commit 91742328b3cd66a9ba94b2211a4a5457a82cea94 parent 3c797bb745ce2b14ef5fa407599dbf57e726fa43 Author: Kagami Sascha Rosylight <krosylight@proton.me> Date: Sun, 21 Dec 2025 12:38:27 +0000 Bug 1629388 - Use HTML panel for color picker r=desktop-theme-reviewers,webidl,frontend-codestyle-reviewers,tschuster,emilio This adds a pref `dom.forms.html_color_picker.enabled`. Differential Revision: https://phabricator.services.mozilla.com/D274610 Diffstat:
21 files changed, 414 insertions(+), 6 deletions(-)
diff --git a/.stylelintrc.js b/.stylelintrc.js @@ -422,6 +422,7 @@ module.exports = { "testing/**", // UA Widgets should not use design tokens "toolkit/themes/shared/colorpicker-common.css", + "toolkit/themes/shared/colorpicker.css", "toolkit/themes/shared/media/pipToggle.css", "toolkit/themes/shared/media/videocontrols.css", "toolkit/content/widgets/datetimebox.css", diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp @@ -23,6 +23,7 @@ #include "mozilla/PresShell.h" #include "mozilla/PresState.h" #include "mozilla/ServoCSSParser.h" +#include "mozilla/ServoComputedData.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/StaticPrefs_signon.h" #include "mozilla/TextControlState.h" @@ -803,6 +804,16 @@ nsresult HTMLInputElement::InitColorPicker() { return NS_OK; } + // NOTE(krosylight): Android doesn't support HTML widgets. We can modify + // GeckoView to handle MozOpenColorPicker and let it keep using its current + // picker, but for now this is ok. +#ifndef ANDROID + if (StaticPrefs::dom_forms_html_color_picker_enabled()) { + OpenColorPicker(); + return NS_OK; + } +#endif + // Get Loc title nsAutoString title; nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES, @@ -2046,6 +2057,46 @@ Decimal HTMLInputElement::GetStepBase() const { return kDefaultStepBase; } +void HTMLInputElement::GetColor(InputPickerColor& aValue) { + MOZ_ASSERT(mType == FormControlType::InputColor, + "getColor is only for type=color."); + + nsAutoString value; + GetValue(value, CallerType::System); + + StyleAbsoluteColor color = + MaybeComputeColor(OwnerDoc(), value).valueOr(StyleAbsoluteColor::BLACK); + aValue.mComponent1 = color.components._0; + aValue.mComponent2 = color.components._1; + aValue.mComponent3 = color.components._2; + // aValue.mAlpha = color.alpha; + // aValue.mColorSpace = mColorSpace; +} + +void HTMLInputElement::SetUserInputColor(const InputPickerColor& aValue) { + MOZ_ASSERT(mType == FormControlType::InputColor, + "setUserInputColor is only for type=color."); + + // TODO(krosylight): We should ultimately get a helper method where the compat + // serialization happens only conditionally + nsAutoString serialized; + SerializeColorForHTMLCompatibility( + StyleAbsoluteColor{ + .components = + StyleColorComponents{ + ._0 = aValue.mComponent1, + ._1 = aValue.mComponent2, + ._2 = aValue.mComponent3, + }, + .alpha = 1, + .color_space = StyleColorSpace::Srgb, + }, + serialized); + + // (We are either Chrome/UA but the principal doesn't matter for color inputs) + SetUserInput(serialized, *NodePrincipal()); +} + Decimal HTMLInputElement::GetValueIfStepped(int32_t aStep, StepCallerType aCallerType, ErrorResult& aRv) { @@ -2308,7 +2359,7 @@ void HTMLInputElement::OpenDateTimePicker(const DateTimeValue& aInitialValue) { } mDateTimeInputBoxValue = MakeUnique<DateTimeValue>(aInitialValue); - nsContentUtils::DispatchChromeEvent(OwnerDoc(), static_cast<Element*>(this), + nsContentUtils::DispatchChromeEvent(OwnerDoc(), this, u"MozOpenDateTimePicker"_ns, CanBubble::eYes, Cancelable::eYes); } @@ -2318,7 +2369,7 @@ void HTMLInputElement::CloseDateTimePicker() { return; } - nsContentUtils::DispatchChromeEvent(OwnerDoc(), static_cast<Element*>(this), + nsContentUtils::DispatchChromeEvent(OwnerDoc(), this, u"MozCloseDateTimePicker"_ns, CanBubble::eYes, Cancelable::eYes); } @@ -2327,6 +2378,16 @@ void HTMLInputElement::SetOpenState(bool aIsOpen) { SetStates(ElementState::OPEN, aIsOpen); } +void HTMLInputElement::OpenColorPicker() { + if (NS_WARN_IF(mType != FormControlType::InputColor)) { + return; + } + + nsContentUtils::DispatchChromeEvent(OwnerDoc(), this, + u"MozOpenColorPicker"_ns, CanBubble::eYes, + Cancelable::eYes); +} + void HTMLInputElement::SetFocusState(bool aIsFocused) { if (NS_WARN_IF(!IsDateTimeInputType(mType))) { return; diff --git a/dom/html/HTMLInputElement.h b/dom/html/HTMLInputElement.h @@ -790,6 +790,8 @@ class HTMLInputElement final : public TextControlElement, */ void SetOpenState(bool aIsOpen); + void OpenColorPicker(); + /* * Called from datetime input box binding when inner text fields are focused * or blurred. @@ -811,6 +813,16 @@ class HTMLInputElement final : public TextControlElement, double GetMinimumAsDouble() { return GetMinimum().toDouble(); } double GetMaximumAsDouble() { return GetMaximum().toDouble(); } + /** + * Return the current value as InputPickerColor. + */ + void GetColor(InputPickerColor& aValue); + + /** + * Converts the InputPickerColor into a string and set it as user input. + */ + void SetUserInputColor(const InputPickerColor& aValue); + void StartNumberControlSpinnerSpin(); enum SpinnerStopState { eAllowDispatchingEvents, eDisallowDispatchingEvents }; void StopNumberControlSpinnerSpin( diff --git a/dom/html/test/forms/mochitest.toml b/dom/html/test/forms/mochitest.toml @@ -6,7 +6,10 @@ support-files = [ "FAIL.html", "PASS.html", ] -prefs = ["formhelper.autozoom.force-disable.test-only=true"] +prefs = [ + "formhelper.autozoom.force-disable.test-only=true", + "dom.forms.html_color_picker.enabled=false", +] ["test_MozEditableElement_setUserInput.html"] diff --git a/dom/webidl/HTMLInputElement.webidl b/dom/webidl/HTMLInputElement.webidl @@ -246,6 +246,8 @@ partial interface HTMLInputElement { attribute boolean webkitdirectory; }; +// Chrome-only functions for datetime picker + dictionary DateTimeValue { long hour; long minute; @@ -288,3 +290,23 @@ partial interface HTMLInputElement { [Func="IsChromeOrUAWidget", BinaryName="getStepBaseAsDouble"] double getStepBase(); }; + +// Chrome-only functions for color picker + +dictionary InputPickerColor { + required float component1; + required float component2; + required float component3; + + // bug 1919718 + // required float alpha; + // required InputColorSpace colorSpace; +}; + +partial interface HTMLInputElement { + [Func="IsChromeOrUAWidget"] + InputPickerColor getColor(); + + [Func="IsChromeOrUAWidget"] + undefined setUserInputColor(InputPickerColor aColor); +}; diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml @@ -3477,6 +3477,13 @@ value: @IS_ANDROID@ mirror: always +#ifndef ANDROID +- name: dom.forms.html_color_picker.enabled + type: bool + value: @IS_NIGHTLY_BUILD@ + mirror: always +#endif + - name: dom.forms.always_allow_pointer_events.enabled type: bool value: true diff --git a/testing/web-platform/meta/css/css-pseudo/input-element-pseudo-open.optional.html.ini b/testing/web-platform/meta/css/css-pseudo/input-element-pseudo-open.optional.html.ini @@ -31,6 +31,5 @@ [CSS :open for <input type=color>] expected: - if (os == "linux") and not fission and debug: [PASS, FAIL] - if (os == "linux") and not fission and not debug: [FAIL, PASS] - if os == "mac": [PASS, TIMEOUT] + if os == "android": PASS + FAIL diff --git a/testing/web-platform/mozilla/meta/html/semantics/forms/the-input-element/color-picker-value.html.ini b/testing/web-platform/mozilla/meta/html/semantics/forms/the-input-element/color-picker-value.html.ini @@ -0,0 +1,6 @@ +[color-picker-value.html] + expected: + if (os == "android"): TIMEOUT + [Input picker value without alpha nor colorspace attributes] + expected: + if (os == "android"): TIMEOUT diff --git a/testing/web-platform/mozilla/tests/html/semantics/forms/the-input-element/color-picker-value.html b/testing/web-platform/mozilla/tests/html/semantics/forms/the-input-element/color-picker-value.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<input type="color" id="input" value="#ffffff"> +<script> +promise_setup(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.forms.html_color_picker.enabled", true], + // The pref sets `color(srgb 0.1 0.2 0.3)` on showPicker + ["dom.forms.html_color_picker.testing", true], + ] + }); +}); + +promise_test(async () => { + await test_driver.bless(); + input.showPicker(); + + const { promise, resolve } = Promise.withResolvers(); + input.addEventListener("change", resolve, { once: true }); + await promise; + + assert_equals(input.value, "#1a334d", "HTML compatibility serialization should happen"); +}, "Input picker value without alpha nor colorspace attributes"); +</script> diff --git a/toolkit/actors/ColorPickerChild.sys.mjs b/toolkit/actors/ColorPickerChild.sys.mjs @@ -0,0 +1,56 @@ +/* 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/. */ + +import { InputPickerChildCommon } from "./InputPickerChildCommon.sys.mjs"; + +export class ColorPickerChild extends InputPickerChildCommon { + initialValue = null; + + constructor() { + super("ColorPicker"); + } + + /** + * Cleanup function called when picker is closed. + * + * @param {HTMLInputElement} inputElement + */ + closeImpl(inputElement) { + inputElement.setOpenState(false); + if (this.initialValue !== inputElement.value) { + inputElement.dispatchEvent(new inputElement.ownerGlobal.Event("change")); + } + } + + /** + * Element updater function called when the picker value is changed. + * + * @param {ReceiveMessageArgument} aMessage + * @param {HTMLInputElement} inputElement + */ + pickerValueChangedImpl(aMessage, inputElement) { + if (!aMessage.data) { + inputElement.setUserInput(this.initialValue); + return; + } + + const { rgb } = aMessage.data; + inputElement.setUserInputColor({ + component1: rgb[0], + component2: rgb[1], + component3: rgb[2], + }); + } + + /** + * Picker initialization function called when opening the picker + * + * @param {HTMLInputElement} inputElement + * @returns An argument object to pass to the picker panel, or undefined to stop. + */ + openPickerImpl(inputElement) { + this.initialValue = inputElement.value; + return { value: inputElement.getColor() }; + } +} diff --git a/toolkit/actors/ColorPickerParent.sys.mjs b/toolkit/actors/ColorPickerParent.sys.mjs @@ -0,0 +1,26 @@ +/* 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/. */ + +import { InputPickerParentCommon } from "./InputPickerParentCommon.sys.mjs"; + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + ColorPickerPanel: "moz-src:///toolkit/modules/ColorPickerPanel.sys.mjs", +}); + +export class ColorPickerParent extends InputPickerParentCommon { + constructor() { + super("ColorPicker"); + } + + /** + * A picker creator function called when showing a picker + * + * @param {XULElement} panel A panel element + * @returns A panel object that manages the element + */ + createPickerImpl(panel) { + return new lazy.ColorPickerPanel(panel); + } +} diff --git a/toolkit/actors/moz.build b/toolkit/actors/moz.build @@ -83,6 +83,8 @@ FINAL_TARGET_FILES.actors += [ ] MOZ_SRC_FILES += [ + "ColorPickerChild.sys.mjs", + "ColorPickerParent.sys.mjs", "DateTimePickerChild.sys.mjs", "DateTimePickerParent.sys.mjs", "InputPickerChildCommon.sys.mjs", diff --git a/toolkit/content/colorpicker.html b/toolkit/content/colorpicker.html @@ -0,0 +1,32 @@ +<!-- 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 https://mozilla.org/MPL/2.0/. --> +<!doctype html> +<meta http-equiv="Content-Security-Policy" content="default-src chrome:" /> +<link rel="stylesheet" href="chrome://global/skin/colorpicker.css" /> +<script + src="chrome://global/content/bindings/colorpicker.mjs" + type="module" +></script> +<section class="spectrum-color-picker"> + <div + class="spectrum-color spectrum-box" + tabindex="0" + role="slider" + aria-describedby="spectrum-dragger" + > + <div class="spectrum-sat"> + <div class="spectrum-val"> + <div class="spectrum-dragger" id="spectrum-dragger"></div> + </div> + </div> + </div> +</section> +<section class="spectrum-controls"> + <div class="spectrum-color-preview"></div> + <div class="spectrum-slider-container"> + <div class="spectrum-hue spectrum-box"></div> + <!-- Unhide alpha in bug 1919718 --> + <div class="spectrum-alpha spectrum-checker spectrum-box" hidden></div> + </div> +</section> diff --git a/toolkit/content/jar.mn b/toolkit/content/jar.mn @@ -50,6 +50,7 @@ toolkit.jar: content/global/aboutUrlClassifier.css * content/global/buildconfig.html content/global/buildconfig.css + content/global/colorpicker.html content/global/contentAreaUtils.js #ifndef MOZ_FENNEC content/global/editMenuOverlay.js @@ -78,6 +79,7 @@ toolkit.jar: content/global/widgets.css content/global/bindings/calendar.js (widgets/calendar.js) content/global/bindings/colorpicker-common.mjs (widgets/colorpicker-common.mjs) + content/global/bindings/colorpicker.mjs (widgets/colorpicker.mjs) content/global/bindings/datekeeper.js (widgets/datekeeper.js) content/global/bindings/datepicker.js (widgets/datepicker.js) content/global/bindings/datetimebox.css (widgets/datetimebox.css) diff --git a/toolkit/content/widgets/colorpicker.mjs b/toolkit/content/widgets/colorpicker.mjs @@ -0,0 +1,47 @@ +/* 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/. */ + +// @ts-nocheck Do this after migration from devtools + +import { ColorPickerCommon } from "./colorpicker-common.mjs"; + +class ColorPicker extends ColorPickerCommon { + onChange() { + window.postMessage( + { + name: "PickerPopupChanged", + detail: { rgb: this.rgbFloat }, + }, + "*" + ); + } +} + +let picker = new ColorPicker(document.body); +window.addEventListener("message", ev => { + switch (ev.data.name) { + case "PickerInit": { + let { value } = ev.data.detail; + picker.rgbFloat = [ + value.component1, + value.component2, + value.component3, + 1, + ]; + picker.show(); + } + } +}); + +window.addEventListener("keydown", ev => { + if (["Enter", "Escape", " "].includes(ev.key)) { + window.postMessage( + { + name: "ClosePopup", + detail: ev.key === "Escape", + }, + "*" + ); + } +}); diff --git a/toolkit/modules/ActorManagerParent.sys.mjs b/toolkit/modules/ActorManagerParent.sys.mjs @@ -768,6 +768,23 @@ if (AppConstants.platform != "android") { remoteTypes: ["privilegedabout"], enablePreference: "browser.translations.enable", }; + + JSWINDOWACTORS.ColorPicker = { + parent: { + esModuleURI: "moz-src:///toolkit/actors/ColorPickerParent.sys.mjs", + }, + + child: { + esModuleURI: "moz-src:///toolkit/actors/ColorPickerChild.sys.mjs", + events: { + MozOpenColorPicker: {}, + MozCloseColorPicker: {}, + }, + }, + + includeChrome: true, + allFrames: true, + }; } export var ActorManagerParent = { diff --git a/toolkit/modules/ColorPickerPanel.sys.mjs b/toolkit/modules/ColorPickerPanel.sys.mjs @@ -0,0 +1,67 @@ +/* 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/. */ + +import { InputPickerPanelCommon } from "./InputPickerPanelCommon.sys.mjs"; + +/** @import {OpenPickerInfo} from "./InputPickerPanelCommon.sys.mjs" */ + +const COLOR_PICKER_WIDTH = "215px"; +const COLOR_PICKER_HEIGHT = "170px"; + +export class ColorPickerPanel extends InputPickerPanelCommon { + constructor(element) { + super(element, "chrome://global/content/colorpicker.html"); + } + + /** + * Picker window initialization function called when opening the picker + * + * @param {string} type The input element type + * @returns {OpenPickerInfo} + */ + openPickerImpl(type) { + return { + type, + width: COLOR_PICKER_WIDTH, + height: COLOR_PICKER_HEIGHT, + }; + } + + /** + * Popup frame initialization function called when the picker window is loaded + * + * @param {string} _type The picker type + * @param {object} detail The argument from the child actor's openPickerImpl + * @returns An argument object to pass to the popup frame + */ + initPickerImpl(_type, detail) { + if ( + Services.prefs.getBoolPref("dom.forms.html_color_picker.testing", false) + ) { + this.handleMessage({ + data: { + name: "PickerPopupChanged", + detail: { rgb: [0.1, 0.2, 0.3] }, + }, + }); + this.handleMessage({ data: { name: "ClosePopup" } }); + } + + return { + value: detail.value, + }; + } + + /** + * Input element state updater function called when the picker value is changed + * + * @param {string} _type + * @param {object} pickerState + */ + sendPickerValueChangedImpl(_type, pickerState) { + return { + rgb: pickerState.rgb, + }; + } +} diff --git a/toolkit/modules/moz.build b/toolkit/modules/moz.build @@ -215,6 +215,7 @@ EXTRA_JS_MODULES += [ ] MOZ_SRC_FILES += [ + "ColorPickerPanel.sys.mjs", "DateTimePickerPanel.sys.mjs", "InputPickerPanelCommon.sys.mjs", ] diff --git a/toolkit/themes/shared/colorpicker.css b/toolkit/themes/shared/colorpicker.css @@ -0,0 +1,9 @@ +/* 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/. */ + +@import "colorpicker-common.css"; + +body { + margin: 5px; +} diff --git a/toolkit/themes/shared/desktop-jar.inc.mn b/toolkit/themes/shared/desktop-jar.inc.mn @@ -26,6 +26,7 @@ skin/classic/global/radio.css (../../shared/radio.css) skin/classic/global/close-icon.css (../../shared/close-icon.css) skin/classic/global/colorpicker-common.css (../../shared/colorpicker-common.css) + skin/classic/global/colorpicker.css (../../shared/colorpicker.css) skin/classic/global/commonDialog.css (../../shared/commonDialog.css) skin/classic/global/datetimeinputpickers.css (../../shared/datetimeinputpickers.css) skin/classic/global/design-system/text-and-typography.css(../../shared/design-system/src/text-and-typography.css) diff --git a/tools/@types/generated/lib.gecko.dom.d.ts b/tools/@types/generated/lib.gecko.dom.d.ts @@ -1623,6 +1623,12 @@ interface InputEventInit extends UIEventInit { targetRanges?: StaticRange[]; } +interface InputPickerColor { + component1: number; + component2: number; + component3: number; +} + interface InspectorCSSPropertyDefinition { fromJS: boolean; inherits: boolean; @@ -11838,6 +11844,7 @@ interface HTMLInputElement extends HTMLElement, MozEditableElement, MozImageLoad checkValidity(): boolean; closeDateTimePicker(): void; getAutocompleteInfo(): AutocompleteInfo | null; + getColor(): InputPickerColor; getDateTimeInputBoxValue(): DateTimeValue; getFilesAndDirectories(): Promise<(File | Directory)[]>; getMaximum(): number; @@ -11859,6 +11866,7 @@ interface HTMLInputElement extends HTMLElement, MozEditableElement, MozImageLoad setRangeText(replacement: string): void; setRangeText(replacement: string, start: number, end: number, selectionMode?: SelectionMode): void; setSelectionRange(start: number, end: number, direction?: string): void; + setUserInputColor(aColor: InputPickerColor): void; showPicker(): void; stepDown(n?: number): void; stepUp(n?: number): void;