Tucano v0.37.2

Toast

Aviso passageiro num canto da tela, para o que acabou de acontecer: salvo, enviado, não deu certo. Empilha, pausa quando o ponteiro ou o foco entra e é anunciado por leitor de tela. Para o que continua valendo enquanto a pessoa está na tela, use o aviso.

Exemplos

Cada tipo tem cor, ícone e tempo próprios. O erro fica mais tempo porque a pessoa precisa ler e, muitas vezes, agir; o carregando não fecha sozinho, porque quem o encerra é o fim da operação.

Tipos

info, success, warning, error e loading.

Não fecha sozinho

duration: null deixa na tela até alguém clicar no X.

Tempo próprio

duration em milissegundos, no lugar do padrão do tipo.

Pilha

Até três à mostra, empilhados; o ponteiro ou o foco abre em leque. O quinto fecha o mais antigo.

TipoFica na telaAnunciado como
info4 srole="status", região polida
success3,5 srole="status", região polida
warning6 srole="status", região polida
error8 srole="alert", região assertiva
loadingaté o fim da operaçãorole="status", região polida

No toast o vermelho é o tipo error. Nas classes do aviso e da etiqueta, o mesmo tom se chama is-danger.

Como usar

Não há nada para marcar no HTML: o toast é chamado quando algo acontece. O container de cada posição é criado na primeira vez que é usado.

Tucano.toast('Salvo');                          // info
Tucano.toast.success('Cadastro salvo');
Tucano.toast.error('Não foi possível salvar', { title: 'Erro 500' });

Tucano.toast({
  type: 'success',
  title: 'Contrato excluído',
  text: 'Ainda dá para voltar atrás.',
  action: { text: 'Desfazer', onClick: (t) => restore() },
  position: 'bottom-end',
  duration: null,       // não fecha sozinho
  closable: true,       // o X
});

Os atalhos info, success, warning, error e loading recebem o texto e, depois, qualquer outra opção. A ação é um botão do sistema: chama o onClick com o toast e fecha. Tudo devolve a instância, que é o mesmo new Tucano.Toast({ ... }) — ele recebe só as opções, sem alvo.

const t = Tucano.toast('Exportando...');
t.close();

// O evento sai do próprio cartão depois que ele deixa a tela; não borbulha.
t.node.addEventListener('tucano:toast-closed', () => console.log('fechou'));

Carregando e promessa

O toast de loading vira o resultado no mesmo cartão, em vez de fechar um e abrir outro: a pilha não se reorganiza e o olho não perde de vista o aviso que já estava lendo.

// Controle manual: guarde a instância e troque o que precisar.
const t = Tucano.toast.loading('Salvando contrato...');
await save();
t.update({ type: 'success', text: 'Contrato salvo' });

// Ou entregue a promessa e deixe os três estados por conta dela.
Tucano.toast.promise(fetch(url), {
  loading: 'Enviando...',
  success: (response) => `Enviado (${response.status})`,
  error: 'Não deu para enviar',
  position: 'top-center',       // o resto vale para o toast
});

update() troca tipo, título, texto e ação sem recriar o cartão. Mudando de tipo sem duration, vale o tempo do tipo novo, e o relógio recomeça: sem isso o carregando, que não fecha, viraria um "salvo" eterno na tela.

Em toast.promise(), success e error aceitam texto ou uma função que recebe o resultado ou o erro. Sem eles, os textos são "Carregando...", "Pronto" e "Algo deu errado", trocáveis na página inteira por Tucano.setTexts({ toast: { loading, success, error } }). A função devolve a mesma promessa que recebeu, para não atrapalhar quem já encadeava nela — rejeitada, ela continua rejeitada, e o erro se trata onde já se trataria.

null quer dizer "não fecha", e não "use o padrão"

O padrão do tempo é resolvido com in, e não com ??. O null do carregando é um valor: com ?? ele cairia nos 4 segundos de padrão, e o toast sumiria no meio da operação.

Posições

Seis cantos e centros, com bottom-end de padrão. Cada posição tem a própria pilha.

