Agrégale un toque a tu sitio

Cada vez más dispositivos cuentan con pantallas táctiles, desde teléfonos hasta pantallas de escritorio. Tu app debería responder al tacto de manera intuitiva y atractiva.

Cada vez más dispositivos cuentan con pantallas táctiles, desde teléfonos hasta pantallas de escritorio. Cuando los usuarios elijan interactuar con tu IU, tu app debería responder 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ó?

Si simplemente cambias 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 alivia la frustración, sino que también puede transmitir una sensación ágil y responsiva.

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, 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 distintos colores para los estados de los botones

En la mayoría de los navegadores para dispositivos móviles, los estados hover 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 terminen de tocarlos.

Cómo suprimir los estilos predeterminados del navegador

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. Como resultado, muchos navegadores agregaron color o estilo adicional de resaltado para mostrar una respuesta al usuario.

La mayoría de los navegadores usan la propiedad CSS outline para mostrar un anillo alrededor 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;
}

Probar

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

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

Firefox tiene dos efectos secundarios que se deben controlar.

Puedes configurar border: 0 para quitar la seudoclase -moz-focus-inner, que agrega un contorno a los elementos táctiles.

Si usas un elemento <button> en Firefox, se aplica un gradiente, que puedes quitar configurando 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 del usuario

Cuando crees tu IU, es posible que quieras que los usuarios interactúen con tus elementos, pero que quieras suprimir el comportamiento predeterminado de seleccionar texto al mantener presionado o arrastrar un mouse sobre tu IU.

Puedes hacerlo con la propiedad CSS user-select, pero ten en cuenta que si lo haces en contenido, puede ser extremadamente exasperante para los usuarios si quieren seleccionar el texto del 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 implementar interacciones y gestos personalizados en tu sitio, ten en cuenta los siguientes dos temas:

  1. Cómo admitir todos los navegadores
  2. Cómo mantener una velocidad de fotogramas alta

En este artículo, trataremos precisamente estos temas, veremos las APIs 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, 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 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, es posible que desees 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 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 entrada táctil en un elemento

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 es más flexible para los usuarios, pero complica la lógica para manipular la IU y es menos resistente a los errores de los usuarios.

Cómo agregar objetos de escucha de eventos

En Chrome (versión 55 y posteriores), Internet Explorer y Edge, PointerEvents son los métodos recomendados para implementar gestos personalizados.

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

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

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

Si tienes preguntas sobre qué eventos usar, consulta esta tabla de eventos táctiles, de mouse y de 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 capturar e interpretar los eventos. (true significa que deseas que el evento se muestre antes que otros elementos).

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

Probar

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

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

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

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

Estos son los pasos para implementarlo:

  1. Agrega todos los objetos de escucha de TouchEvent y PointerEvent. Para MouseEvents, agrega solo el evento de inicio.
  2. Dentro de la devolución de llamada del gesto de inicio, vincula los eventos de movimiento y finalización del mouse 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 todos los eventos adicionales. 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, 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);

Probar

La devolución de llamada de finalización que agregamos es handleGestureEnd(), que quita los objetos de escucha de eventos de movimiento y finalización del documento, y libera la captura del puntero cuando finaliza el gesto de la siguiente manera:

// 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 seguimos 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 del elemento, seguiremos recibiendo movimientos del mouse, independientemente de dónde se encuentre en 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.

Ilustración de la vinculación de eventos táctiles al documento en &quot;touchstart&quot;

Cómo responder a las acciones táctiles con eficiencia

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

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

En el siguiente ejemplo, se verifica si el evento es de una TouchEvent. Para ello, se verifica si existe targetTouches. Si es así, se extraen clientX y clientY del primer toque. Si el evento es un 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 TouchEvent tiene tres listas con datos de acciones 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 las acciones táctiles actuales en el elemento DOM al que está vinculado el evento.
  • changedTouches: Es la lista de las acciones táctiles que se modificaron y provocaron la activación del evento.

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

Cómo usar requestAnimationFrame

Como las devoluciones de llamada de eventos se activan en el subproceso principal, queremos ejecutar la menor cantidad posible de código en las devoluciones de llamada de nuestros eventos, mantener una velocidad de fotogramas alta y evitar un rendimiento malo.

Si usas requestAnimationFrame(), podemos 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 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() 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 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 pasamos esta función a requestAnimationFrame(), le solicitamos al navegador que la llame justo antes de actualizar la página (es decir, pintar cualquier cambio en la página).

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

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 la acción táctil del usuario, lo que nos permite interceptar todos los eventos táctiles.

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

El uso de touch-action: none es una opción de último recurso, ya que evita todos los comportamientos predeterminados del navegador. En muchos casos, es mejor usar alguna de las opciones que se describen más adelante.

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

Esto te permite implementar tú mismo un gesto de presionar dos veces.

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

Parámetros de acción táctil
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 todavía controla el navegador.
touch-action: pan-y pinch-zoom Controla los desplazamientos horizontales en JavaScript sin inhabilitar el desplazamiento vertical ni el zoom con pellizco (p. ej., carruseles de imágenes).
touch-action: manipulation Inhabilita el gesto de presionar dos veces, lo que evita cualquier retraso del clic del navegador. Permite que el navegador controle los desplazamientos y la acción de pellizcar para hacer zoom.

Compatibilidad con versiones anteriores de IE

Si deseas admitir IE10, deberás controlar las 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 eventos con prefijos del proveedor son 'MSPointerDown', 'MSPointerUp' y 'MSPointerMove'.

En el siguiente ejemplo, se explica cómo corroborar 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 sobre actualizaciones de Microsoft.

Referencia

Pseudoclases para estados de acciones táctiles

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

En Eventos táctiles del W3C, encontrarás la referencia definitiva de los eventos táctiles.

Eventos táctiles, de mouse y de puntero

Estos eventos son la base para agregar nuevos gestos a tu aplicación:

Eventos táctiles, de mouse y de punteros
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.
touchmove, mousemove, pointermove Se llama a este método cuando el usuario mueve el dedo por la pantalla o lo 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 Se llama a este método cuando el navegador cancela los gestos táctiles. Por ejemplo, un usuario toca una app web y, luego, cambia de pestaña.

Listas de acciones táctiles

Cada evento táctil incluye tres atributos de lista:

Atributos del evento táctil
touches Es la lista de todos los toques actuales en la pantalla, independientemente de los elementos que se toquen.
targetTouches Es la lista de los toques que se iniciaron en el elemento que es el objetivo del evento actual. Por ejemplo, si el destino es un <button>, solo obtendrás las acciones táctiles actualmente en ese 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, una lista de los puntos táctiles que se activaron con el evento actual
  • En el evento touchmove, la lista de los puntos táctiles que se movieron desde el último evento
  • En el caso de los eventos touchend y touchcancel, incluye la lista de los puntos táctiles que se acaban de quitar de la superficie.

Habilita la compatibilidad con estado active en iOS

Infortunadamente, Safari en iOS no establece el estado active 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 con 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 alternativa es agregar los objetos de escucha de inicio táctil a todos los elementos interactivos de la página, lo que alivia algunas de las preocupaciones 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);
    }
  }
};