Adicione um toque ao seu site

As touchscreens estão disponíveis em cada vez mais dispositivos, de celulares a telas de computadores. Seu app precisa responder ao toque de maneira intuitiva e bonita.

As touchscreens estão disponíveis em cada vez mais dispositivos, desde smartphones a telas de computadores. Quando os usuários optam por interagir com a interface, o app precisa responder ao toque de maneiras intuitivas.

Responder a estados dos elementos

Você já tocou ou clicou em um elemento em uma página da Web e se perguntou se o site o detectou?

A simples mudança de cor de um elemento à medida que os usuários tocam ou interagem com partes da sua interface oferece uma garantia básica de que o site está funcionando. Isso não apenas alivia a frustração, mas também pode criar uma sensação ágil e responsiva.

Os elementos DOM podem herdar qualquer um destes estados: padrão, foco, passar o cursor e ativo. Para mudar a interface para cada um desses estados, precisamos aplicar estilos às pseudoclasses :hover, :focus e :active, conforme mostrado abaixo:

.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;
}

Faça um teste

Imagem ilustrando cores diferentes para estados
de botão

Na maioria dos navegadores para dispositivos móveis, os estados hover e/ou focus serão aplicados a um elemento depois que ele for tocado.

Considere cuidadosamente quais estilos você define e como eles vão ficar para o usuário depois de terminar o toque.

Supressão de estilos padrão do navegador

Depois de adicionar estilos para os diferentes estados, você vai perceber que a maioria dos navegadores implementa os próprios estilos em resposta ao toque de um usuário. Isso ocorre principalmente porque, quando os dispositivos móveis foram lançados, vários sites não tinham estilo para o estado :active. Como resultado, muitos navegadores adicionaram uma cor ou estilo de destaque adicional para dar feedback ao usuário.

A maioria dos navegadores usa a propriedade CSS outline para mostrar um anel em torno de um elemento quando ele está focado. Ela pode ser suprimida com:

.btn:focus {
    outline: 0;

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

O Safari e o Chrome adicionam uma cor de destaque de toque que pode ser impedida com a propriedade CSS -webkit-tap-highlight-color:

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

Faça um teste

O Internet Explorer no Windows Phone tem um comportamento semelhante, mas é suprimido com uma metatag:

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

O Firefox tem dois efeitos colaterais.

A pseudoclasse -moz-focus-inner, que adiciona um contorno a elementos tocáveis, pode ser removida definindo border: 0.

Se você estiver usando um elemento <button> no Firefox, um gradiente será aplicado, que poderá ser removido definindo background-image: none.

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

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

Faça um teste

Desativando a seleção do usuário

Ao criar a interface, pode haver cenários em que você queira que os usuários interajam com os elementos, mas suprima o comportamento padrão de selecionar texto ao tocar e manter pressionado ou arrastar o mouse sobre a interface.

Você pode fazer isso com a propriedade CSS user-select, mas saiba que fazer isso no conteúdo pode ser extremely irritante para os usuários se eles quiserem selecionar o texto no elemento. Portanto, use esse recurso com cuidado e moderação.

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

Implementar gestos personalizados

Se você tem uma ideia de interações e gestos personalizados para seu site, lembre-se de dois tópicos:

  1. Como oferecer suporte a todos os navegadores.
  2. Como manter o frame rate alto.

Neste artigo, analisaremos exatamente esses tópicos, abordando as APIs que precisamos oferecer suporte para alcançar todos os navegadores e como usamos esses eventos de forma eficiente.

Dependendo do que você quer que o gesto faça, é provável que você queira que o usuário interaja com um elemento por vez ou que ele possa interagir com vários elementos ao mesmo tempo.

Vejamos dois exemplos neste artigo, ambos demonstrando suporte a todos os navegadores e como manter o frame rate alto.

GIF de exemplo de toque no documento

O primeiro exemplo permite que o usuário interaja com um elemento. Nesse caso, você pode querer que todos os eventos de toque sejam fornecidos a esse elemento, desde que o gesto seja iniciado nele. Por exemplo, mover um dedo para fora de um elemento que permite deslizar ainda pode controlar o elemento.

Isso é útil, porque oferece muita flexibilidade para o usuário, mas impõe uma restrição sobre como o usuário pode interagir com a interface.

Exemplo de GIF de toque em um elemento

No entanto, se você espera que os usuários interajam com vários elementos ao mesmo tempo (usando multitoque), restrinja o toque ao elemento específico.

Isso é mais flexível para os usuários, mas complica a lógica de manipulação da IU e é menos resiliente a erros do usuário.

Adicione listeners de eventos

No Chrome (versão 55 e mais recentes), no Internet Explorer e no Edge, PointerEvents são a abordagem recomendada para a implementação de gestos personalizados.

Em outros navegadores, TouchEvents e MouseEvents são a abordagem correta.

A melhor característica da PointerEvents é que ela mescla vários tipos de entrada, incluindo eventos de mouse, toque e caneta, em um conjunto de callbacks. Os eventos a serem detectados são pointerdown, pointermove, pointerup e pointercancel.

Os equivalentes em outros navegadores são touchstart, touchmove, touchend e touchcancel para eventos de toque. Se você quisesse implementar o mesmo gesto para a entrada do mouse, seria necessário implementar mousedown, mousemove e mouseup.

Se você tiver dúvidas sobre quais eventos usar, consulte esta tabela de eventos de toque, mouse e ponteiro.

O uso desses eventos requer chamar o método addEventListener() em um elemento DOM, além do nome de um evento, uma função de callback e um booleano. O booleano determina se você precisa capturar o evento antes ou depois de outros elementos capturarem e interpretarem os eventos. true significa que você quer o evento antes de outros elementos.

Veja um exemplo de detecção do início de uma interação.

// 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);
}

