Montrer la voie à suivre

Sérgio Gomes

Il était autrefois simple de pointer sur des éléments sur le Web. Vous aviez une souris, vous la déplaciez, parfois vous appuyiez sur des boutons, et c'était tout. Tout ce qui n'était pas une souris était émulé en tant que tel, et les développeurs savaient exactement sur quoi ils pouvaient compter.

La simplicité ne signifie pas nécessairement que c'est bien. Au fil du temps, il est devenu de plus en plus important que tout ne soit pas (ou ne prétende pas l'être) une souris: vous pouvez utiliser des stylos sensibles à la pression et à l'inclinaison, pour une liberté de création incroyable ; vous pouvez utiliser vos doigts, vous n'avez donc besoin que de l'appareil et de votre main ; et pourquoi ne pas utiliser plusieurs doigts ?

Nous utilisons depuis un certain temps des événements tactiles pour nous aider à y parvenir, mais il s'agit d'une API entièrement distincte, spécifiquement conçue pour les interactions tactiles. Vous êtes donc obligé de coder deux modèles d'événements distincts si vous souhaitez prendre en charge à la fois la souris et l'écran tactile. Chrome 55 est fourni avec une norme plus récente qui unifie les deux modèles: les événements de pointeur.

Modèle d'événement unique

Les événements de pointeur unifient le modèle d'entrée de pointeur pour le navigateur, en regroupant les interactions tactiles, les stylets et les souris dans un seul ensemble d'événements. Exemple :

document.addEventListener('pointermove',
    ev => console.log('The pointer moved.'));
foo.addEventListener('pointerover',
    ev => console.log('The pointer is now over foo.'));

Voici la liste de tous les événements disponibles, qui devrait vous sembler familière si vous connaissez les événements de souris:

pointerover Le pointeur est entré dans le cadre de délimitation de l'élément. Cela se produit immédiatement pour les appareils compatibles avec le survol ou avant un événement pointerdown pour les appareils qui ne le sont pas.
pointerenter Semblable à pointerover, mais ne remonte pas et gère les descendants différemment. Détails sur les spécifications
pointerdown Le pointeur est entré dans l'état de bouton actif, un bouton étant enfoncé ou un contact étant établi, en fonction de la sémantique de l'appareil d'entrée.
pointermove Le pointeur a changé de position.
pointerup Le pointeur a quitté l'état du bouton actif.
pointercancel Un événement s'est produit, ce qui signifie qu'il est peu probable que le pointeur émette d'autres événements. Cela signifie que vous devez annuler toutes les actions en cours et revenir à un état d'entrée neutre.
pointerout Le pointeur a quitté le cadre de sélection de l'élément ou de l'écran. Également après un pointerup, si l'appareil n'est pas compatible avec le survol.
pointerleave Semblable à pointerout, mais ne remonte pas et gère les descendants différemment. Détails sur les spécifications
gotpointercapture L'élément a reçu la capture du pointeur.
lostpointercapture Le pointeur qui était capturé a été libéré.

Différents types d'entrées

En général, les événements de pointeur vous permettent d'écrire du code de manière indépendante de l'entrée, sans avoir à enregistrer des gestionnaires d'événements distincts pour différents périphériques d'entrée. Bien entendu, vous devrez toujours tenir compte des différences entre les types d'entrées, par exemple si le concept de survol s'applique. Si vous souhaitez distinguer différents types d'appareils d'entrée (par exemple, pour fournir un code/une fonctionnalité distincts pour différentes entrées), vous pouvez le faire à partir des mêmes gestionnaires d'événements à l'aide de la propriété pointerType de l'interface PointerEvent. Par exemple, si vous codiez un panneau de navigation latéral, vous pourriez avoir la logique suivante pour votre événement 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;
}

Actions par défaut

Dans les navigateurs tactiles, certains gestes permettent de faire défiler la page, de zoomer ou d'actualiser la page. Dans le cas des événements tactiles, vous recevrez toujours des événements pendant que ces actions par défaut se produisent. Par exemple, touchmove continuera de se déclencher pendant que l'utilisateur fait défiler l'écran.

