Sorting

Recipe 11.4 Sort columns

Sample:

Scores Group A
Michael 27 Austria
Robert 7 Croatia
Andreas 53 Austria
Dominik 19 Croatia
David 21 Austria
Heinrich 12 Austria
Markus 14 Austria
Paul 42 Austria
Nicolas 3 Spain

Code:

<style>
  .visually-hidden {
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
    width: 1px;
  }

  td {
    min-width: 8rem;
  }

  caption {
    font-weight: bold;
  }

  .sort {
    all: unset;
    display: flex;
    gap: 0.4rem;
    align-items: center;
  }

  .sort path {
    fill: transparent;
    stroke: currentColor;
    stroke-width: 12;
  }

  [aria-sort="ascending"] path:first-child {
    fill: currentColor;
  }

  [aria-sort="descending"] path:last-child {
    fill: currentColor;
  }
</style>

<div role="status" class="visually-hidden"></div>

<table>
  <caption>Scores Group A</caption>
  <thead>
    <tr>
      <th>
        <button class="sort">
          Name
          <svg width="13" viewBox="0 0 126 171">
            <path d="M62.7 3.9 6 70l114-.5z"/>
            <path d="M63 166.5 6 100.6h114z"/>
          </svg>
        </button>
      </th>
      <th>
        <button class="sort">
          Score
          <svg width="13" viewBox="0 0 126 171">
            <path d="M62.7 3.9 6 70l114-.5z"/>
            <path d="M63 166.5 6 100.6h114z"/>
          </svg>
        </button>
      </th>
      <th>
        <button class="sort">
          Country
          <svg width="13" viewBox="0 0 126 171">
            <path d="M62.7 3.9 6 70l114-.5z"/>
            <path d="M63 166.5 6 100.6h114z"/>
          </svg>
        </button>
      </th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Michael</td>
      <td>27</td>
      <td>Austria</td>
    </tr>
    <tr>
      <td>Robert</td>
      <td>7</td>
      <td>Croatia</td>
    </tr>
    <tr>
      <td>Andreas</td>
      <td>53</td>
      <td>Austria</td>
    </tr>
    <tr>
      <td>Dominik</td>
      <td>19</td>
      <td>Croatia</td>
    </tr>
    <tr>
      <td>David</td>
      <td>21</td>
      <td>Austria</td>
    </tr>
    <tr>
      <td>Heinrich</td>
      <td>12</td>
      <td>Austria</td>
    </tr>
    <tr>
      <td>Markus</td>
      <td>14</td>
      <td>Austria</td>
    </tr>
    <tr>
      <td>Paul</td>
      <td>42</td>
      <td>Austria</td>
    </tr>
    <tr>
      <td>Nicolas</td>
      <td>3</td>
      <td>Spain</td>
    </tr>
  </tbody>
</table>

<script>
  const table = document.querySelector("table");
  const liveRegion = document.querySelector("[role='status']");
  let toSort;
  let direction = "ascending";

  table.addEventListener("click", (e) => {
    const button = e.target.closest("thead button");

    if (button) {
      const cell = button.parentNode;
      const tbody = table.querySelector("tbody");
      const rows = tbody.querySelectorAll("tr");

      toSort = [];
      getRows(cell, rows);
      updateButton(cell);
      sortRows(rows);
      updateLiveRegion();
    }
  });

  const getRows = (cell, rows) => {
    const index = [...cell.parentNode.children].indexOf(cell);

    for (let i = 0; i < rows.length; i++) {
      const row = rows[i];
      const cells = row.querySelectorAll("td");

      toSort.push([cells[index].innerText, row.cloneNode(true)]);
    }
  };

  const sortRows = (rows) => {
    toSort.sort(function (a, b) {
      const comp = a[0].localeCompare(b[0], "en", { numeric: true });
      return comp;
    });

    if (direction === "descending") {
      toSort.reverse();
    }

    for (let i = 0; i < rows.length; i++) {
      const row = rows[i];
      row.parentNode.replaceChild(toSort[i][1], row);
    }

    console.log(direction);
  };

  const updateButton = (cell) => {
    const sortedColumn = table.querySelector("[aria-sort]");
    if (sortedColumn && sortedColumn !== cell) {
      sortedColumn.removeAttribute("aria-sort");
    }

    direction =
      cell.getAttribute("aria-sort") === "ascending" ? "descending" : "ascending";
    cell.setAttribute("aria-sort", direction);
  };

  const updateLiveRegion = () => {
    liveRegion.textContent = `Sorted ${direction}`;

    setTimeout(() => {
      liveRegion.textContent = ``;
    }, 1000);
  };
</script>