Faça um teste

Processar interações com um único elemento

No pequeno snippet de código acima, adicionamos apenas o listener de eventos inicial para eventos de mouse. Isso acontece porque os eventos de mouse só são acionados quando o cursor está sobre o elemento em que o listener foi adicionado.

O TouchEvents vai rastrear um gesto depois que ele for iniciado, independente de onde o toque ocorre, e o PointerEvents rastreará eventos, seja qual for o local do toque depois de chamarmos setPointerCapture em um elemento DOM.

Para eventos de movimento e término do mouse, adicionamos os listeners de eventos no método de início de gesto e adicionamos os listeners ao documento, o que significa que ele pode acompanhar o cursor até que o gesto seja concluído.

As etapas para implementar isso são:

  1. Adiciona todos os listeners TouchEvent e PointerEvent. Para MouseEvents, adicione apenas o evento de início.
  2. Dentro do callback do gesto de início, vincule os eventos de movimento e término do mouse ao documento. Dessa forma, todos os eventos do mouse serão recebidos, não importa se ocorrer no elemento original ou não. Para PointerEvents, precisamos chamar setPointerCapture() no elemento original para receber todos os outros eventos. Em seguida, processe o início do gesto.
  3. Gerencie os eventos de movimento.
  4. No evento final, remova os listeners de movimento e término do mouse do documento e encerre o gesto.

Confira abaixo um snippet do método handleGestureStart() que adiciona os eventos de movimentação e encerramento ao 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);

Faça um teste

O callback final que adicionamos é handleGestureEnd(), que remove os listeners de eventos move e end do documento e libera a captura do ponteiro quando o gesto termina, desta forma:

// 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);

Faça um teste

Ao seguir esse padrão de adição do evento de movimento ao documento, se o usuário começar a interagir com um elemento e mover o gesto para fora dele, continuaremos recebendo os movimentos do mouse, independente de onde eles estão na página, porque os eventos estão sendo recebidos do documento.

Este diagrama mostra o que os eventos de toque estão fazendo à medida que adicionamos os eventos de mover e encerrar ao documento quando um gesto é iniciado.

