4 pontos por GN⁺ 2023-09-30 | 1 comentários | Compartilhar no WhatsApp
  • Os diagramas interativos do Red Blob Games exigem que, quando o leitor move um objeto diretamente, a tela reaja imediatamente; por isso, o tratamento de entrada foi organizado em torno de Pointer Events, cobrindo mouse e toque juntos
  • O método antigo com mousedown/mouseup/mousemove podia bagunçar o estado de arraste quando o ponteiro saía do objeto ou o botão era solto fora dele, exigindo complementos como handlers globais
  • A entrada por toque tem um modo de entrega de eventos diferente do mouse, e arrastar com um dedo também faz a página rolar; por isso, chamar preventDefault() em touchstart reduz esses conflitos
  • A receita final combina pointerdown, pointermove, pointerup, pointercancel e setPointerCapture(), além de verificar o botão esquerdo e o deslocamento inicial para evitar que o objeto “salte” de repente
  • Exceções específicas de cada plataforma, como objetos com texto/imagem, multitoque, arraste aninhado, <canvas>, combinação de hover com drag e lostpointercapture, precisam de tratamento separado em UIs reais

Objetivo do handler de entrada para arraste

  • Muitas páginas interativas têm objetos arrastáveis que o leitor move diretamente, e o diagrama precisa reagir às mudanças de posição desses objetos
  • O objetivo é criar um handler de eventos de entrada que funcione tanto com mouse quanto com toque
  • O fluxo básico é começar o arraste em pointerdown, atualizar a posição em pointermove e encerrar o arraste em pointerup ou em um evento de cancelamento
  • Na implementação real, isso fica mais complexo do que o modelo simples por causa de botões do mouse, toques com vários dedos, alvo de entrega dos eventos e menu de contexto ao clicar com o botão direito
  • A receita básica combina os seguintes elementos
    • em pointerdown, retornar se event.button !== 0 para tratar apenas o botão principal
    • converter as coordenadas do evento para as coordenadas do diagrama
    • salvar dx e dy, a diferença entre a posição do objeto e a posição do ponteiro no início
    • capturar o ponteiro com el.setPointerCapture(event.pointerId)
    • em pointermove, atualizar state.pos somando o deslocamento salvo
    • em pointerup e pointercancel, definir state.dragging = null
    • chamar preventDefault() em touchstart
  • Essa receita é um handler de eventos de entrada; o processamento real da saída precisa ser ligado a um handler de estado separado
  • O código completo está na examples page, que também inclui exemplos de arraste horizontal de números em vez de mover objetos, pintura em canvas e arraste com restrições
  • O escopo de testes abrange Gecko/Firefox, Blink/Chrome e WebKit/Safari em Mac, Windows, Linux, Android, iPhone e iPad
  • Stylus com hover, dispositivos híbridos de toque+mouse e entrada por voz não foram testados
  • Essa abordagem não é a Drag and Drop API do HTML para colocar um elemento sobre outro; em diagramas, é preciso ler diretamente os eventos de mouse e toque

Limitações de usar apenas eventos de mouse

  • A implementação inicial anexava mousedown, mouseup e mousemove diretamente ao elemento arrastável
  • Na aparência funcionava, mas surgiam dois problemas
    • se o ponteiro fosse movido rápido demais, ele deixava de estar sobre o círculo e os eventos não eram mais recebidos
    • se o botão fosse solto fora do círculo, o estado dragging podia ficar preso
  • A melhoria foi receber mousedown no círculo e, durante o arraste, anexar temporariamente mousemove e mouseup ao document
  • Remover, em mouseup, os handlers de mousemove e mouseup anexados ao document torna o arraste com mouse mais confiável
  • Essa abordagem trata apenas o mouse e não lida com eventos de toque

