Présentation de visualViewport

Jake Archibald
Jake Archibald

Que se passerait-il si je vous disais qu'il existe plusieurs vues ?

BRRRRAAAAAAAMMMMMMMMMM

La fenêtre d'affichage que vous utilisez actuellement est en fait une fenêtre d'affichage dans une fenêtre d'affichage.

BRRRRAAAAAAAMMMMMMMMMM

Parfois, les données que le DOM vous fournit font référence à l'un de ces viewports et non à l'autre.

BRRRRAAAAM… Qu'est-ce que vous dites ?

C'est vrai. Regardez:

Fenêtre d'affichage de la mise en page par rapport à la fenêtre d'affichage visuelle

La vidéo ci-dessus montre le défilement et le zoom avec pincement d'une page Web, ainsi qu'une mini-carte à droite montrant la position des vues dans la page.

Le défilement standard est assez simple. La zone verte représente la fenêtre d'affichage de la mise en page, à laquelle les éléments position: fixed s'accrochent.

Les choses deviennent bizarres lorsque le zoom par pincement est introduit. Le cadre rouge représente la fenêtre d'affichage visuelle, qui correspond à la partie de la page que nous pouvons réellement voir. Cette fenêtre d'affichage peut se déplacer, tandis que les éléments position: fixed restent à leur place, attachés à la fenêtre d'affichage de la mise en page. Si nous effectuons un panoramique à la limite de la fenêtre d'affichage de la mise en page, la fenêtre d'affichage de la mise en page est entraînée.

Améliorer la compatibilité

Malheureusement, les API Web ne sont pas cohérentes en termes de vue à laquelle elles font référence, et elles ne le sont pas non plus d'un navigateur à l'autre.

Par exemple, element.getBoundingClientRect().y renvoie le décalage dans la fenêtre d'affichage de la mise en page. C'est bien, mais nous souhaitons souvent connaître la position sur la page. Nous écrivons donc:

element.getBoundingClientRect().y + window.scrollY

Toutefois, de nombreux navigateurs utilisent la fenêtre d'affichage visuelle pour window.scrollY, ce qui signifie que le code ci-dessus ne fonctionne pas lorsque l'utilisateur fait un pincement pour zoomer.

Chrome 61 modifie window.scrollY pour qu'il fasse référence au viewport de mise en page. Cela signifie que le code ci-dessus fonctionne même en cas de zoom avec pincement. En fait, les navigateurs modifient progressivement toutes les propriétés de position pour faire référence à la fenêtre d'affichage de la mise en page.

À l'exception d'une nouvelle propriété…

Exposer la fenêtre d'affichage visuelle au script

Une nouvelle API expose la fenêtre d'affichage visuelle en tant que window.visualViewport. Il s'agit d'une spécification provisoire, avec une approbation multinavigateur, et elle sera disponible dans Chrome 61.

console.log(window.visualViewport.width);

Voici ce que window.visualViewport nous donne:

visualViewport établissements
offsetLeft Distance entre le bord gauche de la fenêtre d'affichage visuelle et la fenêtre d'affichage de la mise en page, en pixels CSS.
offsetTop Distance entre le bord supérieur de la fenêtre d'affichage visuelle et la fenêtre d'affichage de la mise en page, en pixels CSS.
pageLeft Distance entre le bord gauche de la fenêtre d'affichage visuelle et la limite gauche du document, en pixels CSS.
pageTop Distance entre le bord supérieur de la fenêtre d'affichage visuelle et la limite supérieure du document, en pixels CSS.
width Largeur de la fenêtre d'affichage visuelle en pixels CSS.
height Hauteur de la fenêtre d'affichage visuelle en pixels CSS.
scale Échelle appliquée en effectuant un pincement pour zoomer. Si le contenu est deux fois plus grand en raison du zoom, la valeur 2 est renvoyée. Cela n'est pas affecté par devicePixelRatio.

Il existe également quelques événements:

window.visualViewport.addEventListener('resize', listener);
visualViewport événements
resize Se déclenche lorsque width, height ou scale change.
scroll Déclenché lorsque offsetLeft ou offsetTop change.

Démo

