Tutorial: Creando un Componente Slider Personalizado en React

Tutorial: Creando un Componente Slider Personalizado en React

Artículospor Luis

Introducción

En este tutorial, vamos a crear un componente Slider personalizado en React que ofrece funcionalidades avanzadas como soporte para valores negativos, pasos personalizables, tooltips y accesibilidad. Este componente es ideal para aplicaciones que requieren una interfaz de usuario interactiva para seleccionar valores numéricos.

Requisitos Previos

  • Conocimientos básicos de React
  • Familiaridad con TypeScript
  • Un proyecto React configurado (podemos usar el que creamos en nuestro tutorial: Next Tutorial.)

Paso 1: Configuración Inicial

Primero, vamos a crear un nuevo archivo para nuestro componente Slider. En un proyecto Next.js, podríamos colocarlo en src/app/components/slider.tsx/Slider.tsx.
"use client";

import React, { useState, useRef, useCallback, useMemo, memo, useEffect } from "react";
El atributo “use client” es necesario en Next.js para indicar que este componente se ejecutará en el lado del cliente. Esto es importante porque nuestro componente utiliza eventos del navegador y manipulación del DOM, que solo están disponibles en el cliente.
Importamos varios hooks de React:

  • useState: Para gestionar el estado local del componente
  • useRef: Para mantener una referencia al elemento DOM del slider
  • useCallback: Para memorizar funciones y evitar recreaciones innecesarias
  • useMemo: Para memorizar valores calculados
  • memo: Para evitar renderizados innecesarios del componente
  • useEffect: Para manejar efectos secundarios como la inicialización

Paso 2: Definición de Tipos

Definimos las interfaces para nuestro componente:
// Definición de tipos para el componente
// StepPoint representa un punto de paso en el slider, con un valor numérico y un indicador de si es un valor extremo
interface StepPoint {
  value: number;
  isExtreme: boolean;
}

// SliderProps define todas las propiedades que puede recibir nuestro componente Slider
interface SliderProps {
  /** El valor máximo del slider */
  value: number;
  /** Indica si el slider debe soportar valores negativos */
  negative?: boolean;
  /** El tamaño del paso entre valores */
  steps: number;
  /** Ancho opcional del slider */
  width?: string;
  /** Función de callback cuando el valor cambia */
  onChange?: (value: number) => void;
  /** Valor inicial del slider */
  defaultValue?: number;
  /** Indica si el slider está deshabilitado */
  disabled?: boolean;
  /** Clase CSS personalizada para el contenedor del slider */
  className?: string;
  /** Función para formatear el valor mostrado */
  formatValue?: (value: number) => string;
  /** Modo de visibilidad del tooltip: 'always' | 'onInteraction' */
  showTooltip?: 'always' | 'onInteraction';
}
Estas interfaces definen la estructura de datos para los puntos de paso y las propiedades que aceptará nuestro componente.
  • StepPoint: Define la estructura de cada punto de paso en el slider, con un valor numérico y un indicador de si es un valor extremo (mínimo o máximo).
  • SliderProps: Define todas las propiedades que puede recibir nuestro componente Slider, con comentarios JSDoc para documentar cada propiedad.

Paso 3: Configuración de Props por Defecto

Establecemos valores predeterminados para las propiedades opcionales:

// Valores predeterminados para las propiedades opcionales
const defaultProps: Partial<SliderProps> = {
  negative: false,
  disabled: false,
  showTooltip: 'onInteraction',
  formatValue: (value: number) => value.toString(),
};
Estos valores predeterminados se utilizarán cuando no se proporcionen las propiedades correspondientes al componente. Esto hace que el componente sea más fácil de usar, ya que no es necesario especificar todas las propiedades cada vez.

Paso 4: Implementación del Componente Principal

Creamos el componente Slider utilizando memo para optimizar el rendimiento:

