UNPKG

@nafr/echo-ui

Version:

A UI library born for WAA

147 lines (146 loc) 8.12 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useState, useEffect, useRef, forwardRef, useCallback, useMemo, isValidElement, useContext, } from 'react'; import { scaleLinear, select } from 'd3'; import { useCommandAltClick, usePropsWithGroup } from '../../../lib/hooks'; import { cn, halfRange, validValue } from '../../../lib/utils'; import { KnobGroupContext } from './context'; import { checkPropsIsValid } from './utils'; import { useStyle } from './styles'; import { DEFAULT_VALUE, MIN, MAX, ROTATION_RANGE, STEP, SENSITIVITY, SIZE, TRACK_COLOR, BUTTON_COLOR, PROGRESS_COLOR, TRACK_WIDTH, POINTER_WIDTH, POINTER_HEIGHT, POINTER_COLOR, } from './constants'; export const Knob = forwardRef((props, ref) => { const groupContext = useContext(KnobGroupContext); const isInGroup = !!groupContext; const { value: _value = DEFAULT_VALUE, min = MIN, max = MAX, step = STEP, bilateral = false, disabled = false, sensitivity = SENSITIVITY, rotationRange: _rotationRange = ROTATION_RANGE, size = SIZE, trackWidth = TRACK_WIDTH, trackColor = TRACK_COLOR, buttonColor = BUTTON_COLOR, progressColor = PROGRESS_COLOR, pointerWidth = POINTER_WIDTH, pointerHeight = POINTER_HEIGHT, pointerColor = POINTER_COLOR, topLabel, bottomLabel, classNames, styles, onChange, onChangeEnd, ...restProps } = usePropsWithGroup(props, groupContext); useEffect(() => { checkPropsIsValid(props); }, []); const [value, setValue] = useState(validValue(_value, min, max)); const [isDragging, setIsDragging] = useState(false); const [direction, setDirection] = useState('negative'); const rotationRange = validValue(_rotationRange, 90, 360); const scale = useRef(scaleLinear().domain([min, max]).range([0, rotationRange])); const [rotation, setRotation] = useState(scale.current(validValue(_value, min, max))); // The current rotation deg const currentValue = useRef(value); const knobRef = useRef(null); const startValue = useRef(value); // Ref to store the initial value const startYRef = useRef(0); // Ref to store the initial Y position // ================== handlers ================== // useEffect(() => { const validatedValue = validValue(_value, min, max); setValue(validatedValue); }, [_value, min, max]); useEffect(() => { if (disabled) return; const validatedValue = validValue(value, min, max); setRotation(scale.current(validatedValue)); }, [value, min, max, scale]); useEffect(() => { if (bilateral) setDirection(rotation < rotationRange / 2 ? 'negative' : 'positive'); updateKnobButtonRotateTransform(); }, [bilateral, rotation, rotationRange]); const handleResetClick = useCommandAltClick(() => { if (bilateral) setValue(halfRange(min, max)); else setValue(min); onChange?.(validValue(bilateral ? halfRange(min, max) : min, min, max)); }); const startDragging = (e) => { if (disabled) return; e.stopPropagation(); setIsDragging(true); startValue.current = value; startYRef.current = e.clientY; document.addEventListener('mousemove', onDragging); document.addEventListener('mouseup', stopDragging); handleResetClick(e); }; const onDragging = (e) => { e.preventDefault(); requestAnimationFrame(() => updateKnobValue(e)); }; const stopDragging = (e) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); onChangeEnd && onChangeEnd(currentValue.current); document.removeEventListener('mousemove', onDragging); document.removeEventListener('mouseup', stopDragging); handleResetClick(e); }; const updateKnobValue = useCallback((e) => { const deltaY = startYRef.current - e.clientY; const deltaValue = deltaY * (sensitivity / 10) * step; let newValue = startValue.current + deltaValue; newValue = parseFloat((Math.round(newValue / step) * step).toFixed(10)); newValue = validValue(newValue, min, max); currentValue.current = newValue; setValue(newValue); onChange?.(validValue(newValue, min, max)); }, [startYRef, startValue, sensitivity, step, min, max, onChange]); // ================== Styles ================== // useEffect(() => { if (isDragging) document.getElementsByTagName('body')[0].style.cursor = 'grabbing'; else document.getElementsByTagName('body')[0].style.cursor = ''; }, [isDragging]); const updateKnobButtonRotateTransform = () => { requestAnimationFrame(() => select(knobRef.current).style('transform', `rotate(${rotation}deg)`)); }; const progressBackground = useMemo(() => { const _progressColor = disabled ? 'var(--echo-muted)' : progressColor; if (!bilateral) { return `conic-gradient(${_progressColor} 0% ${rotation}deg, transparent ${rotation}deg 100%)`; } // bilateral mode if (direction === 'negative') { const startRotation = rotationRange / 2 + rotation + 360 - rotationRange; const endRotation = 360 - rotation; return `conic-gradient(transparent ${startRotation}deg, ${_progressColor} 0% ${endRotation}deg)`; } else { const startRotation = rotation - rotationRange / 2; return `conic-gradient(${_progressColor} ${startRotation}deg, transparent 0% 100%)`; } }, [rotation, bilateral, progressColor, direction, rotationRange, disabled]); const trackBackground = useMemo(() => { const halfRotationRange = rotationRange / 2; return `conic-gradient( ${trackColor} ${halfRotationRange}deg, transparent 0% ${360 - halfRotationRange}deg, ${trackColor} ${halfRotationRange}deg)`; }, [trackColor, rotationRange]); const renderLabel = (label, style, className) => { if (!label && label !== 0) return null; if (isValidElement(label)) return label; return (_jsx("div", { className: className, style: style, children: `${label}` })); }; const { base, button, progress, trigger, triggerPointer, topLabel: _topLabel, bottomLabel: _bottomLabel, } = useStyle({ disabled, isDragging }); return (_jsxs("div", { ...restProps, ref: ref, "data-dragging": isDragging, "data-disabled": disabled, "data-bilateral": bilateral, "data-direction": direction, className: cn(base(), isInGroup && groupContext.classNames?.knob, restProps.className), style: { ...(isInGroup && groupContext.styles?.knob), ...restProps.style, minWidth: size, }, children: [renderLabel(topLabel, styles?.topLabel, cn(_topLabel(), classNames?.topLabel)), _jsxs("div", { onMouseDown: startDragging, className: cn(button(), classNames?.button), style: { ...styles?.button, padding: trackWidth, width: size, height: size, position: 'relative', userSelect: 'none', borderRadius: '100%', background: trackBackground, }, children: [_jsx("div", { className: cn(progress()), style: { rotate: bilateral ? '0deg' : `-${rotationRange / 2}deg`, background: progressBackground, } }), _jsx("div", { ref: knobRef, className: cn(trigger()), style: { rotate: `-${rotationRange / 2}deg`, backgroundColor: buttonColor }, role: "slider", children: _jsx("div", { className: cn(triggerPointer()), style: { width: pointerWidth, height: pointerHeight, backgroundColor: disabled ? 'var(--echo-muted)' : pointerColor, } }) })] }), renderLabel(bottomLabel, styles?.bottomLabel, cn(_bottomLabel(), classNames?.bottomLabel))] })); });