@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
1,273 lines • 51.4 kB
JavaScript
/*!
* All material copyright ESRI, All Rights Reserved, unless otherwise specified.
* See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details.
* v1.5.0-next.4
*/
import { h } from "@stencil/core";
import Color from "color";
import { throttle } from "lodash-es";
import { getElementDir, isPrimaryPointerButton } from "../../utils/dom";
import { CSS, DEFAULT_COLOR, DEFAULT_STORAGE_KEY_PREFIX, DIMENSIONS, HSV_LIMITS, OPACITY_LIMITS, RGB_LIMITS } from "./resources";
import { alphaCompatible, alphaToOpacity, colorEqual, CSSColorMode, hexify, normalizeAlpha, normalizeColor, normalizeHex, opacityToAlpha, parseMode, toAlphaMode, toNonAlphaMode } from "./utils";
import { connectInteractive, disconnectInteractive, updateHostInteraction } from "../../utils/interactive";
import { isActivationKey } from "../../utils/key";
import { componentLoaded, setComponentLoaded, setUpLoadableComponent } from "../../utils/loadable";
import { connectLocalized, disconnectLocalized } from "../../utils/locale";
import { clamp } from "../../utils/math";
import { connectMessages, disconnectMessages, setUpMessages, updateMessages } from "../../utils/t9n";
const throttleFor60FpsInMs = 16;
export class ColorPicker {
constructor() {
this.internalColorUpdateContext = null;
this.mode = CSSColorMode.HEX;
this.shiftKeyChannelAdjustment = 0;
this.handleTabActivate = (event) => {
this.channelMode = event.currentTarget.getAttribute("data-color-mode");
this.updateChannelsFromColor(this.color);
};
this.handleColorFieldScopeKeyDown = (event) => {
const { key } = event;
const arrowKeyToXYOffset = {
ArrowUp: { x: 0, y: -10 },
ArrowRight: { x: 10, y: 0 },
ArrowDown: { x: 0, y: 10 },
ArrowLeft: { x: -10, y: 0 }
};
if (arrowKeyToXYOffset[key]) {
event.preventDefault();
this.scopeOrientation = key === "ArrowDown" || key === "ArrowUp" ? "vertical" : "horizontal";
this.captureColorFieldColor(this.colorFieldScopeLeft + arrowKeyToXYOffset[key].x || 0, this.colorFieldScopeTop + arrowKeyToXYOffset[key].y || 0, false);
}
};
this.handleHueScopeKeyDown = (event) => {
const modifier = event.shiftKey ? 10 : 1;
const { key } = event;
const arrowKeyToXOffset = {
ArrowUp: 1,
ArrowRight: 1,
ArrowDown: -1,
ArrowLeft: -1
};
if (arrowKeyToXOffset[key]) {
event.preventDefault();
const delta = arrowKeyToXOffset[key] * modifier;
const hue = this.baseColorFieldColor.hue();
const color = this.baseColorFieldColor.hue(hue + delta);
this.internalColorSet(color, false);
}
};
this.handleHexInputChange = (event) => {
event.stopPropagation();
const { allowEmpty, color } = this;
const input = event.target;
const hex = input.value;
if (allowEmpty && !hex) {
this.internalColorSet(null);
return;
}
const normalizedHex = color && normalizeHex(hexify(color, alphaCompatible(this.mode)));
if (hex !== normalizedHex) {
this.internalColorSet(Color(hex));
}
};
this.handleSavedColorSelect = (event) => {
const swatch = event.currentTarget;
this.internalColorSet(Color(swatch.color));
};
this.handleChannelInput = (event) => {
const input = event.currentTarget;
const channelIndex = Number(input.getAttribute("data-channel-index"));
const isAlphaChannel = channelIndex === 3;
const limit = isAlphaChannel
? OPACITY_LIMITS.max
: this.channelMode === "rgb"
? RGB_LIMITS[Object.keys(RGB_LIMITS)[channelIndex]]
: HSV_LIMITS[Object.keys(HSV_LIMITS)[channelIndex]];
let inputValue;
if (this.allowEmpty && !input.value) {
inputValue = "";
}
else {
const value = Number(input.value);
const adjustedValue = value + this.shiftKeyChannelAdjustment;
const clamped = clamp(adjustedValue, 0, limit);
inputValue = clamped.toString();
}
input.value = inputValue;
// TODO: refactor calcite-input so we don't need to sync the internals
// https://github.com/Esri/calcite-components/issues/6100
input.internalSyncChildElValue();
};
this.handleChannelChange = (event) => {
const input = event.currentTarget;
const channelIndex = Number(input.getAttribute("data-channel-index"));
const channels = [...this.channels];
const shouldClearChannels = this.allowEmpty && !input.value;
if (shouldClearChannels) {
this.channels = [null, null, null, null];
this.internalColorSet(null);
return;
}
const isAlphaChannel = channelIndex === 3;
const value = Number(input.value);
channels[channelIndex] = isAlphaChannel ? opacityToAlpha(value) : value;
this.updateColorFromChannels(channels);
};
this.handleSavedColorKeyDown = (event) => {
if (isActivationKey(event.key)) {
event.preventDefault();
this.handleSavedColorSelect(event);
}
};
this.handleColorFieldPointerDown = (event) => {
if (!isPrimaryPointerButton(event)) {
return;
}
const { offsetX, offsetY } = event;
document.addEventListener("pointermove", this.globalPointerMoveHandler);
document.addEventListener("pointerup", this.globalPointerUpHandler, { once: true });
this.activeCanvasInfo = {
context: this.colorFieldRenderingContext,
bounds: this.colorFieldRenderingContext.canvas.getBoundingClientRect()
};
this.captureColorFieldColor(offsetX, offsetY);
this.colorFieldScopeNode.focus();
};
this.handleHueSliderPointerDown = (event) => {
if (!isPrimaryPointerButton(event)) {
return;
}
const { offsetX } = event;
document.addEventListener("pointermove", this.globalPointerMoveHandler);
document.addEventListener("pointerup", this.globalPointerUpHandler, { once: true });
this.activeCanvasInfo = {
context: this.hueSliderRenderingContext,
bounds: this.hueSliderRenderingContext.canvas.getBoundingClientRect()
};
this.captureHueSliderColor(offsetX);
this.hueScopeNode.focus();
};
this.handleOpacitySliderPointerDown = (event) => {
if (!isPrimaryPointerButton(event)) {
return;
}
const { offsetX } = event;
document.addEventListener("pointermove", this.globalPointerMoveHandler);
document.addEventListener("pointerup", this.globalPointerUpHandler, { once: true });
this.activeCanvasInfo = {
context: this.opacitySliderRenderingContext,
bounds: this.opacitySliderRenderingContext.canvas.getBoundingClientRect()
};
this.captureOpacitySliderValue(offsetX);
this.opacityScopeNode.focus();
};
this.globalPointerUpHandler = (event) => {
if (!isPrimaryPointerButton(event)) {
return;
}
const previouslyDragging = this.activeCanvasInfo;
this.activeCanvasInfo = null;
this.drawColorControls();
if (previouslyDragging) {
this.calciteColorPickerChange.emit();
}
};
this.globalPointerMoveHandler = (event) => {
const { activeCanvasInfo, el } = this;
if (!el.isConnected || !activeCanvasInfo) {
return;
}
const { context, bounds } = activeCanvasInfo;
let samplingX;
let samplingY;
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 - 1;
}
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);
}
};
this.storeColorFieldScope = (node) => {
this.colorFieldScopeNode = node;
};
this.storeHueScope = (node) => {
this.hueScopeNode = node;
};
this.renderChannelsTabTitle = (channelMode) => {
const { channelMode: activeChannelMode, messages } = this;
const selected = channelMode === activeChannelMode;
const label = channelMode === "rgb" ? messages.rgb : messages.hsv;
return (h("calcite-tab-title", { class: CSS.colorMode, "data-color-mode": channelMode, key: channelMode, onCalciteTabsActivate: this.handleTabActivate, selected: selected }, label));
};
this.renderChannelsTab = (channelMode) => {
const { allowEmpty, channelMode: activeChannelMode, channels, messages, alphaChannel } = this;
const selected = channelMode === activeChannelMode;
const isRgb = channelMode === "rgb";
const channelAriaLabels = isRgb
? [messages.red, messages.green, messages.blue]
: [messages.hue, messages.saturation, messages.value];
const direction = getElementDir(this.el);
const channelsToRender = alphaChannel ? channels : channels.slice(0, 3);
return (h("calcite-tab", { class: CSS.control, key: channelMode, selected: selected }, h("div", { class: CSS.channels, dir: "ltr" }, channelsToRender.map((channelValue, index) => {
const isAlphaChannel = index === 3;
if (isAlphaChannel) {
channelValue =
allowEmpty && !channelValue ? channelValue : alphaToOpacity(channelValue);
}
/* the channel container is ltr, so we apply the host's direction */
return this.renderChannel(channelValue, index, channelAriaLabels[index], direction, isAlphaChannel ? "%" : "");
}))));
};
this.renderChannel = (value, index, ariaLabel, direction, suffix) => {
return (h("calcite-input", { class: CSS.channel, "data-channel-index": index, dir: direction, key: index, label: ariaLabel, lang: this.effectiveLocale, numberButtonType: "none", numberingSystem: this.numberingSystem, onCalciteInputChange: this.handleChannelChange, onCalciteInputInput: this.handleChannelInput, onKeyDown: this.handleKeyDown, scale: this.scale === "l" ? "m" : "s",
// workaround to ensure input borders overlap as desired
// this is because the build transforms margin-left to its
// logical-prop, which is undesired as channels are always ltr
style: {
marginLeft: index > 0 && !(this.scale === "s" && this.alphaChannel && index === 3) ? "-1px" : ""
}, suffixText: suffix, type: "number", value: value?.toString() }));
};
this.deleteColor = () => {
const colorToDelete = hexify(this.color, this.alphaChannel);
const inStorage = this.savedColors.indexOf(colorToDelete) > -1;
if (!inStorage) {
return;
}
const savedColors = this.savedColors.filter((color) => color !== colorToDelete);
this.savedColors = savedColors;
const storageKey = `${DEFAULT_STORAGE_KEY_PREFIX}${this.storageId}`;
if (this.storageId) {
localStorage.setItem(storageKey, JSON.stringify(savedColors));
}
};
this.saveColor = () => {
const colorToSave = hexify(this.color, this.alphaChannel);
const alreadySaved = this.savedColors.indexOf(colorToSave) > -1;
if (alreadySaved) {
return;
}
const savedColors = [...this.savedColors, colorToSave];
this.savedColors = savedColors;
const storageKey = `${DEFAULT_STORAGE_KEY_PREFIX}${this.storageId}`;
if (this.storageId) {
localStorage.setItem(storageKey, JSON.stringify(savedColors));
}
};
this.drawColorControls = throttle((type = "all") => {
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);
this.captureColorFieldColor = (x, y, skipEqual = true) => {
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);
};
this.initColorField = (canvas) => {
this.colorFieldRenderingContext = canvas.getContext("2d");
this.updateCanvasSize("color-field");
this.drawColorControls();
};
this.initHueSlider = (canvas) => {
this.hueSliderRenderingContext = canvas.getContext("2d");
this.updateCanvasSize("hue-slider");
this.drawHueSlider();
};
this.initOpacitySlider = (canvas) => {
this.opacitySliderRenderingContext = canvas.getContext("2d");
this.updateCanvasSize("opacity-slider");
this.drawOpacitySlider();
};
this.storeOpacityScope = (node) => {
this.opacityScopeNode = node;
};
this.handleOpacityScopeKeyDown = (event) => {
const modifier = event.shiftKey ? 10 : 1;
const { key } = event;
const arrowKeyToXOffset = {
ArrowUp: 1,
ArrowRight: 1,
ArrowDown: -1,
ArrowLeft: -1
};
if (arrowKeyToXOffset[key]) {
event.preventDefault();
const delta = opacityToAlpha(arrowKeyToXOffset[key] * modifier);
this.captureHueSliderColor(this.opacityScopeLeft + delta);
}
};
this.allowEmpty = false;
this.alphaChannel = false;
this.channelsDisabled = false;
this.color = DEFAULT_COLOR;
this.disabled = false;
this.format = "auto";
this.hideChannels = false;
this.hexDisabled = false;
this.hideHex = false;
this.hideSaved = false;
this.savedDisabled = false;
this.scale = "m";
this.storageId = undefined;
this.messageOverrides = undefined;
this.numberingSystem = undefined;
this.value = normalizeHex(hexify(DEFAULT_COLOR, this.alphaChannel));
this.defaultMessages = undefined;
this.channelMode = "rgb";
this.channels = this.toChannels(DEFAULT_COLOR);
this.dimensions = DIMENSIONS.m;
this.effectiveLocale = "";
this.messages = undefined;
this.savedColors = [];
this.colorFieldScopeTop = undefined;
this.colorFieldScopeLeft = undefined;
this.hueScopeLeft = undefined;
this.opacityScopeLeft = undefined;
this.scopeOrientation = undefined;
}
handleAlphaChannelChange(alphaChannel) {
const { format } = this;
if (alphaChannel && format !== "auto" && !alphaCompatible(format)) {
console.warn(`ignoring alphaChannel as the current format (${format}) does not support alpha`);
this.alphaChannel = false;
}
}
handleColorChange(color, oldColor) {
this.drawColorControls();
this.updateChannelsFromColor(color);
this.previousColor = oldColor;
}
handleFormatChange(format) {
this.setMode(format);
this.internalColorSet(this.color, false, "internal");
}
handleScaleChange(scale = "m") {
this.updateDimensions(scale);
this.updateCanvasSize("all");
this.drawColorControls();
}
onMessagesChange() {
/* wired up by t9n util */
}
handleValueChange(value, oldValue) {
const { allowEmpty, format } = this;
const checkMode = !allowEmpty || value;
let modeChanged = false;
if (checkMode) {
const nextMode = parseMode(value);
if (!nextMode || (format !== "auto" && nextMode !== format)) {
this.showIncompatibleColorWarning(value, format);
this.value = oldValue;
return;
}
modeChanged = this.mode !== nextMode;
this.setMode(nextMode, this.internalColorUpdateContext === null);
}
const dragging = this.activeCanvasInfo;
if (this.internalColorUpdateContext === "initial") {
return;
}
if (this.internalColorUpdateContext === "user-interaction") {
this.calciteColorPickerInput.emit();
if (!dragging) {
this.calciteColorPickerChange.emit();
}
return;
}
const color = allowEmpty && !value
? null
: Color(value != null && typeof value === "object" && alphaCompatible(this.mode)
? normalizeColor(value)
: value);
const colorChanged = !colorEqual(color, this.color);
if (modeChanged || colorChanged) {
this.internalColorSet(color, this.alphaChannel && !(this.mode.endsWith("a") || this.mode.endsWith("a-css")), "internal");
}
}
get baseColorFieldColor() {
return this.color || this.previousColor || DEFAULT_COLOR;
}
effectiveLocaleChange() {
updateMessages(this, this.effectiveLocale);
}
// using @Listen as a workaround for VDOM listener not firing
handleChannelKeyUpOrDown(event) {
this.shiftKeyChannelAdjustment = 0;
const { key } = event;
if ((key !== "ArrowUp" && key !== "ArrowDown") ||
!event.composedPath().some((node) => node.classList?.contains(CSS.channel))) {
return;
}
const { shiftKey } = event;
event.preventDefault();
if (!this.color) {
this.internalColorSet(this.previousColor);
event.stopPropagation();
return;
}
// this gets applied to the input's up/down arrow increment/decrement
const complementaryBump = 9;
this.shiftKeyChannelAdjustment =
key === "ArrowUp" && shiftKey
? complementaryBump
: key === "ArrowDown" && shiftKey
? -complementaryBump
: 0;
}
//--------------------------------------------------------------------------
//
// Public Methods
//
//--------------------------------------------------------------------------
/** Sets focus on the component's first focusable element. */
async setFocus() {
await componentLoaded(this);
this.el.focus();
}
//--------------------------------------------------------------------------
//
// Lifecycle
//
//--------------------------------------------------------------------------
async componentWillLoad() {
setUpLoadableComponent(this);
const { allowEmpty, color, format, value } = this;
const willSetNoColor = allowEmpty && !value;
const parsedMode = parseMode(value);
const valueIsCompatible = willSetNoColor || (format === "auto" && parsedMode) || format === parsedMode;
const initialColor = willSetNoColor ? null : valueIsCompatible ? Color(value) : color;
if (!valueIsCompatible) {
this.showIncompatibleColorWarning(value, format);
}
this.setMode(format, false);
this.internalColorSet(initialColor, false, "initial");
this.updateDimensions(this.scale);
const storageKey = `${DEFAULT_STORAGE_KEY_PREFIX}${this.storageId}`;
if (this.storageId && localStorage.getItem(storageKey)) {
this.savedColors = JSON.parse(localStorage.getItem(storageKey));
}
await setUpMessages(this);
}
connectedCallback() {
connectInteractive(this);
connectLocalized(this);
connectMessages(this);
}
componentDidLoad() {
setComponentLoaded(this);
}
disconnectedCallback() {
document.removeEventListener("pointermove", this.globalPointerMoveHandler);
document.removeEventListener("pointerup", this.globalPointerUpHandler);
disconnectInteractive(this);
disconnectLocalized(this);
disconnectMessages(this);
}
componentDidRender() {
updateHostInteraction(this);
}
//--------------------------------------------------------------------------
//
// Render Methods
//
//--------------------------------------------------------------------------
render() {
const { allowEmpty, channelsDisabled, color, colorFieldScopeLeft, colorFieldScopeTop, dimensions: { colorField: { width: colorFieldWidth }, slider: { width: sliderWidth }, thumb: { radius: thumbRadius } }, hexDisabled, hideChannels, hideHex, hideSaved, hueScopeLeft, messages, alphaChannel, opacityScopeLeft, savedColors, savedDisabled, scale, scopeOrientation } = this;
const selectedColorInHex = color ? hexify(color, alphaChannel) : null;
const hueTop = thumbRadius;
const hueLeft = hueScopeLeft ?? (sliderWidth * DEFAULT_COLOR.hue()) / HSV_LIMITS.h;
const opacityTop = thumbRadius;
const opacityLeft = opacityScopeLeft ??
(colorFieldWidth * alphaToOpacity(DEFAULT_COLOR.alpha())) / OPACITY_LIMITS.max;
const noColor = color === null;
const vertical = scopeOrientation === "vertical";
const noHex = hexDisabled || hideHex;
const noChannels = channelsDisabled || hideChannels;
const noSaved = savedDisabled || hideSaved;
return (h("div", { class: CSS.container }, h("div", { class: CSS.controlAndScope }, h("canvas", { class: CSS.colorField, onPointerDown: this.handleColorFieldPointerDown,
// eslint-disable-next-line react/jsx-sort-props
ref: this.initColorField }), h("div", { "aria-label": vertical ? messages.value : messages.saturation, "aria-valuemax": vertical ? HSV_LIMITS.v : HSV_LIMITS.s, "aria-valuemin": "0", "aria-valuenow": (vertical ? color?.saturationv() : color?.value()) || "0", class: { [CSS.scope]: true, [CSS.colorFieldScope]: true }, onKeyDown: this.handleColorFieldScopeKeyDown, role: "slider", style: { top: `${colorFieldScopeTop || 0}px`, left: `${colorFieldScopeLeft || 0}px` }, tabindex: "0",
// eslint-disable-next-line react/jsx-sort-props
ref: this.storeColorFieldScope })), h("div", { class: CSS.previewAndSliders }, h("calcite-color-picker-swatch", { class: CSS.preview, color: selectedColorInHex, scale: "l" }), h("div", { class: CSS.sliders }, h("div", { class: CSS.controlAndScope }, h("canvas", { class: { [CSS.slider]: true, [CSS.hueSlider]: true }, onPointerDown: this.handleHueSliderPointerDown,
// eslint-disable-next-line react/jsx-sort-props
ref: this.initHueSlider }), h("div", { "aria-label": messages.hue, "aria-valuemax": HSV_LIMITS.h, "aria-valuemin": "0", "aria-valuenow": color?.round().hue() || DEFAULT_COLOR.round().hue(), class: { [CSS.scope]: true, [CSS.hueScope]: true }, onKeyDown: this.handleHueScopeKeyDown, role: "slider", style: { top: `${hueTop}px`, left: `${hueLeft}px` }, tabindex: "0",
// eslint-disable-next-line react/jsx-sort-props
ref: this.storeHueScope })), alphaChannel ? (h("div", { class: CSS.controlAndScope }, h("canvas", { class: { [CSS.slider]: true, [CSS.opacitySlider]: true }, onPointerDown: this.handleOpacitySliderPointerDown,
// eslint-disable-next-line react/jsx-sort-props
ref: this.initOpacitySlider }), h("div", { "aria-label": messages.opacity, "aria-valuemax": OPACITY_LIMITS.max, "aria-valuemin": OPACITY_LIMITS.min, "aria-valuenow": (color || DEFAULT_COLOR).round().alpha(), class: { [CSS.scope]: true, [CSS.opacityScope]: true }, onKeyDown: this.handleOpacityScopeKeyDown, role: "slider", style: { top: `${opacityTop}px`, left: `${opacityLeft}px` }, tabindex: "0",
// eslint-disable-next-line react/jsx-sort-props
ref: this.storeOpacityScope }))) : null)), noHex && noChannels ? null : (h("div", { class: {
[CSS.controlSection]: true,
[CSS.section]: true
} }, h("div", { class: CSS.hexAndChannelsGroup }, noHex ? null : (h("div", { class: CSS.hexOptions }, h("calcite-color-picker-hex-input", { allowEmpty: allowEmpty, alphaChannel: alphaChannel, class: CSS.control, messages: messages, numberingSystem: this.numberingSystem, onCalciteColorPickerHexInputChange: this.handleHexInputChange, scale: scale, value: selectedColorInHex }))), noChannels ? null : (h("calcite-tabs", { class: {
[CSS.colorModeContainer]: true,
[CSS.splitSection]: true
}, scale: scale === "l" ? "m" : "s" }, h("calcite-tab-nav", { slot: "title-group" }, this.renderChannelsTabTitle("rgb"), this.renderChannelsTabTitle("hsv")), this.renderChannelsTab("rgb"), this.renderChannelsTab("hsv")))))), noSaved ? null : (h("div", { class: { [CSS.savedColorsSection]: true, [CSS.section]: true } }, h("div", { class: CSS.header }, h("label", null, messages.saved), h("div", { class: CSS.savedColorsButtons }, h("calcite-button", { appearance: "transparent", class: CSS.deleteColor, disabled: noColor, iconStart: "minus", kind: "neutral", label: messages.deleteColor, onClick: this.deleteColor, scale: scale, type: "button" }), h("calcite-button", { appearance: "transparent", class: CSS.saveColor, disabled: noColor, iconStart: "plus", kind: "neutral", label: messages.saveColor, onClick: this.saveColor, scale: scale, type: "button" }))), savedColors.length > 0 ? (h("div", { class: CSS.savedColors }, [
...savedColors.map((color) => (h("calcite-color-picker-swatch", { class: CSS.savedColor, color: color, key: color, onClick: this.handleSavedColorSelect, onKeyDown: this.handleSavedColorKeyDown, scale: scale, tabIndex: 0 })))
])) : null))));
}
// --------------------------------------------------------------------------
//
// Private Methods
//
//--------------------------------------------------------------------------
handleKeyDown(event) {
if (event.key === "Enter") {
event.preventDefault();
}
}
showIncompatibleColorWarning(value, format) {
console.warn(`ignoring color value (${value}) as it is not compatible with the current format (${format})`);
}
setMode(format, warn = true) {
const mode = format === "auto" ? this.mode : format;
this.mode = this.ensureCompatibleMode(mode, warn);
}
ensureCompatibleMode(mode, warn) {
const { alphaChannel } = this;
const isAlphaCompatible = alphaCompatible(mode);
if (alphaChannel && !isAlphaCompatible) {
const alphaMode = toAlphaMode(mode);
if (warn) {
console.warn(`setting format to (${alphaMode}) as the provided one (${mode}) does not support alpha`);
}
return alphaMode;
}
if (!alphaChannel && isAlphaCompatible) {
const nonAlphaMode = toNonAlphaMode(mode);
if (warn) {
console.warn(`setting format to (${nonAlphaMode}) as the provided one (${mode}) does not support alpha`);
}
return nonAlphaMode;
}
return mode;
}
captureHueSliderColor(x) {
const { dimensions: { slider: { width } } } = this;
const hue = (360 / width) * x;
this.internalColorSet(this.baseColorFieldColor.hue(hue), false);
}
captureOpacitySliderValue(x) {
const { dimensions: { slider: { width } } } = this;
const alpha = opacityToAlpha((OPACITY_LIMITS.max / width) * x);
this.internalColorSet(this.baseColorFieldColor.alpha(alpha), false);
}
internalColorSet(color, skipEqual = true, context = "user-interaction") {
if (skipEqual && colorEqual(color, this.color)) {
return;
}
this.internalColorUpdateContext = context;
this.color = color;
this.value = this.toValue(color);
this.internalColorUpdateContext = null;
}
toValue(color, format = this.mode) {
if (!color) {
return null;
}
const hexMode = "hex";
if (format.includes(hexMode)) {
const hasAlpha = format === CSSColorMode.HEXA;
return normalizeHex(hexify(color.round(), hasAlpha), hasAlpha);
}
if (format.includes("-css")) {
const value = color[format.replace("-css", "").replace("a", "")]().round().string();
// Color omits alpha values when alpha is 1
const needToInjectAlpha = (format.endsWith("a") || format.endsWith("a-css")) && color.alpha() === 1;
if (needToInjectAlpha) {
const model = value.slice(0, 3);
const values = value.slice(4, -1);
return `${model}a(${values}, ${color.alpha()})`;
}
return value;
}
const colorObject =
/* Color() does not support hsva, hsla nor rgba, so we use the non-alpha mode */
color[toNonAlphaMode(format)]().round().object();
if (format.endsWith("a")) {
return normalizeAlpha(colorObject);
}
return colorObject;
}
getSliderCapSpacing() {
const { dimensions: { slider: { height }, thumb: { radius } } } = this;
return radius * 2 - height;
}
updateDimensions(scale = "m") {
this.dimensions = DIMENSIONS[scale];
}
drawColorField() {
const context = this.colorFieldRenderingContext;
const { dimensions: { colorField: { height, width } } } = this;
context.fillStyle = this.baseColorFieldColor
.hsv()
.saturationv(100)
.value(100)
.alpha(1)
.string();
context.fillRect(0, 0, width, height);
const whiteGradient = context.createLinearGradient(0, 0, width, 0);
whiteGradient.addColorStop(0, "rgba(255,255,255,1)");
whiteGradient.addColorStop(1, "rgba(255,255,255,0)");
context.fillStyle = whiteGradient;
context.fillRect(0, 0, width, height);
const blackGradient = context.createLinearGradient(0, 0, 0, height);
blackGradient.addColorStop(0, "rgba(0,0,0,0)");
blackGradient.addColorStop(1, "rgba(0,0,0,1)");
context.fillStyle = blackGradient;
context.fillRect(0, 0, width, height);
this.drawActiveColorFieldColor();
}
setCanvasContextSize(canvas, { height, width }) {
if (!canvas) {
return;
}
const devicePixelRatio = window.devicePixelRatio || 1;
canvas.width = width * devicePixelRatio;
canvas.height = height * devicePixelRatio;
canvas.style.height = `${height}px`;
canvas.style.width = `${width}px`;
const context = canvas.getContext("2d");
context.scale(devicePixelRatio, devicePixelRatio);
}
updateCanvasSize(context = "all") {
const { dimensions } = this;
if (context === "all" || context === "color-field") {
this.setCanvasContextSize(this.colorFieldRenderingContext?.canvas, dimensions.colorField);
}
const adjustedSliderDimensions = {
width: dimensions.slider.width,
height: dimensions.slider.height + (dimensions.thumb.radius - dimensions.slider.height / 2) * 2
};
if (context === "all" || context === "hue-slider") {
this.setCanvasContextSize(this.hueSliderRenderingContext?.canvas, adjustedSliderDimensions);
}
if (context === "all" || context === "opacity-slider") {
this.setCanvasContextSize(this.opacitySliderRenderingContext?.canvas, adjustedSliderDimensions);
}
}
drawActiveColorFieldColor() {
const { color } = this;
if (!color) {
return;
}
const hsvColor = color.hsv();
const { dimensions: { colorField: { height, width }, thumb: { radius } } } = this;
const x = hsvColor.saturationv() / (HSV_LIMITS.s / width);
const y = height - hsvColor.value() / (HSV_LIMITS.v / height);
requestAnimationFrame(() => {
this.colorFieldScopeLeft = x;
this.colorFieldScopeTop = y;
});
this.drawThumb(this.colorFieldRenderingContext, radius, x, y, hsvColor);
}
drawThumb(context, radius, x, y, color) {
const startAngle = 0;
const endAngle = 2 * Math.PI;
const outlineWidth = 1;
radius = radius - outlineWidth;
context.beginPath();
context.arc(x, y, radius, startAngle, endAngle);
context.fillStyle = "#fff";
context.fill();
context.strokeStyle = "rgba(0,0,0,0.3)";
context.lineWidth = outlineWidth;
context.stroke();
context.beginPath();
context.arc(x, y, radius - 3, startAngle, endAngle);
context.fillStyle = color.rgb().alpha(1).string();
context.fill();
}
drawActiveHueSliderColor() {
const { color } = this;
if (!color) {
return;
}
const hsvColor = color.hsv().saturationv(100).value(100);
const { dimensions: { slider: { height, width }, thumb: { radius } } } = this;
const x = hsvColor.hue() / (360 / width);
const y = radius - height / 2 + height / 2;
requestAnimationFrame(() => {
this.hueScopeLeft = x;
});
this.drawThumb(this.hueSliderRenderingContext, radius, x, y, hsvColor);
}
drawHueSlider() {
const context = this.hueSliderRenderingContext;
const { dimensions: { slider: { height, width }, thumb: { radius: thumbRadius } } } = this;
const x = 0;
const y = thumbRadius - height / 2;
const gradient = context.createLinearGradient(0, 0, width, 0);
const hueSliderColorStopKeywords = ["red", "yellow", "lime", "cyan", "blue", "magenta", "red"];
const offset = 1 / (hueSliderColorStopKeywords.length - 1);
let currentOffset = 0;
hueSliderColorStopKeywords.forEach((keyword) => {
gradient.addColorStop(currentOffset, Color(keyword).string());
currentOffset += offset;
});
context.clearRect(0, 0, width, height + this.getSliderCapSpacing() * 2);
this.drawSliderPath(context, height, width, x, y);
context.fillStyle = gradient;
context.fill();
context.strokeStyle = "rgba(0,0,0,0.3)";
context.lineWidth = 1;
context.stroke();
this.drawActiveHueSliderColor();
}
drawOpacitySlider() {
const context = this.opacitySliderRenderingContext;
const { baseColorFieldColor: previousColor, dimensions: { slider: { height, width }, thumb: { radius: thumbRadius } } } = this;
const x = 0;
const y = thumbRadius - height / 2;
context.clearRect(0, 0, width, height + this.getSliderCapSpacing() * 2);
const gradient = context.createLinearGradient(0, y, width, 0);
const startColor = previousColor.rgb().alpha(0);
const midColor = previousColor.rgb().alpha(0.5);
const endColor = previousColor.rgb().alpha(1);
gradient.addColorStop(0, startColor.string());
gradient.addColorStop(0.5, midColor.string());
gradient.addColorStop(1, endColor.string());
this.drawSliderPath(context, height, width, x, y);
const pattern = context.createPattern(this.getCheckeredBackgroundPattern(), "repeat");
context.fillStyle = pattern;
context.fill();
context.fillStyle = gradient;
context.fill();
context.strokeStyle = "rgba(0,0,0,0.3)";
context.lineWidth = 1;
context.stroke();
this.drawActiveOpacitySliderColor();
}
drawSliderPath(context, height, width, x, y) {
const radius = height / 2 + 1;
context.beginPath();
context.moveTo(x + radius, y);
context.lineTo(x + width - radius, y);
context.quadraticCurveTo(x + width, y, x + width, y + radius);
context.lineTo(x + width, y + height - radius);
context.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
context.lineTo(x + radius, y + height);
context.quadraticCurveTo(x, y + height, x, y + height - radius);
context.lineTo(x, y + radius);
context.quadraticCurveTo(x, y, x + radius, y);
context.closePath();
}
getCheckeredBackgroundPattern() {
if (this.checkerPattern) {
return this.checkerPattern;
}
const pattern = document.createElement("canvas");
pattern.width = 10;
pattern.height = 10;
const patternContext = pattern.getContext("2d");
patternContext.fillStyle = "#ccc";
patternContext.fillRect(0, 0, 10, 10);
patternContext.fillStyle = "#fff";
patternContext.fillRect(0, 0, 5, 5);
patternContext.fillRect(5, 5, 5, 5);
this.checkerPattern = pattern;
return pattern;
}
drawActiveOpacitySliderColor() {
const { color } = this;
if (!color) {
return;
}
const hsvColor = color;
const { dimensions: { slider: { width }, thumb: { radius } } } = this;
const x = alphaToOpacity(hsvColor.alpha()) / (OPACITY_LIMITS.max / width);
const y = radius;
requestAnimationFrame(() => {
this.opacityScopeLeft = x;
});
this.drawThumb(this.opacitySliderRenderingContext, radius, x, y, hsvColor);
}
updateColorFromChannels(channels) {
this.internalColorSet(Color(channels, this.channelMode));
}
updateChannelsFromColor(color) {
this.channels = color ? this.toChannels(color) : [null, null, null, null];
}
toChannels(color) {
const { channelMode } = this;
const channels = color[channelMode]()
.array()
.map((value, index) => {
const isAlpha = index === 3;
return isAlpha ? value : Math.floor(value);
});
if (channels.length === 3) {
channels.push(1); // Color omits alpha when 1
}
return channels;
}
static get is() { return "calcite-color-picker"; }
static get encapsulation() { return "shadow"; }
static get delegatesFocus() { return true; }
static get originalStyleUrls() {
return {
"$": ["color-picker.scss"]
};
}
static get styleUrls() {
return {
"$": ["color-picker.css"]
};
}
static get assetsDirs() { return ["assets"]; }
static get properties() {
return {
"allowEmpty": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "When `true`, an empty color (`null`) will be allowed as a `value`. When `false`, a color value is enforced, and clearing the input or blurring will restore the last valid `value`."
},
"attribute": "allow-empty",
"reflect": true,
"defaultValue": "false"
},
"alphaChannel": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "When true, the component will allow updates to the color's alpha value."
},
"attribute": "alpha-channel",
"reflect": false,
"defaultValue": "false"
},
"channelsDisabled": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "When true, hides the RGB/HSV channel inputs"
},
"attribute": "channels-disabled",
"reflect": false,
"defaultValue": "false"
},
"color": {
"type": "unknown",
"mutable": true,
"complexType": {
"original": "InternalColor | null",
"resolved": "Color<ColorParam>",
"references": {
"InternalColor": {
"location": "import",
"path": "./interfaces"
}
}
},
"required": false,
"optional": false,
"docs": {
"tags": [{
"name": "internal",
"text": undefined
}],
"text": "Internal prop for advanced use-cases."
},
"defaultValue": "DEFAULT_COLOR"
},
"disabled": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "When `true`, interaction is prevented and the component is displayed with lower opacity."
},
"attribute": "disabled",
"reflect": true,
"defaultValue": "false"
},
"format": {
"type": "string",
"mutable": false,
"complexType": {
"original": "Format",
"resolved": "\"auto\" | \"hex\" | \"hexa\" | \"hsl\" | \"hsl-css\" | \"hsla\" | \"hsla-css\" | \"hsv\" | \"hsva\" | \"rgb\" | \"rgb-css\" | \"rgba\" | \"rgba-css\"",
"references": {
"Format": {
"location": "import",
"path": "./utils"
}
}
},
"required": false,
"optional": false,
"docs": {
"tags": [{
"name": "default",
"text": "\"auto\""
}],
"text": "The format of `value`.\n\nWhen `\"auto\"`, the format will be inferred from `value` when set."
},
"attribute": "format",
"reflect": true,
"defaultValue": "\"auto\""
},
"hideChannels": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [{
"name": "deprecated",
"text": "use `channelsDisabled` instead"
}],
"text": "When `true`, hides the RGB/HSV channel inputs."
},
"attribute": "hide-channels",
"reflect": true,
"defaultValue": "false"
},
"hexDisabled": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "When true, hides the hex input"
},
"attribute": "hex-disabled",
"reflect": false,
"defaultValue": "false"
},
"hideHex": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [{
"name": "deprecated",
"text": "use `hexDisabled` instead"
}],
"text": "When `true`, hides the hex input."
},
"attribute": "hide-hex",
"reflect": true,
"defaultValue": "false"
},
"hideSaved": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [{
"name": "deprecated",
"text": "use `savedDisabled` instead"
}],
"text": "When `true`, hides the saved colors section."
},
"attribute": "hide-saved",
"reflect": true,
"defaultValue": "false"
},
"savedDisabled": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "When true, hides the saved colors section"
},
"attribute": "saved-disabled",
"reflect": true,
"defaultValue": "false"
},
"scale": {
"type": "string",
"mutable": false,
"complexType": {
"original": "Scale",
"resolved": "\"l\" | \"m\" | \"s\"",
"references": {
"Scale": {
"location": "import",
"path": "../interfaces"
}
}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Specifies the size of the component."
},
"attribute": "scale",
"reflect": true,
"defaultValue": "\"m\""
},
"storageId": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Specifies the storage ID for colors."
},
"attribute": "storage-id",
"reflect": true
},
"messageOverrides": {
"type": "unknown",
"mutable": true,
"complexType": {
"original": "Partial<ColorPickerMessages>",
"resolved": "{ b?: string; blue?: string; deleteColor?: string; g?: string; green?: string; h?: string; hsv?: string; hex?: string; hue?: string; noColor?: string; opacity?: string; r?: string; red?: string; rgb?: string; s?: string; saturation?: string; saveColor?: string; saved?: string; v?: string; value?: string; }",
"references": {
"Partial": {
"location": "global"
},
"ColorPickerMessages": {
"location": "import",
"path": "./assets/color-picker/t9n"
}
}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Use this property to override individual strings used by the component."
}
},
"numberingSystem": {
"type": "string",
"mutable": false,
"complexType": {
"original": "NumberingSystem",
"resolved": "\"arab\" | \"arabext\" | \"bali\" | \"beng\" | \"deva\" | \"fullwide\" | \"gujr\" | \"guru\" | \"hanidec\" | \"khmr\" | \"knda\" | \"laoo\" | \"latn\" | \"limb\" | \"mlym\" | \"mong\" | \"mymr\" | \"orya\" | \"tamldec\" | \"telu\" | \"thai\" | \"tibt\"",
"references": {
"NumberingSystem": {
"location": "import",
"path": "../../utils/locale"
}
}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Specifies the Unicode numeral system used by the component for localization."
},
"attribute": "numbering-system",
"reflect": true
},
"value": {
"type": "string",
"mutable": true,
"complexType": {
"original": "ColorValue | null",
"resolved": "HSL | HSL & ObjectWithAlpha | HSV | HSV & ObjectWithAlpha | RGB | RGB & ObjectWithAlpha | string",
"references": {
"ColorValue": {
"location": "import",
"path": "./interfaces"
}
}
},
"required": false,
"optional": false,
"docs": {
"tags": [{
"name": "default",
"text": "\"#007ac2\""
}, {
"name": "see",
"text": "[CSS Color](https://developer.mozilla.org/en-US/docs/Web/CSS/color)"
}, {
"name": "see",
"text": "[ColorValue](https://github.com/Esri/calcite-components/blob/master/src/components/color-picker/interfaces.ts#L10)"
}],
"text": "The component's value, where the value can be a CSS color string, or a RGB, HSL or HSV object.\n\nThe type will be preserved as the color is updated."
},
"attribute": "value",
"reflect": false,
"defaultValue": "normalizeHex(\n hexify(DEFAULT_COLOR, this.alphaChannel)\n )"
},
"messages": {
"type": "unknown",
"mutable": true,
"complexType": {
"original": "ColorPickerMessages",
"resolved": "{ b: string; blue: string; deleteColor: string; g: string; green: string; h: string; hsv: string; hex: string; hue: string; noColor: string; opacity: string; r: string; red: string; rgb: string; s: string; saturation: string; saveColor: string; saved: string; v: string; value: string; }",
"references": {
"ColorPickerMessages": {
"location": "import",
"path": "./assets/color-picker/t9n"
}
}
},
"required": false,
"optional": false,
"docs": {
"tags": [{
"name": "internal",
"text": undefined
}],
"text": "Made into a prop for testing purposes only"
}
}
};
}
static get states() {
return {
"defaultMessages": {},
"channelMode": {},
"channels": {},
"dimensions": {},
"effectiveLocale": {},
"savedColors": {},
"colorFieldScopeTop": {},
"colorFieldScopeLeft": {},
"hueScopeLeft": {},
"opacityScopeLeft": {},
"scopeOrientation": {}
};
}
static get events() {
return [{
"method": "calciteColorPickerChange",
"name": "calciteColorPickerChange",
"bubbles": true,
"cancelable": false,
"composed": true,
"docs": {
"tags": [],
"text": "Fires when the color value has changed."
},
"complexType": {
"original": "void",
"resolved": "void",
"references": {}
}
}, {
"method": "calciteColorPickerInput",
"name": "calciteColorPickerInput",
"bubbles": true,
"cancelable": false,
"composed": true,
"docs": {
"tags": [],
"text": "Fires as the color value changes.\n\nSimilar to the `calciteColorPickerChange` event with the exception of dragging. When dragging the color field or hue slider thumb, this event fires as the thumb is moved."
},
"complexType": {
"original": "void",
"resolved": "void",
"references": {}
}
}];
}
static get methods() {
return {
"setFocus": {
"complexType": {
"signa