UNPKG

@codeworker.br/govbr-tw-react

Version:

Biblioteca de componentes React usando Tailwind CSS que implementa o Padrão Digital de Governo.

253 lines (252 loc) 11.9 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { sliderRootVariants, sliderTrackVariants, sliderTrailVariants, sliderLevelVariants, sliderThumbVariants, } from "./variants"; function cx(...classes) { return classes.filter(Boolean).join(" "); } export default function Slider({ value, defaultValue, onChange, range, min = 0, max = 100, step = 1, disabled = false, showValue = true, colors, className, density = "default", variant = "default", }) { const inferredRange = useMemo(() => range !== null && range !== void 0 ? range : Array.isArray(value !== null && value !== void 0 ? value : defaultValue), [range, value, defaultValue]); const fallbackValue = useMemo(() => (inferredRange ? [min, max] : min), [inferredRange, min, max]); const normalizedDefaultValue = useMemo(() => normalizeValue(defaultValue !== null && defaultValue !== void 0 ? defaultValue : fallbackValue, inferredRange, min, max, step), [defaultValue, fallbackValue, inferredRange, min, max, step]); const normalizedControlledValue = useMemo(() => value !== undefined ? normalizeValue(value, inferredRange, min, max, step) : undefined, [value, inferredRange, min, max, step]); const isControlled = value !== undefined; const [internal, setInternal] = useState(normalizedDefaultValue); useEffect(() => { if (!isControlled) { setInternal(normalizedDefaultValue); } }, [isControlled, normalizedDefaultValue]); const current = useMemo(() => { if (isControlled) { return normalizedControlledValue !== null && normalizedControlledValue !== void 0 ? normalizedControlledValue : normalizedDefaultValue; } return normalizeValue(internal, inferredRange, min, max, step); }, [ internal, inferredRange, isControlled, max, min, normalizedControlledValue, normalizedDefaultValue, step, ]); const rootRef = useRef(null); const activeThumbRef = useRef(null); const valueRef = useRef(current); useEffect(() => { valueRef.current = current; }, [current]); const span = useMemo(() => max - min || 1, [max, min]); const getRangeFromRef = useCallback(() => { const raw = valueRef.current; return clampRange(toRange(raw, [min, max]), min, max, step); }, [min, max, step]); const emitChange = useCallback((next) => { valueRef.current = next; if (!isControlled) { setInternal((prev) => (valuesAreEqual(prev, next) ? prev : next)); } onChange === null || onChange === void 0 ? void 0 : onChange(next); }, [isControlled, onChange]); const setSingleValue = useCallback((next) => { const normalized = clamp(roundTo(next, step), min, max); emitChange(normalized); }, [emitChange, min, max, step]); const setRangeValue = useCallback((index, next) => { const rangeValue = getRangeFromRef(); const updated = index === 0 ? [next, rangeValue[1]] : [rangeValue[0], next]; const normalized = clampRange(updated, min, max, step); emitChange(normalized); }, [emitChange, getRangeFromRef, min, max, step]); const getClosestThumbIndex = useCallback((val) => { const [low, high] = getRangeFromRef(); return Math.abs(val - low) <= Math.abs(val - high) ? 0 : 1; }, [getRangeFromRef]); const positionToValue = useCallback((clientX) => { const el = rootRef.current; if (!el) { const raw = valueRef.current; return typeof raw === "number" ? raw : raw[0]; } const rect = el.getBoundingClientRect(); const x = clamp(clientX - rect.left, 0, rect.width); const ratio = rect.width ? x / rect.width : 0; return min + ratio * (max - min); }, [min, max]); const parseThumbIndexFromEvent = (event) => { const thumb = event.target.closest("[data-slider-thumb-index]"); if (!thumb) return null; const index = Number(thumb.getAttribute("data-slider-thumb-index")); return Number.isNaN(index) ? null : index; }; const onPointerDown = useCallback((event) => { if (disabled) return; const track = event.currentTarget; track.setPointerCapture(event.pointerId); const pointerValue = positionToValue(event.clientX); let thumbIndex = parseThumbIndexFromEvent(event); const isRangeMode = Array.isArray(valueRef.current); if (!isRangeMode) { thumbIndex = 0; } else if (thumbIndex === null) { thumbIndex = getClosestThumbIndex(pointerValue); } activeThumbRef.current = thumbIndex; if (Array.isArray(valueRef.current)) { setRangeValue((thumbIndex !== null && thumbIndex !== void 0 ? thumbIndex : 0), pointerValue); } else { setSingleValue(pointerValue); } }, [disabled, getClosestThumbIndex, positionToValue, setRangeValue, setSingleValue]); const onPointerMove = useCallback((event) => { if (disabled) return; const thumbIndex = activeThumbRef.current; if (thumbIndex === null) return; const pointerValue = positionToValue(event.clientX); if (Array.isArray(valueRef.current)) { setRangeValue(thumbIndex, pointerValue); } else { setSingleValue(pointerValue); } }, [disabled, positionToValue, setRangeValue, setSingleValue]); const endPointerInteraction = useCallback((event) => { if (activeThumbRef.current === null) return; activeThumbRef.current = null; event.currentTarget.releasePointerCapture(event.pointerId); }, []); const handleThumbKeyDown = useCallback((index) => (event) => { if (disabled) return; const isRangeMode = Array.isArray(valueRef.current); const resolvedIndex = (isRangeMode ? index !== null && index !== void 0 ? index : 0 : 0); const baseValue = isRangeMode ? getRangeFromRef()[resolvedIndex] : valueRef.current; const large = Math.max(step * 10, (max - min) / 10); switch (event.key) { case "ArrowLeft": case "ArrowDown": event.preventDefault(); isRangeMode ? setRangeValue(resolvedIndex, baseValue - step) : setSingleValue(baseValue - step); break; case "ArrowRight": case "ArrowUp": event.preventDefault(); isRangeMode ? setRangeValue(resolvedIndex, baseValue + step) : setSingleValue(baseValue + step); break; case "PageDown": event.preventDefault(); isRangeMode ? setRangeValue(resolvedIndex, baseValue - large) : setSingleValue(baseValue - large); break; case "PageUp": event.preventDefault(); isRangeMode ? setRangeValue(resolvedIndex, baseValue + large) : setSingleValue(baseValue + large); break; case "Home": event.preventDefault(); isRangeMode ? setRangeValue(resolvedIndex, min) : setSingleValue(min); break; case "End": event.preventDefault(); isRangeMode ? setRangeValue(resolvedIndex, max) : setSingleValue(max); break; } }, [disabled, getRangeFromRef, max, min, setRangeValue, setSingleValue, step]); const percents = useMemo(() => { if (Array.isArray(current)) { return current.map((val) => clampPercent(((val - min) / span) * 100)); } return [clampPercent((current - min) / span * 100)]; }, [current, min, span]); const levelStyle = useMemo(() => { if (Array.isArray(current)) { const [start, end] = percents; const left = Math.min(start, end); const width = Math.abs(end - start); return { left: `${left}%`, width: `${width}%` }; } return { left: "0%", width: `${percents[0]}%` }; }, [current, percents]); const displayValue = Array.isArray(current) ? `${current[0]} - ${current[1]}` : current; const thumbs = Array.isArray(current) ? current.map((val, index) => { const percent = percents[index]; const [low, high] = current; return (_jsx("button", { type: "button", "data-slider-thumb-index": index, className: cx(sliderThumbVariants({ variant, density, disabled }), colors === null || colors === void 0 ? void 0 : colors.thumb), style: { left: `${percent}%` }, onKeyDown: handleThumbKeyDown(index), role: "slider", "aria-valuemin": index === 0 ? min : low, "aria-valuemax": index === 0 ? high : max, "aria-valuenow": Math.round(val * 1000) / 1000, "aria-disabled": disabled || undefined }, index)); }) : [ _jsx("button", { type: "button", "data-slider-thumb-index": 0, className: cx(sliderThumbVariants({ variant, density, disabled }), colors === null || colors === void 0 ? void 0 : colors.thumb), style: { left: `${percents[0]}%` }, onKeyDown: handleThumbKeyDown(), role: "slider", "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": Math.round(current * 1000) / 1000, "aria-disabled": disabled || undefined }, "single"), ]; return (_jsxs("div", { className: cx(sliderRootVariants({ density }), className), children: [_jsxs("div", { ref: rootRef, className: cx(sliderTrackVariants({ density }), colors === null || colors === void 0 ? void 0 : colors.track), onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: endPointerInteraction, onPointerCancel: endPointerInteraction, children: [_jsx("div", { className: cx(sliderTrailVariants({ variant, density }), colors === null || colors === void 0 ? void 0 : colors.trail) }), _jsx("div", { className: cx(sliderLevelVariants({ variant, density }), colors === null || colors === void 0 ? void 0 : colors.level), style: levelStyle }), thumbs] }), showValue && (_jsx("div", { className: "ml-2 inline-block min-w-10 align-middle text-right text-sm tabular-nums", children: displayValue }))] })); } /* utils */ function clamp(n, a, b) { return Math.min(Math.max(n, a), b); } function roundTo(n, step) { const inv = 1 / (step || 1); return Math.round(n * inv) / inv; } function toRange(value, fallback) { if (Array.isArray(value)) { return [value[0], value[1]]; } if (typeof value === "number") { return [value, value]; } return fallback; } function clampRange(range, min, max, step) { let [start, end] = range; start = clamp(roundTo(start, step), min, max); end = clamp(roundTo(end, step), min, max); if (start > end) { [start, end] = [end, start]; } return [start, end]; } function normalizeValue(raw, isRange, min, max, step) { if (isRange) { return clampRange(toRange(raw, [min, max]), min, max, step); } const numeric = typeof raw === "number" ? raw : Array.isArray(raw) ? raw[0] : min; return clamp(roundTo(numeric, step), min, max); } function valuesAreEqual(a, b) { if (Array.isArray(a) && Array.isArray(b)) { return a[0] === b[0] && a[1] === b[1]; } return !Array.isArray(a) && !Array.isArray(b) && a === b; } function clampPercent(value) { return Math.min(Math.max(value, 0), 100); }