4 puntos por GN⁺ 2023-09-30 | 1 comentarios | Compartir por WhatsApp
  • Los diagramas interactivos de Red Blob Games necesitan que, cuando el lector mueva directamente un objeto, la pantalla reaccione de inmediato, por lo que organizó el manejo de entrada en torno a Pointer Events para cubrir tanto mouse como touch
  • El método anterior con mousedown/mouseup/mousemove podía hacer que el estado de arrastre se desincronizara si el puntero salía del objeto o si se soltaba el botón fuera de él, así que hacía falta complementarlo con manejadores globales y similares
  • La entrada táctil transmite eventos de forma distinta al mouse, y arrastrar con un dedo también provoca el desplazamiento de la página, por lo que se llama a preventDefault() en touchstart para reducir conflictos
  • La receta final combina pointerdown, pointermove, pointerup, pointercancel y setPointerCapture(), y además comprueba el botón izquierdo y el desplazamiento inicial para evitar que el objeto salte de golpe
  • Los casos especiales según la plataforma —como objetos con texto o imágenes, multitouch, arrastres anidados, <canvas>, la combinación de hover y drag, o lostpointercapture— requieren manejo adicional en una UI real

Objetivo del manejador de entrada para arrastre

  • Muchas páginas interactivas tienen objetos arrastrables que el lector puede mover directamente, y el diagrama debe reaccionar a los cambios en la posición del objeto
  • El objetivo es crear un manejador de eventos de entrada que funcione tanto con mouse como con entrada táctil
  • El flujo básico consiste en iniciar el arrastre en pointerdown, actualizar la posición en pointermove y terminar el arrastre en pointerup o en un evento de cancelación
  • En una implementación real, el comportamiento se vuelve más complejo que este modelo simple debido a los botones del mouse, los toques con varios dedos, el destino de entrega de eventos y el menú contextual del clic derecho
  • La receta básica combina los siguientes elementos
    • En pointerdown, si event.button !== 0, retornar para procesar solo el botón principal
    • Convertir las coordenadas del evento a coordenadas del diagrama
    • Guardar dx y dy, que son la diferencia entre la posición del objeto y la posición del puntero al inicio
    • Capturar el puntero con el.setPointerCapture(event.pointerId)
    • En pointermove, actualizar state.pos sumando el desplazamiento guardado
    • En pointerup y pointercancel, establecer state.dragging = null
    • Llamar a preventDefault() en touchstart
  • Esta receta es un manejador de eventos de entrada; el procesamiento real de salida debe conectarse a un manejador de estado aparte
  • El código completo está en la página de ejemplos, e incluye también ejemplos de arrastre horizontal de números, pintura en canvas y arrastre restringido
  • El alcance de las pruebas incluye Mac, Windows, Linux, Android, iPhone y iPad en Gecko/Firefox, Blink/Chrome y WebKit/Safari
  • No se probaron stylus con hover, dispositivos híbridos touch+mouse ni entrada por voz
  • Este método no es la API de Drag and Drop de HTML para soltar elementos sobre otros elementos; en diagramas hay que leer directamente los eventos de mouse y touch

Limitaciones de usar solo eventos de mouse

  • La implementación inicial consistía en adjuntar mousedown, mouseup y mousemove directamente al elemento arrastrable
  • A simple vista funcionaba, pero aparecían dos problemas
    • Si el puntero se movía rápido, dejaba de estar sobre el círculo y ya no recibía eventos
    • Si se soltaba el botón fuera del círculo, podía quedarse atrapado en el estado dragging
  • La mejora fue recibir mousedown en el círculo, y durante el arrastre adjuntar temporalmente mousemove y mouseup a document
  • Si en mouseup se eliminan los manejadores mousemove y mouseup añadidos a document, el arrastre con mouse funciona de forma más estable
  • Este método solo maneja mouse y no trata eventos táctiles