Diferenças dos eventos de toque

  • A entrada por toque usa touchstart, touchend e touchmove no lugar de mousedown, mouseup e mousemove
  • Eventos de toque são automaticamente capturados após touchstart, de modo que todo touchmove é entregue ao elemento original
  • Por causa dessa característica, não é necessário anexar temporariamente handlers globais ao document, como no caso dos eventos de mouse
  • Quando o navegador precisa cancelar a sequência de toque, ele envia touchcancel
  • A implementação de arraste por toque chama preventDefault() em touchstart e atualiza a posição em touchmove com as coordenadas de event.changedTouches[0]
  • Essa abordagem trata apenas toque e não trata eventos de mouse

Unificando mouse e toque com Pointer Events

  • Para tratar mouse e toque ao mesmo tempo, eram necessários muitos handlers de evento, e essa era a abordagem usada antes de 2021
  • Entre 2011 e 2014, o projeto d3 usava d3-drag, e em projetos sem d3 era criado código separado para mouse+toque
  • Pointer Events unifica e simplifica o tratamento de mouse e toque
    • o MS IE adicionou suporte em 2012
    • o Chrome adicionou suporte em 2017, o Firefox em 2018 e o Safari em 2020
  • O comportamento dos navegadores mudou com o tempo, e em 2017 o Chrome passou alguns eventos para o modo passivo por padrão, causando o problema de a página rolar ao tentar arrastar um objeto
  • Safari e Firefox também aplicaram a mesma mudança em 2018
  • Usar pointer capture permite uma lógica mais simples, sem adicionar e remover handlers globais no document como era necessário com eventos de mouse

Mantendo o rastreamento do ponteiro com Pointer Capture

  • Pointer capture permite continuar rastreando o ponteiro mesmo que ele saia do círculo, do diagrama ou da janela do navegador
  • Com eventos de mouse, para obter o mesmo efeito era preciso anexar handlers ao document, mas em Pointer Events setPointerCapture(event.pointerId) é mais simples
  • Sem captura do ponteiro, o arraste pode parar ao arrastar rápido demais ou ao sair do diagrama e voltar
  • Soltar o botão fora do diagrama ou mudar para outra janela com Alt+Tab também altera o tratamento de encerramento do arraste dependendo de haver captura ou não
  • Em dispositivos de toque, a captura de ponteiro é o comportamento padrão, mas no mouse ela precisa ser configurada explicitamente

Rolagem por toque e preventDefault()

  • Em dispositivos de toque, arrastar com um dedo normalmente faz a página rolar
  • Se esse mesmo arraste com um dedo também mover o círculo, a rolagem e o arraste do objeto acontecem ao mesmo tempo
  • A solução mais simples é aplicar touch-action: none em CSS ao diagrama, mas nesse caso não é possível rolar em nenhuma parte do diagrama
  • O comportamento desejado é bloquear a rolagem apenas ao arrastar o círculo, mas permitir rolagem ao arrastar o próprio diagrama
  • O resultado da comparação é o seguinte
    • touch-action: none no diagrama inteiro: tanto o arraste do círculo quanto o do diagrama interrompem a rolagem
    • valor padrão: nenhum dos dois consegue bloquear a rolagem
    • touch-action: none apenas no círculo: não funciona como esperado
    • preventDefault() em touchstart: bloqueia apenas a rolagem durante o arraste do círculo e permite a rolagem do diagrama
  • preventDefault() é anexado a touchstart, não a pointerstart
  • Essa abordagem funciona na maioria das situações, mas não funcionou no “mobile emulation mode” do Firefox para desktop, embora funcione no Firefox mobile no Android

Armazenar o offset do início do arrasto

  • Ao arrastar um objeto pela borda, se o centro do objeto pular instantaneamente para a posição do ponteiro, a experiência de uso piora
  • A solução é salvar, no início do arrasto, a posição relativa entre o centro do objeto e a posição do ponteiro
  • Depois, ao mover, a posição do objeto é calculada somando dx e dy salvos às coordenadas atuais do ponteiro
  • Esse método muda state.dragging de um simples true/false para informações de posição relativa como {dx, dy}
  • No mouse, a diferença do salto fica bem visível ao segurar pela borda; no toque, isso pode ser menos perceptível porque o dedo cobre a área
  • Um pequeno detalhe relacionado também está resumido na página little details

