Haz que tu sitio sea táctil

Cada vez más dispositivos cuentan con pantallas táctiles, desde teléfonos hasta pantallas de escritorio. Cuando los usuarios elijen interactuar con tu IU y la tocan, tu app debería responder de forma intuitiva.

Responder a estados de elementos

¿Alguna vez tocaste o hiciste clic en un elemento de una página web y te preguntaste si el sitio realmente lo detectó?

Si simplemente cambia el color de un elemento cuando el usuario lo toca o interactúa con partes de tu IU, se brinda la tranquilidad básica de que tu sitio funciona. Esto no solo reduce la frustración, también puede transmitir un estilo ágil y adaptable.

Los elementos del DOM pueden heredar cualquiera de los siguientes estados: default, focus, hover y active. Para cambiar nuestra IU en cada uno de estos estados, necesitamos aplicar estilos a las seudoclases :hover, :focus y :active, como se muestra a continuación:

.btn {
  background-color: #4285f4;
}

.btn:hover {
  background-color: #296CDB;
}

.btn:focus {
  background-color: #0F52C1;

  /* The outline parameter surpresses the border
  color / outline when focused */
  outline: 0;
}

.btn:active {
  background-color: #0039A8;
}

Pruébalo

Imagen que muestra distintos colores para los estados de los
botones

En la mayoría de los navegadores móviles, los estados hover y/o focus se aplicarán a un elemento después de presionarlo.

Piensa detenidamente qué estilos usarás y cómo los verán los usuarios después de que toquen sobre ellos.

Suprimir estilos predeterminados de navegadores

Después de agregar estilos para los diferentes estados, notarás que la mayoría de los navegadores implementan su propio estilo cuando responden a la interacción del usuario. Esto se debe principalmente a que cuando se lanzaron los primeros dispositivos móviles, muchos sitios no tenían estilos para el estado :active. Por lo tanto, muchos navegadores agregaron color o estilo adicional de resaltado para mostrar una respuesta al usuario.

La mayoría de los navegadores usan la propiedad outline de CSS para mostrar un anillo al rededor de un elemento cuando este elemento tiene el foco. Puedes suprimirlo de la siguiente manera:

.btn:focus {
  outline: 0;

  // Add replacement focus styling here (i.e. border)
}

Safari y Chrome agregan un color de resalte cuando se presiona un elemento. Puede evitarse con la propiedad -webkit-tap-highlight-color de CSS:

/* Webkit / Chrome Specific CSS to remove tap
highlight color */
.btn {
  -webkit-tap-highlight-color: transparent;
}

Pruébalo

Internet Explorer tiene un comportamiento similar en Windows Phone, pero se suprime por medio de una metaetiqueta:

<meta name="msapplication-tap-highlight" content="no">

Firefox tiene dos efectos secundarios que se deben controlar.

La seudoclase -moz-focus-inner, que agrega un contorno a los elementos táctiles, y que puede quitarse con la configuración border: 0.

Si usas un elemento <button> en Firefox, se le aplica un degradado, que puede quitarse con la configuración background-image: none.

/* Firefox Specific CSS to remove button
differences and focus ring */
.btn {
  background-image: none;
}

.btn::-moz-focus-inner {
  border: 0;
}

Pruébalo

Deshabilitar user-select

Cuando creas tu IU, es posible que desees permitirles a los usuarios interactuar con tus elementos, pero necesites suprimir el comportamiento predeterminado de seleccionar texto al mantener presionado o al desplazar un mouse por tu IU.

Puedes hacerlo con la propiedad user-select de CSS, pero ten en cuenta que si lo haces en contenido, puede ser extremadamente exasperante para los usuarios que quieran seleccionar el texto del elemento. Por lo tanto, asegúrate de usarlo con precaución y moderación.

user-select: none;

Implementar gestos personalizados

Si tienes una idea para implementar interacciones y gestos personalizados en tu sitio, existen dos temas a tener en cuenta:

  1. Cómo admitir todos los navegadores.
  2. Cómo mantener un índice de fotogramas alto.

En este artículo, trataremos precisamente estos temas, veremos las API que necesitamos admitir para todos los navegadores y también cómo usar estos eventos de manera eficiente.

Según lo que desees que haga tu gesto, probablemente quieras que los usuarios interactúen con un elemento a la vez o quieras que puedan interactuar con varios elementos al mismo tiempo.

Veremos dos ejemplos en este artículo, los cuales demuestran la compatibilidad para todos los navegadores y cómo mantener el índice de fotogramas alto.

