4 points par GN⁺ 2023-09-30 | 1 commentaires | Partager sur WhatsApp
  • Les diagrammes interactifs de Red Blob Games doivent réagir immédiatement à l’écran lorsque le lecteur déplace lui-même des objets ; la gestion des entrées a donc été organisée autour des Pointer Events pour traiter à la fois la souris et le tactile
  • L’ancienne approche avec mousedown/mouseup/mousemove pouvait laisser l’état de glisser dans une situation incohérente lorsque le pointeur sortait de l’objet ou que le bouton était relâché en dehors, ce qui nécessitait des correctifs comme des gestionnaires globaux
  • Les entrées tactiles suivent un mode de propagation différent de celui de la souris, et un glisser à un doigt peut aussi déclencher le défilement de la page ; on appelle donc preventDefault() dans touchstart pour réduire les conflits
  • La recette finale combine pointerdown, pointermove, pointerup, pointercancel et setPointerCapture(), tout en vérifiant le bouton gauche et le décalage initial pour éviter que l’objet ne saute brusquement
  • Les exceptions propres aux plateformes comme les objets contenant du texte ou des images, le multitouch, les glissements imbriqués, <canvas>, la combinaison hover + drag, ou encore lostpointercapture, demandent un traitement séparé dans une vraie interface

Objectif du gestionnaire d’entrée par glisser

  • De nombreuses pages interactives comportent des objets déplaçables par glisser que le lecteur manipule directement, et le diagramme doit réagir aux changements de position de ces objets
  • L’objectif est de créer un gestionnaire d’événements d’entrée qui fonctionne à la fois avec la souris et avec les entrées tactiles
  • Le flux de base consiste à démarrer le glisser dans pointerdown, mettre à jour la position dans pointermove, puis terminer le glisser dans pointerup ou lors d’un événement d’annulation
  • Dans une implémentation réelle, cela devient plus complexe que ce modèle simple à cause des boutons de souris, du toucher à plusieurs doigts, de la cible de propagation des événements et du menu contextuel au clic droit
  • La recette de base combine les éléments suivants
    • dans pointerdown, retourner si event.button !== 0 afin de ne traiter que le bouton principal
    • convertir les coordonnées de l’événement en coordonnées du diagramme
    • enregistrer au départ dx et dy, c’est-à-dire la différence entre la position de l’objet et celle du pointeur
    • capturer le pointeur avec el.setPointerCapture(event.pointerId)
    • dans pointermove, mettre à jour state.pos en ajoutant le décalage enregistré
    • dans pointerup et pointercancel, faire state.dragging = null
    • appeler preventDefault() dans touchstart
  • Cette recette est un gestionnaire d’événements d’entrée ; le traitement réel de la sortie doit être relié à un gestionnaire d’état distinct
  • Le code complet se trouve sur la page d’exemples, qui inclut aussi des exemples de glissement horizontal de nombres, de peinture sur canvas et de glisser limité
  • Les tests couvrent Gecko/Firefox, Blink/Chrome et WebKit/Safari sur Mac, Windows, Linux, Android, iPhone et iPad
  • Le stylet avec hover, les appareils hybrides tactile + souris et l’entrée vocale n’ont pas été testés
  • Cette méthode n’utilise pas l’API HTML Drag and Drop API, qui sert à déposer un élément sur un autre ; pour les diagrammes, il faut lire directement les événements souris et tactiles

Limites de l’utilisation exclusive des événements souris

  • L’implémentation initiale consistait à attacher directement mousedown, mouseup et mousemove à l’élément glissable
  • En apparence cela fonctionne, mais deux problèmes surviennent
    • si le pointeur se déplace rapidement, il peut ne plus être sur le cercle et ne plus recevoir les événements
    • si le bouton est relâché en dehors du cercle, l’état dragging peut rester bloqué
  • Une amélioration consiste à recevoir mousedown sur le cercle, puis à attacher temporairement mousemove et mouseup au document pendant le glisser
  • En supprimant dans mouseup les gestionnaires mousemove et mouseup attachés au document, le glisser à la souris devient plus fiable
  • Cette approche ne gère que la souris et ne traite pas les événements tactiles

