Introdução ao visualViewport

Jake Archibald
Jake Archibald

E se eu dissesse que há mais de uma janela de visualização.

BRRRRAAAAAAAMMMMMMMMMM

A janela de visualização que você está usando agora é, na verdade, uma janela de visualização dentro de uma janela de visualização.

BRRRRAAAAAAAMMMMMMMMMM

Às vezes, os dados fornecidos pelo DOM se referem a uma dessas janelas de visualização, não à outra.

BRRRRAAAAM... espera o quê?

É verdade, confira:

Janela de visualização de layout x janela de visualização visual

O vídeo acima mostra uma página da Web sendo rolada e ampliada com o gesto de pinça, além de um minimapa à direita mostrando a posição das janelas de visualização na página.

As coisas são muito diretas durante a rolagem normal. A área verde representa a janela de visualização de layout, à qual os itens position: fixed se mantêm.

Tudo fica estranho quando o zoom com gesto de pinça é introduzido. A caixa vermelha representa a janela de visualização visual, que é a parte da página que realmente podemos ver. Essa janela de visualização pode se mover enquanto os elementos position: fixed permanecem onde estavam, anexados à janela de visualização de layout. Se o deslocamento for feito em um limite da janela de visualização de layout, ela será arrastada junto.

Como melhorar a compatibilidade

Infelizmente, as APIs da Web são inconsistentes em relação à janela de visualização a que se referem e também são inconsistentes em todos os navegadores.

Por exemplo, element.getBoundingClientRect().y retorna o deslocamento dentro da janela de visualização de layout. Isso é legal, mas muitas vezes queremos a posição dentro da página, por isso escrevemos:

element.getBoundingClientRect().y + window.scrollY

No entanto, muitos navegadores usam a janela de visualização visual para window.scrollY, o que significa que o código acima é corrompido quando o usuário faz gesto de pinça.

O Chrome 61 muda window.scrollY para se referir à janela de visualização de layout, ou seja, o código acima funciona mesmo quando o zoom é aplicado com o gesto de pinça. Na verdade, os navegadores estão mudando lentamente todas as propriedades de posição para se referir à janela de visualização de layout.

Com exceção de uma nova propriedade...

Como expor a janela de visualização ao script

Uma nova API expõe a janela de visualização visual como window.visualViewport. É uma especificação de rascunho, com aprovação para diferentes navegadores, e é destinada ao Chrome 61.

console.log(window.visualViewport.width);

Confira o que o window.visualViewport oferece:

visualViewport propriedades
offsetLeft Distância entre a borda esquerda da janela de visualização visual e a janela de visualização de layout em pixels CSS.
offsetTop Distância entre a borda superior da janela de visualização visual e a janela de visualização de layout em pixels CSS.
pageLeft É a distância entre a borda esquerda da janela de visualização e o limite esquerdo do documento, em pixels CSS.
pageTop É a distância entre a borda superior da janela de visualização e o limite superior do documento, em pixels CSS.
width Largura da janela de visualização visual em pixels CSS.
height Altura da janela de visualização visual em pixels CSS.
scale A escala aplicada pelo zoom em pinça. Se o conteúdo tiver o dobro do tamanho devido ao zoom, isso vai retornar 2. Isso não é afetado por devicePixelRatio.

Há também alguns eventos:

window.visualViewport.addEventListener('resize', listener);
visualViewport eventos
resize Disparado quando width, height ou scale mudam.
scroll Disparado quando offsetLeft ou offsetTop mudam.

Demonstração

O vídeo no início deste artigo foi criado usando visualViewport. Confira no Chrome 61 e versões mais recentes. Ele usa visualViewport para fazer com que o minimapa fique no canto superior direito da janela de visualização visual e aplica uma escala inversa para que apareça sempre do mesmo tamanho, apesar do zoom em pinça.

Pegadinhas

Os eventos só são disparados quando a janela de visualização visual é alterada