GIF de ejemplo de entrada táctil en un documento

El primer ejemplo permitirá al usuario interactuar con un elemento. En este caso, tal vez quieras que se le otorguen todos los eventos táctiles a ese elemento, siempre que el gesto tenga origen en el mismo elemento. Por ejemplo, mover un dedo fuera del elemento deslizable aún puede controlar el elemento.

Esto resulta útil ya que le proporciona mucha flexibilidad al usuario, pero impone una restricción sobre la forma en la que el usuario puede interactuar con tu IU.

GIF de ejemplo de entrada táctil en un elemento

Sin embargo, si esperas que los usuarios interactúen con varios elementos a la vez (con función multitáctil), deberías limitar la función táctil al elemento específico.

Esto brinda mayor flexibilidad a los usuarios, pero complica la lógica para manipular la IU y es menos resistente a los errores de los usuarios.

Agregar receptores de eventos

En Chrome (a partir de la versión 55), Internet Explorer y Edge, se recomienda usar PointerEvents como método para implementar gestos personalizados.

En otros navegadores, lo correcto es utilizar TouchEvents y MouseEvents.

La mayor función de PointerEvents es que combina varios tipos de entrada, incluidos los eventos de mouse, lápiz o táctiles, en un grupo de callbacks. Los eventos que se deben recibir son pointerdown, pointermove, pointerup y pointercancel.

Los equivalentes para otros navegadores son touchstart, touchmove, touchend y touchcancel para eventos táctiles; y si quisieras implementar los mismos gestos para la entrada de mouse, necesitarías implementar mousedown, mousemove y mouseup.

Si tienes preguntas sobre qué eventos debes usar, mira esta tabla de eventos táctiles, de mouse y puntero.

Para usar estos eventos, se requiere llamar al método addEventListener() en un elemento de DOM, con el nombre de un evento, una función callback y un booleano. El booleano determina si deberías detectar el evento antes o después de que otros elementos hayan tenido la oportunidad de detectar e interpretar los eventos. (true significa que quieres al evento antes que otros elementos).

El siguiente es un ejemplo de recepción para el comienzo de una interacción.

// Check if pointer events are supported.
if (window.PointerEvent) {
  // Add Pointer Event Listener
  swipeFrontElement.addEventListener('pointerdown', this.handleGestureStart, true);
  swipeFrontElement.addEventListener('pointermove', this.handleGestureMove, true);
  swipeFrontElement.addEventListener('pointerup', this.handleGestureEnd, true);
  swipeFrontElement.addEventListener('pointercancel', this.handleGestureEnd, true);
} else {
  // Add Touch Listener
  swipeFrontElement.addEventListener('touchstart', this.handleGestureStart, true);
  swipeFrontElement.addEventListener('touchmove', this.handleGestureMove, true);
  swipeFrontElement.addEventListener('touchend', this.handleGestureEnd, true);
  swipeFrontElement.addEventListener('touchcancel', this.handleGestureEnd, true);

  // Add Mouse Listener
  swipeFrontElement.addEventListener('mousedown', this.handleGestureStart, true);
}

Pruébalo

Controlar la interacción para un único elemento

En el breve fragmento de código que se muestra anteriormente, solo se añadió el receptor de eventos de inicio para eventos de mouse. La razón de esto es que los eventos de mouse solo se desencadenarán cuando el cursor se desplace sobre el elemento al que se agrega el receptor de eventos.

TouchEvents realizará el seguimiento de un gesto tras su comienzo independientemente del lugar donde ocurrió la respuesta táctil y PointerEvents realizará el seguimiento de eventos independientemente del lugar donde ocurrió la respuesta táctil. Llamamos a setPointerCapture en un elemento DOM.

Para los eventos de finalización y movimientos de mouse, se agregan los receptores de eventos en el método de inicio para el gesto y se agregan los receptores al documento, lo que significa que puedes seguir el cursor hasta que el gesto se complete.

Estos son los pasos para implementarlo:

  1. Agrega todos los receptores de TouchEvent y PointerEvent. Para MouseEvents, agrega únicamente el evento de inicio.
  2. En el callback del gesto de inicio, enlaza los eventos de movimiento y finalización al documento. De esta forma, se recibirán todos los eventos del mouse, tanto si el evento ocurrió en el elemento original o no. Para PointerEvents, debemos llamar a setPointerCapture() en el elemento original para recibir el resto de los eventos. A continuación, se debe gestionar el inicio del gesto.
  3. Gestiona los eventos de movimiento.
  4. En el evento de finalización, quita del documento los receptores de movimiento y finalización del mouse, y finaliza el gesto.

