Skip to content

Commit

Permalink
fix: color picker channel handling
Browse files Browse the repository at this point in the history
  • Loading branch information
segunadebayo committed Sep 20, 2023
1 parent 1764822 commit adaf344
Show file tree
Hide file tree
Showing 16 changed files with 161 additions and 89 deletions.
8 changes: 8 additions & 0 deletions .changeset/fluffy-cups-live.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@zag-js/color-picker": patch
"@zag-js/color-utils": patch
---

- Fix issue where color area changes format when you type custom hex values
- Fix issue where alpha channel input resets to `1` after blurring hex channel input
- Add support for entering native color names (e.g. `red`, `blue`, `green`, etc.)
15 changes: 2 additions & 13 deletions .xstate/color-picker.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ const {
const fetchMachine = createMachine({
id: "color-picker",
initial: "idle",
context: {
"isTextField": false
},
context: {},
on: {
"VALUE.SET": {
actions: ["setValue"]
Expand Down Expand Up @@ -115,13 +113,6 @@ const fetchMachine = createMachine({
"CHANNEL_INPUT.CHANGE": {
actions: ["setChannelColorFromInput"]
},
"CHANNEL_INPUT.BLUR": [{
cond: "isTextField",
target: "idle",
actions: ["setChannelColorFromInput"]
}, {
target: "idle"
}],
"CHANNEL_SLIDER.BLUR": {
target: "idle"
},
Expand Down Expand Up @@ -159,7 +150,5 @@ const fetchMachine = createMachine({
};
})
},
guards: {
"isTextField": ctx => ctx["isTextField"]
}
guards: {}
});
37 changes: 23 additions & 14 deletions packages/machines/color-picker/src/color-picker.connect.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { normalizeColor, type Color, type ColorChannel, type ColorFormat } from "@zag-js/color-utils"
import {
getColorAreaGradient,
normalizeColor,
type Color,
type ColorChannel,
type ColorFormat,
} from "@zag-js/color-utils"
import {
getEventKey,
getEventPoint,
Expand All @@ -8,7 +14,7 @@ import {
isModifiedEvent,
type EventKeyMap,
} from "@zag-js/dom-event"
import { dataAttr } from "@zag-js/dom-query"
import { dataAttr, raf } from "@zag-js/dom-query"
import type { NormalizeProps, PropTypes } from "@zag-js/types"
import { visuallyHiddenStyle } from "@zag-js/visually-hidden"
import { parts } from "./color-picker.anatomy"
Expand All @@ -25,7 +31,6 @@ import type {
import { getChannelDetails } from "./utils/get-channel-details"
import { getChannelDisplayColor } from "./utils/get-channel-display-color"
import { getChannelInputRange, getChannelInputValue } from "./utils/get-channel-input-value"
import { getColorAreaGradient } from "./utils/get-color-area-gradient"
import { getSliderBgImage } from "./utils/get-slider-background"

export function connect<T extends PropTypes>(state: State, send: Send, normalize: NormalizeProps<T>): MachineApi<T> {
Expand Down Expand Up @@ -66,11 +71,16 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
contentProps: normalize.element({
...parts.content.attrs,
id: dom.getContentId(state.context),
dir: state.context.dir,
}),

getAreaProps(props: ColorAreaProps) {
const { xChannel, yChannel } = props
const { areaStyles } = getColorAreaGradient(state.context, xChannel, yChannel)
const { areaStyles } = getColorAreaGradient(state.context.valueAsColor, {
xChannel,
yChannel,
dir: state.context.dir,
})

return normalize.element({
...parts.area.attrs,
Expand Down Expand Up @@ -98,7 +108,11 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize

getAreaGradientProps(props: ColorAreaProps) {
const { xChannel, yChannel } = props
const { areaGradientStyles } = getColorAreaGradient(state.context, xChannel, yChannel)
const { areaGradientStyles } = getColorAreaGradient(valueAsColor, {
xChannel,
yChannel,
dir: state.context.dir,
})

return normalize.element({
...parts.areaGradient.attrs,
Expand Down Expand Up @@ -325,20 +339,15 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize
min: range?.minValue,
max: range?.maxValue,
step: range?.step,
onFocus() {
onFocus(event) {
send({ type: "CHANNEL_INPUT.FOCUS", channel })
},
onChange(event) {
if (isTextField) return
const value = event.currentTarget.value
send({ type: "CHANNEL_INPUT.CHANGE", channel, value, isTextField })
raf(() => event.target.select())
},
onBlur(event) {
const value = event.currentTarget.value
send({ type: "CHANNEL_INPUT.BLUR", channel, value, isTextField })
send({ type: "CHANNEL_INPUT.CHANGE", channel, value, isTextField })
},
onKeyDown(event) {
if (!isTextField) return
if (event.key === "Enter") {
const value = event.currentTarget.value
send({ type: "CHANNEL_INPUT.CHANGE", channel, value, isTextField })
Expand Down Expand Up @@ -403,7 +412,7 @@ export function connect<T extends PropTypes>(state: State, send: Send, normalize

getSwatchProps(props: ColorSwatchProps) {
const { value, readOnly } = props
const color = normalizeColor(value).toFormat(valueAsColor.getColorSpace())
const color = normalizeColor(value).toFormat(valueAsColor.getColorFormat())
return normalize.element({
...parts.swatch.attrs,
onClick() {
Expand Down
63 changes: 37 additions & 26 deletions packages/machines/color-picker/src/color-picker.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,16 @@ import { raf } from "@zag-js/dom-query"
import { trackFormControl } from "@zag-js/form-utils"
import { clampValue, getPercentValue, snapValueToStep } from "@zag-js/numeric-range"
import { disableTextSelection } from "@zag-js/text-selection"
import { compact } from "@zag-js/utils"
import { compact, tryCatch } from "@zag-js/utils"
import { dom } from "./color-picker.dom"
import type { ExtendedColorChannel, MachineContext, MachineState, UserDefinedContext } from "./color-picker.types"
import type {
ColorFormat,
ColorType,
ExtendedColorChannel,
MachineContext,
MachineState,
UserDefinedContext,
} from "./color-picker.types"
import { getChannelDetails } from "./utils/get-channel-details"
import { getChannelInputValue } from "./utils/get-channel-input-value"

Expand All @@ -32,7 +39,7 @@ export function machine(userContext: UserDefinedContext) {
isRtl: (ctx) => ctx.dir === "rtl",
isDisabled: (ctx) => !!ctx.disabled || ctx.fieldsetDisabled,
isInteractive: (ctx) => !(ctx.isDisabled || ctx.readOnly),
valueAsColor: (ctx) => parseColor(ctx.value),
valueAsColor: (ctx) => parseColor(ctx.value) as Color,
},

on: {
Expand Down Expand Up @@ -137,14 +144,6 @@ export function machine(userContext: UserDefinedContext) {
"CHANNEL_INPUT.CHANGE": {
actions: ["setChannelColorFromInput"],
},
"CHANNEL_INPUT.BLUR": [
{
guard: "isTextField",
target: "idle",
actions: ["setChannelColorFromInput"],
},
{ target: "idle" },
],
"CHANNEL_SLIDER.BLUR": {
target: "idle",
},
Expand Down Expand Up @@ -177,9 +176,6 @@ export function machine(userContext: UserDefinedContext) {
},
},
{
guards: {
isTextField: (_ctx, evt) => !!evt.isTextField,
},
activities: {
trackFormControl(ctx, _evt, { send, initialContext }) {
const inputEl = dom.getHiddenInputEl(ctx)
Expand Down Expand Up @@ -217,8 +213,8 @@ export function machine(userContext: UserDefinedContext) {
picker
.open()
.then(({ sRGBHex }: { sRGBHex: string }) => {
const format = ctx.valueAsColor.getColorSpace()
const color = parseColor(sRGBHex).toFormat(format)
const format = ctx.valueAsColor.getColorFormat()
const color = parseColor(sRGBHex).toFormat(format) as Color
set.value(ctx, color)
ctx.onValueChangeEnd?.({ value: ctx.value, valueAsColor: color })
})
Expand Down Expand Up @@ -286,20 +282,35 @@ export function machine(userContext: UserDefinedContext) {
},
setChannelColorFromInput(ctx, evt) {
const { channel, isTextField, value } = evt
try {
const format = ctx.valueAsColor.getColorSpace()

const newColor = isTextField
? parseColor(value).toFormat(format)
: ctx.valueAsColor.withChannelValue(channel, value)

// handle alpha channel
if (channel === "alpha") {
const newColor = ctx.valueAsColor.withChannelValue("alpha", value)
set.value(ctx, newColor)
//
} catch {
// reset input value
return
}

// handle other text channels
if (isTextField) {
const format: ColorFormat = "hsl"
const currentAlpha = ctx.valueAsColor.getChannelValue("alpha")

const newColor = tryCatch(
() => parseColor(value).toFormat(format).withChannelValue("alpha", currentAlpha),
() => ctx.valueAsColor,
)

// set channel input value immediately (in event user types native css color, we need to convert it to the current channel format)
const inputEl = dom.getChannelInputEl(ctx, channel)
dom.setValue(inputEl, getChannelInputValue(ctx.valueAsColor, channel))

set.value(ctx, newColor)
return
}

// handle other channels
const newColor = ctx.valueAsColor.withChannelValue(channel, value)
set.value(ctx, newColor)
},

incrementChannel(ctx, evt) {
Expand Down Expand Up @@ -384,7 +395,7 @@ const invoke = {
}

const set = {
value(ctx: MachineContext, color: Color) {
value(ctx: MachineContext, color: Color | ColorType) {
if (ctx.valueAsColor.isEqual(color)) return
ctx.value = color.toString("css")
invoke.change(ctx)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Color, ColorChannel } from "@zag-js/color-utils"
import type { Color, ColorChannel, ColorType } from "@zag-js/color-utils"
import { getPercentValue, snapValueToStep } from "@zag-js/numeric-range"

export function getChannelDetails(color: Color, xChannel: ColorChannel, yChannel: ColorChannel) {
Expand Down Expand Up @@ -42,7 +42,7 @@ export function getChannelDetails(color: Color, xChannel: ColorChannel, yChannel
let newXValue = getPercentValue(x, minValueX, maxValueX, stepX)
let newYValue = getPercentValue(1 - y, minValueY, maxValueY, stepY)

let newColor: Color | undefined
let newColor: ColorType | undefined

if (newXValue !== xValue) {
newXValue = snapValueToStep(newXValue, minValueX, maxValueX, stepX)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ export function getChannelInputValue(color: Color, channel: ExtendedColorChannel
return color.toString("hex")
case "css":
return color.toString("css")
case "hue":
case "saturation":
case "lightness":
return color.toFormat("hsl").getChannelValue("lightness").toString()
case "brightness":
return color.toFormat("hsb").getChannelValue("brightness").toString()
case "red":
case "green":
case "blue":
return color.toFormat("rgb").getChannelValue(channel).toString()
default:
return color.getChannelValue(channel).toString()
}
Expand All @@ -19,6 +29,16 @@ export function getChannelInputRange(color: Color, channel: ExtendedColorChannel
case "hex":
case "css":
return undefined
case "hue":
case "saturation":
case "lightness":
return color.toFormat("hsl").getChannelRange(channel)
case "brightness":
return color.toFormat("hsb").getChannelRange(channel)
case "red":
case "green":
case "blue":
return color.toFormat("rgb").getChannelRange(channel)
default:
return color.getChannelRange(channel)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,43 @@
import type { ColorChannel } from "@zag-js/color-utils"
import type { Style } from "@zag-js/types"
import type { MachineContext } from "../color-picker.types"
import type { Color } from "./color"
import {
generateHSB_B,
generateRGB_R,
generateRGB_G,
generateRGB_B,
generateHSL_H,
generateHSB_H,
generateHSL_S,
generateHSB_S,
generateHSL_H,
generateHSB_B,
generateHSL_L,
generateHSL_S,
generateRGB_B,
generateRGB_G,
generateRGB_R,
} from "./generate-format-background"
} from "./color-format-gradient"
import type { ColorChannel } from "./types"

interface GradientOptions {
xChannel: ColorChannel
yChannel: ColorChannel
dir: "rtl" | "ltr"
}

interface GradientStyles {
areaStyles: Record<string, string>
areaGradientStyles: Record<string, string>
}

export function getColorAreaGradient(ctx: MachineContext, xChannel: ColorChannel, yChannel: ColorChannel) {
const value = ctx.valueAsColor
export function getColorAreaGradient(color: Color, options: GradientOptions): GradientStyles {
const { xChannel, yChannel, dir: dirProp } = options

const { zChannel } = value.getColorSpaceAxes({ xChannel, yChannel })
const zValue = value.getChannelValue(zChannel)
const { zChannel } = color.getColorSpaceAxes({ xChannel, yChannel })
const zValue = color.getChannelValue(zChannel)

const { minValue: zMin, maxValue: zMax } = value.getChannelRange(zChannel)
const orientation: [string, string] = ["top", ctx.dir === "rtl" ? "left" : "right"]
const { minValue: zMin, maxValue: zMax } = color.getChannelRange(zChannel)
const orientation: [string, string] = ["top", dirProp === "rtl" ? "left" : "right"]

let dir = false
let background = { areaStyles: {} as Style, areaGradientStyles: {} as Style }

let background = { areaStyles: {}, areaGradientStyles: {} }

let alphaValue = (zValue - zMin) / (zMax - zMin)
let isHSL = value.getColorSpace() === "hsl"
let isHSL = color.getColorFormat() === "hsl"

switch (zChannel) {
case "red": {
Expand Down
6 changes: 5 additions & 1 deletion packages/utilities/color-utils/src/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ export abstract class Color implements ColorType {
abstract toString(format: ColorFormat | "css"): string
abstract clone(): ColorType
abstract getChannelRange(channel: ColorChannel): ColorChannelRange
abstract getColorSpace(): ColorFormat
abstract getColorFormat(): ColorFormat
abstract getColorChannels(): [ColorChannel, ColorChannel, ColorChannel]

hasColorChannel(channel: ColorChannel): boolean {
return this.getColorChannels().includes(channel)
}

toHexInt(): number {
return this.toFormat("rgb").toHexInt()
}
Expand Down
2 changes: 1 addition & 1 deletion packages/utilities/color-utils/src/hsb-color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export class HSBColor extends Color {
}
}

getColorSpace(): ColorFormat {
getColorFormat(): ColorFormat {
return "hsb"
}

Expand Down
2 changes: 1 addition & 1 deletion packages/utilities/color-utils/src/hsl-color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export class HSLColor extends Color {
}
}

getColorSpace(): ColorFormat {
getColorFormat(): ColorFormat {
return "hsl"
}

Expand Down
Loading

0 comments on commit adaf344

Please sign in to comment.