Tema
BaseApprovalBar
Barra de decisão que fica presa embaixo da tela enquanto o fiscal confere uma medição ou um aditivo. Mostra o que está sendo decidido e quanto vale, e reúne as ações: devolver com justificativa, assinar (quando a pessoa também assina) e aprovar.
Nesta página as barras aparecem paradas dentro dos exemplos, para não se sobreporem. Numa tela de verdade, ela fica presa embaixo.
Medição 29 · Fórum de Alegre · R$ 412.903,55
Lançada por Paulo Ramos em 20/10 · 3 itens com observação
Lançada por Paulo Ramos em 20/10 · 3 itens com observação
Nenhuma decisão ainda.
Uso
- Use uma barra por tela, só quando a pessoa pode decidir. Sem permissão para decidir, não mostre a barra (
v-if); com permissão mas sem poder decidir agora, usedisablede diga o motivo no resumo. - O resumo (slot padrão) repete o documento, a obra e o valor: ninguém aprova sem ver quanto. A segunda linha traz quem lançou, quando e o que merece atenção.
- Aprovar é o único botão primário da tela enquanto a barra existe. O texto diz verbo e objeto: "Aprovar medição", não "OK".
- Devolver usa o contorno de atenção, nunca o vermelho de perigo: devolver não destrói nada, só volta o fluxo um passo. Sempre pede justificativa; sem texto, "Devolver medição" não envia.
- A barra abre o modal de devolução. Quem salva também fecha: use sempre
v-model:return-opene, depois do eventoreturn, troqueloadingpara'return', salve e feche. Se der erro, deixe o modal aberto, e a justificativa continua lá. - Depois de uma decisão salva, leve o foco a um ponto estável (o título da página ou a mensagem de resultado) e anuncie o resultado com
BaseToastou uma regiãorole="status". Se a barra sai da tela comv-if, o botão que tinha o foco some junto; para a devolução, passe esse ponto emreturn-focus. - Coloque a barra como último filho da área que rola. Ela é
position: stickye passa por cima do que rola embaixo dela, então a área precisa deixar livre a altura da barra noscroll-padding-bottom, senão um campo com foco perto do fim da tela fica escondido. A barra mede a própria altura e publica em--tj-approval-heightno<html>; o CSS dela já aplica esse valor na página e na área que tem a barra como filho direto. Se a área que rola for outra, apliquescroll-padding-bottom: var(--tj-approval-height)nela. - Em telas baixas (até 480 px de altura, como celular deitado ou zoom de 400%), a barra deixa de ser presa e fica no fim do conteúdo, para não tomar metade da tela.
- No celular o resumo ocupa a primeira linha, os botões dividem a largura e "Devolver com justificativa" encurta para "Devolver".
Com assinatura
Para quem aprova e assina no mesmo lugar, signable mostra o botão Assinar entre devolver e aprovar.
Aditivo 3 · Fórum de Serra · + R$ 86.400,00
Prazo de 60 dias a mais · enviado por Ana Lima em 21/10
Prazo de 60 dias a mais · enviado por Ana Lima em 21/10
Estados
Medição 30 · R$ 98.120,00
Salvando a aprovação
Salvando a aprovação…Salvando a aprovação
Medição 31 · R$ 54.300,00
Aguardando o parecer da engenharia para liberar a decisão
Aguardando o parecer da engenharia para liberar a decisão
loading: o botão da ação que está salvando mostra progresso e continua no foco do teclado; os outros esperam desabilitados. Com'return', o modal de devolução fica aberto e não fecha até terminar.disabled: nenhuma ação pode ser tomada. Os botões continuam alcançáveis pelo teclado comaria-disabled, para a pessoa saber que a decisão existe, mas não fazem nada. Explique o motivo no resumo, porque um botão desabilitado não diz por quê. Se o modal de devolução estiver aberto, ele fecha sem devolver.
Estilo
| Propriedade | Token |
|---|---|
| Fundo | --tj-layer-01 a 92% com desfoque do que passa por trás; sólido com transparência reduzida no sistema |
| Borda de cima | 1 px em --tj-border-subtle-02 |
| Sombra | --tj-shadow-rgb a 18%, para cima |
| Espaço interno | --tj-space-3 em cima e embaixo; --tj-space-8 dos lados no computador e --tj-space-4 abaixo de 992 px |
| Espaço entre partes | --tj-space-4 entre resumo e botões e entre botões |
| Resumo | --tj-text-sm, --tj-text-secondary; <strong> em --tj-text-primary e --tj-weight-semibold. Quebra linha em vez de cortar com reticências, diferente do protótipo, para nunca esconder o valor que está sendo decidido |
| Recuo de rolagem | scroll-padding-bottom com --tj-approval-height (altura medida da barra); sem medição, --tj-control-md + 2 × --tj-space-3 |
| Tela baixa | Até 480 px de altura: position: static e recuo de rolagem zerado |
| Botões | BaseButton médio (--tj-control-md, 40 px): devolver com contorno de atenção (--tj-button-warning-border, --tj-support-warning-text), assinar secundário, aprovar primário |
| Camada | z-index 1030: acima da página, abaixo do modal (1050) e dos avisos (1060) |
| Desabilitada | Botões com aria-disabled na aparência de desabilitado do BaseButton, com o anel de foco |
| Alto contraste | Fundo Canvas e borda CanvasText; botões desabilitados em GrayText |
Código
vue
<script setup lang="ts">
import { BaseApprovalBar } from '@lab-inovacao/design-system'
</script>
<BaseApprovalBar
v-if="canDecide"
v-model:return-open="returning"
:label="`Decisão da medição ${measurement.number}`"
approve-label="Aprovar medição"
:return-title="`Devolver a medição ${measurement.number}?`"
:return-description="`A medição volta para ${measurement.launchedBy} com a sua justificativa.`"
return-confirm-label="Devolver medição"
:loading="saving"
return-focus="#page-title"
@approve="approveMeasurement"
@return="returnMeasurement"
>
<strong>Medição {{ measurement.number }} · {{ formatMoney(measurement.totalCents) }}</strong><br>
Lançada por {{ measurement.launchedBy }} em {{ measurement.launchedAt }}
</BaseApprovalBar>| Propriedade | Tipo | Padrão | Descrição |
|---|---|---|---|
label | string | — | Nome da região, por exemplo "Decisão da medição 29"; obrigatório |
approveLabel | string | 'Aprovar' | Verbo e objeto da decisão principal |
returnLabel | string | 'Devolver com justificativa' | Texto do botão de devolver |
returnShortLabel | string | 'Devolver' | Texto no celular; precisa ser o começo de returnLabel, que continua sendo o nome lido |
signLabel | string | 'Assinar' | Texto do botão de assinar |
signable | boolean | false | Mostra o botão de assinar |
disabled | boolean | false | Trava todas as ações; os botões continuam focáveis com aria-disabled |
loading | 'approve' | 'return' | 'sign' | — | Ação que está salvando |
savingTexts | Partial<Record<'approve' | 'return' | 'sign', string>> | 'Salvando a aprovação…', 'Salvando a devolução…', 'Salvando a assinatura…' | Texto lido pelo leitor de tela enquanto cada ação salva |
actionsLabel | string | 'Ações da decisão' | Nome da barra de ferramentas que agrupa os botões |
returnTitle | string | 'Devolver para correção?' | Pergunta do modal de devolução |
returnDescription | string | — | O que acontece e com quem |
returnConfirmLabel | string | 'Devolver' | Botão que confirma a devolução |
reasonHelperText | string | — | Ajuda embaixo da justificativa |
reasonPlaceholder | string | — | Exemplo dentro da justificativa |
returnFocus | HTMLElement | string | — | Recebe o foco quando o modal de devolução fecha e a barra já saiu da tela; elemento ou seletor CSS |
v-model:return-open | boolean | false | Modal de devolução aberto |
Eventos: approve, return(reason: string) com a justificativa sem espaços nas pontas e nunca vazia, sign.
Slot padrão: o resumo do que está sendo decidido.
Acessibilidade
- A barra é uma
<section>comaria-label, ou seja, uma região: o leitor de tela acha a decisão pelo atalho de regiões. - Os botões ficam num
role="toolbar"(padrão Toolbar do APG) comaria-labelearia-describedbyno resumo, então ao chegar nos botões o leitor de tela ouve também o documento e o valor. - Teclado: a barra é uma parada só do Tab. ← e → passam de um botão para outro (voltando ao início no fim), Home e End vão ao primeiro e ao último, e Enter ou Espaço acionam. Botões que esperam outra ação salvar são pulados, e o Tab volta ao último botão usado. Com Alt, Ctrl, Shift ou Cmd as setas ficam com o navegador.
- Com
disabled, os botões continuam no teclado comaria-disabled(o APG permite quando é importante saber que a função existe): o leitor de tela anuncia "indisponível" e lê o resumo, que diz o motivo. Se a barra trava com o foco num botão, o foco fica nele. - O modal de devolução é o
BaseConfirmModalcomrole="alertdialog": o foco vai para a justificativa, fica preso no modal e volta ao botão de devolver ao fechar, mesmo no Safari, que não dá foco ao botão clicado. Se a barra saiu da tela, o foco vai parareturn-focus. Esc, "Voltar" e o botão de fechar fecham sem devolver; clicar fora não fecha, para não perder o texto. - No celular, "Devolver" aparece na tela e "Devolver com justificativa" continua como nome lido. O texto visível é o começo do nome, como pede o critério 2.5.3 (rótulo no nome).
- A cor nunca é o único sinal: cada botão tem texto e ícone próprios. O carregamento é marcado com
aria-busyno botão e anunciado por uma regiãorole="status"escondida ("Salvando a aprovação…"), porque NVDA e JAWS não leemaria-busynum botão com foco. O resultado da decisão é anunciado por quem salva (veja Uso). - Depois da decisão, o foco vai para um ponto estável escolhido pela tela, nunca cai no
<body>(veja Uso). - Foco não encoberto (critério 2.4.11): a área que rola deixa livre a altura da barra com
scroll-padding-bottom, então um controle com foco nunca fica escondido atrás dela. Em telas baixas a barra deixa de ser presa (critério 1.4.10, refluxo). - O foco aparece com o contorno sólido e o anel
--tj-ringdos botões. A barra não tem animação própria; o indicador de carregamento doBaseButtongira, e com movimento reduzido no sistema gira devagar, sem as transições do botão. - Testado com axe no estado padrão, com assinatura, sem resumo, desabilitado, salvando cada uma das três ações, com o modal recém-aberto, com a justificativa em erro e salvando a devolução com o modal aberto. O jsdom não aplica media queries, então o layout do celular, a transparência reduzida e o alto contraste não têm teste automatizado.