Ilustração de eventos de toque de vinculação ao documento em
&quot;touchstart&quot;

Resposta eficiente ao toque

Agora que já cuidamos dos eventos start e end, podemos responder aos eventos de toque.

Para qualquer um dos eventos start e move, é fácil extrair x e y de um evento.

O exemplo a seguir confere se o evento é de um TouchEvent verificando se targetTouches existe. Em caso positivo, ele extrai a clientX e o clientY do primeiro toque. Se o evento for PointerEvent ou MouseEvent, ele vai extrair clientX e clientY diretamente do 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;
  }

Faça um teste

Uma TouchEvent tem três listas que contêm dados de toque:

  • touches: lista de todos os toques atuais na tela, independente do elemento DOM em que eles estão.
  • targetTouches: lista de toques atualmente no elemento DOM a que o evento está vinculado.
  • changedTouches: lista de toques com mudanças resultantes do evento ser disparado.

Na maioria dos casos, targetTouches oferece tudo o que você precisa e quer. Para mais informações sobre essas listas, consulte Listas de toques.

Usar requestAnimationFrame

Como os callbacks de evento são disparados na linha de execução principal, queremos executar o mínimo de código possível nos callbacks dos eventos, mantendo a taxa de frames alta e evitando instabilidades.

Ao usar requestAnimationFrame(), temos a oportunidade de atualizar a interface pouco antes que o navegador pretenda renderizar um frame. Isso vai nos ajudar a remover algum trabalho dos callbacks de eventos.

Se você não estiver familiarizado com requestAnimationFrame(), saiba mais aqui.

Uma implementação típica é salvar as coordenadas x e y dos eventos "start" e "move" e solicitar um frame de animação dentro do callback do evento de movimento.

Na demonstração, armazenamos a posição de toque inicial em handleGestureStart() (procure 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);

O método handleGestureMove() armazena a posição do evento antes de solicitar um frame de animação, se necessário, transmitindo nossa função onAnimFrame() como o callback:

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

  if (!initialTouchPos) {
    return;
  }

  lastTouchPos = getGesturePointFromEvent(evt);

  if (rafPending) {
    return;
  }

  rafPending = true;

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

O valor onAnimFrame é uma função que, quando chamada, muda a interface para movê-la. Ao transmitir essa função para requestAnimationFrame(), instruímos o navegador a chamá-la pouco antes que a página esteja prestes a ser atualizada (ou seja, pintar quaisquer alterações na página).

No callback handleGestureMove(), verificamos inicialmente se rafPending é falso, o que indica se onAnimFrame() foi chamado por requestAnimationFrame() desde o último evento de movimento. Isso significa que só temos um requestAnimationFrame() aguardando para ser executado por vez.

Quando nosso callback onAnimFrame() é executado, definimos a transformação em todos os elementos que queremos mover antes de atualizar rafPending para false, permitindo que o próximo evento de toque solicite um novo frame de animação.

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 usando ações de toque

A propriedade CSS touch-action permite controlar o comportamento de toque padrão de um elemento. Nos nossos exemplos, usamos touch-action: none para impedir que o navegador faça algo com o toque de um usuário, permitindo interceptar todos os eventos de toque.

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

O uso de touch-action: none é um pouco exagerado, porque impede todos os comportamentos padrão do navegador. Em muitos casos, uma das opções abaixo é uma solução melhor.

touch-action permite desativar os gestos implementados por um navegador. Por exemplo, o IE10+ suporta o gesto de tocar duas vezes para aplicar zoom. Ao definir um touch-action de manipulation, você evita o comportamento padrão de toque duas vezes.

Isso permite que você implemente um gesto de tocar duas vezes por conta própria.

Veja abaixo uma lista dos valores touch-action usados com frequência:

