# Tables

Tables are used to display tabular data in rows and columns. They can allow users to select rows and sort by columns.

<!-- docs: demo display:block -->
```html
<script type="module">
  import '@brightspace-ui/core/components/table/demo/table-test.js';
</script>
<d2l-test-table></d2l-test-table>
```

## Best Practices
<!-- docs: start best practices -->
<!-- docs: start dos -->
* Use a table if your data has many dimensions
* Use a table when your data has multiple dimensions and any of the following are true:
  * There are more than just a few dimensions
  * The dimensions need to be sortable
  * The dimensions need to be easily compared across rows (ie- scannable)
* Specify [column and row headings](https://www.w3.org/WAI/tutorials/tables/) so data is meaningful to screen reader users
<!-- docs: end dos -->

<!-- docs: start donts -->
* Don't use a table to display data that should appear as cohesive objects or entities - use a list instead
<!-- docs: end donts -->
<!-- docs: end best practices -->

## Responsive Behavior
If the browser window is too narrow to display the table’s contents, a scroll button appears. This alerts users to overflowing content and provides a way for users to scroll horizontally. The scroll button sticks to the top of the screen so that it's available as long as the table is in the viewport.

<!-- docs: demo size:large -->
```html
<script type="module">
  import '@brightspace-ui/core/components/table/demo/table-test.js';
</script>
<div style="width: 400px;">
  <d2l-test-table wide></d2l-test-table>
</div>
```

If the viewport is very narrow — for example, on a mobile device — it may be preferable to replace a wide table with a list, a set of cards, or an alternate layout. However, the responsive table component works well as a consistent fallback solution.

## Table Wrapper [d2l-table-wrapper]

The `d2l-table-wrapper` element can be combined with table styles to apply default/light styling, row selection styles, overflow scrolling and sticky headers to native `<table>` elements within your Lit components.

<!-- docs: demo code properties name:d2l-table-wrapper sandboxTitle:'Table Wrapper' display:block -->
```html
<script type="module">
  import { html, LitElement } from 'lit';
  import { tableStyles } from '@brightspace-ui/core/components/table/table-wrapper.js';

  const fruits = ['Apples', 'Oranges', 'Bananas'];

  const data = [
    { name: 'Canada', fruit: { 'apples': 356863, 'oranges': 0, 'bananas': 0 }, selected: false },
    { name: 'Australia', fruit: { 'apples': 308298, 'oranges': 398610, 'bananas': 354241 }, selected: false },
    { name: 'Mexico', fruit: { 'apples': 716931, 'oranges': 4603253, 'bananas': 2384778 }, selected: false },
    { name: 'Brazil', fruit: { 'apples': 1300000, 'oranges': 50000, 'bananas': 6429875 }, selected: false },
    { name: 'England', fruit: { 'apples': 345782, 'oranges': 4, 'bananas': 1249875 }, selected: false },
    { name: 'Hawaii', fruit: { 'apples': 129875, 'oranges': 856765, 'bananas': 123 }, selected: false },
    { name: 'Japan', fruit: { 'apples': 8534, 'oranges': 1325, 'bananas': 78382756 }, selected: false }
  ];

  class SampleTable extends LitElement {

    static styles = tableStyles;

    render() {
      const type = this.type === 'light' ? 'light' : 'default';

      return html`
        <d2l-table-wrapper>
          <table class="d2l-table">
            <thead>
              <tr>
                <th>Country</th>
                ${fruits.map((fruit) => html`<th>${fruit}</th>`)}
              </tr>
            </thead>
            <tbody>
              ${data.map((row) => html`
                <tr>
                  <th>${row.name}</th>
                  ${fruits.map((fruit) => html`<td>${row.fruit[fruit.toLowerCase()]}</td>`)}
                </tr>
              `)}
            </tbody>
          </table>
        </d2l-table-wrapper>
      `;
    }

  }
  customElements.define('d2l-sample-table', SampleTable);
</script>
<d2l-sample-table></d2l-sample-table>
```

<!-- docs: start hidden content -->
### Properties

| Property | Type | Description | Default Value |
|---|---|---|---|
| `no-column-border` | boolean | Hides the column borders on "default" table type | false |
| `sticky-headers` | boolean | Whether to make the header row sticky | false |
| `type` | string | Type of the table style. Can be one of  `default`, `light`. | 'default' |

## Light Type

For a table style with fewer borders and tighter padding, there's the `light` type:

```html
<d2l-table-wrapper type="light">
  <table class="d2l-table">...</table>
</d2l-table-wrapper>
```

## Sticky Headers

For long tables, the header row can be made to "stick" in place as the user scrolls.

```html
<d2l-table-wrapper sticky-headers>
  <table class="d2l-table">...</table>
</d2l-table-wrapper>
```
<!-- docs: end hidden content -->

## Sortable Column Buttons [d2l-table-col-sort-button]

When tabular data can be sorted, the `<d2l-table-col-sort-button>` can be used to provide an interactive sort button as well as arrows to indicate the ascending/descending sort direction. This is meant to be used within a `d2l-table-wrapper`.

For the example below:
- The implementation is hidden. See the code in [Multi-Faceted Sort Button](#multi-faceted-sort-button) for a more detailed implementation example.
- The code itself is handling the `desc` attribute so updating it in the Properties table will not have an impact on the live demo.

<!-- docs: demo properties name:d2l-table-col-sort-button sandboxTitle:'Table Sortable Column Buttons' -->
```html
<script type="module">
  import '@brightspace-ui/core/components/table/table-col-sort-button.js';
  import { html, LitElement } from 'lit';
  import { tableStyles } from '@brightspace-ui/core/components/table/table-wrapper.js';

  class MySortableTableElem extends LitElement {

    static properties = {
      _sortDesc: { state: true }
    };

    static styles = tableStyles;

    constructor() {
      super();
      this._sortDesc = false;
    }

    render() {
      const data = [1, 2];
      const sorted = data.sort((a, b) => {
        if (this._sortDesc) {
          return b - a;
        }
        return a - b;
      });
      const rows = sorted.map(i => {
        return html`<tr>
            <td>Cell ${i}-A</td>
            <td>Cell ${i}-B</td>
          </tr>
        `;
      });

      return html`
        <d2l-table-wrapper>
          <table class="d2l-table">
            <thead>
              <tr>
                <th>
                  <d2l-table-col-sort-button @click="${this._handleSort}" ?desc="${this._sortDesc}" source-type="words">
                    Column A
                  </d2l-table-col-sort-button>
                </th>
                <th>Column B</th>
              </tr>
            </thead>
            <tbody>
              ${rows}
            </tbody>
          </table>
        </d2l-table-wrapper>
      `;
    }

    _handleSort(e) {
      const desc = e.target.hasAttribute('desc');
      this._sortDesc = !desc;
    }

  }
  customElements.define('d2l-my-sortable-table-elem', MySortableTableElem);
</script>
<d2l-my-sortable-table-elem></d2l-my-sortable-table-elem>
```

The simplified property usage looks like this:

```html
<d2l-table-wrapper>
  <table class="d2l-table">
    <thead>
      <tr>
        <th><d2l-table-col-sort-button>Ascending</d2l-table-col-sort-button></th>
        <th><d2l-table-col-sort-button desc>Descending</d2l-table-col-sort-button></th>
        <th><d2l-table-col-sort-button nosort>Not Sorted</d2l-table-col-sort-button></th>
      </tr>
    </thead>
  </table>
</d2l-table-wrapper>
```

<!-- docs: start hidden content -->
### Properties

| Property | Type | Description | Default Value |
|---|---|---|---|
| `desc` | boolean | Whether sort direction is descending | false |
| `nosort` | boolean | Column is not currently sorted. Hides the ascending/descending sort icon. | false |
| `position` | string | Position of the button content. Accepted values are 'start', 'center', and 'end'. | 'start' |
| `source-type` | string | The type of data in the column. Used to set the title. Accepted values are 'words', 'numbers', and 'dates'. This is only applicable to cases that are not using the multi-faceted dropdown. | 'unknown' |

### Slots
| Name | Description |
|---|---|
| `Default` | Column header text |
| `items` | Multi-facted sort items. Generally assigned to the `slot` attribute on a nested `d2l-table-col-sort-button-item`. |
<!-- docs: end hidden content -->

### Slotted Item [d2l-table-col-sort-button-item]

This is a radio menu item to be used within the `d2l-table-col-sort-button` component for a [multi-faceted sort](#multi-faceted-sort-button).

<!-- docs: demo code properties autoSize:false align:start name:d2l-table-col-sort-button-item sandboxTitle:'Table Sortable Column Button - Slotted Item' -->
```html
<script type="module">
  import '@brightspace-ui/core/components/table/table-col-sort-button.js';
  import '@brightspace-ui/core/components/table/table-col-sort-button-item.js';
</script>
<!-- docs: start hidden content -->
<style>
  d2l-table-col-sort-button {
    --d2l-table-cell-padding: 10px;
  }
</style>
<!-- docs: end hidden content -->
<d2l-table-col-sort-button desc>
  Items
  <d2l-table-col-sort-button-item text="Item 1" slot="items" value="item1"></d2l-table-col-sort-button-item>
  <d2l-table-col-sort-button-item text="Item 2" slot="items" value="item2"></d2l-table-col-sort-button-item>
</d2l-table-col-sort-button>
```

### Multi-Faceted Sort Button

When a single column is responsible for sorting in multiple facets (e.g., first name and last name), it is recommended to use the dropdown menu approach by slotting `d2l-table-col-sort-button-item` components within the `d2l-table-col-sort-button`. Please note that the consumer is responsible for all sort logic, including when `desc` and `nosort` are set on `d2l-table-col-sort-button`.

**WARNING**: Do NOT use this if the table is using `sticky-headers` AND multiple header rows. It is not currently supported. In that siuation, continue to put multiple `d2l-table-col-sort-button` components in the same column.

<!-- docs: demo code display:block sandboxName:table-multi-faceted-sort-button sandboxTitle:'Table Multi-Faceted Sort Button' -->
```html
<script type="module">
  import '@brightspace-ui/core/components/table/table-col-sort-button.js';
  import '@brightspace-ui/core/components/table/table-col-sort-button-item.js';
  import { html, LitElement } from 'lit';
  import { tableStyles } from '@brightspace-ui/core/components/table/table-wrapper.js';
  const data = () => [
    { firstname: 'John', lastname: 'Smith', grade: 85 },
    { firstname: 'Emily', lastname: 'Jones', grade: 92 },
    { firstname: 'Michael', lastname: 'Davis', grade: 78 },
    { firstname: 'Sarah', lastname: 'Brown', grade: 90 },
    { firstname: 'David', lastname: 'Wilson', grade: 88 },
    { firstname: 'Jessica', lastname: 'Taylor', grade: 95 },
    { firstname: 'Christopher', lastname: 'Martinez', grade: 83 }
  ];
  class MyComplexSortableTableElem extends LitElement {
    static properties = {
      _desc: { state: true },
      _field: { state: true }
    };
    static styles = tableStyles;
    constructor() {
      super();
      this._data = data();
      this._desc = false;
    }
    render() {
      const rowData = this._field ? this._data.sort((a, b) => {
        if (this._desc) {
          if (a[this._field] > b[this._field]) return -1;
          if (a[this._field] < b[this._field]) return 1;
        } else {
          if (a[this._field] < b[this._field]) return -1;
          if (a[this._field] > b[this._field]) return 1;
        }
        return 0;
      }) : this._data;
      const rows = rowData.map(i => {
        return html`<tr>
            <td>${i.firstname} ${i.lastname}</td>
            <td>${i.grade}</td>
          </tr>
        `;
      });
      return html`
        <d2l-table-wrapper>
          <table class="d2l-table">
            <thead>
              <tr>
                <th>
                  <d2l-table-col-sort-button ?desc="${this._desc}" ?nosort="${this._field !== 'firstname' && this._field !== 'lastname'}">
                    Learner
                    <d2l-table-col-sort-button-item slot="items" text="First Name, A to Z" data-field="firstname" @d2l-table-col-sort-button-item-change="${this._handleSortComplex}" value="1"></d2l-table-col-sort-button-item>
                    <d2l-table-col-sort-button-item slot="items" text="First Name, Z to A" data-field="firstname" data-desc @d2l-table-col-sort-button-item-change="${this._handleSortComplex}" value="2"></d2l-table-col-sort-button-item>
                    <d2l-table-col-sort-button-item slot="items" text="Last Name, A to Z" data-field="lastname" @d2l-table-col-sort-button-item-change="${this._handleSortComplex}" value="3"></d2l-table-col-sort-button-item>
                    <d2l-table-col-sort-button-item slot="items" text="Last Name, Z to A" data-field="lastname" data-desc @d2l-table-col-sort-button-item-change="${this._handleSortComplex}" value="4"></d2l-table-col-sort-button-item>
                  </d2l-table-col-sort-button>
                </th>
                <th>
                  <d2l-table-col-sort-button ?desc="${this._desc}" data-field="grade" @click="${this._handleSort}" ?nosort="${this._field !== 'grade'}">Grade</d2l-table-col-sort-button>
                </th>
              </tr>
            </thead>
            <tbody>
              ${rows}
            </tbody>
          </table>
        </d2l-table-wrapper>
      `;
    }
    _handleSort(e) {
      const field = e.target.getAttribute('data-field');
      const desc = e.target.hasAttribute('desc');
      this._desc = field === this._field ? !desc : false;
      this._field = field;
    }
    _handleSortComplex(e) {
      this._field = e.target.getAttribute('data-field');
      this._desc = e.target.hasAttribute('data-desc');
    }
  }
  customElements.define('d2l-my-complex-sortable-table-elem', MyComplexSortableTableElem);
</script>
<d2l-my-complex-sortable-table-elem></d2l-my-complex-sortable-table-elem>
```

## Selection

Table rows can support both single- and multi-select by leveraging [Selection](../../components/selection/) components.

To enable selection, add `d2l-selection-input` components in the selection column, and a `d2l-selection-select-all` component in the the column's header cell. Apply the `selected` attribute to `<tr>` row elements which are actively selected.

**Important:** Single selection tables won't need the Select All component in the header, so be sure to add an `aria-label` for screen reader users.

<!-- docs: demo -->
```html
<script type="module">
  import '@brightspace-ui/core/components/selection/selection-input.js';
  import '@brightspace-ui/core/components/selection/selection-select-all.js';
  import { html, LitElement } from 'lit';
  import { tableStyles } from '@brightspace-ui/core/components/table/table-wrapper.js';

  class SampleTableWithSelectionInputs extends LitElement {

    static properties = {
      selectionSingle: { type: Boolean, attribute: 'selection-single' },
      _data: { state: true }
    };

    static styles = tableStyles;

    constructor() {
      super();
      this.selectionSingle = false;
      this._data = [{ name: 'John Smith', checked: true }, { name: 'Emily Jones', checked: false }];
    }

    render() {
      return html`
        <d2l-table-wrapper ?selection-single="${this.selectionSingle}">
          <table class="d2l-table">
            <thead>
              <tr>
                ${!this.selectionSingle ? html`<th><d2l-selection-select-all></d2l-selection-select-all></th>` : html`<th aria-label="Selection column"></th>`}
                <th>Learner</th>
              </tr>
            </thead>
            <tbody>
              ${this._data.map((rowData, i) => html`
                <tr ?selected="${rowData.checked}">
                  <td>
                    <d2l-selection-input key="${i}" label="${rowData.name}" ?selected="${rowData.checked}" @d2l-selection-change="${this._selectRow}"></d2l-selection-input>
                  </td>
                  <td>${rowData.name}</td>
                </tr>
              `)}
            </tbody>
          </table>
        </d2l-table-wrapper>
      `;
    }

    _selectRow(e) {
      const key = e.target.key;
      this._data[key].checked = e.target.selected;
      this.requestUpdate();
    }

  }
  customElements.define('d2l-sample-table-with-selection-inputs', SampleTableWithSelectionInputs);
</script>
<d2l-sample-table-with-selection-inputs></d2l-sample-table-with-selection-inputs>
<d2l-sample-table-with-selection-inputs selection-single></d2l-sample-table-with-selection-inputs>
```

```html
<d2l-table-wrapper>
  <table class="d2l-table">
    <thead>
      <tr>
        <th><d2l-selection-select-all></d2l-selection-select-all></th>
        <th>Learner</th>
      </tr>
    </thead>
    <tbody>
        <tr selected>
          <td><d2l-selection-input key="1" label="John Smith" selected></d2l-selection-input></td>
          <td>John Smith</td>
        </tr>
        <tr>
          <td><d2l-selection-input key="2" label="Emily Jones"></d2l-selection-input></td>
          <td>Emily Jones</td>
        </tr>
    </tbody>
  </table>
</d2l-table-wrapper>
```

```html
<d2l-table-wrapper selection-single>
  <table class="d2l-table">
    <thead>
      <tr>
        <th aria-label="Selection column"></th>
        <th>Learner</th>
      </tr>
    </thead>
    ...
  </table>
</d2l-table-wrapper>  
```

## Pageable Tables

Load-More paging functionality can be implemented in tables by placing a `d2l-pager-load-more` in `d2l-table-wrapper`'s `pager` slot. The consumer must handle the `d2l-pager-load-more` event by loading more items, updating the pager state, and signalling completion by calling `complete()` on the event detail. Focus will be automatically moved on the first new item once complete. See [Paging](../../components/paging) for more details.

## Table Controls [d2l-table-controls]

The `d2l-table-controls` component can be placed in the `d2l-table-wrapper`'s `controls` slot to provide a selection summary, a slot for `d2l-selection-action`s, and overflow-group behaviour.

<!-- docs: demo code properties name:d2l-table-controls sandboxTitle:'Table Controls' display:block -->
```html
<script type="module">
  import '@brightspace-ui/core/components/selection/selection-action.js';
  import '@brightspace-ui/core/components/selection/selection-input.js';
  import '@brightspace-ui/core/components/selection/selection-select-all.js';
  import '@brightspace-ui/core/components/table/table-controls.js';
  import { html, LitElement } from 'lit';
  import { tableStyles } from '@brightspace-ui/core/components/table/table-wrapper.js';

  class SampleTableWithControls extends LitElement {

    static properties = {
      _data: { state: true }
    };

    static styles = tableStyles;

    constructor() {
      super();
      this._data = {
        a: { checked: false, notes: 'Scroll down to see sticky header behaviour.'  },
        b: { checked: false, notes: 'Reduce the width to see the control actions chomp.' },
      };
    }

    render() {
      return html`
        <d2l-table-wrapper>
          <d2l-table-controls slot="controls">
            <d2l-selection-action icon="tier1:edit" text="Edit" requires-selection></d2l-selection-action>
            <d2l-selection-action icon="tier1:delete" text="Delete" requires-selection></d2l-selection-action>
            <d2l-selection-action icon="tier1:gear" text="Settings"></d2l-selection-action>
            <d2l-selection-action icon="tier1:help" text="Help"></d2l-selection-action>
          </d2l-table-controls>
          <table class="d2l-table">
            <thead>
              <tr>
                <th style="width: 1px;"><d2l-selection-select-all></d2l-selection-select-all></th>
                <th>Notes</th>
              </tr>
            </thead>
            <tbody>
              ${Object.keys(this._data).map((key, i) => html`
                <tr ?selected="${this._data[key].checked}">
                  <td>
                    <d2l-selection-input key="${key}" label="${key}" ?selected="${this._data[key].checked}" @d2l-selection-change="${this._selectRow}"></d2l-selection-input>
                  </td>
                  <td>${this._data[key].notes}</td>
                </tr>
              `)}
            </tbody>
          </table>
        </d2l-table-wrapper>
      `;
    }

    _selectRow(e) {
      const key = e.target.key;
      this._data[key].checked = e.target.selected;
      this.requestUpdate();
    }

  }
  customElements.define('d2l-sample-table-with-controls', SampleTableWithControls);
</script>
<!-- docs: start hidden content -->
<style>
  #demo-element {
    margin-bottom: 300px;
    margin-top: 0;
  }
</style>
<!-- docs: end hidden content -->
<d2l-sample-table-with-controls></d2l-sample-table-with-controls>
```

<!-- docs: start hidden content -->
### Properties

| Property | Type | Description |
|---|---|---|
| `no-selection` | Boolean | Whether to render the selection summary |
| `no-sticky` | Boolean | Disables sticky positioning for the controls |
| `select-all-pages-allowed` | Boolean | Whether all pages can be selected |
<!-- docs: end hidden content -->

## Accessibility

The `d2l-table-wrapper` simply wraps an HTML table in order to add styles and functionality to that table. As such, it is important to use proper table markup. Recommendations are available in [this tutorial](https://www.w3.org/WAI/tutorials/tables/). Make sure to include column and row headings so data is meaningful to screen reader users.

The `d2l-table-col-sort-button` component was built with screen reader users at front-of-mind. Important features include:
- A `source-type` attribute which lets users specify the type of data in the column (e.g., 'words', 'dates', or 'numbers') in order to provide a more descriptive title to both screen reader and sighted users
- Changes to sort order and/or sort state are reflected in attributes such that they can be communicated to screen reader users if supported by the screen reader, which they are in most scenarios
- Usage of our accessible menu component for the multi-faceted sort case