// Componente Slider principal
// Utilizamos memo para evitar renderizados innecesarios cuando las props no cambian
const Slider = memo(({
  value,
  negative = defaultProps.negative,
  steps: initialSteps,
  onChange,
  defaultValue,
  disabled = defaultProps.disabled,
  className = "",
  formatValue = defaultProps.formatValue,
  showTooltip = defaultProps.showTooltip,
}: SliderProps) => {
  // Component implementation
});
Utilizamos memo para evitar renderizados innecesarios del componente cuando sus props no cambian. Esto es especialmente útil para componentes que se renderizan frecuentemente o que tienen cálculos costosos.
En la desestructuración de props, asignamos valores predeterminados a las propiedades opcionales utilizando los valores definidos en defaultProps.

Paso 5: Validación de Props

Agregamos validaciones para asegurar que los valores proporcionados sean válidos:

// Validación de props para asegurar que los valores sean válidos
if (value <= 0) {
  throw new Error("El valor del slider debe ser mayor que 0");
}
if (initialSteps <= 0) {
  throw new Error("Los pasos del slider deben ser mayores que 0");
}
if (initialSteps > value) {
  console.warn("Los pasos del slider no pueden ser mayores que el valor. Usando el valor como pasos.");
}

// Aseguramos que los pasos no sean mayores que el valor
const steps = Math.min(initialSteps, value);
Estas validaciones garantizan que el componente reciba valores válidos y proporcione mensajes de error claros cuando no sea así. También ajustamos automáticamente el valor de steps si es mayor que value, para evitar comportamientos inesperados.

Paso 6: Estado y Referencias

Configuramos el estado y las referencias necesarias:

// Estado del componente
// mounted: indica si el componente está montado (para evitar problemas de hidratación)
// position: posición actual del thumb en porcentaje (0-100)
// currentValue: valor numérico actual del slider
// isDragging: indica si el usuario está arrastrando el thumb
// sliderRef: referencia al elemento DOM del slider
const [mounted, setMounted] = useState(false);
const [position, setPosition] = useState(0);
const [currentValue, setCurrentValue] = useState(0);
const [isDragging, setIsDragging] = useState(false);
const sliderRef = useRef<HTMLDivElement>(null);
  • mounted: Un estado booleano que indica si el componente está montado. Esto es útil para evitar problemas de hidratación en Next.js.
  • position: La posición actual del thumb del slider en porcentaje (0-100).
  • currentValue: El valor numérico actual del slider.
  • isDragging: Un estado booleano que indica si el usuario está arrastrando el thumb del slider.
  • sliderRef: Una referencia al elemento DOM del slider, que utilizamos para calcular posiciones relativas.

Paso 7: Inicialización del Estado

Utilizamos useEffect para inicializar el estado después del montaje del componente:
// Inicialización del estado después del montaje del componente
// Esto evita problemas de hidratación en Next.js
useEffect(() => {
  setMounted(true);
  
  // Calculamos la posición inicial del thumb basada en el valor predeterminado
  const initialPosition = defaultValue !== undefined
    ? negative
      ? ((defaultValue + value) / (value * 2)) * 100 // Para valores negativos, el 0 está en el medio (50%)
      : (defaultValue / value) * 100 // Para valores positivos, el 0 está al principio (0%)
    : negative ? 50 : 0; // Si no hay valor predeterminado, usamos 50% para negativos o 0% para positivos

  // Calculamos el valor inicial, asegurándonos de que esté dentro del rango permitido
  const initialValue = defaultValue !== undefined
    ? Math.max(
        negative ? -value : 0,
        Math.min(value, defaultValue)
      )
    : negative ? 0 : 0;

  // Actualizamos el estado con los valores calculados
  setPosition(initialPosition);
  setCurrentValue(initialValue);
}, [defaultValue, value, negative]);
Este efecto se ejecuta después del montaje del componente y cuando cambian defaultValuevalue o negative. Calcula la posición inicial del thumb y el valor inicial del slider basándose en las props proporcionadas.
La fórmula para calcular initialPosition es diferente dependiendo de si el slider soporta valores negativos o no:

  • Si negative es true, el rango va de -value a value, por lo que el valor 0 está en el medio (50%).
  • Si negative es false, el rango va de 0 a value, por lo que el valor 0 está al principio (0%).

Paso 8: Cálculo de Puntos de Paso

Utilizamos useMemo para calcular los puntos de paso de manera eficiente:

