Tema
BaseDataTable
Tabela de registros do mesmo tipo, para comparar pelas mesmas colunas, achar um registro e agir sobre ele. Ordena pelas colunas, seleciona linhas, pagina e mostra os estados de carregando e vazio. Abaixo de 768 px, cada linha vira um cartão.
| Situação | Ações | ||||
|---|---|---|---|---|---|
| CF014/2024 | Fórum de Linhares Engenharia Modelo S.A. | R$ 32.100.000,00 | Em dia | ||
| CF001/2022 | Fórum de Alegre Construtora Exemplo Ltda. | R$ 14.862.310,40 | Em atraso | ||
| CF003/2021 | Fórum de Cachoeiro de Itapemirim Construtora Exemplo Ltda. | R$ 9.105.442,17 | Concluída | ||
| CF007/2023 | Reforma do Fórum de Serra Obras Teste Ltda. | R$ 7.420.115,08 | Paralisada | ||
| CF019/2025 | Fórum de Guarapari Engenharia Modelo S.A. | R$ 5.230.000,00 | Em atraso |
Ordenado por Valor atual, decrescente
Selecionados: nenhum · Ordem: value desc
Uso
- A tabela recebe todos os registros em
rowse cuida da ordem e da página. Busca e filtro ficam com a tela: passe emrowssó os registros que passaram no filtro. row-keyé o campo único de cada linha. A seleção guarda esses valores, então ela sobrevive à ordenação, à troca de página e ao filtro. Veja em Seleção o que acontece com as linhas selecionadas que o filtro esconde.- Em TypeScript, o tipo das linhas vem de
rows: em#cell-<key>e#actions,rowjá tem o tipo do registro (row.codeé checado). labelé obrigatório: vira a legenda (<caption>) da tabela, só para leitores de tela. O título visível fica no cabeçalho da página ou do cartão.- A primeira coluna nomeia a linha: ela vira cabeçalho de linha (
<th scope="row">) e dá o nome da caixa de seleção, "Selecionar CF001/2022". Userow-headerpara escolher outra coluna. - No máximo 7 colunas. A oitava vira subtítulo de outra coluna (como a empresa abaixo da obra) ou vai para a tela de detalhe.
- Cabeçalho em frase curta, sem abreviar: "Valor atual", não "Vlr. at.".
- Sem
v-model:page-size, todas as linhas aparecem e o rodapé mostra "7 registros". Com ele, o rodapé ganha a BasePagination e só a página atual aparece.
Colunas e alinhamento
Cada coluna é um objeto { key, label, sortable, align, numeric, width }.
| Dado | Como configurar | Como aparece |
|---|---|---|
| Texto, nome | { key, label } | À esquerda |
| Dinheiro, quantidade, percentual | numeric: true | À direita, algarismos tabulares, sem quebra de linha |
| Código | Slot #cell-<key> com a classe tj-datatable-code | À esquerda, fonte mono 13 px |
| Detalhe de outra coluna | Slot com a classe tj-datatable-sub na segunda linha | 13 px em --tj-text-helper, abaixo do texto principal |
| Situação | Slot #cell-status com BaseBadge | Selo com ponto e texto, nunca só a cor |
align ('start', 'center' ou 'end') troca o alinhamento quando o padrão não serve. width aceita qualquer medida CSS, como '140px' ou '20%'.
Sem slot, a célula mostra o valor: números no formato brasileiro (14.862.310,4), true e false como "Sim" e "Não", e qualquer outra coisa (datas, objetos, null) vazia. Formate dinheiro e datas no slot #cell-<key>, que recebe { row, value, column }.
Ordenação
| Descrição | Quantidade | ||
|---|---|---|---|
| 4.2 | Alvenaria de vedação | 892,2 | 86.410,2 |
| 5.1 | Chapisco e reboco interno | 1.240 | 54.118 |
| 8.3 | Porta de veneziana | 14 | 12.904,35 |
3 registros
- Só colunas com
sortable: trueganham o botão no cabeçalho. Com pelo menos uma, a legenda ganha, só para leitores de tela, "colunas com botão podem ser ordenadas". - O primeiro clique ordena texto de A a Z e números do maior para o menor, porque em valores a pessoa procura primeiro os maiores. Cada clique seguinte inverte a direção.
- A ordem nova volta para a página 1. A ordenação usa
sortRows: estável (empates mantêm a ordem derows), sem diferenciar maiúsculas e acentos e com números dentro do texto em ordem natural ("Item 2" antes de "Item 10"). v-model:sortrecebe{ key, direction }ounull, para a tela abrir já ordenada ou guardar a ordem na URL.
Seleção
Com selectable, cada linha ganha uma caixa de seleção e o cabeçalho ganha "Selecionar todas as linhas da página". Ela fica marcada quando todas as linhas da página estão selecionadas e mista (indeterminate) quando só algumas estão. Marcá-la ou desmarcá-la atua só nas linhas da página atual e mantém a seleção das outras páginas. O rodapé mostra "2 de 7 selecionados".
Uma linha selecionada que o filtro esconde continua em v-model:selected, para a seleção voltar quando o filtro sair. O rodapé conta essas linhas à parte, "1 de 3 selecionados, mais 1 fora do filtro", para a pessoa saber que a seleção vai além do que vê. Ações em lote usam a lista inteira de selected; se a tela preferir agir só no que está à vista, ela tira de selected as chaves que saíram de rows ao filtrar.
A linha selecionada ganha fundo --tj-layer-selected-01 e uma barra azul à esquerda, além da caixa marcada.
Densidade
| Descrição | Quantidade | ||
|---|---|---|---|
| 4.2 | Alvenaria de vedação | 892,2 | 86.410,2 |
| 5.1 | Chapisco e reboco interno | 1.240 | 54.118 |
| 8.3 | Porta de veneziana | 14 | 12.904,35 |
3 registros
density="default" tem linhas de 52 px (--tj-row-default), para listas que a pessoa lê. density="compact" tem linhas de 40 px (--tj-row-compact), para planilhas que a pessoa confere linha a linha, como os itens da medição.
Estados
| Situação | ||||
|---|---|---|---|---|
Carregando registros
Nenhum contrato cadastrado
Os contratos de obra aparecem aqui assim que o primeiro for cadastrado.
- Carregando (
loading): linhas de esqueleto no lugar dos dados (5 por padrão,skeleton-rowsmuda), a tabela fica comaria-busy="true"e o leitor de tela ouve "Carregando registros", também na primeira carga. Os botões de ordenar e a paginação continuam no foco, mas ignoram cliques; as caixas de seleção ficam desabilitadas. - Vazio: sem registros, a BaseEmptyState toma o lugar da tabela e do rodapé, com
empty-title,empty-text,empty-icone o slot#empty-action. Diga o que vai aparecer ali ("Nenhum contrato cadastrado") ou, depois de uma busca, o que não foi achado e como sair dali ("Limpar busca"). O título é um<h3>;empty-heading-leveltroca o nível para ficar um abaixo do título do cartão ou da seção. O slot#emptytroca o estado vazio inteiro; mesmo com ele, o leitor de tela ouve oempty-title, então escreva um que sirva. - Erro: mostre a BasePageState no lugar da tabela; a tabela não busca dados e não conhece o erro.
No celular
Abaixo de 768 px, cada linha vira um cartão: a caixa de seleção à esquerda, as ações à direita e cada valor com o nome da coluna em cima. A linha de cabeçalho vira uma barra com a caixa e o texto "Selecionar todas" e, depois de "Ordenar por", os botões de ordenar. Sem seleção e sem coluna ordenável, a barra some. A tabela larga em telas maiores rola dentro do próprio contêiner, e a página nunca rola para o lado.
Estilo
| Propriedade | Token |
|---|---|
| Contêiner | Fundo --tj-layer-01, borda --tj-border-subtle-02, raio --tj-radius-lg, sombra --tj-shadow-1 |
| Cabeçalho | Fundo --tj-layer-02, texto --tj-text-helper 12 px semibold (5,61:1 no tema claro, 6,92:1 no escuro); coluna ordenada em --tj-text-primary |
| Botão de ordenar | Hover --tj-layer-hover-01, pressionado --tj-layer-accent-01; ícone --tj-icon-secondary, --tj-icon-primary na coluna ordenada |
| Linha | Altura --tj-row-default (52 px) ou --tj-row-compact (40 px), divisória --tj-border-subtle-01, hover --tj-layer-hover-01 |
| Linha selecionada | Fundo --tj-layer-selected-01, hover --tj-layer-selected-hover-01, barra de 3 px --tj-border-interactive |
| Cabeçalho de linha | --tj-weight-semibold, --tj-text-primary |
| Rodapé | Fundo --tj-layer-02, texto --tj-text-helper 13 px, espaço --tj-space-3 × --tj-space-4 |
| Espaços | Célula --tj-space-3 × --tj-space-4 (--tj-space-2 na vertical quando compacta); barra de ferramentas --tj-space-4 |
| Foco | Contorno de 2 px em --tj-border-interactive por dentro do botão de ordenar e da área de rolagem |
Diferenças em relação ao protótipo v3, de propósito: o cabeçalho não é sticky, porque a tabela rola para o lado dentro do próprio contêiner e um cabeçalho fixo grudaria nesse contêiner, que não rola na vertical; o hover da linha usa --tj-layer-hover-01, o token de hover sobre --tj-layer-01, em vez de --tj-layer-02, que é a cor do cabeçalho e quase não se distingue do branco (1,03:1); no celular a linha de cabeçalho vira barra em vez de sumir, para ordenar e selecionar a página continuarem possíveis; e não há .is-wide nem .is-hide-sm, porque cada valor do cartão já ocupa a coluna inteira e esconder uma coluna no celular esconderia dado.
Código
vue
<BaseDataTable
v-model:sort="sort"
v-model:selected="selected"
v-model:page="page"
v-model:page-size="pageSize"
:columns="columns"
:rows="filteredContracts"
row-key="id"
label="Contratos de obra"
:page-sizes="[10, 25, 50]"
:loading="loading"
selectable
>
<template #toolbar="{ tableId }">
<BaseSearch v-model="query" label="Buscar por contrato, obra ou empresa" />
<BaseSegmented v-model="filter" label="Situação" :options="filters" :controls="tableId" />
</template>
<template #cell-status="{ value }">
<BaseBadge :tone="tones[value]">{{ statusText[value] }}</BaseBadge>
</template>
<template #actions="{ row }">
<BaseButton kind="ghost" size="sm" icon="three-dots" icon-only :label="`Ações do contrato ${row.code}`" />
</template>
</BaseDataTable>| Propriedade | Tipo | Padrão | Descrição |
|---|---|---|---|
columns | DataTableColumn[] | — | Colunas: { key, label, sortable?, align?, numeric?, width? } (obrigatório) |
rows | T[] | — | Todos os registros, já filtrados pela tela (obrigatório); T vira o tipo de row nos slots |
rowKey | string | — | Campo único de cada linha (obrigatório) |
label | string | — | Nome da tabela, na legenda para leitores de tela (obrigatório) |
rowHeader | string | primeira coluna | Coluna que nomeia a linha e a caixa de seleção |
selectable | boolean | false | Coluna de caixas de seleção com "selecionar todas" |
loading | boolean | false | Linhas de esqueleto, tabela ocupada e controles inertes |
density | 'default' | 'compact' | 'default' | Linhas de 52 ou de 40 px |
pageSizes | number[] | — | Opções de "Itens por página"; precisa de v-model:page-size |
skeletonRows | number | 5 | Linhas de esqueleto enquanto carrega |
emptyTitle | string | 'Nenhum registro' | Título do estado vazio |
emptyText | string | — | Texto do estado vazio |
emptyIcon | string | 'inbox' | Ícone do estado vazio, nome do Bootstrap Icons sem bi- |
emptyHeadingLevel | 2 | 3 | 4 | 5 | 6 | 3 | Nível do título do estado vazio |
v-model:sort | { key, direction } | null | null | Coluna e direção ('asc' ou 'desc') da ordem |
v-model:selected | (string | number)[] | [] | Valores de rowKey das linhas selecionadas, inclusive as que o filtro escondeu |
v-model:page | number | 1 | Página atual |
v-model:page-size | number | — | Linhas por página; sem ele, não há paginação |
| Slot | Props | Descrição |
|---|---|---|
#toolbar | { tableId } | Busca, filtros e ações acima da tabela; passe tableId ao controls da BaseSegmented |
#cell-<key> | { row, value, column } | Conteúdo próprio das células de uma coluna |
#actions | { row } | Ações da linha, em geral um BaseButton só com ícone cujo label cita o registro |
#empty | — | Troca o estado vazio inteiro |
#empty-action | — | Ação do estado vazio padrão |
Eventos: update:sort, update:selected, update:page e update:pageSize. Uma ordem nova também emite update:page com 1.
Os tipos DataTableColumn, DataTableSort, DataTableDensity e RowKey são exportados pelo pacote.
Acessibilidade
- Segue o padrão de tabela ordenável do WAI-ARIA APG: um
<table>de verdade, com<caption>,<th scope="col">nas colunas e<th scope="row">na célula que nomeia a linha. Nada de grade dediv. - A ordenação é um
<button>nativo dentro do<th>, com o nome da coluna. Tab chega nele; Enter e Espaço ordenam. Só a coluna ordenada levaaria-sort="ascending"ou"descending". Depois de cada troca, uma regiãorole="status"anuncia "Ordenado por Valor atual, decrescente", porque nem todo leitor de tela fala a mudança dearia-sort. - A cor não é o único sinal: o ícone de ordenar muda de forma (setas duplas, seta para cima, seta para baixo), e a linha selecionada tem a caixa marcada e a barra à esquerda, além do fundo.
- Cada caixa de seleção tem nome próprio, "Selecionar CF001/2022", e a do cabeçalho é "Selecionar todas as linhas da página". O estado misto vem da propriedade
indeterminatedo<input>, que o navegador expõe comoaria-checked="mixed". A área de toque é de 24 px (WCAG 2.5.8). - Uma região
role="status"sempre presente, escondida da tela, anuncia o que muda: "Carregando registros" (também na primeira carga, porque a mensagem só entra depois que a região existe), a contagem depois de selecionar ou filtrar ("2 de 7 selecionados", "3 registros") e oempty-titlequando o filtro zera a lista. Ela fica fora do estado vazio, por isso o anúncio não some junto com a tabela. O texto visível do rodapé não é região viva, para nada ser dito duas vezes. Com paginação, a BasePagination anuncia também o intervalo. - Os botões de ação da linha são só ícone e precisam de
labelque cite o registro: "Ações do contrato CF001/2022". O cabeçalho da coluna de ações tem o texto "Ações" para leitores de tela. - Quando a tabela é mais larga que o contêiner, a área de rolagem vira uma região "Contratos de obra, área de rolagem" e entra no Tab (
tabindex="0"), para quem usa teclado rolar com as setas (WCAG 2.1.1). Sem rolagem lateral, como no layout de cartões, ela não tem papel nem entra no Tab. UmResizeObserverconfere a largura. - Quando o carregamento ou o estado vazio tira da tela o controle que estava com o foco (uma caixa de seleção, um botão de ação, o botão de ordenar), o foco vai para a área da tabela ou para o estado vazio, e não cai no
<body>(WCAG 2.4.3). - Enquanto carrega, a tabela tem
aria-busy="true", o<tbody>do esqueleto temaria-hidden="true"(a tabela expõe só a linha de cabeçalho) e "Carregando registros" é anunciado. Os botões de ordenar recebemaria-disabledem vez dedisabled, para o foco não sumir. - O foco do botão de ordenar e da área de rolagem é um contorno sólido em
--tj-border-interactive, não o--tj-ringtranslúcido, que fica abaixo de 3:1 sobre o fundo do cabeçalho (WCAG 1.4.11). No modo de alto contraste do Windows, o contorno e a barra da linha selecionada usam a cor de destaque do sistema. - No layout de cartões, os nomes de coluna que aparecem acima dos valores ficam com
aria-hidden, porque o leitor já ouve o cabeçalho. A tabela, os grupos, as linhas e as células levamroleexplícito, para manter a semântica de tabela quando o CSS troca odisplaydelas; essa é a correção conhecida para navegadores que descartam a semântica nesse caso. Os cabeçalhos que não ordenam continuam lá, só escondidos da tela, e os textos "Selecionar todas" e "Ordenar por" da barra têmaria-hidden, porque a caixa e os botões já têm nome. - As transições de cor da linha e do botão de ordenar somem com
prefers-reduced-motion; o esqueleto segue as regras da BaseSkeleton. Funciona nos temas claro e escuro. - Testado com axe nos estados que o jsdom cobre: padrão, ordenada crescente e decrescente, troca de coluna ordenada, seleção nenhuma, parcial e total, seleção com linha fora do filtro, paginada, carregando (com seleção, ações e paginação), primeira carga sem registros, vazia (padrão e com o slot
#empty), slots de célula e de ações, cabeçalho de linha em outra coluna, área de rolagem com rolagem lateral, barra de ferramentas com BaseSearch e BaseSegmented e densidade compacta. O layout de cartões, o contraste e o modo de alto contraste dependem de CSS e são conferidos visualmente.