Crear Listas Deslizables con Botones de Acción en JavaScript Puro

El patrón de interfaz de usuario que permite deslizar un elemento de una lista horizontalmente para revelar botones de acción es una característica común en muchas aplicaciones móviles, como clientes de correo electrónico o aplicaciones de mensajería. Este enfoque mejora la experiencia del usuario al proporcionar acceso rápido a funcionalidades secundarias como "Eliminar" o "Favorito" sin saturar la interfaz principal. Aunque existen bibliotecas y frameworks que ofrecen esta funcionalidad preconstruida, comprender los principios subyacentes y construirla con JavaScript, HTML y CSS puros es fundamental para cualquier desarrollador.

A continuación, exploraremos cómo implementar un sistema de elementos de lista deslizables utilizando eventos táctiles nativos del navegador para detectar gestos y manipular la interfaz.

Estructura HTML

La base de nuestra implementación es una estructura HTML semántica. Necesitamos un contenedor para la lista, y para cada elemento de la lista, un div principal que contenga el contenido visible y un div separado para las acciones que se revelarán al deslizar.


<html lang="es">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Lista Deslizable con Acciones</title>
 <style>
   /* Estilos CSS (se mostrarán a continuación) */
   .contenedor-lista {
     max-width: 600px;
     margin: 20px auto;
     border: 1px solid #ddd;
     border-radius: 8px;
     overflow: hidden;
     box-shadow: 0 4px 12px rgba(0,0,0,0.08);
     background-color: #f9f9f9;
   }

   .elemento-lista {
     position: relative;
     display: flex;
     background-color: #fff;
     border-bottom: 1px solid #eee;
     cursor: grab;
     touch-action: pan-y; /* Permite desplazamiento vertical nativo */
   }

   .elemento-lista:last-child {
     border-bottom: none;
   }

   .contenido-principal {
     flex-grow: 1;
     padding: 18px 20px;
     background-color: #fff;
     z-index: 2;
     transform: translateX(0);
     transition: transform 0.3s ease-out;
     white-space: nowrap;
     overflow: hidden;
     text-overflow: ellipsis;
     font-size: 1.1em;
     color: #333;
   }

   .acciones-ocultas {
     position: absolute;
     top: 0;
     right: 0;
     height: 100%;
     display: flex;
     align-items: center;
     z-index: 1;
     transform: translateX(100%); /* Oculto inicialmente */
     transition: transform 0.3s ease-out;
   }

   .elemento-lista.abierto .contenido-principal {
     transform: translateX(-160px); /* Ajustar según el ancho total de los botones */
   }

   .elemento-lista.abierto .acciones-ocultas {
     transform: translateX(0);
   }

   .boton-accion {
     padding: 0 20px;
     border: none;
     color: white;
     cursor: pointer;
     height: 100%;
     font-size: 1em;
     min-width: 80px;
     box-sizing: border-box;
     display: flex;
     align-items: center;
     justify-content: center;
     text-transform: uppercase;
     font-weight: 500;
   }

   .boton-favorito {
     background-color: #007bff; /* Azul */
   }

   .boton-eliminar {
     background-color: #dc3545; /* Rojo */
   }
 </style>
</head>
<body>
 <div class="contenedor-lista">
   <div class="elemento-lista">
     <div class="contenido-principal">Correo importante de Juan Pérez</div>
     <div class="acciones-ocultas">
       <button class="boton-accion boton-favorito">Fav.</button>
       <button class="boton-accion boton-eliminar">Borrar</button>
     </div>
   </div>
   <div class="elemento-lista">
     <div class="contenido-principal">Recordatorio: Reunión de equipo mañana a las 10:00</div>
     <div class="acciones-ocultas">
       <button class="boton-accion boton-favorito">Fav.</button>
       <button class="boton-accion boton-eliminar">Borrar</button>
     </div>
   </div>
   <div class="elemento-lista">
     <div class="contenido-principal">Actualización de la política de privacidad de la app X</div>
     <div class="acciones-ocultas">
       <button class="boton-accion boton-favorito">Fav.</button>
       <button class="boton-accion boton-eliminar">Borrar</button>
     </div>
   </div>
 </div>

 <script>
   // Código JavaScript (se mostrará a continuación)
 </script>
</body>
</html>

Estilos CSS