// Cálculo de los puntos de paso
// Utilizamos useMemo para evitar recálculos innecesarios
const stepPoints = useMemo(() => {
  const points: StepPoint[] = [];

  if (negative) {
    // Para valores negativos, generamos puntos desde -value hasta value
    const totalSteps = Math.floor((value * 2) / steps);
    
    // Punto mínimo (-value)
    points.push({ value: -value, isExtreme: true });

    // Puntos negativos intermedios
    for (let i = 1; i < totalSteps / 2; i++) {
      points.push({ value: -value + i * steps, isExtreme: false });
    }

    // Punto cero
    points.push({ value: 0, isExtreme: true });

    // Puntos positivos intermedios
    for (let i = 1; i < totalSteps / 2; i++) {
      points.push({ value: i * steps, isExtreme: false });
    }

    // Punto máximo (value)
    points.push({ value: value, isExtreme: true });
  } else {
    // Para valores positivos, generamos puntos desde 0 hasta value
    const totalSteps = Math.floor(value / steps);
    
    // Punto mínimo (0)
    points.push({ value: 0, isExtreme: true });

    // Puntos intermedios
    for (let i = 1; i < totalSteps; i++) {
      points.push({ value: i * steps, isExtreme: false });
    }

    // Punto máximo (value)
    points.push({ value: value, isExtreme: true });
  }

  return points;
}, [value, steps, negative]);
Este cálculo se realiza solo cuando cambian valuesteps o negative, gracias a useMemo. Genera un array de puntos de paso que se utilizarán para renderizar las marcas en el slider.
La lógica es diferente dependiendo de si el slider soporta valores negativos o no:

  • Si negative es true, generamos puntos desde -value hasta value, con 0 en el medio.
  • Si negative es false, generamos puntos desde 0 hasta value.
Los valores extremos (mínimo, cero y máximo) se marcan con isExtreme: true para que se puedan renderizar de manera diferente.

Paso 9: Lógica de Actualización de Posición

Implementamos la función para actualizar la posición del slider:

// Función para actualizar la posición del slider
// Se llama cuando el usuario interactúa con el slider
const updatePosition = useCallback((clientX: number) => {
  // Si el slider está deshabilitado o no tenemos referencia al elemento DOM, no hacemos nada
  if (!sliderRef.current || disabled) return;

  // Calculamos la posición relativa del cursor dentro del slider
  const rect = sliderRef.current.getBoundingClientRect();
  const x = clientX - rect.left;
  const rawPosition = (x / rect.width) * 100;
  
  // Aseguramos que la posición esté entre 0 y 100
  const clampedPosition = Math.max(0, Math.min(100, rawPosition));

  // Calculamos el valor numérico correspondiente a esta posición
  let newValue: number;
  if (negative) {
    // Para valores negativos, el rango va de -value a value
    const range = value * 2;
    newValue = -value + range * (clampedPosition / 100);
  } else {
    // Para valores positivos, el rango va de 0 a value
    newValue = value * (clampedPosition / 100);
  }

  // Redondeamos el valor al paso más cercano
  let roundedValue = Math.round(newValue / steps) * steps;

  // Aseguramos que el valor esté dentro del rango permitido
  if (negative) {
    roundedValue = Math.max(-value, Math.min(value, roundedValue));
  } else {
    roundedValue = Math.max(0, Math.min(value, roundedValue));
  }

  // Calculamos la posición final del thumb basada en el valor redondeado
  let newPosition: number;
  if (negative) {
    newPosition = ((roundedValue + value) / (value * 2)) * 100;
  } else {
    newPosition = (roundedValue / value) * 100;
  }

  // Actualizamos el estado y llamamos al callback onChange si está definido
  setPosition(newPosition);
  setCurrentValue(roundedValue);
  onChange?.(roundedValue);
}, [negative, value, steps, disabled, onChange]);
Esta función se llama cuando el usuario interactúa con el slider (arrastrando, haciendo clic, etc.). Calcula la nueva posición y valor del slider basándose en la posición del cursor.
  1. Primero, verificamos si el slider está deshabilitado o si no tenemos una referencia al elemento DOM.
  1. Calculamos la posición relativa del cursor dentro del slider.
  1. Convertimos esta posición a un porcentaje (0-100).
  1. Calculamos el valor numérico correspondiente a esta posición, teniendo en cuenta si el slider soporta valores negativos o no.
  1. Redondeamos el valor al paso más cercano.
  1. Aseguramos que el valor esté dentro del rango permitido.
  1. Calculamos la posición final del thumb basándose en el valor redondeado.
  1. Actualizamos el estado y llamamos al callback onChange si está definido.
