Skip to content

Commit 04b6c80

Browse files
alaric-rdalaricsp
andauthored
Table accessability (#131)
* Added some ARIA roles to the table in range_selector.njk * WIP: Remove tab handler * WIP: Dynamically move the tabindex around to preserve the focus when we tab out and back * Fix CellRef right/below logic so cursor movements work around merged cells * Fix #73 by tracking the *actual* end cell around when we shift+arrow key, rather than the *effective* end cell We were getting stuck in loops where the actual end cell was a cell that, due to merging, was expanding the selection further - and then trying to shift+arrow in the opposite direction to that extension put the end cell back in the original "actual" end cell, which then expanded out to put the selection exactly back where it was. * Tabbing in and out of the table now works and preserves the focus... ...but tabbing out destroys the selection, which means our use case of setting up a selection then tabbing to a submit button doesn't work. * Stopped tabbing out of a table from deleting the selection * Remove debug logging, fix indentation * Apply aria-selected attributes to grid cells, and aria-multiselectable to the table * Fix crtl+a (select all) * Comments and TODOs * Minimum width on table cells * WCAG 2.2 focused element has a solid border * Max column width, text wrapping --------- Co-authored-by: Alaric Snell-Pym <alaric@snell-pym.org.uk>
1 parent 2b39fe4 commit 04b6c80

File tree

4 files changed

+118
-28
lines changed

4 files changed

+118
-28
lines changed

fixtures/realistic.ods

1.97 KB
Binary file not shown.

lib/importer/assets/css/selectable_table.css

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@ table.selectable td {
2929
border-right-width: 1px;
3030
white-space: pre-wrap;
3131
font-variant-numeric: tabular-nums;
32+
min-width: 8em;
33+
max-width: 15em;
3234
}
3335

3436
table.selectable th *,
3537
table.selectable td * {
3638
box-sizing: border-box;
3739
font-family: inherit;
38-
cursor: inherit
40+
cursor: inherit;
3941
}
4042

4143
table.selectable th a,
@@ -237,6 +239,10 @@ table.selectable:not(.editable) tbody:empty::before {
237239
box-sizing: border-box
238240
}
239241

242+
table.selectable td.selected.focus {
243+
border: 2px solid blue;
244+
}
245+
240246
table.selectable tbody td,
241247
table.selectable tbody td.selected.focus {
242248
background-color: white

lib/importer/assets/js/selectable_table.js

Lines changed: 100 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
/*
2+
Accessability TODOs:
3+
4+
- Implement all of the data grid keyboard commands in https://www.w3.org/WAI/ARIA/apg/patterns/grid/: Page Up, Page Down, Home, End, Ctrl+Home, Ctrl+End, Ctrl+Space, Shift+Space
5+
6+
*/
7+
18
const get_platform = () => {
29
// userAgentData is not widely supported yet
310
if (typeof navigator.userAgentData !== 'undefined' && navigator.userAgentData != null) {
@@ -64,6 +71,48 @@ window.addEventListener("load", function() {
6471
.join("|") ;
6572

6673
let EventState = class {
74+
75+
/*
76+
A map of event states that you want to go full-screen to view because
77+
it's wide, but believe me, it's impossible to read when I wrote it as a
78+
tree:
79+
80+
| EventState | Event handlers active in this state | When we enter | When we leave |
81+
|-----------------------+---------------------------------------------------------------------------------------------------------------+------------------------------------------------------+------------------------------------------------------|
82+
| DocumentSelectMode | keydown -> function updateFromKeyEvent, function selectModeKeyboardShortcuts | function cellSelectMode | function cellEditMode |
83+
| | keyup -> function updateFromKeyEvent | function tableFocusIn | function loseFocus |
84+
| | click -> function loseFocus -> -DocumentSelectMode, +DocumentUnfocusedMode | function getFocus | |
85+
| | cut -> function cutSelection | | |
86+
| | copy -> function copySelection | | |
87+
| | paste -> function pasteSelection | | |
88+
|-----------------------+---------------------------------------------------------------------------------------------------------------+------------------------------------------------------+------------------------------------------------------|
89+
| DocumentEditMode | keydown -> function editModeKeyboardShortcuts | function cellEditMode | function cellSelectMode |
90+
|-----------------------+---------------------------------------------------------------------------------------------------------------+------------------------------------------------------+------------------------------------------------------|
91+
| DocumentUnfocusedMode | mousedown -> getFocus -> +DocumentSelectMode, -DocumentUnfocusedMode | function loseFocus | function tableFocusIn |
92+
| | | initialisation | function getFocus |
93+
|-----------------------+---------------------------------------------------------------------------------------------------------------+------------------------------------------------------+------------------------------------------------------|
94+
| TableSelectMode | selectstart -> function preventDefault | function cellSelectMode | function cellEditMode |
95+
| | mousedown -> function startDrag | function tableFocusIn | |
96+
| | | initialisation | |
97+
|-----------------------+---------------------------------------------------------------------------------------------------------------+------------------------------------------------------+------------------------------------------------------|
98+
| TableEditMode | | function cellEditMode | function cellSelectMode |
99+
|-----------------------+---------------------------------------------------------------------------------------------------------------+------------------------------------------------------+------------------------------------------------------|
100+
| TableUnfocusedMode | focusIn -> tableFocusIn -> -DocumentUnfocusedMode, -TableUnfocusedMode, +TableSelectMode, +DocumentSelectMode | function loseFocus | function tableFocusIn |
101+
| | | | function getFocus |
102+
|-----------------------+---------------------------------------------------------------------------------------------------------------+------------------------------------------------------+------------------------------------------------------|
103+
| InputSelectMode | mousedown -> function preventInputFocus, function removeFocus | function cellSelectMode (input elements inside cell) | function cellEditMode (input elements inside cell) |
104+
| | click -> function preventInputFocus | initialisation ("new row"/"new column" logic?!?) | |
105+
| | focus -> function enterCell | initialisation (all input elements in table) | |
106+
| | blur -> function leaveCell | | |
107+
|-----------------------+---------------------------------------------------------------------------------------------------------------+------------------------------------------------------+------------------------------------------------------|
108+
| InputEditMode | blur -> function leaveCell | function cellEditMode (input elements inside cell) | function cellSelectMode (input elements inside cell) |
109+
|-----------------------+---------------------------------------------------------------------------------------------------------------+------------------------------------------------------+------------------------------------------------------|
110+
| InputAliveState | input -> function autoResize, (function createNewRows/function createNewColumns?), function addRowClasses | initialisation ("new row"/"new column" logic?!?) | |
111+
| | change -> function autoResize, (function createNewRows/function createNewColumns?), function addRowClasses | initialisation (all input elements in table) | |
112+
|-----------------------+---------------------------------------------------------------------------------------------------------------+------------------------------------------------------+------------------------------------------------------|
113+
114+
*/
115+
67116
constructor(name) {
68117
this.name = name;
69118
this.events = [];
@@ -165,9 +214,9 @@ window.addEventListener("load", function() {
165214
};
166215

167216
get left() { return CellRef.fromCoords(this.table, this.row + 0, this.col - 1); }
168-
get right() { return CellRef.fromCoords(this.table, this.row + 0, this.col + 1); }
217+
get right() { return CellRef.fromCoords(this.table, this.row + 0, this.col + this.colspan); }
169218
get above() { return CellRef.fromCoords(this.table, this.row - 1, this.col + 0); }
170-
get below() { return CellRef.fromCoords(this.table, this.row + 1, this.col + 0); }
219+
get below() { return CellRef.fromCoords(this.table, this.row + this.rowspan, this.col + 0); }
171220

172221
equals(cell) {
173222
return this.table == cell.table &&
@@ -408,7 +457,6 @@ window.addEventListener("load", function() {
408457
// getRangeOfCells will do the CellRef.fromCoords lookup for every cell,
409458
// any of which might require a scan through the entire <table>.
410459

411-
412460
// Start by normalising min/max values
413461
let minRow = Math.min(this.actualStartCell.row, this.actualEndCell.row);
414462
let maxRow = Math.max(this.actualStartCell.row, this.actualEndCell.row);
@@ -605,6 +653,11 @@ window.addEventListener("load", function() {
605653

606654
var tables = document.querySelectorAll("table.selectable");
607655
for (var table of tables) {
656+
{
657+
const selectables = table.querySelectorAll('td');
658+
selectables[0].setAttribute("tabindex", 0); // First one is tabbable, to get the user into the table
659+
}
660+
608661
var selection = NilSelection;
609662

610663
const changeSelection = function(startCell, endCell) {
@@ -619,13 +672,25 @@ window.addEventListener("load", function() {
619672
for (var cell of oldCells) {
620673
for (var className of CellSelectedClasses) {
621674
cell.node.classList.remove(className);
675+
cell.node.setAttribute("aria-selected", "false");
622676
}
623677
}
624678

679+
// Should just be one, but let's be sure
680+
var oldSelectables = table.querySelectorAll("[tabindex=\"0\"]")
681+
for (var cell of oldSelectables) {
682+
cell.setAttribute("tabindex", -1);
683+
}
684+
625685
selection = newSelection;
626686
if (selection == NilSelection) { return; }
687+
selection.focus.node.focus();
688+
selection.focus.node.setAttribute("tabindex", 0);
627689
selection.focus.node.classList.add(CellSelectedFocusClassName);
628-
for (var cell of selection.cells) { cell.node.classList.add(CellSelectedClassName); }
690+
for (var cell of selection.cells) {
691+
cell.node.classList.add(CellSelectedClassName);
692+
cell.node.setAttribute("aria-selected", "truee");
693+
}
629694
for (var cell of selection.bottomCells) { cell.node.classList.add(CellSelectedBottomClassName); }
630695
for (var cell of selection.topCells) { cell.node.classList.add(CellSelectedTopClassName); }
631696
for (var cell of selection.leftCells) { cell.node.classList.add(CellSelectedLeftClassName); }
@@ -783,6 +848,7 @@ window.addEventListener("load", function() {
783848
}
784849

785850
var TableSelectMode = new EventState("select");
851+
var TableUnfocusedMode = new EventState("unfocused");
786852
var TableEditMode = new EventState("edit");
787853
var InputSelectMode = new EventState("select");
788854
var InputEditMode = new EventState("edit");
@@ -901,15 +967,16 @@ window.addEventListener("load", function() {
901967
}
902968

903969
var selectAll = function() {
904-
changeSelection(TableSelection.fromElement(table));
970+
applySelection(TableSelection.fromElement(table));
905971
}
906972

907973
// List of key codes that we think shouldn't trigger cell editing
908974
const ControlKeyCodes = Object.keys(KeyGroups)
909975
.filter(g => g != "Whitespace" && g != "IMEAndComposition")
910976
.map(k => KeyGroups[k])
911977
.reduce((acc, cur) => acc.concat(cur), [])
912-
.filter(k => k != "Backspace");
978+
.filter(k => k != "Backspace")
979+
.concat("Tab");
913980

914981
var valueKeyPressed = function(keyEvent) {
915982
return (!ControlKeyCodes.includes(keyEvent.key) && !keyEvent.ctrlKey && !keyEvent.metaKey);
@@ -925,14 +992,12 @@ window.addEventListener("load", function() {
925992
else if (!keyEvent.shiftKey && keyEvent.key == "ArrowRight") { changeSelection(selection.focus.right); keyEvent.preventDefault(); }
926993
else if (!keyEvent.shiftKey && keyEvent.key == "ArrowDown") { changeSelection(selection.focus.below); keyEvent.preventDefault(); }
927994
else if (!keyEvent.shiftKey && keyEvent.key == "ArrowUp") { changeSelection(selection.focus.above); keyEvent.preventDefault(); }
928-
else if (keyEvent.shiftKey && keyEvent.key == "ArrowLeft") { changeSelection(selection.focus, selection.endCell.left); keyEvent.preventDefault(); }
929-
else if (keyEvent.shiftKey && keyEvent.key == "ArrowRight") { changeSelection(selection.focus, selection.endCell.right); keyEvent.preventDefault(); }
930-
else if (keyEvent.shiftKey && keyEvent.key == "ArrowDown") { changeSelection(selection.focus, selection.endCell.below); keyEvent.preventDefault(); }
931-
else if (keyEvent.shiftKey && keyEvent.key == "ArrowUp") { changeSelection(selection.focus, selection.endCell.above); keyEvent.preventDefault(); }
995+
else if (keyEvent.shiftKey && keyEvent.key == "ArrowLeft") { changeSelection(selection.focus, selection.actualEndCell.left); keyEvent.preventDefault(); }
996+
else if (keyEvent.shiftKey && keyEvent.key == "ArrowRight") { changeSelection(selection.focus, selection.actualEndCell.right); keyEvent.preventDefault(); }
997+
else if (keyEvent.shiftKey && keyEvent.key == "ArrowDown") { changeSelection(selection.focus, selection.actualEndCell.below); keyEvent.preventDefault(); }
998+
else if (keyEvent.shiftKey && keyEvent.key == "ArrowUp") { changeSelection(selection.focus, selection.actualEndCell.above); keyEvent.preventDefault(); }
932999
else if (!keyEvent.shiftKey && keyEvent.key == "Enter") { applySelection(selection.focusCursor.nextFocusByColumn()); keyEvent.preventDefault(); }
933-
else if (!keyEvent.shiftKey && keyEvent.key == "Tab") { applySelection(selection.focusCursor.nextFocusByRow()); keyEvent.preventDefault(); }
9341000
else if (keyEvent.shiftKey && keyEvent.key == "Enter") { applySelection(selection.focusCursor.prevFocusByColumn()); keyEvent.preventDefault(); }
935-
else if (keyEvent.shiftKey && keyEvent.key == "Tab") { applySelection(selection.focusCursor.prevFocusByRow()); keyEvent.preventDefault(); }
9361001
else if (valueKeyPressed(keyEvent)) {
9371002
var input = selection.focus.node.querySelector(InputElementsSelector);
9381003
var event = new KeyboardEvent(keyEvent.type, keyEvent);
@@ -952,10 +1017,6 @@ window.addEventListener("load", function() {
9521017
cellSelectMode(selection.focus);
9531018
applySelection(selection.focusCursor.nextFocusByColumn());
9541019
}
955-
if (keyEvent.key == "Tab") {
956-
cellSelectMode(selection.focus);
957-
applySelection(selection.focusCursor.nextFocusByRow());
958-
}
9591020
if (keyEvent.key == "Escape") {
9601021
cellSelectMode(selection.focus);
9611022
}
@@ -986,14 +1047,14 @@ window.addEventListener("load", function() {
9861047
if (event.target.closest("table") !== table) {
9871048
DocumentSelectMode.leave(document);
9881049

989-
990-
// If we not set the table's data-persist-selection attribute to "true" then we will apply
991-
// the Nil selection when the table loses focus.
992-
if ( !table.dataset.persistSelection || table.dataset.persistSelection.toLowerCase() != "true") {
993-
applySelection(NilSelection);
994-
}
1050+
// If we not set the table's data-persist-selection attribute to "true" then we will apply
1051+
// the Nil selection when the table loses focus.
1052+
if ( !table.dataset.persistSelection || table.dataset.persistSelection.toLowerCase() != "true") {
1053+
applySelection(NilSelection);
1054+
}
9951055

9961056
DocumentUnfocusedMode.enter(document);
1057+
TableUnfocusedMode.enter(table);
9971058
}
9981059
}
9991060
DocumentSelectMode.addEvent("click", loseFocus);
@@ -1002,8 +1063,25 @@ window.addEventListener("load", function() {
10021063
elementsOutsideTable.snapshotItem(e).addEventListener("focus", loseFocus);
10031064
}
10041065

1066+
var tableFocusIn = function(event) {
1067+
var tableSelectables = table.querySelectorAll("[tabindex=\"0\"]")
1068+
if(tableSelectables[0]) {
1069+
// If we don't have a focus, set it to the externally-focussed cell
1070+
if(selection == NilSelection) {
1071+
selection = TableSelection.fromElement(tableSelectables[0]);
1072+
applySelection(selection);
1073+
}
1074+
}
1075+
DocumentUnfocusedMode.leave(document);
1076+
TableUnfocusedMode.leave(table);
1077+
TableSelectMode.enter(table);
1078+
DocumentSelectMode.enter(document);
1079+
}
1080+
TableUnfocusedMode.addEvent("focusin", tableFocusIn);
1081+
10051082
var getFocus = function(event) {
10061083
DocumentUnfocusedMode.leave(document);
1084+
TableUnfocusedMode.leave(table);
10071085
DocumentSelectMode.enter(document);
10081086
}
10091087
DocumentUnfocusedMode.addEvent("mousedown", getFocus);

lib/importer/nunjucks/importer/macros/range_selector.njk

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,21 @@
99
</div>
1010

1111
<div class="rd-range-selector">
12-
<table class="selectable govuk-body" data-persist-selection="true">
12+
<table class="selectable govuk-body"
13+
data-persist-selection="true"
14+
role="grid" aria-multiselectable="true"
15+
{% if caption %}
16+
aria-labelledby="tablecaption"
17+
{% endif %}
18+
>
1319
{% if caption %}
14-
<caption class="govuk-table__caption govuk-table__caption--m">{{caption}}</caption>
20+
<caption id="tablecaption" class="govuk-table__caption govuk-table__caption--m">{{caption}}</caption>
1521
{% endif %}
16-
<tbody>
22+
<tbody role="rowgroup">
1723
{% for row in rows %}
18-
<tr>
24+
<tr role="row">
1925
{% for cell in row %}
20-
<td
26+
<td aria-selected="false"
2127
{% if cell.colspan %}
2228
colspan="{{ cell.colspan }}"
2329
{% endif %}

0 commit comments

Comments
 (0)