Différences des événements tactiles

  • Les entrées tactiles utilisent touchstart, touchend et touchmove à la place de mousedown, mouseup et mousemove
  • Les événements tactiles sont automatiquement capturés après touchstart, de sorte que tous les touchmove sont envoyés à l’élément d’origine
  • Grâce à cette propriété, il n’est pas nécessaire d’ajouter temporairement des gestionnaires globaux au document comme avec les événements souris
  • Lorsque le navigateur doit annuler une séquence tactile, il envoie touchcancel
  • Une implémentation de glisser tactile appelle preventDefault() dans touchstart, puis met à jour la position dans touchmove à partir des coordonnées de event.changedTouches[0]
  • Cette approche ne gère que le tactile et ne traite pas les événements souris

Unifier souris et tactile avec Pointer Events

  • Pour gérer à la fois la souris et le tactile, il fallait auparavant beaucoup de gestionnaires d’événements ; cette approche a été utilisée jusqu’en 2021
  • Entre 2011 et 2014, le projet d3 utilisait d3-drag, et les projets sans d3 implémentaient leur propre code séparé souris + tactile
  • Les Pointer Events unifient et simplifient la gestion de la souris et du tactile
    • MS IE a ajouté la prise en charge en 2012
    • Chrome en 2017, Firefox en 2018 et Safari en 2020
  • Le comportement des navigateurs a évolué avec le temps, et Chrome a basculé certains événements en passive mode par défaut en 2017, ce qui a provoqué un problème où la page défilait lorsqu’on essayait de faire glisser un objet
  • Safari et Firefox ont appliqué le même changement en 2018
  • L’utilisation de la pointer capture permet une logique plus simple, sans avoir à ajouter et supprimer des gestionnaires globaux sur document comme c’était nécessaire avec les événements souris

Conserver le suivi du pointeur avec Pointer Capture

  • La pointer capture permet de continuer à suivre le pointeur même s’il sort du cercle, du diagramme ou de la fenêtre du navigateur
  • Avec les événements souris, il fallait attacher des gestionnaires au document pour obtenir le même effet, alors qu’avec Pointer Events, setPointerCapture(event.pointerId) est plus simple
  • Sans capture du pointeur, le glisser peut s’interrompre lorsqu’on déplace rapidement l’objet ou qu’on sort du diagramme avant d’y revenir
  • Le fait de relâcher le bouton hors du diagramme ou de passer à une autre fenêtre avec Alt+Tab modifie aussi la gestion de fin de glisser selon qu’il y a capture ou non
  • Sur les appareils tactiles, la capture du pointeur est le comportement par défaut, mais avec la souris elle doit être définie explicitement

Défilement tactile et preventDefault()

  • Sur les appareils tactiles, un glisser à un doigt provoque par défaut le défilement de la page
  • Si ce même glisser à un doigt déplace aussi le cercle, le défilement et le glisser de l’objet se produisent en même temps
  • La solution la plus simple consiste à appliquer touch-action: none au diagramme en CSS, mais dans ce cas il devient impossible de faire défiler n’importe quelle zone du diagramme
  • Le comportement souhaité est de bloquer le défilement uniquement lorsqu’on fait glisser le cercle, tout en autorisant le défilement lorsqu’on fait glisser le diagramme lui-même
  • Le résultat comparatif est le suivant
    • touch-action: none sur l’ensemble du diagramme : le défilement s’arrête aussi bien pour le glisser du cercle que pour celui du diagramme
    • valeur par défaut : aucun des deux ne bloque le défilement
    • touch-action: none uniquement sur le cercle : cela ne fonctionne pas comme prévu
    • preventDefault() dans touchstart : bloque uniquement le défilement pendant le glisser du cercle et autorise le défilement du diagramme
  • preventDefault() est attaché à touchstart, et non à pointerstart
  • Cette approche fonctionne dans la plupart des cas, mais elle ne fonctionnait pas dans le « mobile emulation mode » de Firefox sur desktop, alors qu’elle fonctionnait sur Firefox mobile sous Android

