Implémenter des objets déplaçables par glisser-déposer
(redblobgames.com)- 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/mousemovepouvait 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()danstouchstartpour réduire les conflits - La recette finale combine
pointerdown,pointermove,pointerup,pointercanceletsetPointerCapture(), 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 encorelostpointercapture, 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 danspointermove, puis terminer le glisser danspointerupou 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 sievent.button !== 0afin de ne traiter que le bouton principal - convertir les coordonnées de l’événement en coordonnées du diagramme
- enregistrer au départ
dxetdy, 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 à jourstate.posen ajoutant le décalage enregistré - dans
pointerupetpointercancel, fairestate.dragging = null - appeler
preventDefault()danstouchstart
- dans
- 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,mouseupetmousemoveà 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
draggingpeut rester bloqué
- Une amélioration consiste à recevoir
mousedownsur le cercle, puis à attacher temporairementmousemoveetmouseupaudocumentpendant le glisser - En supprimant dans
mouseuples gestionnairesmousemoveetmouseupattachés audocument, 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,touchendettouchmoveà la place demousedown,mouseupetmousemove - Les événements tactiles sont automatiquement capturés après
touchstart, de sorte que tous lestouchmovesont 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
documentcomme 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()danstouchstart, puis met à jour la position danstouchmoveà partir des coordonnées deevent.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
documentcomme 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
documentpour 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: noneau 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: nonesur 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: noneuniquement sur le cercle : cela ne fonctionne pas comme prévupreventDefault()danstouchstart: 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
dxetdyenregistrés aux coordonnées actuelles du pointeur - Cette méthode remplace
state.draggingutilisé comme simpletrue/falsepar 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
pointerdownest visible, mais quepointerupne 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
draggingqu’avec le bouton gauche de la souris - Dans la spécification Pointer Events, la propriété
buttonavecbutton = 0dé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
pointerdownde bouton gauche, il faut donc aussi vérifierevent.ctrlKey - Les options sont les suivantes
- On peut bloquer le menu via
preventDefault()surcontextmenu, 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
- On peut bloquer le menu via
- Même si un changement d’état des boutons n’est pas transmis par
pointerdownoupointerup, on peut en vérifier une partie via l’état des boutons danspointermove
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-selectsans préfixe, il faut donc aussi définirwebkitUserSelect - 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()dansdragstartpermet 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.pointerIddansstate.draggingou dans l’état lors depointerdown, puis à l’ignorer danspointermovesi ce n’est pas le mêmepointerId - 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 unmouseupà chaque relâchement - D’après la spécification Pointer Events,
pointerdownse produit sur le premier bouton enfoncé etpointerupsur le dernier bouton relâché - À cause de cela,
pointerdownetpointeruppeuvent 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
pointermoveavecevent.buttons & 1, et à appelerend(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
pointerdownetpointermove - 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édiatpointermove: 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)danspointermove - 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
moveest 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,
pointerdowncapture la couleur de peinture initiale - Ensuite, tous les
pointermovejusqu’àpointeruputilisent cette même couleur de peinture - Pour cela,
state.draggingdoit contenir non seulement lesx,yinitiaux, mais aussi la couleur de peinture initiale - Une démo est disponible sur la page d’exemples
Cas limite de lostpointercapture
lostpointercapturepeut ê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,
lostpointercapturese déclenche, tandis que sur d’autres il ne se déclenche pas avant un clic ailleurs
1 commentaires
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.
À 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.
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/
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é.
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.
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/
J’ai des gestionnaires
mousedownetmouseupclassiques, 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 unmouseupse produit, ou si la souris sort de l’écran et qu’un glissement est en cours, j’appellestopdrag. Pour moi, le glissement n’est qu’un cas particulier parmi d’autres.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.
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 cacheposderriè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.
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...
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énementsdragstart/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.É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
documentpendant 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 ?
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.
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.
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.