Adicione um toque ao seu site

Telas sensíveis a toque estão disponíveis em cada vez mais dispositivos, de celulares a telas de computadores. Seu aplicativo deve responder ao toque de forma intuitiva e atraente.

Telas sensíveis a toque estão disponíveis em cada vez mais dispositivos, de celulares a telas de computadores. Quando os usuários optam por interagir com sua interface, o app precisa responder ao toque de forma intuitiva.

Responder a estados de elementos

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

A simples alteração de cor de um elemento quando os usuários tocam ou interagem com partes da sua interface proporciona uma garantia básica de que seu site está funcionando. Isso não só alivia a frustração do usuário, mas também pode proporcionar uma sensação de que o site é ágil e responsivo.

Elementos do DOM podem herdar qualquer um dos seguintes estados: padrão, foco, passar o cursor e ativo. Para mudar a interface para cada um desses estados, precisamos aplicar estilos às seguintes 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 diferentes cores para estados
de botões

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 com cautela quais estilos definir e como eles ficarão após a finalização do toque do usuário.

Como suprimir estilos padrão de navegadores

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 em grande parte porque, quando os dispositivos móveis foram lançados pela primeira vez, vários sites não tinham estilo para o estado :active. Como resultado, muitos navegadores adicionaram uma cor ou um estilo de destaque adicional para dar feedback ao usuário.

A maioria dos navegadores usa a propriedade CSS outline para mostrar um círculo em volta de um elemento quando ele é focado. Você pode suprimir essa propriedade da seguinte maneira:

.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 do Windows Phone tem um comportamento semelhante, mas ele pode ser suprimido com uma meta tag:

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

O Firefox tem dois efeitos colaterais a serem tratados.

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 pode ser removido ao definir 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

Como desativar a seleção de usuários

Durante a criação da sua interface, pode haver cenários nos quais você quer que os usuários interajam com os elementos, mas suprimindo o comportamento padrão de seleção de texto com ações manter o botão pressionado ou arrastando o mouse pela interface.

É possível fazer isso com a propriedade CSS user-select, mas saiba que fazer isso no conteúdo pode ser extremamente irritante para os usuários se eles quiserem selecionar o texto no elemento. Portanto, use 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ê tiver uma ideia para implementar interações e gestos personalizados em seu site, tenha em mente dois tópicos:

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

Neste artigo, veremos exatamente esses tópicos abrangendo as APIs que precisamos oferecer suporte para atingir todos os navegadores e, em seguida, abordaremos como usamos esses eventos de forma eficiente.

Dependendo do que você quer fazer com o gesto, é 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.

Vamos analisar dois exemplos neste artigo, ambos demonstrando suporte para todos os navegadores e como manter o frame rate alto.

GIF de exemplo de toque em um documento

O primeiro exemplo permitirá que o usuário interaja com um elemento. Nesse caso, pode ser pertinente que todos os eventos de toque sejam fornecidos ao elemento em questão, desde que o gesto seja iniciado nesse elemento. Por exemplo, mover o dedo para fora de um elemento que comporte o gesto deslizar ainda permite que o usuário controle esse elemento.

Isso é bastante útil, pois proporciona mais flexibilidade para o usuário, mas aplica uma restrição em como o usuário pode interagir com sua IU.

GIF de exemplo de toque em um elemento

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

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

Adicione listeners de eventos

No Chrome (versão 55 e posteriores), no Internet Explorer e no Edge, PointerEvents são a abordagem recomendada para implementar gestos personalizados.

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

Uma das melhores características de PointerEvents é que esse recurso mescla diversos tipos de entrada, incluindo eventos de mouse, toque e caneta, em um só 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ê quiser implementar o mesmo gesto para entrada de mouse, será necessário implementar mousedown, mousemove e mouseup.

Em caso de dúvidas sobre quais eventos usar, confira esta tabela de eventos de toque, mouse e ponteiro.

O uso desses eventos requer a chamada do método addEventListener() em um elemento DOM, junto com o nome de um evento, uma função de callback e um booleano. O booleano determina se você precisa capturar o evento antes ou depois que outros elementos tenham tido a oportunidade de capturar e interpretar os eventos. true significa que você quer que o evento apareça antes de outros elementos.

Confira um exemplo de como detectar o 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 detector de evento inicial para eventos do mouse. O motivo disso é que a maioria dos eventos de mouse só é acionada quando o cursor está sendo passado sobre o elemento ao qual o detector foi adicionado.

TouchEvents vai rastrear um gesto depois que ele for iniciado, independentemente de onde o toque ocorre, e PointerEvents vai rastrear eventos, independentemente de onde o toque ocorre depois que chamarmos setPointerCapture em um elemento DOM.

Para eventos move e end do mouse, adicionamos os detectores de evento no método de início do gesto e adicionamos os detectores ao documento, o que significa que eles podem rastrear o cursor até que o gesto seja concluído.

As etapas para implementar isso são as seguintes:

  1. Adicione todos os listeners de TouchEvent e PointerEvent. Para MouseEvents, adicione apenas o evento de início.
  2. Dentro do callback do gesto de início, vincule os eventos move e end do mouse ao documento. Assim, todos os eventos do mouse são recebidos, não importando se o evento ocorre 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. Processe os eventos de movimento.
  4. No evento de término, remova os detectores de eventos move e end do mouse do documento e encerre o gesto.

