# <aihio-data-grid>

A virtualized data table for large datasets, more rows than a page can hold (thousands to hundreds of thousands): one scrolling box in which only the rows in view are rendered, by your code, when the grid asks for them. Arrow keys move between cells as in a spreadsheet, and the grid tells assistive technology how many rows there are and which ones it shows. Find in page, printing, and reading the rows in a screen reader's browse mode only reach the rendered rows; when people need those, use aihio-table with aihio-pagination instead.

Intents: `tabular-data`.

## Attributes

- `row-count` (number): How many rows there are in all. Setting it, as when a filter changes the rows, asks for the rows in view again.
- `density` (`default` | `compact`): Row height. compact fits more rows in the box. Default: `default`.
- `loading` (boolean): Marks the rows as stale while new ones load: they are dimmed, and the table has aria-busy. Default: `false`.
- `sort-ascending-text` (string): Announced after sorting a column ascending, with {column} for the header's text. For localisation. Default: `Sorted by {column}, ascending`.
- `sort-descending-text` (string): Announced after sorting a column descending, with {column} for the header's text. Default: `Sorted by {column}, descending`.

## Properties

- `table` (`HTMLTableElement | null`, read-only): The <table> the element enhances.
- `body` (`HTMLTableSectionElement | null`, read-only): The <tbody> your code renders the rows into. The grid keeps a section of its own above and below it, so table.tBodies[0] is not it.
- `start` (`number`, read-only): The first row asked for, counting from 0.
- `end` (`number`, read-only): The row after the last one asked for.
- `rowCount` (`number`): Get or set row-count.

## Methods

- `scrollToRow()`: Scrolls so the row at index, counting from 0, is at the top of the view, and asks for the rows there.

## Events

- `aihio-range` (detail: start: number, end: number): Asks for rows: render the rows from start up to (not including) end into body, in order, replacing those there. Fired when the grid connects, when its rows in view change, when row-count is set, and after a sort. Rows fetched for a range that is no longer start to end should be dropped.
- `aihio-sort` (detail: column: string, direction: 'ascending' | 'descending'): Fired when the person sorts by a column, once its header carries the new aria-sort. Sort your rows by column; the grid then scrolls to the top and asks for the rows again with aihio-range.

## Native elements

<aihio-data-grid> reads these attributes on the native elements inside it.

### <th>

A header cell. The grid uses its one header row for column headers, and keeps it in view.

#### <th> attributes

- `data-sortable` (string): Makes the column sortable: the header's content becomes a button, and a click fires aihio-sort for your code to sort the rows by this name.
- `aria-sort` (`ascending` | `descending` | `none` | `other`): The order your rows are in. The grid moves it from header to header as people sort.
- `data-numeric` (boolean): Lines a column of figures up by place value. Put it on the header and on each of the column's cells. Default: `false`.

### <td>

A data cell, one line high: text that does not fit ends in an ellipsis.

#### <td> attributes

- `data-numeric` (boolean): Lines the figure up with the rest of its column. Default: `false`.

## Accessibility obligations

- (error, data-grid-row-count) When row-count is missing or not a whole number: Set row-count to how many rows there are in all. The grid asks for rows by number, and sizes its scroll box from the count.
- (warn, data-grid-row-limit) When row-count is above 350,000: Narrow the rows with a filter, or show them a page at a time with aihio-table and aihio-pagination. Firefox stops a box's height at 17.9 million pixels, about 389,000 rows of the default height, and rows past it cannot be scrolled to.
- (error, table-accessible-name) When the table has no caption, aria-label, or aria-labelledby: Name the table: a <caption>, aria-labelledby pointing at the visible heading above it, or aria-label.
- (error, table-header-cells) When the table has no <th> cells: Name each column with a <th> in the <thead> row.
- (warn, table-sort-column-name) When a sortable header has no column name: Give each sortable header a name, data-sortable="duration", so aihio-sort says which column to sort by without depending on the header's wording.
- (warn) When people need to find text in the page, print every row, or read the rows in a screen reader's browse mode: Use aihio-table with aihio-pagination instead. A grid has only the rows in view in the page, and those are all that find in page, printing, and browse mode reach.

## Examples

### A hundred thousand rows

The rows live in script. aihio-range asks for the ones in view and the script renders them into body; aihio-sort asks for them in a new order. Try the arrow keys, Page Down, and Control+End.

