Tema
BaseWizard
Guia um cadastro longo, uma etapa por vez: o andamento fica no alto, a etapa atual no meio e os botões Voltar, Continuar e Concluir no rodapé. Antes de avançar, o assistente confere a etapa.
- Dados do contrato, etapa atual
- Planilha vencedora, etapa pendente
- Empenho, etapa pendente
- Revisão, etapa pendente
Experimente avançar com o número vazio: a etapa fica, o campo mostra o erro e recebe o foco, e o andamento marca a etapa com erro.
Uso
- Use para um cadastro longo que se divide em partes com começo e fim claros: cadastro de contrato, lançamento de medição. Para mostrar onde um documento está num fluxo fixo, sem preencher nada, use o
BaseStatusFlow. - De três a cinco etapas. Com duas, um formulário só com seções (
BaseFormSection) resolve; com mais de cinco, junte etapas. - O nome da etapa diz o que ela pede, com as palavras da tela: "Planilha vencedora", não "Passo 2".
- O conteúdo de cada etapa vai no slot
step-<id>, com oidda etapa:#step-contrato,#step-planilha. Só a etapa atual fica montada, então guarde os dados no componente pai. - O
v-modelé oidda etapa atual. Umidvazio ou desconhecido mostra a primeira etapa. validaterecebe oidda etapa e devolvetruepara avançar oufalsepara ficar. Pode ser assíncrono: enquanto roda, Continuar fica ocupado. Se a função lançar erro, a etapa fica e o assistente emiteerrorcom o erro, para o pai registrar ou explicar a falha.- Se a etapa mudar ou o assistente for desabilitado enquanto
validateroda, o resultado é descartado: nenhuma etapa avança sem ter sido conferida. - Ao falhar, marque os campos com
invalide uma mensagem que diga como corrigir: o foco vai para o primeiro campo comaria-invalid="true"; num grupo de opções (BaseRadioGroup), para a opção marcada ou a primeira. - Sem campo marcado, por exemplo numa regra da etapa inteira ou numa falha de rede, a etapa mostra um aviso de erro com
errorTextlogo abaixo do título. Para uma causa conhecida, passe umerrorTextque diga o que fazer, como "A planilha tem itens sem preço unitário.". - Voltar não confere a etapa: ninguém fica preso por um campo que ainda vai corrigir.
- O último botão é Concluir e dispara
complete. Enquanto salva, passeloading. - Ações de apoio, como "Salvar rascunho", vão no slot
actions, no começo do rodapé. UseBaseButton, que já étype="button", outype="button"num botão comum: um<button>sem tipo envia o formulário e aciona Continuar. - A etapa já é um
<form>: não ponha outro<form>dentro de um slot de etapa. - Uma etapa com muitos campos se divide em
BaseFormSectiondentro do slot; o assistente tira o recuo próprio da seção e mantém a linha entre elas.
Estados
- Dados do contrato, etapa concluída
- Planilha vencedora, etapa com erro
- Empenho, etapa pendente
- Revisão, etapa pendente
- Dados do contrato, etapa concluída
- Planilha vencedora, etapa concluída
- Empenho, etapa concluída
- Revisão, etapa atual
- Dados do contrato, etapa concluída
- Planilha vencedora, etapa atual
- Empenho, etapa pendente
- Revisão, etapa pendente
| Estado | Como aparece |
|---|---|
| Etapa atual | Anel azul com ponto no andamento e "Etapa 2 de 4" no cartão; o nome da etapa no título |
| Etapa com erro | Depois de validate falhar, ou com invalid: círculo com exclamação e nome em vermelho; o leitor de tela ouve "etapa com erro". Some quando a etapa muda |
| Falha sem campo marcado | validate falhou ou lançou erro sem campo com aria-invalid: aviso de erro (BaseAlertBanner) com errorText abaixo do título. Some na próxima tentativa ou quando a etapa muda |
| Conferindo ou salvando | loading, ou validate assíncrono em curso: o botão principal mostra o giro, diz "ocupado" e ignora cliques; Voltar espera |
| Desabilitado | disabled: Voltar e Continuar desabilitados, por exemplo enquanto a etapa carrega |
Estilo
| Propriedade | Token |
|---|---|
| Cartões do andamento e da etapa | Fundo --tj-layer-01, borda 1 px --tj-border-subtle-02, raio --tj-radius-lg, sombra --tj-shadow-1 |
| Cartão do andamento | Recuo --tj-space-5 --tj-space-6 (--tj-space-4 no celular), --tj-space-3 entre "Etapa 1 de 4" e a lista |
| "Etapa 1 de 4" | No cartão do andamento: --tj-text-xs, --tj-weight-semibold, caixa alta, --tj-link-primary (6,53:1 no claro, 7,59:1 no escuro) |
| Nome da etapa | Estilo tj-h3 (--tj-text-xl, --tj-weight-semibold) |
| Descrição | --tj-text-sm, --tj-text-helper (5,80:1 no claro, 7,49:1 no escuro) |
| Rodapé | Fundo --tj-layer-02, borda superior 1 px --tj-border-subtle-01, --tj-space-3 entre botões, cantos de baixo com --tj-radius-lg menos a borda |
| Espaço | --tj-space-4 entre os cartões; --tj-space-6 dentro da etapa (--tj-space-4 no celular) |
| Andamento | O do BaseStatusFlow, tamanho médio |
| Foco | --tj-ring nos botões e no título da etapa |
Os contrastes são do texto contra --tj-layer-01, calculados a partir dos tokens nos dois temas.
Troca em relação ao protótipo v3: o .tj-wizard do protótipo (faixas no alto com "Etapa N") virou o BaseStatusFlow, para o andamento ter a mesma forma, os mesmos sinais e o mesmo texto para o leitor de tela em todo o sistema. A posição "Etapa 1 de 4" aparece no cartão do andamento, acima da lista; o leitor de tela a ouve no título da etapa, então no cartão ela fica escondida dele (aria-hidden).
O cartão da etapa não corta o conteúdo (overflow visível), para a lista de um BaseCombobox perto do fim da etapa passar da borda; o rodapé arredonda os próprios cantos.
No celular (até 575 px), os botões dividem a linha, cada um com a mesma largura, e o principal fica à direita.
Código
vue
<script setup lang="ts">
import { ref } from 'vue'
import { BaseField, BaseInput, BaseWizard, type WizardStep } from '@lab-inovacao/design-system'
const etapas: WizardStep[] = [
{ id: 'contrato', label: 'Dados do contrato', description: 'Identificação, objeto e prazos.' },
{ id: 'planilha', label: 'Planilha vencedora' },
{ id: 'revisao', label: 'Revisão' },
]
const etapa = ref('contrato')
const numero = ref('')
const numeroInvalido = ref(false)
function conferir(id: string): boolean {
if (id !== 'contrato') return true
numeroInvalido.value = numero.value.trim() === ''
return !numeroInvalido.value
}
function salvar(): void {
// salva o contrato
}
function registrar(erro: unknown): void {
console.error('Falha ao conferir a etapa', erro)
}
</script>
<template>
<BaseWizard v-model="etapa" label="Etapas do cadastro" :steps="etapas" :validate="conferir" @complete="salvar" @error="registrar">
<template #step-contrato>
<BaseField label="Número do contrato" required :invalid="numeroInvalido" invalid-text="Informe o número do contrato.">
<BaseInput v-model="numero" />
</BaseField>
</template>
<template #step-planilha>…</template>
<template #step-revisao>…</template>
</BaseWizard>
</template>| Propriedade | Tipo | Padrão | Descrição |
|---|---|---|---|
steps | WizardStep[] | — | Etapas em ordem, cada uma com id único; obrigatório. Sem etapas, nada aparece |
label | string | — | Nome da lista do andamento para o leitor de tela; obrigatório |
v-model | string | primeira etapa | id da etapa atual |
validate | (id: string) => boolean | Promise<boolean> | — | Confere a etapa antes de Continuar e Concluir; false ou erro mantém a etapa |
headingLevel | 2 | 3 | 4 | 5 | 6 | 2 | Nível do título da etapa; a aparência fica tj-h3 |
invalid | boolean | false | Marca a etapa atual com erro no andamento |
disabled | boolean | false | Desabilita Voltar e Continuar |
loading | boolean | false | Botão principal ocupado, Voltar espera |
backLabel | string | 'Voltar' | Texto do botão de voltar |
nextLabel | string | 'Continuar' | Texto do botão de avançar, como "Continuar para a planilha" |
finishLabel | string | 'Concluir' | Texto do botão da última etapa, como "Salvar contrato" |
errorText | string | 'Não foi possível continuar. Revise a etapa e tente de novo.' | Aviso na etapa quando a conferência falha sem campo marcado |
WizardStep:
| Campo | Tipo | Descrição |
|---|---|---|
id | string | Chave estável e única; dá o nome do slot step-<id> |
label | string | Nome da etapa, no andamento e no título |
description | string | Opcional. Uma linha embaixo do título sobre o que a etapa pede |
| Slot | Conteúdo |
|---|---|
step-<id> | Conteúdo da etapa com esse id |
actions | Botões de apoio no começo do rodapé, como "Salvar rascunho" |
| Evento | Quando |
|---|---|
update:modelValue | Continuar passou na conferência, ou Voltar foi acionado; traz o id da nova etapa |
complete | Concluir passou na conferência na última etapa |
error | validate lançou erro ou rejeitou; traz o erro. A etapa fica e mostra errorText |
Acessibilidade
- O andamento é o
BaseStatusFlow: lista ordenada com o nome dolabel, a etapa atual comaria-current="step"e cada etapa com o texto da situação ("etapa concluída", "etapa atual", "etapa pendente", "etapa com erro"). A cor nunca vem sozinha: cada situação tem um sinal de forma própria. - A etapa é um
<form>com o nome do título, "Etapa 2 de 4: Planilha vencedora", e a descrição emaria-describedby. O leitor de tela ouve a posição e o nome juntos. - Quando Voltar ou Continuar muda a etapa, o foco vai para o título (
tabindex="-1"): o leitor de tela anuncia a nova etapa e quem usa teclado começa do alto dela. Uma troca feita pelo pai move o foco só se ele já estiver dentro do assistente. Ao abrir a tela, ao retomar um rascunho depois de carregar, ou ao inserir uma etapa antes da atual, o foco não se move. - Enter num campo aciona Continuar, como em qualquer formulário. Voltar não envia o formulário; no slot
actionse dentro das etapas, useBaseButton(que já étype="button") outype="button", senão o botão aciona Continuar. - Se a conferência falhar, o foco vai para o primeiro campo com
aria-invalid="true", onde está a mensagem de erro (WCAG 3.3.1); numBaseRadioGroupinválido, para a opção marcada ou a primeira. - Sem campo marcado, o foco vai para o título da etapa e logo abaixo aparece o aviso de erro com
errorText, emrole="alert": quem está no rodapé ouve que não avançou, e quem enxerga vê o aviso com ícone e texto, não só a cor do andamento (WCAG 3.3.1 e 4.1.3). - Enquanto confere ou salva, o botão principal continua no foco, recebe
aria-busy="true"e ignora cliques e Enter, para ninguém avançar duas vezes. - Voltar não aparece na primeira etapa, em vez de aparecer desabilitado.
- A etapa troca no lugar, sem animação. O único movimento é o giro do botão ocupado, que vem do
BaseButton. - Testado com axe na primeira, numa do meio e na última etapa, com erro, desabilitado, salvando, conferindo numa etapa do meio, com outro nível de título, com campo inválido depois de uma conferência que falhou e com o aviso de falha sem campo marcado. O contraste foi calculado a partir dos tokens nos dois temas (veja Estilo), porque o jsdom não calcula cores.