diff --git a/packages/calcite-components/src/components/color-picker/color-picker.e2e.ts b/packages/calcite-components/src/components/color-picker/color-picker.e2e.ts index c97eefcf062..50f4d633615 100644 --- a/packages/calcite-components/src/components/color-picker/color-picker.e2e.ts +++ b/packages/calcite-components/src/components/color-picker/color-picker.e2e.ts @@ -12,15 +12,17 @@ import { getElementRect, } from "../../tests/utils"; import { html } from "../../../support/formatting"; -import { CSS, DEFAULT_COLOR, DEFAULT_STORAGE_KEY_PREFIX, DIMENSIONS, SCOPE_SIZE } from "./resources"; +import { CSS, DEFAULT_COLOR, DEFAULT_STORAGE_KEY_PREFIX, STATIC_DIMENSIONS, SCOPE_SIZE } from "./resources"; import { ColorValue } from "./interfaces"; -import { getSliderWidth } from "./utils"; +import { getColorFieldDimensions, getSliderWidth } from "./utils"; import type { ColorPicker } from "./color-picker"; type SpyInstance = MockInstance; describe("calcite-color-picker", () => { let consoleSpy: SpyInstance; + const defaultMediumWidthInPx = 240; + const centerColorFieldColor = "#408047"; async function clickScope(page: E2EPage, scope: "hue" | "color-field"): Promise { // helps workaround puppeteer not being able to click on a 0x0 element @@ -117,9 +119,8 @@ describe("calcite-color-picker", () => { ]); }); - // #408047 is a color in the middle of the color field describe("disabled", () => { - disabled(""); + disabled(html``); }); describe("translation support", () => { @@ -580,9 +581,10 @@ describe("calcite-color-picker", () => { const picker = await page.find(`calcite-color-picker`); const spy = await picker.spyOnEvent("calciteColorPickerChange"); let changes = 0; - const mediumScaleDimensions = DIMENSIONS.m; + const mediumScaleDimensions = STATIC_DIMENSIONS.m; const widthOffset = 0.5; const [colorFieldX, colorFieldY] = await getElementXY(page, "calcite-color-picker", `.${CSS.colorField}`); + const mediumScaleColorFieldDimensions = await getColorFieldDimensions(defaultMediumWidthInPx); // clicking color field colors to pick a color await page.mouse.click(colorFieldX, colorFieldY); @@ -590,19 +592,19 @@ describe("calcite-color-picker", () => { expect(await picker.getProperty("value")).toBe("#ffffff"); expect(spy).toHaveReceivedEventTimes(++changes); - await page.mouse.click(colorFieldX, colorFieldY + mediumScaleDimensions.colorField.height - 0.1); + await page.mouse.click(colorFieldX, colorFieldY + mediumScaleColorFieldDimensions.height - 0.1); await page.waitForChanges(); expect(await picker.getProperty("value")).toBe("#000000"); expect(spy).toHaveReceivedEventTimes(++changes); - await page.mouse.click(colorFieldX + mediumScaleDimensions.colorField.width - widthOffset, colorFieldY); + await page.mouse.click(colorFieldX + mediumScaleColorFieldDimensions.width - widthOffset, colorFieldY); await page.waitForChanges(); expect(await picker.getProperty("value")).toBe("#ff0000"); expect(spy).toHaveReceivedEventTimes(++changes); await page.mouse.click( - colorFieldX + mediumScaleDimensions.colorField.width - widthOffset, - colorFieldY + mediumScaleDimensions.colorField.height - 0.1, + colorFieldX + mediumScaleColorFieldDimensions.width - widthOffset, + colorFieldY + mediumScaleColorFieldDimensions.height - 0.1, ); await page.waitForChanges(); expect(await picker.getProperty("value")).toBe("#000000"); @@ -615,7 +617,8 @@ describe("calcite-color-picker", () => { // clicking on color slider to set hue const colorsToSample = 7; - const offsetX = (getSliderWidth(mediumScaleDimensions, false) - widthOffset) / colorsToSample; + const offsetX = + (getSliderWidth(defaultMediumWidthInPx, mediumScaleDimensions, false) - widthOffset) / colorsToSample; const [hueSliderX, hueSliderY] = await getElementXY(page, "calcite-color-picker", `.${CSS.hueSlider}`); let x = hueSliderX; @@ -661,7 +664,7 @@ describe("calcite-color-picker", () => { (window as TestWindow).internalColor = color.color; }); - const middleOfSlider = getSliderWidth(mediumScaleDimensions, false) / 2; + const middleOfSlider = getSliderWidth(defaultMediumWidthInPx, mediumScaleDimensions, false) / 2; await page.mouse.click(x + middleOfSlider, sliderHeight); const internalColorChanged = await page.evaluate(() => { @@ -771,7 +774,7 @@ describe("calcite-color-picker", () => { const page = await newE2EPage(); await page.setContent(``); const [hueSliderX] = await getElementXY(page, "calcite-color-picker", `.${CSS.hueSlider}`); - const sliderWidth = getSliderWidth(DIMENSIONS.m, false); + const sliderWidth = getSliderWidth(defaultMediumWidthInPx, STATIC_DIMENSIONS.m, false); let [hueScopeX, hueScopeY] = await getElementXY(page, "calcite-color-picker", `.${CSS.hueScope}`); let [hueScopeCenterX, hueScopeCenterY] = getScopeCenter(hueScopeX, hueScopeY); @@ -785,7 +788,7 @@ describe("calcite-color-picker", () => { [hueScopeX, hueScopeY] = await getElementXY(page, "calcite-color-picker", `.${CSS.hueScope}`); [hueScopeCenterX, hueScopeCenterY] = getScopeCenter(hueScopeX, hueScopeY); - expect(hueScopeCenterX).toBe(hueSliderX + DIMENSIONS.m.thumb.radius); + expect(hueScopeCenterX).toBe(hueSliderX + STATIC_DIMENSIONS.m.thumb.radius); await page.mouse.move(hueScopeCenterX, hueScopeCenterY); await page.mouse.down(); @@ -796,7 +799,7 @@ describe("calcite-color-picker", () => { [hueScopeX] = await getElementXY(page, "calcite-color-picker", `.${CSS.hueScope}`); [hueScopeCenterX] = getScopeCenter(hueScopeX, hueScopeY); - expect(hueScopeCenterX).toBe(hueSliderX + sliderWidth - DIMENSIONS.m.thumb.radius); + expect(hueScopeCenterX).toBe(hueSliderX + sliderWidth - STATIC_DIMENSIONS.m.thumb.radius); }); it("does not allow text selection when color field/sliders are used", async () => { @@ -2310,7 +2313,8 @@ describe("calcite-color-picker", () => { await page.waitForChanges(); const finalStyle = await scope.getComputedStyle(); - expect(finalStyle.left).toBe(`${DIMENSIONS.m.colorField.width - SCOPE_SIZE / 2}px`); + const mediumScaleColorFieldDimensions = await getColorFieldDimensions(defaultMediumWidthInPx); + expect(finalStyle.left).toBe(`${mediumScaleColorFieldDimensions.width - SCOPE_SIZE / 2}px`); }); it("allows nudging color's hue even if it does not change RGB value", async () => { @@ -2330,7 +2334,7 @@ describe("calcite-color-picker", () => { const getScopeLeftOffset = async () => parseFloat((await scope.getComputedStyle()).left); - expect(await getScopeLeftOffset()).toBeCloseTo(DIMENSIONS.m.thumb.radius - 0.5, 0); + expect(await getScopeLeftOffset()).toBeCloseTo(STATIC_DIMENSIONS.m.thumb.radius - 0.5, 0); await nudgeAThirdOfSlider(); expect(await getScopeLeftOffset()).toBeCloseTo(58.9, 0); @@ -2345,7 +2349,7 @@ describe("calcite-color-picker", () => { // nudge it to wrap around await scope.press("ArrowRight"); - expect(await getScopeLeftOffset()).toBeCloseTo(DIMENSIONS.m.thumb.radius - 0.5, 0); + expect(await getScopeLeftOffset()).toBeCloseTo(STATIC_DIMENSIONS.m.thumb.radius - 0.5, 0); }); it("allows editing hue slider via keyboard", async () => { @@ -2384,15 +2388,16 @@ describe("calcite-color-picker", () => { expect(await hueSliderScope.getComputedStyle()).toMatchObject({ top: "6.5px", - left: `${DIMENSIONS.m.thumb.radius - 0.5}px`, + left: `${STATIC_DIMENSIONS.m.thumb.radius - 0.5}px`, }); }); describe("mouse", () => { - const scopeSizeOffset = 0.8; + const moveByInPx = 2; + it("should update value when color field scope is moved", async () => { const page = await newE2EPage(); - await page.setContent(``); + await page.setContent(html``); const colorPicker = await page.find("calcite-color-picker"); const [colorFieldScopeX, colorFieldScopeY] = await getElementXY( @@ -2402,31 +2407,29 @@ describe("calcite-color-picker", () => { ); const value = await colorPicker.getProperty("value"); - await page.mouse.move(colorFieldScopeX, colorFieldScopeY + scopeSizeOffset); - await page.mouse.down(); - await page.mouse.up(); + await page.mouse.click(colorFieldScopeX - moveByInPx, colorFieldScopeY); await page.waitForChanges(); expect(await colorPicker.getProperty("value")).not.toBe(value); }); it("should update value when hue scope is moved", async () => { const page = await newE2EPage(); - await page.setContent(``); + await page.setContent(html``); const colorPicker = await page.find("calcite-color-picker"); const [hueScopeX, hueScopeY] = await getElementXY(page, "calcite-color-picker", `.${CSS.hueScope}`); const value = await colorPicker.getProperty("value"); - await page.mouse.move(hueScopeX + scopeSizeOffset, hueScopeY); - await page.mouse.down(); - await page.mouse.up(); + await page.mouse.click(hueScopeX - moveByInPx, hueScopeY); await page.waitForChanges(); expect(await colorPicker.getProperty("value")).not.toBe(value); }); it("should update value when opacity scope is moved", async () => { const page = await newE2EPage(); - await page.setContent(``); + await page.setContent( + html``, + ); const [opacityScopeX, opacityScopeY] = await getElementXY( page, "calcite-color-picker", @@ -2435,9 +2438,7 @@ describe("calcite-color-picker", () => { const colorPicker = await page.find("calcite-color-picker"); const value = await colorPicker.getProperty("value"); - await page.mouse.move(opacityScopeX - 2, opacityScopeY); - await page.mouse.down(); - await page.mouse.up(); + await page.mouse.click(opacityScopeX - moveByInPx, opacityScopeY); await page.waitForChanges(); expect(await colorPicker.getProperty("value")).not.toBe(value); }); diff --git a/packages/calcite-components/src/components/color-picker/color-picker.scss b/packages/calcite-components/src/components/color-picker/color-picker.scss index 6c7e8b0f10d..6607574bf1a 100644 --- a/packages/calcite-components/src/components/color-picker/color-picker.scss +++ b/packages/calcite-components/src/components/color-picker/color-picker.scss @@ -1,16 +1,16 @@ :host { @apply text-n2h inline-block font-normal; + + inline-size: var(--calcite-internal-color-picker-min-width); + min-inline-size: var(--calcite-internal-color-picker-min-width); } @include disabled(); :host([scale="s"]) { + --calcite-internal-color-picker-min-width: 200px; --calcite-color-picker-spacing: 8px; - .container { - inline-size: 198px; - } - .saved-colors { @apply gap-1; grid-template-columns: repeat(auto-fill, 20px); @@ -18,22 +18,16 @@ } :host([scale="m"]) { + --calcite-internal-color-picker-min-width: 240px; --calcite-color-picker-spacing: 12px; - - .container { - inline-size: 238px; - } } :host([scale="l"]) { + --calcite-internal-color-picker-min-width: 304px; --calcite-color-picker-spacing: 16px; @apply text-n1h; - .container { - inline-size: 302px; - } - .section { &:first-of-type { padding-block-start: var(--calcite-color-picker-spacing); @@ -45,14 +39,12 @@ } .control-section { - @apply flex-nowrap items-baseline; - } - - .control-section { - @apply flex-wrap; + @apply flex flex-col flex-wrap items-baseline; } .color-hex-options { + inline-size: 100%; + @apply flex flex-shrink flex-col @@ -66,7 +58,9 @@ .container { @apply bg-foreground-1; - display: inline-block; + display: flex; + flex-direction: column; + block-size: min-content; border: 1px solid var(--calcite-color-border-1); } @@ -96,12 +90,7 @@ } .hex-and-channels-group { - @apply w-full; -} - -.hex-and-channels-group, -.control-section { - @apply flex flex-row flex-wrap; + @apply flex flex-col flex-wrap w-full; } .section { diff --git a/packages/calcite-components/src/components/color-picker/color-picker.stories.ts b/packages/calcite-components/src/components/color-picker/color-picker.stories.ts index e946c7ad290..42586209cd1 100644 --- a/packages/calcite-components/src/components/color-picker/color-picker.stories.ts +++ b/packages/calcite-components/src/components/color-picker/color-picker.stories.ts @@ -1,4 +1,4 @@ -import { boolean, modesDarkDefault } from "../../../.storybook/utils"; +import { boolean, createBreakpointStories, modesDarkDefault } from "../../../.storybook/utils"; import { html } from "../../../support/formatting"; import { ATTRIBUTES } from "../../../.storybook/resources"; import { ColorPicker } from "./color-picker"; @@ -83,3 +83,19 @@ export const Focus_TestOnly = (): string => Focus_TestOnly.parameters = { chromatic: { delay: 2000 }, }; + +export const responsive = (): string => + createBreakpointStories(html` + + + + `); diff --git a/packages/calcite-components/src/components/color-picker/color-picker.tsx b/packages/calcite-components/src/components/color-picker/color-picker.tsx index ce157eb0d39..0f290e8bfc4 100644 --- a/packages/calcite-components/src/components/color-picker/color-picker.tsx +++ b/packages/calcite-components/src/components/color-picker/color-picker.tsx @@ -2,14 +2,14 @@ import Color from "color"; import { throttle } from "lodash-es"; import { PropertyValues } from "lit"; -import { LitElement, property, createEvent, h, method, state, JsxNode } from "@arcgis/lumina"; +import { createEvent, h, JsxNode, LitElement, method, property, state } from "@arcgis/lumina"; import { Direction, focusFirstTabbable, getElementDir, isPrimaryPointerButton, } from "../../utils/dom"; -import { Scale } from "../interfaces"; +import { Dimensions, Scale } from "../interfaces"; import { InteractiveComponent, InteractiveContainer, @@ -28,12 +28,14 @@ import { useT9n } from "../../controllers/useT9n"; import type { InputNumber } from "../input-number/input-number"; import type { ColorPickerSwatch } from "../color-picker-swatch/color-picker-swatch"; import type { ColorPickerHexInput } from "../color-picker-hex-input/color-picker-hex-input"; +import { createObserver } from "../../utils/observers"; import { alphaCompatible, alphaToOpacity, colorEqual, CSSColorMode, Format, + getColorFieldDimensions, getSliderWidth, hexify, normalizeAlpha, @@ -49,12 +51,12 @@ import { CSS, DEFAULT_COLOR, DEFAULT_STORAGE_KEY_PREFIX, - DIMENSIONS, HSV_LIMITS, HUE_LIMIT_CONSTRAINED, OPACITY_LIMITS, RGB_LIMITS, SCOPE_SIZE, + STATIC_DIMENSIONS, } from "./resources"; import { Channels, ColorMode, ColorValue, HSLA, HSVA, InternalColor, RGBA } from "./interfaces"; import T9nStrings from "./assets/t9n/messages.en.json"; @@ -82,25 +84,17 @@ export class ColorPicker extends LitElement implements InteractiveComponent, Loa bounds: DOMRect; }; + private dynamicDimensions: + | { + colorField: Dimensions; + slider: Dimensions; + } + | undefined; + private get baseColorFieldColor(): Color { return this.color || this.previousColor || DEFAULT_COLOR; } - private captureColorFieldColor = (x: number, y: number, skipEqual = true): void => { - const { - dimensions: { - colorField: { height, width }, - }, - } = this; - const saturation = Math.round((HSV_LIMITS.s / width) * x); - const value = Math.round((HSV_LIMITS.v / height) * (height - y)); - - this.internalColorSet( - this.baseColorFieldColor.hsv().saturationv(saturation).value(value), - skipEqual, - ); - }; - private checkerPattern: HTMLCanvasElement; private _color: InternalColor | null = DEFAULT_COLOR; @@ -109,88 +103,9 @@ export class ColorPicker extends LitElement implements InteractiveComponent, Loa private colorFieldScopeNode: HTMLDivElement; - private drawColorControls = throttle( - (type: "all" | "color-field" | "hue-slider" | "opacity-slider" = "all"): void => { - if ((type === "all" || type === "color-field") && this.colorFieldRenderingContext) { - this.drawColorField(); - } - - if ((type === "all" || type === "hue-slider") && this.hueSliderRenderingContext) { - this.drawHueSlider(); - } - - if ( - this.alphaChannel && - (type === "all" || type === "opacity-slider") && - this.opacitySliderRenderingContext - ) { - this.drawOpacitySlider(); - } - }, - throttleFor60FpsInMs, - ); - - private effectiveSliderWidth: number; - - private globalPointerMoveHandler = (event: PointerEvent): void => { - const { activeCanvasInfo, el } = this; - - if (!el.isConnected || !activeCanvasInfo) { - return; - } - - const { context, bounds } = activeCanvasInfo; - - let samplingX: number; - let samplingY: number; - - const { clientX, clientY } = event; - - if (context.canvas.matches(":hover")) { - samplingX = clientX - bounds.x; - samplingY = clientY - bounds.y; - } else { - // snap x and y to the closest edge - - if (clientX < bounds.x + bounds.width && clientX > bounds.x) { - samplingX = clientX - bounds.x; - } else if (clientX < bounds.x) { - samplingX = 0; - } else { - samplingX = bounds.width; - } - - if (clientY < bounds.y + bounds.height && clientY > bounds.y) { - samplingY = clientY - bounds.y; - } else if (clientY < bounds.y) { - samplingY = 0; - } else { - samplingY = bounds.height; - } - } - - if (context === this.colorFieldRenderingContext) { - this.captureColorFieldColor(samplingX, samplingY, false); - } else if (context === this.hueSliderRenderingContext) { - this.captureHueSliderColor(samplingX); - } else if (context === this.opacitySliderRenderingContext) { - this.captureOpacitySliderValue(samplingX); - } - }; - - private globalPointerUpHandler = (event: PointerEvent): void => { - if (!isPrimaryPointerButton(event)) { - return; - } - - const previouslyDragging = this.activeCanvasInfo; - this.activeCanvasInfo = null; - this.drawColorControls(); - - if (previouslyDragging) { - this.calciteColorPickerChange.emit(); - } - }; + private get effectiveSliderWidth(): number { + return this.dynamicDimensions.slider.width; + } private hueScopeNode: HTMLDivElement; @@ -210,6 +125,8 @@ export class ColorPicker extends LitElement implements InteractiveComponent, Loa private previousColor: InternalColor | null; + private resizeObserver = createObserver("resize", (entries) => this.resizeCanvas(entries)); + private shiftKeyChannelAdjustment = 0; private upOrDownArrowKeyTracker: "down" | "up" | null = null; @@ -230,7 +147,7 @@ export class ColorPicker extends LitElement implements InteractiveComponent, Loa @state() colorFieldScopeTop: number; - @state() dimensions = DIMENSIONS.m; + @state() staticDimensions = STATIC_DIMENSIONS.m; @state() hueScopeLeft: number; @@ -377,6 +294,10 @@ export class ColorPicker extends LitElement implements InteractiveComponent, Loa this.listen("keyup", this.handleChannelKeyUpOrDown, { capture: true }); } + connectedCallback(): void { + this.observeResize(); + } + async load(): Promise { if (!this._valueWasSet) { this._value ??= normalizeHex(hexify(DEFAULT_COLOR, this.alphaChannel)); @@ -384,7 +305,6 @@ export class ColorPicker extends LitElement implements InteractiveComponent, Loa setUpLoadableComponent(this); this.handleAllowEmptyOrClearableChange(); - this.handleAlphaChannelDimensionsChange(); const { isClearable, color, format, value } = this; const willSetNoColor = isClearable && !value; @@ -399,7 +319,8 @@ export class ColorPicker extends LitElement implements InteractiveComponent, Loa this.setMode(format, false); this.internalColorSet(initialColor, false, "initial"); - this.updateDimensions(this.scale); + this.updateStaticDimensions(this.scale); + this.updateDynamicDimensions(STATIC_DIMENSIONS[this.scale].minWidth); const storageKey = `${DEFAULT_STORAGE_KEY_PREFIX}${this.storageId}`; @@ -425,8 +346,9 @@ export class ColorPicker extends LitElement implements InteractiveComponent, Loa } if ( - (changes.has("alphaChannel") && (this.hasUpdated || this.alphaChannel !== false)) || - (changes.has("dimensions") && (this.hasUpdated || this.dimensions !== DIMENSIONS.m)) + this.hasUpdated && + ((changes.has("alphaChannel") && this.alphaChannel !== false) || + (changes.has("staticDimensions") && this.staticDimensions !== STATIC_DIMENSIONS.m)) ) { this.handleAlphaChannelDimensionsChange(); } @@ -449,6 +371,7 @@ export class ColorPicker extends LitElement implements InteractiveComponent, Loa loaded(): void { setComponentLoaded(this); + this.handleAlphaChannelDimensionsChange(); } override disconnectedCallback(): void { @@ -460,12 +383,126 @@ export class ColorPicker extends LitElement implements InteractiveComponent, Loa "pointerup", this.globalPointerUpHandler, ) /* TODO: [MIGRATION] If possible, refactor to use on* JSX prop or this.listen()/this.listenOn() utils - they clean up event listeners automatically, thus prevent memory leaks */; + this.resizeObserver?.disconnect(); } // #endregion // #region Private Methods + private observeResize(): void { + this.resizeObserver?.observe(this.el); + } + + private captureColorFieldColor = (x: number, y: number, skipEqual = true): void => { + const { width, height } = this.dynamicDimensions.colorField; + const saturation = Math.round((HSV_LIMITS.s / width) * x); + const value = Math.round((HSV_LIMITS.v / height) * (height - y)); + + this.internalColorSet( + this.baseColorFieldColor.hsv().saturationv(saturation).value(value), + skipEqual, + ); + }; + + private drawColorControls = throttle( + (type: "all" | "color-field" | "hue-slider" | "opacity-slider" = "all"): void => { + if ((type === "all" || type === "color-field") && this.colorFieldRenderingContext) { + this.drawColorField(); + } + + if ((type === "all" || type === "hue-slider") && this.hueSliderRenderingContext) { + this.drawHueSlider(); + } + + if ( + this.alphaChannel && + (type === "all" || type === "opacity-slider") && + this.opacitySliderRenderingContext + ) { + this.drawOpacitySlider(); + } + }, + throttleFor60FpsInMs, + ); + + private globalPointerMoveHandler = (event: PointerEvent): void => { + const { activeCanvasInfo, el } = this; + + if (!el.isConnected || !activeCanvasInfo) { + return; + } + + const { context, bounds } = activeCanvasInfo; + + let samplingX: number; + let samplingY: number; + + const { clientX, clientY } = event; + + if (context.canvas.matches(":hover")) { + samplingX = clientX - bounds.x; + samplingY = clientY - bounds.y; + } else { + // snap x and y to the closest edge + + if (clientX < bounds.x + bounds.width && clientX > bounds.x) { + samplingX = clientX - bounds.x; + } else if (clientX < bounds.x) { + samplingX = 0; + } else { + samplingX = bounds.width; + } + + if (clientY < bounds.y + bounds.height && clientY > bounds.y) { + samplingY = clientY - bounds.y; + } else if (clientY < bounds.y) { + samplingY = 0; + } else { + samplingY = bounds.height; + } + } + + if (context === this.colorFieldRenderingContext) { + this.captureColorFieldColor(samplingX, samplingY, false); + } else if (context === this.hueSliderRenderingContext) { + this.captureHueSliderColor(samplingX); + } else if (context === this.opacitySliderRenderingContext) { + this.captureOpacitySliderValue(samplingX); + } + }; + + private globalPointerUpHandler = (event: PointerEvent): void => { + if (!isPrimaryPointerButton(event)) { + return; + } + + const previouslyDragging = this.activeCanvasInfo; + this.activeCanvasInfo = null; + this.drawColorControls(); + + if (previouslyDragging) { + this.calciteColorPickerChange.emit(); + } + }; + + private resizeCanvas = throttle((entries: ResizeObserverEntry[]): void => { + if (!this.hasUpdated) { + return; + } + + const [first] = entries; + const availableWidth = Math.floor(first.contentBoxSize[0].inlineSize); + + if (this.dynamicDimensions.colorField.width === availableWidth) { + return; + } + + this.updateDynamicDimensions(availableWidth); + this.updateCanvasSize(); + this.drawColorControls(); + }, throttleFor60FpsInMs); + private handleAllowEmptyOrClearableChange(): void { this.isClearable = this.clearable || this.allowEmpty; } @@ -482,7 +519,6 @@ export class ColorPicker extends LitElement implements InteractiveComponent, Loa } private handleAlphaChannelDimensionsChange(): void { - this.effectiveSliderWidth = getSliderWidth(this.dimensions, this.alphaChannel); this.drawColorControls(); } @@ -498,8 +534,8 @@ export class ColorPicker extends LitElement implements InteractiveComponent, Loa } private handleScaleChange(scale: Scale = "m"): void { - this.updateDimensions(scale); - this.updateCanvasSize("all"); + this.updateStaticDimensions(scale); + this.updateCanvasSize(); this.drawColorControls(); } @@ -945,7 +981,7 @@ export class ColorPicker extends LitElement implements InteractiveComponent, Loa private getSliderCapSpacing(): number { const { - dimensions: { + staticDimensions: { slider: { height }, thumb: { radius }, }, @@ -954,8 +990,8 @@ export class ColorPicker extends LitElement implements InteractiveComponent, Loa return radius * 2 - height; } - private updateDimensions(scale: Scale = "m"): void { - this.dimensions = DIMENSIONS[scale]; + private updateStaticDimensions(scale: Scale = "m"): void { + this.staticDimensions = STATIC_DIMENSIONS[scale]; } private deleteColor(): void { @@ -998,11 +1034,7 @@ export class ColorPicker extends LitElement implements InteractiveComponent, Loa private drawColorField(): void { const context = this.colorFieldRenderingContext; - const { - dimensions: { - colorField: { height, width }, - }, - } = this; + const { width, height } = this.dynamicDimensions.colorField; context.fillStyle = this.baseColorFieldColor .hsv() @@ -1074,19 +1106,35 @@ export class ColorPicker extends LitElement implements InteractiveComponent, Loa this.drawOpacitySlider(); } + private updateDynamicDimensions = (width: number): void => { + const sliderDims = { + width: getSliderWidth(width, this.staticDimensions, this.alphaChannel), + height: this.staticDimensions.slider.height, + }; + + this.dynamicDimensions = { + colorField: getColorFieldDimensions(width), + slider: sliderDims, + }; + }; + private updateCanvasSize( context: "all" | "color-field" | "hue-slider" | "opacity-slider" = "all", ): void { - const { dimensions } = this; + const { dynamicDimensions, staticDimensions } = this; if (context === "all" || context === "color-field") { - this.setCanvasContextSize(this.colorFieldRenderingContext?.canvas, dimensions.colorField); + this.setCanvasContextSize( + this.colorFieldRenderingContext?.canvas, + dynamicDimensions.colorField, + ); } const adjustedSliderDimensions = { width: this.effectiveSliderWidth, height: - dimensions.slider.height + (dimensions.thumb.radius - dimensions.slider.height / 2) * 2, + staticDimensions.slider.height + + (staticDimensions.thumb.radius - dynamicDimensions.slider.height / 2) * 2, }; if (context === "all" || context === "hue-slider") { @@ -1111,12 +1159,13 @@ export class ColorPicker extends LitElement implements InteractiveComponent, Loa const hsvColor = color.hsv(); const { - dimensions: { - colorField: { height, width }, + staticDimensions: { thumb: { radius }, }, } = this; + const { width, height } = this.dynamicDimensions.colorField; + const x = hsvColor.saturationv() / (HSV_LIMITS.s / width); const y = height - hsvColor.value() / (HSV_LIMITS.v / height); @@ -1178,7 +1227,7 @@ export class ColorPicker extends LitElement implements InteractiveComponent, Loa const hsvColor = color.hsv().saturationv(100).value(100); const { - dimensions: { + staticDimensions: { thumb: { radius }, }, } = this; @@ -1198,7 +1247,7 @@ export class ColorPicker extends LitElement implements InteractiveComponent, Loa private drawHueSlider(): void { const context = this.hueSliderRenderingContext; const { - dimensions: { + staticDimensions: { slider: { height }, thumb: { radius: thumbRadius }, }, @@ -1246,7 +1295,7 @@ export class ColorPicker extends LitElement implements InteractiveComponent, Loa const context = this.opacitySliderRenderingContext; const { baseColorFieldColor: previousColor, - dimensions: { + staticDimensions: { slider: { height }, thumb: { radius: thumbRadius }, }, @@ -1335,7 +1384,7 @@ export class ColorPicker extends LitElement implements InteractiveComponent, Loa const hsvColor = color; const { - dimensions: { + staticDimensions: { thumb: { radius }, }, } = this; @@ -1424,7 +1473,7 @@ export class ColorPicker extends LitElement implements InteractiveComponent, Loa color, colorFieldScopeLeft, colorFieldScopeTop, - dimensions: { + staticDimensions: { thumb: { radius: thumbRadius }, }, hexDisabled, diff --git a/packages/calcite-components/src/components/color-picker/resources.ts b/packages/calcite-components/src/components/color-picker/resources.ts index 008b9ffd08c..1a7f4e82c96 100644 --- a/packages/calcite-components/src/components/color-picker/resources.ts +++ b/packages/calcite-components/src/components/color-picker/resources.ts @@ -1,4 +1,9 @@ import Color from "color"; +import { + calciteSpacingFixedSm, + calciteSpacingFixedMd, + calciteSpacingFixedXl, +} from "@esri/calcite-design-tokens/dist/es6/global"; export const CSS = { channel: "channel", @@ -56,15 +61,11 @@ export const OPACITY_LIMITS = { max: 100, }; -export const DIMENSIONS = { +export const STATIC_DIMENSIONS = { s: { + gap: parseInt(calciteSpacingFixedSm), slider: { height: 12, - width: 142, - }, - colorField: { - height: 110, - width: 198, }, thumb: { radius: 7, @@ -72,15 +73,12 @@ export const DIMENSIONS = { preview: { size: 20, }, + minWidth: 200, }, m: { + gap: parseInt(calciteSpacingFixedMd), slider: { height: 12, - width: 170, - }, - colorField: { - height: 132, - width: 238, }, thumb: { radius: 7, @@ -88,15 +86,12 @@ export const DIMENSIONS = { preview: { size: 24, }, + minWidth: 240, }, l: { + gap: parseInt(calciteSpacingFixedXl), slider: { height: 12, - width: 222, - }, - colorField: { - height: 168, - width: 302, }, thumb: { radius: 7, @@ -104,6 +99,7 @@ export const DIMENSIONS = { preview: { size: 32, }, + minWidth: 304, }, }; diff --git a/packages/calcite-components/src/components/color-picker/utils.ts b/packages/calcite-components/src/components/color-picker/utils.ts index a3f9e2eec75..07c20e9e63f 100644 --- a/packages/calcite-components/src/components/color-picker/utils.ts +++ b/packages/calcite-components/src/components/color-picker/utils.ts @@ -1,8 +1,8 @@ // @ts-strict-ignore import Color from "color"; -import { Scale } from "../interfaces"; +import { Dimensions, Scale } from "../interfaces"; import { ColorValue, HSLA, HSVA, RGB, RGBA } from "./interfaces"; -import { DIMENSIONS } from "./resources"; +import { STATIC_DIMENSIONS } from "./resources"; export const hexChar = /^[0-9A-F]$/i; const shorthandHex = /^#[0-9A-F]{3}$/i; @@ -261,17 +261,27 @@ export function toNonAlphaMode(mode: SupportedMode): SupportedMode { return nonAlphaMode; } -export function getSliderWidth(activeDimensions: (typeof DIMENSIONS)[Scale], hasAlpha: boolean): number { - const { - slider: { width }, - preview, - } = activeDimensions; +const borderWidthInPx = 1; +const inlineSizeBorderTotalWidth = borderWidthInPx * 2; - if (hasAlpha) { - return width; - } +export function getSliderWidth( + availableWidth: number, + activeStaticDimensions: (typeof STATIC_DIMENSIONS)[Scale], + hasAlpha: boolean, +): number { + const previewWidth = hasAlpha ? STATIC_DIMENSIONS["l"].preview.size : activeStaticDimensions.preview.size; + const effectiveWidth = availableWidth - inlineSizeBorderTotalWidth; + const inlineSpaceAroundElements = activeStaticDimensions.gap * 3; + + return Math.max(effectiveWidth - inlineSpaceAroundElements - previewWidth, 0); +} - const previewWidthOffset = DIMENSIONS["l"].preview.size - preview.size; +export function getColorFieldDimensions(availableWidth: number): Dimensions { + const colorFieldAspectRatio = 1.8; + const effectiveWidth = availableWidth - inlineSizeBorderTotalWidth; - return width + previewWidthOffset; + return { + width: Math.max(effectiveWidth, 0), + height: Math.max(Math.floor(effectiveWidth / colorFieldAspectRatio), 0), + }; } diff --git a/packages/calcite-components/src/components/graph/interfaces.ts b/packages/calcite-components/src/components/graph/interfaces.ts index 3824c582576..9842a18b352 100644 --- a/packages/calcite-components/src/components/graph/interfaces.ts +++ b/packages/calcite-components/src/components/graph/interfaces.ts @@ -1,3 +1,5 @@ +import { Dimensions } from "../interfaces"; + /** x,y coordinate set */ export type Point = [number, number]; @@ -7,12 +9,6 @@ export type DataSeries = Point[]; /** Function that converts point from data space to pixels */ export type Translator = (p: Point) => Point; -/** Dimensions (in pixels) */ -export interface Dimensions { - width: number; - height: number; -} - /** Min/Max from all values of a given data set */ export interface Extent { min: Point; diff --git a/packages/calcite-components/src/components/interfaces.ts b/packages/calcite-components/src/components/interfaces.ts index 24e3c962267..27bde843e63 100644 --- a/packages/calcite-components/src/components/interfaces.ts +++ b/packages/calcite-components/src/components/interfaces.ts @@ -1,6 +1,10 @@ /* Note: using `.d.ts` file extension will exclude it from the output build */ export type Alignment = "start" | "center" | "end"; export type Appearance = "solid" | "outline" | "outline-fill" | "transparent"; +export interface Dimensions { + width: number; + height: number; +} export type FlipContext = "both" | "start" | "end"; export type Height = Scale; export type Kind = "brand" | "danger" | "info" | "inverse" | "neutral" | "warning" | "success";