Enregistrer le décalage au début du glisser

  • Quand on saisit le bord d’un objet pour le faire glisser, si le centre de l’objet saute instantanément à la position du pointeur, l’expérience devient mauvaise
  • La solution consiste à enregistrer, au début du glisser, la position relative entre le centre de l’objet et la position du pointeur
  • Ensuite, lors du déplacement, on calcule la position de l’objet en ajoutant les dx et dy enregistrés aux coordonnées actuelles du pointeur
  • Cette méthode remplace state.dragging utilisé comme simple true/false par des informations de position relative comme {dx, dy}
  • Avec la souris, le saut se voit bien quand on saisit depuis le bord, alors qu’en tactile cela peut être moins visible parce que le doigt masque l’objet
  • Quelques détails connexes sont également résumés sur la page little details

Menu contextuel et boutons de souris

  • La façon d’activer le menu contextuel varie selon les plateformes
    • Windows : clic droit down+up, Shift+F10
    • Linux : bouton droit down, Shift+F10
    • Mac : bouton droit down, Ctrl+clic gauche
    • iOS : appui long uniquement sur le texte
    • Android : appui long n’importe où
  • Le problème est que pointerdown est visible, mais que pointerup ne l’est pas toujours
  • Le bouton peut en réalité avoir été relâché alors que le code le considère encore comme enfoncé
  • L’approche consiste à ignorer le bouton droit et à ne définir l’état dragging qu’avec le bouton gauche de la souris
  • Dans la spécification Pointer Events, la propriété button avec button = 0 désigne le bouton principal, ce qui permet d’exclure les boutons du milieu et droit
  • Sur Chrome et Safari pour Mac, Ctrl+clic peut arriver comme un pointerdown de bouton gauche, il faut donc aussi vérifier event.ctrlKey
  • Les options sont les suivantes
    • On peut bloquer le menu via preventDefault() sur contextmenu, mais cela peut entrer en conflit avec le comportement par défaut du clic du milieu ou du glisser
    • Limiter le glisser au bouton gauche permet d’éviter la plupart des différences liées au clic droit
    • Pour éviter aussi le Ctrl+clic sur Mac, il faut également exclure ctrlKey
  • Même si un changement d’état des boutons n’est pas transmis par pointerdown ou pointerup, on peut en vérifier une partie via l’état des boutons dans pointermove

Objets contenant du texte et des images

  • Si un élément déplaçable contient du texte ou des images, un traitement supplémentaire est nécessaire
  • Pendant qu’on fait glisser un cercle, le texte à l’intérieur peut être sélectionné
  • La solution consiste à appliquer le CSS user-select: none
    • Si on l’applique en permanence, le texte interne ne pourra jamais être sélectionné
    • Si on l’applique uniquement pendant le glisser, la sélection de texte reste possible le reste du temps
  • Au début de l’année 2023, Safari ne prenait pas encore en charge user-select sans préfixe, il faut donc aussi définir webkitUserSelect
  • Windows, Linux et Mac prennent en charge le drag and drop inter-applications pour le texte et les images, ce qui entre en conflit avec le glisser des objets
  • Appeler preventDefault() dans dragstart permet de donner la priorité au glisser de l’objet sur le glisser de texte ou d’image sélectionnés
  • Le comportement varie légèrement selon le navigateur et le système d’exploitation

