|
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