Tucano v0.37.2

Abas

O template já traz a lista desenhada, a aba inicial marcada e os outros painéis com hidden — a página nasce certa antes do script. O JavaScript põe o que o HTML não dá: os papéis que fazem o leitor de tela anunciar "aba, 2 de 4", a ligação entre aba e painel e o teclado, em que a lista é uma parada só do Tab.

Exemplos

A mesma marcação, com uma classe ou um atributo a mais.

Segmentadas

is-segmented tem a altura de um campo, para trocar a visão de uma lista ao lado de um filtro.

128 contratos.

Segmentadas ao lado de um campo

Borda e respiro somam a altura do controle: a linha não desalinha.

Ativação manual

data-manual="true": as setas só movem o foco, e Enter ou Espaço abrem.

Aqui entraria o painel carregado por HTMX.

Com ícone e contador

O svg e a .tuc-badge is-plain dentro da aba seguem a cor dela.

Visão geral do projeto.

Muitas abas

A lista rola na horizontal em vez de quebrar em duas linhas; a aba focada pelas setas entra na tela.

Agosto — a aba marcada no template.

Aba desativada

disabled ou aria-disabled="true": não abre, e as setas pulam por ela.

Em edição.

Como usar

Escreva as classes, aria-selected="true" na aba inicial e hidden nos outros painéis. As abas e os painéis casam pela ordem: a terceira aba abre o terceiro painel.

<div class="tuc-tabs" data-tuc-tabs>
  <div class="tuc-tabs__list">
    <button class="tuc-tabs__tab" aria-selected="true">Dados</button>
    <button class="tuc-tabs__tab">Endereço</button>
  </div>
  <div class="tuc-tabs__panel">…</div>
  <div class="tuc-tabs__panel" hidden>…</div>
</div>

Sem aria-selected em nenhuma, abre a primeira aba ativa. O script dá type="button" às abas que não têm type — sem isso, trocar de aba dentro de um <form> enviaria o formulário —, cria os id que faltam e liga aba e painel por aria-controls e aria-labelledby.

Por que as classes vêm do template, e não do script

Assim a lista já nasce desenhada no primeiro quadro, sem bloco de espera e sem pulo quando o JavaScript chega. Pelo mesmo motivo o hidden dos painéis é obrigatório: sem ele, todos apareceriam empilhados até o script rodar.

Com HTMX, painel sob demanda

Para não carregar o conteúdo de todas as abas de uma vez, cada aba busca o próprio painel na primeira vez que é aberta. Ligue data-manual="true": no modo manual, abrir é sempre um clique — de mouse, de Enter ou de Espaço —, então o hx-trigger na aba cobre todos os caminhos.

<div class="tuc-tabs" data-tuc-tabs data-manual="true">
  <div class="tuc-tabs__list">
    <button class="tuc-tabs__tab" aria-selected="true">Resumo</button>
    <button class="tuc-tabs__tab" hx-get="{% url 'customer-invoices' customer.pk %}"
            hx-target="#invoices" hx-trigger="click once">Faturas</button>
  </div>
  <div class="tuc-tabs__panel">{% include "customers/summary.html" %}</div>
  <div class="tuc-tabs__panel" id="invoices" hidden></div>
</div>

Por que o manual existe

Na ativação automática, a seta troca o painel no mesmo instante. Com painel que carrega do servidor, atravessar quatro abas até a desejada seria disparar quatro requisições. No manual a seta só leva o foco, e quem decide abrir é a pessoa.

Em JavaScript

const tabs = new Tucano.Tabs('#customer', {
  selected: 1,        // índice da aba inicial
  manual: true,
  onChange: (index, { tab, panel }) => console.log(index, tab.textContent),
});

tabs.select(2);                     // abre a terceira; dispara onChange
tabs.select(0, { silent: true });   // abre sem disparar nada
tabs.index;                         // índice da aba aberta, ou -1
tabs.tabs;                          // as abas, na ordem
tabs.panels;                        // os painéis, na ordem

Cada troca — pela pessoa ou por select() — dispara tucano:change no elemento das abas, e o evento borbulha. select() numa aba desativada não faz nada.

document.addEventListener('tucano:change', (e) => {
  if (!e.target.matches('.tuc-tabs')) return;
  e.detail.value;      // índice da aba aberta
  e.detail.tab;        // o botão da aba
  e.detail.panel;      // o painel
  e.detail.instance;   // as Tabs
});

Abas dentro de um painel são outro conjunto: o componente só olha a lista e os painéis que são filhos diretos do seu elemento.

Teclado e acessibilidade

O teclado segue o padrão de abas do ARIA APG. A lista inteira é uma parada só do Tab — senão quem navega por teclado atravessaria todas as abas antes de chegar ao conteúdo da aberta.

TeclaAção
TabEntra na aba aberta; o próximo Tab vai ao conteúdo do painel
← →Aba anterior e próxima, pulando as desativadas e dando a volta nas pontas
Home EndPrimeira e última aba ativas
Enter EspaçoNo modo manual, abre a aba focada

A lista recebe role="tablist", cada aba role="tab" com aria-selected, e cada painel role="tabpanel". O painel sem nada focável dentro ganha tabindex="0", para o Tab chegar ao conteúdo; com um campo ou link dentro, não — seria uma parada inútil antes dele.

API

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

Marcação
[data-tuc-tabs]
Em JS
new Tucano.Tabs(alvo, opcoes)
Atributos
data-manual
Métodos
select destroy
Eventos
tucano:change

Opções

OpçãoPadrãoPara quê
selectednullindice da aba inicial; sem ele vale a marcada com aria-selected="true", ou a primeira
manualfalsesetas so movem o foco, e Enter ou Espaco trocam o painel — para painel que carrega por HTMX
onChangenull(index, detail) a cada troca feita pela pessoa ou por select()