Introdução ao visualViewport

Jake Archibald
Jake Archibald

E se eu disser que há mais de uma viewport?

BRRRRAAAAAAMMMMMMMMMM

E a janela de visualização que você está usando agora é uma janela de visualização dentro de uma janela de visualização.

BRRRRAAAAAAMMMMMMMMMM

Às vezes, os dados fornecidos pelo DOM se referem a uma das viewports e não à outra.

BRRRRAAAAM… espera aí.

É 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 com zoom, além de um minimapa à direita mostrando a posição das viewports na página.

As coisas são bem simples durante a rolagem normal. A área verde representa a viewport do layout, que os itens position: fixed grudam.

As coisas ficam estranhas quando o zoom por pinça é introduzido. A caixa vermelha representa a janela de visualização, que é a parte da página que podemos ver. Essa janela de visualização pode se mover enquanto os elementos position: fixed permanecem onde estavam, anexados à janela de visualização do layout. Se você mover o cursor em um limite da janela de visualização do layout, ela vai arrastar a janela de visualização do layout.

Como melhorar a compatibilidade

Infelizmente, as APIs da Web são inconsistentes em termos de qual viewport elas se referem e também são inconsistentes entre os navegadores.

Por exemplo, element.getBoundingClientRect().y retorna o deslocamento dentro da janela de visualização do layout. Isso é legal, mas muitas vezes queremos a posição na página. Então 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 é interrompido quando o usuário faz zoom.

O Chrome 61 muda window.scrollY para se referir à viewport do layout, o que significa que o código acima funciona mesmo com zoom de pinça. Na verdade, os navegadores estão mudando lentamente todas as propriedades posicionais para se referir à janela de visualização de layout.

Com exceção de uma nova propriedade…

Como expor a janela de visualização visual para o script

Uma nova API expõe a área de visualização visual como window.visualViewport. É um rascunho de especificação, com aprovação em vários navegadores, e está sendo lançado no Chrome 61.

console.log(window.visualViewport.width);

Confira o que window.visualViewport nos dá:

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 de cima da janela de visualização visual e a janela de visualização do layout, em pixels CSS.
pageLeft Distância entre a borda esquerda da janela de visualização visual e o limite esquerdo do documento, em pixels CSS.
pageTop Distância entre a borda superior da janela de visualização visual e o limite superior do documento, em pixels CSS.
width Largura da viewport visual em pixels CSS.
height Altura da janela de visualização visual em pixels CSS.
scale A escala aplicada pelo zoom de aproximação. Se o conteúdo tiver o dobro do tamanho devido ao zoom, ele retornará 2. Isso não é afetado por devicePixelRatio.

Há também alguns eventos:

window.visualViewport.addEventListener('resize', listener);
visualViewport eventos
resize É acionado quando width, height ou scale muda.
scroll Acionado quando offsetLeft ou offsetTop muda.

Demonstração

O vídeo no início deste artigo foi criado usando visualViewport. Confira no Chrome 61 ou mais recente. Ele usa visualViewport para fixar o minimapa no canto superior direito da viewport visual e aplica uma escala inversa para que ele sempre apareça com o mesmo tamanho, mesmo com o gesto de pinça.

Problemas

Os eventos só são acionados quando a viewport visual muda

Parece uma coisa óbvia de declarar, mas me surpreendeu quando joguei com visualViewport pela primeira vez.

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

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

Se você quiser saber sobre todas as mudanças na viewport visual, incluindo pageTop e pageLeft, também vai precisar detectar o evento de rolagem da janela:

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

Evitar trabalho duplicado com vários listeners

Assim como detectar scroll e resize na janela, é provável que você chame algum tipo de função "update" como resultado. No entanto, é comum que muitos desses eventos aconteçam ao mesmo tempo. Se o usuário redimensionar a janela, ela acionará resize, mas também scroll com bastante frequência. Para melhorar o desempenho, 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
    });
}

Enviei uma questão 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

Buggy: usa um manipulador de eventos.

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

Em vez disso, faça o seguinte:

O que fazer

Funciona: usa um listener de eventos

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

Os valores de deslocamento são arredondados

Acho que isso é outro bug do Chrome.

offsetLeft e offsetTop são arredondados, o que é bastante impreciso depois que o usuário aumenta o zoom. É possível notar os problemas com isso durante a demonstração. Se o usuário aproximar e mover lentamente, o minimapa vai se encaixar entre os pixels sem zoom.

A taxa de eventos é lenta

Assim como outros eventos resize e scroll, eles não são acionados em todos os frames, principalmente em dispositivos móveis. Você pode conferir isso durante a demonstração. Depois de aplicar o zoom, o minimapa tem problemas para permanecer fixado na janela de visualização.

Acessibilidade

Na demonstração, usei visualViewport para evitar o zoom de pinça do usuário. Isso faz sentido para esta demonstração específica, mas pense bem 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, você pode ocultar itens decorativos position: fixed para tirá-los do caminho do usuário. Mas, novamente, tome cuidado para não ocultar algo que o usuário esteja tentando analisar mais de perto.

Você pode considerar postar em um serviço de análise quando o usuário ampliar. Isso pode ajudar a identificar páginas que os usuários estão tendo dificuldade no nível de zoom padrão.

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

Pronto. visualViewport é uma API que resolve problemas de compatibilidade.