A continuación, encontrarás un fragmento de nuestro método handleGestureStart(), que agrega los eventos de movimiento y finalización al documento:

// Handle the start of gestures
this.handleGestureStart = function(evt) {
  evt.preventDefault();

  if(evt.touches && evt.touches.length > 1) {
    return;
  }

  // Add the move and end listeners
  if (window.PointerEvent) {
    evt.target.setPointerCapture(evt.pointerId);
  } else {
    // Add Mouse Listeners
    document.addEventListener('mousemove', this.handleGestureMove, true);
    document.addEventListener('mouseup', this.handleGestureEnd, true);
  }

  initialTouchPos = getGesturePointFromEvent(evt);

  swipeFrontElement.style.transition = 'initial';
}.bind(this);

Pruébalo

El callback de finalización que agregamos es handleGestureEnd(), que quita los receptores de movimiento y finalización del documento, y libera la captura del puntero cuando finaliza el gesto:

// Handle end gestures
this.handleGestureEnd = function(evt) {
  evt.preventDefault();

  if(evt.touches && evt.touches.length > 0) {
    return;
  }

  rafPending = false;

  // Remove Event Listeners
  if (window.PointerEvent) {
    evt.target.releasePointerCapture(evt.pointerId);
  } else {
    // Remove Mouse Listeners
    document.removeEventListener('mousemove', this.handleGestureMove, true);
    document.removeEventListener('mouseup', this.handleGestureEnd, true);
  }

  updateSwipeRestPosition();

  initialTouchPos = null;
}.bind(this);

Pruébalo

Utilizando este patrón para agregar el evento de movimiento al documento, si el usuario comienza a interactuar con un elemento y traslada el gesto fuera del elemento, seguiremos recibiendo movimientos del mouse en cualquier lugar de la página porque los eventos se reciben del documento.

En este diagrama, se muestra el comportamiento de los eventos táctiles si agregamos los eventos de movimiento y finalización al documento cuando comienza un gesto.

Ejemplo de enlace de eventos táctiles al documento en
klzzwxh:0048

Responder a las acciones táctiles con eficiencia

Ahora que ya solucionamos los eventos de inicio y finalización, estamos en condiciones de responder a los eventos táctiles.

En cualquier evento de inicio y movimiento, puedes extraer fácilmente x e y de un evento.

En el siguiente ejemplo, para verificar si el evento es de un TouchEvent, se verifica si existe targetTouches. Si es así, se extrae clientX y clientY de la primera acción táctil. Si el evento es un PointerEvent o MouseEvent, se extrae clientX y clientY directamente del evento.

function getGesturePointFromEvent(evt) {
    var point = {};

    if(evt.targetTouches) {
      // Prefer Touch Events
      point.x = evt.targetTouches[0].clientX;
      point.y = evt.targetTouches[0].clientY;
    } else {
      // Either Mouse event or Pointer Event
      point.x = evt.clientX;
      point.y = evt.clientY;
    }

    return point;
  }

Pruébalo

Un TouchEvent tiene tres listas con datos de acciones táctiles:

  • touches: lista de todas las acciones táctiles actuales en la pantalla, independientemente del elemento DOM en las que se encuentren.
  • targetTouches: lista de las acciones táctiles actuales en el elemento DOM al que está enlazado el evento.
  • changedTouches: lista de las acciones táctiles que se modificaron y provocaron la ejecución del evento.

En la mayoría de los casos, targetTouches te brinda toda la información que necesitas. Para obtener más información sobre estas listas, consulta las listas de acciones táctiles.

Usar requestAnimationFrame

Como los callbacks de eventos se ejecutan en el subproceso principal, es buena idea ejecutar la mínima cantidad de código en los callbacks de nuestros eventos para que nuestro índice de fotogramas sea alto y evitar un rendimiento malo.

Usando requestAnimationFrame(), podemos actualizar la IU justo antes de que el navegador intente dibujar un fotograma. Además, nos ayudará a quitar actividades de nuestros callbacks de eventos.

Si no conoces requestAnimationFrame(), aquí podrás encontrar más información.

Una implementación muy utilizada es guardar las coordenadas x e y de los eventos de inicio y movimiento, y solicitar un fotograma de animación dentro del callback del evento de movimiento.

