Added color reducer. Performed state management refactor to prevent circular behavior.
This commit is contained in:
@@ -1,13 +1,10 @@
|
||||
import styles from "./ColorValues.module.css";
|
||||
import * as colorlib from "colorlib";
|
||||
|
||||
function ColorValues() {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.valueItem}>RGB: 255, 0, 0</div>
|
||||
<div className={styles.valueItem}>HEX: #FF0000</div>
|
||||
<div className={styles.valueItem}>HSL: 0, 100%, 50%</div>
|
||||
</div>
|
||||
);
|
||||
import styles from "./ColorValues.module.css";
|
||||
import SpaceEditor from "./SpaceEditor";
|
||||
|
||||
function ColorValues({ selectedColor }: { selectedColor: colorlib.Color }) {
|
||||
return <div className={styles.wrapper}></div>;
|
||||
}
|
||||
|
||||
export default ColorValues;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as colorlib from "colorlib";
|
||||
|
||||
import styles from "./SpaceEditor.module.css";
|
||||
|
||||
function SpaceEditor({}: {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
export default SpaceEditor;
|
||||
@@ -2,7 +2,7 @@
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: var(--height, 25px);
|
||||
font-family: monospace;
|
||||
border: 1px solid black;
|
||||
border-top: none;
|
||||
@@ -27,11 +27,16 @@
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.sliderWrapper {
|
||||
.sliderSection {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.sliderWrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sliderBar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
@@ -1,91 +1,62 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { ChangeEvent, Dispatch, RefObject, SetStateAction } from "react";
|
||||
import type { ChangeEvent, RefObject } from "react";
|
||||
|
||||
import clsx from "clsx";
|
||||
|
||||
import type { CartesianSpace } from "@/types";
|
||||
import { minmax, setMeasurements } from "@/util";
|
||||
import type { CartesianSpace, Range, Setter, Timeout } from "@/types";
|
||||
import { Direction } from "@/types";
|
||||
import { minmax, setMeasurements, valueToPosition } from "@/util";
|
||||
import { useScroll } from "@hooks/scroll";
|
||||
import { Direction, useSlider } from "@hooks/slider";
|
||||
import { useSlider } from "@hooks/slider";
|
||||
import { useResize } from "@hooks/window";
|
||||
|
||||
import styles from "./ValueEditor.module.css";
|
||||
|
||||
// Types
|
||||
type Timeout = ReturnType<typeof setTimeout>;
|
||||
interface Range {
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
// Calculation functions
|
||||
const getPositionFromValue = (
|
||||
newValue: number,
|
||||
maxValue: number,
|
||||
range: Range,
|
||||
) => {
|
||||
const newPosition = parseFloat(
|
||||
(
|
||||
((newValue + Math.abs(range.min)) /
|
||||
(Math.abs(range.min) + Math.abs(range.max))) *
|
||||
maxValue
|
||||
).toFixed(0),
|
||||
);
|
||||
return newPosition;
|
||||
};
|
||||
|
||||
const getValueFromPosition = (
|
||||
newPosition: number,
|
||||
maxValue: number,
|
||||
range: Range,
|
||||
) => {
|
||||
const newValue = parseFloat(
|
||||
(
|
||||
(newPosition / maxValue) * (Math.abs(range.min) + Math.abs(range.max)) -
|
||||
Math.abs(range.min)
|
||||
).toFixed(0),
|
||||
);
|
||||
return newValue;
|
||||
};
|
||||
|
||||
// Component
|
||||
function ValueEditor({
|
||||
componentSymbol,
|
||||
range,
|
||||
valueRange,
|
||||
value,
|
||||
setValue,
|
||||
scale = 1,
|
||||
}: {
|
||||
componentSymbol: string;
|
||||
range: Range;
|
||||
valueRange: Range;
|
||||
value: number;
|
||||
setValue: Dispatch<SetStateAction<number>>;
|
||||
setValue: Setter<number>;
|
||||
scale?: number;
|
||||
}) {
|
||||
// Set up component state
|
||||
const direction = Direction.HORIZONTAL;
|
||||
const [origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||
const [position, setPosition] = useState(0);
|
||||
const position = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
position.current = valueToPosition(value, dimensions.x, valueRange);
|
||||
}, [value, dimensions, valueRange]);
|
||||
|
||||
// Handler functions
|
||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const inputValue = parseInt(e.target.value, 10);
|
||||
if (!isNaN(inputValue)) {
|
||||
const newValue = minmax(inputValue, range.min, range.max);
|
||||
const newPosition = getPositionFromValue(newValue, dimensions.x, range);
|
||||
const actualValue = inputValue / scale;
|
||||
const newValue = minmax(actualValue, valueRange.min, valueRange.max);
|
||||
|
||||
setValue(newValue);
|
||||
setPosition(newPosition);
|
||||
} else {
|
||||
setValue(range.min);
|
||||
setPosition(0);
|
||||
setValue(valueRange.min);
|
||||
}
|
||||
};
|
||||
|
||||
const handleValueStep = (step: number) => {
|
||||
setValue((prev) => {
|
||||
const newValue = minmax(prev + step, range.min, range.max);
|
||||
const newPosition = getPositionFromValue(newValue, dimensions.x, range);
|
||||
setPosition(newPosition);
|
||||
const scaledStep = step / scale;
|
||||
const newValue = minmax(
|
||||
prev + scaledStep,
|
||||
valueRange.min,
|
||||
valueRange.max,
|
||||
);
|
||||
return newValue;
|
||||
});
|
||||
};
|
||||
@@ -95,7 +66,9 @@ function ValueEditor({
|
||||
direction,
|
||||
origin,
|
||||
dimensions,
|
||||
setPosition,
|
||||
valueRange,
|
||||
value,
|
||||
setValue,
|
||||
});
|
||||
|
||||
// Set component dimensions for slider hook
|
||||
@@ -106,17 +79,6 @@ function ValueEditor({
|
||||
);
|
||||
}, [sliderRef, setOrigin, setDimensions]);
|
||||
|
||||
// Update value when position, etc. changes
|
||||
useEffect(() => {
|
||||
const maxValue = dimensions.x;
|
||||
if (maxValue > 0) {
|
||||
const newValue = getValueFromPosition(position, maxValue, range);
|
||||
setValue(newValue);
|
||||
} else {
|
||||
setValue(range.min);
|
||||
}
|
||||
}, [dimensions, position, range, setValue]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.componentWrapper}
|
||||
@@ -128,9 +90,8 @@ function ValueEditor({
|
||||
|
||||
<Slider
|
||||
sliderRef={sliderRef}
|
||||
position={position}
|
||||
value={value}
|
||||
range={range}
|
||||
position={position.current}
|
||||
dimensions={dimensions}
|
||||
componentSymbol={componentSymbol}
|
||||
/>
|
||||
|
||||
@@ -143,9 +104,10 @@ function ValueEditor({
|
||||
<Value
|
||||
value={value}
|
||||
onChange={handleInputChange}
|
||||
range={range}
|
||||
valueRange={valueRange}
|
||||
componentSymbol={componentSymbol}
|
||||
handleValueStep={handleValueStep}
|
||||
scale={scale}
|
||||
/>
|
||||
|
||||
<Button
|
||||
@@ -173,33 +135,33 @@ function Label({ componentSymbol }: { componentSymbol: string }) {
|
||||
function Slider({
|
||||
sliderRef,
|
||||
position,
|
||||
value,
|
||||
range,
|
||||
dimensions,
|
||||
componentSymbol,
|
||||
}: {
|
||||
sliderRef: RefObject<HTMLDivElement | null>;
|
||||
position: number;
|
||||
value: number;
|
||||
range: { min: number; max: number };
|
||||
dimensions: CartesianSpace;
|
||||
componentSymbol: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.section, styles.sliderWrapper)}
|
||||
ref={sliderRef}
|
||||
role="slider"
|
||||
aria-valuemin={range.min}
|
||||
aria-valuemax={range.max}
|
||||
aria-valuenow={value}
|
||||
aria-labelledby={`${componentSymbol}-label`}
|
||||
tabIndex={0}
|
||||
data-cy={`${componentSymbol}-slider`}
|
||||
>
|
||||
<div className={clsx(styles.section, styles.sliderSection)}>
|
||||
<div
|
||||
className={styles.sliderBar}
|
||||
style={{ width: position }}
|
||||
data-cy={`${componentSymbol}-slider-bar`}
|
||||
></div>
|
||||
className={styles.sliderWrapper}
|
||||
ref={sliderRef}
|
||||
role="slider"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={dimensions.x}
|
||||
aria-valuenow={position}
|
||||
aria-labelledby={`${componentSymbol}-label`}
|
||||
tabIndex={0}
|
||||
data-cy={`${componentSymbol}-slider`}
|
||||
>
|
||||
<div
|
||||
className={styles.sliderBar}
|
||||
style={{ width: position }}
|
||||
data-cy={`${componentSymbol}-slider-bar`}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -240,15 +202,17 @@ function Button({
|
||||
function Value({
|
||||
value,
|
||||
onChange,
|
||||
range,
|
||||
valueRange,
|
||||
componentSymbol,
|
||||
handleValueStep,
|
||||
scale,
|
||||
}: {
|
||||
value: number;
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
range: { min: number; max: number };
|
||||
valueRange: { min: number; max: number };
|
||||
componentSymbol: string;
|
||||
handleValueStep: (step: number) => void;
|
||||
scale: number;
|
||||
}) {
|
||||
const valueRef = useRef(null);
|
||||
const valueScroller = useScroll({
|
||||
@@ -263,9 +227,7 @@ function Value({
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (valueRef.current) {
|
||||
valueScroller.removeScrollListener();
|
||||
}
|
||||
valueScroller.removeScrollListener();
|
||||
};
|
||||
}, [valueScroller]);
|
||||
|
||||
@@ -274,13 +236,13 @@ function Value({
|
||||
<input
|
||||
type="text"
|
||||
ref={valueRef}
|
||||
value={value}
|
||||
value={Math.round(value * scale)}
|
||||
onChange={onChange}
|
||||
className={styles.value}
|
||||
onFocus={(e) => e.target.select()}
|
||||
aria-label={`${componentSymbol} value input`}
|
||||
aria-valuemin={range.min}
|
||||
aria-valuemax={range.max}
|
||||
aria-valuemin={valueRange.min}
|
||||
aria-valuemax={valueRange.max}
|
||||
aria-valuenow={value}
|
||||
data-cy={`${componentSymbol}-value-input`}
|
||||
/>
|
||||
@@ -294,35 +256,30 @@ function useLongPressRepeat(
|
||||
startDelay = 650,
|
||||
repeatInterval = 150,
|
||||
) {
|
||||
const [pressing, setPressing] = useState(false);
|
||||
const timerRef = useRef<Timeout>(null);
|
||||
const timeoutRef = useRef<Timeout>(null);
|
||||
const intervalRef = useRef<Timeout>(null);
|
||||
|
||||
const start = useCallback(
|
||||
(e: Event | any) => {
|
||||
e.preventDefault();
|
||||
setPressing(true);
|
||||
timerRef.current = setTimeout(() => {
|
||||
callback();
|
||||
intervalRef.current = setInterval(callback, repeatInterval);
|
||||
}, startDelay);
|
||||
},
|
||||
[callback, startDelay, repeatInterval],
|
||||
);
|
||||
|
||||
const stop = useCallback(() => {
|
||||
setPressing(false);
|
||||
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
const cleanup = () => {
|
||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||
}, []);
|
||||
timeoutRef.current = null;
|
||||
intervalRef.current = null;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||
};
|
||||
}, []);
|
||||
// Intentional 'any' to avoid overly complex typing
|
||||
const start = (e: Event | any) => {
|
||||
e.preventDefault();
|
||||
cleanup();
|
||||
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
callback();
|
||||
intervalRef.current = setInterval(callback, repeatInterval);
|
||||
}, startDelay);
|
||||
};
|
||||
|
||||
const stop = cleanup;
|
||||
|
||||
useEffect(() => cleanup, []);
|
||||
|
||||
return {
|
||||
onMouseDown: start,
|
||||
@@ -330,8 +287,8 @@ function useLongPressRepeat(
|
||||
onMouseLeave: stop,
|
||||
onTouchStart: start,
|
||||
onTouchEnd: stop,
|
||||
// Intentional 'any' to avoid overly complex typing
|
||||
onContextMenu: (e: Event | any) => e.preventDefault(),
|
||||
pressing: pressing.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import { useState } from "react";
|
||||
import { useReducer } from "react";
|
||||
|
||||
import { Color } from "colorlib";
|
||||
|
||||
import { colorReducer, createColorActions } from "@hooks/color";
|
||||
|
||||
import ValueEditor from "./ValueEditor";
|
||||
|
||||
const initialState = {
|
||||
color: Color.from_hex("000"),
|
||||
};
|
||||
|
||||
function TestWrapper() {
|
||||
const [value, setValue] = useState(0);
|
||||
const [state, dispatch] = useReducer(colorReducer, initialState);
|
||||
const actions = createColorActions(dispatch);
|
||||
|
||||
return (
|
||||
<div style={{ width: 400, height: 25 }}>
|
||||
<div style={{ width: 400 }}>
|
||||
<ValueEditor
|
||||
componentSymbol="R"
|
||||
range={{ min: 0, max: 255 }}
|
||||
value={value}
|
||||
setValue={setValue}
|
||||
valueRange={{ min: 0, max: 255 }}
|
||||
value={state.color.rgb.r}
|
||||
setValue={actions.rgb.setR}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -37,9 +47,9 @@ describe("component editor tests", () => {
|
||||
cy.dataCy("R-slider")
|
||||
.click()
|
||||
.dataCy("R-slider-bar")
|
||||
.should("have.css", "width", "141px")
|
||||
.should("have.css", "width", "140px")
|
||||
.dataCy("R-value-input")
|
||||
.should("have.value", "128");
|
||||
.should("have.value", "127");
|
||||
|
||||
// Test slider scroll
|
||||
cy.dataCy("R-slider")
|
||||
@@ -47,7 +57,8 @@ describe("component editor tests", () => {
|
||||
.trigger("wheel", { deltaY: -100, eventConstructor: "WheelEvent" })
|
||||
.trigger("wheel", { deltaY: -100, eventConstructor: "WheelEvent" })
|
||||
.dataCy("R-value-input")
|
||||
.should("have.value", "129");
|
||||
.should("have.value", "128")
|
||||
.wait(50);
|
||||
|
||||
// Input focus should select text
|
||||
cy.dataCy("R-value-input")
|
||||
@@ -61,14 +72,15 @@ describe("component editor tests", () => {
|
||||
.type("100")
|
||||
.should("have.value", "100")
|
||||
.dataCy("R-slider-bar")
|
||||
.should("have.css", "width", "111px");
|
||||
.should("have.css", "width", "110px");
|
||||
|
||||
// Scrolling input should update value
|
||||
cy.dataCy("R-value-input")
|
||||
.trigger("wheel", { deltaY: -100, eventConstructor: "WheelEvent" })
|
||||
.should("have.value", "101")
|
||||
.should("have.value", "100")
|
||||
.dataCy("R-slider-bar")
|
||||
.should("have.css", "width", "112px");
|
||||
.should("have.css", "width", "111px")
|
||||
.wait(50);
|
||||
|
||||
// Test increment/decrement buttons
|
||||
cy.dataCy("R-decrement-button")
|
||||
@@ -114,9 +126,9 @@ describe("component editor tests", () => {
|
||||
cy.dataCy("R-slider")
|
||||
.click()
|
||||
.dataCy("R-slider-bar")
|
||||
.should("have.css", "width", "141px")
|
||||
.should("have.css", "width", "140px")
|
||||
.dataCy("R-value-input")
|
||||
.should("have.value", "128");
|
||||
.should("have.value", "127");
|
||||
|
||||
// Test button long press repeat
|
||||
cy.dataCy("R-decrement-button")
|
||||
@@ -128,7 +140,7 @@ describe("component editor tests", () => {
|
||||
.dataCy("R-decrement-button")
|
||||
.trigger("touchend")
|
||||
.dataCy("R-value-input")
|
||||
.should("have.value", "125")
|
||||
.should("have.value", "124")
|
||||
.dataCy("R-increment-button")
|
||||
.trigger("touchstart")
|
||||
.then(() => {
|
||||
@@ -138,6 +150,6 @@ describe("component editor tests", () => {
|
||||
.dataCy("R-increment-button")
|
||||
.trigger("touchend")
|
||||
.dataCy("R-value-input")
|
||||
.should("have.value", "128");
|
||||
.should("have.value", "127");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user