Skip to content

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

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, use disabled e 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-open e, depois do evento return, troque loading para '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 BaseToast ou uma região role="status". Se a barra sai da tela com v-if, o botão que tinha o foco some junto; para a devolução, passe esse ponto em return-focus.
  • Coloque a barra como último filho da área que rola. Ela é position: sticky e passa por cima do que rola embaixo dela, então a área precisa deixar livre a altura da barra no scroll-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-height no <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, aplique scroll-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".

Anatomia ​

  1. Resumo: o slot padrão, com o documento, a obra e o valor.
  2. Botão Devolver: contorno de atenção, abre o modal de devolução.
  3. Botão Assinar: só com signable.
  4. Botão Aprovar: o primário, no fim da barra.
  5. Modal de devolução: um BaseConfirmModal que pede a justificativa.

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

Estados ​

Medição 30 · R$ 98.120,00
Salvando a aprovação
Salvando a aprovação…
Medição 32 · R$ 71.450,00
Salvando a devolução
Salvando a devolução…
Aditivo 4 · + R$ 32.800,00
Salvando a assinatura
Salvando a assinatura…
Medição 31 · R$ 54.300,00
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 com aria-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.

Faça e não faça ​

FaçaNão faça
Repetir no resumo a medição, a obra e o valor: "Medição 29 · Fórum de Linhares · R$ 412.380,00".Mostrar a barra sem dizer o que está sendo aprovado.
Esconder a barra com v-if para quem não pode decidir.Mostrar a barra desabilitada para quem nunca vai poder aprovar.
Levar o foco ao título da página depois de salvar a aprovação.Deixar o foco cair no começo da página quando a barra some.

Estilo ​

PropriedadeToken
Fundo--tj-layer-01 a 92% com desfoque do que passa por trás; sólido com transparência reduzida no sistema
Borda de cima1 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 rolagemscroll-padding-bottom com --tj-approval-height (altura medida da barra); sem medição, --tj-control-md + 2 × --tj-space-3
Tela baixaAté 480 px de altura: position: static e recuo de rolagem zerado
BotõesBaseButton 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
Camadaz-index 1030: acima da página, abaixo do modal (1050) e dos avisos (1060)
DesabilitadaBotões com aria-disabled na aparência de desabilitado do BaseButton, com o anel de foco
Alto contrasteFundo Canvas e borda CanvasText; botões desabilitados em GrayText

Código ​

vue
<script setup lang="ts">
import { BaseApprovalBar } from '@lab-inovacao-tjes/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>

Propriedades ​

Propriedade Tipo Padrão Descrição
labelstringobrigatóriaNome da região, por exemplo "Decisão da medição 29"
approveLabelstring'Aprovar'Verbo e objeto da decisão principal
returnLabelstring'Devolver com justificativa'Texto do botão de devolver
returnShortLabelstring'Devolver'Texto no celular; precisa ser o começo de returnLabel, que continua sendo o nome lido
signLabelstring'Assinar'Texto do botão de assinar
signablebooleanfalseMostra o botão de assinar
disabledbooleanfalseTrava todas as ações; os botões continuam focáveis com aria-disabled
loading'approve' | 'return' | 'sign'—Ação que está salvando
savingTextsPartial<Record<ApprovalAction, string>>—Texto lido pelo leitor de tela enquanto cada ação salva. Padrão: "Salvando a aprovação…", "Salvando a devolução…" e "Salvando a assinatura…"
actionsLabelstring'Ações da decisão'Nome da barra de ferramentas que agrupa os botões
returnTitlestring'Devolver para correção?'Pergunta do modal de devolução
returnDescriptionstring—O que acontece e com quem
returnConfirmLabelstring'Devolver'Botão que confirma a devolução
reasonHelperTextstring—Ajuda embaixo da justificativa
reasonPlaceholderstring—Exemplo dentro da justificativa
returnFocusstring | HTMLElement—Recebe o foco quando o modal de devolução fecha e a barra já saiu da tela; elemento ou seletor CSS
v-model:return-openbooleanfalseModal 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> com aria-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", o padrão barra de ferramentas (toolbar) do WAI-ARIA APG, com aria-label e aria-describedby no resumo, então ao chegar nos botões o leitor de tela ouve também o documento e o valor.
  • Teclado: os botões são uma parada só do Tab.
TeclaO que faz
TabEntra na barra pelo último botão usado e sai dela no próximo Tab
→ / ←Próximo e anterior botão; do último volta ao primeiro, e do primeiro vai ao último
Home / EndPrimeiro e último botão
Enter ou EspaçoAciona o botão

Botões que esperam outra ação salvar são pulados. Com Alt, Ctrl, Shift ou Cmd, as setas ficam com o navegador.

  • Com disabled, os botões continuam no teclado com aria-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 BaseConfirmModal com role="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 para return-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-busy no botão e anunciado por uma região role="status" escondida ("Salvando a aprovação…"), porque NVDA e JAWS não leem aria-busy num 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-ring dos botões. A barra não tem animação própria; o indicador de carregamento do BaseButton gira, 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.