Menu de contexto e botões do mouse

  • A forma de ativar o menu de contexto varia entre plataformas
    • Windows: clique direito down+up, Shift+F10
    • Linux: botão direito down, Shift+F10
    • Mac: botão direito down, Ctrl+clique esquerdo
    • iOS: long press apenas em texto
    • Android: long press em qualquer lugar
  • O problema é que pointerdown aparece, mas pointerup pode nem sempre ser visível
  • O botão pode na prática já ter sido solto, mas o código ainda pode considerá-lo como pressionado
  • A forma de lidar com isso é ignorar o botão direito e definir o estado dragging apenas com o botão esquerdo do mouse
  • Na propriedade button da especificação Pointer Events, button = 0 significa o botão primário, e isso é usado para excluir os botões do meio e direito
  • No Chrome e Safari no Mac, Ctrl+clique pode chegar como pointerdown do botão esquerdo, então também é preciso verificar event.ctrlKey
  • As opções são as seguintes
    • Em contextmenu, é possível bloquear o menu com preventDefault(), mas isso pode entrar em conflito com o clique do meio padrão ou com o comportamento de arrastar
    • Arrastar apenas com o botão esquerdo evita a maior parte das diferenças de clique direito
    • Para evitar também Ctrl+clique no Mac, ctrlKey também deve ser excluído
  • Mesmo que a mudança de botão não seja transmitida por pointerdown ou pointerup, dá para verificar parte disso pelo estado dos botões em pointermove

Objetos com texto e imagem

  • Se houver texto ou imagem dentro de um elemento arrastável, é necessário tratamento adicional
  • Ao arrastar um círculo, o texto interno pode ser selecionado
  • A solução é aplicar CSS user-select: none
    • Se for aplicado sempre, o texto interno nunca poderá ser selecionado
    • Se for aplicado apenas durante o arrasto, a seleção de texto continua possível no uso normal
  • No início de 2023, o Safari ainda não suportava user-select sem prefixo, então também era necessário definir webkitUserSelect
  • Windows, Linux e Mac suportam drag and drop de texto e imagens entre aplicativos, e isso entra em conflito com o arrasto de objetos
  • Chamar preventDefault() em dragstart permite priorizar o arrasto do objeto em vez do arrasto de texto ou imagem selecionados
  • O comportamento varia um pouco conforme o navegador e o sistema operacional

Vários ponteiros e arrasto simultâneo

  • Poder arrastar vários objetos ao mesmo tempo com vários dedos ou vários mouses é um caso de borda
  • No toque, o código apresentado já funciona para arrastar vários objetos ao mesmo tempo
  • Arrastar o mesmo objeto com dois dedos exige tratamento separado
  • A solução é salvar event.pointerId no pointerdown em state.dragging ou no estado e, em pointermove, ignorar se não for o mesmo pointerId
  • Esse exemplo de implementação não está aplicado por completo no texto principal, mas pode ser visto em canvas dragging test
  • No caso do mouse, a especificação Pointer Events afirma que sistemas operacionais e agentes de usuário comuns normalmente não têm o conceito de várias entradas de mouse
  • Mesmo com trackpad e mouse externo juntos, em geral eles são tratados como um único ponteiro de mouse, então entende-se que não há como arrastar objetos diferentes com mouses diferentes

Ao pressionar vários botões do mouse ao mesmo tempo

  • Quando vários botões do mouse são pressionados ao mesmo tempo, o comportamento de Pointer Events e Mouse Events é diferente
  • Mouse Events envia mousedown a cada pressionamento de botão e mouseup a cada liberação de botão
  • Segundo a especificação Pointer Events, pointerdown ocorre no primeiro botão pressionado e pointerup no último botão liberado
  • Por isso, pointerdown e pointerup podem ocorrer com estados de botão diferentes
  • Por exemplo, se você pressionar o botão esquerdo, depois o direito e então soltar o esquerdo, o objeto pode continuar em estado de arrasto
  • A solução é verificar o estado do botão esquerdo em pointermove com event.buttons & 1 e, se ele não estiver mais pressionado, chamar end(event)
  • A captura do ponteiro pode continuar até que todos os botões sejam soltos e pode prosseguir a menos que a captura seja liberada explicitamente
  • Esta implementação não trata completamente esse problema nem todos os outros casos de borda