Avec les événements de pointeur, chaque fois qu'une action par défaut telle que le défilement ou le zoom est déclenchée, vous recevez un événement pointercancel pour vous indiquer que le navigateur a pris le contrôle du pointeur. Exemple :

document.addEventListener('pointercancel',
    ev => console.log('Go home, the browser is in charge now.'));

Vitesse intégrée: ce modèle offre de meilleures performances par défaut, par rapport aux événements tactiles, pour lesquels vous devez utiliser des écouteurs d'événements passifs pour obtenir le même niveau de réactivité.

Vous pouvez empêcher le navigateur de prendre le contrôle avec la propriété CSS touch-action. Si vous définissez cette valeur sur none pour un élément, toutes les actions définies par le navigateur lancées sur cet élément seront désactivées. Toutefois, il existe un certain nombre d'autres valeurs pour un contrôle plus précis, comme pan-x, qui permet au navigateur de réagir au mouvement sur l'axe X, mais pas sur l'axe Y. Chrome 55 accepte les valeurs suivantes:

auto Par défaut, le navigateur peut effectuer n'importe quelle action par défaut.
none Le navigateur n'est pas autorisé à effectuer d'actions par défaut.
pan-x Le navigateur n'est autorisé qu'à effectuer l'action par défaut de défilement horizontal.
pan-y Le navigateur n'est autorisé qu'à effectuer l'action par défaut de défilement vertical.
pan-left Le navigateur n'est autorisé à effectuer que l'action par défaut de défilement horizontal et uniquement pour faire un panoramique de la page vers la gauche.
pan-right Le navigateur n'est autorisé à effectuer que l'action par défaut de défilement horizontal et uniquement pour faire un panoramique de la page vers la droite.
pan-up Le navigateur n'est autorisé qu'à effectuer l'action par défaut de défilement vertical et uniquement pour faire un panoramique vers le haut de la page.
pan-down Le navigateur n'est autorisé qu'à effectuer l'action par défaut de défilement vertical et uniquement pour faire défiler la page vers le bas.
manipulation Le navigateur n'est autorisé qu'à effectuer des actions de défilement et de zoom.

Capture du pointeur

Avez-vous déjà passé une heure frustrante à déboguer un événement mouseup défectueux, jusqu'à ce que vous réalisiez que c'est parce que l'utilisateur lâche le bouton en dehors de votre cible de clic ? Personne ? OK, peut-être que ce n'est que moi.

Cependant, jusqu'à présent, il n'existait pas de solution vraiment efficace pour résoudre ce problème. Bien sûr, vous pouvez configurer le gestionnaire mouseup sur le document et enregistrer un état dans votre application pour garder une trace des choses. Ce n'est toutefois pas la solution la plus propre, en particulier si vous créez un composant Web et que vous essayez de tout garder bien isolé.

Les événements de pointeur offrent une bien meilleure solution: vous pouvez capturer le pointeur pour être sûr de recevoir cet événement pointerup (ou tout autre de ses amis insaisissables).

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!'));

Prise en charge des navigateurs

Au moment de la rédaction de cet article, les événements de pointeur sont compatibles avec Internet Explorer 11, Microsoft Edge, Chrome et Opera, et partiellement avec Firefox. Vous trouverez une liste à jour sur caniuse.com.

Vous pouvez utiliser le polyfill d'événements de pointeur pour combler les lacunes. Vous pouvez également vérifier la compatibilité du navigateur au moment de l'exécution:

if (window.PointerEvent) {
    // Yay, we can use pointer events!
} else {
    // Back to mouse and touch events, I guess.
}

Les événements de pointeur sont un excellent candidat pour l'amélioration progressive: il vous suffit de modifier vos méthodes d'initialisation pour effectuer la vérification ci-dessus, d'ajouter des gestionnaires d'événements de pointeur dans le bloc if et de déplacer vos gestionnaires d'événements de souris/de contact vers le bloc else.

Alors, n'hésitez pas à les essayer et à nous faire part de vos commentaires.