Foco no campo com erro: o formulário Blazor que leva o cursor aonde precisa
Coloque o foco no primeiro campo ao abrir o formulário, leve-o ao primeiro campo inválido depois do envio e saiba quando não mexer no foco, com exemplo em Blazor.
O cadastro de clientes tem doze campos. A atendente preenche, aperta Enter, e nada parece acontecer. A mensagem "CPF inválido" apareceu em vermelho, lá no meio da página, fora da tela. O cursor continua no botão Salvar. Quem usa leitor de tela nem sabe que houve erro. A resposta curta: depois de um envio com erro, mova o foco para o primeiro campo inválido; ao abrir uma tela cuja tarefa principal é o formulário, coloque o foco no primeiro campo; e fora disso, não mexa no foco sem que a pessoa tenha pedido.
O exemplo é esse cadastro, num app Blazor com EditForm e validação por DataAnnotations. A mesma lógica vale para HTML puro, e há uma versão em JavaScript no fim. Para o foco que entra e sai de modais, veja modal que devolve o foco; para o anel de foco que some no tema escuro, foco visível no escuro.
Por que o foco importa mais do que a mensagem
O foco é o lugar onde a próxima tecla cai. Quem usa mouse compensa um foco mal posicionado clicando no campo certo. Quem usa teclado precisa apertar Tab várias vezes até achar o campo errado, sem saber qual é. Quem usa leitor de tela ouve o que está em foco; se o foco ficou no botão, ouve "Salvar, botão" e mais nada.
Por isso a mensagem de erro, sozinha, não basta. Ela precisa estar ligada ao campo (com aria-describedby ou dentro do rótulo) e o foco precisa ir até lá. Quando o foco chega a um campo com mensagem associada, o leitor anuncia o rótulo, o valor e o erro de uma vez.
Quando mover o foco e quando não mover
- Mover: ao abrir uma página que é só o formulário (login, cadastro); depois de um envio que falhou na validação; ao abrir um diálogo; ao fechar um diálogo, de volta a quem o abriu.
- Não mover: enquanto a pessoa digita, porque uma busca em segundo plano terminou; ao carregar uma página com conteúdo para ler acima do formulário; quando uma validação "ao sair do campo" acusa erro e a pessoa já está no campo seguinte.
Para avisos que não pedem ação imediata ("salvo às 14:32"), use uma região com aria-live="polite", que o leitor anuncia sem tirar o foco do lugar.
Como fazer no Blazor
- Guarde uma referência ao primeiro campo com
@ref. - No primeiro render, chame
FocusAsyncno elemento desse campo. - Trate o
OnInvalidSubmitdoEditFormmarcando que o foco precisa ir para o erro. - No render seguinte, quando as classes de erro já estiverem no HTML, foque o primeiro campo com a classe
invalid. - Ligue cada mensagem ao campo, para o leitor de tela anunciar o erro junto.
@inject IJSRuntime JS
<EditForm Model="cliente" OnValidSubmit="Salvar" OnInvalidSubmit="AoFalhar" id="form-cliente">
<DataAnnotationsValidator />
<label for="nome">Nome completo</label>
<InputText id="nome" @ref="campoNome" @bind-Value="cliente.Nome"
aria-describedby="erro-nome" />
<div id="erro-nome"><ValidationMessage For="() => cliente.Nome" /></div>
<label for="cpf">CPF</label>
<InputText id="cpf" @bind-Value="cliente.Cpf" inputmode="numeric"
aria-describedby="erro-cpf" />
<div id="erro-cpf"><ValidationMessage For="() => cliente.Cpf" /></div>
<button type="submit">Salvar</button>
</EditForm>
@code {
Cliente cliente = new();
InputText? campoNome;
bool focarErro;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && campoNome?.Element is ElementReference el)
{
await el.FocusAsync();
return;
}
if (focarErro)
{
focarErro = false;
await JS.InvokeVoidAsync("focarPrimeiroInvalido", "#form-cliente");
}
}
void AoFalhar(EditContext contexto) => focarErro = true;
async Task Salvar() { /* grava e navega */ }
}
// wwwroot/js/foco.js, incluído na página principal
window.focarPrimeiroInvalido = function (seletorDoForm) {
var campo = document.querySelector(seletorDoForm + " .invalid");
if (campo) {
campo.focus();
}
};
Por que não focar direto no AoFalhar? Porque, naquele momento, o Blazor ainda não renderizou as classes de erro. O OnAfterRenderAsync roda depois que o HTML novo está na página, e a classe invalid, que o Blazor coloca nos inputs com mensagem de validação, já pode ser encontrada. Abra as ferramentas do navegador e confira no HTML gerado quais classes e atributos a sua versão aplica, antes de confiar no seletor.
O FocusAsync no Element do InputText é o que a pergunta do Stack Overflow original procurava: os componentes de input expõem o elemento HTML por essa propriedade, e o foco é aplicado sem JavaScript seu.
O resultado, no cadastro do exemplo: a atendente envia com o CPF em branco, o cursor vai para o campo CPF, e o leitor de tela anuncia "CPF, editar texto, o campo CPF é obrigatório". Ela digita e envia de novo, sem procurar nada na página.
A mesma ideia sem Blazor
Em HTML com JavaScript, o padrão é igual: no submit, valide, e se houver erro, cancele o envio e foque o primeiro campo com aria-invalid="true". O atributo autofocus resolve o foco inicial em páginas de formulário, sem script. Para os demais detalhes de rótulo, mensagem e botão, veja formulário que não cansa.
Erros comuns
- Mensagem de erro sem foco. A pessoa de teclado não acha o campo, e a de leitor de tela não sabe que houve erro.
- Foco no resumo de erros sem links. O leitor lê a lista, mas a pessoa ainda precisa caçar cada campo com Tab.
- Foco automático numa página com texto antes do formulário. O leitor de tela pula o conteúdo, e a página rola para baixo sem pedido.
- Roubar o foco quando um resultado assíncrono chega. A pessoa estava digitando e as próximas letras caem em outro lugar.
- Focar antes do render. O seletor não encontra nada e o código parece "não funcionar às vezes".
- Remover o anel de foco no CSS. O foco vai para o lugar certo, mas ninguém vê onde ele está.
Checklist
- Página de formulário abre com o foco no primeiro campo.
- Envio com erro leva o foco ao primeiro campo inválido.
- Cada mensagem de erro está ligada ao campo por
aria-describedby. - Nenhuma mudança de foco enquanto a pessoa digita.
- Avisos sem ação imediata em região
aria-live="polite". - Anel de foco visível em todos os temas.
- Teste completo feito só com teclado, e um com leitor de tela (NVDA ou VoiceOver).
Abra o seu formulário, desligue o mouse e envie com o CPF em branco. Onde está o anel de foco depois do Enter? Se estiver no botão ou no topo da página, a mensagem existe e o caminho até ela não. E a pergunta seguinte: ao corrigir o campo e enviar de novo, a pessoa precisa voltar de Tab em Tab até o botão?
Referência: https://stackoverflow.com/questions/59137973/how-to-set-the-focus-to-an-inputtext-element