Table (Sortable)
Table (Sortable)
This script allows a table to be sorted by the column headers, in ascending or descending order.
Turn on a screen reader to experience this example in action.
1 | Myk | 33 | Purple |
---|---|---|---|
2 | Hannah | 28 | Blue |
3 | Salim | 7 | Green |
4 | Greg | 45 | Orange |
5 | Caitlin | 21 | Red |
6 | Cyan | 35 | Burgundy |
Initial HTML Markup
<div class="deque-table-sortable-group">
<table class="deque-table-sortable" role="grid" aria-readonly="true">
<caption>People, Ages, and Favorite Colors</caption>
<thead>
<tr role="row">
<th role="columnheader" scope="col">
<button class="sortableColumnLabel">
"Index"
</button>
</th>
<th role="columnheader" scope="col">
<button class="sortableColumnLabel">
"Name"
</button>
</th>
<th role="columnheader" scope="col">
<button class="sortableColumnLabel">
"Age"
</button>
</th>
<th role="columnheader" scope="col">
<button class="sortableColumnLabel">
"Favorite Color"
</button>
</th>
</tr>
</thead>
<tbody>
<tr role="row">
<th scope="row" role="rowheader">1</th>
<td role="gridcell">Myk</td>
<td role="gridcell">33</td>
<td role="gridcell">Purple</td>
</tr>
<tr role="row">
<th scope="row" role="rowheader">2</th>
<td role="gridcell">Hannah</td>
<td role="gridcell">28</td>
<td role="gridcell">Blue</td>
</tr>
<tr role="row">
<th scope="row" role="rowheader">3</th>
<td role="gridcell">Salim</td>
<td role="gridcell">7</td>
<td role="gridcell">Green</td>
</tr>
<tr role="row">
<th scope="row" role="rowheader">4</th>
<td role="gridcell">Greg</td>
<td role="gridcell">45</td>
<td role="gridcell">Orange</td>
</tr>
<tr role="row">
<th scope="row" role="rowheader">5</th>
<td role="gridcell">Caitlin</td>
<td role="gridcell">21</td>
<td role="gridcell">Red</td>
</tr>
<tr role="row">
<th scope="row" role="rowheader">6</th>
<td role="gridcell">Cyan</td>
<td role="gridcell">35</td>
<td role="gridcell">Burgundy</td>
</tr>
</tbody>
</table>
<span id="liveRegion" aria-live="polite" readCaptions="false"></span>
</div>
JavaScript
Required: The complete JavaScript file (for all patterns in the library): deque-patterns.min.js
Key parts of the JavaScript file
Note: The code below is functional only in the context of the complete JavaScript file.
In the @table-sortable section:
function createSortableTable(tableGroup) {
var table = tableGroup.querySelector('table');
var headerGroup = table.querySelector('thead');
var headerRow = headerGroup.querySelector('tr');
var headers = headerRow.querySelectorAll('th');
var rowGroup = table.querySelector('tbody');
var rows = rowGroup.querySelectorAll('tr');
var captionElement = table.querySelector('caption');
var caption = captionElement.innerText;
var liveRegion = tableGroup.querySelector('#liveRegion');
var readCaptions = liveRegion.getAttribute('readCaptions');
if (readCaptions === null) {
readCaptions = false;
}
liveRegion.classList.add('deque-visuallyhidden');
liveRegion.notify = function (text) {
liveRegion.innerHTML = text;
};
var sortOrder = null;
var sortDirection = 1;
function getSortHeader() {
if (sortOrder === null) {
return null;
}
return headerRow.children[sortOrder];
}
function getSortLabel() {
var header = getSortHeader();
if (!header) {
return null;
}
return header.innerText;
}
function getSortDirection() {
return sortDirection > 0 ? 'ascending' : 'descending';
}
function getSortInfo() {
if (sortOrder === null) {
return 'unsorted';
}
return 'sorted by ' + getSortLabel() + ', ' + getSortDirection();
}
function renderSorting() {
updateCaption();
updateAriaSort();
updateLiveRegion();
}
function updateAriaSort() {
for (var i = 0; i < headerRow.children.length; i++) {
var child = headerRow.children[i];
if (sortOrder !== null && i === Math.abs(sortOrder)) {
var direction = getSortDirection();
child.setAttribute('aria-sort', direction);
} else {
child.removeAttribute('aria-sort');
}
}
}
function updateCaption() {
var captionText = caption + ', ' + getSortInfo();
captionElement.innerText = captionText;
}
function updateLiveRegion() {
if (readCaptions) {
var captionText = 'Table ' + caption + ' is now ' + getSortInfo();
liveRegion.notify(captionText);
}
}
rows = Array.prototype.slice.call(rows);
var isValid = rows.every(function (row) {
return row.children.length === headers.length;
});
if (!isValid) {
throw new Error('Each row must be the same length as the headers row.');
}
headers = Array.prototype.slice.call(headers);
[].slice.call(headers).forEach(function (header, i) {
createHeaderCell(header, function (e) {
e.preventDefault();
rows = sortByIndex(rows, i);
table.renderData(rows);
});
});
table.renderData = function (rows) {
rowGroup.innerHTML = toHTML(rows);
renderSorting();
};
table.renderData(rows);
function sortByIndex(rows, index) {
rows = tableGroup.querySelectorAll('tbody tr');
rows = [].slice.call(rows);
if (sortOrder === index) {
sortDirection = -sortDirection;
return rows.reverse();
} else {
sortDirection = 1;
sortOrder = index;
return rows.sort(function (a, b) {
a = Array.prototype.slice.call(a.children);
b = Array.prototype.slice.call(b.children);
var aVal = null;
var bVal = null;
if (a[index]) {
aVal = a[index].innerText;
}
if (b[index]) {
bVal = b[index].innerText;
}
if (!isNaN(parseInt(aVal)) && !isNaN(parseInt(bVal))) {
if (parseInt(aVal) < parseInt(bVal)) {
return -1;
}
if (parseInt(aVal) > parseInt(bVal)) {
return 1;
}
return 0;
} else {
if (aVal < bVal) {
return -1;
}
if (aVal > bVal) {
return 1;
}
return 0;
}
});
}
}
var firstOne = table.querySelector('.sortableColumnLabel');
if (firstOne) {
firstOne.click();
} // give the table a default sort...
}
function createHeaderCell(header, handler) {
var button = header.querySelector('button');
button.setAttribute('tabindex', '0');
button.addEventListener('click', handler);
}
function toHTML(rows) {
return rows.map(function (row) {
row = Array.prototype.slice.call(row.children);
return '<tr role="row">\n ' + row.map(function (item, index) {
if (index === 0) {
return '<th scope="row" role="rowheader">' + item.innerText + '</th>';
}
return '<td role="gridcell">' + item.innerText + '</td>';
}).join('') + '</tr>';
}).join('');
}
function activateAllSortableTables() {
var sortableTables = document.querySelectorAll('.deque-table-sortable-group');
for (var i = 0; i < sortableTables.length; i++) {
createSortableTable(sortableTables[i]);
}
}
activateAllSortableTables();
In the @containerUtils section:
function elementIsChildOfElement(child, potentialParent) {
while (child) {
if (child === potentialParent) {
return true;
}
child = child.parentNode;
}
return false;
}
function createFieldset(label) {
var fieldset = document.createElement('fieldset');
var legend = document.createElement('legend');
legend.classList.add('legend'); // for easy lookup regardless of mode
legend.id = (0, _guidUtils.generateGuid)();
legend.innerText = label;
fieldset.appendChild(legend);
return fieldset;
}
function createLiveRegion() {
var level = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'polite';
var classes = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
var output = document.createElement('span');
classes.forEach(function (c) {
return output.classList.add(c);
});
output.id = (0, _guidUtils.generateGuid)();
output.setAttribute('aria-live', level);
output.classList.add('deque-visuallyhidden');
output.innerText = '';
output.notify = function (text) {
// TODO: Clean this up...no need to extend the element prototype
while (output.firstChild) {
output.removeChild(output.firstChild);
}
var msg = document.createElement('div');
msg.innerHTML = text;
output.appendChild(msg);
};
return output;
}
In the @guidUtils section:
/*
note - not a true guid. I prepend 'g' because
the ID of an element cannot start with a numeral
*/
function generateGuid() {
var S4 = function S4() {
return ((1 + Math.random()) * 0x10000 | 0).toString(16).substring(1);
};
return 'g' + (S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4());
}
Note: No additional JavaScript initialization code is necessary for this pattern. All elements with class="deque-table-sortable-group"
will be initialized automatically by the external JavaScript file.
CSS
Required: The complete CSS file (for all patterns in the library): deque-patterns.min.css
Key styles within the CSS file (other styles may also be necessary):
.deque-table-sortable-group caption,
.deque-table-responsive caption {
margin-bottom: 15px;
}
.deque-table-sortable-group table,
.deque-table-responsive table {
border-collapse: collapse;
border-spacing: 0;
overflow: visible;
margin-top: 8px;
width: 100%;
}
.deque-table-sortable-group table thead,
.deque-table-responsive table thead {
border-bottom: 1px solid #e3e3e3;
}
.deque-table-sortable-group table thead th,
.deque-table-responsive table thead th {
position: relative;
line-height: 16px;
vertical-align: bottom;
font-weight: 400;
color: rgba(0, 0, 0, 0.6);
border-right: 1px solid #cccccc;
}
.deque-table-sortable-group table thead th:last-child,
.deque-table-responsive table thead th:last-child {
border-right: none;
}
.deque-table-sortable-group table thead th .sortableColumnLabel,
.deque-table-responsive table thead th .sortableColumnLabel {
width: 100%;
display: inline-block;
height: 100%;
outline: none;
cursor: pointer;
padding: 0 0 0 25px;
text-align: left;
}
.deque-table-sortable-group table thead th .sortableColumnLabel:focus,
.deque-table-responsive table thead th .sortableColumnLabel:focus,
.deque-table-sortable-group table thead th .sortableColumnLabel:active,
.deque-table-responsive table thead th .sortableColumnLabel:active {
outline: 1px dashed #000000;
}
.deque-table-sortable-group table thead th .sortableColumnLabel:hover,
.deque-table-responsive table thead th .sortableColumnLabel:hover {
outline: 1px dashed #767676;
}
.deque-table-sortable-group table thead th .sortableColumnLabel:after,
.deque-table-responsive table thead th .sortableColumnLabel:after {
content: '\E738';
font-family: 'mwf-glyphs';
position: absolute;
left: 6px;
font-size: 12px;
}
.deque-table-sortable-group table thead th button,
.deque-table-responsive table thead th button {
font-size: 11px;
line-height: 16px;
font-weight: 400;
color: rgba(0, 0, 0, 0.6);
background: transparent;
border: 0;
padding-left: 0;
}
.deque-table-sortable-group table thead th button:hover,
.deque-table-responsive table thead th button:hover {
color: rgba(0, 0, 0, 0.8);
margin: 0;
}
.deque-table-sortable-group table thead th button:focus,
.deque-table-responsive table thead th button:focus {
outline: 1px dashed rgba(0, 0, 0, 0.6);
margin: 0;
}
.deque-table-sortable-group table thead th button:active,
.deque-table-responsive table thead th button:active {
color: #000000;
outline: 1px solid transparent;
margin: 0;
}
.deque-table-sortable-group table thead th[colspan]:not([colspan='1']),
.deque-table-responsive table thead th[colspan]:not([colspan='1']) {
text-align: center;
}
.deque-table-sortable-group table thead th[aria-sort='descending'] .sortableColumnLabel:after,
.deque-table-responsive table thead th[aria-sort='descending'] .sortableColumnLabel:after {
content: '\E70D';
font-family: 'mwf-glyphs';
position: absolute;
left: 6px;
font-size: 12px;
}
.deque-table-sortable-group table thead th[aria-sort='ascending'] .sortableColumnLabel:after,
.deque-table-responsive table thead th[aria-sort='ascending'] .sortableColumnLabel:after {
content: '\E70E';
font-family: 'mwf-glyphs';
position: absolute;
left: 6px;
font-size: 12px;
}
.deque-table-sortable-group table thead:before,
.deque-table-responsive table thead:before {
content: '';
display: block;
width: 7px;
}
.deque-table-sortable-group table thead td,
.deque-table-responsive table thead td,
.deque-table-sortable-group table thead th,
.deque-table-responsive table thead th {
padding: 12px 0;
text-align: left;
}
.deque-table-sortable-group table tbody tr:nth-child(even),
.deque-table-responsive table tbody tr:nth-child(even) {
background-color: #eeeeee;
}
.deque-table-sortable-group table tbody td,
.deque-table-responsive table tbody td,
.deque-table-sortable-group table tbody th,
.deque-table-responsive table tbody th {
text-align: left;
padding: 7px;
border-right: 1px solid #cccccc;
}
.deque-table-sortable-group table tbody td:last-child,
.deque-table-responsive table tbody td:last-child,
.deque-table-sortable-group table tbody th:last-child,
.deque-table-responsive table tbody th:last-child {
border-right: none;
}
Fonts
Note: You will need to edit the src
for font-family:'mwf-glyphs'
in the external CSS file.
Implementation Instructions
Step 1: Add Dependencies
Add deque-patterns.min.css in the <head>
of the document.
<link rel="stylesheet" type="text/css" href="deque-patterns.min.css">
Add a script link to deque-patterns.min.js to the bottom of the page.
<script type="text/javascript" src="deque-patterns.min.js"></script>
Fonts: Font glyphs are required for this pattern (as currently styled). The CSS file refers to them in a folder called "_fonts" as a child element of the folder in which the CSS file resides. The fonts, from the Microsoft Web Framework (MWF) are available at these links:
Step 2: Add HTML
- Create a
<div>
or<span>
container, withclass="deque-table-sortable-group"
. Every container with this class is selected and activated by the javascript. This class is also for styling purposes. - Add a
<table>
container withclass="deque-table-sortable"
,role="grid"
, andaria-readonly="true"
. - Create a
<caption>
container with the inner text being the caption for your table. The javascript automatically adds the column chosen by the user and whether the table is ascending or descending. - The header row of the table is created by adding a
<thead>
container. Make sure that the number of headers created matches the number of rows. - Create a
<tr>
container withrole="row"
. - For each header add a
<th>
container withrole="columnheader"
andscope="col"
. The javascript code automatically sorts the first column in descending order and addsaria-sort="descending"
to the first header. - Within the header create a
<button>
container withclass="sortableColumnLabel"
. The inner text will be the header title. - To make the main content of the table add a
<tbody>
container - For each row create a
<tr>
container withrole="row"
. - If you would like to include a row header add a
<th>
container withscope="row"
androle="rowheader"
. The inner text will appear within the data cell. - Every other data cell in this row is a
<td>
container withrole="gridcell"
. - Create a
<span>
container withid="liveRegion"
andaria-live="polite"
.