fix : color picker absoulute bug

This commit is contained in:
2026-09-15 18:06:24 +07:00 Unverified
parent 45f11466fe
commit 0f48989e9c
2 changed files with 84 additions and 58 deletions
@@ -88,6 +88,7 @@ import {
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import { import {
ColorPicker, ColorPicker,
ColorPickerEyeDropper,
ColorPickerHue, ColorPickerHue,
ColorPickerSelection, ColorPickerSelection,
} from "@/components/ui/color-picker"; } from "@/components/ui/color-picker";
@@ -571,7 +572,7 @@ function ColorField({
}) { }) {
const invalid = !normalizeHex(value); const invalid = !normalizeHex(value);
const handlePickerChange = useCallback( const handlePickerChange = useCallback(
(color: number[] | Record<string, number>) => { (color: unknown) => {
if (!Array.isArray(color)) return; if (!Array.isArray(color)) return;
const hex = color const hex = color
.slice(0, 3) .slice(0, 3)
@@ -610,6 +611,7 @@ function ColorField({
> >
<ColorPickerSelection className="h-32" /> <ColorPickerSelection className="h-32" />
<ColorPickerHue /> <ColorPickerHue />
<ColorPickerEyeDropper aria-label="ดูดสีจากหน้าจอ" />
</ColorPicker> </ColorPicker>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
+81 -57
View File
@@ -51,7 +51,7 @@ export const useColorPicker = () => {
return context return context
} }
export type ColorPickerProps = HTMLAttributes<HTMLDivElement> & { export type ColorPickerProps = Omit<HTMLAttributes<HTMLDivElement>, "onChange"> & {
value?: Parameters<typeof Color>[0] value?: Parameters<typeof Color>[0]
defaultValue?: Parameters<typeof Color>[0] defaultValue?: Parameters<typeof Color>[0]
onChange?: (value: Parameters<typeof Color.rgb>[0]) => void onChange?: (value: Parameters<typeof Color.rgb>[0]) => void
@@ -64,41 +64,68 @@ export const ColorPicker = ({
className, className,
...props ...props
}: ColorPickerProps) => { }: ColorPickerProps) => {
const selectedColor = Color(value) const selectedColor = Color(value ?? defaultValue)
const defaultColor = Color(defaultValue)
const [hue, setHue] = useState(selectedColor.hue() || defaultColor.hue() || 0) const [hue, setHueState] = useState(selectedColor.hue() || 0)
const [saturation, setSaturation] = useState( const [saturation, setSaturationState] = useState(selectedColor.saturationl())
selectedColor.saturationl() || defaultColor.saturationl() || 100, const [lightness, setLightnessState] = useState(selectedColor.lightness())
) const [alpha, setAlphaState] = useState(selectedColor.alpha() * 100)
const [lightness, setLightness] = useState(
selectedColor.lightness() || defaultColor.lightness() || 50,
)
const [alpha, setAlpha] = useState(selectedColor.alpha() * 100 || defaultColor.alpha() * 100)
const [mode, setMode] = useState("hex") const [mode, setMode] = useState("hex")
const channelsRef = useRef({ hue, saturation, lightness, alpha })
const updateColor = useCallback(
(patch: Partial<typeof channelsRef.current>) => {
const next = { ...channelsRef.current, ...patch }
channelsRef.current = next
setHueState(next.hue)
setSaturationState(next.saturation)
setLightnessState(next.lightness)
setAlphaState(next.alpha)
if (onChange) {
const rgba = Color.hsl(next.hue, next.saturation, next.lightness)
.alpha(next.alpha / 100)
.rgb()
.array()
onChange([rgba[0], rgba[1], rgba[2], next.alpha / 100])
}
},
[onChange],
)
const setHue = useCallback((next: number) => updateColor({ hue: next }), [updateColor])
const setSaturation = useCallback(
(next: number) => updateColor({ saturation: next }),
[updateColor],
)
const setLightness = useCallback(
(next: number) => updateColor({ lightness: next }),
[updateColor],
)
const setAlpha = useCallback(
(next: number) => updateColor({ alpha: next }),
[updateColor],
)
// Update color when controlled value changes // Update color when controlled value changes
useEffect(() => { useEffect(() => {
if (value) { if (value) {
const color = Color(value).hsl() const color = Color(value).hsl()
const next = {
hue: color.hue() || 0,
saturation: color.saturationl(),
lightness: color.lightness(),
alpha: color.alpha() * 100,
}
setHue(color.hue() || 0) channelsRef.current = next
setSaturation(color.saturationl()) setHueState(next.hue)
setLightness(color.lightness()) setSaturationState(next.saturation)
setAlpha(color.alpha() * 100) setLightnessState(next.lightness)
setAlphaState(next.alpha)
} }
}, [value]) }, [value])
// Notify parent of changes
useEffect(() => {
if (onChange) {
const color = Color.hsl(hue, saturation, lightness).alpha(alpha / 100)
const rgba = color.rgb().array()
onChange([rgba[0], rgba[1], rgba[2], alpha / 100])
}
}, [hue, saturation, lightness, alpha, onChange])
return ( return (
<ColorPickerContext.Provider <ColorPickerContext.Provider
value={{ value={{
@@ -124,9 +151,10 @@ export type ColorPickerSelectionProps = HTMLAttributes<HTMLDivElement>
export const ColorPickerSelection = memo(({ className, ...props }: ColorPickerSelectionProps) => { export const ColorPickerSelection = memo(({ className, ...props }: ColorPickerSelectionProps) => {
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const [isDragging, setIsDragging] = useState(false) const [isDragging, setIsDragging] = useState(false)
const [positionX, setPositionX] = useState(0) const { hue, saturation, lightness, setSaturation, setLightness } = useColorPicker()
const [positionY, setPositionY] = useState(0) const positionX = saturation / 100
const { hue, setSaturation, setLightness } = useColorPicker() const topLightness = 100 - positionX * 50
const positionY = Math.max(0, Math.min(1, 1 - lightness / topLightness))
const backgroundGradient = useMemo(() => { const backgroundGradient = useMemo(() => {
return `linear-gradient(0deg, rgba(0,0,0,1), rgba(0,0,0,0)), return `linear-gradient(0deg, rgba(0,0,0,1), rgba(0,0,0,0)),
@@ -134,47 +162,38 @@ export const ColorPickerSelection = memo(({ className, ...props }: ColorPickerSe
hsl(${hue}, 100%, 50%)` hsl(${hue}, 100%, 50%)`
}, [hue]) }, [hue])
const handlePointerMove = useCallback( const updateFromPointer = useCallback(
(event: PointerEvent) => { (event: { clientX: number; clientY: number }) => {
if (!(isDragging && containerRef.current)) { if (!containerRef.current) return
return
}
const rect = containerRef.current.getBoundingClientRect() const rect = containerRef.current.getBoundingClientRect()
const x = Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width)) const x = Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width))
const y = Math.max(0, Math.min(1, (event.clientY - rect.top) / rect.height)) const y = Math.max(0, Math.min(1, (event.clientY - rect.top) / rect.height))
setPositionX(x)
setPositionY(y)
setSaturation(x * 100) setSaturation(x * 100)
const topLightness = x < 0.01 ? 100 : 50 + 50 * (1 - x) const nextTopLightness = 100 - x * 50
const lightness = topLightness * (1 - y) const nextLightness = nextTopLightness * (1 - y)
setLightness(lightness) setLightness(nextLightness)
}, },
[isDragging, setSaturation, setLightness], [setSaturation, setLightness],
) )
useEffect(() => {
const handlePointerUp = () => setIsDragging(false)
if (isDragging) {
window.addEventListener("pointermove", handlePointerMove)
window.addEventListener("pointerup", handlePointerUp)
}
return () => {
window.removeEventListener("pointermove", handlePointerMove)
window.removeEventListener("pointerup", handlePointerUp)
}
}, [isDragging, handlePointerMove])
return ( return (
<div <div
className={cn("relative size-full cursor-crosshair rounded", className)} className={cn("relative size-full cursor-crosshair rounded", className)}
onPointerDown={e => { onPointerDown={event => {
e.preventDefault() event.preventDefault()
event.currentTarget.setPointerCapture(event.pointerId)
setIsDragging(true) setIsDragging(true)
handlePointerMove(e.nativeEvent) updateFromPointer(event)
}} }}
onPointerMove={event => {
if (isDragging) updateFromPointer(event)
}}
onPointerUp={event => {
event.currentTarget.releasePointerCapture(event.pointerId)
setIsDragging(false)
}}
onPointerCancel={() => setIsDragging(false)}
ref={containerRef} ref={containerRef}
style={{ style={{
background: backgroundGradient, background: backgroundGradient,
@@ -290,7 +309,12 @@ export const ColorPickerOutput = ({ className, ...props }: ColorPickerOutputProp
const { mode, setMode } = useColorPicker() const { mode, setMode } = useColorPicker()
return ( return (
<Select onValueChange={setMode} value={mode}> <Select
onValueChange={value => {
if (value) setMode(value)
}}
value={mode}
>
<SelectTrigger className="h-8 w-20 shrink-0 text-xs" {...(props as any)}> <SelectTrigger className="h-8 w-20 shrink-0 text-xs" {...(props as any)}>
<SelectValue placeholder="Mode" /> <SelectValue placeholder="Mode" />
</SelectTrigger> </SelectTrigger>