Presentación de visualViewport

Jake Archibald
Jake Archibald

¿Y si te dijera que hay más de un viewport?

BRRRAAAAAAAMMMMMMMMMM

Y el viewport que estás usando ahora es en realidad uno dentro de otro.

BRRRAAAAAAAMMMMMMMMMM

Y, a veces, los datos que te proporciona el DOM hacen referencia a uno de esos viewports y no al otro.

BRRRRAAAAM... espera, ¿qué?

Es cierto, mira lo siguiente:

Ventana gráfica de diseño frente a viewport visual

En el video de arriba, se muestra una página web en la que se desplaza y se pellizca, junto con un minimapa a la derecha que muestra la posición de los viewports en la página.

Todo es bastante sencillo durante el desplazamiento normal. El área verde representa el viewport de diseño, al que se adhieren los elementos position: fixed.

Las cosas se complican cuando se presenta el zoom de pellizcar. El recuadro rojo representa la ventana gráfica visual, que es la parte de la página que realmente se puede ver. Este viewport puede moverse mientras los elementos position: fixed permanecen donde estaban, adjuntos al viewport de diseño. Si nos desplazamos hacia un límite del viewport de diseño, se arrastrará el viewport de diseño junto con él.

Mejora la compatibilidad

Lamentablemente, las APIs web no son uniformes en cuanto al viewport al que hacen referencia, y tampoco son uniformes en los distintos navegadores.

Por ejemplo, element.getBoundingClientRect().y muestra el desplazamiento dentro del viewport de diseño. Eso es genial, pero a menudo queremos la posición dentro de la página, así que escribimos:

element.getBoundingClientRect().y + window.scrollY

Sin embargo, muchos navegadores usan la ventana gráfica visual para window.scrollY, lo que significa que el código anterior se interrumpe cuando el usuario pellizca la pantalla.

En Chrome 61, se cambia window.scrollY para hacer referencia al viewport del diseño, lo que significa que el código anterior funciona incluso cuando se pellizca el zoom. De hecho, los navegadores están cambiando lentamente todas las propiedades posicionales para hacer referencia al viewport del diseño.

Con la excepción de una propiedad nueva...

Cómo exponer la viewport visual a una secuencia de comandos

Una nueva API expone el viewport visual como window.visualViewport. Es una especificación en borrador, con aprobación en varios navegadores, y llega a Chrome 61.

console.log(window.visualViewport.width);

window.visualViewport nos brinda lo siguiente:

visualViewport propiedades
offsetLeft Distancia entre el borde izquierdo del viewport visual y el viewport de diseño, en píxeles de CSS.
offsetTop Distancia entre el borde superior del viewport visual y el viewport de diseño, en píxeles de CSS.
pageLeft Distancia entre el borde izquierdo del viewport visual y el límite izquierdo del documento, en píxeles de CSS.
pageTop Distancia entre el borde superior del viewport visual y el límite superior del documento, en píxeles de CSS.
width Ancho de la viewport visual en píxeles de CSS.
height Es la altura de la viewport visual en píxeles de CSS.
scale Es la escala que se aplica al pellizcar la pantalla. Si el contenido tiene el doble de tamaño debido al zoom, se mostrará 2. No se ve afectado por devicePixelRatio.

También hay un par de eventos:

window.visualViewport.addEventListener('resize', listener);
visualViewport de eventos
resize Se activa cuando cambian width, height o scale.
scroll Se activa cuando cambian offsetLeft o offsetTop.

Demostración

El video al comienzo de este artículo se creó con visualViewport (puedes verlo en Chrome 61 y versiones posteriores). Utiliza visualViewport para hacer que el minimapa se adhiera a la parte superior derecha de la ventana gráfica visual y aplica una escala inversa para que siempre se vea del mismo tamaño, a pesar del zoom.

Problemas

Los eventos solo se activan cuando cambia el viewport visual

Parece algo obvio, pero me llamó la atención cuando jugué con visualViewport por primera vez.

Si el viewport del diseño cambia de tamaño, pero el viewport visual no, no obtendrás un evento resize. Sin embargo, no es común que el viewport del diseño cambie de tamaño sin que el viewport visual cambie también el ancho o la altura.

La verdadera trampa es desplazarse. Si se produce el desplazamiento, pero el viewport visual permanece estático en relación con el viewport de diseño, no se obtiene un evento scroll en visualViewport, y esto es muy común. Durante el desplazamiento normal del documento, el viewport visual permanece bloqueado en la parte superior izquierda del viewport de diseño, por lo que scroll no se activa en visualViewport.

Si deseas escuchar todos los cambios en el viewport visual, incluidos pageTop y pageLeft, también tendrás que escuchar el evento de desplazamiento de la ventana:

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

Evita duplicar el trabajo con varios objetos de escucha

De manera similar a cuando escuchas scroll y resize en la ventana, es probable que llames a algún tipo de función de "actualización". Sin embargo, es común que muchos de estos eventos ocurran al mismo tiempo. Si el usuario cambia el tamaño de la ventana, se activará resize, pero a menudo también scroll. Para mejorar el rendimiento, evita manejar el cambio varias veces:

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

Envié un problema de especificaciones sobre esto, ya que creo que puede haber una mejor manera, como un solo evento update.

Los controladores de eventos no funcionan

Debido a un error de Chrome, esto no funciona:

Qué no debes hacer

Buggy: usa un controlador de eventos.

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

En su lugar, siga estos pasos:

Funciona: usa un objeto de escucha de eventos.

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

Los valores de desplazamiento se redondean.

Creo (bueno, espero) que es otro error de Chrome.

offsetLeft y offsetTop están redondeados, lo cual es bastante inexacto una vez que el usuario acerca la imagen. Puedes ver los problemas relacionados con este proceso durante la demostración. Si el usuario acerca la imagen y se desplaza lentamente, el minimapa se ajusta entre los píxeles sin zoom.

La frecuencia de eventos es lenta

Al igual que otros eventos resize y scroll, no se activan en todos los fotogramas, especialmente en dispositivos móviles. Puedes verlo durante la demostración: una vez que pellizcas el zoom, el minimapa tiene dificultades para permanecer bloqueado en el viewport.

Accesibilidad

En la demostración, usé visualViewport para contrarrestar el zoom de pellizcar del usuario. Tiene sentido para esta demostración en particular, pero debes pensar con cuidado antes de hacer algo que anule el deseo del usuario de acercar la imagen.

Se puede usar visualViewport para mejorar la accesibilidad. Por ejemplo, si el usuario está acercando la imagen, puedes optar por ocultar los elementos decorativos de position: fixed para que no se distraigan. Pero, de nuevo, ten cuidado de no ocultar algo que el usuario intenta examinar con más detalle.

Podrías considerar publicar en un servicio de estadísticas cuando el usuario acerque el mapa. Esto podría ayudarte a identificar las páginas con las que los usuarios tienen dificultades con el nivel de zoom predeterminado.

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

Eso es todo. visualViewport es una API pequeña y útil que resuelve los problemas de compatibilidad en el proceso.