Agrégale un toque a tu sitio

Cada vez más dispositivos tienen pantallas táctiles, desde teléfonos hasta pantallas de escritorio. Tu app debe responder al tacto de forma intuitiva y atractiva.

Cada vez más dispositivos tienen pantallas táctiles, desde teléfonos hasta pantallas de computadoras de escritorio. Cuando los usuarios eligen interactuar con la IU, la app debe responder al tacto de forma intuitiva.

Cómo 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ó?

El simple hecho de alterar el color de un elemento cuando los usuarios tocan o interactúan con partes de la IU ofrece la seguridad básica de que el sitio funciona. Esto no solo disminuye la frustración, sino que también brinda un estilo ágil y responsivo.

Los elementos del DOM pueden heredar cualquiera de los siguientes estados: predeterminado, foco, desplazamiento y activo. Para cambiar nuestra IU en cada uno de estos estados, debemos aplicar estilos a las siguientes 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 suppresses the border
  color / outline when focused */
  outline: 0;
}

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

Probar

Imagen que muestra diferentes colores para los estados de los botones

En la mayoría de los navegadores para dispositivos móviles, los estados de hover o hover se aplican a un elemento después de que se presiona.

Piensa detenidamente qué estilos establecerás y cómo se verán para los usuarios después de que terminen de tocar la pantalla.

Suprimir estilos predeterminados del navegador

Una vez que agregues estilos para los diferentes estados, notarás que la mayoría de los navegadores implementan sus propios estilos cuando el usuario lo toca. Esto se debe en gran medida a que, cuando se lanzaron los dispositivos móviles por primera vez, muchos sitios no tenían un diseño para el estado :active. Como resultado, muchos navegadores agregaron color o estilo de resaltado adicional para brindar comentarios a los usuarios.

La mayoría de los navegadores usan la propiedad outline de CSS para mostrar un anillo alrededor de un elemento cuando este está enfocado. 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 resaltado cuando se presiona, que se puede evitar con la propiedad -webkit-tap-highlight-color de CSS:

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

Probar

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

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

Firefox tiene dos efectos secundarios que se deben controlar.

Configura border: 0 para quitar la seudoclase -moz-focus-inner, que agrega un esquema de los elementos táctiles.

Si usas un elemento <button> en Firefox, se aplica un gradiente que puedes quitar si configuras background-image: none.

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

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

Probar

Inhabilitando la selección de usuarios

Cuando creas tu IU, puede haber situaciones en las que quieras que los usuarios interactúen con tus elementos, pero quieras suprimir el comportamiento predeterminado de seleccionar texto cuando mantienes presionado o arrastras el mouse sobre la IU.

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

/* Example: Disable selecting text on a paragraph element: */
p.disable-text-selection {
  user-select: none;
}

Cómo implementar gestos personalizados

Si tienes una idea para interacciones y gestos personalizados en tu sitio, hay dos temas que debes tener en cuenta:

  1. Cómo brindar compatibilidad con todos los navegadores
  2. Cómo mantener una velocidad de fotogramas alta

En este artículo, analizaremos exactamente estos temas, veremos las APIs que necesitamos admitir para todos los navegadores y, luego, veremos cómo usamos estos eventos de manera eficiente.

Según lo que quieras que haga tu gesto, es probable que quieras que el usuario interactúe con un elemento a la vez o que pueda interactuar con varios elementos al mismo tiempo.

Veremos dos ejemplos en este artículo, los cuales demuestran la compatibilidad con todos los navegadores y cómo mantener la velocidad de fotogramas alta.

GIF de ejemplo de entrada táctil en un documento

El primer ejemplo permitirá al usuario interactuar con un elemento. En este caso, es posible que quieras que se otorguen todos los eventos táctiles a ese elemento, siempre que el gesto haya comenzado inicialmente en el elemento. Por ejemplo, mover un dedo fuera del elemento deslizable aún puede controlar el elemento.

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

GIF de ejemplo de un elemento táctil

Sin embargo, si esperas que los usuarios interactúen con varios elementos al mismo tiempo (mediante la función multitáctil), debes restringir la función táctil al elemento específico.

Esto resulta más flexible para los usuarios, pero complica la lógica para manipular la IU y es menos resistente a los errores del usuario.

Cómo agregar objetos de escucha de eventos

En Chrome (versión 55 y posteriores), Internet Explorer y Edge, PointerEvents es el enfoque recomendado para implementar gestos personalizados.

En otros navegadores, TouchEvents y MouseEvents son el enfoque correcto.

La excelente función de PointerEvents es que combina varios tipos de entrada (incluidos los eventos del mouse, los eventos táctiles y de lápiz) en un conjunto de devoluciones de llamada. Los eventos que se deben escuchar son pointerdown, pointermove, pointerup y pointercancel.

Los equivalentes en otros navegadores son touchstart, touchmove, touchend y touchcancel para los eventos táctiles. Si quisieras implementar el mismo gesto para la entrada del mouse, tendrías que implementar mousedown, mousemove y mouseup.

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