Diferencias de los eventos táctiles

  • La entrada táctil usa touchstart, touchend y touchmove en lugar de mousedown, mouseup y mousemove
  • Después de touchstart, los eventos táctiles se capturan automáticamente, y todos los touchmove se entregan al elemento original
  • Por esta característica, no hace falta adjuntar temporalmente manejadores globales a document como sí ocurría con los eventos de mouse
  • Cuando el navegador necesita cancelar una secuencia táctil, envía touchcancel
  • La implementación de arrastre táctil llama a preventDefault() en touchstart y actualiza la posición en touchmove usando las coordenadas de event.changedTouches[0]
  • Este método solo maneja touch y no trata eventos de mouse

Unificar mouse y touch con Pointer Events

  • Para manejar tanto mouse como touch antes hacían falta muchos manejadores de eventos, y así se trabajó hasta antes de 2021
  • Entre 2011 y 2014, en el proyecto d3 se usó d3-drag, y en proyectos que no usaban d3 se creaba código aparte para mouse+touch
  • Pointer Events unifica y simplifica el manejo de mouse y touch
    • MS IE agregó soporte en 2012
    • Chrome agregó soporte en 2017, Firefox en 2018 y Safari en 2020
  • El comportamiento de los navegadores fue cambiando con el tiempo, y en 2017 Chrome puso algunos eventos en modo passive por defecto, lo que hacía que la página se desplazara al intentar arrastrar un objeto
  • Safari y Firefox también aplicaron el mismo cambio en 2018
  • Usar pointer capture permite resolverlo con lógica más simple, sin tener que agregar y quitar manejadores globales en document como ocurría con los eventos de mouse

Mantener el seguimiento del puntero con Pointer Capture

  • Pointer capture permite seguir rastreando el puntero aunque salga del círculo, del diagrama o de la ventana del navegador
  • Con eventos de mouse, para lograr el mismo efecto había que adjuntar manejadores a document, pero con Pointer Events setPointerCapture(event.pointerId) es más simple
  • Sin captura del puntero, el arrastre puede detenerse si se arrastra rápido o si el puntero sale del diagrama y luego vuelve
  • Si se suelta el botón fuera del diagrama o se cambia a otra ventana con Alt+Tab, el manejo del fin del arrastre también cambia según haya o no captura
  • En dispositivos táctiles, la captura del puntero es el comportamiento predeterminado, pero con mouse hay que establecerla explícitamente

Desplazamiento táctil y preventDefault()

  • En dispositivos táctiles, arrastrar con un dedo provoca por defecto el desplazamiento de la página
  • Si ese mismo arrastre con un dedo también mueve el círculo, entonces el scroll y el arrastre del objeto ocurren al mismo tiempo
  • La solución más simple es aplicar touch-action: none al diagrama con CSS, pero en ese caso ya no se puede hacer scroll en ninguna parte del diagrama
  • El comportamiento deseado es bloquear el scroll solo al arrastrar el círculo, y permitirlo al arrastrar el propio diagrama
  • Los resultados comparativos fueron los siguientes
    • touch-action: none en todo el diagrama: tanto el arrastre del círculo como el del diagrama detienen el scroll
    • Valor predeterminado: ninguno de los dos impide el scroll
    • touch-action: none solo en el círculo: no funciona como se esperaba
    • preventDefault() en touchstart: bloquea solo el scroll durante el arrastre del círculo y permite el scroll del diagrama
  • preventDefault() se aplica en touchstart, no en pointerstart
  • Este método funciona en la mayoría de los casos, pero no funcionó en el “mobile emulation mode” de Firefox de escritorio, mientras que sí funcionó en Firefox móvil sobre Android

Guardar el desplazamiento inicial del arrastre

  • Si al arrastrar sujetando el borde del objeto, el centro del objeto salta instantáneamente a la posición del puntero, la experiencia de uso empeora
  • La solución es guardar al inicio del arrastre la posición relativa entre el centro del objeto y la posición del puntero
  • Luego, al moverlo, se calcula la posición del objeto sumando dx y dy guardados a las coordenadas actuales del puntero
  • Este enfoque cambia state.dragging de un simple true/false a información de posición relativa como {dx, dy}
  • Con mouse, la diferencia del salto se nota bien al sujetar desde el borde; con touch, puede notarse menos porque el dedo lo tapa
  • Este tipo de pequeños detalles también están resumidos en la página little details

