# DataTable

> Una tabla de datos sobre la lista de Drive: columnas por definición, orden con la cabecera, búsqueda, páginas o «Cargar más», selección con casillas, vacío, esqueletos y grupos.

```tsx
import { DataTable } from "sebs7n-ui/data-table"
```

## Ejemplos

### Facturas emitidas

Orden con la cabecera, búsqueda sin tildes ni mayúsculas, selección con casillas y páginas. La fila elegida va en el acento mientras la tabla tiene el foco, como la lista de Drive.

```tsx
import { Button } from "sebs7n-ui"
import { DataTable } from "sebs7n-ui/data-table"
import { PlusIcon } from "lucide-react"
import { useState } from "react"

function Basico() {
  const [elegidas, setElegidas] = useState<string[]>([])
  return (
    <div className="w-full">
      <DataTable
        aria-label="Facturas emitidas"
        columns={COLUMNAS}
        data={FACTURAS}
        defaultSort={{ id: "fecha", direction: "desc" }}
        filter
        getRowId={(row) => row.id}
        getRowLabel={(row) => `${row.numero} de ${row.cliente}`}
        onSelectedChange={setElegidas}
        pageSize={8}
        selectable
        selected={elegidas}
        toolbar={
          <>
            {elegidas.length > 0 && <span className="text-callout text-label-secondary">{elegidas.length} elegidas</span>}
            <Button size="sm">
              <PlusIcon />
              Nueva factura
            </Button>
          </>
        }
      />
    </div>
  )
}
```

### Por estado, con «Cargar más»

`groupBy` arma un grupo por estado con el título de Drive y su contador; `paging="more"` suma filas abajo en vez de paginar.

```tsx
import { DataTable } from "sebs7n-ui/data-table"

function Agrupada() {
  return (
    <div className="w-full">
      <DataTable
        aria-label="Facturas por estado"
        columns={COLUMNAS.filter((column) => column.id !== "estado")}
        data={[...FACTURAS].sort((a, b) => ESTADOS.indexOf(a.estado) - ESTADOS.indexOf(b.estado))}
        getRowId={(row) => row.id}
        groupBy={(row) => row.estado}
        pageSize={8}
        paging="more"
      />
    </div>
  )
}
```

### Cargando y vacía

Mientras llegan los datos, filas de esqueleto y `aria-busy`; sin filas, la tabla lo dice.

```tsx
import { Button } from "sebs7n-ui"
import { DataTable } from "sebs7n-ui/data-table"
import { useState } from "react"

function Estados() {
  const [cargando, setCargando] = useState(true)
  return (
    <div className="flex w-full flex-col gap-4">
      <Button className="self-start" onClick={() => setCargando(!cargando)} size="sm" variant="secondary">
        {cargando ? "Mostrar vacía" : "Volver a cargar"}
      </Button>
      <DataTable
        aria-label="Facturas del mes"
        columns={COLUMNAS}
        data={[]}
        empty="Todavía no emitiste facturas este mes."
        getRowId={(row) => row.id}
        loading={cargando}
        loadingRows={4}
      />
    </div>
  )
}
```

## Props

### DataTable

| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
| `columns` * | `DataTableColumn<T>[]` | — | Las columnas: `id`, `header`, `value` (ordenar y buscar), `cell` (dibujar), `sortable`, `numeric`, `searchable`, `className` (el ancho). |
| `data` * | `T[]` | — | Las filas. |
| `getRowId` * | `(row: T) => string` | — | El id estable de cada fila: la `key` y lo que guarda la selección. |
| `aria-label` | `string` | — | Nombre accesible del elemento. |
| `defaultPage` | `number` | `1` | La página al arrancar. |
| `defaultQuery` | `string` | `""` | La búsqueda al arrancar. |
| `defaultSelected` | `string[]` | `[]` | Los elegidos al arrancar. |
| `defaultSort` | `DataTableSort` | `null` | El orden al arrancar. |
| `density` | `"default" \| "compact"` | — | La densidad de la `Table`: `default` (filas de 41, la de Drive) o `compact` (32). |
| `empty` | `React.ReactNode` | — | Lo que dice la tabla sin filas. Por defecto, «Sin resultados». |
| `filter` | `boolean` | `false` | Muestra la búsqueda arriba. |
| `filters` | `React.ReactNode` | — | Lo que va pegado a la derecha de la búsqueda, en la misma fila mientras entre: los selectores de filtro. |
| `getRowLabel` | `(row: T) => string` | — | El nombre de la fila para su casilla. Por defecto, el valor de la primera columna. |
| `groupBy` | `(row: T) => string` | — | El título del grupo de cada fila: agrupa en el orden en que aparecen, con su contador. |
| `labels` | `Partial<Labels["dataTable"]>` | — | Textos: `search`, `selectAll`, `selectRow`, `loadMore`, `empty`, `result`, `results`, `item`, `items`. |
| `loading` | `boolean` | `false` | Filas de esqueleto en lugar de los datos, y `aria-busy`. |
| `loadingRows` | `number` | — | Cuántas filas de esqueleto. Por defecto `pageSize` o 5. |
| `locale` | `string` | — | El locale del orden alfabético. |
| `onPageChange` | `(page: number) => void` | — | Se llama con la página nueva. |
| `onQueryChange` | `(query: string) => void` | — | Se llama con el texto nuevo. |
| `onSelectedChange` | `(ids: string[]) => void` | — | Se llama con los ids elegidos. |
| `onSortChange` | `(sort: DataTableSort) => void` | — | Se llama con el orden nuevo. |
| `page` | `number` | — | La página, en base 1. Pasarla la vuelve controlada. |
| `pageSize` | `number` | — | Filas por página. Sin él, todas. |
| `paging` | `"pages" \| "more"` | `"pages"` | `pages` (default, con `Pagination`) o `more` (un botón «Cargar más»). |
| `query` | `string` | — | El texto de la búsqueda. Pasarlo lo vuelve controlado. |
| `selectable` | `boolean` | `false` | Una casilla por fila y la de todas en la cabecera. |
| `selected` | `string[]` | — | Los ids elegidos, en el orden de `data`. Pasarlo lo vuelve controlado. |
| `sort` | `DataTableSort` | — | El orden (`{ id, direction }` o `null`). Pasarlo lo vuelve controlado. |
| `toolbar` | `React.ReactNode` | — | Lo que va a la derecha de la búsqueda: filtros, «Nueva factura». |
| `className` | `string` | — | Se fusiona con las clases del componente vía `cn()` (tailwind-merge): lo que pongas gana. |

`*` obligatoria.

## Teclado

| Tecla | Qué hace |
|---|---|
| Tab | Recorre la búsqueda, los botones de orden de la cabecera, las casillas y la paginación, en ese orden. |
| Enter / Espacio | En una cabecera, ordena: ascendente, descendente, sin orden. En una casilla, la marca. |

## Accesibilidad

- Es una `<table>` de verdad: **nombre obligatorio** con `aria-label`. El lector anuncia columnas y filas y navega celda por celda con sus atajos de tabla.
- La columna ordenada lleva `aria-sort` (`ascending`/`descending`) en su `<th>`; el orden se cambia con un `<button>` adentro de la cabecera. La flecha es decorativa.
- La búsqueda es un `searchbox` con nombre («Buscar»), y el total («12 resultados») sale por una región `status` que se anuncia al cambiar.
- Cada casilla se nombra con la fila (`getRowLabel`, por defecto la primera columna): «Seleccionar Acme S.A.». La de la cabecera, «Seleccionar todas», queda en `mixed` con algunas.
- Cargando, la tabla está `aria-busy` y los esqueletos no tienen texto.
- Los grupos son `TableGroupHeader` (`<th scope="rowgroup">`) en un `<tbody>` por grupo: el lector los asocia a sus filas.

## Reglas de uso

- **Para datos que la persona ordena, busca o elige**: facturas, clientes, movimientos. Una lista de pocas filas fijas es una `Table` a mano.
- `value` es lo que se ordena y se busca; `cell` lo que se dibuja (un `Badge`, un importe formateado). Números y fechas se ordenan como tales; el texto, con el orden del idioma y sin distinguir tildes.
- El orden y la búsqueda son **del cliente**, sobre `data`. Para datos del servidor, controlá `sort`, `query` y `page` y pasá en `data` la página que llegó.
- **«Seleccionar todas» marca las que se ven** (la página actual), no las que el filtro esconde.
- `paging="more"` para un historial que se lee de arriba abajo; `pages` para buscar una fila puntual.
- Solo por subpath (`sebs7n-ui/data-table`): no está en el barrel, por peso.

## Relacionados

[table](/docs/components/table.md) · [pagination](/docs/components/pagination.md) · [checkbox](/docs/components/checkbox.md)
