├── .gitattributes ├── BugzillaA11yFixes.user.js ├── TelegramA11yFixes.user.js ├── MonorailA11yFixes.user.js ├── ReviewBoardA11yFixes.user.js ├── framework ├── readme.md └── axSGreaseSkeleton.js ├── GoogleDocsA11yFixes.user.js ├── JiraA11yFixes.user.js ├── PhabricatorA11yFixes.user.js ├── GreenhouseA11yFixes.user.js ├── PandoraA11yFixes.user.js ├── readme.md ├── AppleMusicA11yFixes.user.js ├── PocketbookA11yFixes.user.js ├── SchedA11yFixes.user.js ├── TrelloA11yFixes.user.js ├── VentraIPControl.user.js ├── GoogleKeepA11yFixes.user.js ├── GitHubA11yFixes.user.js ├── ExpensifyA11yFixes.user.js ├── MessengerA11yFixes.user.js ├── AsusRouterA11yFixes.user.js └── WhatsAppA11yFixes.user.js /.gitattributes: -------------------------------------------------------------------------------- 1 | .gitattributes text 2 | *.md text 3 | *.user.js text 4 | -------------------------------------------------------------------------------- /BugzillaA11yFixes.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Bugzilla Accessibility Fixes 3 | // @namespace http://www.jantrid.net/axSGrease/ 4 | // @description Improves the accessibility of Bugzilla. 5 | // @author James Teh 6 | // @copyright 2014 James Teh 7 | // @license GNU General Public License version 2.0 8 | // @version 2014.1.1 9 | // @include */show_bug.cgi?* 10 | // @include */process_bug.cgi 11 | // ==/UserScript== 12 | 13 | function makeHeading(elem, level) { 14 | elem.setAttribute("role", "heading"); 15 | elem.setAttribute("aria-level", level); 16 | } 17 | 18 | function tweak() { 19 | var elem = document.getElementById("short_desc_nonedit_display"); 20 | if (!elem) 21 | return; // Not a Bugzilla bug. 22 | // Bug title. 23 | makeHeading(elem, 1); 24 | 25 | // Attachments heading. 26 | if (elem = document.getElementById("attachment_table")) 27 | makeHeading(elem.rows[0].cells[0], 2); 28 | 29 | // Comment numbers. 30 | for (elem of document.getElementsByClassName("bz_comment_number")) 31 | makeHeading(elem, 2); 32 | 33 | // Label user images. 34 | for (elem of document.querySelectorAll("span.bz_comment_user a img")) 35 | elem.setAttribute("alt", "User image"); 36 | } 37 | 38 | tweak(); 39 | -------------------------------------------------------------------------------- /TelegramA11yFixes.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Telegram Accessibility Fixes 3 | // @namespace http://axSGrease.nvaccess.org/ 4 | // @description Improves the accessibility of Telegram. 5 | // @author Michael Curran 6 | // @copyright 2017 NV Access Limited 7 | // @license GNU General Public License version 2.0 8 | // @version 2017.1 9 | // @grant GM_log 10 | // @include https://web.telegram.org/* 11 | // ==/UserScript== 12 | 13 | function init() { 14 | var elem; 15 | 16 | if (elem = document.querySelector(".im_history_messages_peer")) { 17 | // Chat history. 18 | elem.setAttribute("aria-live", "polite"); 19 | } 20 | } 21 | 22 | function onNodeAdded(target) { 23 | if(target.classList.contains('im_content_message_wrap')) { 24 | target.setAttribute("aria-live", "polite"); 25 | } 26 | } 27 | 28 | function onClassModified(target) { 29 | } 30 | 31 | var observer = new MutationObserver(function(mutations) { 32 | for (var mutation of mutations) { 33 | try { 34 | if (mutation.type === "childList") { 35 | for (var node of mutation.addedNodes) { 36 | if (node.nodeType != Node.ELEMENT_NODE) 37 | continue; 38 | onNodeAdded(node); 39 | } 40 | } else if (mutation.type === "attributes") { 41 | if (mutation.attributeName == "class") 42 | onClassModified(mutation.target); 43 | } 44 | } catch (e) { 45 | // Catch exceptions for individual mutations so other mutations are still handled. 46 | GM_log("Exception while handling mutation: " + e); 47 | } 48 | } 49 | }); 50 | observer.observe(document, {childList: true, attributes: true, 51 | subtree: true, attributeFilter: ["class"]}); 52 | 53 | init(); 54 | -------------------------------------------------------------------------------- /MonorailA11yFixes.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Monorail Accessibility Fixes 3 | // @namespace http://axSGrease.nvaccess.org/ 4 | // @description Improves the accessibility of Google Code. 5 | // @author James Teh 6 | // @copyright 2016-2017 NV Access Limited 7 | // @license GNU General Public License version 2.0 8 | // @version 2017.1 9 | // @include https://bugs.chromium.org/p/*/issues/* 10 | // ==/UserScript== 11 | 12 | function fixStar(node) { 13 | node.setAttribute("aria-label", node.getAttribute("title")); 14 | } 15 | 16 | function makeHeading(elem, level) { 17 | elem.setAttribute("role", "heading"); 18 | elem.setAttribute("aria-level", level); 19 | } 20 | 21 | function makeHeadings() { 22 | // Title. 23 | var elem = document.querySelector(".issueheader"); 24 | makeHeading(elem, 1); 25 | 26 | // Comments. 27 | for (elem of document.getElementsByClassName("issuecommentheader")) 28 | makeHeading(elem, 2); 29 | 30 | // Add a comment heading. 31 | var elem = document.querySelector("#makechanges div.h4"); 32 | makeHeading(elem, 2); 33 | } 34 | 35 | var observer = new MutationObserver(function(mutations) { 36 | mutations.forEach(function(mutation) { 37 | try { 38 | if (mutation.type === "attributes") { 39 | if (mutation.attributeName == "src" && mutation.target.id == "star") 40 | fixStar(mutation.target); 41 | } 42 | } catch (e) { 43 | // Catch exceptions for individual mutations so other mutations are still handled. 44 | GM_log("Exception while handling mutation: " + e); 45 | } 46 | }); 47 | }); 48 | observer.observe(document, {attributes: true, 49 | subtree: true, attributeFilter: ["src"]}); 50 | 51 | function initial() { 52 | var star = document.getElementById("star"); 53 | if (star) 54 | fixStar(star); 55 | makeHeadings(); 56 | } 57 | initial(); 58 | -------------------------------------------------------------------------------- /ReviewBoardA11yFixes.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Review Board Accessibility Fixes 3 | // @namespace http://axSGrease.nvaccess.org/ 4 | // @description Improves the accessibility of Review Board. 5 | // @author James Teh 6 | // @copyright 2016 NV Access Limited 7 | // @license GNU General Public License version 2.0 8 | // @version 2016.1 9 | // @grant GM_log 10 | // @include https://reviewboard.*/r/*/diff/* 11 | // ==/UserScript== 12 | 13 | function tweakSideBySide(side) { 14 | // Make the diff file name into a heading instead of a table header. 15 | // Among other things, this prevents it from being reported as a header for every cell. 16 | var elem = side.querySelector('thead th'); 17 | if (elem) { 18 | elem.setAttribute("role", "heading"); 19 | elem.setAttribute("aria-level", "2"); 20 | } 21 | // Similarly, don't treat the revision cells as headers. 22 | for (elem of side.querySelectorAll(".revision-col")) 23 | elem.setAttribute("role", "cell"); 24 | 25 | // For changed lines, prefix the right hand line number with off-screen text indicating the type of change. 26 | for (var tbody of side.querySelectorAll("tbody.insert,tbody.replace,tbody.delete")) { 27 | for (var th of tbody.querySelectorAll("tr th:nth-child(3)")) 28 | th.innerHTML = '' + tbody.className + ' ' + th.innerHTML; 29 | } 30 | } 31 | 32 | function onNodeAdded(target) { 33 | if (target.classList.contains("sidebyside")) 34 | tweakSideBySide(target); 35 | } 36 | 37 | var observer = new MutationObserver(function(mutations) { 38 | for (var mutation of mutations) { 39 | try { 40 | if (mutation.type === "childList") { 41 | for (var node of mutation.addedNodes) { 42 | if (node.nodeType != Node.ELEMENT_NODE) 43 | continue; 44 | onNodeAdded(node); 45 | } 46 | } 47 | } catch (e) { 48 | // Catch exceptions for individual mutations so other mutations are still handled. 49 | GM_log("Exception while handling mutation: " + e); 50 | } 51 | } 52 | }); 53 | observer.observe(document, {childList: true, subtree: true}); 54 | -------------------------------------------------------------------------------- /framework/readme.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | 1. Copy `axSGreaseSkeleton.js` to a new file with a `.user.js` extension; e.g. `SomeSiteA11yFixes.user.js`. 4 | 2. Edit the metadata at the top as appropriate, especially the `@name` and `@include` keys. 5 | 3. Configure and add tweaks in the section starting with the comment: "Define the actual tweaks." 6 | See below for more information about defining tweaks. 7 | 8 | You shouldn't need to edit anything in other sections of the file. 9 | However, you may wish to explore the functions in the section entitled "Functions for common tweaks." 10 | You can use these instead of writing your own functions for common scenarios. 11 | For example, `makeHeading` makes the target element into a heading with the specified level. 12 | 13 | # Defining Tweaks 14 | 15 | There are two arrays of tweaks: 16 | 17 | - `LOAD_TWEAKS`: Tweaks that only need to be applied on load. 18 | - `dynamic_tweaks`: Tweaks that must be applied whenever an element is added or when an observed attribute changes. 19 | 20 | The `DYNAMIC_TWEAK_ATTRIBS` array allows you to specify names of attributes which should be observed for changes. 21 | For example, if there is a dynamic tweak which handles the state of a check box and that state is determined using an attribute, that attribute should be included here. 22 | It is often necessary to observe the `class` attribute, as this often indicates changes to the state of a control. 23 | In some cases, it can be necessary to observe the `style` attribute if the site applies style changes directly to an element rather than via style sheets. 24 | if `DYNAMIC_TWEAK_ATTRIBS` is empty, no attributes will be observed. 25 | 26 | In the `LOAD_TWEAKS` and `DYNAMIC_TWEAKS` arrays, each tweak is an object with these keys: 27 | 28 | - `selector`: A CSS selector for the element(s) you want to tweak. 29 | - whenAttrChangedOnAncestor: Whether to apply this tweak when an attribute change occurs on an ancestor. 30 | By default, the tweak will apply for attribute changes on both the node itself, as well as for an attribute change on any ancestor. 31 | This can be problematic if, for example, you're using the style attribute to make a decision about focus, but the style changes on an ancestor. 32 | In that case, you can set this to false. 33 | - `tweak`: Either: 34 | 1. A function which is passed a single element to tweak. 35 | For example: 36 | * `tweak: makePresentationl` 37 | * `tweak: el => el.setAttribute("role", "cell")` 38 | 2. An array of `[func, ...args]`. 39 | The function will be passed an element, along with the arguments in the array. 40 | For example: 41 | 42 | `tweak: [makeHeading, 2]` 43 | 44 | will call `makeHeading(element, 2)`. 45 | -------------------------------------------------------------------------------- /GoogleDocsA11yFixes.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Google Docs Accessibility Fixes 3 | // @namespace http://www.jantrid.net/axSGrease/ 4 | // @description Improves the accessibility of Google Docs. 5 | // @author James Teh 6 | // @copyright 2011 James Teh 7 | // @license GNU General Public License version 2.0 8 | // @version 0.20111110.03 9 | // @include https://docs.google.com/* 10 | // ==/UserScript== 11 | 12 | function onNodeInserted(evt) { 13 | var target = evt.target; 14 | if (target.nodeType != Node.ELEMENT_NODE) 15 | return; 16 | if (target.getAttribute("id") == "docs-aria-speakable") { 17 | // The Google Docs live region has aria-relevant set to additions, 18 | // but the changes are actually text. 19 | // Therefore, remove aria-relevant. 20 | target.removeAttribute("aria-relevant"); 21 | } 22 | } 23 | 24 | document.addEventListener("DOMNodeInserted", onNodeInserted, false); 25 | 26 | var url = document.location.href; 27 | if (url.indexOf("/spreadsheet/ccc") != -1) { 28 | // In a spreadsheet, the input box always has focus, even when not editing. 29 | // This can cause screen readers to try to read text at the cursor when cursor keys are pressed. 30 | // Work around this by faking focus on a non-editable control when not editing. 31 | var inputBox = document.getElementsByTagName("textarea")[0]; 32 | inputBox.setAttribute("id", "input-box"); 33 | // Create our fake focus node. 34 | var gridFocus = document.createElement("div"); 35 | gridFocus.setAttribute("id", "grid-focus"); 36 | gridFocus.style.position = "-10000px"; 37 | gridFocus.setAttribute("role", "grid"); 38 | inputBox.parentNode.appendChild(gridFocus); 39 | 40 | function setInputBoxFocus() { 41 | // aria-hidden is set to true when not editing. 42 | if (inputBox.getAttribute("aria-hidden") == "true") 43 | inputBox.setAttribute("aria-activedescendant", "grid-focus"); 44 | else 45 | inputBox.setAttribute("aria-activedescendant", "input-box"); 46 | } 47 | 48 | function onInputBoxAttrModified(evt) { 49 | attrName = evt.attrName; 50 | if ((attrName == "aria-hidden" || attrName == "aria-activedescendant")) { 51 | // Editing has started or stopped. 52 | setInputBoxFocus(); 53 | } 54 | } 55 | 56 | setInputBoxFocus(); 57 | inputBox.addEventListener("DOMAttrModified", onInputBoxAttrModified, false); 58 | 59 | } else if (url.indexOf("/spreadsheet/gform?") != -1) { 60 | // Fix some buttons which have no content or role. 61 | const BUTTON_LABELS = { 62 | "ss-formwidget-edit-icon": "Edit", 63 | "ss-formwidget-duplicate-icon": "Duplicate", 64 | "ss-formwidget-delete-icon": "Delete", 65 | "ss-x-box": "Delete", 66 | } 67 | 68 | function fixButton(node) { 69 | var classes = node.getAttribute("class"); 70 | if (!classes) 71 | return; 72 | classes = classes.split(" "); 73 | for (var i = 0; i < classes.length; ++i) { 74 | var label; 75 | if (!(label = BUTTON_LABELS[classes[i]])) 76 | continue; 77 | node.setAttribute("role", "button"); 78 | node.setAttribute("aria-label", label); 79 | } 80 | } 81 | 82 | function onFormNodeInserted(evt) { 83 | var target = evt.target; 84 | if (target.nodeType != Node.ELEMENT_NODE) 85 | return; 86 | if (target.tagName != "DIV") 87 | return; 88 | var elements = target.getElementsByTagName("div"); 89 | for (var i = 0; i < elements.length; ++i) 90 | fixButton(elements[i]); 91 | elements = target.getElementsByTagName("span"); 92 | for (var i = 0; i < elements.length; ++i) 93 | fixButton(elements[i]); 94 | } 95 | 96 | document.addEventListener("DOMNodeInserted", onFormNodeInserted, false); 97 | var elements = document.getElementsByTagName("div"); 98 | for (var i = 0; i < elements.length; ++i) 99 | fixButton(elements[i]); 100 | } 101 | -------------------------------------------------------------------------------- /JiraA11yFixes.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Jira Accessibility Fixes 3 | // @namespace http://axSgrease.nvaccess.org/ 4 | // @description Improves the accessibility of Jira. 5 | // @author James Teh 6 | // @copyright 2019-2021 Mozilla Corporation, Derek Riemer 7 | // @license Mozilla Public License version 2.0 8 | // @version 2021.1 9 | // @include https://*.atlassian.net/browse/* 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 | if (label) { 27 | el.setAttribute("aria-label", label); 28 | } 29 | } 30 | 31 | function makePresentational(el) { 32 | el.setAttribute("role", "presentation"); 33 | } 34 | 35 | function setLabel(el, label) { 36 | el.setAttribute("aria-label", label); 37 | } 38 | 39 | function makeHidden(el) { 40 | el.setAttribute("aria-hidden", "true"); 41 | } 42 | 43 | function setExpanded(el, expanded) { 44 | el.setAttribute("aria-expanded", expanded ? "true" : "false"); 45 | } 46 | 47 | var idCounter = 0; 48 | // Get a node's id. If it doesn't have one, make and set one first. 49 | function setAriaIdIfNecessary(elem) { 50 | if (!elem.id) { 51 | elem.setAttribute("id", "axsg-" + idCounter++); 52 | } 53 | return elem.id; 54 | } 55 | 56 | function makeElementOwn(parentElement, listOfNodes){ 57 | ids = []; 58 | for(let node of listOfNodes){ 59 | ids.push(setAriaIdIfNecessary(node)); 60 | } 61 | parentElement.setAttribute("aria-owns", ids.join(" ")); 62 | } 63 | 64 | // Focus something even if it wasn't made focusable by the author. 65 | function forceFocus(el) { 66 | let focusable = el.hasAttribute("tabindex"); 67 | if (focusable) { 68 | el.focus(); 69 | return; 70 | } 71 | el.setAttribute("tabindex", "-1"); 72 | el.focus(); 73 | } 74 | 75 | /*** Code to apply the tweaks when appropriate. ***/ 76 | 77 | function applyTweak(el, tweak) { 78 | if (Array.isArray(tweak.tweak)) { 79 | let [func, ...args] = tweak.tweak; 80 | func(el, ...args); 81 | } else { 82 | tweak.tweak(el); 83 | } 84 | } 85 | 86 | function applyTweaks(root, tweaks, checkRoot) { 87 | for (let tweak of tweaks) { 88 | for (let el of root.querySelectorAll(tweak.selector)) { 89 | try { 90 | applyTweak(el, tweak); 91 | } catch (e) { 92 | console.log("Exception while applying tweak for '" + tweak.selector + "': " + e); 93 | } 94 | } 95 | if (checkRoot && root.matches(tweak.selector)) { 96 | try { 97 | applyTweak(root, tweak); 98 | } catch (e) { 99 | console.log("Exception while applying tweak for '" + tweak.selector + "': " + e); 100 | } 101 | } 102 | } 103 | } 104 | 105 | let observer = new MutationObserver(function(mutations) { 106 | for (let mutation of mutations) { 107 | try { 108 | if (mutation.type === "childList") { 109 | for (let node of mutation.addedNodes) { 110 | if (node.nodeType != Node.ELEMENT_NODE) { 111 | continue; 112 | } 113 | applyTweaks(node, DYNAMIC_TWEAKS, true); 114 | } 115 | } else if (mutation.type === "attributes") { 116 | applyTweaks(mutation.target, DYNAMIC_TWEAKS, true); 117 | } 118 | } catch (e) { 119 | // Catch exceptions for individual mutations so other mutations are still handled. 120 | console.log("Exception while handling mutation: " + e); 121 | } 122 | } 123 | }); 124 | 125 | function init() { 126 | applyTweaks(document, LOAD_TWEAKS, false); 127 | applyTweaks(document, DYNAMIC_TWEAKS, false); 128 | options = {childList: true, subtree: true}; 129 | if (DYNAMIC_TWEAK_ATTRIBS.length > 0) { 130 | options.attributes = true; 131 | options.attributeFilter = DYNAMIC_TWEAK_ATTRIBS; 132 | } 133 | observer.observe(document, options); 134 | } 135 | 136 | /*** Define the actual tweaks. ***/ 137 | 138 | // Tweaks that only need to be applied on load. 139 | const LOAD_TWEAKS = [ 140 | ]; 141 | 142 | // Attributes that should be watched for changes and cause dynamic tweaks to be 143 | // applied. 144 | const DYNAMIC_TWEAK_ATTRIBS = []; 145 | 146 | // Tweaks that must be applied whenever an element is added/changed. 147 | const DYNAMIC_TWEAKS = [ 148 | // Make comments into headings. 149 | {selector: '.ezs3xpg1', 150 | tweak: [makeHeading, 3]}, 151 | ]; 152 | 153 | /*** Lights, camera, action! ***/ 154 | init(); 155 | -------------------------------------------------------------------------------- /PhabricatorA11yFixes.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Phabricator Accessibility Fixes 3 | // @namespace http://axSgrease.nvaccess.org/ 4 | // @description Improves the accessibility of Phabricator. 5 | // @author James Teh 6 | // @copyright 2018-2025 Mozilla Corporation 7 | // @license Mozilla Public License version 2.0 8 | // @version 2025.1 9 | // @grant GM_log 10 | // @include https://phabricator.services.mozilla.com/D* 11 | // ==/UserScript== 12 | 13 | /*** Functions for common tweaks. ***/ 14 | 15 | function makeHeading(el, level) { 16 | el.setAttribute("role", "heading"); 17 | el.setAttribute("aria-level", level); 18 | } 19 | 20 | function makeRegion(el, label) { 21 | el.setAttribute("role", "region"); 22 | el.setAttribute("aria-label", label); 23 | } 24 | 25 | function makeButton(el, label) { 26 | el.setAttribute("role", "button"); 27 | el.setAttribute("aria-label", label); 28 | } 29 | 30 | function makePresentational(el) { 31 | el.setAttribute("role", "presentation"); 32 | } 33 | 34 | function setLabel(el, label) { 35 | el.setAttribute("aria-label", label); 36 | } 37 | 38 | function labelHiddenStatusIcon(el, label) { 39 | el.removeAttribute("aria-hidden"); 40 | el.setAttribute("role", "image"); 41 | setLabel(el, label); 42 | } 43 | 44 | /*** Code to apply the tweaks when appropriate. ***/ 45 | 46 | function applyTweaks(root, tweaks) { 47 | for (let tweak of tweaks) { 48 | for (let el of root.querySelectorAll(tweak.selector)) { 49 | if (Array.isArray(tweak.tweak)) { 50 | let [func, ...args] = tweak.tweak; 51 | func(el, ...args); 52 | } else { 53 | tweak.tweak(el); 54 | } 55 | } 56 | } 57 | } 58 | 59 | function init() { 60 | applyTweaks(document, LOAD_TWEAKS); 61 | applyTweaks(document, DYNAMIC_TWEAKS); 62 | } 63 | 64 | let observer = new MutationObserver(function(mutations) { 65 | for (let mutation of mutations) { 66 | try { 67 | if (mutation.type === "childList") { 68 | for (let node of mutation.addedNodes) { 69 | if (node.nodeType != Node.ELEMENT_NODE) { 70 | continue; 71 | } 72 | applyTweaks(node, DYNAMIC_TWEAKS); 73 | } 74 | }/* else if (mutation.type === "attributes") { 75 | if (mutation.attributeName == "class") { 76 | onClassModified(mutation.target); 77 | } 78 | }*/ 79 | } catch (e) { 80 | // Catch exceptions for individual mutations so other mutations are still handled. 81 | GM_log("Exception while handling mutation: " + e); 82 | } 83 | } 84 | }); 85 | observer.observe(document, {childList: true,/* attributes: true,*/ 86 | subtree: true/*, attributeFilter: ["class"]*/}); 87 | 88 | /*** Define the actual tweaks. ***/ 89 | 90 | // Tweaks that only need to be applied on load. 91 | const LOAD_TWEAKS = [ 92 | // There are some off-screen headings to denote various sections, but they 93 | // are h3 instead of h1 as they should be. 94 | {selector: '.phui-main-column .phui-timeline-view h3.aural-only, .phui-comment-preview-view .phui-timeline-view h3.aural-only, .phui-comment-form-view h3.aural-only', 95 | tweak: [makeHeading, 1]}, 96 | // The diff is an h1, so the files inside the diff should be an h2, not an h1. 97 | {selector: '.differential-file-icon-header', 98 | tweak: [makeHeading, 2]}, 99 | // Reviewer status icons. 100 | {selector: '.phui-status-item-target .fa-circle-o', 101 | tweak: [labelHiddenStatusIcon, "pending"]}, 102 | {selector: '.phui-status-item-target .fa-minus-circle', 103 | tweak: [labelHiddenStatusIcon, "pending blocking"]}, 104 | {selector: '.phui-status-item-target .fa-check-circle', 105 | tweak: [labelHiddenStatusIcon, "accepted"]}, 106 | {selector: '.phui-status-item-target .fa-check-circle-o', 107 | tweak: [labelHiddenStatusIcon, "accepted prior"]}, 108 | {selector: '.phui-status-item-target .fa-times-circle', 109 | tweak: [labelHiddenStatusIcon, "requested changes"]}, 110 | {selector: '.phui-status-item-target .fa-times-circle-o', 111 | tweak: [labelHiddenStatusIcon, "requested changes to prior"]}, 112 | {selector: '.phui-status-item-target .fa-comment', 113 | tweak: [labelHiddenStatusIcon, "comment"]}, 114 | ] 115 | 116 | // Tweaks that must be applied whenever a node is added. 117 | const DYNAMIC_TWEAKS = [ 118 | // Timeline headings, "Summary" heading. 119 | {selector: '.phui-timeline-title, .phui-property-list-section-header', 120 | tweak: [makeHeading, 2]}, 121 | // Inline comment headings. 122 | {selector: '.differential-inline-comment-head .inline-head-left', 123 | tweak: [makeHeading, 3]}, 124 | {selector: '.phui-timeline-image, .phui-head-thing-image', 125 | tweak: makePresentational}, 126 | // Code line numbers. 127 | {selector: '.remarkup-code th', 128 | // We don't want these to be header cells, as this causes a heap of spurious 129 | // verbosity. 130 | tweak: el => el.setAttribute("role", "cell")}, 131 | ] 132 | 133 | /*** Lights, camera, action! ***/ 134 | init(); 135 | -------------------------------------------------------------------------------- /GreenhouseA11yFixes.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Greenhouse Accessibility Fixes 3 | // @namespace http://axSgrease.nvaccess.org/ 4 | // @description Improves the accessibility of Greenhouse. 5 | // @author James Teh 6 | // @copyright 2019 Mozilla Corporation 7 | // @license Mozilla Public License version 2.0 8 | // @version 2019.1 9 | // @grant GM_log 10 | // @include https://*.greenhouse.io/* 11 | // ==/UserScript== 12 | 13 | /*** Functions for common tweaks. ***/ 14 | 15 | function makeHeading(el, level) { 16 | el.setAttribute("role", "heading"); 17 | el.setAttribute("aria-level", level); 18 | } 19 | 20 | function makeRegion(el, label) { 21 | el.setAttribute("role", "region"); 22 | el.setAttribute("aria-label", label); 23 | } 24 | 25 | function makeButton(el, label) { 26 | el.setAttribute("role", "button"); 27 | el.setAttribute("aria-label", label); 28 | } 29 | 30 | function makePresentational(el) { 31 | el.setAttribute("role", "presentation"); 32 | } 33 | 34 | function setLabel(el, label) { 35 | el.setAttribute("aria-label", label); 36 | } 37 | 38 | function makeHidden(el) { 39 | el.setAttribute("aria-hidden", "true"); 40 | } 41 | 42 | function setExpanded(el, expanded) { 43 | el.setAttribute("aria-expanded", expanded ? "true" : "false"); 44 | } 45 | 46 | /*** Code to apply the tweaks when appropriate. ***/ 47 | 48 | function applyTweak(el, tweak) { 49 | if (Array.isArray(tweak.tweak)) { 50 | let [func, ...args] = tweak.tweak; 51 | func(el, ...args); 52 | } else { 53 | tweak.tweak(el); 54 | } 55 | } 56 | 57 | function applyTweaks(root, tweaks, checkRoot) { 58 | for (let tweak of tweaks) { 59 | for (let el of root.querySelectorAll(tweak.selector)) { 60 | applyTweak(el, tweak); 61 | } 62 | if (checkRoot && root.matches(tweak.selector)) { 63 | applyTweak(root, tweak); 64 | } 65 | } 66 | } 67 | 68 | let observer = new MutationObserver(function(mutations) { 69 | for (let mutation of mutations) { 70 | try { 71 | if (mutation.type === "childList") { 72 | for (let node of mutation.addedNodes) { 73 | if (node.nodeType != Node.ELEMENT_NODE) { 74 | continue; 75 | } 76 | applyTweaks(node, DYNAMIC_TWEAKS, true); 77 | } 78 | } else if (mutation.type === "attributes") { 79 | applyTweaks(mutation.target, DYNAMIC_TWEAKS, true); 80 | } 81 | } catch (e) { 82 | // Catch exceptions for individual mutations so other mutations are still handled. 83 | GM_log("Exception while handling mutation: " + e); 84 | } 85 | } 86 | }); 87 | 88 | function init() { 89 | applyTweaks(document, LOAD_TWEAKS, false); 90 | applyTweaks(document, DYNAMIC_TWEAKS, false); 91 | observer.observe(document, {childList: true, attributes: DYNAMIC_TWEAK_ATTRIBS.length > 0, 92 | subtree: true, attributeFilter: DYNAMIC_TWEAK_ATTRIBS}); 93 | } 94 | 95 | /*** Define the actual tweaks. ***/ 96 | 97 | function labelRating(el, ratingText) { 98 | let name = el.getAttribute("title"); 99 | setLabel(el, name + ": " + ratingText); 100 | } 101 | 102 | // Tweaks that only need to be applied on load. 103 | const LOAD_TWEAKS = [ 104 | {selector: '.tabs-nav', 105 | tweak: el => el.setAttribute("role", "tablist")}, 106 | {selector: '.tabs-nav > li', 107 | tweak: makePresentational}, 108 | ]; 109 | 110 | // Attributes that should be watched for changes and cause dynamic tweaks to be 111 | // applied. For example, if there is a dynamic tweak which handles the state of 112 | // a check box and that state is determined using an attribute, that attribute 113 | // should be included here. 114 | const DYNAMIC_TWEAK_ATTRIBS = ["class"]; 115 | 116 | // Tweaks that must be applied whenever a node is added/changed. 117 | const DYNAMIC_TWEAKS = [ 118 | {selector: '.thumbs-up:not(.rating-with-name)', 119 | tweak: [labelRating, "thumbs up"]}, 120 | {selector: '.two-thumbs-up:not(.rating-with-name)', 121 | tweak: [labelRating, "two thumbs up"]}, 122 | {selector: '.mixed-rating:not(.rating-with-name)', 123 | tweak: [labelRating, "mixed"]}, 124 | {selector: '.tabs-nav a', 125 | tweak: el => { 126 | el.setAttribute("role", "tab"); 127 | let selected = el.parentElement.classList.contains("selected"); 128 | el.setAttribute("aria-selected", selected ? "true" : "false"); 129 | }}, 130 | {selector: '.closed', 131 | tweak: [setExpanded, false]}, 132 | {selector: '.open', 133 | tweak: [setExpanded, true]}, 134 | {selector: '.scorecard-attributes-table .name.focus', 135 | tweak: el => { 136 | // Importance is only indicated through colour. 137 | // We can't just set aria-label here because it doesn't replace the content 138 | // for table cells. 139 | // Therefore, create a visually hidden indicator. 140 | let important = document.createElement("span"); 141 | important.style = "position: absolute; left: -1000px; width: 1px; height: 1px;"; 142 | important.setAttribute("aria-label", "important"); 143 | el.insertBefore(important, el.firstChild); 144 | }}, 145 | {selector: '.selectable', 146 | tweak: el => { 147 | el.setAttribute("role", "radio"); 148 | let checked = el.classList.contains("selected"); 149 | el.setAttribute("aria-checked", checked ? "true" : "false"); 150 | }}, 151 | ]; 152 | 153 | /*** Lights, camera, action! ***/ 154 | init(); 155 | -------------------------------------------------------------------------------- /PandoraA11yFixes.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Pandora Accessibility Fixes 3 | // @namespace http://www.jantrid.net/axSGrease/ 4 | // @description Improves the accessibility of Pandora. 5 | // @author James Teh 6 | // @copyright 2013 James Teh 7 | // @license GNU General Public License version 2.0 8 | // @version 0.20130113.01 9 | // @include http://www.pandora.com/* 10 | // @homepageURL http://userscripts.org/scripts/show/156173 11 | // @updateURL https://userscripts.org/scripts/source/156173.user.js 12 | // ==/UserScript== 13 | 14 | BUTTONS_LABELS = { 15 | "thumbUpButton": "Thumb up", 16 | "thumbDownButton": "Thumb down", 17 | "pauseButton": "Pause", 18 | "playButton": "Play", 19 | "skipButton": "Skip", 20 | } 21 | function fixButton(target) { 22 | var classes = target.classList; 23 | if (!classes) 24 | return; 25 | for (var i = 0; i < classes.length; ++i) { 26 | var cls = classes[i]; 27 | var label = BUTTONS_LABELS[cls]; 28 | if (!label) 29 | continue; 30 | var button = target.firstChild; 31 | button.setAttribute("role", "button"); 32 | button.setAttribute("aria-label", label); 33 | if (cls == "thumbUpButton") 34 | button.setAttribute("aria-pressed", 35 | classes.contains("indicator") ? "true" : "false"); 36 | break; 37 | } 38 | } 39 | 40 | function fixStationDetailsItem(target) { 41 | var node; 42 | if (node = target.querySelector(".deletable")) { 43 | node.setAttribute("role", "button"); 44 | node.setAttribute("aria-label", "Delete"); 45 | } 46 | if (node = target.querySelector(".sample")) 47 | node.firstChild.setAttribute("aria-label", "Sample"); 48 | } 49 | 50 | function onClassModified(target) { 51 | var classes = target.classList; 52 | if (!classes) 53 | return; 54 | fixButton(target); 55 | if (classes.contains("stationListItem")) 56 | target.setAttribute("aria-checked", 57 | classes.contains("selected") ? "true" : "false"); 58 | } 59 | 60 | function onNodeAdded(target) { 61 | if (target.nodeType != Node.ELEMENT_NODE) 62 | return; 63 | var nodes; 64 | var node; 65 | if (target.classList.contains("backstage")) { 66 | document.getElementById("addArtistSeed").setAttribute("aria-label", "Add artist"); 67 | nodes = target.querySelectorAll("div.list"); 68 | for (var i = 0; i < nodes.length; ++i) 69 | fixStationDetailsItem(nodes[i]); 70 | return; 71 | } 72 | if (target.nodeName == "LI" && target.querySelector("div.list")) { 73 | fixStationDetailsItem(target); 74 | return; 75 | } 76 | if (node = document.getElementById("stationList")) 77 | node.setAttribute("role", "radiogroup"); 78 | nodes = target.getElementsByClassName("stationListItem"); 79 | for (var i = 0; i < nodes.length; ++i) { 80 | node = nodes[i]; 81 | node.setAttribute("role", "radio"); 82 | if (node.classList.contains("selected")) 83 | node.setAttribute("aria-checked", "true"); 84 | } 85 | nodes = target.getElementsByClassName("stationName"); 86 | for (var i = 0; i < nodes.length; ++i) 87 | nodes[i].setAttribute("role", "presentation"); 88 | nodes = target.getElementsByClassName("option"); 89 | for (var i = 0; i < nodes.length; ++i) 90 | nodes[i].setAttribute("role", "button"); 91 | } 92 | 93 | function onStyleModified(target) { 94 | var style = target.style; 95 | if ((target.id == "station_menu_dd" || target.id == "cd_menu_dd") && style.visibility == "visible") { 96 | var nodes = target.getElementsByTagName("a"); 97 | for (var i = 0; i < nodes.length; ++i) { 98 | var node = nodes[i]; 99 | if (node.style.display == "none") 100 | continue; 101 | node.focus(); 102 | break; 103 | } 104 | } 105 | } 106 | 107 | function init() { 108 | var nodes; 109 | nodes = document.getElementsByClassName("buttons")[0].childNodes; 110 | for (var i = 0; i < nodes.length; ++i) 111 | fixButton(nodes[i]); 112 | var node; 113 | if (node = document.getElementsByClassName("buyButton")[0]) { 114 | node.setAttribute("role", "button"); 115 | node.setAttribute("aria-label", "Buy"); 116 | } 117 | if (node = document.getElementsByClassName("cd_activator")[0]) { 118 | node = node.firstChild; 119 | node.setAttribute("role", "button"); 120 | node.setAttribute("aria-haspopup", "true"); 121 | node.setAttribute("aria-label", "Track options"); 122 | } 123 | } 124 | 125 | var observer = new MutationObserver(function(mutations) { 126 | mutations.forEach(function(mutation) { 127 | try { 128 | if (mutation.type === "childList") { 129 | for (var i = 0; i < mutation.addedNodes.length; ++i) 130 | onNodeAdded(mutation.addedNodes[i]); 131 | } else if (mutation.type === "attributes") { 132 | if (mutation.attributeName == "class") 133 | onClassModified(mutation.target); 134 | else if (mutation.attributeName == "style") 135 | onStyleModified(mutation.target); 136 | } 137 | } catch (e) { 138 | // Catch exceptions for individual mutations so other mutations are still handled. 139 | GM_log("Exception while handling mutation: " + e); 140 | } 141 | }); 142 | }); 143 | observer.observe(document, {childList: true, attributes: true, 144 | subtree: true, attributeFilter: ["class", "style"]}); 145 | init(); 146 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # AxSGrease 2 | 3 | - Author: James Teh <jamie@jantrid.net> & other contributors 4 | - Copyright: 2011-2022 NV Access Limited, James Teh 5 | 6 | AxSGrease is a set of user scripts (also known as GreaseMonkey scripts) to improve the accessibility of various websites. 7 | 8 | ## Installation 9 | Before you can install any of these scripts, you must first install a user script manager for your browser. 10 | For Firefox, you can install [GreaseMonkey](https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/). 11 | For Chrome, you can install [Tampermonkey](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo). 12 | There are also user script managers for other browsers. 13 | See [Greasy Fork's page on How to install user scripts](https://greasyfork.org/en/help/installing-user-scripts) for more details. 14 | 15 | Once you have a user script manager installed, simply activate the download link for the relevant script below to download and install it. 16 | 17 | ## Scripts 18 | Note: This documentation is out of date. 19 | Some newer scripts are missing, some older scripts should be removed, etc. 20 | 21 | Following is information about each script. 22 | 23 | ### Asus Router Accessibility fixes 24 | 25 | [Download Asus Router Accessibility Fixes](https://github.com/jcsteh/axSGrease/raw/master/AsusRouterA11yFixes.user.js) 26 | This script improves the accessibility of the asus router firmware. (this has only been tested on RT-AX56U router). it does the following: 27 | 28 | - makes tutor help messages automatically read. 29 | - Creates a primary and secondary navigation region, and removes layout tables for navigation. 30 | - Adds section headers to the nav menu, at heading level 2. 31 | - Makes pages that have a title have an h1. 32 | - Labels some unlabeled images. 33 | 34 | ### Bugzilla Accessibility Fixes 35 | [Download Bugzilla Accessibility Fixes](https://github.com/jcsteh/axSGrease/raw/master/BugzillaA11yFixes.user.js) 36 | 37 | This script improves the accessibility of bug pages in the [Bugzilla](http://www.bugzilla.org/) bug tracker used by many projects. 38 | It does the following: 39 | 40 | - Makes the bug title, attachments heading and comment number headings accessible as headings. 41 | - Sets alternate text for user images so that screen readers don't derive an unfriendly name from the URL. 42 | 43 | ### GitHub Accessibility Fixes 44 | [Download GitHub Accessibility Fixes](https://github.com/jcsteh/axSGrease/raw/master/GitHubA11yFixes.user.js) 45 | 46 | This script improves the accessibility of [GitHub](https://github.com/). 47 | It does the following: 48 | 49 | - Makes various headings accessible as headings, including: 50 | - Comment headers in issues, pull requests and commits 51 | - Commit group headers in commit listings 52 | - The commit title for single commits 53 | - The header for each changed file in pull requests and commits 54 | - Ensures that various data tables aren't treated as layout tables, including: 55 | - The file content when viewing a single file 56 | - File listings 57 | - Diff content 58 | - Tables in Markdown content 59 | - When there are lines of code which can be commented on (e.g. a pull request or commit), puts the comment buttons after (rather than before) the code. 60 | - Makes the state of checkable menu items accessible; e.g. in the watch and labels pop-ups. 61 | - Marks "Add your reaction" buttons as having a pop-up, focuses the first reaction when the add button is pressed and makes the labels of the reaction buttons less verbose. 62 | 63 | ### Monorail Accessibility Fixes 64 | [Download Monorail Accessibility Fixes](https://github.com/jcsteh/axSGrease/raw/master/MonorailA11yFixes.user.js) 65 | 66 | This script improves the accessiblity of the [Monorail](https://bugs.chromium.org/) issue tracker used by Google for Chromium-related projects. 67 | It does the following: 68 | 69 | - Makes issue titles and comment headings accessible as headings. 70 | - Makes the star control and status accessible. 71 | 72 | ### Telegram accessibility fixes 73 | [Download Telegram Accessibility Fixes](https://github.com/jcsteh/axSGrease/raw/master/TelegramA11yFixes.user.js) 74 | 75 | This script improves the accessibility of the [Telegram instant messaging](https://web.telegram.org/) web interface. 76 | 77 | It so far does the following: 78 | 79 | - Marks the chat history as a live region so new messages are announced automatically. 80 | 81 | ### Trello Accessibility Fixes 82 | [Download Trello Accessibility Fixes](https://github.com/jcsteh/axSGrease/raw/master/TrelloA11yFixes.user.js) 83 | 84 | This script improves the accessibility of [Trello](https://trello.com/). 85 | It does the following: 86 | 87 | - Makes lists and cards accessible as lists and list items, respectively. 88 | - Focuses the active card when moving between lists and cards with the arrow keys. 89 | If you are using a screen reader, you will need to ensure that the arrow keys are passed to the application to make use of this. 90 | For NVDA, you can achieve this by switching to focus mode to move through cards. 91 | - Labels badges in cards. 92 | - Makes list headers and activity item headers accessible as headings. 93 | - Prevents loss of position for screen reader users when pressing the control key. 94 | - Adds a shift+m keyboard shortcut to quickly move a card. 95 | - Makes checklists accessible. 96 | - Makes the checkbox for due date completion accessible. 97 | -------------------------------------------------------------------------------- /AppleMusicA11yFixes.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Apple Music Accessibility Fixes 3 | // @namespace http://axSgrease.nvaccess.org/ 4 | // @description Improves the accessibility of Apple Music. 5 | // @author James Teh 6 | // @copyright 2019-2024 Mozilla Corporation, Derek Riemer 7 | // @license Mozilla Public License version 2.0 8 | // @version 2024.1 9 | // @include https://music.apple.com/* 10 | // @include https://beta.music.apple.com/* 11 | // ==/UserScript== 12 | 13 | /*** Functions for common tweaks. ***/ 14 | 15 | function makeHeading(el, level) { 16 | el.setAttribute("role", "heading"); 17 | el.setAttribute("aria-level", level); 18 | } 19 | 20 | function makeRegion(el, label) { 21 | el.setAttribute("role", "region"); 22 | el.setAttribute("aria-label", label); 23 | } 24 | 25 | function makeButton(el, label) { 26 | el.setAttribute("role", "button"); 27 | if (label) { 28 | el.setAttribute("aria-label", label); 29 | } 30 | } 31 | 32 | function makePresentational(el) { 33 | el.setAttribute("role", "presentation"); 34 | } 35 | 36 | function setLabel(el, label) { 37 | el.setAttribute("aria-label", label); 38 | } 39 | 40 | function makeHidden(el) { 41 | el.setAttribute("aria-hidden", "true"); 42 | } 43 | 44 | function setExpanded(el, expanded) { 45 | el.setAttribute("aria-expanded", expanded ? "true" : "false"); 46 | } 47 | 48 | var idCounter = 0; 49 | // Get a node's id. If it doesn't have one, make and set one first. 50 | function setAriaIdIfNecessary(elem) { 51 | if (!elem.id) { 52 | elem.setAttribute("id", "axsg-" + idCounter++); 53 | } 54 | return elem.id; 55 | } 56 | 57 | function makeElementOwn(parentElement, listOfNodes){ 58 | ids = []; 59 | for(let node of listOfNodes){ 60 | ids.push(setAriaIdIfNecessary(node)); 61 | } 62 | parentElement.setAttribute("aria-owns", ids.join(" ")); 63 | } 64 | 65 | // Focus something even if it wasn't made focusable by the author. 66 | function forceFocus(el) { 67 | let focusable = el.hasAttribute("tabindex"); 68 | if (focusable) { 69 | el.focus(); 70 | return; 71 | } 72 | el.setAttribute("tabindex", "-1"); 73 | el.focus(); 74 | } 75 | 76 | /*** Code to apply the tweaks when appropriate. ***/ 77 | 78 | function applyTweak(el, tweak) { 79 | if (Array.isArray(tweak.tweak)) { 80 | let [func, ...args] = tweak.tweak; 81 | func(el, ...args); 82 | } else { 83 | tweak.tweak(el); 84 | } 85 | } 86 | 87 | function applyTweaks(root, tweaks, checkRoot) { 88 | for (let tweak of tweaks) { 89 | for (let el of root.querySelectorAll(tweak.selector)) { 90 | try { 91 | applyTweak(el, tweak); 92 | } catch (e) { 93 | console.log("Exception while applying tweak for '" + tweak.selector + "': " + e); 94 | } 95 | } 96 | if (checkRoot && root.matches(tweak.selector)) { 97 | try { 98 | applyTweak(root, tweak); 99 | } catch (e) { 100 | console.log("Exception while applying tweak for '" + tweak.selector + "': " + e); 101 | } 102 | } 103 | } 104 | } 105 | 106 | let observer = new MutationObserver(function(mutations) { 107 | for (let mutation of mutations) { 108 | try { 109 | if (mutation.type === "childList") { 110 | for (let node of mutation.addedNodes) { 111 | if (node.nodeType != Node.ELEMENT_NODE) { 112 | continue; 113 | } 114 | applyTweaks(node, DYNAMIC_TWEAKS, true); 115 | } 116 | } else if (mutation.type === "attributes") { 117 | applyTweaks(mutation.target, DYNAMIC_TWEAKS, true); 118 | } 119 | } catch (e) { 120 | // Catch exceptions for individual mutations so other mutations are still handled. 121 | console.log("Exception while handling mutation: " + e); 122 | } 123 | } 124 | }); 125 | 126 | function init() { 127 | applyTweaks(document, LOAD_TWEAKS, false); 128 | applyTweaks(document, DYNAMIC_TWEAKS, false); 129 | options = {childList: true, subtree: true}; 130 | if (DYNAMIC_TWEAK_ATTRIBS.length > 0) { 131 | options.attributes = true; 132 | options.attributeFilter = DYNAMIC_TWEAK_ATTRIBS; 133 | } 134 | observer.observe(document, options); 135 | } 136 | 137 | /*** Define the actual tweaks. ***/ 138 | 139 | // Tweaks that only need to be applied on load. 140 | const LOAD_TWEAKS = [ 141 | ]; 142 | 143 | // Attributes that should be watched for changes and cause dynamic tweaks to be 144 | // applied. 145 | const DYNAMIC_TWEAK_ATTRIBS = []; 146 | 147 | // Tweaks that must be applied whenever a node is added/changed. 148 | const DYNAMIC_TWEAKS = [ 149 | // Remove pointless semantics on a form inside the search box so screen 150 | // readers can find and focus the search box properly. 151 | {selector: '#search-input-form', 152 | tweak: el => { 153 | el.removeAttribute("tabindex"); 154 | el.setAttribute("role", "none"); 155 | }}, 156 | // Make the section containing playback controls, etc. into a region. 157 | {selector: '.player-bar', 158 | tweak: el => el.setAttribute("role", "region")}, 159 | // Make the section containing the song info into a region. 160 | {selector: '[slot=lcd]', 161 | tweak: [makeRegion, "Info"]}, 162 | // Fix cells in song lists. 163 | {selector: '.songs-list__col', 164 | tweak: el => el.setAttribute("role", "cell")}, 165 | // The title of an active radio station. 166 | {selector: '.typography-large-title-emphasized', 167 | tweak: [makeHeading, 1]}, 168 | ]; 169 | 170 | /*** Lights, camera, action! ***/ 171 | init(); 172 | -------------------------------------------------------------------------------- /PocketbookA11yFixes.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Pocketbook Accessibility Fixes 3 | // @namespace http://axSgrease.nvaccess.org/ 4 | // @description Improves the accessibility of Pocketbook. 5 | // @author James Teh 6 | // @copyright 2019 Mozilla Corporation, Derek Riemer 7 | // @license Mozilla Public License version 2.0 8 | // @version 2019.1 9 | // @include https://getpocketbook.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 | if (label) { 27 | el.setAttribute("aria-label", label); 28 | } 29 | } 30 | 31 | function makePresentational(el) { 32 | el.setAttribute("role", "presentation"); 33 | } 34 | 35 | function setLabel(el, label) { 36 | el.setAttribute("aria-label", label); 37 | } 38 | 39 | function makeHidden(el) { 40 | el.setAttribute("aria-hidden", "true"); 41 | } 42 | 43 | function setExpanded(el, expanded) { 44 | el.setAttribute("aria-expanded", expanded ? "true" : "false"); 45 | } 46 | 47 | var idCounter = 0; 48 | // Get a node's id. If it doesn't have one, make and set one first. 49 | function setAriaIdIfNecessary(elem) { 50 | if (!elem.id) { 51 | elem.setAttribute("id", "axsg-" + idCounter++); 52 | } 53 | return elem.id; 54 | } 55 | 56 | function makeElementOwn(parentElement, listOfNodes){ 57 | ids = []; 58 | for(let node of listOfNodes){ 59 | ids.push(setAriaIdIfNecessary(node)); 60 | } 61 | parentElement.setAttribute("aria-owns", ids.join(" ")); 62 | } 63 | 64 | // Focus something even if it wasn't made focusable by the author. 65 | function forceFocus(el) { 66 | let focusable = el.hasAttribute("tabindex"); 67 | if (focusable) { 68 | el.focus(); 69 | return; 70 | } 71 | el.setAttribute("tabindex", "-1"); 72 | el.focus(); 73 | } 74 | 75 | /*** Code to apply the tweaks when appropriate. ***/ 76 | 77 | function applyTweak(el, tweak) { 78 | if (Array.isArray(tweak.tweak)) { 79 | let [func, ...args] = tweak.tweak; 80 | func(el, ...args); 81 | } else { 82 | tweak.tweak(el); 83 | } 84 | } 85 | 86 | function applyTweaks(root, tweaks, checkRoot) { 87 | for (let tweak of tweaks) { 88 | for (let el of root.querySelectorAll(tweak.selector)) { 89 | try { 90 | applyTweak(el, tweak); 91 | } catch (e) { 92 | console.log("Exception while applying tweak for '" + tweak.selector + "': " + e); 93 | } 94 | } 95 | if (checkRoot && root.matches(tweak.selector)) { 96 | try { 97 | applyTweak(root, tweak); 98 | } catch (e) { 99 | console.log("Exception while applying tweak for '" + tweak.selector + "': " + e); 100 | } 101 | } 102 | } 103 | } 104 | 105 | let observer = new MutationObserver(function(mutations) { 106 | for (let mutation of mutations) { 107 | try { 108 | if (mutation.type === "childList") { 109 | for (let node of mutation.addedNodes) { 110 | if (node.nodeType != Node.ELEMENT_NODE) { 111 | continue; 112 | } 113 | applyTweaks(node, DYNAMIC_TWEAKS, true); 114 | } 115 | } else if (mutation.type === "attributes") { 116 | applyTweaks(mutation.target, DYNAMIC_TWEAKS, true); 117 | } 118 | } catch (e) { 119 | // Catch exceptions for individual mutations so other mutations are still handled. 120 | console.log("Exception while handling mutation: " + e); 121 | } 122 | } 123 | }); 124 | 125 | function init() { 126 | applyTweaks(document, LOAD_TWEAKS, false); 127 | applyTweaks(document, DYNAMIC_TWEAKS, false); 128 | options = {childList: true, subtree: true}; 129 | if (DYNAMIC_TWEAK_ATTRIBS.length > 0) { 130 | options.attributes = true; 131 | options.attributeFilter = DYNAMIC_TWEAK_ATTRIBS; 132 | } 133 | observer.observe(document, options); 134 | } 135 | 136 | /*** Define the actual tweaks. ***/ 137 | 138 | // Tweaks that only need to be applied on load. 139 | const LOAD_TWEAKS = [ 140 | ]; 141 | 142 | // Attributes that should be watched for changes and cause dynamic tweaks to be 143 | // applied. For example, if there is a dynamic tweak which handles the state of 144 | // a check box and that state is determined using an attribute, that attribute 145 | // should be included here. 146 | const DYNAMIC_TWEAK_ATTRIBS = ["style"]; 147 | 148 | // Tweaks that must be applied whenever a node is added/changed. 149 | const DYNAMIC_TWEAKS = [ 150 | // The transition description dialog. 151 | {selector: '#trandescdiv:not(.hide) h3', 152 | tweak: el => { 153 | let dialog = document.querySelector("#trandescdiv"); 154 | dialog.setAttribute("role", "dialog"); 155 | forceFocus(el); 156 | }}, 157 | // The category chooser dialog. 158 | {selector: '#categoryBox form', 159 | tweak: el => { 160 | if (el.clientWidth == 0) { 161 | return; // Hidden. 162 | } 163 | let dialog = el.closest("#categoryBox"); 164 | dialog.setAttribute("role", "dialog"); 165 | // Focus the category selector. 166 | let select = el.querySelector('[name="userCategoryId"]'); 167 | select.focus(); 168 | }}, 169 | // Recategorise link in the transaction description dialog. 170 | {selector: '#trancategory-recategorise', 171 | tweak: [setLabel, "Recategorise"]}, 172 | ]; 173 | 174 | /*** Lights, camera, action! ***/ 175 | init(); 176 | -------------------------------------------------------------------------------- /SchedA11yFixes.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Sched Accessibility Fixes 3 | // @namespace http://axSgrease.nvaccess.org/ 4 | // @description Improves the accessibility of Sched. 5 | // @author James Teh 6 | // @copyright 2019-2022 Mozilla Corporation, Derek Riemer 7 | // @license Mozilla Public License version 2.0 8 | // @version 2022.1 9 | // @include https://*.sched.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 | if (label) { 27 | el.setAttribute("aria-label", label); 28 | } 29 | } 30 | 31 | function makePresentational(el) { 32 | el.setAttribute("role", "presentation"); 33 | } 34 | 35 | function setLabel(el, label) { 36 | el.setAttribute("aria-label", label); 37 | } 38 | 39 | function makeHidden(el) { 40 | el.setAttribute("aria-hidden", "true"); 41 | } 42 | 43 | function setExpanded(el, expanded) { 44 | el.setAttribute("aria-expanded", expanded ? "true" : "false"); 45 | } 46 | 47 | var idCounter = 0; 48 | // Get a node's id. If it doesn't have one, make and set one first. 49 | function setAriaIdIfNecessary(elem) { 50 | if (!elem.id) { 51 | elem.setAttribute("id", "axsg-" + idCounter++); 52 | } 53 | return elem.id; 54 | } 55 | 56 | function makeElementOwn(parentElement, listOfNodes){ 57 | ids = []; 58 | for(let node of listOfNodes){ 59 | ids.push(setAriaIdIfNecessary(node)); 60 | } 61 | parentElement.setAttribute("aria-owns", ids.join(" ")); 62 | } 63 | 64 | // Focus something even if it wasn't made focusable by the author. 65 | function forceFocus(el) { 66 | let focusable = el.hasAttribute("tabindex"); 67 | if (focusable) { 68 | el.focus(); 69 | return; 70 | } 71 | el.setAttribute("tabindex", "-1"); 72 | el.focus(); 73 | } 74 | 75 | /*** Code to apply the tweaks when appropriate. ***/ 76 | 77 | function applyTweak(el, tweak) { 78 | if (Array.isArray(tweak.tweak)) { 79 | let [func, ...args] = tweak.tweak; 80 | func(el, ...args); 81 | } else { 82 | tweak.tweak(el); 83 | } 84 | } 85 | 86 | function applyTweaks(root, tweaks, checkRoot) { 87 | for (let tweak of tweaks) { 88 | for (let el of root.querySelectorAll(tweak.selector)) { 89 | try { 90 | applyTweak(el, tweak); 91 | } catch (e) { 92 | console.log("Exception while applying tweak for '" + tweak.selector + "': " + e); 93 | } 94 | } 95 | if (checkRoot && root.matches(tweak.selector)) { 96 | try { 97 | applyTweak(root, tweak); 98 | } catch (e) { 99 | console.log("Exception while applying tweak for '" + tweak.selector + "': " + e); 100 | } 101 | } 102 | } 103 | } 104 | 105 | let observer = new MutationObserver(function(mutations) { 106 | for (let mutation of mutations) { 107 | try { 108 | if (mutation.type === "childList") { 109 | for (let node of mutation.addedNodes) { 110 | if (node.nodeType != Node.ELEMENT_NODE) { 111 | continue; 112 | } 113 | applyTweaks(node, DYNAMIC_TWEAKS, true); 114 | } 115 | } else if (mutation.type === "attributes") { 116 | applyTweaks(mutation.target, DYNAMIC_TWEAKS, true); 117 | } 118 | } catch (e) { 119 | // Catch exceptions for individual mutations so other mutations are still handled. 120 | console.log("Exception while handling mutation: " + e); 121 | } 122 | } 123 | }); 124 | 125 | function init() { 126 | applyTweaks(document, LOAD_TWEAKS, false); 127 | applyTweaks(document, DYNAMIC_TWEAKS, false); 128 | options = {childList: true, subtree: true}; 129 | if (DYNAMIC_TWEAK_ATTRIBS.length > 0) { 130 | options.attributes = true; 131 | options.attributeFilter = DYNAMIC_TWEAK_ATTRIBS; 132 | } 133 | observer.observe(document, options); 134 | } 135 | 136 | /*** Define the actual tweaks. ***/ 137 | 138 | // Tweaks that only need to be applied on load. 139 | const LOAD_TWEAKS = [ 140 | {selector: '#sched-logo a', 141 | tweak: [setLabel, "Home"]}, 142 | {selector: '.sched-share-mobile', 143 | tweak: [setLabel, "Mobile App + iCal"]}, 144 | {selector: '.sched-container-header', 145 | tweak: [makeHeading, 2]}, 146 | // Text on event pages which says "Click here to add to My Sched". Redundant 147 | // because clicking it does nothing and the actual button is labeled below. 148 | {selector: '#add-reminder', 149 | tweak: makeHidden}, 150 | // Avatars are unlabelled. They have tool tips, but they get assigned to 151 | // aria-describedby and only after mouse hover. 152 | // Fortunately, the tool tip text is stored in an "oldtitle" attribute. 153 | {selector: '.sched-avatar', 154 | tweak: el => { 155 | let label = el.getAttribute("oldtitle"); 156 | if (label) { 157 | el.setAttribute("aria-label", label); 158 | } 159 | }}, 160 | {selector: '.pinned', 161 | tweak: el => el.setAttribute("aria-description", "pinned")}, 162 | ] 163 | 164 | // Attributes that should be watched for changes and cause dynamic tweaks to be 165 | // applied. 166 | const DYNAMIC_TWEAK_ATTRIBS = ["class"]; 167 | 168 | // Tweaks that must be applied whenever a node is added/changed. 169 | const DYNAMIC_TWEAKS = [ 170 | {selector: ':not(.sub)>.ev-save', 171 | tweak: [makeButton, "Add to My Sched"]}, 172 | {selector: '.sub>.ev-save', 173 | tweak: [makeButton, "Remove from My Sched"]}, 174 | {selector: '.dropdown:not(.open)>.dropdown-toggle', 175 | tweak: [setExpanded, false]}, 176 | {selector: '.dropdown.open>.dropdown-toggle', 177 | tweak: [setExpanded, true]}, 178 | ] 179 | 180 | /*** Lights, camera, action! ***/ 181 | init(); 182 | -------------------------------------------------------------------------------- /TrelloA11yFixes.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Trello Accessibility Fixes 3 | // @namespace http://axSgrease.nvaccess.org/ 4 | // @description Improves the accessibility of Trello. 5 | // @author James Teh 6 | // @copyright 2017 NV Access Limited 7 | // @license GNU General Public License version 2.0 8 | // @version 2017.1 9 | // @grant GM_log 10 | // @include https://trello.com/* 11 | // ==/UserScript== 12 | 13 | // Used when we need to generate ids for ARIA. 14 | var idCounter = 0; 15 | // Get a node's id. If it doesn't have one, make and set one first. 16 | function getAriaId(elem) { 17 | if (elem.id) { 18 | return elem.id; 19 | } 20 | elem.setAttribute("id", "axsg-" + idCounter++); 21 | return elem.id; 22 | } 23 | 24 | function makeHeading(elem, level) { 25 | elem.setAttribute("role", "heading"); 26 | elem.setAttribute("aria-level", level); 27 | } 28 | 29 | function tweakCard(card) { 30 | // Make this a focusable list item. 31 | card.setAttribute("tabindex", "-1"); 32 | card.setAttribute("role", "listitem"); 33 | } 34 | 35 | // Make checklists accessible. 36 | function tweakCheckItem(checkItem, isNew) { 37 | var checkbox = checkItem.querySelector(".checklist-item-checkbox-check"); 38 | if (isNew) { 39 | checkbox.setAttribute("role", "checkbox"); 40 | checkbox.setAttribute("tabindex", "-1"); 41 | var checkLabel = checkItem.querySelector(".checklist-item-details-text"); 42 | if (checkLabel) { 43 | checkbox.setAttribute("aria-labelledby", getAriaId(checkLabel)); 44 | } 45 | } 46 | var complete = checkItem.classList.contains("checklist-item-state-complete"); 47 | checkbox.setAttribute("aria-checked", complete ? "true" : "false"); 48 | } 49 | 50 | function tweakDueDateComplete(completeBadge, isNew) { 51 | var checkbox = completeBadge.querySelector(".card-detail-badge-due-date-complete-box"); 52 | if (isNew) { 53 | checkbox.setAttribute("role", "checkbox"); 54 | checkbox.setAttribute("tabindex", "-1"); 55 | checkbox.setAttribute("aria-label", "Complete"); 56 | } 57 | var complete = completeBadge.classList.contains("is-due-complete"); 58 | checkbox.setAttribute("aria-checked", complete ? "true" : "false"); 59 | } 60 | 61 | function onNodeAdded(target) { 62 | if (target.classList.contains("list-card")) { 63 | // A card just got added. 64 | tweakCard(target); 65 | return; 66 | } 67 | if (target.classList.contains("badge")) { 68 | // Label badges. 69 | var label = target.getAttribute("title"); 70 | // Include the badge count (if any) in the label. 71 | label += target.textContent; 72 | target.setAttribute("aria-label", label); 73 | return; 74 | } 75 | if (target.id == "clipboard") { 76 | // Pressing control focuses a contentEditable div for clipboard stuff, 77 | // but this causes screen reader users to lose their position. 78 | target.blur(); 79 | return; 80 | } 81 | if (target.classList.contains("checklist-item")) { 82 | // A checklist item just got added. 83 | tweakCheckItem(target, true); 84 | return; 85 | } 86 | for (var list of target.querySelectorAll(".list")) { 87 | list.setAttribute("role", "list"); 88 | var header = list.querySelector(".list-header-name"); 89 | if (header) { 90 | // Label the list with its header. 91 | list.setAttribute("aria-labelledby", getAriaId(header)); 92 | // Make the header's container into a heading. 93 | makeHeading(header, 2); 94 | } 95 | } 96 | for (var card of target.querySelectorAll(".list-card")) { 97 | tweakCard(card); 98 | } 99 | for (var activityCreator of target.querySelectorAll(".phenom-creator")) { 100 | // Make the creator of an activity item into a heading 101 | // to facilitate quick jumping between activity items. 102 | makeHeading(activityCreator, 4); 103 | } 104 | for (var checkItem of target.querySelectorAll(".checklist-item")) { 105 | tweakCheckItem(checkItem, true); 106 | } 107 | for (var dueComplete of target.querySelectorAll(".card-detail-due-date-badge")) { 108 | tweakDueDateComplete(dueComplete, true); 109 | } 110 | } 111 | 112 | function onClassModified(target) { 113 | var classes = target.classList; 114 | if (!classes) 115 | return; 116 | if (classes.contains("active-card")) { 117 | // When the active card changes, focus it. 118 | target.focus(); 119 | } else if (classes.contains("checklist-item")) { 120 | tweakCheckItem(target, false); 121 | } else if (classes.contains("card-detail-due-date-badge")) { 122 | tweakDueDateComplete(target, false); 123 | } 124 | } 125 | 126 | var observer = new MutationObserver(function(mutations) { 127 | for (var mutation of mutations) { 128 | try { 129 | if (mutation.type === "childList") { 130 | for (var node of mutation.addedNodes) { 131 | if (node.nodeType != Node.ELEMENT_NODE) 132 | continue; 133 | onNodeAdded(node); 134 | } 135 | } else if (mutation.type === "attributes") { 136 | if (mutation.attributeName == "class") 137 | onClassModified(mutation.target); 138 | } 139 | } catch (e) { 140 | // Catch exceptions for individual mutations so other mutations are still handled. 141 | GM_log("Exception while handling mutation: " + e); 142 | } 143 | } 144 | }); 145 | observer.observe(document, {childList: true, attributes: true, 146 | subtree: true, attributeFilter: ["class"]}); 147 | 148 | function moveCard() { 149 | // Open the quick editor. 150 | var op = document.querySelector(".active-card .list-card-operation"); 151 | if (!op) { 152 | return; 153 | } 154 | op.click(); 155 | // Click the Move button. 156 | var move = document.querySelector(".js-move-card"); 157 | if (!move) { 158 | return; 159 | } 160 | move.click(); 161 | // Focus the list selector. 162 | // This doesn't work if we don't delay it. Not quite sure why. 163 | setTimeout(function() { 164 | var sel = document.querySelector(".js-select-list"); 165 | if (!sel) { 166 | return; 167 | } 168 | sel.focus(); 169 | }, 50); 170 | } 171 | 172 | // Add some keyboard shortcuts. 173 | document.addEventListener("keydown", function(evt) { 174 | if (document.activeElement.nodeName == "INPUT" || document.activeElement.nodeName == "TEXTAREA" || document.activeElement.isContentEditable) { 175 | return false; 176 | } 177 | if (evt.key == "M") { 178 | moveCard(); 179 | } 180 | }); 181 | -------------------------------------------------------------------------------- /VentraIPControl.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name VIPControl Accessibility Fixes 3 | // @namespace http://axSgrease.nvaccess.org/ 4 | // @description Improves the accessibility of VentraIP VIPControl. 5 | // @author James Teh 6 | // @copyright 2019-2022 James Teh, Mozilla Corporation, Derek Riemer 7 | // @license Mozilla Public License version 2.0 8 | // @version 2022.1 9 | // @include https://vip.ventraip.com.au/* 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 | if (label) { 27 | el.setAttribute("aria-label", label); 28 | } 29 | } 30 | 31 | function makePresentational(el) { 32 | el.setAttribute("role", "presentation"); 33 | } 34 | 35 | function setLabel(el, label) { 36 | el.setAttribute("aria-label", label); 37 | } 38 | 39 | function makeHidden(el) { 40 | el.setAttribute("aria-hidden", "true"); 41 | } 42 | 43 | function setExpanded(el, expanded) { 44 | el.setAttribute("aria-expanded", expanded ? "true" : "false"); 45 | } 46 | 47 | var idCounter = 0; 48 | // Get a node's id. If it doesn't have one, make and set one first. 49 | function setAriaIdIfNecessary(elem) { 50 | if (!elem.id) { 51 | elem.setAttribute("id", "axsg-" + idCounter++); 52 | } 53 | return elem.id; 54 | } 55 | 56 | function makeElementOwn(parentElement, listOfNodes){ 57 | ids = []; 58 | for(let node of listOfNodes){ 59 | ids.push(setAriaIdIfNecessary(node)); 60 | } 61 | parentElement.setAttribute("aria-owns", ids.join(" ")); 62 | } 63 | 64 | // Focus something even if it wasn't made focusable by the author. 65 | function forceFocus(el) { 66 | let focusable = el.hasAttribute("tabindex"); 67 | if (focusable) { 68 | el.focus(); 69 | return; 70 | } 71 | el.setAttribute("tabindex", "-1"); 72 | el.focus(); 73 | } 74 | 75 | /*** Code to apply the tweaks when appropriate. ***/ 76 | 77 | function applyTweak(el, tweak) { 78 | if (Array.isArray(tweak.tweak)) { 79 | let [func, ...args] = tweak.tweak; 80 | func(el, ...args); 81 | } else { 82 | tweak.tweak(el); 83 | } 84 | } 85 | 86 | function applyTweaks(root, tweaks, checkRoot, forAttrChange=false) { 87 | for (let tweak of tweaks) { 88 | if (!forAttrChange || tweak.whenAttrChangedOnAncestor !== false) { 89 | for (let el of root.querySelectorAll(tweak.selector)) { 90 | try { 91 | applyTweak(el, tweak); 92 | } catch (e) { 93 | console.log("Exception while applying tweak for '" + tweak.selector + "': " + e); 94 | } 95 | } 96 | } 97 | if (checkRoot && root.matches(tweak.selector)) { 98 | try { 99 | applyTweak(root, tweak); 100 | } catch (e) { 101 | console.log("Exception while applying tweak for '" + tweak.selector + "': " + e); 102 | } 103 | } 104 | } 105 | } 106 | 107 | let observer = new MutationObserver(function(mutations) { 108 | for (let mutation of mutations) { 109 | try { 110 | if (mutation.type === "childList") { 111 | for (let node of mutation.addedNodes) { 112 | if (node.nodeType != Node.ELEMENT_NODE) { 113 | continue; 114 | } 115 | applyTweaks(node, DYNAMIC_TWEAKS, true); 116 | } 117 | } else if (mutation.type === "attributes") { 118 | applyTweaks(mutation.target, DYNAMIC_TWEAKS, true, true); 119 | } 120 | } catch (e) { 121 | // Catch exceptions for individual mutations so other mutations are still handled. 122 | console.log("Exception while handling mutation: " + e); 123 | } 124 | } 125 | }); 126 | 127 | function init() { 128 | applyTweaks(document, LOAD_TWEAKS, false); 129 | applyTweaks(document, DYNAMIC_TWEAKS, false); 130 | options = {childList: true, subtree: true}; 131 | if (DYNAMIC_TWEAK_ATTRIBS.length > 0) { 132 | options.attributes = true; 133 | options.attributeFilter = DYNAMIC_TWEAK_ATTRIBS; 134 | } 135 | observer.observe(document, options); 136 | } 137 | 138 | /*** Define the actual tweaks. ***/ 139 | 140 | // Tweaks that only need to be applied on load. 141 | const LOAD_TWEAKS = [ 142 | ]; 143 | 144 | // Attributes that should be watched for changes and cause dynamic tweaks to be 145 | // applied. 146 | const DYNAMIC_TWEAK_ATTRIBS = []; 147 | 148 | // Tweaks that must be applied whenever an element is added/changed. 149 | const DYNAMIC_TWEAKS = [ 150 | {selector: '.sharedTable__table', 151 | tweak: el => el.setAttribute("role", "table")}, 152 | {selector: '.sharedTable__head, .sharedTable__row', 153 | tweak: el => el.setAttribute("role", "row")}, 154 | // Intervening div between rows and cells which interferes with table 155 | // structure. 156 | {selector: '.sharedTable__details', 157 | tweak: makePresentational}, 158 | {selector: '.sharedTable__head--text', 159 | tweak: el => el.setAttribute("role", "columnheader")}, 160 | {selector: '.sharedTable__column, .sharedTable__details--actions', 161 | tweak: el => el.setAttribute("role", "cell")}, 162 | // IconButton is a