Parâmetros de ação de toque
touch-action: none Nenhuma interação de toque será processada pelo navegador.
touch-action: pinch-zoom Desativa todas as interações do navegador, como "touch-action: none", exceto "pinch-zoom", que ainda é processada pelo navegador.
touch-action: pan-y pinch-zoom Processe rolagens horizontais em JavaScript sem desativar a rolagem vertical ou o zoom por gesto de pinça (por exemplo, carrosséis de imagem).
touch-action: manipulation Desativa o gesto de tocar duas vezes, que evita qualquer atraso de clique do navegador. Deixa a rolagem e o gesto de pinça para cima no navegador.

Suporte a versões mais antigas do IE

Se você quiser oferecer suporte ao IE10, será necessário processar versões do PointerEvents prefixadas pelo fornecedor.

Para verificar o suporte a PointerEvents, você normalmente procuraria por window.PointerEvent, mas, no IE10, você usaria window.navigator.msPointerEnabled.

Os nomes de eventos com prefixos de fornecedor são: 'MSPointerDown', 'MSPointerUp' e 'MSPointerMove'.

O exemplo abaixo mostra como verificar o suporte e mudar os nomes dos 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 mais informações, consulte este artigo de atualizações da Microsoft.

Referência

Pseudoclasses para estados de toque

Turma Exemplo Descrição
:hover
Botão no estado pressionado
Inserido quando um cursor é colocado sobre um elemento. As mudanças na IU ao passar o cursor são úteis para incentivar os usuários a interagir com os elementos.
:foco
Botão com estado de foco
Inserido quando o usuário percorre os elementos de uma página. O estado de foco permite que o usuário saiba com qual elemento está interagindo no momento. Além disso, permite que os usuários naveguem pela interface com facilidade usando um teclado.
:ativo
Botão no estado pressionado
Inserido quando um elemento está sendo selecionado, por exemplo, quando um usuário está clicando ou tocando nele.

A referência definitiva de eventos de toque pode ser encontrada aqui: Eventos de toque do W3C.

Eventos de toque, mouse e ponteiro

Esses eventos são elementos básicos para adicionar novos gestos ao seu aplicativo:

Eventos de toque, mouse e ponteiro
touchstart, mousedown, pointerdown Isso é chamado quando um dedo toca em um elemento pela primeira vez ou quando o usuário clica no mouse.
touchmove, mousemove, pointermove Isso é chamado quando o usuário move o dedo pela tela ou arrasta com o mouse.
touchend, mouseup, pointerup Isso é chamado quando o usuário levanta o dedo da tela ou solta o mouse.
touchcancel pointercancel Isso é chamado quando o navegador cancela os gestos de toque. Por exemplo, um usuário toca em um app da Web e depois muda de guia.

Listas de toque

Cada evento de toque inclui três atributos de lista:

Atributos do evento de toque
touches Lista de todos os toques atuais na tela, independentemente dos elementos sendo tocados.
targetTouches Lista de toques iniciados no elemento de destino do evento atual. Por exemplo, se você vincular um <button>, só vai receber toques nesse botão que já estão ativos. Se você vincular o documento, receberá todos os toques que estão nele.
changedTouches Lista de toques com mudanças que resultaram no disparo do evento:
  • Para o evento touchstart: lista dos pontos de contato que acabaram de ficar ativos com o evento atual.
  • Para o evento touchmove: lista dos pontos de contato que foram movidos desde o último evento.
  • Para os eventos touchend e touchcancel: lista dos pontos de contato que acabaram de ser removidos da plataforma.

Como ativar o suporte ao estado ativo no iOS

Infelizmente, o Safari no iOS não aplica o estado active por padrão. Para que ele funcione, é preciso adicionar um listener de eventos touchstart ao corpo do documento ou a cada elemento.

Isso precisa ser feito com um teste de user agent para que ele seja executado apenas em dispositivos iOS.

Adicionar um início de toque ao corpo tem a vantagem de ser aplicado a todos os elementos no DOM. No entanto, isso pode causar problemas de desempenho ao rolar a página.

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

A alternativa é adicionar os listeners de início de toque a todos os elementos interagíveis na página, aliviando algumas questões de performance.

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);
    }
  }
};