import { type KeyboardEvent, type PointerEvent, type ReactNode, useCallback, useRef, } from "react"; import { fractionForValue, keyboardStep, resolveSliderValue, } from "./slider-helpers.js"; import type { SliderVProps } from "./slider-v.types.js"; type DivPointerEvent = PointerEvent; type DivKeyboardEvent = KeyboardEvent; const DEFAULT_TRACK_PX = 160; const DEFAULT_THICKNESS_PX = 24; const DEFAULT_STEP = 0.01; const DEFAULT_FINE_FACTOR = 0.1; /** * Vertical slider with optional detents. Reports its range via * standard `aria-valuemin/max/now` attributes and behaves as a * `role="slider"` with `aria-orientation="vertical"`. */ export function SliderV(props: SliderVProps): ReactNode { const { value, onChange, min, max, step = DEFAULT_STEP, disabled, label, ariaLabel, detents, snapThreshold, trackPx = DEFAULT_TRACK_PX, thicknessPx = DEFAULT_THICKNESS_PX, fineModeFactor = DEFAULT_FINE_FACTOR, className, } = props; const dragRef = useRef<{ pointerId: number } | null>(null); const emit = useCallback( (raw: number) => { const next = resolveSliderValue({ raw, min, max, step, detents, snapThreshold, }); if (next !== value) onChange(next); }, [min, max, step, detents, snapThreshold, value, onChange] ); const valueFromClientY = useCallback( (event: DivPointerEvent) => { const rect = event.currentTarget.getBoundingClientRect(); const offsetY = event.clientY - rect.top; const usable = rect.height === 0 ? trackPx : rect.height; const t = 1 - offsetY / usable; return min + Math.max(0, Math.min(1, t)) * (max - min); }, [min, max, trackPx] ); const onPointerDown = useCallback( (event: DivPointerEvent) => { if (disabled) return; event.currentTarget.setPointerCapture(event.pointerId); dragRef.current = { pointerId: event.pointerId }; emit(valueFromClientY(event)); }, [disabled, emit, valueFromClientY] ); const onPointerMove = useCallback( (event: DivPointerEvent) => { if (dragRef.current === null) return; emit(valueFromClientY(event)); }, [emit, valueFromClientY] ); const onPointerUp = useCallback((event: DivPointerEvent) => { const drag = dragRef.current; if (drag === null) return; // happy-dom returns false from hasPointerCapture for any id — // releasing unconditionally is safe (the DOM ignores the call // when no capture exists) and keeps branch coverage clean. event.currentTarget.releasePointerCapture(drag.pointerId); dragRef.current = null; }, []); const onKeyDown = useCallback( (event: DivKeyboardEvent) => { if (disabled) return; const next = keyboardStep({ key: event.key, value, min, max, step, fineFactor: fineModeFactor, shiftKey: event.shiftKey, orientation: "vertical", }); if (next === null) return; event.preventDefault(); emit(next); }, [disabled, value, min, max, step, fineModeFactor, emit] ); const f = fractionForValue(value, min, max); const thumbCenterY = trackPx - f * trackPx; const thumbHeight = 10; const half = thicknessPx / 2; return (
); }