Para usar estos eventos, debes llamar al método addEventListener() en un elemento del DOM, junto con el nombre de un evento, una función de devolución de llamada y un valor booleano. El valor booleano determina si debes capturar el evento antes o después de que otros elementos hayan tenido la oportunidad de captar y de interpretar los eventos. (true significa que quieres el evento antes que otros elementos).

Este es un ejemplo de escucha para el inicio 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);
}

Probar

Cómo controlar la interacción con un solo elemento

En el breve fragmento de código anterior, solo agregamos el objeto de escucha de eventos de inicio para eventos del mouse. El motivo es que los eventos del mouse solo se activarán cuando el cursor se desplace sobre el elemento al que se agrega el objeto de escucha de eventos.

TouchEvents realizará un seguimiento de un gesto después de que se inicie, independientemente del lugar en el que se produzca el toque, y PointerEvents hará un seguimiento de los eventos, independientemente de dónde se produzca el toque después de que llamemos a setPointerCapture en un elemento del DOM.

Para los eventos de finalización y movimiento del mouse, agregamos los objetos de escucha de eventos en el método de inicio del gesto y los agregamos al documento, lo que significa que puede hacer un seguimiento del cursor hasta que se complete el gesto.

Los pasos para implementar esto son los siguientes:

  1. Agrega todos los objetos de escucha TouchEvent y PointerEvent. Para MouseEvents, agrega solo el evento de inicio.
  2. Dentro de la devolución de llamada de gesto de inicio, vincula los eventos de movimiento y finalización del mouse al documento. De esta manera, se recibirán todos los eventos del mouse, independientemente de si el evento ocurrió en el elemento original o no. Para PointerEvents, debemos llamar a setPointerCapture() en nuestro elemento original para recibir todos los demás eventos. Luego, controla el inicio del gesto.
  3. Controla los eventos de movimiento.
  4. En el evento de finalización, quita del documento los objetos de escucha de movimiento y finalización del mouse, y finaliza el gesto.

A continuación, se muestra 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);

Probar

La devolución de llamada de finalización que agregamos es handleGestureEnd(), que quita los objetos de escucha de movimiento y de 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);

Probar

Si se sigue este patrón de agregar el evento de movimiento al documento, si el usuario comienza a interactuar con un elemento y mueve su gesto fuera de él, seguiremos recibiendo movimientos del mouse independientemente de dónde se encuentren en la página, ya que los eventos se reciben desde el documento.

En este diagrama, se muestra lo que hacen los eventos táctiles cuando agregamos los eventos de movimiento y finalización al documento una vez que comienza un gesto.

Ilustración de la vinculación de eventos táctiles a documentos en &quot;touchstart&quot;

Responder al tacto con eficiencia

Ahora que ya resolvimos los eventos de inicio y finalización, podemos responder a los eventos táctiles.

Para cualquiera de los eventos de inicio y movimiento, puedes extraer fácilmente x y y de un evento.

En el siguiente ejemplo, se comprueba si el evento proviene de un TouchEvent. Para ello, se verifica si targetTouches existe. Si es así, extrae clientX y clientY del primer toque. Si el evento es PointerEvent o MouseEvent, 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;
  }

Probar

Un elemento TouchEvent tiene tres listas que contienen datos táctiles:

  • touches: Es la lista de todas las acciones táctiles actuales en la pantalla, independientemente del elemento DOM en el que se encuentren.
  • targetTouches: Es la lista de toques que se realizan actualmente en el elemento DOM al que está vinculado el evento.
  • changedTouches: Es la lista de los toques que se modificaron y que se activó el evento.

En la mayoría de los casos, targetTouches te brinda todo lo que necesitas y quieres. (para obtener más información sobre estas listas, consulta Listas de contactos táctiles).

Usa requestAnimationFrame

Dado que las devoluciones de llamada de eventos se activan en el subproceso principal, debemos ejecutar la menor cantidad de código posible en las devoluciones de llamada para nuestros eventos a fin de mantener la velocidad de fotogramas alta y evitar los bloqueos.

Con requestAnimationFrame(), tenemos la oportunidad de actualizar la IU justo antes de que el navegador intente dibujar un fotograma y nos ayudará a quitar parte del trabajo de nuestras devoluciones de llamada de eventos.

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

Una implementación típica es guardar las coordenadas x y y de los eventos de inicio y movimiento, y solicitar un fotograma de animación dentro de la devolución de llamada del evento de movimiento.

En nuestra demostración, almacenamos la posición inicial del toque 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() almacena la posición de su evento antes de solicitar un fotograma de animación si es necesario y pasa nuestra función onAnimFrame() como la devolución de llamada:

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 pasas esta función a requestAnimationFrame(), le indicamos al navegador que la llame justo antes de que esté a punto de actualizar la página (es decir, que pinte cualquier cambio en la página).

En la devolución de llamada handleGestureMove(), primero verificamos si rafPending es falso, lo que indica si requestAnimationFrame() llamó a onAnimFrame() desde el último evento de movimiento. Esto significa que solo tenemos un requestAnimationFrame() en espera de ejecución a la vez.

