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
- Guarde a ordem numa única lista no componente. Arrastar e botões alteram essa lista e nada mais.
- Escreva uma função que move um item para um índice e grava. Ela é chamada pelo soltar e pelos botões.
- Coloque em cada item os botões "Subir" e "Descer" com o nome do item no rótulo acessível.
- Use uma região
aria-livepara anunciar a posição nova com o nome do item. - Antes de gravar, copie a ordem antiga. Se a gravação falhar, restaure e anuncie a falha.
- 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
dragoverno 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-livecom 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
@keycom 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?
Referência: https://www.c-sharpcorner.com/article/drag-and-drop-in-blazor-without-javascript/