Wrote space editor component and test.

This commit is contained in:
Jay
2025-08-08 11:17:21 -04:00
parent f47d46f382
commit 0d08d805a3
9 changed files with 310 additions and 16 deletions
@@ -0,0 +1,106 @@
import { useReducer } from "react";
import { Color } from "colorlib";
import { roundTo } from "@/util";
import { colorReducer, createColorActions } from "@hooks/color";
import SpaceEditor from "./SpaceEditor";
const initialState = {
color: Color.from_hex("2edd9d"),
};
function TestWrapper() {
const [state, dispatch] = useReducer(colorReducer, initialState);
const actions = createColorActions(dispatch);
return (
<>
<div
style={{
width: "100%",
height: 300,
display: "flex",
flexDirection: "column",
gap: "20px",
}}
>
<SpaceEditor
space="HCL"
color={state.color.hcl}
actions={actions.hcl}
/>
<SpaceEditor
space="HSV"
color={state.color.hsv}
actions={actions.hsv}
/>
<SpaceEditor
space="RGB"
color={state.color.rgb}
actions={actions.rgb}
/>
</div>
<div style={{ fontFamily: "monospace" }}>
<p data-cy="hcl-value">
HCL ({roundTo(state.color.hcl.h, 0)}, {roundTo(state.color.hcl.c, 2)},{" "}
{roundTo(state.color.hcl.l, 2)})
</p>
<p data-cy="hsv-value">
HSV ({roundTo(state.color.hsv.h, 0)}, {roundTo(state.color.hsv.s, 2)},{" "}
{roundTo(state.color.hsv.v, 2)})
</p>
<p data-cy="rgb-value">
RGB ({roundTo(state.color.rgb.r, 0)}, {roundTo(state.color.rgb.g, 0)},{" "}
{roundTo(state.color.rgb.b, 0)})
</p>
<p data-cy="hex-value">HEX: #{state.color.hex.to_code()}</p>
</div>
</>
);
}
describe("space editor tests", () => {
it("can edit color values", () => {
cy.mount(<TestWrapper />);
// Confirm initial values
cy.dataCy("RGB-editor").within(() => {
cy.dataCy("R-value-input").should("have.value", 46);
cy.dataCy("G-value-input").should("have.value", 221);
cy.dataCy("B-value-input").should("have.value", 157);
});
cy.dataCy("HSV-editor").within(() => {
cy.dataCy("H-value-input").should("have.value", 158);
cy.dataCy("S-value-input").should("have.value", 79);
cy.dataCy("V-value-input").should("have.value", 87);
});
cy.dataCy("HCL-editor").within(() => {
cy.dataCy("H-value-input").should("have.value", 158);
cy.dataCy("C-value-input").should("have.value", 79);
cy.dataCy("L-value-input").should("have.value", 70);
});
cy.dataCy("rgb-value").should("have.text", "RGB (46, 221, 157)");
cy.dataCy("hsv-value").should("have.text", "HSV (158, 0.79, 0.87)");
cy.dataCy("hcl-value").should("have.text", "HCL (158, 0.79, 0.7)");
cy.dataCy("hex-value").should("have.text", "HEX: #2EDD9D");
// Update the color values
cy.wait(50); // ensure render
cy.dataCy("HCL-editor").within(() => {
cy.dataCy("H-slider").click();
cy.dataCy("C-decrement-button").click();
cy.dataCy("L-value-input").type("25");
});
cy.dataCy("rgb-value").should("have.text", "RGB (17, 76, 74)");
cy.dataCy("hsv-value").should("have.text", "HSV (179, 0.78, 0.3)");
cy.dataCy("hcl-value").should("have.text", "HCL (179, 0.78, 0.25)");
cy.dataCy("hex-value").should("have.text", "HEX: #104B4A");
});
});
@@ -0,0 +1,6 @@
.spaceWrapper {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
+159 -3
View File
@@ -1,9 +1,165 @@
import * as colorlib from "colorlib";
import styles from "./SpaceEditor.module.css";
import type {
HCLColorActions,
HSVColorActions,
RGBColorActions,
} from "@hooks/color";
function SpaceEditor({}: {}) {
return;
import styles from "./SpaceEditor.module.css";
import ValueEditor from "./ValueEditor";
type SpaceEditorProps =
| { space: "RGB"; color: colorlib.RGB; actions: RGBColorActions }
| { space: "HSV"; color: colorlib.HSV; actions: HSVColorActions }
| { space: "HCL"; color: colorlib.HCL; actions: HCLColorActions };
function SpaceEditor({ space, color, actions }: SpaceEditorProps) {
switch (space) {
case "RGB":
return <RGBSpaceEditor color={color} actions={actions} />;
case "HSV":
return <HSVSpaceEditor color={color} actions={actions} />;
case "HCL":
return <HCLSpaceEditor color={color} actions={actions} />;
default:
return <></>;
}
}
const COLOR_SPACES = {
RGB: {
symbols: { r: "R", g: "G", b: "B" },
ranges: {
r: { min: 0, max: 255 },
g: { min: 0, max: 255 },
b: { min: 0, max: 255 },
},
},
HSV: {
symbols: { h: "H", s: "S", v: "V" },
ranges: {
h: { min: 0, max: 359 },
s: { min: 0, max: 1 },
v: { min: 0, max: 1 },
},
scales: {
s: 100,
v: 100,
},
},
HCL: {
symbols: { h: "H", c: "C", l: "L" },
ranges: {
h: { min: 0, max: 359 },
c: { min: 0, max: 1 },
l: { min: 0, max: 1 },
},
scales: {
c: 100,
l: 100,
},
},
};
function RGBSpaceEditor({
color,
actions,
}: {
color: colorlib.RGB;
actions: RGBColorActions;
}) {
return (
<div data-cy="RGB-editor" className={styles.spaceWrapper}>
<ValueEditor
componentSymbol={COLOR_SPACES.RGB.symbols.r}
valueRange={COLOR_SPACES.RGB.ranges.r}
value={color.r}
setValue={actions.setR}
/>
<ValueEditor
componentSymbol={COLOR_SPACES.RGB.symbols.g}
valueRange={COLOR_SPACES.RGB.ranges.g}
value={color.g}
setValue={actions.setG}
/>
<ValueEditor
componentSymbol={COLOR_SPACES.RGB.symbols.b}
valueRange={COLOR_SPACES.RGB.ranges.b}
value={color.b}
setValue={actions.setB}
/>
</div>
);
}
function HSVSpaceEditor({
color,
actions,
}: {
color: colorlib.HSV;
actions: HSVColorActions;
}) {
return (
<div data-cy="HSV-editor" className={styles.spaceWrapper}>
<ValueEditor
componentSymbol={COLOR_SPACES.HSV.symbols.h}
valueRange={COLOR_SPACES.HSV.ranges.h}
value={color.h}
setValue={actions.setH}
/>
<ValueEditor
componentSymbol={COLOR_SPACES.HSV.symbols.s}
valueRange={COLOR_SPACES.HSV.ranges.s}
value={color.s}
setValue={actions.setS}
scale={COLOR_SPACES.HSV.scales.s}
/>
<ValueEditor
componentSymbol={COLOR_SPACES.HSV.symbols.v}
valueRange={COLOR_SPACES.HSV.ranges.v}
value={color.v}
setValue={actions.setV}
scale={COLOR_SPACES.HSV.scales.v}
/>
</div>
);
}
function HCLSpaceEditor({
color,
actions,
}: {
color: colorlib.HCL;
actions: HCLColorActions;
}) {
return (
<div data-cy="HCL-editor" className={styles.spaceWrapper}>
<ValueEditor
componentSymbol={COLOR_SPACES.HCL.symbols.h}
valueRange={COLOR_SPACES.HCL.ranges.h}
value={color.h}
setValue={actions.setH}
/>
<ValueEditor
componentSymbol={COLOR_SPACES.HCL.symbols.c}
valueRange={COLOR_SPACES.HCL.ranges.c}
value={color.c}
setValue={actions.setC}
scale={COLOR_SPACES.HCL.scales.c}
/>
<ValueEditor
componentSymbol={COLOR_SPACES.HCL.symbols.l}
valueRange={COLOR_SPACES.HCL.ranges.l}
value={color.l}
setValue={actions.setL}
scale={COLOR_SPACES.HCL.scales.l}
/>
</div>
);
}
export default SpaceEditor;
@@ -2,7 +2,7 @@
display: flex;
align-items: stretch;
width: 100%;
height: var(--height, 25px);
min-height: 0;
font-family: monospace;
border: 1px solid black;
border-top: none;
@@ -84,5 +84,4 @@
font-family: monospace;
font-size: 14px;
text-align: right;
box-sizing: border-box;
}
+7 -6
View File
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from "react";
import type { ChangeEvent, RefObject } from "react";
import type { CSSProperties, ChangeEvent, RefObject } from "react";
import {
faChevronLeft,
@@ -36,17 +36,18 @@ function ValueEditor({
const direction = Direction.HORIZONTAL;
const [origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 });
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
const position = useRef(0);
const [position, setPosition] = useState(0);
useEffect(() => {
position.current = valueToPosition(value, dimensions.x, valueRange);
const newPosition = valueToPosition(value, dimensions.x, valueRange);
setPosition(newPosition);
}, [value, dimensions, valueRange]);
// Handler functions
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
const inputValue = parseInt(e.target.value, 10);
if (!isNaN(inputValue)) {
const actualValue = inputValue / scale;
const actualValue = Math.floor(inputValue) / scale;
const newValue = minmax(actualValue, valueRange.min, valueRange.max);
setValue(newValue);
@@ -59,7 +60,7 @@ function ValueEditor({
setValue((prev) => {
const scaledStep = step / scale;
const newValue = minmax(
prev + scaledStep,
Math.floor(prev * scale) / scale + scaledStep,
valueRange.min,
valueRange.max,
);
@@ -96,7 +97,7 @@ function ValueEditor({
<Slider
sliderRef={sliderRef}
position={position.current}
position={position}
dimensions={dimensions}
componentSymbol={componentSymbol}
/>
@@ -15,7 +15,14 @@ function TestWrapper() {
const actions = createColorActions(dispatch);
return (
<div style={{ width: 400 }}>
<div
style={{
width: 400,
height: 27,
display: "flex",
flexDirection: "column",
}}
>
<ValueEditor
componentSymbol="R"
valueRange={{ min: 0, max: 255 }}
@@ -36,7 +43,7 @@ describe("component editor tests", () => {
cy.clock().then((clock) => clock.restore());
});
it("works with mouse events", () => {
it.only("works with mouse events", () => {
// Check initial state
cy.dataCy("R-slider-bar")
.should("have.css", "width", "0px")