UNPKG

@zag-js/color-picker

Version:

Core logic for the color-picker widget implemented as a state machine

685 lines (683 loc) • 26 kB
"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 });