Parece uma coisa óbvia para dizer, mas isso me chamou pela primeira vez quando jogou com visualViewport.

Se a janela de visualização de layout for redimensionada, mas a de layout não, você não receberá um evento resize. No entanto, não é comum que a janela de visualização de layout seja redimensionada sem que ela também mude a largura/altura.

O maior problema é rolar a tela. Se a rolagem ocorrer, mas a janela de visualização visual permanecer estática em relação à janela de visualização de layout, você não vai receber um evento scroll em visualViewport, e isso é muito comum. Durante a rolagem normal do documento, a janela de visualização visual permanece bloqueada no canto superior esquerdo da janela de layout. Portanto, scroll não é acionado em visualViewport.

Se quiser saber sobre todas as mudanças na janela de visualização visual, incluindo pageTop e pageLeft, também é necessário detectar o evento de rolagem da janela:

visualViewport.addEventListener('scroll', update);
visualViewport.addEventListener('resize', update);
window.addEventListener('scroll', update);

Evite duplicar o trabalho com vários listeners

Assim como na detecção de scroll e resize na janela, você provavelmente vai chamar algum tipo de função de atualização como resultado. No entanto, é comum que muitos desses eventos aconteçam ao mesmo tempo. Se o usuário redimensionar a janela, resize será acionado, mas geralmente scroll também. Para melhorar a performance, evite processar a mudança várias vezes:

// Add listeners
visualViewport.addEventListener('scroll', update);
visualViewport.addEventListener('resize', update);
addEventListener('scroll', update);

let pendingUpdate = false;

function update() {
    // If we're already going to handle an update, return
    if (pendingUpdate) return;

    pendingUpdate = true;

    // Use requestAnimationFrame so the update happens before next render
    requestAnimationFrame(() => {
    pendingUpdate = false;

    // Handle update here
    });
}

Registrei um problema de especificação para isso, porque acho que pode haver uma maneira melhor, como um único evento update.

Os manipuladores de eventos não funcionam

Devido a um bug do Chrome, isso não funciona:

O que não fazer

Erros: usa um manipulador de eventos

visualViewport.onscroll = () => console.log('scroll!');

Como alternativa:

O que fazer

Funciona: usa um listener de eventos.

visualViewport.addEventListener('scroll', () => console.log('scroll'));

Os valores de deslocamento são arredondados

Acho que (bom, espero) esse é outro bug do Chrome.

offsetLeft e offsetTop são arredondados, o que é bastante impreciso quando o usuário aumenta o zoom. É possível conferir os problemas com isso durante a demonstração. Se o usuário aumentar o zoom e movimentar lentamente, o minimapa vai ser alinhado entre pixels sem zoom.

A taxa de eventos está lenta

Como outros eventos resize e scroll, eles não disparam todos os frames, especialmente em dispositivos móveis. É possível observar isso durante a demonstração. Ao fazer o gesto de pinça, o minimapa terá problemas para permanecer fixo na janela de visualização.

Acessibilidade

Na demonstração, usei visualViewport para neutralizar o zoom de pinça do usuário. Faz sentido para essa demonstração específica, mas pense com cuidado antes de fazer qualquer coisa que substitua o desejo do usuário de aumentar o zoom.

O visualViewport pode ser usado para melhorar a acessibilidade. Por exemplo, se o usuário estiver aumentando o zoom, poderá ocultar itens position: fixed decorativos para tirá-los do caminho. Mas, novamente, tenha cuidado para não ocultar algo que o usuário está tentando olhar mais de perto.

Você pode considerar postar em um serviço de análise quando o usuário aumenta o zoom. Isso ajuda a identificar as páginas com dificuldade para os usuários no nível de zoom padrão.

visualViewport.addEventListener('resize', () => {
    if (visualViewport.scale > 1) {
    // Post data to analytics service
    }
});

Pronto. A visualViewport é uma API pequena que resolve problemas de compatibilidade ao longo do caminho.