Apontar para o futuro

Sérgio Gomes

Apontar para coisas na Web era simples. Você tinha um mouse, movia-o para lá e para cá, às vezes pressionava botões, e era isso. Tudo que não era um mouse era emulado como um, e os desenvolvedores sabiam exatamente o que esperar.

Simples não significa necessariamente bom. Com o tempo, tornou-se cada vez mais importante que nem tudo fosse (ou fingindo ser) um mouse: você podia ter canetas sensíveis à pressão e à inclinação, para uma liberdade criativa incrível. Você podia usar os dedos, então tudo o que você precisava era o dispositivo e a mão. E por que não usar mais de um dedo enquanto você está nisso?

Já temos eventos de toque há algum tempo para ajudar com isso, mas eles são uma API totalmente separada especificamente para toque, forçando você a programar dois modelos de evento separados se quiser oferecer suporte a mouse e toque. O Chrome 55 vem com um padrão mais recente que unifica os dois modelos: eventos de ponteiro.

Um modelo de evento único

Os eventos de ponteiro unificam o modelo de entrada de ponteiro para o navegador, reunindo toques, canetas e mouses em um único conjunto de eventos. Exemplo:

document.addEventListener('pointermove',
    ev => console.log('The pointer moved.'));
foo.addEventListener('pointerover',
    ev => console.log('The pointer is now over foo.'));

Confira uma lista de todos os eventos disponíveis, que devem parecer bastante familiares se você já conhece os eventos do mouse:

pointerover O ponteiro entrou na caixa delimitadora do elemento. Isso acontece imediatamente para dispositivos com suporte ao passar o cursor ou antes de um evento pointerdown para dispositivos sem suporte.
pointerenter Semelhante a pointerover, mas não flutua e processa descendentes de maneira diferente. Detalhes sobre a especificação.
pointerdown O ponteiro entrou no estado de botão ativo, com um botão sendo pressionado ou o contato sendo estabelecido, dependendo da semântica do dispositivo de entrada.
pointermove O ponteiro mudou de posição.
pointerup O ponteiro deixou o estado do botão ativo.
pointercancel Algo aconteceu e é improvável que o ponteiro emita mais eventos. Isso significa que você precisa cancelar todas as ações em andamento e voltar para um estado de entrada neutro.
pointerout O ponteiro saiu da caixa delimitadora do elemento ou da tela. Também após um pointerup, se o dispositivo não oferecer suporte ao passar o cursor.
pointerleave Semelhante a pointerout, mas não flutua e processa descendentes de maneira diferente. Detalhes sobre a especificação.
gotpointercapture O elemento recebeu a captura de ponteiro.
lostpointercapture O ponteiro que estava sendo capturado foi liberado.

Diferentes tipos de entrada

Em geral, os eventos de ponteiro permitem que você escreva código de forma independente da entrada, sem precisar registrar manipuladores de eventos separados para diferentes dispositivos de entrada. É claro que você ainda precisa estar atento às diferenças entre os tipos de entrada, como se o conceito de passar o cursor se aplica. Se você quiser diferenciar diferentes tipos de dispositivos de entrada, talvez para fornecer código/funcionalidade separada para entradas diferentes, faça isso nos mesmos manipuladores de eventos usando a propriedade pointerType da interface PointerEvent. Por exemplo, se você estivesse codificando uma gaveta de navegação lateral, poderia ter a seguinte lógica no evento pointermove:

switch(ev.pointerType) {
    case 'mouse':
    // Do nothing.
    break;
    case 'touch':
    // Allow drag gesture.
    break;
    case 'pen':
    // Also allow drag gesture.
    break;
    default:
    // Getting an empty string means the browser doesn't know
    // what device type it is. Let's assume mouse and do nothing.
    break;
}

Ações padrão

Em navegadores com recursos de toque, alguns gestos são usados para rolar, aplicar zoom ou atualizar a página. No caso de eventos de toque, você ainda vai receber eventos enquanto essas ações padrão estiverem ocorrendo. Por exemplo, touchmove ainda será acionado enquanto o usuário estiver rolando.

Com eventos de ponteiro, sempre que uma ação padrão como rolagem ou zoom for acionada, você receberá um evento pointercancel para informar que o navegador assumiu o controle do ponteiro. Exemplo:

document.addEventListener('pointercancel',
    ev => console.log('Go home, the browser is in charge now.'));

Velocidade integrada: esse modelo permite um desempenho melhor por padrão, em comparação com eventos de toque, em que você precisa usar listeners de eventos passivos para alcançar o mesmo nível de capacidade de resposta.

É possível impedir que o navegador assuma o controle com a propriedade CSS touch-action. Definir como none em um elemento desativa todas as ações definidas pelo navegador iniciadas sobre esse elemento. No entanto, há vários outros valores para controle mais refinado, como pan-x, para permitir que o navegador reaja ao movimento no eixo x, mas não no eixo y. O Chrome 55 é compatível com os seguintes valores:

auto Padrão: o navegador pode realizar qualquer ação padrão.
none O navegador não tem permissão para realizar ações padrão.
pan-x O navegador só pode realizar a ação padrão de rolagem horizontal.
pan-y O navegador só pode executar a ação padrão de rolagem vertical.
pan-left O navegador só pode executar a ação padrão de rolagem horizontal e apenas mover a página para a esquerda.
pan-right O navegador só pode executar a ação padrão de rolagem horizontal e só pode mover a página para a direita.
pan-up O navegador só pode executar a ação padrão de rolagem vertical e apenas para mover a página para cima.
pan-down O navegador só pode executar a ação padrão de rolagem vertical e apenas para mover a página para baixo.
manipulation O navegador só pode executar ações de rolagem e zoom.

Captura de ponteiro

Já passou uma hora frustrante depurando um evento mouseup quebrado até perceber que o usuário estava soltando o botão fora do alvo de clique? Não? Talvez seja só eu.

Ainda assim, até agora não havia uma maneira muito boa de resolver esse problema. Claro, você pode configurar o gerenciador mouseup no documento e salvar um estado no aplicativo para acompanhar as coisas. Essa não é a solução mais limpa, mas é uma boa opção se você estiver criando um componente da Web e tentando manter tudo bem isolado.

Com os eventos do ponteiro, há uma solução muito melhor: é possível capturar o ponteiro para garantir que você receba o evento pointerup (ou qualquer outro dos amigos evasivos).

const foo = document.querySelector('#foo');
foo.addEventListener('pointerdown', ev => {
    console.log('Button down, capturing!');
    // Every pointer has an ID, which you can read from the event.
    foo.setPointerCapture(ev.pointerId);
});

foo.addEventListener('pointerup', 
    ev => console.log('Button up. Every time!'));

Suporte ao navegador

No momento da redação deste artigo, os eventos de ponteiro têm suporte no Internet Explorer 11, Microsoft Edge, Chrome e Opera, e têm suporte parcial no Firefox. Confira uma lista atualizada em caniuse.com.

Você pode usar o polyfill de eventos de ponteiro para preencher as lacunas. Como alternativa, a verificação de suporte do navegador no momento da execução é simples:

if (window.PointerEvent) {
    // Yay, we can use pointer events!
} else {
    // Back to mouse and touch events, I guess.
}

Os eventos de ponteiro são um candidato fantástico para melhoria progressiva: basta modificar seus métodos de inicialização para fazer a verificação acima, adicionar manipuladores de eventos de ponteiro no bloco if e mover os manipuladores de eventos de mouse/toque para o bloco else.

Então, faça um teste e conte o que achou!