Menú contextual y botones del mouse

  • La forma de activar el menú contextual varía según la plataforma
    • Windows: clic derecho down+up, Shift+F10
    • Linux: botón derecho down, Shift+F10
    • Mac: botón derecho down, Ctrl+clic izquierdo
    • iOS: long press solo sobre texto
    • Android: long press en cualquier lugar
  • El problema es que pointerdown sí se ve, pero pointerup puede no verse siempre
  • El botón en realidad puede haberse soltado, pero el código todavía puede considerarlo como presionado
  • La forma de manejarlo es ignorar el botón derecho y establecer el estado dragging solo con el botón izquierdo del mouse
  • En la propiedad button de la especificación Pointer Events, button = 0 significa el botón primario, y con eso se excluyen los botones central y derecho
  • En Chrome y Safari de Mac, Ctrl+clic puede entrar como un pointerdown de botón izquierdo, así que también hay que revisar event.ctrlKey
  • Las opciones son las siguientes
    • En contextmenu se puede bloquear el menú con preventDefault(), pero puede entrar en conflicto con el comportamiento predeterminado de clic central o arrastre
    • Si el arrastre se limita al botón izquierdo, se pueden evitar la mayoría de las diferencias del clic derecho
    • Si también se quiere evitar el Ctrl+clic de Mac, hay que excluir ctrlKey
  • Aunque los cambios de botón no se transmitan por pointerdown o pointerup, se puede verificar una parte con el estado de botones de pointermove

Objetos que incluyen texto e imágenes

  • Si dentro de un elemento arrastrable hay texto o imágenes, se necesita procesamiento adicional
  • Mientras se arrastra el círculo, el texto interno puede quedar seleccionado
  • La solución es aplicar CSS user-select: none
    • Si se aplica siempre, el texto interno nunca podrá seleccionarse
    • Si se aplica solo durante el arrastre, normalmente sí será posible seleccionar el texto
  • A inicios de 2023, Safari todavía no soportaba user-select sin prefijo, por lo que también hay que configurar webkitUserSelect
  • Windows, Linux y Mac soportan drag and drop entre aplicaciones para texto e imágenes, y eso entra en conflicto con el arrastre de objetos
  • Si se llama a preventDefault() en dragstart, se puede dar prioridad al arrastre del objeto por encima del arrastre de texto o imágenes seleccionadas
  • El comportamiento varía un poco según el navegador y el sistema operativo

Múltiples punteros y arrastre simultáneo

  • Poder arrastrar varios objetos al mismo tiempo con varios dedos o varios mouse es un caso límite
  • En touch, el código presentado ya funciona para arrastrar varios objetos al mismo tiempo
  • Si se arrastra el mismo objeto con dos dedos, hace falta un manejo adicional
  • La solución es guardar event.pointerId en state.dragging o en el estado durante pointerdown, e ignorarlo en pointermove si no es el mismo pointerId
  • Este ejemplo de implementación no está aplicado por completo en el texto principal, pero se puede revisar en canvas dragging test
  • En el caso del mouse, la especificación Pointer Events indica que los sistemas operativos y agentes de usuario comunes normalmente no tienen el concepto de múltiples entradas de mouse
  • Aunque haya un trackpad y un mouse externo al mismo tiempo, por lo general se tratan como un solo puntero de mouse, así que se considera que no hay forma de arrastrar distintos objetos con mouse diferentes

Al presionar varios botones del mouse a la vez

  • Si se presionan varios botones del mouse al mismo tiempo, el comportamiento de Pointer Events y Mouse Events cambia
  • Mouse Events envía mousedown por cada presión de botón y mouseup por cada liberación de botón
  • Según la especificación Pointer Events, pointerdown ocurre con el primer botón presionado y pointerup con el último botón liberado
  • Por eso, pointerdown y pointerup pueden ocurrir con estados de botones diferentes
  • Por ejemplo, si se presiona el botón izquierdo, luego el derecho, y después se suelta el izquierdo, el objeto puede quedar en estado de arrastre
  • La solución es comprobar en pointermove el estado del botón izquierdo con event.buttons & 1, y si ya no está presionado, llamar a end(event)
  • La captura del puntero puede continuar hasta que se suelten todos los botones, y puede seguir activa a menos que se libere explícitamente la captura
  • Esta implementación no resuelve por completo este problema ni todos los casos límite