Utilizamos useCallback para memorizar esta función y evitar recreaciones innecesarias.

Paso 10: Manejadores de Eventos

Implementamos los manejadores para eventos de mouse, touch y teclado:

// Manejadores de eventos de mouse
const handleMouseDown = useCallback((e: React.MouseEvent) => {
  if (disabled) return;
  setIsDragging(true);
  updatePosition(e.clientX);
}, [disabled, updatePosition]);

const handleMouseMove = useCallback((e: React.MouseEvent) => {
  if (!isDragging || disabled) return;
  updatePosition(e.clientX);
}, [isDragging, disabled, updatePosition]);

const handleMouseUp = useCallback(() => {
  setIsDragging(false);
}, []);

// Manejadores de eventos táctiles
const handleTouchStart = useCallback((e: React.TouchEvent) => {
  if (disabled) return;
  setIsDragging(true);
  updatePosition(e.touches[0].clientX);
}, [disabled, updatePosition]);

const handleTouchMove = useCallback((e: React.TouchEvent) => {
  if (!isDragging || disabled) return;
  updatePosition(e.touches[0].clientX);
}, [isDragging, disabled, updatePosition]);

const handleTouchEnd = useCallback(() => {
  setIsDragging(false);
}, []);

// Manejador de eventos de teclado
// Permite navegar por los valores usando las teclas de flecha
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
  if (disabled) return;

  const step = steps;
  let newValue = currentValue;

  switch (e.key) {
    case "ArrowRight":
    case "ArrowUp":
      // Incrementar el valor
      newValue = Math.min(value, currentValue + step);
      break;
    case "ArrowLeft":
    case "ArrowDown":
      // Decrementar el valor
      newValue = Math.max(negative ? -value : 0, currentValue - step);
      break;
    default:
      return;
  }

  // Calculamos la nueva posición basada en el nuevo valor
  const newPosition = negative
    ? ((newValue + value) / (value * 2)) * 100
    : (newValue / value) * 100;

  // Actualizamos el estado y llamamos al callback onChange
  setPosition(newPosition);
  setCurrentValue(newValue);
  onChange?.(newValue);
}, [currentValue, value, steps, negative, disabled, onChange]);
Estos manejadores de eventos permiten al usuario interactuar con el slider de varias maneras:
  1. Eventos de Mouse:
  • handleMouseDown: Se llama cuando el usuario hace clic en el thumb del slider. Activa el modo de arrastre y actualiza la posición.
  • handleMouseMove: Se llama cuando el usuario mueve el mouse mientras arrastra el thumb. Actualiza la posición si el modo de arrastre está activo.
  • handleMouseUp: Se llama cuando el usuario suelta el botón del mouse. Desactiva el modo de arrastre.
  1. Eventos de Touch:
  • handleTouchStart: Similar a handleMouseDown, pero para eventos táctiles.
  • handleTouchMove: Similar a handleMouseMove, pero para eventos táctiles.
  • handleTouchEnd: Similar a handleMouseUp, pero para eventos táctiles.
  1. Eventos de Teclado:
  • handleKeyDown: Se llama cuando el usuario presiona una tecla mientras el slider tiene el foco. Permite navegar por los valores usando las teclas de flecha.
Todos estos manejadores utilizan useCallback para memorizar las funciones y evitar recreaciones innecesarias.

Paso 11: Función para Ajustar el Valor

