tor-browser

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

commit 3c797bb745ce2b14ef5fa407599dbf57e726fa43
parent c51d3e49ee9e537256a1f181afcd90fe7d31983e
Author: Kagami Sascha Rosylight <krosylight@proton.me>
Date:   Sun, 21 Dec 2025 12:38:27 +0000

Bug 2000644 - Introduce a superclass of color picker to prepare for multiple subclasses r=devtools-reviewers,nchevobbe,desktop-theme-reviewers,frontend-codestyle-reviewers,hjones

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

Diffstat:
M.stylelintrc.js | 1+
Mdevtools/client/shared/test/browser_spectrum.js | 12++++++------
Mdevtools/client/shared/widgets/Spectrum.js | 355+++----------------------------------------------------------------------------
Mdevtools/client/shared/widgets/spectrum.css | 178+------------------------------------------------------------------------------
Mdevtools/client/shared/widgets/tooltip/SwatchColorPickerTooltip.js | 3---
Mtoolkit/content/jar.mn | 1+
Atoolkit/content/widgets/colorpicker-common.mjs | 371+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atoolkit/themes/shared/colorpicker-common.css | 183+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/themes/shared/desktop-jar.inc.mn | 1+
9 files changed, 577 insertions(+), 528 deletions(-)

diff --git a/.stylelintrc.js b/.stylelintrc.js @@ -421,6 +421,7 @@ 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/media/pipToggle.css", "toolkit/themes/shared/media/videocontrols.css", "toolkit/content/widgets/datetimebox.css", diff --git a/devtools/client/shared/test/browser_spectrum.js b/devtools/client/shared/test/browser_spectrum.js @@ -192,8 +192,8 @@ async function testChangingColorShouldEmitEvents(container, doc) { ); testChangingColorShouldEmitEventsHelper(s, sendDownKey, [125, 62, 62, 1]); testChangingColorShouldEmitEventsHelper(s, sendLeftKey, [125, 63, 63, 1]); - testChangingColorShouldEmitEventsHelper(s, sendUpKey, [128, 64, 64, 1]); - testChangingColorShouldEmitEventsHelper(s, sendRightKey, [127, 63, 63, 1]); + testChangingColorShouldEmitEventsHelper(s, sendUpKey, [127, 64, 64, 1]); + testChangingColorShouldEmitEventsHelper(s, sendRightKey, [128, 63, 63, 1]); info( "Test that moving the hue slider with arrow keys emits color changed event." @@ -211,8 +211,8 @@ async function testChangingColorShouldEmitEvents(container, doc) { "spectrum-hue-input", "Hue slider has successfully received focus." ); - testChangingColorShouldEmitEventsHelper(s, sendRightKey, [127, 66, 63, 1]); - testChangingColorShouldEmitEventsHelper(s, sendLeftKey, [127, 63, 63, 1]); + testChangingColorShouldEmitEventsHelper(s, sendRightKey, [128, 66, 63, 1]); + testChangingColorShouldEmitEventsHelper(s, sendLeftKey, [128, 63, 63, 1]); info( "Test that moving the hue slider with arrow keys emits color changed event." @@ -224,8 +224,8 @@ async function testChangingColorShouldEmitEvents(container, doc) { "spectrum-alpha-input", "Alpha slider has successfully received focus." ); - testChangingColorShouldEmitEventsHelper(s, sendLeftKey, [127, 63, 63, 0.99]); - testChangingColorShouldEmitEventsHelper(s, sendRightKey, [127, 63, 63, 1]); + testChangingColorShouldEmitEventsHelper(s, sendLeftKey, [128, 63, 63, 0.99]); + testChangingColorShouldEmitEventsHelper(s, sendRightKey, [128, 63, 63, 1]); s.destroy(); } diff --git a/devtools/client/shared/widgets/Spectrum.js b/devtools/client/shared/widgets/Spectrum.js @@ -4,6 +4,10 @@ "use strict"; +const { ColorPickerCommon } = ChromeUtils.importESModule( + "chrome://global/content/bindings/colorpicker-common.mjs" +); + const EventEmitter = require("resource://devtools/shared/event-emitter.js"); const { MultiLocalizationHelper, @@ -23,21 +27,7 @@ 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. @@ -62,22 +52,15 @@ const SLIDER = { * Fires the following events: * - changed : When the user changes the current color */ -class Spectrum { +class Spectrum extends ColorPickerCommon { constructor(parentEl, rgb) { - EventEmitter.decorate(this); - - this.document = parentEl.ownerDocument; - this.element = parentEl.ownerDocument.createElementNS(XHTML_NS, "div"); - this.parentEl = parentEl; - - this.element.className = "spectrum-container"; + const element = parentEl.ownerDocument.createElement("div"); // eslint-disable-next-line no-unsanitized/property - this.element.innerHTML = ` + 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"> @@ -111,20 +94,10 @@ class Spectrum { </div> </section> `; + super(element); + EventEmitter.decorate(this); - 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"); + parentEl.appendChild(this.element); // Create the eyedropper. const eyedropper = this.document.createElementNS(XHTML_NS, "button"); @@ -137,14 +110,6 @@ class Spectrum { ); 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" @@ -178,11 +143,6 @@ class Spectrum { : 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; } @@ -195,117 +155,11 @@ class Spectrum { 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) * @@ -374,81 +228,6 @@ class Spectrum { ); } - 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. @@ -545,128 +324,18 @@ class Spectrum { } updateUI() { - this.updateHelperLocations(); - - this.updateColorPreview(); - this.updateDragger(); - this.updateHueSlider(); - this.updateAlphaSlider(); + super.updateUI(); this.updateContrast(); } 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; + super.destroy(); 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,6 +2,8 @@ * 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)); @@ -27,45 +29,10 @@ /* 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; } @@ -77,147 +44,6 @@ 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,9 +189,6 @@ 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/toolkit/content/jar.mn b/toolkit/content/jar.mn @@ -77,6 +77,7 @@ 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/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-common.mjs b/toolkit/content/widgets/colorpicker-common.mjs @@ -0,0 +1,371 @@ +/* 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/themes/shared/colorpicker-common.css b/toolkit/themes/shared/colorpicker-common.css @@ -0,0 +1,183 @@ +/* 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/desktop-jar.inc.mn b/toolkit/themes/shared/desktop-jar.inc.mn @@ -25,6 +25,7 @@ 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/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)