From 051c6602b5359d357e308ffce34ff31603aac9ca Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 23 Jul 2025 11:05:47 -0400 Subject: [PATCH] Completed crosshair, slider, and scroll hooks. --- src/hooks/crosshair.tsx | 167 +++++++++++++++++ src/hooks/scroll.ts | 71 ++++++++ src/hooks/slider.tsx | 263 +++++++++++++++++---------- src/hooks/tests/crosshairTest.cy.tsx | 175 ++++++++++++++++++ src/hooks/tests/scrollTest.cy.tsx | 0 src/hooks/tests/sliderTest.cy.tsx | 232 ++++++++++++++++++++--- vite.config.ts | 15 +- 7 files changed, 794 insertions(+), 129 deletions(-) delete mode 100644 src/hooks/tests/scrollTest.cy.tsx diff --git a/src/hooks/crosshair.tsx b/src/hooks/crosshair.tsx index e69de29..bed322b 100644 --- a/src/hooks/crosshair.tsx +++ b/src/hooks/crosshair.tsx @@ -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>; + setYPosition: Dispatch>; +}) { + const [isDragging, setIsDragging] = useState(false); + const crosshairRef = useRef(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 }; +} diff --git a/src/hooks/scroll.ts b/src/hooks/scroll.ts index 7cefe15..e009e1e 100644 --- a/src/hooks/scroll.ts +++ b/src/hooks/scroll.ts @@ -1,3 +1,6 @@ +import { useState, useRef, useEffect } from "react"; +import type { RefObject } from "react"; + export function handleScroll( prevLength: number, scrollDelta: number, @@ -18,3 +21,71 @@ export function handleScroll( return newLength; } + +type ScrollHandler = () => void; + +export function useScroll({ + targetRef, + onScrollUp, + onScrollDown, + deltaYMultiplier: deltaYMultiplier = 1, +}: { + targetRef: RefObject; + 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, + }; +} diff --git a/src/hooks/slider.tsx b/src/hooks/slider.tsx index 2297d4c..13d9055 100644 --- a/src/hooks/slider.tsx +++ b/src/hooks/slider.tsx @@ -2,13 +2,30 @@ import { useState, useRef, useEffect } from "react"; import type { Dispatch, SetStateAction, RefObject } from "react"; import { minmax } from "../util"; 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 { HORIZONTAL = "horizontal", VERTICAL = "vertical", } +function chooseValueByDirection( + direction: Direction, + xValue: number, + yValue: number, +) { + return direction === Direction.HORIZONTAL ? xValue : yValue; +} + export function useSlider({ direction, origin, @@ -22,107 +39,169 @@ export function useSlider({ }): { sliderRef: RefObject; isDragging: boolean } { const [isDragging, setIsDragging] = useState(false); const sliderRef = useRef(null); - const [_, setScrollLength] = useState(0); - function calculatePosition(event: MouseEvent | TouchEvent) { - const clientCoord = - event instanceof TouchEvent - ? direction === Direction.HORIZONTAL - ? event.touches[0].clientX - : event.touches[0].clientY - : direction === Direction.HORIZONTAL - ? event.clientX - : event.clientY; - const positionValue = minmax( - clientCoord - (direction === Direction.HORIZONTAL ? origin.x : origin.y), - 0, - direction === Direction.HORIZONTAL ? dimensions.x : dimensions.y, - ); - setPosition(positionValue); - } + // Construct event handler refs + // Prevents unnecessary function recreation + const calculatePositionRef = useRef((_: MouseEvent | TouchEvent) => {}); + const startSliderInteractionRef = useRef((_: MouseEvent | TouchEvent) => {}); + const processSliderInteractionRef = useRef( + (_: MouseEvent | TouchEvent) => {}, + ); + const endSliderInteractionRef = useRef((_: MouseEvent | TouchEvent) => {}); - function startSliderInteraction(event: MouseEvent | TouchEvent) { - event.preventDefault(); - calculatePosition(event); - setIsDragging(true); - if (event instanceof MouseEvent) { - 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, - ), - ), - ), - ); - } + // Store dependencies as refs + // Always use latest values + const directionRef = useRef(direction); + const originRef = useRef(origin); + const dimensionsRef = useRef(dimensions); useEffect(() => { - if (sliderRef.current) { - sliderRef.current.addEventListener("wheel", adjustPositionWithScroll); - sliderRef.current.addEventListener("mousedown", startSliderInteraction); - sliderRef.current.addEventListener("touchstart", startSliderInteraction); - } + directionRef.current = direction; + originRef.current = origin; + dimensionsRef.current = dimensions; + }, [direction, origin, dimensions]); - return () => { - if (sliderRef.current) { - sliderRef.current.removeEventListener( - "wheel", - adjustPositionWithScroll, + // Setup scroll handlers + const handleScrollUp = () => { + const dir = directionRef.current; + const dims = dimensionsRef.current; + + 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( - "mousedown", - startSliderInteraction, + document.addEventListener("mouseup", endSliderInteractionRef.current, { + passive: true, + }); + } else { + document.addEventListener( + "touchmove", + processSliderInteractionRef.current, ); - sliderRef.current.removeEventListener( - "touchstart", - startSliderInteraction, + document.addEventListener("touchend", endSliderInteractionRef.current, { + passive: true, + }); + 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 }; } diff --git a/src/hooks/tests/crosshairTest.cy.tsx b/src/hooks/tests/crosshairTest.cy.tsx index e69de29..c038c68 100644 --- a/src/hooks/tests/crosshairTest.cy.tsx +++ b/src/hooks/tests/crosshairTest.cy.tsx @@ -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({ x: 0, y: 0 }); + const [dimensions, setDimensions] = useState({ 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(null); + const xCrosshairRef = useRef(null); + const yCrosshairRef = useRef(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 ( + <> +
+
+ + +
+

+ X Position: {xPosition}px +
Y Position: {yPosition} + px +

+

+ Is Dragging:{" "} + + {isDragging ? "True" : "False"} + +

+ + ); +} + +// 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(); + }); + + 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); + }); +}); diff --git a/src/hooks/tests/scrollTest.cy.tsx b/src/hooks/tests/scrollTest.cy.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/hooks/tests/sliderTest.cy.tsx b/src/hooks/tests/sliderTest.cy.tsx index 05cfe01..17b4347 100644 --- a/src/hooks/tests/sliderTest.cy.tsx +++ b/src/hooks/tests/sliderTest.cy.tsx @@ -4,18 +4,17 @@ import type { CartesianSpace } from "../../types"; // Test Fixtures -function printMeasurements(name: string, rect: DOMRect | undefined) { - console.log( - `${name} Measurements: (${rect?.left}, ${rect?.top}), (${rect?.width}, ${rect?.height})`, - ); -} - -function TestSlider() { +function TestSlider({ + direction = Direction.HORIZONTAL, +}: { + direction?: Direction; +}) { const [origin, setOrigin] = useState({ x: 0, y: 0 }); const [dimensions, setDimensions] = useState({ x: 0, y: 0 }); const [position, setPosition] = useState(0); + const [sliderValue, setSliderValue] = useState(0); const { sliderRef, isDragging } = useSlider({ - direction: Direction.HORIZONTAL, + direction, origin, dimensions, setPosition, @@ -32,10 +31,23 @@ function TestSlider() { const rect = element.getBoundingClientRect(); 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 ( <>
@@ -67,8 +79,9 @@ function TestSlider() { ref={handleRef} style={{ position: "absolute", - left: position, - top: 0, + ...(isHorizontal + ? { left: position, top: 0 } + : { left: 0, top: position }), width: 50, height: 50, background: "rgba(255,0,0,0.5)", @@ -78,23 +91,18 @@ function TestSlider() {

Position: {position}px +
Value: {sliderValue}

); } -// Tests - -describe("Slider Hook Tests", () => { - beforeEach(() => { - cy.mount(); - }); - +function createTestUtils(isHorizontal = true) { const assertPosition = (expectedPosition: number) => { cy.dataCy("position-display").should("contain", expectedPosition); cy.dataCy("slider-handle").should( "have.css", - "left", + isHorizontal ? "left" : "top", `${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(); + }); + + const { + assertPosition, + triggerMouseEvent, + triggerTouchEvent, + triggerWheelEvent, + } = createTestUtils(true); + + it("moves the slider with mouse events.", () => { assertPosition(0); triggerMouseEvent("mousedown", 86, 53); @@ -117,7 +167,7 @@ describe("Slider Hook Tests", () => { assertPosition(64); triggerMouseEvent("mousemove", 500, 500); - assertPosition(325); + assertPosition(250); triggerMouseEvent("mousemove", 250, 250); assertPosition(164); @@ -125,4 +175,132 @@ describe("Slider Hook Tests", () => { triggerMouseEvent("mouseup", 250, 250); 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(); + }); + + 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); + }); }); diff --git a/vite.config.ts b/vite.config.ts index 90c0136..dc75322 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,17 +1,12 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; import wasm from "vite-plugin-wasm"; import topLevelAwait from "vite-plugin-top-level-await"; - // https://vite.dev/config/ export default defineConfig({ - plugins: [ - react(), - wasm(), - topLevelAwait(), - ], + plugins: [react(), wasm(), topLevelAwait()], server: { port: 5173, - } -}) + }, +});