@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
JavaScript
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);
}