Plusieurs pointeurs et glisser simultané

  • La possibilité de faire glisser plusieurs objets en même temps avec plusieurs doigts ou plusieurs souris est un cas limite
  • En tactile, le code proposé fonctionne déjà pour faire glisser plusieurs objets simultanément
  • Le cas où deux doigts font glisser le même objet nécessite un traitement séparé
  • La solution consiste à enregistrer event.pointerId dans state.dragging ou dans l’état lors de pointerdown, puis à l’ignorer dans pointermove si ce n’est pas le même pointerId
  • Cet exemple d’implémentation n’est pas entièrement appliqué dans le corps de l’article, mais on peut le voir dans canvas dragging test
  • Pour la souris, la spécification Pointer Events précise que les systèmes d’exploitation et agents utilisateurs classiques n’ont généralement pas la notion d’entrées provenant de plusieurs souris
  • Même avec un trackpad et une souris externe, ils sont en général traités comme un seul pointeur de souris, donc il est considéré qu’il n’y a pas de moyen de faire glisser des objets différents avec des souris distinctes

Quand plusieurs boutons de souris sont enfoncés en même temps

  • Quand plusieurs boutons de souris sont pressés simultanément, Pointer Events et Mouse Events se comportent différemment
  • Mouse Events envoie un mousedown à chaque pression de bouton, et un mouseup à chaque relâchement
  • D’après la spécification Pointer Events, pointerdown se produit sur le premier bouton enfoncé et pointerup sur le dernier bouton relâché
  • À cause de cela, pointerdown et pointerup peuvent se produire avec des états de boutons différents
  • Par exemple, si on appuie sur le bouton gauche, puis sur le bouton droit, puis qu’on relâche le bouton gauche, l’objet peut rester en état de glisser
  • La solution consiste à vérifier l’état du bouton gauche dans pointermove avec event.buttons & 1, et à appeler end(event) s’il n’est plus enfoncé
  • La capture du pointeur peut se poursuivre jusqu’à ce que tous les boutons soient relâchés, et continuer tant qu’on ne libère pas explicitement cette capture
  • Cette implémentation ne traite pas complètement ce problème ni tous les cas limites associés

Éléments déplaçables imbriqués

  • S’il y a un autre élément déplaçable à l’intérieur d’un élément déplaçable, les éléments interne et externe traitent tous deux le glisser
  • La solution consiste à appeler event.stopPropagation() dans l’élément déplaçable interne afin d’empêcher l’événement de remonter vers l’élément déplaçable externe
  • Cette interruption de propagation est utilisée à la fois dans pointerdown et pointermove
  • La démo n’apparaît pas dans le corps de l’article, mais l’exemple Vue playground montre un cas où l’élément déplaçable rouge est un enfant de l’élément déplaçable jaune

Le glisser dans Canvas

  • On utilise généralement SVG, mais dans <canvas>, il n’est pas possible de définir des gestionnaires d’événements ou la forme du pointeur directement sur les éléments déplaçables eux-mêmes
  • En 2D Canvas ou en WebGL, il faut attacher les gestionnaires d’événements à <canvas> et déterminer soi-même si les coordonnées se trouvent sur un objet déplaçable
  • Le traitement fonctionne comme suit
    • pointerdown, touchstart, dragstart : si ce n’est pas sur un objet déplaçable, retour immédiat
    • pointermove : définir le curseur selon que le pointeur se trouve ou non sur un objet déplaçable
  • Une démo est disponible sur la page d’exemples

Combiner hover à la souris et drag au toucher

  • Certains diagrammes doivent réagir au hover sans clic avec la souris, et sur les appareils tactiles réagir par drag puisqu’il n’y a pas de hover
  • La recette de base suppose un drag à la fois pour la souris et pour le tactile
  • Pour utiliser ensemble le hover à la souris et le drag au toucher, il faut supprimer la condition if (!state.dragging) dans pointermove
  • Sur la page Responsive Design, le layout change lorsqu’on survole une ligne avec la souris, et il change par drag sur les appareils tactiles
  • Dans le guide Hexagons, de nombreux diagrammes utilisent également ensemble le hover à la souris et le drag au toucher
  • Lors d’un drag tactile d’un élément A vers B, si la capture est conservée, l’événement move est envoyé à A ; pour mettre B en surbrillance, il peut être nécessaire de relâcher la capture