Elementos arrastáveis aninhados

  • Se houver outro elemento arrastável dentro de um elemento arrastável, tanto o elemento interno quanto o externo processarão o arrasto
  • A solução é chamar event.stopPropagation() no elemento arrastável interno para impedir que o evento suba até o elemento arrastável externo
  • Usa-se a abordagem de interromper a propagação tanto em pointerdown quanto em pointermove
  • A demonstração não está no texto principal, mas em exemplo no Vue playground há um caso em que o elemento arrastável vermelho é filho do elemento arrastável amarelo

Arrastar no Canvas

  • Normalmente usa-se SVG, mas no <canvas> não é possível definir manipuladores de evento nem o formato do ponteiro do mouse no próprio elemento arrastável
  • Em 2D Canvas ou WebGL, é preciso anexar os manipuladores de evento ao <canvas> e verificar manualmente se as coordenadas estão sobre o objeto arrastável
  • O modo de tratamento é o seguinte
    • pointerdown, touchstart, dragstart: retorne imediatamente se não estiver sobre um objeto arrastável
    • pointermove: defina o cursor de acordo com estar ou não sobre um objeto arrastável
  • A demo está na examples page

Combinação de hover do mouse e drag no toque

  • Alguns diagramas reagem a hover no mouse sem pressionar botão, e em dispositivos touch, como não há hover, precisam reagir com drag
  • A receita básica pressupõe drag tanto para mouse quanto para toque
  • Para usar hover do mouse e drag no toque juntos, remova a condição if (!state.dragging) em pointermove
  • Na Responsive Design page, o layout muda quando se passa o mouse sobre a linha, e no touch o layout muda com drag
  • No Hexagons Guide, muitos diagramas também usam juntos hover do mouse e drag no toque
  • Ao arrastar no touch do item A para B, se a captura for mantida, o evento de move será entregue a A; para destacar B, pode ser necessário liberar a captura

Toggle Paint e expansão do estado de drag

  • No Rounded Cell Painter, pointerdown captura a cor de pintura inicial
  • Depois disso, até pointerup, todo pointermove usa a mesma cor de pintura
  • Para isso, state.dragging precisa incluir não só o x, y inicial, mas também a cor de pintura inicial
  • A demo está na examples page

Caso de borda de lostpointercapture

  • lostpointercapture pode ser usado para detectar se a captura do ponteiro foi perdida
  • Ainda não está totalmente claro em que situações isso acontece e o que deve ser feito
  • Os cenários tratados nos comentários são os seguintes
    • Iniciar o arraste de um círculo com o botão esquerdo
    • Mover para fora da área de arraste mantendo o botão esquerdo pressionado
    • Pressionar o botão direito e depois soltar o esquerdo
    • Clicar em qualquer lugar fora da área de arraste
  • O standard diz que, depois que a captura do ponteiro é liberada, o agente do usuário deve obrigatoriamente disparar o evento lostpointercapture
  • O comportamento real dos navegadores varia bastante; em alguns sistemas lostpointercapture ocorre, mas em outros ele só ocorre depois de clicar em outro lugar