No celular, abaixo de 40rem, o toast ocupa a largura da tela e entra pelo eixo vertical, seja qual for a posição: preso a um canto ele ficava com margens diferentes de cada lado, e deslizar na horizontal um cartão tão largo quanto a tela é percurso longo demais para um aviso curto. A borda respeita o indicador de home do iPhone e o recorte da câmera.

max limita os toasts abertos na mesma posição, 4 por padrão; o que passa fecha o mais antigo. Na pilha, além do terceiro eles somem da vista, porque uma pilha de dez não ajuda ninguém.

Mensagens do Django

O framework de messages vira toast sem uma linha de JavaScript. Ponha o laço uma vez, no template base: cada <div> vira um toast e sai da página.

{% for m in messages %}
  <div data-tuc-toast data-type="{{ m.level_tag }}">{{ m }}</div>
{% endfor %}
messages.success(request, "Contrato salvo")
return redirect("contracts")
AtributoPara quê
data-tuc-toastMarca o elemento; o texto dele vira o texto do toast
data-typeO tipo. debug vira info; info, success, warning e error passam direto
data-titleTítulo
data-textTexto, no lugar do conteúdo do elemento
data-durationMilissegundos; false não fecha sozinho
data-positionUma das seis posições

m.level_tag, e não m.tags, se você usa extra_tags

Do data-type vale só a primeira palavra. O m.tags do Django escreve as extra_tags antes do nível, e aí a primeira palavra deixa de ser o tipo. Sem extra_tags, os dois dão no mesmo.

Pelo cabeçalho do HTMX

Numa resposta de HTMX não há redirect nem template base para o laço de mensagens. O servidor dispara o toast pelo cabeçalho HX-Trigger, e o script do CDN já está ouvindo.

import json

def save(request, pk):
    ...
    return HttpResponse(headers={
        "HX-Trigger": json.dumps({
            "tucano:toast": {"type": "success", "text": "Contrato salvo"},
        }),
    })

O detalhe aceita qualquer opção do toast, ou só um texto: {"tucano:toast": "Salvo"}. O HTMX dispara o evento tucano:toast no elemento da requisição, ele sobe até o <body>, e ali vira toast. O mesmo evento serve de qualquer outro código:

document.body.dispatchEvent(new CustomEvent('tucano:toast', {
  detail: { type: 'success', text: 'Contrato salvo' },
}));

Pelo CDN, o listenForEvents() é ligado junto com a inicialização. Importando de tucano pelo npm nada roda sozinho: chame listenForEvents(), ou importe tucano/auto.

Acessibilidade

O toast não rouba o foco: ele avisa sem tirar a pessoa do que estava fazendo. Quem anuncia é o leitor de tela.

A região é criada antes da mensagem

O aria-live fica no container, e não no toast. Uma região viva precisa existir no DOM antes de o conteúdo chegar; criada junto com a mensagem, o leitor de tela não anuncia. Erro fala numa região assertive e o resto numa polite, e as duas dividem o mesmo palco: posicionadas cada uma por conta, viravam duas pilhas paralelas na tela e o limite contava em dobro.

Quando update() muda um toast para error, ou de error para outro tipo, ele troca de região, e é isso que faz o leitor de tela anunciar a virada. Na tela nada se desloca, porque a posição vem do palco.

O relógio pausa quando o ponteiro entra no toast ou algo dentro dele recebe foco: ninguém consegue ler algo que some enquanto tenta clicar no "Desfazer". Pelo mesmo motivo a pilha abre em leque também no foco, e não só no ponteiro. O X é um botão de verdade, com aria-label="Fechar". Com prefers-reduced-motion, entrar e sair viram só esmaecer.

API

Gerada do código a cada build — se algo não está aqui, não existe.

Marcação
[data-tuc-toast]
Em JS
new Tucano.Toast(opcoes) Tucano.toast() Tucano.listenForEvents()
Atributos
data-duration data-position data-text data-title data-type
Métodos
update close
Eventos
tucano:toast-closed

Opções

OpçãoPadrãoPara quê
type'info''info' | 'success' | 'warning' | 'error' | 'loading'
titlenull
text''
durationundefinedms. null nao fecha sozinho. Padrao depende do tipo
position'bottom-end'top-start|top-center|top-end|bottom-start|bottom-center|bottom-end
closabletrue
actionnull{ text, onClick }
max4toasts simultaneos na mesma posicao