Agregamos una función para incrementar o decrementar el valor:
// Función para incrementar o decrementar el valor
// Se llama cuando el usuario hace clic en los botones de flecha
const adjustValue = useCallback((increment: boolean) => {
  if (disabled) return;
  
  const step = steps;
  let newValue = currentValue;
  
  if (increment) {
    // Incrementar el valor
    newValue = Math.min(value, currentValue + step);
  } else {
    // Decrementar el valor
    newValue = Math.max(negative ? -value : 0, currentValue - step);
  }
  
  // Calculamos la nueva posición basada en el nuevo valor
  const newPosition = negative
    ? ((newValue + value) / (value * 2)) * 100
    : (newValue / value) * 100;
  
  // Actualizamos el estado y llamamos al callback onChange
  setPosition(newPosition);
  setCurrentValue(newValue);
  onChange?.(newValue);
}, [currentValue, value, steps, negative, disabled, onChange]);
Esta función se llama cuando el usuario hace clic en los botones de incremento o decremento. Ajusta el valor del slider en incrementos de steps y actualiza la posición del thumb en consecuencia.

Paso 12: Renderizado del Componente

Finalmente, implementamos el JSX para renderizar nuestro componente:

// Renderizado del componente
return (
  // Contenedor principal del slider
  // Utilizamos clases de Tailwind CSS para el estilo
  <div className={`w-[calc(100%-40px)] h-[30px] overflow-visible my-6 ${className}`}>
    <div className="relative w-full">
      {/* Botón de flecha izquierda para decrementar el valor */}
      <button
        type="button"
        className={`absolute left-0 top-1/2 transform -translate-y-1/2 w-6 h-6 flex items-center justify-center rounded-full bg-gray-200 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 z-30 ${
          disabled || (negative ? currentValue <= -value : currentValue <= 0) ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
        }`}
        onClick={() => adjustValue(false)}
        disabled={disabled || (negative ? currentValue <= -value : currentValue <= 0)}
        aria-label="Decrease value"
      >
        <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-700" viewBox="0 0 20 20" fill="currentColor">
          <path fillRule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clipRule="evenodd" />
        </svg>
      </button>

      {/* Contenedor del slider */}
      <div
        ref={sliderRef}
        className="w-[calc(100%-72px)] h-[30px] overflow-visible mx-auto my-0 relative"
        onMouseMove={handleMouseMove}
        onMouseUp={handleMouseUp}
        onMouseLeave={handleMouseUp}
        onTouchMove={handleTouchMove}
        onTouchEnd={handleTouchEnd}
        onKeyDown={handleKeyDown}
      >
        {/* Pista del slider */}
        <div 
          className={`w-full h-1.5 transform -translate-y-1/2 overflow-visible mx-0 my-[15px] rounded-sm bg-gray-400 z-20 border border-white absolute transition-colors duration-200 ${
            disabled ? 'opacity-50' : ''
          }`}
        />

        {/* Marcas de paso */}
        {mounted && stepPoints.map((point, index) => {
          // Calculamos la posición de cada marca basada en su valor
          const linePosition = negative
            ? ((point.value + value) / (value * 2)) * 100
            : (point.value / value) * 100;

          return (
            <div
              key={index}
              className={`absolute h-${
                point.isExtreme ? "[24px]" : "[12px]"
              } w-[1px] bg-gray-400 transition-opacity duration-200 ${
                disabled ? 'opacity-50' : ''
              }`}
              style={{
                left: `${linePosition}%`,
                top: point.isExtreme ? "3px" : "9px",
                height: point.isExtreme ? "24px" : "12px",
                transform: "translateX(-50%)",
                zIndex: "10",
              }}
            />
          );
        })}

        {/* Tooltip que muestra el valor actual */}
        {mounted && showTooltip && (
          <div
            className={`absolute bg-blue-500 text-white text-xs px-2 py-1 rounded transform -translate-x-1/2 -translate-y-full -mt-2 z-50 select-none transition-opacity duration-200 ${
              showTooltip === 'always' || isDragging ? 'opacity-100' : 'opacity-0'
            }`}
            style={{ left: `${position}%` }}
          >
            {formatValue ? formatValue(currentValue) : currentValue.toString()}
            <div className="absolute w-0 h-0 border-l-[6px] border-r-[6px] border-t-[6px] border-l-transparent border-r-transparent border-t-blue-500 left-1/2 transform -translate-x-1/2 bottom-[-6px]" />
          </div>
        )}

        {/* Thumb del slider (el elemento que el usuario puede arrastrar) */}
        {mounted && (
          <div
            className={`w-4 h-4 border-2 border-blue-300 z-50 overflow-hidden my-[7px] transform rounded-full bg-blue-400 absolute transition-all duration-200 ${
              disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-grab active:cursor-grabbing'
            }`}
            style={{ 
              left: `${position}%`,
              transform: `translateX(-50%) scale(${isDragging ? 1.2 : 1})`
            }}
            onMouseDown={handleMouseDown}
            onTouchStart={handleTouchStart}
          />
        )}

        {/* Etiquetas que muestran los valores mínimo y máximo */}
        {mounted && (
          <div className="numbers w-[calc(100%+12px)] h-5 mt-[30px] ml-[-6px] absolute flex justify-between text-black text-xs">
            {negative ? (
              <>
                <span>-{value}</span>
                <span>0</span>
                <span>{value}</span>
              </>
            ) : (
              <>
                <span>0</span>
                <span>{value}</span>
              </>
            )}
          </div>
        )}
      </div>

      {/* Botón de flecha derecha para incrementar el valor */}
      <button
        type="button"
        className={`absolute right-0 top-1/2 transform -translate-y-1/2 w-6 h-6 flex items-center justify-center rounded-full bg-gray-200 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 z-30 ${
          disabled || currentValue >= value ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'
        }`}
        onClick={() => adjustValue(true)}
        disabled={disabled || currentValue >= value}
        aria-label="Increase value"
      >
        <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-700" viewBox="0 0 20 20" fill="currentColor">
          <path fillRule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clipRule="evenodd" />
        </svg>
      </button>
    </div>
  </div>
);
El JSX está estructurado de la siguiente manera:
  1. Contenedor Principal: Un div que contiene todo el componente, con clases para el ancho, alto y márgenes.
  1. Contenedor Relativo: Un div con posición relativa que contiene los botones de flecha y el slider.
  1. Botón de Flecha Izquierda: Un botón para decrementar el valor del slider. Se deshabilita cuando el valor actual es el mínimo.
  1. Contenedor del Slider: Un div que contiene la pista del slider, las marcas de paso, el tooltip, el thumb y las etiquetas.
  • Pista del Slider: Un div que representa la pista del slider.
  • Marcas de Paso: Se renderizan dinámicamente basándose en stepPoints.
  • Tooltip: Muestra el valor actual y se puede configurar para estar siempre visible o solo durante la interacción.
  • Thumb del Slider: El elemento que el usuario puede arrastrar para cambiar el valor.
  • Etiquetas: Muestran los valores mínimo y máximo del slider.
  1. Botón de Flecha Derecha: Un botón para incrementar el valor del slider. Se deshabilita cuando el valor actual es el máximo.