Cuando se ejecuta nuestra devolución de llamada a onAnimFrame(), configuramos la transformación en cualquier elemento que queremos mover antes de actualizar rafPending a false, lo que permite que el siguiente 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;
}

Cómo 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 realice alguna acción con el toque del usuario, lo que nos permite interceptar todos los eventos táctiles.

/* Pass all touches to javascript: */
button.custom-touch-logic {
  touch-action: none;
}

touch-action: none es una opción esencial, ya que impide todos los comportamientos predeterminados del navegador. En muchos casos, es mejor usar alguna de las siguientes opciones.

touch-action te permite inhabilitar los gestos que implementa un navegador. Por ejemplo, IE10+ admite el gesto de presionar dos veces para hacer zoom. Si configuras un touch-action de manipulation, evitas el comportamiento predeterminado de presionar dos veces.

De esta manera, puedes implementar por tu cuenta un gesto de presionar dos veces.

A continuación, se muestra una lista de valores touch-action de uso general:

Parámetros de acción táctil
touch-action: none El navegador no controlará las interacciones táctiles.
touch-action: pinch-zoom Inhabilita todas las interacciones del navegador como `touch-action: none`, excepto `pelliz-zoom`, que el navegador aún controla.
touch-action: pan-y pinch-zoom Controla los desplazamientos horizontales en JavaScript sin inhabilitar el desplazamiento vertical ni el pellizco para acercar (p. ej., carruseles de imágenes).
touch-action: manipulation Inhabilita el gesto de presionar dos veces, lo que evita cualquier retraso de clic por parte del navegador. Permite que el navegador controle el desplazamiento y la acción de pellizcar para acercar.

Compatibilidad con versiones anteriores de IE

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

Para comprobar la compatibilidad de PointerEvents, debes buscar window.PointerEvent, pero en IE10, debes buscar window.navigator.msPointerEnabled.

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

En el siguiente ejemplo, se muestra cómo verificar la compatibilidad y cambiar los nombres 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 de actualizaciones de Microsoft.

Reference

Seudoclases para estados táctiles

Clase Ejemplo Descripción
:hover
Botón presionado
Se ingresa cuando el cursor se coloca sobre un elemento. Los cambios en la IU cuando se coloca el cursor sobre ellos son útiles para alentar a los usuarios a interactuar con los elementos.
:foco
Botón con estado de enfoque
Se ingresa cuando el usuario navega por los elementos de una página. El estado del enfoque le permite al usuario saber con qué elemento está interactuando en ese momento. También le permite navegar fácilmente por la IU con un teclado.
:activo
Botón presionado
Se ingresa cuando se selecciona un elemento, por ejemplo, cuando un usuario hace clic en un elemento o lo toca.

Puedes encontrar la referencia definitiva de los eventos táctiles aquí: Eventos táctiles de W3C.

Eventos táctiles, de mouse y del puntero

Estos eventos son los componentes básicos para agregar gestos nuevos a tu aplicación:

Eventos táctiles, de mouse y de puntero
touchstart, mousedown, pointerdown Se llama a este método cuando un dedo toca un elemento por primera vez o cuando el usuario hace clic con el mouse hacia abajo.
touchmove, mousemove, pointermove Se llama a este método cuando el usuario mueve el dedo por la pantalla o arrastra con el mouse.
touchend, mouseup, pointerup Se llama a este método cuando el usuario levanta el dedo de la pantalla o suelta el mouse.
touchcancel pointercancel Este método se llama cuando el navegador cancela los gestos táctiles. Por ejemplo, un usuario toca una app web y, luego, cambia de pestaña.

Listas de contactos

Cada evento táctil incluye tres atributos de lista:

Atributos del evento táctil
touches Es la lista de todas las acciones táctiles actuales en la pantalla, independientemente de los elementos que se toquen.
targetTouches Lista de toques que comenzaron en el elemento que es el objetivo del evento actual. Por ejemplo, si te vinculas a un <button>, solo recibirás las acciones táctiles actuales en ese botón. Si vinculas el documento, obtendrás todos los toques.
changedTouches Lista de las acciones táctiles que se modificaron y provocaron la ejecución del evento:
  • Para el evento touchstart, lista de los puntos táctiles que se acaban de activar con el evento actual
  • Para el evento touchmove, la lista de los puntos táctiles que se movieron desde el último evento.
  • Para los eventos touchend y touchcancel, lista de los puntos táctiles que se acaban de quitar de la superficie.

Habilita la compatibilidad con el estado activo en iOS

Lamentablemente, Safari en iOS no aplica el estado activo de forma predeterminada. Para que funcione, debes agregar un objeto de escucha de eventos touchstart al cuerpo del documento o a cada elemento.

Debes hacerlo mediante una prueba de usuario-agente, de modo que solo se ejecute en dispositivos iOS.

Agregar un inicio táctil al cuerpo tiene la ventaja de aplicarse a todos los elementos del DOM; sin embargo, esto puede presentar problemas de rendimiento cuando te desplazas por la página.

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

La alternativa es agregar los objetos de escucha de inicio táctil a todos los elementos de la página con los que se pueda interactuar. De esta forma, se aliviarán 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);
    }
  }
};