Confira abaixo um snippet do nosso método handleGestureStart(), que adiciona os eventos move e end 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 de end que adicionamos é handleGestureEnd(), que remove os detectores de eventos move e end do documento e libera a captura do ponteiro quando o gesto é concluído, assim:

// 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 adicionar o evento de movimento ao documento, se o usuário começar a interagir com um elemento e mover o gesto para fora do elemento, vamos continuar recebendo movimentos do mouse, independentemente 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 conforme adicionamos os eventos move e end ao documento após o início de um gesto.

Ilustração da vinculação de eventos de toque 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", é possível extrair facilmente x e y de um evento.

O exemplo a seguir verifica se o evento é de um TouchEvent, verificando se targetTouches existe. Se existir, ele extrai o clientX e o clientY do primeiro toque. Se o evento for um PointerEvent ou MouseEvent, ele vai extrair clientX e clientY diretamente do próprio 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

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

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

Na maioria dos casos, o 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 eventos são acionados na linha de execução principal, queremos executar o menor código possível nos callbacks dos nossos eventos, mantendo a taxa de frames alta e evitando atrasos.

Ao usar requestAnimationFrame(), temos uma oportunidade de atualizar a interface antes que o navegador renderize um frame e isso nos ajudará a remover parte do trabalho nos retornos de chamada de eventos.

Se você não conhece o requestAnimationFrame(), saiba mais aqui.

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

Na nossa demonstração, nós 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 a função 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);

O valor onAnimFrame é uma função que, quando chamada, altera a IU para movê-la. Ao transmitir essa função para requestAnimationFrame(), informamos ao navegador para chamá-la pouco antes da atualização da página (ou seja, pintar todas as 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 temos apenas um requestAnimationFrame() aguardando para ser executado em qualquer momento.

Quando nosso callback onAnimFrame() é executado, definimos a transformação nos 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 qualquer coisa com o toque de um usuário, permitindo que interceptemos todos os eventos de toque.

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

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

touch-action permite desativar gestos implementados por um navegador. Por exemplo, o IE 10 e versões mais recentes oferecem suporte a um gesto de dois toques para ativar o zoom. Ao definir um touch-action de manipulation, você impede o comportamento padrão de toque duplo.

Isso permite que você implemente um gesto de dois toques pessoalmente.

Confira abaixo uma lista de valores touch-action usados com frequência:

Parâmetros de ação de toque
touch-action: none Nenhuma interação por 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 é processado pelo navegador.
touch-action: pan-y pinch-zoom Processe rolagens horizontais no JavaScript sem desativar a rolagem vertical ou o zoom de aproximação (por exemplo, carrosséis de imagens).
touch-action: manipulation Desativa o gesto de tocar duas vezes, o que evita qualquer atraso de clique pelo navegador. Deixa a rolagem e o zoom por gesto de pinça a cargo do navegador.

Oferecer suporte a versões mais antigas do IE

Se quiser oferecer suporte ao IE10, você vai precisar processar versões do prefixo do fornecedor de PointerEvents.

Para verificar o suporte de PointerEvents, você geralmente procuraria window.PointerEvent, mas no IE10, você procuraria 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, confira este artigo de atualizações da Microsoft.

Referência

Pseudoclasses para estados de toque

Turma Exemplo Descrição
:hover
Botão no estado pressionado
Inserida quando um cursor é posicionado sobre um elemento. As mudanças na interface em caso de eventos hover são úteis para incentivar os usuários a interagir com os elementos.
:focus
Botão com estado de foco
Inserida quando o usuário percorre elementos de uma página usando a tecla Tab. O estado de foco permite que o usuário saiba com qual elemento ele está interagindo no momento. Ele também 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 para eventos de toque pode ser encontrada aqui: W3C Touch Events.

Eventos de toque, mouse e ponteiro

Esses eventos são os blocos de construção para adicionar novos gestos ao 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 É 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 Chamado quando o navegador cancela os gestos de toque. Por exemplo, um usuário toca em um app da Web e 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 que estão sendo tocados.
targetTouches Lista de toques iniciados no elemento que é o alvo do evento atual. Por exemplo, se você vincular a um <button>, só vai receber os toques que estiverem nesse botão. Se você vincular o evento ao documento, vai receber todos os toques que se encontrarem no documento.
changedTouches Lista de toques com mudanças resultantes do evento ser acionado:
  • Para o evento touchstart, - lista dos pontos de toque que acabaram de se tornar ativos com o evento atual.
  • Para o evento touchmove, - lista dos pontos de toque que foram movidos desde o último evento.
  • Para os eventos touchend e touchcancel - lista dos pontos de toque que acabaram de ser removidos da superfície.

Como ativar o suporte ao estado active no iOS

Infelizmente, o Safari no iOS não aplica o estado active por padrão. Para fazer com que ele funcione, adicione um listener de evento touchstart ao corpo do documento ou a cada elemento.

Isso deve ser feito em um teste de user-agent para que essa configuração só seja executada em dispositivos iOS.

Adicionar um touchstart ao corpo do documento tem a vantagem de ser aplicado a todos os elementos no DOM, mas 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 detectores de touchstart a todos os elementos interativos da página, o que amenizará alguns dos problemas de desempenho.

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