Elementos de arrastre anidados

  • Si dentro de un elemento arrastrable hay otro elemento arrastrable, tanto el elemento interno como el externo procesarán el arrastre
  • La solución es llamar a event.stopPropagation() en el elemento arrastrable interno para evitar que el evento suba al elemento arrastrable externo
  • Se usa el enfoque de detener la propagación tanto en pointerdown como en pointermove
  • La demo no está en el texto principal, pero en el ejemplo de Vue playground se puede ver el caso donde el elemento arrastrable rojo es hijo del elemento arrastrable amarillo

Arrastre en Canvas

  • Normalmente se usa SVG, pero en <canvas> no se pueden configurar manejadores de eventos ni la forma del puntero del mouse en los propios elementos arrastrables
  • En 2D Canvas o WebGL, hay que adjuntar los manejadores de eventos a <canvas> y determinar manualmente si las coordenadas están sobre un objeto arrastrable
  • La forma de procesarlo es la siguiente
    • pointerdown, touchstart, dragstart: si no está sobre un objeto arrastrable, regresar de inmediato
    • pointermove: configurar el cursor según si está sobre un objeto arrastrable
  • Hay una demo en la examples page

Combinación de hover con mouse y drag con toque

  • Algunos diagramas reaccionan con hover cuando se usa mouse sin presionar botones, y en dispositivos táctiles, como no existe hover, deben reaccionar con drag
  • La receta básica asume drag tanto para mouse como para toque
  • Para usar juntos el hover con mouse y el drag con toque, se elimina la condición if (!state.dragging) en pointermove
  • En la Responsive Design page, al hacer hover sobre una fila con el mouse cambia el diseño, y en toque el diseño cambia con drag
  • En la Hexagons Guide, muchos diagramas también usan en conjunto hover con mouse y drag con toque
  • Si al arrastrar en toque desde el elemento A hacia B se mantiene la captura, el evento move se envía a A, así que para resaltar B puede ser necesario liberar la captura

Extensión de Toggle Paint y del estado de drag

  • En Rounded Cell Painter, pointerdown captura el color inicial de pintura
  • Después, todos los pointermove hasta pointerup usan ese mismo color de pintura
  • Para esto, state.dragging debe incluir no solo las x, y iniciales, sino también el color inicial de pintura
  • Hay una demo en la examples page

Caso límite de lostpointercapture

  • lostpointercapture puede usarse para detectar si se perdió la captura del puntero
  • Todavía no está del todo claro en qué situaciones ocurre y qué se debe hacer
  • Los escenarios tratados en los comentarios son los siguientes
    • Iniciar el arrastre de un círculo con el botón izquierdo
    • Moverse fuera del área de arrastre mientras se mantiene presionado el botón izquierdo
    • Presionar el botón derecho y luego soltar el izquierdo
    • Hacer clic en cualquier lugar fuera del área de arrastre
  • El standard dice que, después de que se libera la captura del puntero, el agente de usuario debe disparar obligatoriamente un evento lostpointercapture
  • El comportamiento real de los navegadores varía bastante: en algunos sistemas sí ocurre lostpointercapture, pero en otros no ocurre hasta que se hace clic en otro lugar

