Table (Sortable)
Table (Sortable)
This script allows a table to be sorted by the column headers, in ascending or descending order.
Please note: JAWS with Chrome browser is unable to announce the column heading association with row data
Turn on a screen reader to experience this example in action.
Index | Name | Age | Code | Favorite Color |
---|---|---|---|---|
2 | Hannah | 28 | LQdBF | Blue |
6 | Cyan | 35 | gskQw | Burgundy |
3 | Salim | 7 | yqU5U | Green |
4 | Greg | 45 | 2xdOH | Orange |
1 | Myk | 33 | 8piQT | Purple |
5 | Caitlin | 21 | jP9sa | Red |
HTML Source Code
<div class="table-wrap">
<table class="sortable" aria-readonly="true" role="grid" >
<caption>People, Ages, and Favorite Colors</caption>
<thead>
<tr>
<th class="num">Index</th>
<th aria-sort="ascending">Name</th>
<th class="num">Age</th>
<th class="no-sort">Code</th>
<th >Favorite Color</th>
</tr>
</thead>
<tbody>
<tr>
<td>2</td>
<td>Hannah</td>
<td class="num">28</td>
<td>LQdBF</td>
<td >Blue</td>
</tr>
<tr>
<td>6</td>
<td>Cyan</td>
<td class="num">35</td>
<td>gskQw</td>
<td >Burgundy</td>
</tr>
<tr>
<td>3</td>
<td>Salim</td>
<td class="num">7</td>
<td>yqU5U</td>
<td >Green</td>
</tr>
<tr>
<td>4</td>
<td>Greg</td>
<td class="num">45</td>
<td>2xdOH</td>
<td >Orange</td>
</tr>
<tr>
<td>1</td>
<td>Myk</td>
<td class="num">33</td>
<td>8piQT</td>
<td >Purple</td>
</tr>
<tr>
<td>5</td>
<td>Caitlin</td>
<td class="num">21</td>
<td>jP9sa</td>
<td >Red</td>
</tr>
</tbody>
</table>
</div>
JavaScript Source Code
var langText = {
"sortedByColumn": "sorted by column",
"ascending": "ascending",
"descending":"descending"
}
class SortableTable {
constructor(tableNode) {
this.tableNode = tableNode;
this.columnHeaders = tableNode.querySelectorAll('thead th');
this.sortColumns = [];
for (var i = 0; i < this.columnHeaders.length; i++) {
var ch = this.columnHeaders[i];
var buttonNode = ch.querySelector('button');
if (buttonNode) {
this.sortColumns.push(i);
buttonNode.setAttribute('data-column-index', i);
buttonNode.addEventListener('click', this.handleClick.bind(this));
}
else
{
if (ch.classList.contains("no-sort")) continue;
var buttonNode = document.createElement("button");
buttonNode.innerHTML = ch.innerHTML.trim();
buttonNode.setAttribute('data-column-index', i);
buttonNode.addEventListener('click', this.handleClick.bind(this));
var span = document.createElement("span");
span.setAttribute("aria-hidden", "true");
span.classList.add("sort-arrow");
buttonNode.appendChild(span);
ch.innerHTML ="";
ch.appendChild(buttonNode);
}
}
this.optionCheckbox = document.querySelector(
'input[type="checkbox"][value="show-unsorted-icon"]'
);
if (this.optionCheckbox) {
this.optionCheckbox.addEventListener(
'change',
this.handleOptionChange.bind(this)
);
if (this.optionCheckbox.checked) {
this.tableNode.classList.add('show-unsorted-icon');
}
}
var span = document.createElement("span");
span.setAttribute("aria-live", "polite");
span.classList.add("sr-announce");
this.tableNode.parentNode.appendChild(span);
}
setColumnHeaderSort(columnIndex) {
if (typeof columnIndex === 'string') {
columnIndex = parseInt(columnIndex);
}
for (var i = 0; i < this.columnHeaders.length; i++) {
var ch = this.columnHeaders[i];
var buttonNode = ch.querySelector('button');
if (i === columnIndex) {
var value = ch.getAttribute('aria-sort');
if (value === 'descending') {
ch.setAttribute('aria-sort', 'ascending');
this.sortColumn(
columnIndex,
'ascending',
ch.classList.contains('num')
);
var captionText = this.tableNode.caption.innerText;
if(captionText.includes("(") ) captionText = captionText.substring(0, captionText.indexOf("("));
captionText = captionText + " ("+ langText["sortedByColumn"]+ " '" + buttonNode.innerText +"' "+ langText["ascending"]+")";
this.tableNode.caption.innerHTML = captionText;
this.tableNode.parentNode.querySelector(".sr-announce").innerHTML = captionText;
} else {
ch.setAttribute('aria-sort', 'descending');
this.sortColumn(
columnIndex,
'descending',
ch.classList.contains('num')
);
var captionText = this.tableNode.caption.innerText;
if(captionText.includes("(") ) captionText = captionText.substring(0, captionText.indexOf("("));
captionText = captionText + " ("+ langText["sortedByColumn"]+ " '" + buttonNode.innerText +"' "+ langText["descending"]+")";
this.tableNode.caption.innerHTML = captionText;
this.tableNode.parentNode.querySelector(".sr-announce").innerHTML = captionText;
}
} else {
if (ch.hasAttribute('aria-sort') && buttonNode) {
ch.removeAttribute('aria-sort');
}
}
}
}
sortColumn(columnIndex, sortValue, isNumber) {
function compareValues(a, b) {
if (sortValue === 'ascending') {
if (a.value === b.value) {
return 0;
} else {
if (isNumber) {
return a.value - b.value;
} else {
return a.value < b.value ? -1 : 1;
}
}
} else {
if (a.value === b.value) {
return 0;
} else {
if (isNumber) {
return b.value - a.value;
} else {
return a.value > b.value ? -1 : 1;
}
}
}
}
if (typeof isNumber !== 'boolean') {
isNumber = false;
}
var tbodyNode = this.tableNode.querySelector('tbody');
var rowNodes = [];
var dataCells = [];
var rowNode = tbodyNode.firstElementChild;
var index = 0;
while (rowNode) {
rowNodes.push(rowNode);
var rowCells = rowNode.querySelectorAll('th, td');
var dataCell = rowCells[columnIndex];
var data = {};
data.index = index;
data.value = dataCell.textContent.toLowerCase().trim();
if (isNumber) {
data.value = parseFloat(data.value);
}
dataCells.push(data);
rowNode = rowNode.nextElementSibling;
index += 1;
}
dataCells.sort(compareValues);
// remove rows
while (tbodyNode.firstChild) {
tbodyNode.removeChild(tbodyNode.lastChild);
}
// add sorted rows
for (var i = 0; i < dataCells.length; i += 1) {
tbodyNode.appendChild(rowNodes[dataCells[i].index]);
}
}
/* EVENT HANDLERS */
handleClick(event) {
var tgt = event.currentTarget;
this.setColumnHeaderSort(tgt.getAttribute('data-column-index'));
}
handleOptionChange(event) {
var tgt = event.currentTarget;
if (tgt.checked) {
this.tableNode.classList.add('show-unsorted-icon');
} else {
this.tableNode.classList.remove('show-unsorted-icon');
}
}
}
// Initialize sortable table buttons
var sortableTable
window.addEventListener('load', function () {
sortableTable = new SortableTable(document.querySelector('table.sortable'));
sortableTable.setColumnHeaderSort(1);
});
//This component has been adapted from an example provided by the W3C, in accordance with the W3C Software and Document License https://www.w3.org/copyright/software-license-2023/
CSS Source Code
.sr-only {
position: absolute;
top: -30em;
}
table.sortable td,
table.sortable th {
padding: 0.125em 0.25em;
width: 8em;
}
table.sortable th {
font-weight: bold;
border-bottom: thin solid #888;
background-color: #aaebea;
position: relative;
padding: 3px;;
}
table.sortable th.no-sort {
padding-top: 0.35em;
}
table.sortable th:nth-child(5) {
width: 10em;
}
table.sortable th button {
padding: 4px;
margin: 1px;
font-size: 100%;
font-weight: bold;
background: transparent;
border: none;
display: inline;
right: 0;
left: 0;
top: 0;
bottom: 0;
width: 100%;
text-align: left;
outline: none;
cursor: pointer;
}
table.sortable th button span {
position: absolute;
right: 4px;
}
table.sortable th[aria-sort="descending"] span::after {
content: "▼ ";
color: currentcolor;
font-size: 100%;
top: 0;
}
table.sortable th[aria-sort="ascending"] span::after {
content: "▲ ";
color: currentcolor;
font-size: 100%;
top: 0;
}
table.show-unsorted-icon th:not([aria-sort]) button span::after {
content: "♢";
color: currentcolor;
font-size: 100%;
position: relative;
top: -3px;
left: -4px;
}
table.sortable td.num {
text-align: right;
}
table.sortable tbody tr:nth-child(odd) {
background-color: #ddd;
}
/* Focus and hover styling */
table.sortable th button:focus,
table.sortable th button:hover {
padding: 2px;
border: 2px solid currentcolor;
background-color: #e5f4ff;
}
table.sortable th button:focus span,
table.sortable th button:hover span {
right: 2px;
}
table.sortable th:not([aria-sort]) button:focus span::after,
table.sortable th:not([aria-sort]) button:hover span::after {
content: "▼";
color: currentcolor;
font-size: 100%;
top: 0;
}
.sort-arrow {
margin-right: 3px;
}
.sr-announce{
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}