Table (Sortable)

This script allows a table to be sorted by the column headers, in ascending or descending order.



People, Ages, and Favorite Colors
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);
	  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) {
	    if (sortOrder === index) {
	      sortDirection = -sortDirection;
	      return rows.reverse();
	    } else {
	      sortOrder = index;
	      return rows.sort(function (a, b) {
	        a = Array.prototype.slice.call(a.children);
	        b = Array.prototype.slice.call(b.children);
	        var aVal = a[index].innerText;
	        var 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"></li>

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, with class="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 with class="deque-table-sortable", role="grid", and aria-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 acending or decending.
      • The header row of the table is created by adding a <thead> container. Make sure that the number of headers created matches the numner of rows.
        • Create a <tr> container with role="row".
          • For each header add a <th> container with role="columnheader" and scope="col". The javascript code automatically sorts the first column in decending order and adds aria-sort="decending" to the first header.
            • Within the header create a <button> container with class="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 with role="row".
          • If you would like to include a row header add a <th> container with scope="row" and role="rowheader". The inner text will appear within the data cell.
          • Every other data cell in this row is a <td> container with role="gridcell".
    • Create a <span> container with id="liveRegion" and aria-live="polite".