Completed color value editor component.
This commit is contained in:
+1
-4
@@ -49,10 +49,7 @@
|
|||||||
"^react(.*)$",
|
"^react(.*)$",
|
||||||
"^@(?!(components|hooks|providers|/))(.*)$",
|
"^@(?!(components|hooks|providers|/))(.*)$",
|
||||||
"^(?!@|[.])(.*)$",
|
"^(?!@|[.])(.*)$",
|
||||||
"^@/(.*)$",
|
"^@(/|components|hooks|providers)(.*)$",
|
||||||
"^@components(.*)$",
|
|
||||||
"^@hooks(.*)$",
|
|
||||||
"^@providers(.*)$",
|
|
||||||
"^[./]"
|
"^[./]"
|
||||||
],
|
],
|
||||||
"importOrderSeparation": true,
|
"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) => {
|
const handleWheelEvent = useCallback((event: WheelEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
console.log("Handling wheel event.");
|
||||||
|
|
||||||
setScrollLength((prev) =>
|
setScrollLength((prev) =>
|
||||||
handleScroll(
|
handleScroll(
|
||||||
|
|||||||
+12
-13
@@ -87,17 +87,14 @@ export function useSlider({
|
|||||||
[calculatePosition],
|
[calculatePosition],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleEnd = useCallback(
|
const handleEnd = useCallback(() => {
|
||||||
(event: MouseEvent | TouchEvent) => {
|
document.removeEventListener("mousemove", handleMove);
|
||||||
document.removeEventListener("mousemove", handleMove);
|
document.removeEventListener("mouseup", handleEnd);
|
||||||
document.removeEventListener("mouseup", handleEnd);
|
document.removeEventListener("touchmove", handleMove);
|
||||||
document.removeEventListener("touchmove", handleMove);
|
document.removeEventListener("touchend", handleEnd);
|
||||||
document.removeEventListener("touchend", handleEnd);
|
document.removeEventListener("touchcancel", handleEnd);
|
||||||
document.removeEventListener("touchcancel", handleEnd);
|
setIsDragging(false);
|
||||||
setIsDragging(false);
|
}, [handleMove]);
|
||||||
},
|
|
||||||
[handleMove],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleStart = useCallback(
|
const handleStart = useCallback(
|
||||||
(event: MouseEvent | TouchEvent) => {
|
(event: MouseEvent | TouchEvent) => {
|
||||||
@@ -121,18 +118,20 @@ export function useSlider({
|
|||||||
const handleScrollUp = useCallback(() => {
|
const handleScrollUp = useCallback(() => {
|
||||||
const dir = directionRef.current;
|
const dir = directionRef.current;
|
||||||
const dims = dimensionsRef.current;
|
const dims = dimensionsRef.current;
|
||||||
|
const inc = chooseValueByDirection(dir, 1, -1);
|
||||||
|
|
||||||
setPosition((prev: number) =>
|
setPosition((prev: number) =>
|
||||||
minmax(prev - 1, 0, chooseValueByDirection(dir, dims.x, dims.y)),
|
minmax(prev + inc, 0, chooseValueByDirection(dir, dims.x, dims.y)),
|
||||||
);
|
);
|
||||||
}, [setPosition]);
|
}, [setPosition]);
|
||||||
|
|
||||||
const handleScrollDown = useCallback(() => {
|
const handleScrollDown = useCallback(() => {
|
||||||
const dir = directionRef.current;
|
const dir = directionRef.current;
|
||||||
const dims = dimensionsRef.current;
|
const dims = dimensionsRef.current;
|
||||||
|
const inc = chooseValueByDirection(dir, -1, 1);
|
||||||
|
|
||||||
setPosition((prev: number) =>
|
setPosition((prev: number) =>
|
||||||
minmax(prev + 1, 0, chooseValueByDirection(dir, dims.x, dims.y)),
|
minmax(prev + inc, 0, chooseValueByDirection(dir, dims.x, dims.y)),
|
||||||
);
|
);
|
||||||
}, [setPosition]);
|
}, [setPosition]);
|
||||||
|
|
||||||
|
|||||||
@@ -142,10 +142,6 @@ function createTestUtils(isHorizontal = true) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTouchSupported = () => {
|
|
||||||
return typeof TouchEvent !== "undefined";
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
|
|
||||||
describe("horizontal slider hook tests", () => {
|
describe("horizontal slider hook tests", () => {
|
||||||
@@ -201,25 +197,25 @@ describe("horizontal slider hook tests", () => {
|
|||||||
it("moves the slider with mouse wheel scrolling", () => {
|
it("moves the slider with mouse wheel scrolling", () => {
|
||||||
assertPosition(0);
|
assertPosition(0);
|
||||||
|
|
||||||
triggerWheelEvent(100);
|
|
||||||
assertPosition(1);
|
|
||||||
|
|
||||||
triggerWheelEvent(100);
|
|
||||||
assertPosition(2);
|
|
||||||
|
|
||||||
triggerWheelEvent(-100);
|
triggerWheelEvent(-100);
|
||||||
assertPosition(1);
|
assertPosition(1);
|
||||||
|
|
||||||
|
triggerWheelEvent(-100);
|
||||||
|
assertPosition(2);
|
||||||
|
|
||||||
|
triggerWheelEvent(100);
|
||||||
|
assertPosition(1);
|
||||||
|
|
||||||
// Many smaller scrolls, to simulate touchpads
|
// 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);
|
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) {
|
export function minmax(number: number, min: number, max: number) {
|
||||||
return Math.min(max, Math.max(min, number));
|
return Math.min(max, Math.max(min, number));
|
||||||
}
|
}
|
||||||
@@ -25,3 +29,18 @@ export function extractEventCoordinates(event: MouseEvent | TouchEvent): {
|
|||||||
clientY: event.clientY,
|
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