commit 05867971415d62939ebe3b530f7f5256fd67b220 parent aa2685ed07d7404e3293e0df9d8497b9217c1d5f Author: pstanciu <pstanciu@mozilla.com> Date: Sat, 20 Dec 2025 01:49:11 +0200 Revert "Bug 1629388, Bug 2000644, Bug 2004409 - Use HTML panel for color picker r=desktop-theme-reviewers,webidl,frontend-codestyle-reviewers,tschuster,emilio" for causing wpt failures @color-picker-value.html This reverts commit 6c5157b82813a73c7585eb2cdf810ecbcb5dc817. Revert "Bug 2000644 - Introduce a superclass of color picker to prepare for multiple subclasses r=devtools-reviewers,nchevobbe,desktop-theme-reviewers,frontend-codestyle-reviewers,hjones" This reverts commit 06041619774f8930107cffb86200abed5dccde1b. Revert "Bug 2004409 - Split window opener logic from DateTimePicker classes r=emilio" This reverts commit dbe80fc5ce76631fcfb1a7c9863610bccd3f0fdb. Diffstat:
38 files changed, 985 insertions(+), 1659 deletions(-)
diff --git a/.stylelintrc.js b/.stylelintrc.js @@ -421,8 +421,6 @@ module.exports = { // Testing does not use design tokens "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/devtools/client/shared/widgets/Spectrum.js b/devtools/client/shared/widgets/Spectrum.js @@ -4,10 +4,6 @@ "use strict"; -const { ColorPickerCommon } = ChromeUtils.importESModule( - "chrome://global/content/bindings/colorpicker-common.mjs" -); - const EventEmitter = require("resource://devtools/shared/event-emitter.js"); const { MultiLocalizationHelper, @@ -27,7 +23,21 @@ const L10N = new MultiLocalizationHelper( "devtools/client/locales/accessibility.properties", "devtools/client/locales/inspector.properties" ); +const ARROW_KEYS = ["ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft"]; +const [ArrowUp, ArrowRight, ArrowDown, ArrowLeft] = ARROW_KEYS; const XHTML_NS = "http://www.w3.org/1999/xhtml"; +const SLIDER = { + hue: { + MIN: "0", + MAX: "128", + STEP: "1", + }, + alpha: { + MIN: "0", + MAX: "1", + STEP: "0.01", + }, +}; /** * Spectrum creates a color picker widget in any container you give it. @@ -52,15 +62,22 @@ const XHTML_NS = "http://www.w3.org/1999/xhtml"; * Fires the following events: * - changed : When the user changes the current color */ -class Spectrum extends ColorPickerCommon { +class Spectrum { constructor(parentEl, rgb) { - const element = parentEl.ownerDocument.createElement("div"); + EventEmitter.decorate(this); + + this.document = parentEl.ownerDocument; + this.element = parentEl.ownerDocument.createElementNS(XHTML_NS, "div"); + this.parentEl = parentEl; + + this.element.className = "spectrum-container"; // eslint-disable-next-line no-unsanitized/property - element.innerHTML = ` + this.element.innerHTML = ` <section class="spectrum-color-picker"> <div class="spectrum-color spectrum-box" tabindex="0" role="slider" + title="${ColorPickerBundle.formatValueSync("colorpicker-tooltip-spectrum-dragger-title")}" aria-describedby="spectrum-dragger"> <div class="spectrum-sat"> <div class="spectrum-val"> @@ -94,10 +111,20 @@ class Spectrum extends ColorPickerCommon { </div> </section> `; - super(element); - EventEmitter.decorate(this); - parentEl.appendChild(this.element); + this.onElementClick = this.onElementClick.bind(this); + this.element.addEventListener("click", this.onElementClick); + + this.parentEl.appendChild(this.element); + + // Color spectrum dragger. + this.dragger = this.element.querySelector(".spectrum-color"); + this.dragHelper = this.element.querySelector(".spectrum-dragger"); + draggable(this.dragger, this.dragHelper, this.onDraggerMove.bind(this)); + + // Here we define the components for the "controls" section of the color picker. + this.controls = this.element.querySelector(".spectrum-controls"); + this.colorPreview = this.element.querySelector(".spectrum-color-preview"); // Create the eyedropper. const eyedropper = this.document.createElementNS(XHTML_NS, "button"); @@ -110,6 +137,14 @@ class Spectrum extends ColorPickerCommon { ); this.controls.insertBefore(eyedropper, this.colorPreview); + // Hue slider and alpha slider + this.hueSlider = this.createSlider("hue", this.onHueSliderMove.bind(this)); + this.hueSlider.setAttribute("aria-describedby", this.dragHelper.id); + this.alphaSlider = this.createSlider( + "alpha", + this.onAlphaSliderMove.bind(this) + ); + // Color contrast this.spectrumContrast = this.element.querySelector( ".spectrum-color-contrast" @@ -143,6 +178,11 @@ class Spectrum extends ColorPickerCommon { : null; } + /** @param {[number, number, number, number]} color */ + set rgb([r, g, b, a]) { + this.hsv = [...InspectorUtils.rgbToHsv(r / 255, g / 255, b / 255), a]; + } + set backgroundColorData(colorData) { this._backgroundColorData = colorData; } @@ -155,11 +195,117 @@ class Spectrum extends ColorPickerCommon { return this._textProps; } + #toRgbInt(rgbFloat) { + return rgbFloat.map(c => Math.round(c * 255)); + } + + get rgbFloat() { + const [h, s, v, a] = this.hsv; + return [...InspectorUtils.hsvToRgb(h, s, v), a]; + } + + get rgb() { + const [r, g, b, a] = this.rgbFloat; + return [...this.#toRgbInt([r, g, b]), a]; + } + + /** + * Map current rgb to the closest color available in the database by + * calculating the delta-E between each available color and the current rgb + * + * @return {string} + * Color name or closest color name + */ + get colorName() { + const [r, g, b] = this.rgbFloat; + const { exact, colorName } = InspectorUtils.rgbToNearestColorName(r, g, b); + return exact + ? colorName + : ColorPickerBundle.formatValueSync( + "colorpicker-tooltip-color-name-title", + { colorName } + ); + } + + get rgbNoSatVal() { + return [ + ...this.#toRgbInt(InspectorUtils.hsvToRgb(this.hsv[0], 1, 1)), + this.hsv[3], + ]; + } + + get rgbCssString() { + const rgb = this.rgb; + return ( + "rgba(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ", " + rgb[3] + ")" + ); + } + + show() { + this.dragWidth = this.dragger.offsetWidth; + this.dragHeight = this.dragger.offsetHeight; + this.dragHelperHeight = this.dragHelper.offsetHeight; + + this.updateUI(); + } + + onElementClick(e) { + e.stopPropagation(); + } + + onHueSliderMove() { + this.hsv[0] = this.hueSlider.value / this.hueSlider.max; + this.updateUI(); + this.onChange(); + } + + onDraggerMove(dragX, dragY) { + this.hsv[1] = dragX / this.dragWidth; + this.hsv[2] = (this.dragHeight - dragY) / this.dragHeight; + this.updateUI(); + this.onChange(); + } + + onAlphaSliderMove() { + this.hsv[3] = this.alphaSlider.value / this.alphaSlider.max; + this.updateUI(); + this.onChange(); + } + onChange() { this.emit("changed", this.rgb, this.rgbCssString); } /** + * Creates and initializes a slider element, attaches it to its parent container + * based on the slider type and returns it + * + * @param {string} sliderType + * The type of the slider (i.e. alpha or hue) + * @param {Function} onSliderMove + * The function to tie the slider to on input + * @return {DOMNode} + * Newly created slider + */ + createSlider(sliderType, onSliderMove) { + const container = this.element.querySelector(`.spectrum-${sliderType}`); + + const slider = this.document.createElementNS(XHTML_NS, "input"); + slider.className = `spectrum-${sliderType}-input`; + slider.type = "range"; + slider.min = SLIDER[sliderType].MIN; + slider.max = SLIDER[sliderType].MAX; + slider.step = SLIDER[sliderType].STEP; + slider.title = ColorPickerBundle.formatValueSync( + `colorpicker-tooltip-${sliderType}-slider-title` + ); + slider.addEventListener("input", onSliderMove); + + container.appendChild(slider); + return slider; + } + + /** * Updates the contrast label with appropriate content (i.e. large text indicator * if the contrast is calculated for large text, or a base label otherwise) * @@ -228,6 +374,81 @@ class Spectrum extends ColorPickerCommon { ); } + updateAlphaSlider() { + // Set alpha slider background + const rgb = this.rgb; + + const rgbNoAlpha = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")"; + const rgbAlpha0 = "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ", 0)"; + const alphaGradient = + "linear-gradient(to right, " + rgbAlpha0 + ", " + rgbNoAlpha + ")"; + this.alphaSlider.style.background = alphaGradient; + } + + updateColorPreview() { + // Overlay the rgba color over a checkered image background. + this.colorPreview.style.setProperty("--overlay-color", this.rgbCssString); + + // We should be able to distinguish the color preview on high luminance rgba values. + // Give the color preview a light grey border if the luminance of the current rgba + // tuple is great. + const colorLuminance = InspectorUtils.relativeLuminance(...this.rgbFloat); + this.colorPreview.classList.toggle("high-luminance", colorLuminance > 0.85); + + // Set title on color preview for better UX + this.colorPreview.title = this.colorName; + } + + updateDragger() { + // Set dragger background color + const flatColor = + "rgb(" + + this.rgbNoSatVal[0] + + ", " + + this.rgbNoSatVal[1] + + ", " + + this.rgbNoSatVal[2] + + ")"; + this.dragger.style.backgroundColor = flatColor; + + // Set dragger aria attributes + this.dragger.setAttribute("aria-valuetext", this.rgbCssString); + } + + updateHueSlider() { + // Set hue slider aria attributes + this.hueSlider.setAttribute("aria-valuetext", this.rgbCssString); + } + + updateHelperLocations() { + const h = this.hsv[0]; + const s = this.hsv[1]; + const v = this.hsv[2]; + + // Placing the color dragger + let dragX = s * this.dragWidth; + let dragY = this.dragHeight - v * this.dragHeight; + const helperDim = this.dragHelperHeight / 2; + + dragX = Math.max( + -helperDim, + Math.min(this.dragWidth - helperDim, dragX - helperDim) + ); + dragY = Math.max( + -helperDim, + Math.min(this.dragHeight - helperDim, dragY - helperDim) + ); + + this.dragHelper.style.top = dragY + "px"; + this.dragHelper.style.left = dragX + "px"; + + // Placing the hue slider + this.hueSlider.value = h * this.hueSlider.max; + + // Placing the alpha slider + this.alphaSlider.value = this.hsv[3] * this.alphaSlider.max; + } + /* Calculates the contrast ratio for the currently selected * color against a single or range of background colors and displays contrast ratio section * components depending on the contrast ratio calculated. @@ -324,18 +545,128 @@ class Spectrum extends ColorPickerCommon { } updateUI() { - super.updateUI(); + this.updateHelperLocations(); + + this.updateColorPreview(); + this.updateDragger(); + this.updateHueSlider(); + this.updateAlphaSlider(); this.updateContrast(); } destroy() { - super.destroy(); + this.element.removeEventListener("click", this.onElementClick); + this.hueSlider.removeEventListener("input", this.onHueSliderMove); + this.alphaSlider.removeEventListener("input", this.onAlphaSliderMove); + + this.parentEl.removeChild(this.element); + + this.dragger = this.dragHelper = null; + this.alphaSlider = null; + this.hueSlider = null; + this.colorPreview = null; + this.element = null; + this.parentEl = null; this.spectrumContrast = null; this.contrastValue = this.contrastValueMin = this.contrastValueMax = null; this.contrastLabel = null; } } +function draggable(element, dragHelper, onmove) { + onmove = onmove || function () {}; + + const doc = element.ownerDocument; + let dragging = false; + let offset = {}; + let maxHeight = 0; + let maxWidth = 0; + + function setDraggerDimensionsAndOffset() { + maxHeight = element.offsetHeight; + maxWidth = element.offsetWidth; + offset = element.getBoundingClientRect(); + } + + function prevent(e) { + e.stopPropagation(); + e.preventDefault(); + } + + function move(e) { + if (dragging) { + if (e.buttons === 0) { + // The button is no longer pressed but we did not get a mouseup event. + stop(); + return; + } + const pageX = e.pageX; + const pageY = e.pageY; + + const dragX = Math.max(0, Math.min(pageX - offset.left, maxWidth)); + const dragY = Math.max(0, Math.min(pageY - offset.top, maxHeight)); + + onmove.apply(element, [dragX, dragY]); + } + } + + function start(e) { + const rightClick = e.which === 3; + + if (!rightClick && !dragging) { + dragging = true; + setDraggerDimensionsAndOffset(); + + move(e); + + doc.addEventListener("selectstart", prevent); + doc.addEventListener("dragstart", prevent); + doc.addEventListener("mousemove", move); + doc.addEventListener("mouseup", stop); + + prevent(e); + } + } + + function stop() { + if (dragging) { + doc.removeEventListener("selectstart", prevent); + doc.removeEventListener("dragstart", prevent); + doc.removeEventListener("mousemove", move); + doc.removeEventListener("mouseup", stop); + } + dragging = false; + } + + function onKeydown(e) { + const { key } = e; + + if (!ARROW_KEYS.includes(key)) { + return; + } + + setDraggerDimensionsAndOffset(); + const { offsetHeight, offsetTop, offsetLeft } = dragHelper; + let dragX = offsetLeft + offsetHeight / 2; + let dragY = offsetTop + offsetHeight / 2; + + if (key === ArrowLeft && dragX > 0) { + dragX -= 1; + } else if (key === ArrowRight && dragX < maxWidth) { + dragX += 1; + } else if (key === ArrowUp && dragY > 0) { + dragY -= 1; + } else if (key === ArrowDown && dragY < maxHeight) { + dragY += 1; + } + + onmove.apply(element, [dragX, dragY]); + } + + element.addEventListener("mousedown", start); + element.addEventListener("keydown", onKeydown); +} + /** * Calculates the contrast ratio for a DOM node's computed style against * a given background. diff --git a/devtools/client/shared/widgets/spectrum.css b/devtools/client/shared/widgets/spectrum.css @@ -2,8 +2,6 @@ * 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 "chrome://global/skin/colorpicker-common.css"; - :root { --accessibility-contrast-swatch-border-color: var(--grey-40); --learn-more-underline: light-dark(var(--grey-30), var(--grey-50)); @@ -29,10 +27,45 @@ /* Mix-in classes */ +.spectrum-checker { + background-color: #eee; + background-image: + linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc), + linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc); + background-size: 12px 12px; + background-position: + 0 0, + 6px 6px; + /* Make sure that the background color is properly set in High Contrast Mode */ + forced-color-adjust: none; +} + +.spectrum-box { + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 2px; + background-clip: content-box; + + :root[forced-colors-active] & { + border-color: initial; + } +} + +/* Elements */ + #spectrum-tooltip { padding: 5px; } +/** + * Spectrum controls set the layout for the controls section of the color picker. + */ +.spectrum-controls { + display: flex; + justify-content: space-between; + margin-block-start: 10px; + margin-inline-end: 5px; +} + .spectrum-controls { width: 200px; } @@ -44,6 +77,147 @@ padding-block-end: 6px; } +/** + * This styles the color preview and adds a checkered background overlay inside of it. The overlay + * can be manipulated using the --overlay-color variable. + */ +.spectrum-color-preview { + --overlay-color: transparent; + border: 1px solid transparent; + border-radius: 50%; + width: 27px; + height: 27px; + background-color: #fff; + background-image: + linear-gradient(var(--overlay-color), var(--overlay-color)), linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%), + linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%); + background-size: 12px 12px; + background-position: + 0 0, + 6px 6px; + /* Make sure that the background color is properly set in High Contrast Mode */ + forced-color-adjust: none; + + :root[forced-colors-active] & { + border-color: CanvasText; + } +} + +.spectrum-color-preview.high-luminance { + border-color: #ccc; +} + +.spectrum-slider-container { + display: flex; + flex-direction: column; + justify-content: space-around; + width: 130px; + margin-inline-start: 10px; + height: 30px; +} + +/* Keep aspect ratio: +http://www.briangrinstead.com/blog/keep-aspect-ratio-with-html-and-css */ +.spectrum-color-picker { + position: relative; + width: 205px; + height: 120px; + /* Make sure that the background color is properly set in High Contrast Mode */ + forced-color-adjust: none; +} + +.spectrum-color { + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: 100%; +} + +.spectrum-sat, +.spectrum-val { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.spectrum-alpha { + margin-block-start: 3px; +} + +.spectrum-alpha, +.spectrum-hue { + position: relative; + height: 8px; +} + +.spectrum-alpha-input, +.spectrum-hue-input { + width: 100%; + margin: 0; + position: absolute; + height: 8px; + border-radius: 2px; + direction: initial; +} + +.spectrum-hue-input, +.spectrum-alpha-input { + outline-offset: 4px; +} + +.spectrum-hue-input::-moz-range-thumb, +.spectrum-alpha-input::-moz-range-thumb { + cursor: pointer; + height: 12px; + width: 12px; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.6); + background: #fff; + border-radius: 50%; + opacity: 0.9; + border: none; +} + +:root[forced-colors-active] :is(.spectrum-hue-input, .spectrum-alpha-input)::-moz-range-thumb { + background: ButtonFace; + border: 2px solid ButtonText; +} + +:root[forced-colors-active] :is(.spectrum-hue-input, .spectrum-alpha-input):is(:hover, :focus-visible)::-moz-range-thumb { + border-color: SelectedItem; +} + +.spectrum-hue-input::-moz-range-track { + border-radius: 2px; + height: 8px; + background: linear-gradient(to right, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); + /* Make sure that the background color is properly set in High Contrast Mode */ + forced-color-adjust: none; +} + +.spectrum-sat { + background-image: linear-gradient(to right, #fff, rgba(204, 154, 129, 0)); +} + +.spectrum-val { + background-image: linear-gradient(to top, #000000, rgba(204, 154, 129, 0)); +} + +.spectrum-dragger { + user-select: none; + position: absolute; + top: 0; + left: 0; + cursor: pointer; + border-radius: 50%; + height: 8px; + width: 8px; + border: 1px solid white; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.6); +} + .spectrum-color-contrast { padding-block-start: 8px; padding-inline-start: 4px; diff --git a/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js b/devtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js @@ -189,6 +189,9 @@ class SwatchColorPickerTooltip extends SwatchBasedEditorTooltip { learnMoreButton.addEventListener("keydown", e => e.stopPropagation()); } + // Add focus to the first focusable element in the tooltip and attach keydown + // event listener to tooltip + this.focusableElements[0].focus(); this.tooltip.container.addEventListener( "keydown", this._onTooltipKeydown, diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp @@ -23,7 +23,6 @@ #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" @@ -804,16 +803,6 @@ 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, @@ -2057,46 +2046,6 @@ 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) { @@ -2359,7 +2308,7 @@ void HTMLInputElement::OpenDateTimePicker(const DateTimeValue& aInitialValue) { } mDateTimeInputBoxValue = MakeUnique<DateTimeValue>(aInitialValue); - nsContentUtils::DispatchChromeEvent(OwnerDoc(), this, + nsContentUtils::DispatchChromeEvent(OwnerDoc(), static_cast<Element*>(this), u"MozOpenDateTimePicker"_ns, CanBubble::eYes, Cancelable::eYes); } @@ -2369,7 +2318,7 @@ void HTMLInputElement::CloseDateTimePicker() { return; } - nsContentUtils::DispatchChromeEvent(OwnerDoc(), this, + nsContentUtils::DispatchChromeEvent(OwnerDoc(), static_cast<Element*>(this), u"MozCloseDateTimePicker"_ns, CanBubble::eYes, Cancelable::eYes); } @@ -2378,16 +2327,6 @@ 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,8 +790,6 @@ 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. @@ -813,16 +811,6 @@ 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,10 +6,7 @@ support-files = [ "FAIL.html", "PASS.html", ] -prefs = [ - "formhelper.autozoom.force-disable.test-only=true", - "dom.forms.html_color_picker.enabled=false", -] +prefs = ["formhelper.autozoom.force-disable.test-only=true"] ["test_MozEditableElement_setUserInput.html"] diff --git a/dom/webidl/HTMLInputElement.webidl b/dom/webidl/HTMLInputElement.webidl @@ -246,8 +246,6 @@ partial interface HTMLInputElement { attribute boolean webkitdirectory; }; -// Chrome-only functions for datetime picker - dictionary DateTimeValue { long hour; long minute; @@ -290,23 +288,3 @@ 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,13 +3477,6 @@ 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/mochitest/BrowserTestUtils/BrowserTestUtils.sys.mjs b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.sys.mjs @@ -1467,7 +1467,7 @@ export var BrowserTestUtils = { let getPanel = () => win.document.getElementById("DateTimePickerPanel"); let panel = getPanel(); let ensureReady = async () => { - let frame = panel.querySelector("#DateTimePickerPanelPopupFrame"); + let frame = panel.querySelector("#dateTimePopupFrame"); let isValidUrl = () => { return ( frame.browsingContext?.currentURI?.spec == 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 @@ -30,4 +30,7 @@ if os == "mac": [PASS, TIMEOUT, NOTRUN] [CSS :open for <input type=color>] - expected: FAIL + 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] 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 @@ -1,29 +0,0 @@ -<!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 @@ -1,56 +0,0 @@ -/* 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 @@ -1,26 +0,0 @@ -/* 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/DateTimePickerChild.sys.mjs b/toolkit/actors/DateTimePickerChild.sys.mjs @@ -2,30 +2,77 @@ * 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"; +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + LayoutUtils: "resource://gre/modules/LayoutUtils.sys.mjs", +}); -export class DateTimePickerChild extends InputPickerChildCommon { +/** + * DateTimePickerChild is the communication channel between the input box + * (content) for date/time input types and its picker (chrome). + */ +export class DateTimePickerChild extends JSWindowActorChild { + /** + * On init, just listen for the event to open the picker, once the picker is + * opened, we'll listen for update and close events. + */ constructor() { - super("DateTimePicker"); + super(); + + this._inputElement = null; } /** * Cleanup function called when picker is closed. - * - * @param {HTMLInputElement} inputElement */ - closeImpl(inputElement) { - let dateTimeBoxElement = inputElement.dateTimeBoxElement; + close() { + this.removeListeners(this._inputElement); + let dateTimeBoxElement = this._inputElement.dateTimeBoxElement; if (!dateTimeBoxElement) { + this._inputElement = null; return; } // dateTimeBoxElement is within UA Widget Shadow DOM. // An event dispatch to it can't be accessed by document. - let win = inputElement.ownerGlobal; + let win = this._inputElement.ownerGlobal; dateTimeBoxElement.dispatchEvent( new win.CustomEvent("MozSetDateTimePickerState", { detail: false }) ); + + this._inputElement = null; + } + + /** + * Called after picker is opened to start listening for input box update + * events. + */ + addListeners(aElement) { + aElement.ownerGlobal.addEventListener("pagehide", this); + } + + /** + * Stop listeneing for events when picker is closed. + */ + removeListeners(aElement) { + aElement.ownerGlobal.removeEventListener("pagehide", this); + } + + /** + * Helper function that returns the CSS direction property of the element. + */ + getComputedDirection(aElement) { + return aElement.ownerGlobal + .getComputedStyle(aElement) + .getPropertyValue("direction"); + } + + /** + * Helper function that returns the rect of the element, which is the position + * relative to the left/top of the content area. + */ + getBoundingContentRect(aElement) { + return lazy.LayoutUtils.getElementBoundingScreenRect(aElement); } getTimePickerPref() { @@ -33,62 +80,119 @@ export class DateTimePickerChild extends InputPickerChildCommon { } /** - * Element updater function called when the picker value is changed. - * - * @param {ReceiveMessageArgument} aMessage - * @param {HTMLInputElement} inputElement + * nsIMessageListener. */ - pickerValueChangedImpl(aMessage, inputElement) { - let dateTimeBoxElement = inputElement.dateTimeBoxElement; - if (!dateTimeBoxElement) { - return; - } + receiveMessage(aMessage) { + switch (aMessage.name) { + case "FormDateTime:PickerClosed": { + if (!this._inputElement) { + return; + } - let win = inputElement.ownerGlobal; + this.close(); + break; + } + case "FormDateTime:PickerValueChanged": { + if (!this._inputElement) { + return; + } - // dateTimeBoxElement is within UA Widget Shadow DOM. - // An event dispatch to it can't be accessed by document. - dateTimeBoxElement.dispatchEvent( - new win.CustomEvent("MozPickerValueChanged", { - detail: Cu.cloneInto(aMessage.data, win), - }) - ); + let dateTimeBoxElement = this._inputElement.dateTimeBoxElement; + if (!dateTimeBoxElement) { + return; + } + + let win = this._inputElement.ownerGlobal; + + // dateTimeBoxElement is within UA Widget Shadow DOM. + // An event dispatch to it can't be accessed by document. + dateTimeBoxElement.dispatchEvent( + new win.CustomEvent("MozPickerValueChanged", { + detail: Cu.cloneInto(aMessage.data, win), + }) + ); + break; + } + default: + break; + } } /** - * 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. + * nsIDOMEventListener, for chrome events sent by the input element and other + * DOM events. */ - openPickerImpl(inputElement) { - // Time picker is disabled when preffed off - if (inputElement.type == "time" && !this.getTimePickerPref()) { - return undefined; - } + handleEvent(aEvent) { + switch (aEvent.type) { + case "MozOpenDateTimePicker": { + // Time picker is disabled when preffed off + if ( + !aEvent.originalTarget.ownerGlobal.HTMLInputElement.isInstance( + aEvent.originalTarget + ) || + (aEvent.originalTarget.type == "time" && !this.getTimePickerPref()) + ) { + return; + } - let dateTimeBoxElement = inputElement.dateTimeBoxElement; - if (!dateTimeBoxElement) { - throw new Error("How do we get this event without a UA Widget?"); - } + if (this._inputElement) { + // This happens when we're trying to open a picker when another picker + // is still open. We ignore this request to let the first picker + // close gracefully. + return; + } - // dateTimeBoxElement is within UA Widget Shadow DOM. - // An event dispatch to it can't be accessed by document, because - // the event is not composed. - let win = inputElement.ownerGlobal; - dateTimeBoxElement.dispatchEvent( - new win.CustomEvent("MozSetDateTimePickerState", { detail: true }) - ); + this._inputElement = aEvent.originalTarget; + + let dateTimeBoxElement = this._inputElement.dateTimeBoxElement; + if (!dateTimeBoxElement) { + throw new Error("How do we get this event without a UA Widget?"); + } + + // dateTimeBoxElement is within UA Widget Shadow DOM. + // An event dispatch to it can't be accessed by document, because + // the event is not composed. + let win = this._inputElement.ownerGlobal; + dateTimeBoxElement.dispatchEvent( + new win.CustomEvent("MozSetDateTimePickerState", { detail: true }) + ); - let value = inputElement.getDateTimeInputBoxValue(); - return { - // Pass partial value if it's available, otherwise pass input - // element's value. - value: Object.keys(value).length ? value : inputElement.value, - min: inputElement.getMinimum(), - max: inputElement.getMaximum(), - step: inputElement.getStep(), - stepBase: inputElement.getStepBase(), - }; + this.addListeners(this._inputElement); + + let value = this._inputElement.getDateTimeInputBoxValue(); + this.sendAsyncMessage("FormDateTime:OpenPicker", { + rect: this.getBoundingContentRect(this._inputElement), + dir: this.getComputedDirection(this._inputElement), + type: this._inputElement.type, + detail: { + // Pass partial value if it's available, otherwise pass input + // element's value. + value: Object.keys(value).length ? value : this._inputElement.value, + min: this._inputElement.getMinimum(), + max: this._inputElement.getMaximum(), + step: this._inputElement.getStep(), + stepBase: this._inputElement.getStepBase(), + }, + }); + break; + } + case "MozCloseDateTimePicker": { + this.sendAsyncMessage("FormDateTime:ClosePicker", {}); + this.close(); + break; + } + case "pagehide": { + if ( + this._inputElement && + this._inputElement.ownerDocument == aEvent.target + ) { + this.sendAsyncMessage("FormDateTime:ClosePicker", {}); + this.close(); + } + break; + } + default: + break; + } } } diff --git a/toolkit/actors/DateTimePickerParent.sys.mjs b/toolkit/actors/DateTimePickerParent.sys.mjs @@ -2,25 +2,141 @@ * 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 DEBUG = false; +function debug(aStr) { + if (DEBUG) { + dump("-*- DateTimePickerParent: " + aStr + "\n"); + } +} const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - DateTimePickerPanel: "moz-src:///toolkit/modules/DateTimePickerPanel.sys.mjs", + DateTimePickerPanel: "resource://gre/modules/DateTimePickerPanel.sys.mjs", }); -export class DateTimePickerParent extends InputPickerParentCommon { - constructor() { - super("DateTimePicker"); +/* + * DateTimePickerParent receives message from content side (input box) and + * is reposible for opening, closing and updating the picker. Similarly, + * DateTimePickerParent listens for picker's events and notifies the content + * side (input box) about them. + */ +export class DateTimePickerParent extends JSWindowActorParent { + receiveMessage(aMessage) { + debug("receiveMessage: " + aMessage.name); + switch (aMessage.name) { + case "FormDateTime:OpenPicker": { + this.showPicker(aMessage.data); + break; + } + case "FormDateTime:ClosePicker": { + if (!this._picker) { + return; + } + this.close(); + break; + } + default: + break; + } + } + + handleEvent(aEvent) { + debug("handleEvent: " + aEvent.type); + switch (aEvent.type) { + case "DateTimePickerValueCleared": { + this.sendAsyncMessage("FormDateTime:PickerValueChanged", null); + break; + } + case "DateTimePickerValueChanged": { + this.sendAsyncMessage("FormDateTime:PickerValueChanged", aEvent.detail); + break; + } + case "popuphidden": { + this.sendAsyncMessage("FormDateTime:PickerClosed", {}); + this.close(); + break; + } + default: + break; + } + } + + // Get picker from browser and show it anchored to the input box. + showPicker(aData) { + let rect = aData.rect; + let type = aData.type; + let detail = aData.detail; + + debug("Opening picker with details: " + JSON.stringify(detail)); + let topBC = this.browsingContext.top; + let window = topBC.topChromeWindow; + if (Services.focus.activeWindow != window) { + debug("Not in the active window"); + return; + } + + { + let browser = topBC.embedderElement; + if ( + browser && + browser.ownerGlobal.gBrowser && + browser.ownerGlobal.gBrowser.selectedBrowser != browser + ) { + debug("In background tab"); + return; + } + } + + this._cleanupPicker(); + let doc = window.document; + let panel = doc.getElementById("DateTimePickerPanel"); + if (!panel) { + panel = doc.createXULElement("panel"); + panel.id = "DateTimePickerPanel"; + panel.setAttribute("type", "arrow"); + panel.setAttribute("orient", "vertical"); + panel.setAttribute("ignorekeys", "true"); + panel.setAttribute("noautofocus", "true"); + // This ensures that clicks on the anchored input box are never consumed. + panel.setAttribute("consumeoutsideclicks", "never"); + panel.setAttribute("level", "parent"); + panel.setAttribute("tabspecific", "true"); + let container = + doc.getElementById("mainPopupSet") || + doc.querySelector("popupset") || + doc.documentElement.appendChild(doc.createXULElement("popupset")); + container.appendChild(panel); + } + this._oldFocus = doc.activeElement; + this._picker = new lazy.DateTimePickerPanel(panel); + this._picker.openPicker(type, rect, detail); + this._picker.element.addEventListener("popuphidden", this); + this._picker.element.addEventListener("DateTimePickerValueChanged", this); + this._picker.element.addEventListener("DateTimePickerValueCleared", this); + } + + _cleanupPicker() { + if (!this._picker) { + return; + } + this._picker.closePicker(); + this._picker.element.removeEventListener("popuphidden", this); + this._picker.element.removeEventListener( + "DateTimePickerValueChanged", + this + ); + this._picker.element.removeEventListener( + "DateTimePickerValueCleared", + this + ); + this._picker = null; } - /** - * 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.DateTimePickerPanel(panel); + // Close the picker and do some cleanup. + close() { + this._cleanupPicker(); + // Restore focus to where it was before the picker opened. + this._oldFocus?.focus(); + this._oldFocus = null; } } diff --git a/toolkit/actors/InputPickerChildCommon.sys.mjs b/toolkit/actors/InputPickerChildCommon.sys.mjs @@ -1,188 +0,0 @@ -/* 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/. */ - -const lazy = {}; -ChromeUtils.defineESModuleGetters(lazy, { - LayoutUtils: "resource://gre/modules/LayoutUtils.sys.mjs", -}); - -/** - * InputPickerChildCommon is the communication channel between the input box - * (content) for each input types and its picker (chrome). - */ -export class InputPickerChildCommon extends JSWindowActorChild { - /** @type {HTMLInputElement} */ - #inputElement = null; - #inputType = ""; - #namespace; - /** @type {AbortController} */ - #abortController; - - /** - * On init, just listen for the event to open the picker, once the picker is - * opened, we'll listen for update and close events. - * - * @param {string} namespace Affects the event names, e.g. Foo makes it - * accept FooValueChanged event. - * Align it with ActorManagerParent declaration. - */ - constructor(namespace) { - super(); - this.#namespace = namespace; - } - - /** - * Cleanup function called when picker is closed. - */ - close() { - this.#abortController.abort(); - this.closeImpl(this.#inputElement); - this.#inputElement = null; - this.#inputType = ""; - } - - /** - * @param {HTMLInputElement} _inputElement - */ - closeImpl(_inputElement) { - throw new Error("Not implemented"); - } - - /** - * Called after picker is opened to start listening for input box update - * events. - */ - addListeners(aElement) { - this.#abortController = new AbortController(); - aElement.ownerGlobal.addEventListener("pagehide", this, { - signal: this.#abortController.signal, - }); - } - - /** - * Helper function that returns the CSS direction property of the element. - */ - getComputedDirection(aElement) { - return aElement.ownerGlobal - .getComputedStyle(aElement) - .getPropertyValue("direction"); - } - - /** - * Helper function that returns the rect of the element, which is the position - * relative to the left/top of the content area. - */ - getBoundingContentRect(aElement) { - return lazy.LayoutUtils.getElementBoundingScreenRect(aElement); - } - - /** - * MessageListener - */ - receiveMessage(aMessage) { - if (!this.#inputElement || this.#inputElement.type !== this.#inputType) { - // Either we are already closed by content or the input type is changed - return; - } - switch (aMessage.name) { - case "InputPicker:Closed": { - this.close(); - break; - } - case "InputPicker:ValueChanged": { - this.pickerValueChangedImpl(aMessage, this.#inputElement); - break; - } - } - } - - /** - * Element updater function called when the picker value is changed. - * - * @param {ReceiveMessageArgument} _aMessage - * @param {HTMLInputElement} _inputElement - */ - pickerValueChangedImpl(_aMessage, _inputElement) { - throw new Error("Not implemented"); - } - - /** - * nsIDOMEventListener, for chrome events sent by the input element and other - * DOM events. - */ - handleEvent(aEvent) { - switch (aEvent.type) { - case `MozOpen${this.#namespace}`: { - if ( - !aEvent.originalTarget.ownerGlobal.HTMLInputElement.isInstance( - aEvent.originalTarget - ) - ) { - return; - } - - if (this.#inputElement) { - // This happens when we're trying to open a picker when another picker - // is still open. We ignore this request to let the first picker - // close gracefully. - return; - } - - /** @type {HTMLInputElement} */ - const inputElement = aEvent.originalTarget; - const openPickerDetail = this.openPickerImpl(inputElement); - if (!openPickerDetail) { - // The impl doesn't want to proceed in this case - return; - } - - this.#inputElement = inputElement; - this.#inputType = inputElement.type; - this.addListeners(inputElement); - - this.sendAsyncMessage(`InputPicker:Open`, { - rect: this.getBoundingContentRect(inputElement), - dir: this.getComputedDirection(inputElement), - type: inputElement.type, - detail: openPickerDetail, - }); - break; - } - case `MozClose${this.#namespace}`: { - this.sendAsyncMessage(`InputPicker:Close`, {}); - this.close(); - break; - } - case "pagehide": { - if (this.#inputElement?.ownerDocument == aEvent.target) { - this.sendAsyncMessage(`InputPicker:Close`, {}); - this.close(); - } - break; - } - default: - break; - } - } - - /** - * Picker initialization function called when opening the picker - * - * @param {HTMLInputElement} _inputElement - * @returns An argument object to pass to the picker, or undefined to stop opening one. - */ - openPickerImpl(_inputElement) { - throw new Error("Not implemented"); - } - - /** - * Picker updater function when the input value is updated - * - * @param {HTMLInputElement} _inputElement - * @returns An argument object to pass to the picker - */ - updatePickerImpl(_inputElement) { - throw new Error("Not implemented"); - } -} diff --git a/toolkit/actors/InputPickerParentCommon.sys.mjs b/toolkit/actors/InputPickerParentCommon.sys.mjs @@ -1,170 +0,0 @@ -/* 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/. */ - -const DEBUG = false; -function debug(aStr) { - if (DEBUG) { - dump("-*- InputPickerParent: " + aStr + "\n"); - } -} - -/* - * InputPickerParentCommon receives message from content side (input box) and - * is reposible for opening, closing and updating the picker. Similarly, - * InputPickerParentCommon listens for picker's events and notifies the content - * side (input box) about them. - */ -export class InputPickerParentCommon extends JSWindowActorParent { - #namespace; - #picker; - /** @type {Element | undefined} */ - #oldFocus; - /** @type {AbortController} */ - #abortController; - - /** - * @param {string} namespace Affects the input panel id, mostly to prevent - * accidental mis-pairing of wrong panel and actor. - */ - constructor(namespace) { - super(); - this.#namespace = namespace; - } - - receiveMessage(aMessage) { - debug("receiveMessage: " + aMessage.name); - switch (aMessage.name) { - case `InputPicker:Open`: { - this.showPicker(aMessage.data); - break; - } - case `InputPicker:Close`: { - if (!this.#picker) { - return; - } - this.close(); - break; - } - default: - break; - } - } - - handleEvent(aEvent) { - debug("handleEvent: " + aEvent.type); - switch (aEvent.type) { - case "InputPickerValueCleared": { - this.sendAsyncMessage("InputPicker:ValueChanged", null); - break; - } - case "InputPickerValueChanged": { - this.sendAsyncMessage("InputPicker:ValueChanged", aEvent.detail); - break; - } - case "popuphidden": { - this.sendAsyncMessage(`InputPicker:Closed`, {}); - this.close(); - break; - } - default: - break; - } - } - - /** - * A panel creator function called when showing a picker - * - * @param {XULElement} _panel A panel element - * @returns A panel object that manages the element - */ - createPickerImpl(_panel) { - throw new Error("Not implemented"); - } - - // Get picker from browser and show it anchored to the input box. - showPicker(aData) { - let rect = aData.rect; - let type = aData.type; - let detail = aData.detail; - - debug("Opening picker with details: " + JSON.stringify(detail)); - let topBC = this.browsingContext.top; - let window = topBC.topChromeWindow; - if (Services.focus.activeWindow != window) { - debug("Not in the active window"); - return; - } - - { - let browser = topBC.embedderElement; - if ( - browser && - browser.ownerGlobal.gBrowser && - browser.ownerGlobal.gBrowser.selectedBrowser != browser - ) { - debug("In background tab"); - return; - } - } - - this.#cleanupPicker(); - let doc = window.document; - const id = `${this.#namespace}Panel`; - let panel = doc.getElementById(id); - if (!panel) { - panel = doc.createXULElement("panel"); - panel.id = id; - panel.setAttribute("type", "arrow"); - panel.setAttribute("orient", "vertical"); - panel.setAttribute("ignorekeys", "true"); - panel.setAttribute("noautofocus", "true"); - // This ensures that clicks on the anchored input box are never consumed. - panel.setAttribute("consumeoutsideclicks", "never"); - panel.setAttribute("level", "parent"); - panel.setAttribute("tabspecific", "true"); - let container = - doc.getElementById("mainPopupSet") || - doc.querySelector("popupset") || - doc.documentElement.appendChild(doc.createXULElement("popupset")); - container.appendChild(panel); - } - this.#oldFocus = doc.activeElement; - this.#picker = this.createPickerImpl(panel); - this.#picker.openPicker(type, rect, detail); - this.addPickerListeners(panel); - } - - #cleanupPicker() { - if (!this.#picker) { - return; - } - this.#picker.closePicker(); - this.#abortController.abort(); - this.#picker = null; - } - - // Close the picker and do some cleanup. - close() { - this.#cleanupPicker(); - // Restore focus to where it was before the picker opened. - this.#oldFocus?.focus(); - this.#oldFocus = null; - } - - // Listen to picker's event. - addPickerListeners(panel) { - if (!this.#picker) { - return; - } - this.#abortController = new AbortController(); - const { signal } = this.#abortController; - panel.addEventListener("popuphidden", this, { signal }); - panel.addEventListener("InputPickerValueChanged", this, { - signal, - }); - panel.addEventListener("InputPickerValueCleared", this, { - signal, - }); - } -} diff --git a/toolkit/actors/moz.build b/toolkit/actors/moz.build @@ -48,6 +48,8 @@ FINAL_TARGET_FILES.actors += [ "ContentMetaParent.sys.mjs", "ControllersChild.sys.mjs", "ControllersParent.sys.mjs", + "DateTimePickerChild.sys.mjs", + "DateTimePickerParent.sys.mjs", "ExtFindChild.sys.mjs", "FindBarChild.sys.mjs", "FindBarParent.sys.mjs", @@ -81,12 +83,3 @@ FINAL_TARGET_FILES.actors += [ "WebChannelChild.sys.mjs", "WebChannelParent.sys.mjs", ] - -MOZ_SRC_FILES += [ - "ColorPickerChild.sys.mjs", - "ColorPickerParent.sys.mjs", - "DateTimePickerChild.sys.mjs", - "DateTimePickerParent.sys.mjs", - "InputPickerChildCommon.sys.mjs", - "InputPickerParentCommon.sys.mjs", -] diff --git a/toolkit/content/colorpicker.html b/toolkit/content/colorpicker.html @@ -1,32 +0,0 @@ -<!-- 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,7 +50,6 @@ 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,8 +77,6 @@ toolkit.jar: #endif 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/tests/browser/datetime/browser_datetime_blur.js b/toolkit/content/tests/browser/datetime/browser_datetime_blur.js @@ -116,7 +116,7 @@ add_task(async function test_parent_blur() { ); Assert.equal( helper.panel - .querySelector("#DateTimePickerPanelPopupFrame") + .querySelector("#dateTimePopupFrame") .contentDocument.activeElement.getAttribute("role"), "gridcell", "The picker is opened and a calendar day is focused" diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_keynav.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_keynav.js @@ -66,7 +66,7 @@ add_task(async function test_datepicker_keyboard_nav() { ); Assert.equal( - helper.panel.querySelector("#DateTimePickerPanelPopupFrame").contentDocument + helper.panel.querySelector("#dateTimePopupFrame").contentDocument .activeElement.textContent, "15", "Picker is opened with a focus set to the currently selected date" @@ -286,7 +286,7 @@ add_task(async function test_datepicker_keyboard_arrows() { `data:text/html,<input id=date type=date value=${inputValue}>` ); let pickerDoc = helper.panel.querySelector( - "#DateTimePickerPanelPopupFrame" + "#dateTimePopupFrame" ).contentDocument; Assert.equal(helper.panel.state, "open", "Panel should be opened"); @@ -368,7 +368,7 @@ add_task(async function test_datepicker_keyboard_home_end() { `data:text/html,<input id=date type=date value=${inputValue}>` ); let pickerDoc = helper.panel.querySelector( - "#DateTimePickerPanelPopupFrame" + "#dateTimePopupFrame" ).contentDocument; Assert.equal(helper.panel.state, "open", "Panel should be opened"); @@ -454,7 +454,7 @@ add_task(async function test_datepicker_keyboard_pgup_pgdown() { `data:text/html,<input id=date type=date value=${inputValue}>` ); let pickerDoc = helper.panel.querySelector( - "#DateTimePickerPanelPopupFrame" + "#dateTimePopupFrame" ).contentDocument; Assert.equal(helper.panel.state, "open", "Panel should be opened"); diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_monthyear.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_monthyear.js @@ -18,7 +18,7 @@ add_task(async function test_monthyear_close_date() { `data:text/html, <input type="date" value=${inputValue}>` ); let pickerDoc = helper.panel.querySelector( - "#DateTimePickerPanelPopupFrame" + "#dateTimePopupFrame" ).contentDocument; // Move focus from the selected date to the month-year toggle button: @@ -49,7 +49,7 @@ add_task(async function test_monthyear_close_datetime() { `data:text/html, <input type="datetime-local" value=${inputValue}>` ); let pickerDoc = helper.panel.querySelector( - "#DateTimePickerPanelPopupFrame" + "#dateTimePopupFrame" ).contentDocument; // Move focus from the selected date to the month-year toggle button: @@ -78,7 +78,7 @@ add_task(async function test_monthyear_escape_date() { `data:text/html, <input type="date" value=${inputValue}>` ); let pickerDoc = helper.panel.querySelector( - "#DateTimePickerPanelPopupFrame" + "#dateTimePopupFrame" ).contentDocument; // Move focus from the today's date to the month-year toggle button: @@ -150,7 +150,7 @@ add_task(async function test_monthyear_escape_datetime() { `data:text/html, <input type="datetime-local" value=${inputValue}>` ); let pickerDoc = helper.panel.querySelector( - "#DateTimePickerPanelPopupFrame" + "#dateTimePopupFrame" ).contentDocument; // Move focus from the today's date to the month-year toggle button: diff --git a/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_prev_next_month.js b/toolkit/content/tests/browser/datetime/browser_datetime_datepicker_prev_next_month.js @@ -371,7 +371,7 @@ add_task(async function test_datepicker_reopened_prev_next_month_btn() { await ready; Assert.equal( - helper.panel.querySelector("#DateTimePickerPanelPopupFrame").contentDocument + helper.panel.querySelector("#dateTimePopupFrame").contentDocument .activeElement.textContent, "2", "Picker is opened with a focus set to the currently selected date" diff --git a/toolkit/content/tests/browser/datetime/browser_spinner_keynav.js b/toolkit/content/tests/browser/datetime/browser_spinner_keynav.js @@ -29,7 +29,7 @@ add_task(async function test_spinner_month_keyboard_arrows() { `data:text/html, <input type="date" value="${inputValue}">` ); let pickerDoc = helper.panel.querySelector( - "#DateTimePickerPanelPopupFrame" + "#dateTimePopupFrame" ).contentDocument; info("Testing general keyboard navigation"); @@ -341,7 +341,7 @@ add_task(async function test_spinner_year_keyboard_arrows() { `data:text/html, <input type="date" value="${inputValue}">` ); let pickerDoc = helper.panel.querySelector( - "#DateTimePickerPanelPopupFrame" + "#dateTimePopupFrame" ).contentDocument; info("Testing general keyboard navigation"); diff --git a/toolkit/content/tests/browser/datetime/head.js b/toolkit/content/tests/browser/datetime/head.js @@ -58,7 +58,7 @@ class DateTimeTestHelper { }); } this.panel = await shown; - this.frame = this.panel.querySelector("#DateTimePickerPanelPopupFrame"); + this.frame = this.panel.querySelector("#dateTimePopupFrame"); } promisePickerClosed() { @@ -296,7 +296,7 @@ async function testCalendarBtnAttribute(attr, val, presenceOnly = false) { * * @param {string} key: A keyboard Event.key that will be synthesized * @param {object} document: Reference to the content document - * of the #DateTimePickerPanelPopupFrame + * of the #dateTimePopupFrame * @param {number} tabs: How many times "Tab" key should be pressed * to move a keyboard focus to a needed spinner * (1 for month/default and 2 for year) diff --git a/toolkit/content/widgets/colorpicker-common.mjs b/toolkit/content/widgets/colorpicker-common.mjs @@ -1,371 +0,0 @@ -/* 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 - -const lazy = {}; -ChromeUtils.defineLazyGetter(lazy, "l10n", function () { - return new Localization(["devtools/client/inspector.ftl"], true); -}); - -const ARROW_KEYS = ["ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft"]; -const [ArrowUp, ArrowRight, ArrowDown, ArrowLeft] = ARROW_KEYS; -const SLIDER = { - hue: { - MIN: "0", - MAX: "128", - STEP: "1", - }, - alpha: { - MIN: "0", - MAX: "1", - STEP: "0.01", - }, -}; - -/** - * ColorPickerCommon creates a color picker widget in a container you give it. - */ -export class ColorPickerCommon { - constructor(element) { - this.document = element.ownerDocument; - this.element = element; - - this.element.className = "spectrum-container"; - - this.onElementClick = this.onElementClick.bind(this); - this.element.addEventListener("click", this.onElementClick); - - // Color spectrum dragger. - this.dragger = this.element.querySelector(".spectrum-color"); - this.dragger.title = lazy.l10n.formatValueSync( - "colorpicker-tooltip-spectrum-dragger-title" - ); - - this.dragHelper = this.element.querySelector(".spectrum-dragger"); - draggable(this.dragger, this.dragHelper, this.onDraggerMove.bind(this)); - - // Here we define the components for the "controls" section of the color picker. - this.controls = this.element.querySelector(".spectrum-controls"); - this.colorPreview = this.element.querySelector(".spectrum-color-preview"); - - // Hue slider and alpha slider - this.hueSlider = this.createSlider("hue", this.onHueSliderMove.bind(this)); - this.hueSlider.setAttribute("aria-describedby", this.dragHelper.id); - this.alphaSlider = this.createSlider( - "alpha", - this.onAlphaSliderMove.bind(this) - ); - } - - /** @param {[number, number, number, number]} color */ - set rgb([r, g, b, a]) { - this.rgbFloat = [r / 255, g / 255, b / 255, a]; - } - - /** @param {[number, number, number, number]} color */ - set rgbFloat([r, g, b, a]) { - this.hsv = [...InspectorUtils.rgbToHsv(r, g, b), a]; - } - - #toRgbInt(rgbFloat) { - return rgbFloat.map(c => Math.round(c * 255)); - } - - get rgbFloat() { - const [h, s, v, a] = this.hsv; - return [...InspectorUtils.hsvToRgb(h, s, v), a]; - } - - get rgb() { - const [r, g, b, a] = this.rgbFloat; - return [...this.#toRgbInt([r, g, b]), a]; - } - - /** - * Map current rgb to the closest color available in the database by - * calculating the delta-E between each available color and the current rgb - * - * @return {string} - * Color name or closest color name - */ - get colorName() { - const [r, g, b] = this.rgbFloat; - const { exact, colorName } = InspectorUtils.rgbToNearestColorName(r, g, b); - return exact - ? colorName - : lazy.l10n.formatValueSync("colorpicker-tooltip-color-name-title", { - colorName, - }); - } - - get rgbNoSatVal() { - return [ - ...this.#toRgbInt(InspectorUtils.hsvToRgb(this.hsv[0], 1, 1)), - this.hsv[3], - ]; - } - - get rgbCssString() { - const rgb = this.rgb; - return ( - "rgba(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ", " + rgb[3] + ")" - ); - } - - show() { - this.dragWidth = this.dragger.offsetWidth; - this.dragHeight = this.dragger.offsetHeight; - this.dragHelperHeight = this.dragHelper.offsetHeight; - this.dragger.focus({ focusVisible: false }); - - this.updateUI(); - } - - onElementClick(e) { - e.stopPropagation(); - } - - onHueSliderMove() { - this.hsv[0] = this.hueSlider.value / this.hueSlider.max; - this.updateUI(); - this.onChange(); - } - - onDraggerMove(dragX, dragY) { - this.hsv[1] = dragX / this.dragWidth; - this.hsv[2] = (this.dragHeight - dragY) / this.dragHeight; - this.updateUI(); - this.onChange(); - } - - onAlphaSliderMove() { - this.hsv[3] = this.alphaSlider.value / this.alphaSlider.max; - this.updateUI(); - this.onChange(); - } - - onChange() { - throw new Error("Not implemented"); - } - - /** - * Creates and initializes a slider element, attaches it to its parent container - * based on the slider type and returns it - * - * @param {"alpha" | "hue"} sliderType - * The type of the slider (i.e. alpha or hue) - * @param {Function} onSliderMove - * The function to tie the slider to on input - * @return {HTMLInputElement} - * Newly created slider - */ - createSlider(sliderType, onSliderMove) { - const container = this.element.querySelector(`.spectrum-${sliderType}`); - - const slider = this.document.createElement("input"); - slider.className = `spectrum-${sliderType}-input`; - slider.type = "range"; - slider.min = SLIDER[sliderType].MIN; - slider.max = SLIDER[sliderType].MAX; - slider.step = SLIDER[sliderType].STEP; - slider.title = lazy.l10n.formatValueSync( - `colorpicker-tooltip-${sliderType}-slider-title` - ); - slider.addEventListener("input", onSliderMove); - - container.appendChild(slider); - return slider; - } - - updateAlphaSlider() { - // Set alpha slider background - const rgb = this.rgb; - - const rgbNoAlpha = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")"; - const rgbAlpha0 = "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ", 0)"; - const alphaGradient = - "linear-gradient(to right, " + rgbAlpha0 + ", " + rgbNoAlpha + ")"; - this.alphaSlider.style.background = alphaGradient; - } - - updateColorPreview() { - // Overlay the rgba color over a checkered image background. - this.colorPreview.style.setProperty("--overlay-color", this.rgbCssString); - - // We should be able to distinguish the color preview on high luminance rgba values. - // Give the color preview a light grey border if the luminance of the current rgba - // tuple is great. - const colorLuminance = InspectorUtils.relativeLuminance(...this.rgbFloat); - this.colorPreview.classList.toggle("high-luminance", colorLuminance > 0.85); - - // Set title on color preview for better UX - this.colorPreview.title = this.colorName; - } - - updateDragger() { - // Set dragger background color - const flatColor = - "rgb(" + - this.rgbNoSatVal[0] + - ", " + - this.rgbNoSatVal[1] + - ", " + - this.rgbNoSatVal[2] + - ")"; - this.dragger.style.backgroundColor = flatColor; - - // Set dragger aria attributes - this.dragger.setAttribute("aria-valuetext", this.rgbCssString); - } - - updateHueSlider() { - // Set hue slider aria attributes - this.hueSlider.setAttribute("aria-valuetext", this.rgbCssString); - } - - updateHelperLocations() { - const h = this.hsv[0]; - const s = this.hsv[1]; - const v = this.hsv[2]; - - // Placing the color dragger - let dragX = s * this.dragWidth; - let dragY = this.dragHeight - v * this.dragHeight; - const helperDim = this.dragHelperHeight / 2; - - dragX = Math.max( - -helperDim, - Math.min(this.dragWidth - helperDim, dragX - helperDim) - ); - dragY = Math.max( - -helperDim, - Math.min(this.dragHeight - helperDim, dragY - helperDim) - ); - - this.dragHelper.style.top = dragY + "px"; - this.dragHelper.style.left = dragX + "px"; - - // Placing the hue slider - this.hueSlider.value = h * this.hueSlider.max; - - // Placing the alpha slider - this.alphaSlider.value = this.hsv[3] * this.alphaSlider.max; - } - - updateUI() { - this.updateHelperLocations(); - - this.updateColorPreview(); - this.updateDragger(); - this.updateHueSlider(); - this.updateAlphaSlider(); - } - - destroy() { - this.element.removeEventListener("click", this.onElementClick); - this.hueSlider.removeEventListener("input", this.onHueSliderMove); - this.alphaSlider.removeEventListener("input", this.onAlphaSliderMove); - - this.element.remove(); - - this.dragger = this.dragHelper = null; - this.alphaSlider = null; - this.hueSlider = null; - this.colorPreview = null; - this.element = null; - } -} - -function draggable(element, dragHelper, onmove) { - const doc = element.ownerDocument; - let dragging = false; - let offset = {}; - let maxHeight = 0; - let maxWidth = 0; - - function setDraggerDimensionsAndOffset() { - maxHeight = element.offsetHeight; - maxWidth = element.offsetWidth; - offset = element.getBoundingClientRect(); - } - - function prevent(e) { - e.stopPropagation(); - e.preventDefault(); - } - - function move(e) { - if (dragging) { - if (e.buttons === 0) { - // The button is no longer pressed but we did not get a pointerup event. - stop(); - return; - } - const pageX = e.pageX; - const pageY = e.pageY; - - const dragX = Math.max(0, Math.min(pageX - offset.left, maxWidth)); - const dragY = Math.max(0, Math.min(pageY - offset.top, maxHeight)); - - onmove.apply(element, [dragX, dragY]); - } - } - - function start(e) { - const rightClick = e.which === 3; - - if (!rightClick && !dragging) { - dragging = true; - setDraggerDimensionsAndOffset(); - - move(e); - - doc.addEventListener("selectstart", prevent); - doc.addEventListener("dragstart", prevent); - doc.addEventListener("mousemove", move); - doc.addEventListener("mouseup", stop); - - prevent(e); - } - } - - function stop() { - if (dragging) { - doc.removeEventListener("selectstart", prevent); - doc.removeEventListener("dragstart", prevent); - doc.removeEventListener("mousemove", move); - doc.removeEventListener("mouseup", stop); - } - dragging = false; - } - - function onKeydown(e) { - const { key } = e; - - if (!ARROW_KEYS.includes(key)) { - return; - } - - setDraggerDimensionsAndOffset(); - const { offsetHeight, offsetTop, offsetLeft } = dragHelper; - let dragX = offsetLeft + offsetHeight / 2; - let dragY = offsetTop + offsetHeight / 2; - - if (key === ArrowLeft && dragX > 0) { - dragX -= 1; - } else if (key === ArrowRight && dragX < maxWidth) { - dragX += 1; - } else if (key === ArrowUp && dragY > 0) { - dragY -= 1; - } else if (key === ArrowDown && dragY < maxHeight) { - dragY += 1; - } - - onmove.apply(element, [dragX, dragY]); - } - - element.addEventListener("mousedown", start); - element.addEventListener("keydown", onKeydown); -} diff --git a/toolkit/content/widgets/colorpicker.mjs b/toolkit/content/widgets/colorpicker.mjs @@ -1,47 +0,0 @@ -/* 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 @@ -694,11 +694,11 @@ if (AppConstants.platform != "android") { // Note that GeckoView handles MozOpenDateTimePicker in GeckoViewPrompt. JSWINDOWACTORS.DateTimePicker = { parent: { - esModuleURI: "moz-src:///toolkit/actors/DateTimePickerParent.sys.mjs", + esModuleURI: "resource://gre/actors/DateTimePickerParent.sys.mjs", }, child: { - esModuleURI: "moz-src:///toolkit/actors/DateTimePickerChild.sys.mjs", + esModuleURI: "resource://gre/actors/DateTimePickerChild.sys.mjs", events: { MozOpenDateTimePicker: {}, MozCloseDateTimePicker: {}, @@ -768,23 +768,6 @@ 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 @@ -1,67 +0,0 @@ -/* 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/DateTimePickerPanel.sys.mjs b/toolkit/modules/DateTimePickerPanel.sys.mjs @@ -2,69 +2,86 @@ * 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" */ +export class DateTimePickerPanel { + constructor(element) { + this.element = element; -const TIME_PICKER_WIDTH = "13em"; -const TIME_PICKER_HEIGHT = "22em"; -const DATE_PICKER_WIDTH = "24em"; -const DATE_PICKER_HEIGHT = "27em"; -const DATETIME_PICKER_WIDTH = "40em"; -const DATETIME_PICKER_HEIGHT = "27em"; + this.TIME_PICKER_WIDTH = "13em"; + this.TIME_PICKER_HEIGHT = "22em"; + this.DATE_PICKER_WIDTH = "24em"; + this.DATE_PICKER_HEIGHT = "27em"; + this.DATETIME_PICKER_WIDTH = "40em"; + this.DATETIME_PICKER_HEIGHT = "27em"; + } -export class DateTimePickerPanel extends InputPickerPanelCommon { - constructor(element) { - super(element, "chrome://global/content/datetimepicker.xhtml"); + get dateTimePopupFrame() { + let frame = this.element.querySelector("#dateTimePopupFrame"); + if (!frame) { + frame = this.element.ownerDocument.createXULElement("iframe"); + frame.id = "dateTimePopupFrame"; + this.element.appendChild(frame); + } + return frame; } - /** - * Picker window initialization function called when opening the picker - * - * @param {string} type The input element type - * @returns {OpenPickerInfo} - */ - openPickerImpl(type) { + openPicker(type, rect, detail) { if ( type == "datetime-local" && !Services.prefs.getBoolPref("dom.forms.datetime.timepicker") ) { type = "date"; } + this.pickerState = {}; + // TODO: Resize picker according to content zoom level + this.element.style.fontSize = "10px"; + this.type = type; + this.detail = detail; + this.dateTimePopupFrame.addEventListener("load", this, true); + this.dateTimePopupFrame.setAttribute( + "src", + "chrome://global/content/datetimepicker.xhtml" + ); switch (type) { case "time": { - return { - type, - width: TIME_PICKER_WIDTH, - height: TIME_PICKER_HEIGHT, - }; + this.dateTimePopupFrame.style.width = this.TIME_PICKER_WIDTH; + this.dateTimePopupFrame.style.height = this.TIME_PICKER_HEIGHT; + break; } case "date": { - return { - type, - width: DATE_PICKER_WIDTH, - height: DATE_PICKER_HEIGHT, - }; + this.dateTimePopupFrame.style.width = this.DATE_PICKER_WIDTH; + this.dateTimePopupFrame.style.height = this.DATE_PICKER_HEIGHT; + break; } case "datetime-local": { - return { - type, - width: DATETIME_PICKER_WIDTH, - height: DATETIME_PICKER_HEIGHT, - }; + this.dateTimePopupFrame.style.width = this.DATETIME_PICKER_WIDTH; + this.dateTimePopupFrame.style.height = this.DATETIME_PICKER_HEIGHT; + break; } } - throw new Error(`Unexpected type ${type}`); + this.element.openPopupAtScreenRect( + "after_start", + rect.left, + rect.top, + rect.width, + rect.height, + false, + false + ); + } + + closePicker(clear) { + if (clear) { + this.element.dispatchEvent(new CustomEvent("DateTimePickerValueCleared")); + } + this.pickerState = {}; + this.type = undefined; + this.dateTimePopupFrame.removeEventListener("load", this, true); + this.dateTimePopupFrame.contentWindow.removeEventListener("message", this); + this.dateTimePopupFrame.setAttribute("src", ""); + this.element.hidePopup(); } - /** - * 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) { + initPicker(detail) { let locale = new Services.intl.Locale( Services.locale.webExposedLocales[0], { @@ -82,7 +99,7 @@ export class DateTimePickerPanel extends InputPickerPanelCommon { const { year, month, day, hour, minute } = detail.value; const flattenDetail = { - type, + type: this.type, year, // Month value from input box starts from 1 instead of 0 month: month == undefined ? undefined : month - 1, @@ -98,7 +115,7 @@ export class DateTimePickerPanel extends InputPickerPanelCommon { stepBase: detail.stepBase, }; - if (type !== "time") { + if (this.type !== "time") { const { firstDayOfWeek, weekends } = this.getCalendarInfo(locale); const monthDisplayNames = new Services.intl.DisplayNames(locale, { @@ -126,33 +143,59 @@ export class DateTimePickerPanel extends InputPickerPanelCommon { weekdayStrings, }); } - return flattenDetail; + this.postMessageToPicker({ + name: "PickerInit", + detail: flattenDetail, + }); } - /** - * Input element state updater function called when the picker value is changed - * - * @param {string} type - * @param {object} pickerState - */ - sendPickerValueChangedImpl(type, pickerState) { - let { year, month, day, hour, minute } = pickerState; - if (month !== undefined) { - // Month value from input box starts from 1 instead of 0 - month += 1; - } - switch (type) { + setInputBoxValue() { + const value = { + year: this.pickerState.year, + month: this.pickerState.month, + day: this.pickerState.day, + hour: this.pickerState.hour, + minute: this.pickerState.minute, + }; + this.sendPickerValueChanged(value); + } + + sendPickerValueChanged(value) { + let detail = {}; + switch (this.type) { case "time": { - return { hour, minute }; + detail = { + hour: value.hour, + minute: value.minute, + }; + break; } case "date": { - return { year, month, day }; + detail = { + year: value.year, + // Month value from input box starts from 1 instead of 0 + month: value.month == undefined ? undefined : value.month + 1, + day: value.day, + }; + break; } case "datetime-local": { - return { year, month, day, hour, minute }; + detail = { + year: value.year, + // Month value from input box starts from 1 instead of 0 + month: value.month == undefined ? undefined : value.month + 1, + day: value.day, + hour: value.hour, + minute: value.minute, + }; + break; } } - throw new Error(`Unexpected type ${type}`); + this.element.dispatchEvent( + new CustomEvent("DateTimePickerValueChanged", { + detail, + }) + ); } getCalendarInfo(locale) { @@ -175,4 +218,46 @@ export class DateTimePickerPanel extends InputPickerPanelCommon { weekends, }; } + + handleEvent(aEvent) { + switch (aEvent.type) { + case "load": { + this.initPicker(this.detail); + this.dateTimePopupFrame.contentWindow.addEventListener("message", this); + break; + } + case "message": { + this.handleMessage(aEvent); + break; + } + } + } + + handleMessage(aEvent) { + if ( + !this.dateTimePopupFrame.contentDocument.nodePrincipal.isSystemPrincipal + ) { + return; + } + + switch (aEvent.data.name) { + case "PickerPopupChanged": { + this.pickerState = aEvent.data.detail; + this.setInputBoxValue(); + break; + } + case "ClosePopup": { + this.closePicker(aEvent.data.detail); + break; + } + } + } + + postMessageToPicker(data) { + if ( + this.dateTimePopupFrame.contentDocument.nodePrincipal.isSystemPrincipal + ) { + this.dateTimePopupFrame.contentWindow.postMessage(data, "*"); + } + } } diff --git a/toolkit/modules/InputPickerPanelCommon.sys.mjs b/toolkit/modules/InputPickerPanelCommon.sys.mjs @@ -1,163 +0,0 @@ -/* 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/. */ - -export class InputPickerPanelCommon { - #element; - #filename; - #pickerState; - #type; - #detail; - /** @type {AbortController} */ - #abortController; - - /** - * @param {XULElement} element - * @param {string} filename - */ - constructor(element, filename) { - this.#element = element; - this.#filename = filename; - } - - get #popupFrame() { - const id = `${this.#element.id}PopupFrame`; - let frame = this.#element.ownerDocument.getElementById(id); - if (!frame) { - frame = this.#element.ownerDocument.createXULElement("iframe"); - frame.id = id; - this.#element.appendChild(frame); - } - return frame; - } - - openPicker(type, rect, detail) { - const impl = this.openPickerImpl(type); - this.#pickerState = {}; - // TODO: Resize picker according to content zoom level - this.#element.style.fontSize = "10px"; - this.#type = impl.type; - this.#detail = detail; - this.#abortController = new AbortController(); - this.#popupFrame.addEventListener("load", this, { - capture: true, - signal: this.#abortController.signal, - }); - this.#popupFrame.setAttribute("src", this.#filename); - this.#popupFrame.style.width = impl.width; - this.#popupFrame.style.height = impl.height; - this.#element.openPopupAtScreenRect( - "after_start", - rect.left, - rect.top, - rect.width, - rect.height, - false, - false - ); - } - - /** - * @typedef {object} OpenPickerInfo - * @property {string} type The picker type - * @property {string} width The picker width in CSS value - * @property {string} height The picker height in CSS value - * - * Picker window initialization function called when opening the picker - * - * @param {string} _type The input element type - * @returns {OpenPickerInfo} - */ - openPickerImpl(_type) { - throw new Error("Not implemented"); - } - - closePicker(clear) { - if (clear) { - this.#element.dispatchEvent(new CustomEvent(`InputPickerValueCleared`)); - } - this.#pickerState = {}; - this.#type = undefined; - this.#abortController.abort(); - this.#popupFrame.setAttribute("src", ""); - this.#element.hidePopup(); - } - - initPicker(detail) { - const implDetail = this.initPickerImpl(this.#type, detail); - this.postMessageToPicker({ - name: "PickerInit", - detail: implDetail, - }); - } - - /** - * 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) { - throw new Error("Not implemented"); - } - - sendPickerValueChanged() { - let detail = this.sendPickerValueChangedImpl(this.#type, this.#pickerState); - this.#element.dispatchEvent( - new CustomEvent(`InputPickerValueChanged`, { - detail, - }) - ); - } - - /** - * Input element state updater function called when the picker value is changed - * - * @param {string} _type - * @param {object} _pickerState - */ - sendPickerValueChangedImpl(_type, _pickerState) { - throw new Error("Not implemented"); - } - - handleEvent(aEvent) { - switch (aEvent.type) { - case "load": { - this.initPicker(this.#detail); - this.#popupFrame.contentWindow.addEventListener("message", this, { - signal: this.#abortController.signal, - }); - break; - } - case "message": { - this.handleMessage(aEvent); - break; - } - } - } - - handleMessage(aEvent) { - if (!this.#popupFrame.contentDocument.nodePrincipal.isSystemPrincipal) { - return; - } - - switch (aEvent.data.name) { - case "PickerPopupChanged": { - this.#pickerState = aEvent.data.detail; - this.sendPickerValueChanged(); - break; - } - case "ClosePopup": { - this.closePicker(aEvent.data.detail); - break; - } - } - } - - postMessageToPicker(data) { - if (this.#popupFrame.contentDocument.nodePrincipal.isSystemPrincipal) { - this.#popupFrame.contentWindow.postMessage(data, "*"); - } - } -} diff --git a/toolkit/modules/moz.build b/toolkit/modules/moz.build @@ -162,6 +162,7 @@ EXTRA_JS_MODULES += [ "Console.sys.mjs", "ContentDOMReference.sys.mjs", "CreditCard.sys.mjs", + "DateTimePickerPanel.sys.mjs", "DeferredTask.sys.mjs", "E10SUtils.sys.mjs", "EventEmitter.sys.mjs", @@ -214,12 +215,6 @@ EXTRA_JS_MODULES += [ "WebChannel.sys.mjs", ] -MOZ_SRC_FILES += [ - "ColorPickerPanel.sys.mjs", - "DateTimePickerPanel.sys.mjs", - "InputPickerPanelCommon.sys.mjs", -] - if CONFIG["MOZ_ASAN_REPORTER"]: EXTRA_JS_MODULES += [ "AsanReporter.sys.mjs", diff --git a/toolkit/themes/shared/colorpicker-common.css b/toolkit/themes/shared/colorpicker-common.css @@ -1,183 +0,0 @@ -/* 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/. */ - -/* Mix-in classes */ - -.spectrum-checker { - background-color: #eee; - background-image: - linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc), - linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc); - background-size: 12px 12px; - background-position: - 0 0, - 6px 6px; - /* Make sure that the background color is properly set in High Contrast Mode */ - forced-color-adjust: none; -} - -.spectrum-box { - background-clip: content-box; - - :root[forced-colors-active] & { - border-color: initial; - } -} - -/* Elements */ - -/** - * Spectrum controls set the layout for the controls section of the color picker. - */ -.spectrum-controls { - display: flex; - justify-content: space-between; - align-items: center; - margin-block-start: 10px; -} - -/** - * This styles the color preview and adds a checkered background overlay inside of it. The overlay - * can be manipulated using the --overlay-color variable. - */ -.spectrum-color-preview { - --overlay-color: transparent; - border: 1px solid transparent; - border-radius: 50%; - box-sizing: border-box; - width: 25px; - height: 25px; - background-color: #fff; - background-image: - linear-gradient(var(--overlay-color), var(--overlay-color)), linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%), - linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%); - background-size: 12px 12px; - background-position: - 0 0, - 6px 6px; - /* Make sure that the background color is properly set in High Contrast Mode */ - forced-color-adjust: none; - - :root[forced-colors-active] & { - border-color: CanvasText; - } -} - -.spectrum-color-preview.high-luminance { - border-color: #ccc; -} - -.spectrum-slider-container { - display: flex; - flex-direction: column; - justify-content: space-around; - flex: 1; - margin-inline-start: 10px; - height: 30px; -} - -/* Keep aspect ratio: -http://www.briangrinstead.com/blog/keep-aspect-ratio-with-html-and-css */ -.spectrum-color-picker { - position: relative; - border: 1px solid rgba(0, 0, 0, 0.2); - border-radius: 2px; - box-sizing: border-box; - width: 205px; - height: 120px; - /* Make sure that the background color is properly set in High Contrast Mode */ - forced-color-adjust: none; -} - -.spectrum-color { - position: absolute; - top: 0; - left: 0; - bottom: 0; - width: 100%; -} - -.spectrum-sat, -.spectrum-val { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; -} - -.spectrum-alpha { - margin-block-start: 3px; -} - -.spectrum-alpha, -.spectrum-hue { - position: relative; - height: 8px; -} - -.spectrum-alpha-input, -.spectrum-hue-input { - width: 100%; - margin: 0; - position: absolute; - height: 8px; - border-radius: 2px; - direction: initial; -} - -.spectrum-hue-input, -.spectrum-alpha-input { - outline-offset: 4px; -} - -.spectrum-hue-input::-moz-range-thumb, -.spectrum-alpha-input::-moz-range-thumb { - cursor: pointer; - height: 12px; - width: 12px; - box-shadow: 0 0 2px rgba(0, 0, 0, 0.6); - background: #fff; - border-radius: 50%; - opacity: 0.9; - border: none; -} - -:root[forced-colors-active] :is(.spectrum-hue-input, .spectrum-alpha-input)::-moz-range-thumb { - background: ButtonFace; - border: 2px solid ButtonText; -} - -:root[forced-colors-active] :is(.spectrum-hue-input, .spectrum-alpha-input):is(:hover, :focus-visible)::-moz-range-thumb { - border-color: SelectedItem; -} - -.spectrum-hue-input::-moz-range-track { - border-radius: 2px; - height: 8px; - background: linear-gradient(to right, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%); - /* Make sure that the background color is properly set in High Contrast Mode */ - forced-color-adjust: none; -} - -.spectrum-sat { - background-image: linear-gradient(to right, #fff, rgba(204, 154, 129, 0)); -} - -.spectrum-val { - background-image: linear-gradient(to top, #000000, rgba(204, 154, 129, 0)); -} - -.spectrum-dragger { - user-select: none; - position: absolute; - top: 0; - left: 0; - cursor: pointer; - border-radius: 50%; - height: 8px; - width: 8px; - border: 1px solid white; - box-shadow: 0 0 2px rgba(0, 0, 0, 0.6); -} diff --git a/toolkit/themes/shared/colorpicker.css b/toolkit/themes/shared/colorpicker.css @@ -1,9 +0,0 @@ -/* 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 @@ -25,8 +25,6 @@ skin/classic/global/checkbox.css (../../shared/checkbox.css) 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,12 +1623,6 @@ interface InputEventInit extends UIEventInit { targetRanges?: StaticRange[]; } -interface InputPickerColor { - component1: number; - component2: number; - component3: number; -} - interface InspectorCSSPropertyDefinition { fromJS: boolean; inherits: boolean; @@ -11844,7 +11838,6 @@ interface HTMLInputElement extends HTMLElement, MozEditableElement, MozImageLoad checkValidity(): boolean; closeDateTimePicker(): void; getAutocompleteInfo(): AutocompleteInfo | null; - getColor(): InputPickerColor; getDateTimeInputBoxValue(): DateTimeValue; getFilesAndDirectories(): Promise<(File | Directory)[]>; getMaximum(): number; @@ -11866,7 +11859,6 @@ 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;