Apontar para o futuro

Sérgio Gomes

Apontar para coisas na Web costumava ser simples. Você tinha um mouse, movia-o ou apertava botões, e só isso. Tudo o que não era um mouse foi emulado como um, e os desenvolvedores sabiam exatamente com o que contar.

No entanto, simples não significa necessariamente bom. Com o tempo, ficou cada vez mais importante que nem tudo era (ou fingiu ser) um mouse: era possível ter canetas sensíveis à pressão e inclinação, ter uma liberdade criativa incrível; você poderia usar os dedos, então só precisava do dispositivo e da sua mão; e por que não usar mais de um dedo enquanto está trabalhando?

Já temos eventos de toque para nos ajudar com isso, mas eles são uma API totalmente separada especificamente para toque, forçando você a codificar dois modelos de evento separados se quiser oferecer suporte a mouse e toque. O Chrome 55 tem 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.'));

Veja uma lista de todos os eventos disponíveis, que podem ser bastante familiares se você souber usar eventos de mouse:

pointerover O ponteiro entrou na caixa delimitadora do elemento. Isso acontece imediatamente em dispositivos que têm suporte ao recurso de passar o cursor ou antes de um evento pointerdown para dispositivos que não oferecem suporte ao recurso.
pointerenter Semelhante a pointerover, mas não gera bolhas e processa os descendentes de forma diferente. Detalhes da especificação.
pointerdown O ponteiro entrou no estado do botão ativo, com um botão pressionado ou estabelecido um contato, dependendo da semântica do dispositivo de entrada.
pointermove A posição do ponteiro foi alterada.
pointerup O ponteiro saiu do estado do botão ativo.
pointercancel Algo aconteceu, o que significa que é improvável que o ponteiro emita mais eventos. Isso significa que você precisa cancelar qualquer ação em andamento e voltar a 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 recurso de passar o cursor.
pointerleave Semelhante a pointerout, mas não gera bolhas e processa os descendentes de forma diferente. Detalhes da especificação.
gotpointercapture O elemento recebeu uma captura de ponteiro.
lostpointercapture O ponteiro que estava sendo capturado foi liberado.

Diferentes tipos de entrada

Geralmente, os eventos de ponteiro permitem escrever código de maneira independente de entrada, sem a necessidade de registrar manipuladores de eventos separados para diferentes dispositivos de entrada. Obviamente, você ainda precisará ficar atento às diferenças entre os tipos de entrada, por exemplo, se o conceito de passar o cursor se aplica. No entanto, se você quiser diferenciar os tipos de dispositivo de entrada, talvez fornecendo código/funcionalidade separados 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 toque ativado, certos gestos são usados para fazer a página rolar, dar zoom ou atualizar. No caso de eventos de toque, você ainda receberá eventos enquanto essas ações padrão estiverem ocorrendo. Por exemplo, touchmove ainda será acionado enquanto o usuário estiver rolando a tela.

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 oferece melhor desempenho por padrão em comparação com os eventos de toque, em que você precisaria usar listeners de eventos passivos para ter 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 e iniciadas nesse 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 oferece suporte aos seguintes valores:

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

Captura de ponteiro

Você já passou uma hora frustrante na depuração de um evento mouseup corrompido até perceber que a causa disso é que o usuário está soltando o botão fora do destino de clique? Não? Ok, talvez seja só eu, então.

Ainda assim, até agora, não havia uma boa maneira de lidar com esse problema. É claro que você pode configurar o gerenciador mouseup no documento e salvar algum estado no aplicativo para acompanhar tudo. No entanto, essa não é a solução mais limpa, principalmente se você estiver criando um componente da Web e tentando manter tudo bom e isolado.

Com os eventos de ponteiro, há uma solução muito melhor: você pode capturar o ponteiro para ter certeza de que recebeu o evento pointerup (ou qualquer outro dos amigos esquivos dele).

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

Até o momento, os eventos de ponteiro são compatíveis com o Internet Explorer 11, Microsoft Edge, Chrome e Opera, e com suporte parcial no Firefox. Você encontra uma lista atualizada em caniuse.com.

Você pode usar o polyfill de eventos de ponteiro para preencher as lacunas. Como alternativa, a verificação do suporte a navegadores 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 ótimo candidato para o aprimoramento progressivo: basta modificar os 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.

Teste os recursos abaixo e nos conte o que achou.