Utilizamos clases de Tailwind CSS para estilizar el componente, lo que nos permite crear un diseño responsivo y atractivo sin necesidad de CSS personalizado.

Paso 13: Exportación del Componente

Finalmente, exportamos nuestro componente:

// Establecemos el nombre del componente para mejorar la depuración
Slider.displayName = 'Slider';

export default Slider;
Establecemos displayName para mejorar la depuración en las herramientas de desarrollo de React.

Uso del Componente

Ahora puedes utilizar el componente Slider en tu aplicación:

import Slider from './components/slider.tsx/Slider';

function App() {
  const handleChange = (value) => {
    console.log('Slider value changed:', value);
  };

  return (
    <div className="p-8">
      <h1 className="text-2xl font-bold mb-4">Mi Slider Personalizado</h1>
      <Slider 
        value={100}
        steps={10}
        negative={true}
        onChange={handleChange}
        defaultValue={0}
        showTooltip="always"
        formatValue={(value) => `${value}%`}
      />
    </div>
  );
}

Explicación Detallada de la Estructura HTML

La estructura HTML del componente está diseñada para proporcionar una experiencia de usuario intuitiva y accesible:
  1. Estructura General:
  • Utilizamos un contenedor principal con ancho calculado (w-[calc(100%-40px)]) para dejar espacio para los botones de flecha.
  • El contenedor tiene overflow-visible para permitir que el tooltip y las etiquetas se muestren fuera del contenedor.
  1. Botones de Flecha:
  • Los botones de flecha están posicionados absolutamente en los extremos del slider.
  • Utilizamos SVG para los iconos de flecha, lo que proporciona una mejor calidad visual y escalabilidad.
  • Los botones se deshabilitan cuando el valor actual es el mínimo o máximo, respectivamente.
  1. Contenedor del Slider:
  • El contenedor del slider tiene un ancho calculado (w-[calc(100%-72px)]) para dejar espacio para los botones de flecha.
  • Tiene overflow-visible para permitir que el tooltip y las etiquetas se muestren fuera del contenedor.
  • Los eventos de mouse, touch y teclado se manejan en este contenedor.
  1. Pista del Slider:
  • La pista del slider es un div con altura fija y bordes redondeados.
  • Tiene un borde blanco para mejorar la visibilidad.
  1. Marcas de Paso:
  • Las marcas de paso se renderizan dinámicamente basándose en stepPoints.
  • Las marcas extremas (mínimo, cero y máximo) son más altas que las marcas intermedias.
  • La posición de cada marca se calcula basándose en su valor.
  1. Tooltip:
  • El tooltip muestra el valor actual del slider.
  • Tiene un fondo azul y texto blanco para destacar.
  • Incluye una flecha que apunta al thumb del slider.
  • La opacidad del tooltip se controla basándose en showTooltip y isDragging.
  1. Thumb del Slider:
  • El thumb es un div circular con un borde y un fondo.
  • Su posición se controla mediante la propiedad left en el estilo.
  • Se escala ligeramente cuando el usuario lo arrastra para proporcionar retroalimentación visual.
  • Los eventos de mouse y touch se manejan en el thumb.
  1. Etiquetas:
  • Las etiquetas muestran los valores mínimo y máximo del slider.
  • Si el slider soporta valores negativos, también se muestra el valor cero en el medio.