Toggle Paint et extension de l’état de drag

  • Dans Rounded Cell Painter, pointerdown capture la couleur de peinture initiale
  • Ensuite, tous les pointermove jusqu’à pointerup utilisent cette même couleur de peinture
  • Pour cela, state.dragging doit contenir non seulement les x, y initiaux, mais aussi la couleur de peinture initiale
  • Une démo est disponible sur la page d’exemples

Cas limite de lostpointercapture

  • lostpointercapture peut être utilisé pour détecter la perte de la capture du pointeur
  • Les situations dans lesquelles cela se produit et ce qu’il faut faire ne sont pas encore totalement clarifiées
  • Les scénarios évoqués dans les commentaires sont les suivants
    • commencer à faire glisser un cercle avec le bouton gauche
    • se déplacer hors de la zone de drag en maintenant le bouton gauche enfoncé
    • appuyer sur le bouton droit puis relâcher le bouton gauche
    • cliquer n’importe où en dehors de la zone de drag
  • Le standard indique qu’après la libération de la capture du pointeur, l’agent utilisateur doit impérativement déclencher un événement lostpointercapture
  • En pratique, le comportement des navigateurs varie fortement : sur certains systèmes, lostpointercapture se déclenche, tandis que sur d’autres il ne se déclenche pas avant un clic ailleurs

