Programação Por Equipe DevBras

Capa do artigo: Reordenar lista no Blazor: arrastar com o mouse e mover pelo teclado

Reordenar lista no Blazor: arrastar com o mouse e mover pelo teclado

Monte uma lista reordenável no Blazor em que arrastar e os botões subir e descer mudam o mesmo estado, anunciam a mudança e desfazem a tela se a gravação falhar.

A fila de atendimento tem cinco chamados e a supervisora arrasta o "Servidor de e-mail fora" para o topo. Funciona. O colega que usa só teclado abre a mesma tela e não consegue mudar nada: a ordem está congelada para ele. A resposta curta é esta: arrastar é um gesto, mover é a operação. Implemente a operação uma vez (tirar o item de uma posição e colocar em outra), chame essa mesma função a partir do arrastar e de dois botões, "Subir" e "Descer", e grave no servidor antes de considerar a mudança feita.

Este guia monta esse componente em Blazor do começo ao fim, com a lista de chamados como exemplo, o anúncio para leitor de tela, o foco que não se perde e a volta da ordem antiga quando o banco recusa.

Por que só arrastar deixa gente de fora

O arrastar e soltar do HTML depende de um ponteiro: o evento dragstart nasce quando alguém segura e move o mouse. Teclado não gera esse evento. Leitor de tela também não, e em tela de toque o suporte varia de navegador para navegador. Quem não usa mouse fica sem nenhum caminho para a mesma tarefa.

O segundo problema é mais silencioso. Muitos exemplos de arrastar no Blazor só reordenam a lista na memória do componente. A tela muda, a supervisora vê o chamado no topo, e o banco continua com a ordem antiga. Quem abrir a fila em outra máquina vê outra prioridade. A ordem que vale é a que está gravada, não a que está desenhada.

O terceiro é o anúncio. Quando o item muda de lugar, quem enxerga percebe o movimento. Quem ouve a tela precisa de uma frase: "Servidor de e-mail fora movido para a posição 1 de 5". Sem ela, o botão foi apertado e nada aconteceu, até onde a pessoa sabe.

Como montar a lista reordenável

  1. Guarde a ordem numa única lista no componente. Arrastar e botões alteram essa lista e nada mais.
  2. Escreva uma função que move um item para um índice e grava. Ela é chamada pelo soltar e pelos botões.
  3. Coloque em cada item os botões "Subir" e "Descer" com o nome do item no rótulo acessível.
  4. Use uma região aria-live para anunciar a posição nova com o nome do item.
  5. Antes de gravar, copie a ordem antiga. Se a gravação falhar, restaure e anuncie a falha.
  6. Depois de mover pelo teclado, devolva o foco ao botão que a pessoa apertou, já na posição nova.

O componente completo

O serviço IFilaService é seu: ele recebe os ids na ordem nova e grava. O componente não sabe se do outro lado há uma API ou o banco direto.

@inject IFilaService Fila

<ol>
  @foreach (var item in itens)
  {
    <li @key="item.Id" draggable="true"
        @ondragstart="() => arrastando = item"
        @ondragover:preventDefault
        @ondrop="() => SoltarAsync(item)">
      <span>@item.Titulo</span>
      <button type="button" disabled="@salvando"
              aria-label="@("Subir " + item.Titulo)"
              @ref="refsSubir[item.Id]"
              @onclick="() => MoverPeloTecladoAsync(item, -1)">Subir</button>
      <button type="button" disabled="@salvando"
              aria-label="@("Descer " + item.Titulo)"
              @ref="refsDescer[item.Id]"
              @onclick="() => MoverPeloTecladoAsync(item, 1)">Descer</button>
    </li>
  }
</ol>
<div aria-live="polite">@anuncio</div>

