Skip to content

Commit 3b5c24e

Browse files
committed
GitHub: Work-in-progress complete rewrite based on my new framework.
This is probably missing some functionality that was in the previous script. However, the previous script contains quite a bit of stuff that is no longer necessary, was broken in several places and was becoming very hard to maintain.
1 parent f04edee commit 3b5c24e

File tree

1 file changed

+160
-203
lines changed

1 file changed

+160
-203
lines changed

GitHubA11yFixes.user.js

Lines changed: 160 additions & 203 deletions
Original file line numberDiff line numberDiff line change
@@ -1,203 +1,160 @@
1-
// ==UserScript==
2-
// @name GitHub Accessibility Fixes
3-
// @namespace http://axSgrease.nvaccess.org/
4-
// @description Improves the accessibility of GitHub.
5-
// @author James Teh <jamie@nvaccess.org>
6-
// @copyright 2015-2016 NV Access Limited
7-
// @license GNU General Public License version 2.0
8-
// @version 2016.1
9-
// @grant GM_log
10-
// @include https://github.com/*
11-
// ==/UserScript==
12-
13-
function makeHeading(elem, level) {
14-
elem.setAttribute("role", "heading");
15-
elem.setAttribute("aria-level", level);
16-
}
17-
18-
function onSelectMenuItemChanged(target) {
19-
target.setAttribute("aria-checked", target.classList.contains("selected") ? "true" : "false");
20-
}
21-
22-
function onDropdownChanged(target) {
23-
target.firstElementChild.setAttribute("aria-haspopup", "true");
24-
var expanded = target.classList.contains("active");
25-
target.firstElementChild.setAttribute("aria-expanded", expanded ? "true" : "false");
26-
var items = target.children[1];
27-
if (!items) {
28-
return;
29-
}
30-
if (expanded) {
31-
items.removeAttribute("aria-hidden");
32-
// Focus the first item.
33-
var elem = items.querySelector("a,button");
34-
if (elem)
35-
elem.focus();
36-
} else {
37-
// Make sure the items are hidden.
38-
items.setAttribute("aria-hidden", "true");
39-
}
40-
}
41-
42-
// Used when we need to generate ids for ARIA.
43-
var idCounter = 0;
44-
45-
function onNodeAdded(target) {
46-
var elem;
47-
var res = document.location.href.match(/github.com\/[^\/]+\/[^\/]+(?:\/([^\/?]+))?(?:\/([^\/?]+))?(?:\/([^\/?]+))?(?:\/([^\/?]+))?/);
48-
// res[1] to res[4] are 4 path components of the URL after the project.
49-
// res[1] will be "issues", "pull", "commit", etc.
50-
// Empty path components will be undefined.
51-
if (["issues", "pull", "commit"].indexOf(res[1]) >= 0 && res[2]) {
52-
// Issue, pull request or commit.
53-
// Comment headers.
54-
for (elem of target.querySelectorAll(".timeline-comment-header-text, .discussion-item-header"))
55-
makeHeading(elem, 3);
56-
}
57-
if (res[1] == "commits" || (res[1] == "pull" && res[3] == "commits" && !res[4])) {
58-
// Commit listing.
59-
// Commit group headers.
60-
for (elem of target.querySelectorAll(".commit-group-title"))
61-
makeHeading(elem, 2);
62-
} else if ((res[1] == "commit" && res[2]) || (res[1] == "pull" && res[3] == "commits" && res[4])) {
63-
// Single commit.
64-
if (elem = target.querySelector(".commit-title"))
65-
makeHeading(elem, 2);
66-
} else if (res[1] == "blob") {
67-
// Viewing a single file.
68-
// Ensure the table never gets treated as a layout table.
69-
if (elem = target.querySelector(".js-file-line-container"))
70-
elem.setAttribute("role", "table");
71-
} else if (res[1] == "tree" || !res[1]) {
72-
// A file list is on this page.
73-
// Ensure the table never gets treated as a layout table.
74-
if (elem = target.querySelector(".files"))
75-
elem.setAttribute("role", "table");
76-
} else if (res[1] == "compare") {
77-
// Branch selector buttons.
78-
// These have an aria-label which masks the name of the branch, so kill it.
79-
for (elem of target.querySelectorAll("button.select-menu-button"))
80-
elem.removeAttribute("aria-label");
81-
}
82-
if (["pull", "commit"].indexOf(res[1]) >= 0 && res[2]) {
83-
// Pull request or commit.
84-
// Header for each changed file.
85-
for (elem of target.querySelectorAll(".file-info"))
86-
makeHeading(elem, 2);
87-
// Lines of code which can be commented on.
88-
for (elem of target.querySelectorAll(".add-line-comment")) {
89-
// Put the comment button after the code instead of before.
90-
// elem is the Add line comment button.
91-
elem.setAttribute("id", "axsg-alc" + idCounter);
92-
// nextElementSibling is the actual code.
93-
elem.nextElementSibling.setAttribute("id", "axsg-l" + idCounter);
94-
// Reorder children using aria-owns.
95-
elem.parentNode.setAttribute("aria-owns", "axsg-l" + idCounter + " axsg-alc" + idCounter);
96-
++idCounter;
97-
}
98-
// Make sure diff tables never get treated as a layout table.
99-
for (elem of target.querySelectorAll(".diff-table"))
100-
elem.setAttribute("role", "table");
101-
// Review comment headers.
102-
for (elem of target.querySelectorAll(".review-comment-contents > strong"))
103-
makeHeading(elem, 3);
104-
}
105-
106-
// Site-wide stuff.
107-
// Checkable menu items; e.g. in watch and labels pop-ups.
108-
if (target.classList.contains("select-menu-item")) {
109-
target.setAttribute("role", "menuitemcheckbox");
110-
onSelectMenuItemChanged(target);
111-
} else {
112-
for (elem of target.querySelectorAll(".select-menu-item")) {
113-
elem.setAttribute("role", "menuitemcheckbox");
114-
onSelectMenuItemChanged(elem);
115-
}
116-
}
117-
// Table lists; e.g. in issue and commit listings.
118-
for (elem of target.querySelectorAll(".table-list,.Box-body,ul.js-navigation-container"))
119-
elem.setAttribute("role", "table");
120-
for (elem of target.querySelectorAll(".table-list-item,.Box-body-row,.Box-row"))
121-
elem.setAttribute("role", "row");
122-
for (elem of target.querySelectorAll(".Box-body-row,.Box-row .d-table")) {
123-
// There's one of these inside every .Box-body-row/Box-row.
124-
// It's purely presentational.
125-
elem.setAttribute("role", "presentation");
126-
// Its children are the cells, but they have no common class.
127-
for (elem of elem.children)
128-
elem.setAttribute("role", "cell");
129-
}
130-
for (elem of target.querySelectorAll(".table-list-cell"))
131-
elem.setAttribute("role", "cell");
132-
// Tables in Markdown content get display: block, which causes them not to be treated as tables.
133-
for (elem of target.querySelectorAll(".markdown-body table"))
134-
elem.setAttribute("role", "table");
135-
for (elem of target.querySelectorAll(".markdown-body tr"))
136-
elem.setAttribute("role", "row");
137-
for (elem of target.querySelectorAll(".markdown-body th"))
138-
elem.setAttribute("role", "cell");
139-
for (elem of target.querySelectorAll(".markdown-body td"))
140-
elem.setAttribute("role", "cell");
141-
// Tooltipped links (e.g. authors and labels in issue listings) shouldn't get the tooltip as their label.
142-
for (elem of target.querySelectorAll("a.tooltipped")) {
143-
if (!elem.textContent || /^\s+$/.test(elem.textContent))
144-
continue;
145-
var tooltip = elem.getAttribute("aria-label");
146-
// This will unfortunately change the visual presentation.
147-
elem.setAttribute("title", tooltip);
148-
elem.removeAttribute("aria-label");
149-
}
150-
// Dropdowns; e.g. for "Add your reaction".
151-
if (target.classList && target.classList.contains("dropdown"))
152-
onDropdownChanged(target);
153-
else {
154-
for (elem of target.querySelectorAll(".dropdown"))
155-
onDropdownChanged(elem);
156-
}
157-
// Reactions.
158-
for (elem of target.querySelectorAll(".add-reactions-options-item"))
159-
elem.setAttribute("aria-label", elem.getAttribute("data-reaction-label"));
160-
for (elem of target.querySelectorAll(".user-has-reacted")) {
161-
var user = elem.getAttribute("aria-label");
162-
// This will unfortunately change the visual presentation.
163-
elem.setAttribute("title", user);
164-
elem.setAttribute("aria-label", user + " " + elem.getAttribute("value"));
165-
}
166-
}
167-
168-
function onClassModified(target) {
169-
var classes = target.classList;
170-
if (!classes)
171-
return;
172-
if (classes.contains("select-menu-item")) {
173-
// Checkable menu items; e.g. in watch and labels pop-ups.
174-
onSelectMenuItemChanged(target);
175-
} else if (classes.contains("dropdown")) {
176-
// Container for a dropdown.
177-
onDropdownChanged(target);
178-
}
179-
}
180-
181-
var observer = new MutationObserver(function(mutations) {
182-
for (var mutation of mutations) {
183-
try {
184-
if (mutation.type === "childList") {
185-
for (var node of mutation.addedNodes) {
186-
if (node.nodeType != Node.ELEMENT_NODE)
187-
continue;
188-
onNodeAdded(node);
189-
}
190-
} else if (mutation.type === "attributes") {
191-
if (mutation.attributeName == "class")
192-
onClassModified(mutation.target);
193-
}
194-
} catch (e) {
195-
// Catch exceptions for individual mutations so other mutations are still handled.
196-
GM_log("Exception while handling mutation: " + e);
197-
}
198-
}
199-
});
200-
observer.observe(document, {childList: true, attributes: true,
201-
subtree: true, attributeFilter: ["class"]});
202-
203-
onNodeAdded(document);
1+
// ==UserScript==
2+
// @name GitHub Accessibility Fixes
3+
// @namespace http://axSgrease.nvaccess.org/
4+
// @description Improves the accessibility of GitHub.
5+
// @author James Teh <jteh@mozilla.com>
6+
// @copyright 2019 Mozilla Corporation, Derek Riemer
7+
// @license Mozilla Public License version 2.0
8+
// @version 2019.1
9+
// @include https://github.com/*
10+
// ==/UserScript==
11+
12+
/*** Functions for common tweaks. ***/
13+
14+
function makeHeading(el, level) {
15+
el.setAttribute("role", "heading");
16+
el.setAttribute("aria-level", level);
17+
}
18+
19+
function makeRegion(el, label) {
20+
el.setAttribute("role", "region");
21+
el.setAttribute("aria-label", label);
22+
}
23+
24+
function makeButton(el, label) {
25+
el.setAttribute("role", "button");
26+
el.setAttribute("aria-label", label);
27+
}
28+
29+
function makePresentational(el) {
30+
el.setAttribute("role", "presentation");
31+
}
32+
33+
function setLabel(el, label) {
34+
el.setAttribute("aria-label", label);
35+
}
36+
37+
function makeHidden(el) {
38+
el.setAttribute("aria-hidden", "true");
39+
}
40+
41+
function setExpanded(el, expanded) {
42+
el.setAttribute("aria-expanded", expanded ? "true" : "false");
43+
}
44+
45+
var idCounter = 0;
46+
// Get a node's id. If it doesn't have one, make and set one first.
47+
function setAriaIdIfNecessary(elem) {
48+
if (!elem.id) {
49+
elem.setAttribute("id", "axsg-" + idCounter++);
50+
}
51+
return elem.id;
52+
}
53+
54+
function makeElementOwn(parentElement, listOfNodes){
55+
ids = [];
56+
for(let node of listOfNodes){
57+
ids.push(setAriaIdIfNecessary(node));
58+
}
59+
parentElement.setAttribute("aria-owns", ids.join(" "));
60+
}
61+
62+
/*** Code to apply the tweaks when appropriate. ***/
63+
64+
function applyTweak(el, tweak) {
65+
if (Array.isArray(tweak.tweak)) {
66+
let [func, ...args] = tweak.tweak;
67+
func(el, ...args);
68+
} else {
69+
tweak.tweak(el);
70+
}
71+
}
72+
73+
function applyTweaks(root, tweaks, checkRoot) {
74+
for (let tweak of tweaks) {
75+
for (let el of root.querySelectorAll(tweak.selector)) {
76+
applyTweak(el, tweak);
77+
}
78+
if (checkRoot && root.matches(tweak.selector)) {
79+
applyTweak(root, tweak);
80+
}
81+
}
82+
}
83+
84+
let observer = new MutationObserver(function(mutations) {
85+
for (let mutation of mutations) {
86+
try {
87+
if (mutation.type === "childList") {
88+
for (let node of mutation.addedNodes) {
89+
if (node.nodeType != Node.ELEMENT_NODE) {
90+
continue;
91+
}
92+
applyTweaks(node, DYNAMIC_TWEAKS, true);
93+
}
94+
} else if (mutation.type === "attributes") {
95+
applyTweaks(mutation.target, DYNAMIC_TWEAKS, true);
96+
}
97+
} catch (e) {
98+
// Catch exceptions for individual mutations so other mutations are still handled.
99+
console.log("Exception while handling mutation: " + e);
100+
}
101+
}
102+
});
103+
104+
function init() {
105+
applyTweaks(document, LOAD_TWEAKS, false);
106+
applyTweaks(document, DYNAMIC_TWEAKS, false);
107+
options = {childList: true, subtree: true};
108+
if (DYNAMIC_TWEAK_ATTRIBS.length > 0) {
109+
options.attributes = true;
110+
options.attributeFilter = DYNAMIC_TWEAK_ATTRIBS;
111+
}
112+
observer.observe(document, options);
113+
}
114+
115+
/*** Define the actual tweaks. ***/
116+
117+
// Tweaks that only need to be applied on load.
118+
const LOAD_TWEAKS = [
119+
];
120+
121+
// Attributes that should be watched for changes and cause dynamic tweaks to be
122+
// applied. For example, if there is a dynamic tweak which handles the state of
123+
// a check box and that state is determined using an attribute, that attribute
124+
// should be included here.
125+
const DYNAMIC_TWEAK_ATTRIBS = [];
126+
127+
// Tweaks that must be applied whenever a node is added/changed.
128+
const DYNAMIC_TWEAKS = [
129+
// Lines of code which can be commented on.
130+
{selector: '.add-line-comment',
131+
tweak: el => {
132+
// Put the comment button after the code instead of before.
133+
// el is the Add line comment button.
134+
// nextElementSibling is the actual code.
135+
makeElementOwn(el.parentNode, [el.nextElementSibling, el]);
136+
}},
137+
// Make non-comment events into headings; e.g. closing/referencing an issue,
138+
// approving/requesting changes to a PR, merging a PR. Exclude commits and
139+
// commit references because these contain too much detail and there's no
140+
// way to separate the header from the body.
141+
{selector: '.TimelineItem:not(.js-commit) .TimelineItem-body:not(.my-0):not([id^="ref-commit-"])',
142+
tweak: [makeHeading, 3]},
143+
// Table lists; e.g. in issue and commit listings.
144+
{selector: '.js-navigation-container',
145+
tweak: el => el.setAttribute("role", "table")},
146+
{selector: '.Box-row',
147+
tweak: el => el.setAttribute("role", "row")},
148+
{selector: '.Box-row .d-table',
149+
tweak: el => {
150+
// There's one of these inside every row. It's purely presentational.
151+
makePresentational(el);
152+
// Its children are the cells, but they have no common class.
153+
for (let cell of el.children) {
154+
cell.setAttribute("role", "cell");
155+
}
156+
}},
157+
];
158+
159+
/*** Lights, camera, action! ***/
160+
init();

0 commit comments

Comments
 (0)