En nuestra demostración, almacenamos la posición inicial de la acción táctil en handleGestureStart() (busca initialTouchPos):

// Handle the start of gestures
this.handleGestureStart = function(evt) {
  evt.preventDefault();

  if(evt.touches && evt.touches.length > 1) {
    return;
  }

  // Add the move and end listeners
  if (window.PointerEvent) {
    evt.target.setPointerCapture(evt.pointerId);
  } else {
    // Add Mouse Listeners
    document.addEventListener('mousemove', this.handleGestureMove, true);
    document.addEventListener('mouseup', this.handleGestureEnd, true);
  }

  initialTouchPos = getGesturePointFromEvent(evt);

  swipeFrontElement.style.transition = 'initial';
}.bind(this);

El método handleGestureMove() guarda la posición de su evento antes de solicitar un fotograma de animación si es necesario y pasa nuestra función onAnimFrame() como callback:

this.handleGestureMove = function (evt) {
  evt.preventDefault();

  if(!initialTouchPos) {
    return;
  }

  lastTouchPos = getGesturePointFromEvent(evt);

  if(rafPending) {
    return;
  }

  rafPending = true;

  window.requestAnimFrame(onAnimFrame);
}.bind(this);

El valor onAnimFrame es una función que, cuando se la llama, cambia nuestra IU para moverla. Cuando pasamos esta función a requestAnimationFrame(), le solicitamos al navegador que la llame justo antes de actualizar la página (es decir, realizar cualquier cambio en la página).

En el callback handleGestureMove(), primero verificamos si rafPending es false (indicará si requestAnimationFrame() llamó a onAnimFrame() desde el último evento de movimiento). Esto significa que habrá un solo elemento requestAnimationFrame() en espera de ejecución a la vez.

Cuando se ejecuta nuestro callback onAnimFrame(), configuramos la propiedad transform de los elementos que queremos mover antes de actualizar rafPending a false. De esta forma, permitimos que el próximo evento táctil solicite un nuevo fotograma de animación.

function onAnimFrame() {
  if(!rafPending) {
    return;
  }

  var differenceInX = initialTouchPos.x - lastTouchPos.x;

  var newXTransform = (currentXPosition - differenceInX)+'px';
  var transformStyle = 'translateX('+newXTransform+')';
  swipeFrontElement.style.webkitTransform = transformStyle;
  swipeFrontElement.style.MozTransform = transformStyle;
  swipeFrontElement.style.msTransform = transformStyle;
  swipeFrontElement.style.transform = transformStyle;

  rafPending = false;
}

Controlar gestos con acciones táctiles

La propiedad touch-action de CSS te permite controlar el comportamiento táctil predeterminado de un elemento. En nuestros ejemplos, usamos touch-action: none para evitar que el navegador utilice la acción táctil del usuario. En consecuencia, podemos interceptar todos los eventos táctiles.

/* Pass all touches to javascript */
touch-action: none;

touch-action: none es una opción de último recurso ya que no le permite al navegador utilizar sus comportamientos predeterminados. En muchos casos, es mejor usar alguna de las opciones que se describen más adelante.

touch-action te permite inhabilitar gestos implementados por un navegador. Por ejemplo, IE10+ admite el gesto de presionar dos veces para hacer zoom. Si estableces una acción táctil de manipulation, evitas el comportamiento predeterminado que esté asociado a la acción de presionar dos veces.

De esta forma, puedes implementar tú mismo el gesto de presionar dos veces.

A continuación, encontrarás una lista con valores muy utilizados de acciones táctiles:

Parámetros de acciones táctiles
touch-action: none El navegador no controlará ninguna interacción táctil.
touch-action: pinch-zoom Inhabilita todas las interacciones del navegador, como `touch-action: none`, excepto `pinch-zoom`, que la controlará el navegador.
touch-action: pan-y pinch-zoom Gestiona desplazamientos horizontales en JavaScript sin inhabilitar los desplazamientos verticales ni la acción de pellizcar para hacer zoom (p. ej., en carreteles de imágenes).
touch-action: manipulation Inhabilita el gesto de presionar dos veces, que evita demoras en el navegador. Permite que el navegador controle los desplazamientos y la acción de pellizcar para hacer zoom.

Admitir versiones anteriores de IE

Si deseas admitir IE10, deberás gestionar versiones de PointerEvents con prefijos del proveedor.

Para corroborar la compatibilidad de PointerEvents, normalmente buscarías window.PointerEvent, pero en IE10, debes buscar window.navigator.msPointerEnabled.