1 commentaires

 
GN⁺ 2023-09-30
Avis sur Hacker News
  • C’est un article sur le glisser-déposer, et j’ai rencontré presque tous les pièges mentionnés par Amit, pour arriver aux mêmes solutions. Excellent article.
    Côté utilisateur, réordonner par glisser-déposer paraît tout à fait naturel, mais dès qu’on entre dans le terrier des éléments de tailles variables, de la création d’emplacements réservés entre les éléments, de la détection des bords et des algorithmes d’ajustement des boîtes, c’est plutôt difficile à implémenter soi-même.

    • Il y a une astuce assez générale que j’aime bien dans ce genre de cas : quand l’utilisateur commence à faire glisser, calculer toutes les cibles de dépôt possibles, ou le layout des cibles actuellement visibles, et enregistrer, pour chaque layout, la position finale de l’objet en cours de glissement.
      À chaque mouvement de souris, il suffit de choisir, parmi ces positions, celle qui est la plus proche de la position actuelle de l’objet glissé, puis de rendre le layout sélectionné. Cela fonctionne agréablement même avec des layouts complexes ou des reflows.
    • J’ai dû résoudre ce problème dans mon application de fiches Card Buddy. [1] C’était un défi amusant, et j’ai fini par trouver une meilleure méthode.
      Au final, quand on saisissait une carte, je calculais le layout comme si cette carte avait été supprimée du tableau, ce qui simplifiait les choses. Il ne restait qu’à pousser les éléments situés à l’endroit survolé par la souris.
      Malgré tout, il y avait énormément de cas particuliers à gérer, surtout quand on commençait à éditer une nouvelle carte qui n’était pas encore « commitée » dans le modèle de données. Il a aussi fallu ajouter une option permettant de pousser les cartes existantes pour créer l’emplacement d’une carte fantôme.
      Il est utile d’accepter qu’il y a beaucoup de cas particuliers à traiter manuellement. Si l’on aborde le problème en pensant qu’il doit exister une solution plus générale et homogène, on finit par tourner en rond dans la conception. Il faudra sans doute que je rassemble un jour les différents cas particuliers dans un article de blog.
      J’ai aussi trouvé une meilleure approche de layout, qui réduit les calculs inutiles et rend le moteur de layout plus flexible et plus agréable pour l’utilisateur. C’est étonnant de voir à quel point le choix de la représentation du modèle de données peut changer la solution.
      [1] https://www.ussherpress.com/cardbuddy/
    • Si l’on ajoute en plus la prise en charge de l’imbrication au réordonnancement par glisser-déposer, cela devient encore plus difficile.
    • Même avis. L’idée est simple, mais l’implémentation peut être brutale. J’ai aussi considéré la sélection par glisser-déposer comme une évidence pendant des années.
  • L’article ne semble pas aborder la gestion de l’annulation. Par exemple, sous Windows, la convention veut qu’appuyer sur Échap annule le glisser-déposer.
    Parfois, on veut aussi annuler le glissement si la souris est relâchée en dehors d’une zone définie. L’annulation offre à l’utilisateur une forme d’annulation plus rapide, ou une échappatoire du type « ah, en fait, je ne voulais pas faire glisser ça ».
    Donc même s’il n’y a pas de fonction d’annulation séparée, il faut enregistrer l’état initial au moment où le glissement commence, puis le restaurer en cas d’annulation.
    Dans le cas d’une annulation de glissement hors zone, il y a aussi une désannulation. Quand le pointeur est en dehors de la zone, on affiche visuellement l’état comme s’il était revenu à l’état initial, puis quand le pointeur rentre de nouveau dans la zone, on reprend le glissement. L’annulation réelle se produit au relâchement de la souris, mais on indique visuellement à l’avance, pendant le glissement, qu’il sera annulé.

    • Sous Windows, quand on fait glisser quelque chose trop loin, l’objet en cours de glissement et le pointeur disparaissent soudainement, puis l’objet est renvoyé à sa position d’origine sans avertissement.
      J’ai du mal à croire que Microsoft considère encore cela comme une bonne conception d’UI. Cela désoriente complètement l’utilisateur, et on peut voir des gens qui ne savent pas ce qui se passe, qui ont peur de relâcher le bouton gauche de la souris, ou qui répètent l’action comme un yo-yo.
      Je suis d’accord pour dire que l’annulation est un gros sujet souvent négligé, et qu’elle n’est pas simple. L’utilisateur moyen ne sait même pas qu’Échap peut l’aider.
    • Je me demande si, sur d’autres plateformes, il existe des touches d’annulation à gérer en plus d’Échap.
  • Voici un détail supplémentaire appris en 20 ans à faire glisser toutes sortes d’objets dans l’interface graphique d’Ardour [0] : il faut traiter tous les événements d’appui et de relâchement de bouton comme des événements de glissement sans mouvement.
    Autrement dit, un appui démarre toujours un glissement, qui se termine au relâchement, et le code qui traite le relâchement gère spécialement le cas où il n’y a pas eu de mouvement.
    [0] https://ardour.org/

    • Cette approche fonctionnerait aussi, mais ce n’est pas ce que j’ai fait, et une fois les bases bien posées, je n’ai pas eu de problème.
      J’ai des gestionnaires mousedown et mouseup classiques, et j’utilise un timeout configurable d’environ 150 ms pour déterminer s’il s’agit d’un clic, du début d’un glissement ou d’une autre action. Si un mouseup se produit, ou si la souris sort de l’écran et qu’un glissement est en cours, j’appelle stopdrag. Pour moi, le glissement n’est qu’un cas particulier parmi d’autres.
    • Je me demande pourquoi faire comme ça.
  • Il y a très longtemps, il existait à Minneapolis un geometry center financé par la NSF. Une conférence de théorie computationnelle des groupes y avait lieu, et quelques personnes venant de domaines voisins, un peu comme des mascottes, avaient été invitées.
    J’ai été invité parce que j’avais écrit un système de géométrie algébrique, et je me suis levé très tôt par -20 °F pour obtenir un poste de travail Silicon Graphics pour la journée, puis j’ai codé un jeu destiné à mieux comprendre les générateurs et relations de groupes.
    Ce jeu utilisait le glisser-déposer. Les discussions qui ont suivi jusqu’à 2 h du matin étaient excellentes. Mon idée était que le glisser-déposer n’a pas besoin de suivre les lois physiques du monde réel, et qu’il devrait plutôt donner l’impression d’un très puissant buvard de LSD. Tout le monde a accroché, et chacun y est allé de ses idées.

  • L’article implémente un glisser-déposer relativement basique, si l’on met de côté les nombreuses exceptions qui se présentent dans les navigateurs web. Je me demande s’il existe des ressources sur le drag avec contraintes, comme l’aimantation à des guides, l’évitement des collisions avec des limites ou d’autres objets, ou encore des cibles de dépôt animées dont la taille ou la position change en réaction à l’opération de glisser-déposer.
    Il y a quelque temps, je voulais créer une UI de minuteur Pomodoro personnalisée qui découpait une horloge circulaire en segments en forme de secteurs de différentes longueurs pour définir des périodes de concentration/repos. Je n’ai pas beaucoup avancé sur l’implémentation du changement d’ordre de ces secteurs par glisser-déposer.

    • Dans cette approche de code, le mouvement du drag met à jour un état. Lors de la définition de l’état, on peut appliquer des contraintes, puis cet état pilote à nouveau l’affichage.
      Je voulais séparer le système de contraintes du système de gestion des événements. Des bibliothèques comme jquery-ui lient les deux, si bien que le système de gestion des événements doit connaître toutes les contraintes possibles. jquery-ui prend en charge les boîtes englobantes, les axes, les grilles carrées et l’aimantation à des éléments DOM. Mais si l’on veut s’aimanter à une grille hexagonale, à une grille à échelle logarithmique ou à une limite circulaire, ce n’est pas pris en charge.
      Dans le code, on voit state.pos = … : c’est là que l’état est défini. Pour les contraintes, je cache pos derrière un setter. Ainsi, le setter peut appliquer les contraintes sans que le code de gestion des événements de drag ait besoin de savoir quelles contraintes sont nécessaires.
      Je devrais mettre la page à jour pour montrer un exemple de contraintes. J’ai complètement oublié de mentionner cet aspect dans la recette de code.
      Un vieil exemple de contraintes se trouve sur https://www.redblobgames.com/articles/curved-paths/making-of..., et un exemple d’évitement de collisions sur https://redblobgames.github.io/circular-obstacle-pathfinding.... En revanche, je n’ai pas encore essayé le changement d’ordre par drag ni les cibles de dépôt animées.
  • Je suis moi aussi reconnaissant envers ce site. Quand nous avons conçu chez Uber un système hexagonal pour l’analyse géographique, nous nous sommes beaucoup appuyés sur https://www.redblobgames.com/grids/hexagons/.

  • C’est un article qui résume bien les pièges et points d’attention lorsqu’on traite de vraies interactions.
    Pour quelque chose de plus “prêt à l’emploi”, j’utilise interactjs depuis pas mal de temps sur plusieurs projets.

  • Merci à tous. J’ai été surpris de voir ça sur HN aujourd’hui, et je peux répondre aux questions.

    • Je me demande si tu as examiné l’attribut HTML intégré draggable="true" et les événements de drag.
      Je me demande aussi en quoi la fonctionnalité intégrée est insuffisante par rapport à ce code.
      https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement...
      https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement...
      https://developer.mozilla.org/en-US/docs/Web/HTML/Global_att...
    • C’était une excellente discussion sur le drag en JavaScript, et les informations étaient vraiment utiles.
      Petite question. Je me demande comment limiter le mouvement de drag à un seul axe avec des événements DOM intégrés comme dragstart. J’ai implémenté une fonctionnalité de drag and drop avec les événements dragstart/dragenter/dragover/drop, mais je n’ai pas trouvé de moyen rapide de limiter le mouvement à l’axe x. Il me semble que l’API de drag and drop de JQuery prenait cela en charge. J’essaie de n’utiliser que les événements/API DOM natifs, donc toute information ou piste serait bienvenue.
    • Autrefois, j’écrivais constamment du code d’UI pour navigateur, mais j’ai fait une pause de quelques années. C’est la première fois que j’entends quelqu’un parler d’utiliser les Pointer Events.
      Étant donné que Safari a ajouté leur prise en charge en 2020, je me demande si tu n’es pas inquiet que certains navigateurs ne les prennent pas en charge. Les utilisateurs de macOS Mojave doivent utiliser Safari 13 ou 14, donc j’espère que Safari 12 n’est effectivement plus utilisé, mais pour une application de production destinée au grand public, ce serait assez gênant qu’une fonction aussi simple que le drag ne marche pas.
      Ajouter des gestionnaires d’événements sur document pendant un drag est une vieille pratique, et les navigateurs ajoutent sans cesse de nouvelles fonctionnalités censées simplifier certains cas d’usage, mais chacune a ses exceptions et ses pièges. L’article dit d’ailleurs ne pas encore couvrir tout cela complètement. Le résultat final conserve aussi une combinaison de Pointer Events et de Touch Events.
      Je me demande si la “simplicité” au sens d’une réduction du code vaut vraiment le coût d’exceptions supplémentaires, d’une prise en charge navigateur plus faible, et de la nécessité pour les développeurs de connaître les détails et différences entre navigateurs des Pointer Events, probablement moins bien compris et documentés que les événements de souris.
  • Il manque quelque chose : l’accessibilité. Comment rendre le glisser-déposer possible au clavier ?

    • Ici, la question semble un peu à côté. C’est un peu comme demander comment un utilisateur au clavier ferait, dans un logiciel de dessin, pour maintenir un clic afin de déplacer le canevas. Il ne le ferait pas comme ça. Faire un panoramique à la souris ou faire glisser un objet n’est qu’une interface ; la tâche, c’est de déplacer le canevas, et il existe plusieurs façons d’y parvenir.
      Dans mon exemple, la réponse côté accessibilité est de fournir en plus des commandes clavier permettant d’effectuer cette action. Si possible, de préférence sans avoir à maintenir une touche enfoncée. Par exemple, on peut entrer en mode panoramique avec un raccourci, puis déplacer le viewport autour du canevas avec les touches fléchées.
      Dans le cas d’objets déplaçables par glisser-déposer, la tâche est de modifier l’ordre. Pour la rendre accessible, il suffit de fournir un autre moyen de réordonner. Par exemple, faire passer le focus d’un objet à l’autre avec TAB, sélectionner l’objet focalisé avec une touche, puis déplacer l’aperçu de glisser-déposer avec les flèches vers la position valide la plus proche dans cette direction.
    • En général, la réponse est plutôt : « on ne fait pas ça comme ça ». L’UI doit proposer une alternative au glisser-déposer pour que les personnes qui ne peuvent pas utiliser de pointeur ou d’écran tactile puissent quand même interagir avec la page.
      Autrement dit, il ne faut pas forcer la prise en charge du clavier à l’intérieur de l’implémentation du drag ; il faut ajouter une fonctionnalité clavier pertinente, séparée de la fonctionnalité de glisser-déposer.
    • Ce serait plutôt un objet déplaçable, et j’ai l’impression que ce serait facile à construire à partir de cette implémentation.
    • Il suffit de sélectionner un élément avec TAB / Espace / Entrée, puis de déplacer l’élément « en cours de glisser-déposer » avec Ctrl + les touches fléchées. Évidemment, chaque pression de touche ne peut le déplacer que d’un bloc.
  • Très bien. Plus tôt cette semaine, j’implémentais le glisser-déposer d’éléments SVG dans une app Vue, et j’ai dû découvrir moi-même presque tout ce que l’auteur explique, à peu près dans le même ordre ; au final, je suis arrivé à un composant presque identique.
    Sauf que je l’ai écrit sous forme d’utilitaire composable appelé useDragging, plutôt que comme composant fonctionnel.

    • Excellent. J’ai essayé avec une directive et un composant avec slots, mais pas encore sous forme de composable. Je n’étais pas sûr de la manière de configurer les gestionnaires d’événements avec cette approche. Je n’ai pas énormément d’expérience avec les composables.