1 comentários

 
GN⁺ 2023-09-30
Comentários do Hacker News
  • É um texto sobre drag, e passei por quase todas as armadilhas que o Amit menciona e cheguei às mesmas soluções. Excelente texto.
    Do ponto de vista do usuário, reordenar arrastando parece muito natural, mas quando você entra em itens de tamanhos variados, criação de placeholders entre itens, detecção de bordas e até a toca do coelho dos algoritmos de encaixe de caixas, acaba sendo algo bem difícil de implementar por conta própria.

    • Há um truque bastante geral de que gosto para esses casos: quando o usuário começa a arrastar, calcule todos os destinos possíveis de drop, ou o layout dos destinos visíveis no momento, e registre em cada layout onde o objeto arrastado terminaria.
      A cada movimento do mouse, basta escolher, entre essas posições, a mais próxima da posição atual do objeto arrastado e renderizar o layout selecionado. Funciona com uma sensação boa mesmo com layouts complexos ou reflow.
    • Tive que resolver esse problema no meu app de cartões de índice, Card Buddy. [1] Foi um desafio divertido e, depois, encontrei um jeito melhor.
      No fim, quando um cartão era pego, eu calculava o layout como se ele tivesse sido removido do quadro, o que simplificava as coisas. Bastava empurrar para fora os itens que estavam na posição sobre a qual o mouse passava.
      Mesmo assim, havia muitas exceções, especialmente ao começar a editar um cartão novo que ainda não tinha sido “commitado” no modelo de dados. Também precisei adicionar a opção de empurrar os cartões existentes para criar espaço para um cartão fantasma.
      Ajuda admitir que há muitas exceções que precisam ser tratadas manualmente. Se você abordar o problema como se fosse haver uma solução mais geral e homogênea, vai acabar andando em círculos no design. Um dia talvez eu organize os vários casos excepcionais em um post no blog.
      Também encontrei uma forma melhor de layout, que reduz cálculos desnecessários e torna o motor de layout mais flexível e amigável ao usuário. É surpreendente como a escolha da representação no modelo de dados faz uma diferença enorme na solução.
      [1] https://www.ussherpress.com/cardbuddy/
    • Se a reordenação por drag também tiver que dar suporte a aninhamento, fica ainda mais difícil.
    • Penso igual. A ideia é simples, mas a implementação pode ser brutal. Também passei anos achando seleção por arraste óbvia demais.
  • O texto não parece tratar de cancelamento. Por exemplo, no Windows há a convenção de cancelar o drag ao pressionar Escape.
    Às vezes também se quer cancelar o drag ao soltar o mouse fora de uma área definida. O cancelamento oferece ao usuário um desfazer mais rápido, ou uma saída do tipo “ah, na verdade eu não queria arrastar isso”.
    Por isso, mesmo que não haja uma função separada de desfazer, é preciso salvar o estado original no início do drag e restaurá-lo se ele for cancelado.
    No caso de cancelamento ao arrastar para fora da área, também existe o desfazer do cancelamento: quando o ponteiro está fora da área, a interface mostra visualmente como se tivesse voltado ao estado original; quando o ponteiro entra de novo na área, o drag é retomado. O cancelamento real só acontece ao soltar o mouse, mas durante o drag a interface já indica visualmente que ele será cancelado.

    • No Windows, quando você arrasta algo para longe demais, há um comportamento em que o objeto arrastado e o ponteiro desaparecem de repente e voltam ricocheteando para a posição original sem aviso.
      É difícil acreditar que a Microsoft ainda considere isso um bom design de UI. Isso deixa o usuário completamente confuso; dá para ver pessoas com medo de soltar o botão esquerdo do mouse porque não sabem o que está acontecendo, ou repetindo o movimento como um ioiô.
      Concordo que o cancelamento é um problema grande e frequentemente ignorado, e que não é simples. Usuários comuns nem sabem que ESC pode ajudar.
    • Fico me perguntando se, em outras plataformas, há alguma tecla de cancelamento além de ESC que precise ser tratada.
  • Há detalhes adicionais que aprendi em 20 anos arrastando todo tipo de objeto na GUI do Ardour [0]: todos os eventos de pressionar e soltar botões precisam ser tratados como eventos de drag sem movimento.
    Ou seja, pressionar sempre inicia um drag e soltar o encerra; o código que trata o soltar é que faz o tratamento especial da condição de ausência de movimento.
    [0] https://ardour.org/

    • Esse método também funcionaria, mas eu não fiz assim e, depois de acertar o básico, não tive problemas.
      Tenho handlers comuns de mousedown e mouseup, e uso um timeout configurável de cerca de 150 ms para decidir se é um clique, o início de um drag ou outra ação. Se ocorrer mouseup, ou se o mouse sair da tela e houver um drag em andamento, faço stopdrag. Para mim, drag é apenas um entre vários casos especiais.
    • Fico curioso sobre o motivo de fazer assim.
  • Muito tempo atrás havia um geometry center em Minneapolis financiado pela NSF. Houve uma conferência de teoria computacional de grupos, e convidaram algumas pessoas de áreas vizinhas, meio como mascotes.
    Fui convidado porque tinha escrito um sistema de geometria algébrica e, no frio de -20°F, acordei muito cedo para garantir uma estação de trabalho Silicon Graphics para usar naquele dia, depois programei um jogo para entender melhor geradores e relações de grupos.
    Esse jogo tinha drag. O resultado foram boas conversas que seguiram até 2 da manhã. Minha ideia era que o drag não precisava obedecer às leis da física do mundo real; eu achava que drag deveria parecer uma dose bem forte de LSD. Todo mundo se empolgou com isso e cada um trouxe suas ideias.

  • O texto implementa um recurso de arrastar relativamente básico, deixando de lado várias exceções que surgem em navegadores web. Fico curioso se há materiais sobre drag com restrições, como fazer snap a guidelines, impedir colisões com limites ou outros objetos, ou alvos de drop animados cujo tamanho ou posição mudam em resposta à operação de arrastar.
    Um tempo atrás, eu queria criar uma UI personalizada de timer Pomodoro que dividisse um relógio circular em fatias de comprimentos diferentes para definir intervalos de foco/descanso. Não avancei muito na implementação da reordenação dessas fatias por arrasto.

    • Nesta abordagem de código, o movimento de arrastar atualiza algum estado. Ao definir o estado, é possível aplicar restrições, e esse estado volta a conduzir a exibição.
      Eu queria separar o sistema de restrições do sistema de tratamento de eventos. Bibliotecas como jquery-ui acabam acoplando os dois, de modo que o sistema de tratamento de eventos precisa conhecer todas as restrições possíveis. O jquery-ui dá suporte a caixa delimitadora, eixos, grade quadrada e snap a elementos DOM. Mas, se você quiser snap para uma grade hexagonal, uma grade em escala logarítmica ou um limite circular, isso não é suportado.
      No código, dá para ver state.pos = …; é ali que o estado é definido. Para as restrições, eu esconderia pos atrás de um setter. Assim, o setter poderia aplicar as restrições sem que o código de tratamento dos eventos de drag precisasse saber quais restrições são necessárias.
      Preciso atualizar a página para mostrar exemplos de restrições. Esqueci completamente de mencionar esse aspecto na receita de código.
      Há um exemplo antigo de restrições em https://www.redblobgames.com/articles/curved-paths/making-of..., e um exemplo de prevenção de colisão em https://redblobgames.github.io/circular-obstacle-pathfinding.... Mas ainda não tentei reordenação por drag nem alvos de drop animados.
  • Também sou grato a este site. Quando projetamos na Uber um sistema hexagonal para análise geográfica, consultei muito https://www.redblobgames.com/grids/hexagons/.

  • É um texto que organiza bem as armadilhas e os pontos de atenção que aparecem ao lidar com interações bem-feitas.
    Para algo mais “pronto para usar”, venho usando interactjs há bastante tempo em vários projetos.

  • Obrigado a todos. Fiquei surpreso ao ver isso no HN hoje, e posso responder perguntas.

    • Fiquei curioso se você chegou a examinar o atributo nativo draggable="true" do HTML e os eventos de drag.
      Também queria saber em que pontos o recurso nativo fica aquém em comparação com 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...
    • Foi uma ótima discussão sobre drag em JavaScript, e as informações foram realmente úteis.
      Tenho uma pergunta rápida. Como faço para restringir o movimento de arrastar a um único eixo usando eventos DOM nativos, como dragstart? Implementei a funcionalidade de arrastar e soltar com os eventos dragstart/dragenter/dragover/drop, mas não encontrei uma forma rápida de restringir o movimento ao eixo x. Lembro que a API de drag and drop do JQuery oferecia suporte a isso. Estou tentando usar apenas eventos/APIs nativos do DOM, então qualquer informação ou indicação sobre isso seria bem-vinda.
    • Antigamente eu sempre escrevia código de UI para navegadores, mas fiquei alguns anos afastado. É a primeira vez que ouço alguém falar em usar eventos de ponteiro.
      Considerando que o Safari só adicionou suporte em 2020, fico curioso se você não se preocupa com navegadores que talvez não tenham suporte a isso. Usuários de macOS Mojave provavelmente usam Safari 13 ou 14, então espero que o Safari 12 já praticamente não seja mais usado; ainda assim, em um app de produção voltado para usuários gerais da web, seria bem ruim se algo simples como arrastar não funcionasse.
      Adicionar manipuladores de eventos ao document durante o drag é uma prática antiga, e os navegadores continuam adicionando novos recursos para simplificar certos casos de uso, mas cada um vem com suas próprias exceções e armadilhas. O texto também diz que ainda não cobriu isso completamente. No resultado final, ainda resta uma combinação de eventos de ponteiro e eventos de toque.
      Fico me perguntando se a “simplicidade” no sentido de reduzir código vale o custo de exceções adicionais, menor suporte em navegadores e a necessidade de o desenvolvedor conhecer detalhes e diferenças entre navegadores em eventos de ponteiro, que talvez sejam menos compreendidos e documentados do que eventos de mouse.
  • Faltou uma coisa: acessibilidade. Como tornar o arrastar possível por meio do teclado?

    • Aqui isso parece uma pergunta um pouco fora do ponto. É parecido com perguntar, em um programa de desenho, como um usuário de teclado faria a ação de clicar e manter pressionado para fazer pan no canvas. Não faz. Fazer pan com o mouse ou arrastar um objeto é apenas uma interface; a tarefa é fazer pan no canvas, e há várias maneiras de realizá-la.
      No meu exemplo, a resposta para acessibilidade é oferecer também uma operação pelo teclado para concluir essa tarefa. Se possível, é melhor que seja algo que não exija manter uma tecla pressionada. Por exemplo, entrar no modo de pan por meio de um atalho e mover a viewport ao redor do canvas com as setas resolveria.
      No caso de objetos arrastáveis, a tarefa é reordenar. Para torná-la acessível, basta oferecer um meio alternativo de alterar a ordem. Por exemplo, circular o foco entre os objetos com TAB, selecionar o objeto em foco com uma tecla e então mover a prévia de arraste, com as setas, para a posição válida mais próxima naquela direção.
    • Em geral, a resposta fica mais perto de “não se faz assim”. A UI deve oferecer alternativas ao arraste para que pessoas que não podem usar um dispositivo apontador ou touch também consigam interagir com a página.
      Ou seja, em vez de enfiar suporte a teclado à força dentro da implementação de drag, é preciso adicionar uma funcionalidade de teclado significativa separada do recurso de arrastar.
    • Isso seria um objeto movível, e acho que daria para criar facilmente com base nesta implementação.
    • Dá para selecionar o item com TAB / Space / Enter e mover o elemento “sendo arrastado” com Ctrl + setas. Naturalmente, cada pressionamento de tecla só pode mover um bloco por vez.
  • Legal. No começo desta semana eu estava implementando arraste de elementos SVG em um app Vue, e tive que descobrir por conta própria quase tudo que o autor explicou, praticamente na mesma ordem; no fim, cheguei a um componente quase igual.
    Só que, em vez de um componente funcional, escrevi como uma utilidade composable chamada useDragging.

    • Bacana. Eu tentei um componente com directive e slot, mas ainda não experimentei um composable, porque não tinha certeza de como configurar os manipuladores de eventos desse jeito. Não tenho muita experiência com composables.