@code {
    [Parameter] public List<Chamado> Inicial { get; set; } = new();

    private List<Chamado> itens = new();
    private Chamado? arrastando;
    private bool salvando;
    private string anuncio = "";
    private readonly Dictionary<int, ElementReference> refsSubir = new();
    private readonly Dictionary<int, ElementReference> refsDescer = new();
    private (int Id, int Direcao)? focar;

    protected override void OnParametersSet() => itens = Inicial.ToList();

    private async Task SoltarAsync(Chamado alvo)
    {
        if (arrastando is null || arrastando == alvo) return;
        await AplicarAsync(arrastando, itens.IndexOf(alvo));
        arrastando = null;
    }

    private async Task MoverPeloTecladoAsync(Chamado item, int direcao)
    {
        var para = itens.IndexOf(item) + direcao;
        if (para < 0 || para >= itens.Count)
        {
            anuncio = item.Titulo + (para < 0 ? " já é o primeiro." : " já é o último.");
            return;
        }
        await AplicarAsync(item, para);
        focar = (item.Id, direcao);
    }

    private async Task AplicarAsync(Chamado item, int para)
    {
        var anterior = itens.ToList();
        itens.Remove(item);
        itens.Insert(para, item);
        anuncio = item.Titulo + " movido para a posição " + (para + 1) + " de " + itens.Count + ".";
        salvando = true;
        try
        {
            await Fila.SalvarOrdemAsync(itens.Select(i => i.Id).ToList());
        }
        catch (Exception)
        {
            itens = anterior;
            anuncio = "Não foi possível salvar. A ordem anterior foi restaurada.";
        }
        finally
        {
            salvando = false;
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (focar is { } f)
        {
            focar = null;
            var refs = f.Direcao < 0 ? refsSubir : refsDescer;
            if (refs.TryGetValue(f.Id, out var botao)) await botao.FocusAsync();
        }
    }
}

O que cada pedaço resolve

@key="item.Id" faz o Blazor mover o elemento existente em vez de reaproveitar o do vizinho com outro texto. Sem ele, o foco e o estado interno de cada linha podem ficar presos à posição, não ao chamado.

@ondragover:preventDefault sem um método C# associado é intencional. O navegador só aceita soltar se o dragover for cancelado, e esse evento dispara sem parar enquanto o item passa por cima. No Blazor Server, ligar um método a ele mandaria uma mensagem ao servidor a cada disparo. O modificador resolve no próprio navegador.

A função AplicarAsync é o único lugar que muda a ordem. Arrastar e botões chegam nela. Se amanhã você criar um atalho, ele também chama a mesma função, e o comportamento de gravar e desfazer vem de graça.

O foco depois da renderização fica em OnAfterRenderAsync porque o botão só está na posição nova depois que o Blazor redesenhou a lista. Chamar FocusAsync antes disso coloca o foco onde o botão estava. Os botões das pontas não ficam desabilitados de propósito: um botão desabilitado não recebe foco, e a pessoa perderia o lugar ao chegar no topo. Em vez disso, o anúncio diz que o item já é o primeiro.

Gravar a ordem sem brigar com outra pessoa

Na fila de atendimento, duas pessoas podem reordenar ao mesmo tempo. Se cada uma manda a lista inteira, a última gravação vence e apaga a mudança da outra sem aviso. Duas saídas simples: guardar uma versão da fila e recusar a gravação se a versão mudou desde que a tela abriu, ou mandar só a operação ("chamado 42 para a posição 1") e deixar o servidor aplicar sobre a ordem atual. Nos dois casos, a recusa cai no catch, a tela volta e a pessoa recarrega.

No banco, uma coluna posicao inteira por chamado resolve a maioria dos casos. Grave todas as posições numa transação, para nunca existir um instante com dois chamados na posição 1.

Testar sem mouse

Desconecte o mouse ou não toque nele. Com Tab, chegue ao "Descer" do primeiro chamado e aperte Enter duas vezes. O chamado deve estar na terceira posição, o foco ainda no botão dele, e o leitor de tela deve ter lido "movido para a posição 3 de 5". Recarregue a página: a ordem continua. Abra em outro navegador: a mesma ordem. Depois derrube a API e mova de novo: a lista volta e a mensagem de falha aparece. Se você implementa o mesmo padrão em outro framework, o guia de arrastar e soltar no Vue com teclado mostra a versão em JavaScript.

Erros comuns

  • Reordenar só na memória do componente. A tela mostra uma prioridade e o banco guarda outra; a próxima pessoa trabalha na ordem errada.
  • Botões com o texto "Subir" sem o nome do item. O leitor de tela lê cinco botões iguais e a pessoa não sabe qual chamado vai mover.
  • Esquecer o @key. O Blazor reaproveita elementos por posição, e o foco ou o estado de uma linha passam para outro chamado.
  • Ligar um método C# ao dragover no Blazor Server. O evento dispara continuamente e enche o circuito de mensagens enquanto alguém arrasta.
  • Não restaurar a ordem quando a gravação falha. A tela mente até alguém recarregar, e a decisão tomada em cima dela está errada.
  • Remover o contorno de foco para a lista "ficar limpa". Quem usa teclado não sabe em que botão está; veja como manter o anel de foco visível.

Checklist

  • Arrastar e botões chamam a mesma função de mover.
  • Cada botão tem o nome do item no rótulo acessível.
  • Existe uma região aria-live com nome do item e posição nova.
  • A ordem é gravada no servidor e confere depois de recarregar a página.
  • Falha na gravação restaura a ordem anterior e avisa.
  • Depois de mover pelo teclado, o foco continua no botão do mesmo item.
  • Os itens da lista usam @key com o id do registro.
  • Duas pessoas reordenando ao mesmo tempo não apagam a mudança uma da outra em silêncio.

Arrastar continua sendo o atalho mais rápido para quem tem mouse, e não há motivo para tirar. Ele só não pode ser a única porta, como explica o post sobre eventos de mouse que não cobrem o teclado. Abra agora a sua lista reordenável em duas abas, mova um item na primeira só com o teclado e recarregue a segunda: a ordem nova aparece para quem não fez a mudança?

Avalie este artigo

Referência: https://www.c-sharpcorner.com/article/drag-and-drop-in-blazor-without-javascript/