Los estilos CSS son cruciales para el posicionamiento de los elementos y la animación. Utilizamos position: relative en el elemento de la lista y position: absolute en el contenedor de acciones para superponerlo. La clave de la animación reside en las propiedades transform: translateX() y transition, que apliacmos tanto al contenido principal como a las acciones ocultas cuando el elemento tiene la clase .abierto.

El CSS fue incluido en el ejemplo HTML anterior para facilitar la comprensión.

Lógica JavaScript

La funcionalidad principal se implementa mediante la escucha de los eventos táctiles del navegador: touchstart, touchmove y touchend. Esto nos permite detectar el inicio del deslizamiento, su dirección y distancia, y finalmente decidir si se abre o cierra el elemento de la lista.

document.addEventListener('DOMContentLoaded', () => {
 let elementoActivo = null; // Guarda una referencia al elemento de lista actualmente abierto
 let inicioX = 0;          // Posición X al inicio del toque
 let inicioY = 0;          // Posición Y al inicio del toque
 let arrastrando = false; // Indica si se está realizando un arrastre horizontal significativo

 const elementosLista = document.querySelectorAll('.elemento-lista');
 const umbralDeslizamiento = 70; // Píxeles mínimos para considerar un deslizamiento

 elementosLista.forEach(elemento => {
   elemento.addEventListener('touchstart', (e) => {
     inicioX = e.touches[0].clientX;
     inicioY = e.touches[0].clientY;
     arrastrando = false; // Reiniciar en cada toque
     // No llamar preventDefault aquí para permitir el scroll vertical inicial
   });

   elemento.addEventListener('touchmove', (e) => {
     const currentX = e.touches[0].clientX;
     const currentY = e.touches[0].clientY;
     const diferenciaX = currentX - inicioX;
     const diferenciaY = currentY - inicioY;

     // Detectar si el movimiento es predominantemente horizontal
     if (Math.abs(diferenciaX) > Math.abs(diferenciaY) && Math.abs(diferenciaX) > 5) {
       e.preventDefault(); // Prevenir el desplazamiento vertical si es un arrastre horizontal
       arrastrando = true;
     }
   });

   elemento.addEventListener('touchend', (e) => {
     if (!arrastrando) {
       // Si no hubo un arrastre significativo, y el elemento está abierto,
       // esto podría ser un toque para cerrar, o un click normal.
       // Para simplificar, si el elemento activo es este y no hubo arrastre, lo cerramos.
       if (elementoActivo === elemento) {
         elemento.classList.remove('abierto');
         elementoActivo = null;
       }
       return;
     }

     const finalX = e.changedTouches[0].clientX;
     const diferenciaX = finalX - inicioX;

     if (diferenciaX < -umbralDeslizamiento) { // Deslizamiento hacia la izquierda para abrir
       if (elementoActivo && elementoActivo !== elemento) {
         elementoActivo.classList.remove('abierto'); // Cerrar el elemento previamente abierto
       }
       elemento.classList.add('abierto');
       elementoActivo = elemento;
     } else if (diferenciaX > umbralDeslizamiento && elemento.classList.contains('abierto')) { // Deslizamiento hacia la derecha para cerrar
       elemento.classList.remove('abierto');
       if (elementoActivo === elemento) {
         elementoActivo = null;
       }
     } else {
       // Si no se deslizó lo suficiente, o fue en la dirección incorrecta,
       // el elemento mantiene su estado actual (abierto o cerrado).
       // Se puede añadir un 'snap back' si se desea para que el elemento abierto se cierre si no se deslizó lo suficiente a la derecha
       // o para que el elemento cerrado no abra si no se deslizó lo suficiente a la izquierda
       if (elementoActivo === elemento && !elemento.classList.contains('abierto')) {
         // Si el elemento era activo pero de alguna forma perdió la clase 'abierto', corregir.
         // Esto puede pasar si se tocan los botones de acción sin cerrar el deslizamiento.
         // Para esta implementación discreta, no se necesita acción aquí.
       } else if (elementoActivo !== elemento && elemento.classList.contains('abierto')) {
            // Si otro elemento está abierto, este se cierra
           elemento.classList.remove('abierto');
       }
     }
     arrastrando = false; // Resetear el estado de arrastre
   });
 });
});

Etiquetas: JavaScript HTML css Eventos táctiles diseño responsivo

Publicado el 7-2 03:01