Características del Componente

  1. Soporte para valores negativos: El componente puede mostrar un rango que incluye valores negativos, lo que es útil para aplicaciones que requieren seleccionar valores tanto positivos como negativos.
  1. Pasos personalizables: El componente permite definir el tamaño de los pasos entre valores, lo que es útil para aplicaciones que requieren una selección precisa.
  1. Tooltips: El componente muestra el valor actual en un tooltip que puede estar siempre visible o solo durante la interacción, lo que mejora la experiencia del usuario.
  1. Accesibilidad: El componente es compatible con navegación por teclado y lectores de pantalla, lo que lo hace accesible para todos los usuarios.
  1. Diseño responsivo: El componente se adapta al ancho del contenedor, lo que lo hace ideal para aplicaciones responsivas.
  1. Personalización visual: El componente acepta clases CSS personalizadas para estilizar el componente según las necesidades del usuario.
  1. Soporte para dispositivos táctiles: El componente funciona correctamente en dispositivos móviles, lo que lo hace ideal para aplicaciones multiplataforma.

Conclusión

Hemos creado un componente Slider personalizado en React con TypeScript que ofrece una experiencia de usuario rica y accesible. Este componente puede ser utilizado en diversas aplicaciones donde se requiera una selección de valores numéricos con una interfaz intuitiva.
El componente está diseñado para ser flexible y personalizable, permitiendo a los desarrolladores adaptarlo a sus necesidades específicas. La estructura HTML está optimizada para proporcionar una experiencia de usuario intuitiva y accesible, mientras que la lógica de JavaScript maneja la interacción del usuario y actualiza el estado del componente de manera eficiente.
Puedes personalizar aún más este componente según tus necesidades específicas, como cambiar los colores, tamaños o agregar funcionalidades adicionales.

 

-- ¿Quieres recibir mi newsletter? --

No te pierdas de aprender:

Mi newsletter mensual viene con una dosis de inspiración, recursos para descargar, consejos de desarrollo rápidos y los mismos recursos que aprendo.