La vidéo au début de cet article a été créée à l'aide de visualViewport. Découvrez-la dans Chrome 61 ou version ultérieure. Elle utilise visualViewport pour que le mini-plan se fixe en haut à droite du viewport visuel et applique une échelle inverse pour qu'il apparaisse toujours de la même taille, malgré le zoom avec pincement.

Pièges

Les événements ne se déclenchent que lorsque la fenêtre d'affichage visuelle change.

Cela semble évident, mais cela m'a surpris lorsque j'ai commencé à jouer avec visualViewport.

Si la vue du canevas est redimensionnée, mais pas la vue visuelle, vous ne recevez pas d'événement resize. Toutefois, il est inhabituel que la fenêtre d'affichage de mise en page soit redimensionnée sans que la largeur/hauteur de la fenêtre d'affichage visuelle ne change également.

Le véritable problème est le défilement. Si le défilement se produit, mais que la fenêtre d'affichage visuelle reste statique par rapport à la fenêtre d'affichage de la mise en page, vous ne recevez pas d'événement scroll sur visualViewport, ce qui est très courant. Lors du défilement normal du document, la fenêtre d'affichage visuelle reste verrouillée en haut à gauche de la fenêtre d'affichage de la mise en page. scroll ne se déclenche donc pas sur visualViewport.

Si vous souhaitez connaître toutes les modifications apportées au viewport visuel, y compris pageTop et pageLeft, vous devez également écouter l'événement de défilement de la fenêtre:

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

Éviter de dupliquer le travail avec plusieurs écouteurs

Comme pour l'écoute de scroll et resize sur la fenêtre, vous êtes susceptible d'appeler une sorte de fonction "update" en conséquence. Cependant, il est courant que plusieurs de ces événements se produisent en même temps. Si l'utilisateur redimensionne la fenêtre, resize est déclenché, mais aussi très souvent scroll. Pour améliorer les performances, évitez de gérer la modification plusieurs fois:

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

J'ai signalé un problème de spécification à ce sujet, car je pense qu'il existe peut-être une meilleure méthode, comme un seul événement update.

Les gestionnaires d'événements ne fonctionnent pas

En raison d'un bug de Chrome, les opérations suivantes ne fonctionnent pas:

À éviter

Buggé : utilise un gestionnaire d'événements

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

Au lieu de cela :

À faire

Fonctionne : utilise un écouteur d'événements

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

Les valeurs de décalage sont arrondies

Je pense (enfin, j'espère) qu'il s'agit d'un autre bug Chrome.

offsetLeft et offsetTop sont arrondis, ce qui est assez inexact une fois que l'utilisateur a fait un zoom avant. Vous pouvez voir les problèmes liés à cette approche pendant la démonstration. Si l'utilisateur fait un zoom avant et un panoramique lent, la mini-carte se fixe entre les pixels non agrandis.

Le taux d'événements est lent

Comme les autres événements resize et scroll, ils ne se déclenchent pas à chaque frame, en particulier sur mobile. Vous pouvez le constater lors de la démo : une fois que vous effectuez un pincement pour faire un zoom avant, la mini-carte a du mal à rester verrouillée sur la fenêtre d'affichage.

Accessibilité

Dans la démo, j'ai utilisé visualViewport pour contrecarrer le zoom avec pincement de l'utilisateur. Cela a du sens pour cette démonstration particulière, mais vous devez réfléchir attentivement avant de faire quoi que ce soit qui remplace le désir de l'utilisateur de faire un zoom avant.

visualViewport peut être utilisé pour améliorer l'accessibilité. Par exemple, si l'utilisateur fait un zoom avant, vous pouvez choisir de masquer les éléments position: fixed décoratifs pour les éloigner de l'utilisateur. Encore une fois, veillez à ne pas masquer quelque chose que l'utilisateur essaie de voir de plus près.

Vous pouvez envisager de publier des données dans un service d'analyse lorsque l'utilisateur fait un zoom avant. Cela peut vous aider à identifier les pages avec lesquelles les utilisateurs ont des difficultés au niveau du zoom par défaut.

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

Et voilà ! visualViewport est une petite API pratique qui résout les problèmes de compatibilité au fil du temps.