-
Notifications
You must be signed in to change notification settings - Fork 17
/
TrelloA11yFixes.user.js
180 lines (170 loc) · 5.49 KB
/
TrelloA11yFixes.user.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
// ==UserScript==
// @name Trello Accessibility Fixes
// @namespace http://axSgrease.nvaccess.org/
// @description Improves the accessibility of Trello.
// @author James Teh <[email protected]>
// @copyright 2017 NV Access Limited
// @license GNU General Public License version 2.0
// @version 2017.1
// @grant GM_log
// @include https://trello.com/*
// ==/UserScript==
// Used when we need to generate ids for ARIA.
var idCounter = 0;
// Get a node's id. If it doesn't have one, make and set one first.
function getAriaId(elem) {
if (elem.id) {
return elem.id;
}
elem.setAttribute("id", "axsg-" + idCounter++);
return elem.id;
}
function makeHeading(elem, level) {
elem.setAttribute("role", "heading");
elem.setAttribute("aria-level", level);
}
function tweakCard(card) {
// Make this a focusable list item.
card.setAttribute("tabindex", "-1");
card.setAttribute("role", "listitem");
}
// Make checklists accessible.
function tweakCheckItem(checkItem, isNew) {
var checkbox = checkItem.querySelector(".checklist-item-checkbox-check");
if (isNew) {
checkbox.setAttribute("role", "checkbox");
checkbox.setAttribute("tabindex", "-1");
var checkLabel = checkItem.querySelector(".checklist-item-details-text");
if (checkLabel) {
checkbox.setAttribute("aria-labelledby", getAriaId(checkLabel));
}
}
var complete = checkItem.classList.contains("checklist-item-state-complete");
checkbox.setAttribute("aria-checked", complete ? "true" : "false");
}
function tweakDueDateComplete(completeBadge, isNew) {
var checkbox = completeBadge.querySelector(".card-detail-badge-due-date-complete-box");
if (isNew) {
checkbox.setAttribute("role", "checkbox");
checkbox.setAttribute("tabindex", "-1");
checkbox.setAttribute("aria-label", "Complete");
}
var complete = completeBadge.classList.contains("is-due-complete");
checkbox.setAttribute("aria-checked", complete ? "true" : "false");
}
function onNodeAdded(target) {
if (target.classList.contains("list-card")) {
// A card just got added.
tweakCard(target);
return;
}
if (target.classList.contains("badge")) {
// Label badges.
var label = target.getAttribute("title");
// Include the badge count (if any) in the label.
label += target.textContent;
target.setAttribute("aria-label", label);
return;
}
if (target.id == "clipboard") {
// Pressing control focuses a contentEditable div for clipboard stuff,
// but this causes screen reader users to lose their position.
target.blur();
return;
}
if (target.classList.contains("checklist-item")) {
// A checklist item just got added.
tweakCheckItem(target, true);
return;
}
for (var list of target.querySelectorAll(".list")) {
list.setAttribute("role", "list");
var header = list.querySelector(".list-header-name");
if (header) {
// Label the list with its header.
list.setAttribute("aria-labelledby", getAriaId(header));
// Make the header's container into a heading.
makeHeading(header, 2);
}
}
for (var card of target.querySelectorAll(".list-card")) {
tweakCard(card);
}
for (var activityCreator of target.querySelectorAll(".phenom-creator")) {
// Make the creator of an activity item into a heading
// to facilitate quick jumping between activity items.
makeHeading(activityCreator, 4);
}
for (var checkItem of target.querySelectorAll(".checklist-item")) {
tweakCheckItem(checkItem, true);
}
for (var dueComplete of target.querySelectorAll(".card-detail-due-date-badge")) {
tweakDueDateComplete(dueComplete, true);
}
}
function onClassModified(target) {
var classes = target.classList;
if (!classes)
return;
if (classes.contains("active-card")) {
// When the active card changes, focus it.
target.focus();
} else if (classes.contains("checklist-item")) {
tweakCheckItem(target, false);
} else if (classes.contains("card-detail-due-date-badge")) {
tweakDueDateComplete(target, false);
}
}
var observer = new MutationObserver(function(mutations) {
for (var mutation of mutations) {
try {
if (mutation.type === "childList") {
for (var node of mutation.addedNodes) {
if (node.nodeType != Node.ELEMENT_NODE)
continue;
onNodeAdded(node);
}
} else if (mutation.type === "attributes") {
if (mutation.attributeName == "class")
onClassModified(mutation.target);
}
} catch (e) {
// Catch exceptions for individual mutations so other mutations are still handled.
GM_log("Exception while handling mutation: " + e);
}
}
});
observer.observe(document, {childList: true, attributes: true,
subtree: true, attributeFilter: ["class"]});
function moveCard() {
// Open the quick editor.
var op = document.querySelector(".active-card .list-card-operation");
if (!op) {
return;
}
op.click();
// Click the Move button.
var move = document.querySelector(".js-move-card");
if (!move) {
return;
}
move.click();
// Focus the list selector.
// This doesn't work if we don't delay it. Not quite sure why.
setTimeout(function() {
var sel = document.querySelector(".js-select-list");
if (!sel) {
return;
}
sel.focus();
}, 50);
}
// Add some keyboard shortcuts.
document.addEventListener("keydown", function(evt) {
if (document.activeElement.nodeName == "INPUT" || document.activeElement.nodeName == "TEXTAREA" || document.activeElement.isContentEditable) {
return false;
}
if (evt.key == "M") {
moveCard();
}
});