Completed crosshair, slider, and scroll hooks.
This commit is contained in:
@@ -0,0 +1,167 @@
|
|||||||
|
import { useEffect, useState, useRef } from "react";
|
||||||
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
import type { CartesianSpace } from "../types";
|
||||||
|
import { minmax } from "../util";
|
||||||
|
|
||||||
|
if (typeof TouchEvent === "undefined") {
|
||||||
|
// @ts-ignore - intentionally creating global
|
||||||
|
window.TouchEvent = window.MouseEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTouchEvent(event: Event): event is TouchEvent {
|
||||||
|
return "touches" in event;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCrosshair({
|
||||||
|
origin,
|
||||||
|
dimensions,
|
||||||
|
setXPosition,
|
||||||
|
setYPosition,
|
||||||
|
}: {
|
||||||
|
origin: CartesianSpace;
|
||||||
|
dimensions: CartesianSpace;
|
||||||
|
setXPosition: Dispatch<SetStateAction<number>>;
|
||||||
|
setYPosition: Dispatch<SetStateAction<number>>;
|
||||||
|
}) {
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const crosshairRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Construct event handler refs
|
||||||
|
// Prevents unnecessary function recreation
|
||||||
|
const calculatePositionsRef = useRef((_: MouseEvent | TouchEvent) => {});
|
||||||
|
const startCrosshairInteractionRef = useRef(
|
||||||
|
(_: MouseEvent | TouchEvent) => {},
|
||||||
|
);
|
||||||
|
const processCrosshairInteractionRef = useRef(
|
||||||
|
(_: MouseEvent | TouchEvent) => {},
|
||||||
|
);
|
||||||
|
const endCrosshairInteractionRef = useRef((_: MouseEvent | TouchEvent) => {});
|
||||||
|
|
||||||
|
// Store dependencies as refs
|
||||||
|
// Always use latest values
|
||||||
|
const originRef = useRef(origin);
|
||||||
|
const dimensionsRef = useRef(dimensions);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
originRef.current = origin;
|
||||||
|
dimensionsRef.current = dimensions;
|
||||||
|
}, [origin, dimensions]);
|
||||||
|
|
||||||
|
// Update handler functions when dependencies change via reference
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
calculatePositionsRef.current = (event: MouseEvent | TouchEvent) => {
|
||||||
|
const orig = originRef.current;
|
||||||
|
const dims = dimensionsRef.current;
|
||||||
|
|
||||||
|
const clientX = isTouchEvent(event)
|
||||||
|
? event.touches[0].clientX
|
||||||
|
: event.clientX;
|
||||||
|
const clientY = isTouchEvent(event)
|
||||||
|
? event.touches[0].clientY
|
||||||
|
: event.clientY;
|
||||||
|
|
||||||
|
const xPos = minmax(clientX - orig.x, 0, dims.x - 1);
|
||||||
|
const yPos = minmax(clientY - orig.y, 0, dims.y - 1);
|
||||||
|
setXPosition(xPos);
|
||||||
|
setYPosition(yPos);
|
||||||
|
};
|
||||||
|
|
||||||
|
startCrosshairInteractionRef.current = (event: MouseEvent | TouchEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
calculatePositionsRef.current(event);
|
||||||
|
setIsDragging(true);
|
||||||
|
|
||||||
|
if (!isTouchEvent(event)) {
|
||||||
|
document.addEventListener(
|
||||||
|
"mousemove",
|
||||||
|
processCrosshairInteractionRef.current,
|
||||||
|
);
|
||||||
|
document.addEventListener(
|
||||||
|
"mouseup",
|
||||||
|
endCrosshairInteractionRef.current,
|
||||||
|
{ passive: true },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
document.addEventListener(
|
||||||
|
"touchmove",
|
||||||
|
processCrosshairInteractionRef.current,
|
||||||
|
);
|
||||||
|
document.addEventListener(
|
||||||
|
"touchend",
|
||||||
|
endCrosshairInteractionRef.current,
|
||||||
|
{ passive: true },
|
||||||
|
);
|
||||||
|
document.addEventListener(
|
||||||
|
"touchcancel",
|
||||||
|
endCrosshairInteractionRef.current,
|
||||||
|
{ passive: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
processCrosshairInteractionRef.current = (
|
||||||
|
event: MouseEvent | TouchEvent,
|
||||||
|
) => {
|
||||||
|
event.preventDefault();
|
||||||
|
calculatePositionsRef.current(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
endCrosshairInteractionRef.current = (event: MouseEvent | TouchEvent) => {
|
||||||
|
setIsDragging(false);
|
||||||
|
if (!isTouchEvent(event)) {
|
||||||
|
document.removeEventListener(
|
||||||
|
"mousemove",
|
||||||
|
processCrosshairInteractionRef.current,
|
||||||
|
);
|
||||||
|
document.removeEventListener(
|
||||||
|
"mouseup",
|
||||||
|
endCrosshairInteractionRef.current,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
document.removeEventListener(
|
||||||
|
"touchmove",
|
||||||
|
processCrosshairInteractionRef.current,
|
||||||
|
);
|
||||||
|
document.removeEventListener(
|
||||||
|
"touchend",
|
||||||
|
endCrosshairInteractionRef.current,
|
||||||
|
);
|
||||||
|
document.removeEventListener(
|
||||||
|
"touchcancel",
|
||||||
|
endCrosshairInteractionRef.current,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Set up entry listeners
|
||||||
|
useEffect(() => {
|
||||||
|
const currentRef = crosshairRef.current;
|
||||||
|
if (currentRef) {
|
||||||
|
currentRef.addEventListener(
|
||||||
|
"mousedown",
|
||||||
|
startCrosshairInteractionRef.current,
|
||||||
|
);
|
||||||
|
currentRef.addEventListener(
|
||||||
|
"touchstart",
|
||||||
|
startCrosshairInteractionRef.current,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (currentRef) {
|
||||||
|
currentRef.removeEventListener(
|
||||||
|
"mousedown",
|
||||||
|
startCrosshairInteractionRef.current,
|
||||||
|
);
|
||||||
|
currentRef.removeEventListener(
|
||||||
|
"touchstart",
|
||||||
|
startCrosshairInteractionRef.current,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { crosshairRef, isDragging };
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
import type { RefObject } from "react";
|
||||||
|
|
||||||
export function handleScroll(
|
export function handleScroll(
|
||||||
prevLength: number,
|
prevLength: number,
|
||||||
scrollDelta: number,
|
scrollDelta: number,
|
||||||
@@ -18,3 +21,71 @@ export function handleScroll(
|
|||||||
|
|
||||||
return newLength;
|
return newLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ScrollHandler = () => void;
|
||||||
|
|
||||||
|
export function useScroll<T extends HTMLElement>({
|
||||||
|
targetRef,
|
||||||
|
onScrollUp,
|
||||||
|
onScrollDown,
|
||||||
|
deltaYMultiplier: deltaYMultiplier = 1,
|
||||||
|
}: {
|
||||||
|
targetRef: RefObject<T | null>;
|
||||||
|
onScrollUp: ScrollHandler;
|
||||||
|
onScrollDown: ScrollHandler;
|
||||||
|
deltaYMultiplier?: number;
|
||||||
|
}) {
|
||||||
|
const [_, setScrollLength] = useState(0);
|
||||||
|
|
||||||
|
const onScrollUpRef = useRef(onScrollUp);
|
||||||
|
const onScrollDownRef = useRef(onScrollDown);
|
||||||
|
const deltaYMultiplierRef = useRef(deltaYMultiplier);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onScrollUpRef.current = onScrollUp;
|
||||||
|
onScrollDownRef.current = onScrollDown;
|
||||||
|
deltaYMultiplierRef.current = deltaYMultiplier;
|
||||||
|
}, [onScrollUp, onScrollDown, deltaYMultiplier]);
|
||||||
|
|
||||||
|
const handleWheelEventRef = useRef((_: WheelEvent) => {});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleWheelEventRef.current = (event: WheelEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
setScrollLength((prev) =>
|
||||||
|
handleScroll(
|
||||||
|
prev,
|
||||||
|
event.deltaY * deltaYMultiplierRef.current,
|
||||||
|
onScrollDownRef.current,
|
||||||
|
onScrollUpRef.current,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function addScrollListener() {
|
||||||
|
const currentRef = targetRef.current;
|
||||||
|
if (currentRef) {
|
||||||
|
currentRef.addEventListener("wheel", handleWheelEventRef.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeScrollListener() {
|
||||||
|
const currentRef = targetRef.current;
|
||||||
|
if (currentRef) {
|
||||||
|
currentRef.removeEventListener("wheel", handleWheelEventRef.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
removeScrollListener();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
addScrollListener,
|
||||||
|
removeScrollListener,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
+171
-92
@@ -2,13 +2,30 @@ import { useState, useRef, useEffect } from "react";
|
|||||||
import type { Dispatch, SetStateAction, RefObject } from "react";
|
import type { Dispatch, SetStateAction, RefObject } from "react";
|
||||||
import { minmax } from "../util";
|
import { minmax } from "../util";
|
||||||
import type { CartesianSpace } from "../types";
|
import type { CartesianSpace } from "../types";
|
||||||
import { handleScroll } from "./scroll";
|
import { useScroll } from "./scroll";
|
||||||
|
|
||||||
|
if (typeof TouchEvent === "undefined") {
|
||||||
|
// @ts-ignore - intentionally creating global
|
||||||
|
window.TouchEvent = window.MouseEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTouchEvent(event: Event): event is TouchEvent {
|
||||||
|
return "touches" in event;
|
||||||
|
}
|
||||||
|
|
||||||
export enum Direction {
|
export enum Direction {
|
||||||
HORIZONTAL = "horizontal",
|
HORIZONTAL = "horizontal",
|
||||||
VERTICAL = "vertical",
|
VERTICAL = "vertical",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function chooseValueByDirection(
|
||||||
|
direction: Direction,
|
||||||
|
xValue: number,
|
||||||
|
yValue: number,
|
||||||
|
) {
|
||||||
|
return direction === Direction.HORIZONTAL ? xValue : yValue;
|
||||||
|
}
|
||||||
|
|
||||||
export function useSlider({
|
export function useSlider({
|
||||||
direction,
|
direction,
|
||||||
origin,
|
origin,
|
||||||
@@ -22,107 +39,169 @@ export function useSlider({
|
|||||||
}): { sliderRef: RefObject<HTMLDivElement | null>; isDragging: boolean } {
|
}): { sliderRef: RefObject<HTMLDivElement | null>; isDragging: boolean } {
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const sliderRef = useRef<HTMLDivElement>(null);
|
const sliderRef = useRef<HTMLDivElement>(null);
|
||||||
const [_, setScrollLength] = useState(0);
|
|
||||||
|
|
||||||
function calculatePosition(event: MouseEvent | TouchEvent) {
|
// Construct event handler refs
|
||||||
const clientCoord =
|
// Prevents unnecessary function recreation
|
||||||
event instanceof TouchEvent
|
const calculatePositionRef = useRef((_: MouseEvent | TouchEvent) => {});
|
||||||
? direction === Direction.HORIZONTAL
|
const startSliderInteractionRef = useRef((_: MouseEvent | TouchEvent) => {});
|
||||||
? event.touches[0].clientX
|
const processSliderInteractionRef = useRef(
|
||||||
: event.touches[0].clientY
|
(_: MouseEvent | TouchEvent) => {},
|
||||||
: direction === Direction.HORIZONTAL
|
);
|
||||||
? event.clientX
|
const endSliderInteractionRef = useRef((_: MouseEvent | TouchEvent) => {});
|
||||||
: event.clientY;
|
|
||||||
const positionValue = minmax(
|
|
||||||
clientCoord - (direction === Direction.HORIZONTAL ? origin.x : origin.y),
|
|
||||||
0,
|
|
||||||
direction === Direction.HORIZONTAL ? dimensions.x : dimensions.y,
|
|
||||||
);
|
|
||||||
setPosition(positionValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
function startSliderInteraction(event: MouseEvent | TouchEvent) {
|
// Store dependencies as refs
|
||||||
event.preventDefault();
|
// Always use latest values
|
||||||
calculatePosition(event);
|
const directionRef = useRef(direction);
|
||||||
setIsDragging(true);
|
const originRef = useRef(origin);
|
||||||
if (event instanceof MouseEvent) {
|
const dimensionsRef = useRef(dimensions);
|
||||||
document.addEventListener("mousemove", processSliderInteraction);
|
|
||||||
document.addEventListener("mouseup", endSliderInteraction);
|
|
||||||
} else {
|
|
||||||
document.addEventListener("touchmove", processSliderInteraction);
|
|
||||||
document.addEventListener("touchend", endSliderInteraction);
|
|
||||||
document.addEventListener("touchcancel", endSliderInteraction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function processSliderInteraction(event: MouseEvent | TouchEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
calculatePosition(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
function endSliderInteraction(event: MouseEvent | TouchEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
setIsDragging(false);
|
|
||||||
if (event instanceof MouseEvent) {
|
|
||||||
document.removeEventListener("mousemove", processSliderInteraction);
|
|
||||||
document.removeEventListener("mouseup", endSliderInteraction);
|
|
||||||
} else {
|
|
||||||
document.removeEventListener("touchmove", processSliderInteraction);
|
|
||||||
document.removeEventListener("touchend", endSliderInteraction);
|
|
||||||
document.removeEventListener("touchcancel", endSliderInteraction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function adjustPositionWithScroll(event: WheelEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
setScrollLength((prev) =>
|
|
||||||
handleScroll(
|
|
||||||
prev,
|
|
||||||
direction === Direction.HORIZONTAL ? event.deltaY : -event.deltaY,
|
|
||||||
() =>
|
|
||||||
setPosition((prev: number) =>
|
|
||||||
minmax(
|
|
||||||
prev - 1,
|
|
||||||
0,
|
|
||||||
direction === Direction.HORIZONTAL ? dimensions.x : dimensions.y,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
() =>
|
|
||||||
setPosition((prev: number) =>
|
|
||||||
minmax(
|
|
||||||
prev + 1,
|
|
||||||
0,
|
|
||||||
direction === Direction.HORIZONTAL ? dimensions.x : dimensions.y,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sliderRef.current) {
|
directionRef.current = direction;
|
||||||
sliderRef.current.addEventListener("wheel", adjustPositionWithScroll);
|
originRef.current = origin;
|
||||||
sliderRef.current.addEventListener("mousedown", startSliderInteraction);
|
dimensionsRef.current = dimensions;
|
||||||
sliderRef.current.addEventListener("touchstart", startSliderInteraction);
|
}, [direction, origin, dimensions]);
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
// Setup scroll handlers
|
||||||
if (sliderRef.current) {
|
const handleScrollUp = () => {
|
||||||
sliderRef.current.removeEventListener(
|
const dir = directionRef.current;
|
||||||
"wheel",
|
const dims = dimensionsRef.current;
|
||||||
adjustPositionWithScroll,
|
|
||||||
|
setPosition((prev: number) =>
|
||||||
|
minmax(prev - 1, 0, chooseValueByDirection(dir, dims.x, dims.y)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScrollDown = () => {
|
||||||
|
const dir = directionRef.current;
|
||||||
|
const dims = dimensionsRef.current;
|
||||||
|
|
||||||
|
setPosition((prev: number) =>
|
||||||
|
minmax(prev + 1, 0, chooseValueByDirection(dir, dims.x, dims.y)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize scroll handling
|
||||||
|
const { addScrollListener, removeScrollListener } = useScroll({
|
||||||
|
targetRef: sliderRef,
|
||||||
|
onScrollUp: handleScrollUp,
|
||||||
|
onScrollDown: handleScrollDown,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update handler functions when dependencies change via reference
|
||||||
|
useEffect(() => {
|
||||||
|
calculatePositionRef.current = (event: MouseEvent | TouchEvent) => {
|
||||||
|
const dir = directionRef.current;
|
||||||
|
const orig = originRef.current;
|
||||||
|
const dims = dimensionsRef.current;
|
||||||
|
|
||||||
|
const clientCoord = isTouchEvent(event)
|
||||||
|
? chooseValueByDirection(
|
||||||
|
dir,
|
||||||
|
event.touches[0].clientX,
|
||||||
|
event.touches[0].clientY,
|
||||||
|
)
|
||||||
|
: chooseValueByDirection(dir, event.clientX, event.clientY);
|
||||||
|
const positionValue = minmax(
|
||||||
|
clientCoord - chooseValueByDirection(dir, orig.x, orig.y),
|
||||||
|
0,
|
||||||
|
chooseValueByDirection(dir, dims.x, dims.y),
|
||||||
|
);
|
||||||
|
setPosition(positionValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
startSliderInteractionRef.current = (event: MouseEvent | TouchEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
calculatePositionRef.current(event);
|
||||||
|
setIsDragging(true);
|
||||||
|
|
||||||
|
if (!isTouchEvent(event)) {
|
||||||
|
document.addEventListener(
|
||||||
|
"mousemove",
|
||||||
|
processSliderInteractionRef.current,
|
||||||
);
|
);
|
||||||
sliderRef.current.removeEventListener(
|
document.addEventListener("mouseup", endSliderInteractionRef.current, {
|
||||||
"mousedown",
|
passive: true,
|
||||||
startSliderInteraction,
|
});
|
||||||
|
} else {
|
||||||
|
document.addEventListener(
|
||||||
|
"touchmove",
|
||||||
|
processSliderInteractionRef.current,
|
||||||
);
|
);
|
||||||
sliderRef.current.removeEventListener(
|
document.addEventListener("touchend", endSliderInteractionRef.current, {
|
||||||
"touchstart",
|
passive: true,
|
||||||
startSliderInteraction,
|
});
|
||||||
|
document.addEventListener(
|
||||||
|
"touchcancel",
|
||||||
|
endSliderInteractionRef.current,
|
||||||
|
{
|
||||||
|
passive: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
|
||||||
|
processSliderInteractionRef.current = (event: MouseEvent | TouchEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
calculatePositionRef.current(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
endSliderInteractionRef.current = (event: MouseEvent | TouchEvent) => {
|
||||||
|
setIsDragging(false);
|
||||||
|
if (!isTouchEvent(event)) {
|
||||||
|
document.removeEventListener(
|
||||||
|
"mousemove",
|
||||||
|
processSliderInteractionRef.current,
|
||||||
|
);
|
||||||
|
document.removeEventListener(
|
||||||
|
"mouseup",
|
||||||
|
endSliderInteractionRef.current,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
document.removeEventListener(
|
||||||
|
"touchmove",
|
||||||
|
processSliderInteractionRef.current,
|
||||||
|
);
|
||||||
|
document.removeEventListener(
|
||||||
|
"touchend",
|
||||||
|
endSliderInteractionRef.current,
|
||||||
|
);
|
||||||
|
document.removeEventListener(
|
||||||
|
"touchcancel",
|
||||||
|
endSliderInteractionRef.current,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Set up entry listeners
|
||||||
|
useEffect(() => {
|
||||||
|
const currentRef = sliderRef.current;
|
||||||
|
if (currentRef) {
|
||||||
|
addScrollListener();
|
||||||
|
currentRef.addEventListener(
|
||||||
|
"mousedown",
|
||||||
|
startSliderInteractionRef.current,
|
||||||
|
);
|
||||||
|
currentRef.addEventListener(
|
||||||
|
"touchstart",
|
||||||
|
startSliderInteractionRef.current,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (currentRef) {
|
||||||
|
removeScrollListener();
|
||||||
|
currentRef.removeEventListener(
|
||||||
|
"mousedown",
|
||||||
|
startSliderInteractionRef.current,
|
||||||
|
);
|
||||||
|
currentRef.removeEventListener(
|
||||||
|
"touchstart",
|
||||||
|
startSliderInteractionRef.current,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return { sliderRef, isDragging };
|
return { sliderRef, isDragging };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
import { useCrosshair } from "../crosshair";
|
||||||
|
import type { CartesianSpace } from "../../types";
|
||||||
|
|
||||||
|
// Test Fixtures
|
||||||
|
|
||||||
|
function TestSquare() {
|
||||||
|
const [origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||||
|
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||||
|
const [xPosition, setXPosition] = useState(0);
|
||||||
|
const [yPosition, setYPosition] = useState(0);
|
||||||
|
|
||||||
|
const { crosshairRef, isDragging } = useCrosshair({
|
||||||
|
origin,
|
||||||
|
dimensions,
|
||||||
|
setXPosition,
|
||||||
|
setYPosition,
|
||||||
|
});
|
||||||
|
|
||||||
|
const boundaryRef = useRef<HTMLDivElement>(null);
|
||||||
|
const xCrosshairRef = useRef<HTMLSpanElement>(null);
|
||||||
|
const yCrosshairRef = useRef<HTMLSpanElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const element = boundaryRef.current;
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
|
||||||
|
setOrigin({ x: rect.left, y: rect.top });
|
||||||
|
setDimensions({ x: rect.width, y: rect.height });
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
ref={crosshairRef}
|
||||||
|
data-cy="crosshair-container"
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
width: 250,
|
||||||
|
height: 250,
|
||||||
|
margin: 50,
|
||||||
|
border: "3px solid black",
|
||||||
|
cursor: "crosshair",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={boundaryRef}
|
||||||
|
data-cy="boundary"
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 12,
|
||||||
|
bottom: 12,
|
||||||
|
right: 12,
|
||||||
|
left: 12,
|
||||||
|
background: "rgb(200, 200, 200)",
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<span
|
||||||
|
ref={yCrosshairRef}
|
||||||
|
data-cy="y-crosshair"
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
width: 250,
|
||||||
|
height: 25,
|
||||||
|
top: yPosition,
|
||||||
|
left: 0,
|
||||||
|
background: "rgba(255, 0, 0, 0.5)",
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
ref={xCrosshairRef}
|
||||||
|
data-cy="x-crosshair"
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
width: 25,
|
||||||
|
height: 250,
|
||||||
|
top: 0,
|
||||||
|
left: xPosition,
|
||||||
|
background: "rgba(0, 255, 0, 0.5)",
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
X Position: <span data-cy="x-position-display">{xPosition}</span>px
|
||||||
|
<br />Y Position: <span data-cy="y-position-display">{yPosition}</span>
|
||||||
|
px
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Is Dragging:{" "}
|
||||||
|
<span data-cy="is-dragging-display">
|
||||||
|
{isDragging ? "True" : "False"}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
|
||||||
|
const assertPosition = (
|
||||||
|
expectedXPosition: number,
|
||||||
|
expectedYPosition: number,
|
||||||
|
) => {
|
||||||
|
cy.dataCy("x-position-display").should("contain", expectedXPosition);
|
||||||
|
cy.dataCy("y-position-display").should("contain", expectedYPosition);
|
||||||
|
|
||||||
|
cy.dataCy("x-crosshair").should("have.css", "left", `${expectedXPosition}px`);
|
||||||
|
cy.dataCy("y-crosshair").should("have.css", "top", `${expectedYPosition}px`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerMouseEvent = (eventType: string, x: number, y: number) => {
|
||||||
|
cy.dataCy("crosshair-container").trigger(eventType, {
|
||||||
|
clientX: x,
|
||||||
|
clientY: y,
|
||||||
|
eventConstructor: "MouseEvent",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerTouchEvent = (eventType: string, x: number, y: number) => {
|
||||||
|
cy.dataCy("crosshair-container").trigger(eventType, {
|
||||||
|
clientX: x,
|
||||||
|
clientY: y,
|
||||||
|
touches: [{ clientX: x, clientY: y }],
|
||||||
|
eventConstructor: "TouchEvent",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("crosshair tests", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.mount(<TestSquare />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moves the crosshairs with mouse events", () => {
|
||||||
|
assertPosition(0, 0);
|
||||||
|
|
||||||
|
triggerMouseEvent("mousedown", 70, 60);
|
||||||
|
assertPosition(0, 0);
|
||||||
|
|
||||||
|
triggerMouseEvent("mousemove", 180, 180);
|
||||||
|
assertPosition(107, 115);
|
||||||
|
|
||||||
|
triggerMouseEvent("mousemove", 500, 500);
|
||||||
|
assertPosition(225, 225);
|
||||||
|
|
||||||
|
triggerMouseEvent("mousemove", 0, 250);
|
||||||
|
assertPosition(0, 185);
|
||||||
|
|
||||||
|
triggerMouseEvent("mouseup", 0, 250);
|
||||||
|
assertPosition(0, 185);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moves the crosshairs with touch events", () => {
|
||||||
|
assertPosition(0, 0);
|
||||||
|
|
||||||
|
triggerTouchEvent("touchstart", 70, 60);
|
||||||
|
assertPosition(0, 0);
|
||||||
|
|
||||||
|
triggerTouchEvent("touchmove", 180, 180);
|
||||||
|
assertPosition(107, 115);
|
||||||
|
|
||||||
|
triggerTouchEvent("touchmove", 500, 500);
|
||||||
|
assertPosition(225, 225);
|
||||||
|
|
||||||
|
triggerTouchEvent("touchmove", 0, 250);
|
||||||
|
assertPosition(0, 185);
|
||||||
|
|
||||||
|
triggerTouchEvent("touchend", 0, 250);
|
||||||
|
assertPosition(0, 185);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -4,18 +4,17 @@ import type { CartesianSpace } from "../../types";
|
|||||||
|
|
||||||
// Test Fixtures
|
// Test Fixtures
|
||||||
|
|
||||||
function printMeasurements(name: string, rect: DOMRect | undefined) {
|
function TestSlider({
|
||||||
console.log(
|
direction = Direction.HORIZONTAL,
|
||||||
`${name} Measurements: (${rect?.left}, ${rect?.top}), (${rect?.width}, ${rect?.height})`,
|
}: {
|
||||||
);
|
direction?: Direction;
|
||||||
}
|
}) {
|
||||||
|
|
||||||
function TestSlider() {
|
|
||||||
const [origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 });
|
const [origin, setOrigin] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||||
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
|
const [dimensions, setDimensions] = useState<CartesianSpace>({ x: 0, y: 0 });
|
||||||
const [position, setPosition] = useState(0);
|
const [position, setPosition] = useState(0);
|
||||||
|
const [sliderValue, setSliderValue] = useState(0);
|
||||||
const { sliderRef, isDragging } = useSlider({
|
const { sliderRef, isDragging } = useSlider({
|
||||||
direction: Direction.HORIZONTAL,
|
direction,
|
||||||
origin,
|
origin,
|
||||||
dimensions,
|
dimensions,
|
||||||
setPosition,
|
setPosition,
|
||||||
@@ -32,10 +31,23 @@ function TestSlider() {
|
|||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
|
|
||||||
setOrigin({ x: rect.left, y: rect.top });
|
setOrigin({ x: rect.left, y: rect.top });
|
||||||
setDimensions({ x: rect.width, y: rect!.height });
|
setDimensions({ x: rect.width, y: rect.height });
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const maxValue =
|
||||||
|
direction == Direction.HORIZONTAL ? dimensions.x : dimensions.y;
|
||||||
|
if (maxValue > 0) {
|
||||||
|
const percentage = parseFloat(((position / maxValue) * 100).toFixed(3));
|
||||||
|
setSliderValue(percentage);
|
||||||
|
} else {
|
||||||
|
setSliderValue(0);
|
||||||
|
}
|
||||||
|
}, [dimensions, direction, position]);
|
||||||
|
|
||||||
|
const isHorizontal = direction === Direction.HORIZONTAL;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@@ -43,8 +55,8 @@ function TestSlider() {
|
|||||||
ref={sliderRef}
|
ref={sliderRef}
|
||||||
style={{
|
style={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
width: 375,
|
width: isHorizontal ? 300 : 50,
|
||||||
height: 50,
|
height: isHorizontal ? 50 : 300,
|
||||||
border: "3px solid black",
|
border: "3px solid black",
|
||||||
margin: 50,
|
margin: 50,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
@@ -55,10 +67,10 @@ function TestSlider() {
|
|||||||
ref={railRef}
|
ref={railRef}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: 25,
|
left: isHorizontal ? 25 : 0,
|
||||||
right: 25,
|
right: isHorizontal ? 25 : 0,
|
||||||
top: 0,
|
top: isHorizontal ? 0 : 25,
|
||||||
bottom: 0,
|
bottom: isHorizontal ? 0 : 25,
|
||||||
background: "rgb(200,200,200)",
|
background: "rgb(200,200,200)",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -67,8 +79,9 @@ function TestSlider() {
|
|||||||
ref={handleRef}
|
ref={handleRef}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: position,
|
...(isHorizontal
|
||||||
top: 0,
|
? { left: position, top: 0 }
|
||||||
|
: { left: 0, top: position }),
|
||||||
width: 50,
|
width: 50,
|
||||||
height: 50,
|
height: 50,
|
||||||
background: "rgba(255,0,0,0.5)",
|
background: "rgba(255,0,0,0.5)",
|
||||||
@@ -78,23 +91,18 @@ function TestSlider() {
|
|||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
Position: <span data-cy="position-display">{position}</span>px
|
Position: <span data-cy="position-display">{position}</span>px
|
||||||
|
<br /> Value: <span data-cy="value-display">{sliderValue}</span>
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tests
|
function createTestUtils(isHorizontal = true) {
|
||||||
|
|
||||||
describe("Slider Hook Tests", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.mount(<TestSlider />);
|
|
||||||
});
|
|
||||||
|
|
||||||
const assertPosition = (expectedPosition: number) => {
|
const assertPosition = (expectedPosition: number) => {
|
||||||
cy.dataCy("position-display").should("contain", expectedPosition);
|
cy.dataCy("position-display").should("contain", expectedPosition);
|
||||||
cy.dataCy("slider-handle").should(
|
cy.dataCy("slider-handle").should(
|
||||||
"have.css",
|
"have.css",
|
||||||
"left",
|
isHorizontal ? "left" : "top",
|
||||||
`${expectedPosition}px`,
|
`${expectedPosition}px`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -107,7 +115,49 @@ describe("Slider Hook Tests", () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
it("moves the slider with mouse drag.", () => {
|
const triggerTouchEvent = (eventType: string, x: number, y: number) => {
|
||||||
|
cy.dataCy("slider-container").trigger(eventType, {
|
||||||
|
clientX: x,
|
||||||
|
clientY: y,
|
||||||
|
touches: [{ clientX: x, clientY: y }],
|
||||||
|
eventConstructor: "TouchEvent",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerWheelEvent = (deltaY: number) => {
|
||||||
|
cy.dataCy("slider-container").trigger("wheel", {
|
||||||
|
deltaY,
|
||||||
|
eventConstructor: "WheelEvent",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
assertPosition,
|
||||||
|
triggerMouseEvent,
|
||||||
|
triggerTouchEvent,
|
||||||
|
triggerWheelEvent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTouchSupported = () => {
|
||||||
|
return typeof TouchEvent !== "undefined";
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tests
|
||||||
|
|
||||||
|
describe("horizontal slider hook tests", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.mount(<TestSlider direction={Direction.HORIZONTAL} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
assertPosition,
|
||||||
|
triggerMouseEvent,
|
||||||
|
triggerTouchEvent,
|
||||||
|
triggerWheelEvent,
|
||||||
|
} = createTestUtils(true);
|
||||||
|
|
||||||
|
it("moves the slider with mouse events.", () => {
|
||||||
assertPosition(0);
|
assertPosition(0);
|
||||||
|
|
||||||
triggerMouseEvent("mousedown", 86, 53);
|
triggerMouseEvent("mousedown", 86, 53);
|
||||||
@@ -117,7 +167,7 @@ describe("Slider Hook Tests", () => {
|
|||||||
assertPosition(64);
|
assertPosition(64);
|
||||||
|
|
||||||
triggerMouseEvent("mousemove", 500, 500);
|
triggerMouseEvent("mousemove", 500, 500);
|
||||||
assertPosition(325);
|
assertPosition(250);
|
||||||
|
|
||||||
triggerMouseEvent("mousemove", 250, 250);
|
triggerMouseEvent("mousemove", 250, 250);
|
||||||
assertPosition(164);
|
assertPosition(164);
|
||||||
@@ -125,4 +175,132 @@ describe("Slider Hook Tests", () => {
|
|||||||
triggerMouseEvent("mouseup", 250, 250);
|
triggerMouseEvent("mouseup", 250, 250);
|
||||||
assertPosition(164);
|
assertPosition(164);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isTouchSupported()) {
|
||||||
|
it("moves the slider with touch events.", () => {
|
||||||
|
assertPosition(0);
|
||||||
|
|
||||||
|
triggerTouchEvent("touchstart", 86, 53);
|
||||||
|
assertPosition(0);
|
||||||
|
|
||||||
|
triggerTouchEvent("touchmove", 150, 150);
|
||||||
|
assertPosition(64);
|
||||||
|
|
||||||
|
triggerTouchEvent("touchmove", 500, 500);
|
||||||
|
assertPosition(250);
|
||||||
|
|
||||||
|
triggerTouchEvent("touchmove", 250, 250);
|
||||||
|
assertPosition(164);
|
||||||
|
|
||||||
|
triggerTouchEvent("touchend", 250, 250);
|
||||||
|
assertPosition(164);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("Skipping Unsupported Touch Event Tests");
|
||||||
|
}
|
||||||
|
|
||||||
|
it("moves the slider with mouse wheel scrolling", () => {
|
||||||
|
assertPosition(0);
|
||||||
|
|
||||||
|
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);
|
||||||
|
assertPosition(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("vertical slider hook tests", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.mount(<TestSlider direction={Direction.VERTICAL} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
assertPosition,
|
||||||
|
triggerMouseEvent,
|
||||||
|
triggerTouchEvent,
|
||||||
|
triggerWheelEvent,
|
||||||
|
} = createTestUtils(false);
|
||||||
|
|
||||||
|
it("moves the slider with mouse drag.", () => {
|
||||||
|
assertPosition(0);
|
||||||
|
|
||||||
|
triggerMouseEvent("mousedown", 86, 53);
|
||||||
|
assertPosition(0);
|
||||||
|
|
||||||
|
triggerMouseEvent("mousemove", 150, 150);
|
||||||
|
assertPosition(72);
|
||||||
|
|
||||||
|
triggerMouseEvent("mousemove", 500, 500);
|
||||||
|
assertPosition(250);
|
||||||
|
|
||||||
|
triggerMouseEvent("mousemove", 250, 250);
|
||||||
|
assertPosition(172);
|
||||||
|
|
||||||
|
triggerMouseEvent("mouseup", 250, 250);
|
||||||
|
assertPosition(172);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isTouchSupported()) {
|
||||||
|
it("moves the slider with touch events.", () => {
|
||||||
|
assertPosition(0);
|
||||||
|
|
||||||
|
triggerTouchEvent("touchstart", 86, 53);
|
||||||
|
assertPosition(0);
|
||||||
|
|
||||||
|
triggerTouchEvent("touchmove", 150, 150);
|
||||||
|
assertPosition(72);
|
||||||
|
|
||||||
|
triggerTouchEvent("touchmove", 500, 500);
|
||||||
|
assertPosition(250);
|
||||||
|
|
||||||
|
triggerTouchEvent("touchmove", 250, 250);
|
||||||
|
assertPosition(172);
|
||||||
|
|
||||||
|
triggerTouchEvent("touchend", 250, 250);
|
||||||
|
assertPosition(172);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("Skipping Unsupported Touch Event Tests");
|
||||||
|
}
|
||||||
|
|
||||||
|
it("moves the slider with mouse wheel scrolling", () => {
|
||||||
|
assertPosition(0);
|
||||||
|
|
||||||
|
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);
|
||||||
|
assertPosition(4);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+5
-10
@@ -1,17 +1,12 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from "vite";
|
||||||
import react from '@vitejs/plugin-react'
|
import react from "@vitejs/plugin-react";
|
||||||
import wasm from "vite-plugin-wasm";
|
import wasm from "vite-plugin-wasm";
|
||||||
import topLevelAwait from "vite-plugin-top-level-await";
|
import topLevelAwait from "vite-plugin-top-level-await";
|
||||||
|
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [react(), wasm(), topLevelAwait()],
|
||||||
react(),
|
|
||||||
wasm(),
|
|
||||||
topLevelAwait(),
|
|
||||||
],
|
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user