Los nombres de los eventos con prefijos del proveedor son 'MSPointerDown', 'MSPointerUp' and 'MSPointerMove'.

En el siguiente ejemplo, se explica cómo corroborar la compatibilidad y cambiar el nombre de los eventos.

var pointerDownName = 'pointerdown';
var pointerUpName = 'pointerup';
var pointerMoveName = 'pointermove';

if(window.navigator.msPointerEnabled) {
  pointerDownName = 'MSPointerDown';
  pointerUpName = 'MSPointerUp';
  pointerMoveName = 'MSPointerMove';
}

// Simple way to check if some form of pointerevents is enabled or not
window.PointerEventsSupport = false;
if(window.PointerEvent || window.navigator.msPointerEnabled) {
  window.PointerEventsSupport = true;
}

Para obtener más información, consulta este artículo sobre actualizaciones de Microsoft.

Referencia

Seudoclases para estados de acciones táctiles

Clase Ejemplo Descripción
:hover Botón presionado Se ejecuta cuando el cursor se coloca encima de un elemento. Es útil cambiar la IU cuando esto ocurre para incentivar a los usuarios a interactuar con los elementos.
:focus Botón con foco Se ejecuta cuando el usuario pasa de un elemento a otro en la página. Este estado le permite al usuario saber con qué elemento está interactuando; también le permite al usuario navegar fácilmente por la IU con un teclado.
:active Botón presionado Se ejecuta cuando se selecciona el elemento (por ejemplo, cuando el usuario hace clic en un elemento o lo toca).

En Touch Events de w3, encontrarás la referencia completa de los eventos táctiles.

Eventos táctiles, de mouse y de punteros

Estos eventos son la base para agregar nuevos gestos a tu app:

Eventos táctiles, de mouse y de punteros
touchstart, mousedown y pointerdown Se llama cuando un dedo toca por primera vez un elemento o cuando el usuario hace clic con el mouse.
touchmove, mousemove y pointermove Se llama cuando el usuario mueve el dedo por la pantalla o arrastra con el mouse.
touchend, mouseup y pointerup Se llama cuando el usuario quita el dedo de la pantalla o suelta el botón del mouse.
touchcancel pointercancel Se llama cuando el navegador cancela los gestos de acciones táctiles. Por ejemplo, el usuario toca una app web y después cambia de pestaña.

Listas de las acciones táctiles

Cada evento táctil incluye tres atributos de lista:

Atributos de los eventos táctiles
touches Lista de todas las acciones táctiles actuales en la pantalla, independientemente de los elementos que se estén tocando.
targetTouches Lista de las acciones táctiles que se iniciaron en el elemento del evento actual. Por ejemplo, si el destino es un <button>, solo obtendrás las acciones táctiles actualmente en dicho botón. Si el destino es el documento, obtendrás todas las acciones táctiles actualmente en el documento.
changedTouches Lista de las acciones táctiles que se modificaron y provocaron la ejecución del evento:
  • En el evento touchstart, lista de los puntos táctiles que se acaban de activar con el evento actual.
  • En el evento touchmove, lista de los puntos táctiles que se movieron desde el último evento.
  • En los eventos touchend y touchcancel, lista de los puntos táctiles que se acaban de quitar de la superficie.

Habilitar compatibilidad con estado active en iOS

Infortunadamente, Safari en iOS no establece el estado active de forma predeterminada. Para comenzar a usarlo, debes agregar un receptor de evento touchstart al cuerpo del documento o a cada elemento.

Debes hacerlo desde una prueba de usuario-agente para que solo se ejecute en dispositivos iOS.

Agregar un inicio táctil al cuerpo tiene la ventaja de afectar a todos los elementos del DOM; sin embargo, es posible que esto provoque problemas de rendimiento durante el desplazamiento de la página.

window.onload = function() {
  if(/iP(hone|ad)/.test(window.navigator.userAgent)) {
    document.body.addEventListener('touchstart', function() {}, false);
  }
};

La alterativa es agregar los receptores de inicio táctil a todos los elementos de la página con los que se pueda interactuar. De esta forma, se evitan algunos de los problemas de rendimiento.

window.onload = function() {
  if(/iP(hone|ad)/.test(window.navigator.userAgent)) {
    var elements = document.querySelectorAll('button');
    var emptyFunction = function() {};
    for(var i = 0; i < elements.length; i++) {
      elements[i].addEventListener('touchstart', emptyFunction, false);
    }
  }
};