```html
<aihio-data-grid id="requests-grid" row-count="100000" density="compact">
  <table aria-label="Requests">
    <colgroup>
      <col style="width: 8rem">
      <col style="width: 6rem">
      <col>
      <col style="width: 6rem">
      <col style="width: 9rem">
    </colgroup>
    <thead>
      <tr>
        <th scope="col" data-sortable="id" aria-sort="ascending">Request</th>
        <th scope="col" data-sortable="method">Method</th>
        <th scope="col" data-sortable="path">Path</th>
        <th scope="col" data-sortable="status" data-numeric>Status</th>
        <th scope="col" data-sortable="duration" data-numeric>Duration (ms)</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</aihio-data-grid>
<script type="module">
  const grid = document.getElementById('requests-grid');
  const methods = ['GET', 'POST', 'PATCH', 'DELETE'];
  const rows = Array.from({ length: 100000 }, (_, index) => ({
    id: index + 1,
    method: methods[index % 4],
    path: `/api/items/${(index * 7919) % 100000}`,
    status: [200, 201, 204, 404, 500][index % 5],
    duration: (index * 37) % 1500,
  }));

  function render(start, end) {
    grid.body.replaceChildren(...rows.slice(start, end).map((row) => {
      const tr = document.createElement('tr');
      tr.innerHTML = `<th scope="row">#${row.id}</th><td>${row.method}</td><td>${row.path}</td><td data-numeric>${row.status}</td><td data-numeric>${row.duration}</td>`;
      return tr;
    }));
  }

  grid.addEventListener('aihio-range', (event) => render(event.detail.start, event.detail.end));
  grid.addEventListener('aihio-sort', (event) => {
    const { column, direction } = event.detail;
    const order = direction === 'ascending' ? 1 : -1;
    rows.sort((a, b) => (a[column] > b[column] ? 1 : a[column] < b[column] ? -1 : 0) * order);
  });
  render(grid.start, grid.end);
</script>
```

### Rows from a server

Fetch the rows asked for, and mark the grid loading while they come. A response for rows no longer in view is dropped: the person has scrolled on.

```html
<aihio-data-grid id="orders-grid" row-count="25000">
  <table aria-label="Orders">
    <colgroup>
      <col style="width: 8rem">
      <col>
      <col style="width: 9rem">
    </colgroup>
    <thead>
      <tr>
        <th scope="col">Order</th>
        <th scope="col">Customer</th>
        <th scope="col" data-numeric>Total</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</aihio-data-grid>
<script type="module">
  const grid = document.getElementById('orders-grid');

  // Stands in for fetch('/api/orders?start=...&end=...').
  const fetchOrders = (start, end) => new Promise((resolve) => setTimeout(() => resolve(
    Array.from({ length: end - start }, (_, offset) => ({
      id: 70000 + start + offset,
      customer: `Customer ${((start + offset) * 31) % 997}`,
      total: (((start + offset) * 7349) % 100000) / 100,
    }))
  ), 150));

  async function load(start, end) {
    grid.setAttribute('loading', '');
    const orders = await fetchOrders(start, end);
    if (start !== grid.start || end !== grid.end) return;
    grid.body.replaceChildren(...orders.map((order) => {
      const tr = document.createElement('tr');
      tr.innerHTML = `<th scope="row">#${order.id}</th><td>${order.customer}</td><td data-numeric>€${order.total.toFixed(2)}</td>`;
      return tr;
    }));
    grid.removeAttribute('loading');
  }

  grid.addEventListener('aihio-range', (event) => load(event.detail.start, event.detail.end));
  load(grid.start, grid.end);
</script>
```

## Mistakes

### Without row-count the grid does not know how many rows there are, so it asks for none and its scroll box has no height to scroll.

Don't (aihio lint: data-grid-row-count):

```html
<aihio-data-grid>
  <table aria-label="Requests">
    <thead>
      <tr>
        <th scope="col" data-sortable="id">Request</th>
        <th scope="col" data-sortable="method">Method</th>
        <th scope="col" data-sortable="path">Path</th>
        <th scope="col" data-sortable="status" data-numeric>Status</th>
        <th scope="col" data-sortable="duration" data-numeric>Duration (ms)</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</aihio-data-grid>
```

Do:

```html
<aihio-data-grid row-count="100000">
  <table aria-label="Requests">
    <thead>
      <tr>
        <th scope="col" data-sortable="id">Request</th>
        <th scope="col" data-sortable="method">Method</th>
        <th scope="col" data-sortable="path">Path</th>
        <th scope="col" data-sortable="status" data-numeric>Status</th>
        <th scope="col" data-sortable="duration" data-numeric>Duration (ms)</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</aihio-data-grid>
