Completed color value editor component.
This commit is contained in:
+1
-4
@@ -49,10 +49,7 @@
|
||||
"^react(.*)$",
|
||||
"^@(?!(components|hooks|providers|/))(.*)$",
|
||||
"^(?!@|[.])(.*)$",
|
||||
"^@/(.*)$",
|
||||
"^@components(.*)$",
|
||||
"^@hooks(.*)$",
|
||||
"^@providers(.*)$",
|
||||
"^@(/|components|hooks|providers)(.*)$",
|
||||
"^[./]"
|
||||
],
|
||||
"importOrderSeparation": true,
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
.componentWrapper {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: monospace;
|
||||
border: 1px solid black;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.componentWrapper:first-of-type {
|
||||
border-top: 1px solid black;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-right: 1px solid black;
|
||||
}
|
||||
|
||||
.section:last-of-type {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.symbol {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.sliderWrapper {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.sliderBar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background-color: #aaa;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.buttonWrapper {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.button {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
.button:active {
|
||||
background-color: #bbb;
|
||||
}
|
||||
|
||||
.valueWrapper {
|
||||
aspect-ratio: 1.5 / 1;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.value {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0 5px;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
text-align: right;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { ChangeEvent, Dispatch, RefObject, SetStateAction } from "react";
|
||||
|
||||
import clsx from "clsx";
|
||||
|
||||
import type { CartesianSpace } from "@/types";
|
||||
import { minmax, setMeasurements } from "@/util";
|
||||
import { useScroll } from "@hooks/scroll";
|
||||
import { Direction, 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,
|
||||
value,
|
||||
setValue,
|
||||
}: {
|
||||
componentSymbol: string;
|
||||
range: Range;
|
||||
value: number;
|
||||
setValue: Dispatch<SetStateAction<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);
|
||||
|
||||
// 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);
|
||||
|
||||
setValue(newValue);
|
||||
setPosition(newPosition);
|
||||
} else {
|
||||
setValue(range.min);
|
||||
setPosition(0);
|
||||
}
|
||||
};
|
||||
|
||||
const handleValueStep = (step: number) => {
|
||||
setValue((prev) => {
|
||||
const newValue = minmax(prev + step, range.min, range.max);
|
||||
const newPosition = getPositionFromValue(newValue, dimensions.x, range);
|
||||
setPosition(newPosition);
|
||||
return newValue;
|
||||
});
|
||||
};
|
||||
|
||||
// Set up slider hook
|
||||
const { sliderRef } = useSlider({
|
||||
direction,
|
||||
origin,
|
||||
dimensions,
|
||||
setPosition,
|
||||
});
|
||||
|
||||
// Set component dimensions for slider hook
|
||||
useEffect(() => {
|
||||
setMeasurements(sliderRef, setOrigin, setDimensions);
|
||||
return useResize(() =>
|
||||
setMeasurements(sliderRef, setOrigin, setDimensions),
|
||||
);
|
||||
}, [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}
|
||||
role="group"
|
||||
aria-labelledby={`${componentSymbol}-label`}
|
||||
data-cy={`${componentSymbol}-editor`}
|
||||
>
|
||||
<Label componentSymbol={componentSymbol} />
|
||||
|
||||
<Slider
|
||||
sliderRef={sliderRef}
|
||||
position={position}
|
||||
value={value}
|
||||
range={range}
|
||||
componentSymbol={componentSymbol}
|
||||
/>
|
||||
|
||||
<Button
|
||||
direction="decrease"
|
||||
handleValueStep={handleValueStep}
|
||||
componentSymbol={componentSymbol}
|
||||
/>
|
||||
|
||||
<Value
|
||||
value={value}
|
||||
onChange={handleInputChange}
|
||||
range={range}
|
||||
componentSymbol={componentSymbol}
|
||||
handleValueStep={handleValueStep}
|
||||
/>
|
||||
|
||||
<Button
|
||||
direction="increase"
|
||||
handleValueStep={handleValueStep}
|
||||
componentSymbol={componentSymbol}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Subcomponents
|
||||
function Label({ componentSymbol }: { componentSymbol: string }) {
|
||||
return (
|
||||
<div
|
||||
id={`${componentSymbol}-label`}
|
||||
className={clsx(styles.section, styles.symbol)}
|
||||
data-cy={`${componentSymbol}-label`}
|
||||
>
|
||||
{componentSymbol}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Slider({
|
||||
sliderRef,
|
||||
position,
|
||||
value,
|
||||
range,
|
||||
componentSymbol,
|
||||
}: {
|
||||
sliderRef: RefObject<HTMLDivElement | null>;
|
||||
position: number;
|
||||
value: number;
|
||||
range: { min: number; max: number };
|
||||
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={styles.sliderBar}
|
||||
style={{ width: position }}
|
||||
data-cy={`${componentSymbol}-slider-bar`}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Button({
|
||||
direction,
|
||||
componentSymbol,
|
||||
handleValueStep,
|
||||
}: {
|
||||
direction: "increase" | "decrease";
|
||||
componentSymbol: string;
|
||||
handleValueStep: (step: number) => void;
|
||||
}) {
|
||||
const isIncrease = direction === "increase";
|
||||
const label = isIncrease ? "Increase" : "Decrease";
|
||||
const symbol = isIncrease ? ">" : "<";
|
||||
const dataCy = `${componentSymbol}-${isIncrease ? "increment" : "decrement"}-button`;
|
||||
|
||||
const step = isIncrease ? 1 : -1;
|
||||
const onClick = () => handleValueStep(step);
|
||||
const longPressProps = useLongPressRepeat(onClick);
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.section, styles.buttonWrapper)}>
|
||||
<button
|
||||
className={styles.button}
|
||||
onClick={onClick}
|
||||
{...longPressProps}
|
||||
aria-label={`${label} ${componentSymbol}`}
|
||||
data-cy={dataCy}
|
||||
>
|
||||
{symbol}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Value({
|
||||
value,
|
||||
onChange,
|
||||
range,
|
||||
componentSymbol,
|
||||
handleValueStep,
|
||||
}: {
|
||||
value: number;
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
range: { min: number; max: number };
|
||||
componentSymbol: string;
|
||||
handleValueStep: (step: number) => void;
|
||||
}) {
|
||||
const valueRef = useRef(null);
|
||||
const valueScroller = useScroll({
|
||||
targetRef: valueRef,
|
||||
onScrollUp: () => handleValueStep(1),
|
||||
onScrollDown: () => handleValueStep(-1),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (valueRef.current) {
|
||||
valueScroller.addScrollListener();
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (valueRef.current) {
|
||||
valueScroller.removeScrollListener();
|
||||
}
|
||||
};
|
||||
}, [valueScroller]);
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.section, styles.valueWrapper)}>
|
||||
<input
|
||||
type="text"
|
||||
ref={valueRef}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className={styles.value}
|
||||
onFocus={(e) => e.target.select()}
|
||||
aria-label={`${componentSymbol} value input`}
|
||||
aria-valuemin={range.min}
|
||||
aria-valuemax={range.max}
|
||||
aria-valuenow={value}
|
||||
data-cy={`${componentSymbol}-value-input`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Hooks
|
||||
function useLongPressRepeat(
|
||||
callback: () => void,
|
||||
startDelay = 650,
|
||||
repeatInterval = 150,
|
||||
) {
|
||||
const [pressing, setPressing] = useState(false);
|
||||
const timerRef = 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);
|
||||
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
onMouseDown: start,
|
||||
onMouseUp: stop,
|
||||
onMouseLeave: stop,
|
||||
onTouchStart: start,
|
||||
onTouchEnd: stop,
|
||||
onContextMenu: (e: Event | any) => e.preventDefault(),
|
||||
pressing: pressing.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
export default ValueEditor;
|
||||
@@ -0,0 +1,143 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import ValueEditor from "./ValueEditor";
|
||||
|
||||
function TestWrapper() {
|
||||
const [value, setValue] = useState(0);
|
||||
return (
|
||||
<div style={{ width: 400, height: 25 }}>
|
||||
<ValueEditor
|
||||
componentSymbol="R"
|
||||
range={{ min: 0, max: 255 }}
|
||||
value={value}
|
||||
setValue={setValue}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
describe("component editor tests", () => {
|
||||
beforeEach(() => {
|
||||
cy.clock();
|
||||
cy.mount(<TestWrapper />);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.clock().then((clock) => clock.restore());
|
||||
});
|
||||
|
||||
it("works with mouse events", () => {
|
||||
// Check initial state
|
||||
cy.dataCy("R-slider-bar")
|
||||
.should("have.css", "width", "0px")
|
||||
.dataCy("R-value-input")
|
||||
.should("have.value", "0");
|
||||
|
||||
// Test slider click
|
||||
cy.dataCy("R-slider")
|
||||
.click()
|
||||
.dataCy("R-slider-bar")
|
||||
.should("have.css", "width", "141px")
|
||||
.dataCy("R-value-input")
|
||||
.should("have.value", "128");
|
||||
|
||||
// Test slider scroll
|
||||
cy.dataCy("R-slider")
|
||||
// scroll twice to ensure position moves enough to trigger value change.
|
||||
.trigger("wheel", { deltaY: -100, eventConstructor: "WheelEvent" })
|
||||
.trigger("wheel", { deltaY: -100, eventConstructor: "WheelEvent" })
|
||||
.dataCy("R-value-input")
|
||||
.should("have.value", "129");
|
||||
|
||||
// Input focus should select text
|
||||
cy.dataCy("R-value-input")
|
||||
.focus()
|
||||
.should("have.prop", "selectionStart", 0)
|
||||
.should("have.prop", "selectionEnd")
|
||||
.should("be.gt", 0);
|
||||
|
||||
// Input value should update slider
|
||||
cy.dataCy("R-value-input")
|
||||
.type("100")
|
||||
.should("have.value", "100")
|
||||
.dataCy("R-slider-bar")
|
||||
.should("have.css", "width", "111px");
|
||||
|
||||
// Scrolling input should update value
|
||||
cy.dataCy("R-value-input")
|
||||
.trigger("wheel", { deltaY: -100, eventConstructor: "WheelEvent" })
|
||||
.should("have.value", "101")
|
||||
.dataCy("R-slider-bar")
|
||||
.should("have.css", "width", "112px");
|
||||
|
||||
// Test increment/decrement buttons
|
||||
cy.dataCy("R-decrement-button")
|
||||
.click()
|
||||
.dataCy("R-value-input")
|
||||
.should("have.value", "100")
|
||||
.dataCy("R-increment-button")
|
||||
.click()
|
||||
.dataCy("R-value-input")
|
||||
.should("have.value", "101");
|
||||
|
||||
// Test button long press repeat
|
||||
cy.dataCy("R-decrement-button")
|
||||
.trigger("mousedown")
|
||||
.then(() => {
|
||||
cy.tick(650);
|
||||
cy.tick(150 * 2);
|
||||
})
|
||||
.dataCy("R-decrement-button")
|
||||
.trigger("mouseup")
|
||||
.dataCy("R-value-input")
|
||||
.should("have.value", "98")
|
||||
.dataCy("R-increment-button")
|
||||
.trigger("mousedown")
|
||||
.then(() => {
|
||||
cy.tick(650);
|
||||
cy.tick(150 * 2);
|
||||
})
|
||||
.dataCy("R-increment-button")
|
||||
.trigger("mouseup")
|
||||
.dataCy("R-value-input")
|
||||
.should("have.value", "101");
|
||||
});
|
||||
|
||||
it("works with touch events", () => {
|
||||
// Check initial state
|
||||
cy.dataCy("R-slider-bar")
|
||||
.should("have.css", "width", "0px")
|
||||
.dataCy("R-value-input")
|
||||
.should("have.value", "0");
|
||||
|
||||
// Test slider click
|
||||
cy.dataCy("R-slider")
|
||||
.click()
|
||||
.dataCy("R-slider-bar")
|
||||
.should("have.css", "width", "141px")
|
||||
.dataCy("R-value-input")
|
||||
.should("have.value", "128");
|
||||
|
||||
// Test button long press repeat
|
||||
cy.dataCy("R-decrement-button")
|
||||
.trigger("touchstart")
|
||||
.then(() => {
|
||||
cy.tick(650);
|
||||
cy.tick(150 * 2);
|
||||
})
|
||||
.dataCy("R-decrement-button")
|
||||
.trigger("touchend")
|
||||
.dataCy("R-value-input")
|
||||
.should("have.value", "125")
|
||||
.dataCy("R-increment-button")
|
||||
.trigger("touchstart")
|
||||
.then(() => {
|
||||
cy.tick(650);
|
||||
cy.tick(150 * 2);
|
||||
})
|
||||
.dataCy("R-increment-button")
|
||||
.trigger("touchend")
|
||||
.dataCy("R-value-input")
|
||||
.should("have.value", "128");
|
||||
});
|
||||
});
|
||||
@@ -49,6 +49,7 @@ export function useScroll<T extends HTMLElement>({
|
||||
|
||||
const handleWheelEvent = useCallback((event: WheelEvent) => {
|
||||
event.preventDefault();
|
||||
console.log("Handling wheel event.");
|
||||
|
||||
setScrollLength((prev) =>
|
||||
handleScroll(
|
||||
|
||||
@@ -87,17 +87,14 @@ export function useSlider({
|
||||
[calculatePosition],
|
||||
);
|
||||
|
||||
const handleEnd = useCallback(
|
||||
(event: MouseEvent | TouchEvent) => {
|
||||
const handleEnd = useCallback(() => {
|
||||
document.removeEventListener("mousemove", handleMove);
|
||||
document.removeEventListener("mouseup", handleEnd);
|
||||
document.removeEventListener("touchmove", handleMove);
|
||||
document.removeEventListener("touchend", handleEnd);
|
||||
document.removeEventListener("touchcancel", handleEnd);
|
||||
setIsDragging(false);
|
||||
},
|
||||
[handleMove],
|
||||
);
|
||||
}, [handleMove]);
|
||||
|
||||
const handleStart = useCallback(
|
||||
(event: MouseEvent | TouchEvent) => {
|
||||
@@ -121,18 +118,20 @@ export function useSlider({
|
||||
const handleScrollUp = useCallback(() => {
|
||||
const dir = directionRef.current;
|
||||
const dims = dimensionsRef.current;
|
||||
const inc = chooseValueByDirection(dir, 1, -1);
|
||||
|
||||
setPosition((prev: number) =>
|
||||
minmax(prev - 1, 0, chooseValueByDirection(dir, dims.x, dims.y)),
|
||||
minmax(prev + inc, 0, chooseValueByDirection(dir, dims.x, dims.y)),
|
||||
);
|
||||
}, [setPosition]);
|
||||
|
||||
const handleScrollDown = useCallback(() => {
|
||||
const dir = directionRef.current;
|
||||
const dims = dimensionsRef.current;
|
||||
const inc = chooseValueByDirection(dir, -1, 1);
|
||||
|
||||
setPosition((prev: number) =>
|
||||
minmax(prev + 1, 0, chooseValueByDirection(dir, dims.x, dims.y)),
|
||||
minmax(prev + inc, 0, chooseValueByDirection(dir, dims.x, dims.y)),
|
||||
);
|
||||
}, [setPosition]);
|
||||
|
||||
|
||||
@@ -142,10 +142,6 @@ function createTestUtils(isHorizontal = true) {
|
||||
};
|
||||
}
|
||||
|
||||
const isTouchSupported = () => {
|
||||
return typeof TouchEvent !== "undefined";
|
||||
};
|
||||
|
||||
// Tests
|
||||
|
||||
describe("horizontal slider hook tests", () => {
|
||||
@@ -201,25 +197,25 @@ describe("horizontal slider hook tests", () => {
|
||||
it("moves the slider with mouse wheel scrolling", () => {
|
||||
assertPosition(0);
|
||||
|
||||
triggerWheelEvent(100);
|
||||
assertPosition(1);
|
||||
|
||||
triggerWheelEvent(100);
|
||||
assertPosition(2);
|
||||
|
||||
triggerWheelEvent(-100);
|
||||
assertPosition(1);
|
||||
|
||||
triggerWheelEvent(-100);
|
||||
assertPosition(2);
|
||||
|
||||
triggerWheelEvent(100);
|
||||
assertPosition(1);
|
||||
|
||||
// Many smaller scrolls, to simulate touchpads
|
||||
triggerWheelEvent(20);
|
||||
triggerWheelEvent(20);
|
||||
triggerWheelEvent(20);
|
||||
triggerWheelEvent(20);
|
||||
triggerWheelEvent(20);
|
||||
triggerWheelEvent(20);
|
||||
triggerWheelEvent(20);
|
||||
triggerWheelEvent(20);
|
||||
triggerWheelEvent(20);
|
||||
triggerWheelEvent(-20);
|
||||
triggerWheelEvent(-20);
|
||||
triggerWheelEvent(-20);
|
||||
triggerWheelEvent(-20);
|
||||
triggerWheelEvent(-20);
|
||||
triggerWheelEvent(-20);
|
||||
triggerWheelEvent(-20);
|
||||
triggerWheelEvent(-20);
|
||||
triggerWheelEvent(-20);
|
||||
assertPosition(4);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export function useResize(callback: () => void): () => void {
|
||||
window.addEventListener("resize", callback);
|
||||
return () => window.removeEventListener("resize", callback);
|
||||
}
|
||||
+19
@@ -1,3 +1,7 @@
|
||||
import type { RefObject } from "react";
|
||||
|
||||
import type { CartesianSpace } from "./types";
|
||||
|
||||
export function minmax(number: number, min: number, max: number) {
|
||||
return Math.min(max, Math.max(min, number));
|
||||
}
|
||||
@@ -25,3 +29,18 @@ export function extractEventCoordinates(event: MouseEvent | TouchEvent): {
|
||||
clientY: event.clientY,
|
||||
};
|
||||
}
|
||||
|
||||
export function setMeasurements(
|
||||
ref: RefObject<HTMLElement | null>,
|
||||
setOrigin: (newOrigin: CartesianSpace) => void,
|
||||
setDimensions: (newDimensions: CartesianSpace) => void,
|
||||
) {
|
||||
const el = ref.current;
|
||||
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
||||
setOrigin({ x: rect.left, y: rect.top });
|
||||
setDimensions({ x: rect.width, y: rect.height });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user