@zag-js/color-picker
Version:
Core logic for the color-picker widget implemented as a state machine
685 lines (683 loc) • 26 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/color-picker.connect.ts
var color_picker_connect_exports = {};
__export(color_picker_connect_exports, {
connect: () => connect
});
module.exports = __toCommonJS(color_picker_connect_exports);
var import_color_utils = require("@zag-js/color-utils");
var import_dom_query = require("@zag-js/dom-query");
var import_dom_query2 = require("@zag-js/dom-query");
var import_popper = require("@zag-js/popper");
var import_color_picker = require("./color-picker.anatomy.js");
var dom = __toESM(require("./color-picker.dom.js"));
var import_get_channel_display_color = require("./utils/get-channel-display-color.js");
var import_get_channel_input_value = require("./utils/get-channel-input-value.js");
var import_get_slider_background = require("./utils/get-slider-background.js");
function connect(service, normalize) {
const { context, send, prop, computed, state, scope } = service;
const value = context.get("value");
const format = context.get("format");
const areaValue = computed("areaValue");
const valueAsString = computed("valueAsString");
const disabled = computed("disabled");
const readOnly = !!prop("readOnly");
const invalid = !!prop("invalid");
const required = !!prop("required");
const interactive = computed("interactive");
const dragging = state.hasTag("dragging");
const open = state.hasTag("open");
const focused = state.hasTag("focused");
const getAreaChannels = (props) => {
const channels = areaValue.getChannels();
return {
xChannel: props.xChannel ?? channels[1],
yChannel: props.yChannel ?? channels[2]
};
};
const currentPlacement = context.get("currentPlacement");
const currentPlacementSide = currentPlacement ? (0, import_popper.getPlacementSide)(currentPlacement) : void 0;
const popperStyles = (0, import_popper.getPlacementStyles)({
...prop("positioning"),
placement: currentPlacement
});
function getSwatchTriggerState(props) {
const color = (0, import_color_utils.normalizeColor)(props.value).toFormat(context.get("format"));
return {
value: color,
valueAsString: color.toString("hex"),
checked: color.isEqual(value),
disabled: props.disabled || !interactive
};
}
return {
dragging,
open,
valueAsString,
value,
inline: !!prop("inline"),
setOpen(nextOpen) {
if (prop("inline")) return;
const open2 = state.hasTag("open");
if (open2 === nextOpen) return;
send({ type: nextOpen ? "OPEN" : "CLOSE" });
},
setValue(value2) {
send({ type: "VALUE.SET", value: (0, import_color_utils.normalizeColor)(value2), src: "set-color" });
},
getChannelValue(channel) {
return (0, import_get_channel_input_value.getChannelValue)(value, channel);
},
getChannelValueText(channel, locale) {
return value.formatChannelValue(channel, locale);
},
setChannelValue(channel, channelValue) {
const color = value.withChannelValue(channel, channelValue);
send({ type: "VALUE.SET", value: color, src: "set-channel" });
},
format: context.get("format"),
setFormat(format2) {
const formatValue = value.toFormat(format2);
send({ type: "VALUE.SET", value: formatValue, src: "set-format" });
},
alpha: value.getChannelValue("alpha"),
setAlpha(alphaValue) {
const color = value.withChannelValue("alpha", alphaValue);
send({ type: "VALUE.SET", value: color, src: "set-alpha" });
},
getRootProps() {
return normalize.element({
...import_color_picker.parts.root.attrs,
dir: prop("dir"),
id: dom.getRootId(scope),
"data-disabled": (0, import_dom_query2.dataAttr)(disabled),
"data-readonly": (0, import_dom_query2.dataAttr)(readOnly),
"data-invalid": (0, import_dom_query2.dataAttr)(invalid),
style: {
"--value": value.toString("css")
}
});
},
getLabelProps() {
return normalize.element({
...import_color_picker.parts.label.attrs,
dir: prop("dir"),
id: dom.getLabelId(scope),
htmlFor: dom.getHiddenInputId(scope),
"data-disabled": (0, import_dom_query2.dataAttr)(disabled),
"data-readonly": (0, import_dom_query2.dataAttr)(readOnly),
"data-invalid": (0, import_dom_query2.dataAttr)(invalid),
"data-required": (0, import_dom_query2.dataAttr)(required),
"data-focus": (0, import_dom_query2.dataAttr)(focused),
onClick(event) {
event.preventDefault();
const inputEl = (0, import_dom_query2.query)(dom.getControlEl(scope), "[data-channel=hex]");
inputEl?.focus({ preventScroll: true });
}
});
},
getControlProps() {
return normalize.element({
...import_color_picker.parts.control.attrs,
id: dom.getControlId(scope),
dir: prop("dir"),
"data-disabled": (0, import_dom_query2.dataAttr)(disabled),
"data-readonly": (0, import_dom_query2.dataAttr)(readOnly),
"data-invalid": (0, import_dom_query2.dataAttr)(invalid),
"data-state": open ? "open" : "closed",
"data-focus": (0, import_dom_query2.dataAttr)(focused)
});
},
getTriggerProps() {
return normalize.button({
...import_color_picker.parts.trigger.attrs,
id: dom.getTriggerId(scope),
dir: prop("dir"),
disabled,
"aria-label": `select color. current color is ${valueAsString}`,
"aria-controls": dom.getContentId(scope),
"aria-labelledby": dom.getLabelId(scope),
"aria-haspopup": prop("inline") ? void 0 : "dialog",
"data-disabled": (0, import_dom_query2.dataAttr)(disabled),
"data-readonly": (0, import_dom_query2.dataAttr)(readOnly),
"data-invalid": (0, import_dom_query2.dataAttr)(invalid),
"data-placement": currentPlacement,
"data-side": currentPlacementSide,
"aria-expanded": open,
"data-state": open ? "open" : "closed",
"data-focus": (0, import_dom_query2.dataAttr)(focused),
type: "button",
onClick() {
if (!interactive) return;
send({ type: "TRIGGER.CLICK" });
},
onBlur() {
if (!interactive) return;
send({ type: "TRIGGER.BLUR" });
},
style: {
position: "relative"
}
});
},
getPositionerProps() {
return normalize.element({
...import_color_picker.parts.positioner.attrs,
id: dom.getPositionerId(scope),
dir: prop("dir"),
style: popperStyles.floating
});
},
getContentProps() {
return normalize.element({
...import_color_picker.parts.content.attrs,
id: dom.getContentId(scope),
dir: prop("dir"),
role: prop("inline") ? void 0 : "dialog",
tabIndex: -1,
"data-placement": currentPlacement,
"data-side": currentPlacementSide,
"data-state": open ? "open" : "closed",
hidden: !open
});
},
getValueTextProps() {
return normalize.element({
...import_color_picker.parts.valueText.attrs,
dir: prop("dir"),
"data-disabled": (0, import_dom_query2.dataAttr)(disabled),
"data-focus": (0, import_dom_query2.dataAttr)(focused)
});
},
getAreaProps(props = {}) {
const { xChannel, yChannel } = getAreaChannels(props);
const { areaStyles } = (0, import_color_utils.getColorAreaGradient)(areaValue, {
xChannel,
yChannel,
dir: prop("dir")
});
return normalize.element({
...import_color_picker.parts.area.attrs,
id: dom.getAreaId(scope),
role: "group",
"data-invalid": (0, import_dom_query2.dataAttr)(invalid),
"data-disabled": (0, import_dom_query2.dataAttr)(disabled),
"data-readonly": (0, import_dom_query2.dataAttr)(readOnly),
onPointerDown(event) {
if (!interactive) return;
if (!(0, import_dom_query.isLeftClick)(event)) return;
if ((0, import_dom_query.isModifierKey)(event)) return;
const point = (0, import_dom_query.getEventPoint)(event);
const channel = { xChannel, yChannel };
send({ type: "AREA.POINTER_DOWN", point, channel, id: "area" });
event.preventDefault();
},
style: {
position: "relative",
touchAction: "none",
forcedColorAdjust: "none",
...areaStyles
}
});
},
getAreaBackgroundProps(props = {}) {
const { xChannel, yChannel } = getAreaChannels(props);
const { areaGradientStyles } = (0, import_color_utils.getColorAreaGradient)(areaValue, {
xChannel,
yChannel,
dir: prop("dir")
});
return normalize.element({
...import_color_picker.parts.areaBackground.attrs,
id: dom.getAreaGradientId(scope),
"data-invalid": (0, import_dom_query2.dataAttr)(invalid),
"data-disabled": (0, import_dom_query2.dataAttr)(disabled),
"data-readonly": (0, import_dom_query2.dataAttr)(readOnly),
style: {
position: "relative",
touchAction: "none",
forcedColorAdjust: "none",
...areaGradientStyles
}
});
},
getAreaThumbProps(props = {}) {
const { xChannel, yChannel } = getAreaChannels(props);
const channel = { xChannel, yChannel };
const xPercent = areaValue.getChannelValuePercent(xChannel);
const yPercent = 1 - areaValue.getChannelValuePercent(yChannel);
const isRtl = prop("dir") === "rtl";
const finalXPercent = isRtl ? 1 - xPercent : xPercent;
const xValue = areaValue.getChannelValue(xChannel);
const yValue = areaValue.getChannelValue(yChannel);
const color = areaValue.withChannelValue("alpha", 1).toString("css");
return normalize.element({
...import_color_picker.parts.areaThumb.attrs,
id: dom.getAreaThumbId(scope),
dir: prop("dir"),
tabIndex: disabled ? void 0 : 0,
"data-disabled": (0, import_dom_query2.dataAttr)(disabled),
"data-invalid": (0, import_dom_query2.dataAttr)(invalid),
"data-readonly": (0, import_dom_query2.dataAttr)(readOnly),
role: "slider",
"aria-valuemin": 0,
"aria-valuemax": 100,
"aria-valuenow": xValue,
"aria-label": `${xChannel} and ${yChannel}`,
"aria-roledescription": "2d slider",
"aria-valuetext": `${xChannel} ${xValue}, ${yChannel} ${yValue}`,
style: {
position: "absolute",
left: `${finalXPercent * 100}%`,
top: `${yPercent * 100}%`,
transform: "translate(-50%, -50%)",
touchAction: "none",
forcedColorAdjust: "none",
"--color": color,
background: color
},
onFocus() {
if (!interactive) return;
send({ type: "AREA.FOCUS", id: "area", channel });
},
onKeyDown(event) {
if (event.defaultPrevented) return;
if (!interactive) return;
const step = (0, import_dom_query.getEventStep)(event);
const keyMap = {
ArrowUp() {
send({ type: "AREA.ARROW_UP", channel, step });
},
ArrowDown() {
send({ type: "AREA.ARROW_DOWN", channel, step });
},
ArrowLeft() {
send({ type: "AREA.ARROW_LEFT", channel, step });
},
ArrowRight() {
send({ type: "AREA.ARROW_RIGHT", channel, step });
},
PageUp() {
send({ type: "AREA.PAGE_UP", channel, step });
},
PageDown() {
send({ type: "AREA.PAGE_DOWN", channel, step });
},
Escape(event2) {
event2.stopPropagation();
}
};
const exec = keyMap[(0, import_dom_query.getEventKey)(event, {
dir: prop("dir")
})];
if (exec) {
exec(event);
event.preventDefault();
}
}
});
},
getTransparencyGridProps(props = {}) {
const { size = "12px" } = props;
return normalize.element({
...import_color_picker.parts.transparencyGrid.attrs,
style: {
"--size": size,
width: "100%",
height: "100%",
position: "absolute",
backgroundColor: "#fff",
backgroundImage: "conic-gradient(#eeeeee 0 25%, transparent 0 50%, #eeeeee 0 75%, transparent 0)",
backgroundSize: "var(--size) var(--size)",
inset: "0px",
zIndex: "auto",
pointerEvents: "none"
}
});
},
getChannelSliderProps(props) {
const { orientation = "horizontal", channel, format: format2 } = props;
return normalize.element({
...import_color_picker.parts.channelSlider.attrs,
"data-channel": channel,
"data-orientation": orientation,
role: "presentation",
onPointerDown(event) {
if (!interactive) return;
if (!(0, import_dom_query.isLeftClick)(event)) return;
if ((0, import_dom_query.isModifierKey)(event)) return;
const point = (0, import_dom_query.getEventPoint)(event);
send({ type: "CHANNEL_SLIDER.POINTER_DOWN", channel, format: format2, point, id: channel, orientation });
event.preventDefault();
},
style: {
position: "relative",
touchAction: "none"
}
});
},
getChannelSliderTrackProps(props) {
const { orientation = "horizontal", channel, format: format2 } = props;
const normalizedValue = format2 ? value.toFormat(format2) : areaValue;
return normalize.element({
...import_color_picker.parts.channelSliderTrack.attrs,
id: dom.getChannelSliderTrackId(scope, channel),
role: "group",
"data-channel": channel,
"data-orientation": orientation,
style: {
position: "relative",
forcedColorAdjust: "none",
backgroundImage: (0, import_get_slider_background.getSliderBackground)({
orientation,
channel,
dir: prop("dir"),
value: normalizedValue
})
}
});
},
getChannelSliderLabelProps(props) {
const { channel } = props;
return normalize.element({
...import_color_picker.parts.channelSliderLabel.attrs,
"data-channel": channel,
onClick(event) {
if (!interactive) return;
event.preventDefault();
const thumbId = dom.getChannelSliderThumbId(scope, channel);
scope.getById(thumbId)?.focus({ preventScroll: true });
},
style: {
userSelect: "none",
WebkitUserSelect: "none"
}
});
},
getChannelSliderValueTextProps(props) {
return normalize.element({
...import_color_picker.parts.channelSliderValueText.attrs,
"data-channel": props.channel
});
},
getChannelSliderThumbProps(props) {
const { orientation = "horizontal", channel, format: format2 } = props;
const normalizedValue = format2 ? value.toFormat(format2) : areaValue;
const channelRange = normalizedValue.getChannelRange(channel);
const channelValue = normalizedValue.getChannelValue(channel);
const offset = (channelValue - channelRange.minValue) / (channelRange.maxValue - channelRange.minValue);
const isRtl = prop("dir") === "rtl";
const finalOffset = orientation === "horizontal" && isRtl ? 1 - offset : offset;
const placementStyles = orientation === "horizontal" ? { left: `${finalOffset * 100}%`, top: "50%" } : { top: `${offset * 100}%`, left: "50%" };
return normalize.element({
...import_color_picker.parts.channelSliderThumb.attrs,
id: dom.getChannelSliderThumbId(scope, channel),
role: "slider",
"aria-label": channel,
tabIndex: disabled ? void 0 : 0,
"data-channel": channel,
"data-disabled": (0, import_dom_query2.dataAttr)(disabled),
"data-orientation": orientation,
"aria-disabled": (0, import_dom_query2.dataAttr)(disabled),
"aria-orientation": orientation,
"aria-valuemax": channelRange.maxValue,
"aria-valuemin": channelRange.minValue,
"aria-valuenow": channelValue,
"aria-valuetext": `${channel} ${channelValue}`,
style: {
forcedColorAdjust: "none",
position: "absolute",
background: (0, import_get_channel_display_color.getChannelDisplayColor)(areaValue, channel).toString("css"),
...placementStyles
},
onFocus() {
if (!interactive) return;
send({ type: "CHANNEL_SLIDER.FOCUS", channel });
},
onKeyDown(event) {
if (event.defaultPrevented) return;
if (!interactive) return;
const step = (0, import_dom_query.getEventStep)(event) * channelRange.step;
const keyMap = {
ArrowUp() {
send({ type: "CHANNEL_SLIDER.ARROW_UP", channel, step });
},
ArrowDown() {
send({ type: "CHANNEL_SLIDER.ARROW_DOWN", channel, step });
},
ArrowLeft() {
send({ type: "CHANNEL_SLIDER.ARROW_LEFT", channel, step });
},
ArrowRight() {
send({ type: "CHANNEL_SLIDER.ARROW_RIGHT", channel, step });
},
PageUp() {
send({ type: "CHANNEL_SLIDER.PAGE_UP", channel });
},
PageDown() {
send({ type: "CHANNEL_SLIDER.PAGE_DOWN", channel });
},
Home() {
send({ type: "CHANNEL_SLIDER.HOME", channel });
},
End() {
send({ type: "CHANNEL_SLIDER.END", channel });
},
Escape(event2) {
event2.stopPropagation();
}
};
const exec = keyMap[(0, import_dom_query.getEventKey)(event, {
dir: prop("dir")
})];
if (exec) {
exec(event);
event.preventDefault();
}
}
});
},
getChannelInputProps(props) {
const { channel } = props;
const isTextField = channel === "hex" || channel === "css";
const channelRange = (0, import_get_channel_input_value.getChannelRange)(value, channel);
return normalize.input({
...import_color_picker.parts.channelInput.attrs,
dir: prop("dir"),
type: isTextField ? "text" : "number",
"data-channel": channel,
"aria-label": channel,
spellCheck: false,
autoComplete: "off",
disabled,
"data-disabled": (0, import_dom_query2.dataAttr)(disabled),
"data-invalid": (0, import_dom_query2.dataAttr)(invalid),
"data-readonly": (0, import_dom_query2.dataAttr)(readOnly),
readOnly,
defaultValue: (0, import_get_channel_input_value.getChannelValue)(value, channel),
min: channelRange?.minValue,
max: channelRange?.maxValue,
step: channelRange?.step,
onBeforeInput(event) {
if (isTextField || !interactive) return;
const value2 = event.currentTarget.value;
if (value2.match(/[^0-9.]/g)) {
event.preventDefault();
}
},
onFocus(event) {
if (!interactive) return;
send({ type: "CHANNEL_INPUT.FOCUS", channel });
event.currentTarget.select();
},
onBlur(event) {
if (!interactive) return;
const value2 = isTextField ? event.currentTarget.value : event.currentTarget.valueAsNumber;
send({ type: "CHANNEL_INPUT.BLUR", channel, value: value2, isTextField });
},
onKeyDown(event) {
if (event.defaultPrevented) return;
if (!interactive) return;
if (event.key === "Enter") {
const value2 = isTextField ? event.currentTarget.value : event.currentTarget.valueAsNumber;
send({ type: "CHANNEL_INPUT.CHANGE", channel, value: value2, isTextField });
event.preventDefault();
}
},
style: {
appearance: "none",
WebkitAppearance: "none",
MozAppearance: "textfield"
}
});
},
getHiddenInputProps() {
return normalize.input({
type: "text",
disabled,
name: prop("name"),
tabIndex: -1,
readOnly,
required,
id: dom.getHiddenInputId(scope),
style: import_dom_query2.visuallyHiddenStyle,
defaultValue: valueAsString
});
},
getEyeDropperTriggerProps() {
return normalize.button({
...import_color_picker.parts.eyeDropperTrigger.attrs,
type: "button",
dir: prop("dir"),
disabled,
"data-disabled": (0, import_dom_query2.dataAttr)(disabled),
"data-invalid": (0, import_dom_query2.dataAttr)(invalid),
"data-readonly": (0, import_dom_query2.dataAttr)(readOnly),
"aria-label": "Pick a color from the screen",
onClick() {
if (!interactive) return;
send({ type: "EYEDROPPER.CLICK" });
}
});
},
getSwatchGroupProps() {
return normalize.element({
...import_color_picker.parts.swatchGroup.attrs,
role: "group"
});
},
getSwatchTriggerState,
getSwatchTriggerProps(props) {
const swatchState = getSwatchTriggerState(props);
return normalize.button({
...import_color_picker.parts.swatchTrigger.attrs,
disabled: swatchState.disabled,
dir: prop("dir"),
type: "button",
"aria-label": `select ${swatchState.valueAsString} as the color`,
"data-state": swatchState.checked ? "checked" : "unchecked",
"data-value": swatchState.valueAsString,
"data-disabled": (0, import_dom_query2.dataAttr)(swatchState.disabled),
onClick() {
if (swatchState.disabled) return;
send({ type: "SWATCH_TRIGGER.CLICK", value: swatchState.value });
},
style: {
"--color": swatchState.valueAsString,
position: "relative"
}
});
},
getSwatchIndicatorProps(props) {
const swatchState = getSwatchTriggerState(props);
return normalize.element({
...import_color_picker.parts.swatchIndicator.attrs,
dir: prop("dir"),
hidden: !swatchState.checked
});
},
getSwatchProps(props) {
const { respectAlpha = true } = props;
const swatchState = getSwatchTriggerState(props);
const color = swatchState.value.toString(respectAlpha ? "css" : "hex");
return normalize.element({
...import_color_picker.parts.swatch.attrs,
dir: prop("dir"),
"data-state": swatchState.checked ? "checked" : "unchecked",
"data-value": swatchState.valueAsString,
style: {
"--color": color,
position: "relative",
background: color
}
});
},
getFormatTriggerProps() {
return normalize.button({
...import_color_picker.parts.formatTrigger.attrs,
dir: prop("dir"),
type: "button",
"aria-label": `change color format to ${getNextFormat(format)}`,
onClick(event) {
if (event.currentTarget.disabled) return;
const nextFormat = getNextFormat(format);
send({ type: "FORMAT.SET", format: nextFormat, src: "format-trigger" });
}
});
},
getFormatSelectProps() {
return normalize.select({
...import_color_picker.parts.formatSelect.attrs,
"aria-label": "change color format",
dir: prop("dir"),
defaultValue: prop("format"),
disabled,
onChange(event) {
const format2 = assertFormat(event.currentTarget.value);
send({ type: "FORMAT.SET", format: format2, src: "format-select" });
}
});
}
};
}
var formats = ["hsba", "hsla", "rgba"];
var formatRegex = new RegExp(`^(${formats.join("|")})$`);
function getNextFormat(format) {
const index = formats.indexOf(format);
return formats[index + 1] ?? formats[0];
}
function assertFormat(format) {
if (formatRegex.test(format)) return format;
throw new Error(`Unsupported color format: ${format}`);
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
connect
});