1 comentarios

 
GN⁺ 2023-09-30
Opiniones de Hacker News
  • Es un artículo sobre el drag, y me topé con casi todas las trampas que menciona Amit y llegué a las mismas soluciones. Excelente artículo.
    Desde el punto de vista del usuario, reordenar arrastrando parece algo muy natural, pero cuando uno se mete con elementos de distintos tamaños, crear placeholders entre elementos, detectar bordes y la madriguera de conejo de los algoritmos de ajuste de cajas, implementarlo por cuenta propia entra en la categoría de bastante difícil.

    • Hay un truco bastante general que me gusta para estos casos: cuando el usuario empieza a arrastrar, calcular todos los posibles destinos de drop, o el layout de los destinos visibles en ese momento, y registrar en qué posición terminaría el objeto arrastrado en cada layout.
      Cada vez que se mueve el mouse, se elige de esas posiciones la más cercana a la posición actual del objeto arrastrado y se renderiza el layout seleccionado. Funciona con buena sensación incluso con layouts complejos o reflows.
    • Tuve que resolver este problema en mi app de fichas Card Buddy. [1] Fue un reto divertido, y más tarde encontré una forma mejor.
      Al final, cuando levantaba una tarjeta, calculaba el layout como si esa tarjeta se hubiera eliminado del tablero, y eso lo volvía fácil. Solo había que empujar lo que estuviera en la posición sobre la que estaba el mouse.
      Aun así hubo muchísimos casos especiales, y fue particularmente complicado cuando se empezaba a editar una tarjeta nueva que todavía no había sido “confirmada” en el modelo de datos. También tuve que agregar una opción para empujar las tarjetas existentes y crear espacio para una tarjeta fantasma.
      Ayuda aceptar que hay muchos casos especiales que hay que manejar manualmente. Si lo encaras como si fuera a existir una solución más general y homogénea, te la pasás dando vueltas en el diseño. Algún día debería escribir un post de blog recopilando los distintos casos especiales.
      También encontré un mejor enfoque de layout, que reduce cálculos innecesarios y hace que el motor de layout sea más flexible y más amigable para el usuario. Es sorprendente la enorme diferencia que hace la elección de cómo representar el modelo de datos en la solución.
      [1] https://www.ussherpress.com/cardbuddy/
    • Si además ese reordenamiento por drag tiene que soportar anidamiento, se vuelve todavía más difícil.
    • Pienso lo mismo. La idea es simple, pero la implementación puede ser brutal. También di por sentada durante años la selección por arrastre.
  • El artículo no parece tratar el manejo de cancelación. Por ejemplo, en Windows existe la convención de cancelar el drag al presionar Escape.
    A veces también quieres cancelar el drag si se suelta el mouse fuera de un área definida. La cancelación le da al usuario una forma más rápida de deshacer, o una salida del tipo “ah, en realidad no quería arrastrar eso”.
    Por eso, aunque no haya una función separada de deshacer, hay que guardar el estado original al inicio del drag y restaurarlo si se cancela.
    En el caso de cancelar al arrastrar fuera del área, también existe la descancelación. Cuando el puntero está fuera del área, visualmente se muestra como si hubiera vuelto al estado original, y cuando el puntero vuelve a entrar al área, se reanuda el drag. La cancelación real ocurre al soltar el mouse, pero durante el drag se le adelanta visualmente al usuario que se cancelará.

    • En Windows existe un comportamiento en el que, si arrastras algo demasiado lejos, el objeto que se está arrastrando y el puntero desaparecen de repente y vuelven de un salto a su posición original sin advertencia.
      No puedo creer que Microsoft todavía piense que eso es buen diseño de UI. Confunde por completo al usuario; incluso se ve gente que, sin entender qué pasa, tiene miedo de soltar el botón izquierdo del mouse o repite la acción como un yoyó.
      Estoy de acuerdo en que la cancelación es un gran problema que suele pasarse por alto y que no es simple. El usuario promedio ni siquiera sabe que ESC puede ayudar.
    • Me pregunto si en otras plataformas hay alguna tecla de cancelación que haya que manejar aparte de ESC.
  • Tras 20 años arrastrando todo tipo de objetos en la GUI de Ardour [0], aprendí un detalle adicional: todos los eventos de presionar y soltar botones deben tratarse como eventos de drag sin movimiento.
    Es decir, presionar siempre inicia un drag y soltarlo lo termina; el código que maneja el soltar trata como caso especial la condición de que no hubo movimiento.
    [0] https://ardour.org/

    • Ese enfoque también funcionaría, pero yo no lo hice así, y una vez que tuve bien cubierto lo básico no tuve problemas.
      Tengo manejadores normales de mousedown y mouseup, y uso un timeout configurable de unos 150 ms para decidir si se trata de un clic, del inicio de un drag u otra acción. Si ocurre mouseup, o si el mouse sale de la pantalla y hay un drag en curso, hago stopdrag. Para mí, el drag es solo uno de varios casos especiales.
    • Me da curiosidad saber por qué lo haces así.
  • Hace muchísimo tiempo había en Minneapolis un geometry center financiado por la NSF. Se celebró una conferencia de teoría computacional de grupos y habían invitado a algunas personas de áreas cercanas, una especie de mascotas.
    Me invitaron porque había escrito un sistema de geometría algebraica, y me levanté muy temprano con un frío de -20 °F para conseguir una estación de trabajo Silicon Graphics para usar ese día y programé un juego para entender mejor los generadores y relaciones de grupos.
    Ese juego incluía drag. El resultado fueron buenas conversaciones que siguieron hasta las 2 de la mañana. Mi idea era que el drag no tenía por qué seguir las leyes físicas del mundo real; pensaba que el drag debía sentirse como una dosis muy potente de LSD. Todos se engancharon con eso y aportaron sus propias ideas.

  • El artículo implementa un arrastre relativamente básico, dejando de lado varias excepciones que aparecen en los navegadores web. Me pregunto si hay material sobre arrastre con restricciones como hacer snap a guías, evitar colisiones con límites u otros objetos, o destinos de soltado animados que cambian de tamaño o posición en respuesta a la operación de arrastre.
    Hace tiempo quería crear una UI de temporizador Pomodoro personalizado que dividiera un reloj circular en segmentos en forma de cuña de distintas longitudes para definir intervalos de concentración/descanso. No avancé mucho con la implementación del reordenamiento por arrastre de esos segmentos.

    • En este enfoque de código, el movimiento de arrastre actualiza algún estado. Al establecer el estado, se pueden aplicar restricciones, y ese estado vuelve a impulsar la visualización.
      Quería separar el sistema de restricciones del sistema de manejo de eventos. Bibliotecas como jquery-ui terminan acoplando ambos, de modo que el sistema de manejo de eventos tiene que conocer todas las restricciones posibles. jquery-ui admite caja delimitadora, ejes, cuadrícula cuadrada y snap a elementos del DOM. Pero si quieres hacer snap a una cuadrícula hexagonal, una cuadrícula de escala logarítmica o un límite circular, no está soportado.
      En el código se puede ver state.pos = …; ahí es donde se establece el estado. Para las restricciones, ocultaría pos detrás de un setter. Así, aunque el código de manejo de eventos de arrastre no sepa qué restricciones hacen falta, el setter puede aplicarlas.
      Debería actualizar la página para mostrar ejemplos de restricciones. Olvidé por completo mencionar este aspecto en la receta de código.
      Un ejemplo antiguo de restricciones está en https://www.redblobgames.com/articles/curved-paths/making-of... y un ejemplo de prevención de colisiones está en https://redblobgames.github.io/circular-obstacle-pathfinding.... Pero todavía no he probado el reordenamiento por arrastre ni los destinos de soltado animados.
  • Yo también le agradezco a este sitio. Cuando diseñábamos en Uber un sistema hexagonal para análisis geográfico, consulté mucho https://www.redblobgames.com/grids/hexagons/.

  • Es un artículo que resume bien las trampas y precauciones que aparecen al tratar interacciones bien hechas.
    Para algo más “listo para usar”, he usado interactjs durante bastante tiempo en varios proyectos.

  • Gracias a todos. Me sorprendió que llegara hoy a HN, y puedo responder preguntas.

    • Me pregunto si revisaste el atributo integrado de HTML draggable="true" y los eventos de arrastre.
      También me intriga qué le falta a la funcionalidad integrada en comparación con este código.
      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...
    • Fue una excelente discusión sobre el arrastre en JavaScript y la información fue realmente útil.
      Tengo una pregunta rápida. Me pregunto cómo se puede restringir el movimiento de arrastre a un solo eje usando eventos DOM integrados como dragstart. Implementé funcionalidad de arrastrar y soltar con los eventos dragstart/dragenter/dragover/drop, pero no encontré una forma rápida de restringir el movimiento al eje x. Recuerdo que la API de arrastrar y soltar de JQuery sí lo soportaba. Estoy intentando usar solo eventos/API nativos del DOM, así que agradecería cualquier información o referencia al respecto.
    • Antes escribía código de UI para navegadores todo el tiempo, pero me tomé algunos años de pausa. Es la primera vez que oigo que alguien use eventos de puntero.
      Considerando que Safari agregó soporte en 2020, me pregunto si no te preocupa que algunos navegadores no los soporten. Los usuarios de macOS Mojave probablemente usen Safari 13 o 14, así que espero que Safari 12 ya no se use en la práctica, pero en una app de producción dirigida a usuarios web generales sería bastante malo que no funcionara algo tan simple como arrastrar.
      Agregar manejadores de eventos a document durante el arrastre es una práctica antigua, y los navegadores siguen agregando funciones nuevas para simplificar ciertos casos de uso, pero cada una trae sus propias excepciones y trampas. El artículo también dice que todavía no cubre todo por completo. En el resultado final queda una combinación de eventos de puntero y eventos táctiles.
      Me pregunto si la “simplicidad” en el sentido de reducir código vale el costo de asumir excepciones adicionales, menor soporte de navegadores y la necesidad de que los desarrolladores conozcan detalles y diferencias entre navegadores de los eventos de puntero, que probablemente se entienden y documentan menos que los eventos de mouse.
  • Falta algo: accesibilidad. ¿Cómo habría que hacer posible arrastrar con el teclado?

    • Aquí parece una pregunta un poco desviada. Es parecido a preguntar cómo haría un usuario de teclado, en un programa de dibujo, para mantener presionado un clic y desplazar el lienzo. No se hace así. Desplazar con el mouse o arrastrar un objeto es solo una interfaz; la tarea es desplazar el lienzo, y hay varias formas de lograrlo.
      En mi ejemplo, la respuesta de accesibilidad es ofrecer además controles de teclado para completar esa tarea. Si es posible, mejor que no requieran mantener una tecla presionada. Por ejemplo, se puede entrar en modo de desplazamiento con un atajo y mover el viewport alrededor del lienzo con las flechas.
      En el caso de objetos arrastrables, la tarea es reordenar. Para hacerlo accesible, basta con ofrecer un medio alternativo para cambiar el orden. Por ejemplo, recorrer el foco entre los objetos con TAB, seleccionar el objeto enfocado con una tecla y luego mover la vista previa de arrastre con las flechas a la posición válida más cercana en esa dirección.
    • En general, la respuesta se acerca a “no se hace así”. La UI debe ofrecer alternativas al arrastre para que también puedan interactuar con la página las personas que no pueden usar un dispositivo apuntador o táctil.
      Es decir, no hay que forzar el soporte de teclado dentro de la implementación de arrastre, sino agregar una funcionalidad de teclado significativa por separado de la función de arrastre.
    • Eso sería un objeto movible, y creo que se podría hacer fácilmente a partir de esta implementación.
    • Se selecciona un elemento con TAB / Space / Enter, y con Ctrl + flechas se mueve el elemento “que se está arrastrando”. Naturalmente, cada pulsación de tecla solo puede moverlo un bloque.
  • Muy bueno. A principios de esta semana estaba implementando arrastre de elementos SVG en una app Vue, y tuve que descubrir por mi cuenta casi todo lo que describe el autor, prácticamente en el mismo orden; al final terminé con un componente casi igual.
    Aunque yo lo escribí como una utilidad composable llamada useDragging, en vez de como un componente funcional.

    • Genial. Probé con una directiva y con un componente con slots, pero todavía no probé un composable, porque no estaba seguro de cómo configurar los manejadores de eventos de esa forma. No tengo mucha experiencia con composables.