From 9352378d4c709c4d361e960a49f092278a9198e8 Mon Sep 17 00:00:00 2001 From: Jon Gunderson Date: Sun, 31 Oct 2021 12:30:19 -0500 Subject: [PATCH] Add Sortable table example (pull #2046) Adds example of a simple HTML table that includes buttons for sorting in some of the column headers and illustrates implementation of aria--sort. Co-authored-by: Matt King --- aria-practices.html | 5 +- cspell.json | 1 + examples/index.html | 8 +- examples/table/css/sortable-table.css | 101 ++++++++++++ examples/table/js/sortable-table.js | 167 +++++++++++++++++++ examples/table/sortable-table.html | 221 ++++++++++++++++++++++++++ examples/table/table.html | 3 +- test/tests/table_sortable-table.js | 48 ++++++ 8 files changed, 550 insertions(+), 4 deletions(-) create mode 100644 examples/table/css/sortable-table.css create mode 100644 examples/table/js/sortable-table.js create mode 100644 examples/table/sortable-table.html create mode 100644 test/tests/table_sortable-table.js diff --git a/aria-practices.html b/aria-practices.html index 81e8cd62c1..6a1f88bab8 100644 --- a/aria-practices.html +++ b/aria-practices.html @@ -2643,7 +2643,10 @@

Table

Examples

-

Table Example: ARIA table made using HTML div and span elements.

+
    +
  • Table Example: ARIA table made using HTML div and span elements.
  • +
  • Sortable Table Example: Basic HTML table that illustrates implementation of aria-sort in the headers of sortable columns.
  • +

Keyboard Interaction

diff --git a/cspell.json b/cspell.json index 62852d71ef..cbfcc3480f 100644 --- a/cspell.json +++ b/cspell.json @@ -181,6 +181,7 @@ "shizzle", "Shopify", "Smorgeni", + "sortability", "sourcecode", "Spinbuttons", "Starkrimson", diff --git a/examples/index.html b/examples/index.html index 0120d0d69e..ff03274da3 100644 --- a/examples/index.html +++ b/examples/index.html @@ -625,6 +625,7 @@

Examples By Properties and States

  • Date Picker Spin Button
  • Switch Using HTML Button (HC)
  • Switch (HC)
  • +
  • Sortable Table (HC)
  • Toolbar
  • @@ -823,7 +824,12 @@

    Examples By Properties and States

    aria-sort - Data Grid + + + aria-valuemax diff --git a/examples/table/css/sortable-table.css b/examples/table/css/sortable-table.css new file mode 100644 index 0000000000..7b9102a712 --- /dev/null +++ b/examples/table/css/sortable-table.css @@ -0,0 +1,101 @@ +.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; + position: relative; +} + +table.sortable th.no-sort { + padding-top: 0.35em; +} + +table.sortable th:nth-child(5) { + width: 10em; +} + +table.sortable th button { + position: absolute; + 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; +} diff --git a/examples/table/js/sortable-table.js b/examples/table/js/sortable-table.js new file mode 100644 index 0000000000..777c7e4b67 --- /dev/null +++ b/examples/table/js/sortable-table.js @@ -0,0 +1,167 @@ +/* + * This content is licensed according to the W3C Software License at + * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document + * + * File: sortable-table.js + * + * Desc: Adds sorting to a HTML data table that implements ARIA Authoring Practices + */ + +'use strict'; + +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)); + } + } + + 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'); + } + } + } + + 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') + ); + } else { + ch.setAttribute('aria-sort', 'descending'); + this.sortColumn( + columnIndex, + 'descending', + ch.classList.contains('num') + ); + } + } 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 +window.addEventListener('load', function () { + var sortableTables = document.querySelectorAll('table.sortable'); + for (var i = 0; i < sortableTables.length; i++) { + new SortableTable(sortableTables[i]); + } +}); diff --git a/examples/table/sortable-table.html b/examples/table/sortable-table.html new file mode 100644 index 0000000000..7f09ebe719 --- /dev/null +++ b/examples/table/sortable-table.html @@ -0,0 +1,221 @@ + + + + +Sortable Table Example | WAI-ARIA Authoring Practices 1.2 + + + + + + + + + + + + + + + + + +
    +

    Sortable Table Example

    +

    + The example below illustrates an implementation of the table design pattern for a table with sortable rows. + The example uses HTML table markup for all elements of the table structure, e.g., cells, rows, column headers, and caption. + The aria-sort attribute is set on the column header of the currently sorted column, and the header text of sortable columns is wrapped in a button element. + One column, the Address column is not sortable. +

    +

    Similar examples include:

    +
      +
    • Table Example: A table constructed using ARIA roles to convey table semantics.
    • +
    • Data Grid Examples: Three example implementations of grid that include features relevant to presenting tabular information, such as content editing, sort, and column hiding.
    • +
    + +
    +

    Example Option

    + +

    + Adds a diamond shaped icon (e.g. ) to the header of each column that can be sorted but is not currently sorted. + Some sortable tables add an icon to unsorted columns to help users distinguish sortable columns from columns that cannot be sorted. + It is important that the shape of the unsorted icon differ in more than just color and size from the icons that indicate sort direction (e.g. '▼' and '▲') so people with visual impairments can easily distinguish them. +

    +
    + +
    +
    +

    Example

    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Students currently enrolled in WAI-ARIA 101 + , column headers with buttons are sortable. +
    + + + + + + Address + +
    FredJacksonCanary, Inc.123 Broad St.56
    SaraJamesCardinal, Inc.457 First St.7
    RalphJeffersonRobin, Inc.456 Main St.513
    NancyJensenEagle, Inc.2203 Logan Dr.3.5
    +
    + +
    + +
    +

    Accessibility Features

    +
      +
    • + To help screen reader users understand the purpose of the buttons in the column headers, an off-screen description of the sort functionality of the buttons is appended to the caption text. + The description is added to the caption instead of to each button to prevent repetitious verbosity that could interfere with understanding of the column titles. +
    • +
    • To enhance perceivability when operating the sort buttons, visual keyboard focus and hover are styled using CSS :hover and :focus pseudo-classes: +
        +
      • To make it easier to perceive when a button has focus, the focus indicator encompasses both the column label and sort direction icon.
      • +
      • The cursor is changed to a pointer when hovering over the button to help people identify it as an interactive element.
      • +
      • To make it easier to perceive that clicking either the column label or the sort direction icon will sort the table, hover styles the button and icon in the same way that focus does.
      • +
      • To make activating sort easier for people with visual and movement impairments who are using a pointing device, the click target is maximized not only by making both the column label and sort icon clickable but also by using CSS positioning and sizing to make the button fill the entire header cell area.
      • +
      +
    • +
    • To ensure the sorting direction icons have sufficient contrast with the background when high contrast settings invert colors, character entities (e.g. '▼' and '▲') are used to indicate the sorting direction.
    • +
    +
    + +
    +

    Keyboard Support

    +

    Not applicable: The only interactive elements are HTML button elements, and all their keyboard functionality is provided by browsers.

    +
    + +
    +

    Role, Property, State, and Tabindex Attributes

    + + + + + + + + + + + + + + + + + + + + + + + +
    RoleAttributeElementUsage
    aria-sort="value"th +
      +
    • Set on the currently sorted column. When the sorted column is changed, the aria-sort attribute is removed and set on the newly sorted column.
    • +
    • A value of "ascending" indicates the data cells in the column are sorted in ascending order.
    • +
    • A value of "descending" indicates the data cells in the column are sorted in descending order.
    • +
    +
    aria-hidden="true"spanRemoves the character entities used for sort icons from the accessibility tree to prevent them from being included in the accessible name of the sort buttons.
    +
    + +
    +

    Javascript and CSS Source Code

    + +
    + +
    +

    HTML Source Code

    + +
    + + +
    +
    + + + diff --git a/examples/table/table.html b/examples/table/table.html index b28139bd64..9a07fc2b1b 100644 --- a/examples/table/table.html +++ b/examples/table/table.html @@ -32,9 +32,8 @@

    Table Example

    Similar examples include:

      -
    • Layout Grid Examples: Three example implementations of grids that are used to lay out widgets, including a collection of navigation links, a message recipients list, and a set of search results.
    • +
    • Sortable Table Example: Basic HTML table that illustrates implementation of aria-sort in the headers of sortable columns.
    • Data Grid Examples: Three example implementations of grid that include features relevant to presenting tabular information, such as content editing, sort, and column hiding.
    • -
    • Advanced Data Grid Example: Example of a grid with behaviors and features similar to a typical spreadsheet, including cell and row selection.
    diff --git a/test/tests/table_sortable-table.js b/test/tests/table_sortable-table.js new file mode 100644 index 0000000000..255c1e6860 --- /dev/null +++ b/test/tests/table_sortable-table.js @@ -0,0 +1,48 @@ +const { ariaTest } = require('..'); +// const { By, Key } = require('selenium-webdriver'); +const assertAttributeValues = require('../util/assertAttributeValues'); + +const exampleFile = 'table/sortable-table.html'; + +const ex = { + ariaSortSelector: '#ex1 table th[aria-sort]', + sortableColumnHeaderSelectors: [ + '#ex1 table th:nth-of-type(1)', + '#ex1 table th:nth-of-type(2)', + '#ex1 table th:nth-of-type(3)', + '#ex1 table th:nth-of-type(5)', + ], + spanSelector: '#ex1 table th[aria-sort] button span', +}; + +// Attributes + +ariaTest( + 'Visual character entity for the sort order is hidden from AT', + exampleFile, + 'span-aria-hidden', + async (t) => { + await assertAttributeValues(t, ex.spanSelector, 'aria-hidden', 'true'); + } +); + +ariaTest( + 'aria-sort value is updated when button is activated', + exampleFile, + 'th-aria-sort', + async (t) => { + for (let index = 0; index < 2; index++) { + const headerSelector = ex.sortableColumnHeaderSelectors[index]; + const buttonSelector = headerSelector + ' button'; + + const button = await t.context.queryElement(t, buttonSelector); + await button.click(); + + await assertAttributeValues(t, headerSelector, 'aria-sort', 'descending'); + + await button.click(); + + await assertAttributeValues(t, headerSelector, 'aria-sort', 'ascending'); + } + } +);