```

### An unnamed grid is announced only as "grid", which says nothing about what its hundred thousand rows are.

Don't (aihio lint: table-accessible-name):

```html
<aihio-data-grid row-count="100000">
  <table>
    <thead>
      <tr>
        <th scope="col" data-sortable="id">Request</th>
        <th scope="col" data-sortable="method">Method</th>
        <th scope="col" data-sortable="path">Path</th>
        <th scope="col" data-sortable="status" data-numeric>Status</th>
        <th scope="col" data-sortable="duration" data-numeric>Duration (ms)</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</aihio-data-grid>
```

Do:

```html
<aihio-data-grid row-count="100000">
  <table aria-label="Requests">
    <thead>
      <tr>
        <th scope="col" data-sortable="id">Request</th>
        <th scope="col" data-sortable="method">Method</th>
        <th scope="col" data-sortable="path">Path</th>
        <th scope="col" data-sortable="status" data-numeric>Status</th>
        <th scope="col" data-sortable="duration" data-numeric>Duration (ms)</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</aihio-data-grid>
```

### aihio-table renders every row it is given, so it has no row count. A table of more rows than a page can hold is aihio-data-grid, or aihio-table with aihio-pagination.

Don't (aihio lint: unknown-attribute):

```html
<aihio-table row-count="100000">
  <table aria-label="Requests">
    <thead>
      <tr>
        <th scope="col" data-sortable="id">Request</th>
        <th scope="col" data-sortable="method">Method</th>
        <th scope="col" data-sortable="path">Path</th>
        <th scope="col" data-sortable="status" data-numeric>Status</th>
        <th scope="col" data-sortable="duration" data-numeric>Duration (ms)</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</aihio-table>
```

Do:

```html
<aihio-data-grid row-count="100000">
  <table aria-label="Requests">
    <thead>
      <tr>
        <th scope="col" data-sortable="id">Request</th>
        <th scope="col" data-sortable="method">Method</th>
        <th scope="col" data-sortable="path">Path</th>
        <th scope="col" data-sortable="status" data-numeric>Status</th>
        <th scope="col" data-sortable="duration" data-numeric>Duration (ms)</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</aihio-data-grid>
```

### Without a column name aihio-sort reports the header's text, which changes when the page is translated.

Don't (aihio lint: table-sort-column-name):

```html
<aihio-data-grid row-count="100000">
  <table aria-label="Requests">
    <thead>
      <tr>
        <th scope="col" data-sortable>Request</th>
        <th scope="col" data-sortable>Duration (ms)</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</aihio-data-grid>
```

Do:

```html
<aihio-data-grid row-count="100000">
  <table aria-label="Requests">
    <thead>
      <tr>
        <th scope="col" data-sortable="id">Request</th>
        <th scope="col" data-sortable="duration">Duration (ms)</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</aihio-data-grid>
```

### Firefox stops a box's height at 17.9 million pixels, about 389,000 rows of the default height. The scroll ends there, before the rows do, and neither scrolling nor the keyboard reaches the rest.

Don't (aihio lint: data-grid-row-limit):

```html
<aihio-data-grid row-count="2000000">
  <table aria-label="Requests">
    <thead>
      <tr>
        <th scope="col" data-sortable="id">Request</th>
        <th scope="col" data-sortable="method">Method</th>
        <th scope="col" data-sortable="path">Path</th>
        <th scope="col" data-sortable="status" data-numeric>Status</th>
        <th scope="col" data-sortable="duration" data-numeric>Duration (ms)</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</aihio-data-grid>
```

Do:

```html
<aihio-table manual-sort>
  <table aria-label="Requests">
    <thead>
      <tr>
        <th scope="col" data-sortable="id" aria-sort="ascending">Request</th>
        <th scope="col" data-sortable="method">Method</th>
        <th scope="col" data-sortable="path">Path</th>
        <th scope="col" data-sortable="status" data-numeric>Status</th>
        <th scope="col" data-sortable="duration" data-numeric>Duration (ms)</th>
      </tr>
    </thead>
    <tbody>
      <tr><th scope="row">#1</th><td>GET</td><td>/api/items/0</td><td data-numeric>200</td><td data-numeric>12</td></tr>
    </tbody>
  </table>
</aihio-table>
<aihio-pagination page="1" pages="80000" href="/requests?page={page}" aria-label="Request pages"></aihio-pagination>
```
