Skip to content

Commit 9e8830f

Browse files
authored
Add new fixes for asus router firmware. (jcsteh#27)
This also adds a new announce() function to the skeleton to announce a message using a temporary live region.
1 parent 89488d3 commit 9e8830f

File tree

3 files changed

+337
-5
lines changed

3 files changed

+337
-5
lines changed

AsusRouterA11yFixes.user.js

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
// ==UserScript==
2+
// @name asus router interface Accessibility Fixes
3+
// @grant unsafeWindow
4+
// @namespace http://axSgrease.derekriemer.org/
5+
// @description Improves the accessibility of the asus router management interface
6+
// @author James Teh <jteh@mozilla.com>, derek riemer <git@derekriemer.com>
7+
// @copyright 2019-2024 Mozilla Corporation, Derek Riemer
8+
// @license Mozilla Public License version 2.0
9+
// @version 2024.1
10+
// @include http://asusrouter.com/*
11+
// @include http://www.asusrouter.com/*
12+
// ==/UserScript==
13+
14+
/*** Functions for common tweaks. ***/
15+
16+
/**
17+
* Adds text to the given live region, and clears it a second later so it's no
18+
* longer perceivable.
19+
* @param {string} regionid an id of a region.
20+
*/
21+
function announce(text, regionId) {
22+
getLiveRegion(regionId)
23+
.then((region) => {
24+
region.innerText = text;
25+
setTimeout(() => {
26+
region.innerText = '';
27+
}, 1000);
28+
});
29+
}
30+
31+
/**
32+
* create or fetch a live region that can be used with announce(). Returns a promise with the region.
33+
* @param {string} id the name of the new live region. This is an html id.
34+
* @return {!Promise<HTMLElement>} a div that contains the live region. This can typically be ignored, this exists to aid in chaining creation of non-existant regions.
35+
*/
36+
function getLiveRegion(id) {
37+
const updatePromise = new Promise((resolve, reject) => {
38+
if (!id) {
39+
reject('Need a valid id!');
40+
return;
41+
}
42+
const existingRegion = document.getElementById(id);
43+
if (existingRegion) {
44+
resolve(existingRegion);
45+
return;
46+
}
47+
const region = document.createElement('div');
48+
region.id = id;
49+
region.setAttribute('aria-live', 'polite');
50+
region.setAttribute('aria-atomic', 'true');
51+
region.style.position = 'absolute';
52+
region.style.width = '50px';
53+
region.style.height = '50px';
54+
region.style.opasity = 0;
55+
document.body.appendChild(region);
56+
// we need to delay a little to get the new region to actually read contents.
57+
// A11y APIs probably don't considder the relevant changes, additions, until
58+
//an annimation frame has passed. It may, in reality be more like 2-4
59+
// annimation frames, so delay 134 ms to be safe.
60+
setTimeout(() => {
61+
resolve(region);
62+
}, 134);
63+
});
64+
return updatePromise;
65+
}
66+
67+
function makeHeading(el, level) {
68+
el.setAttribute("role", "heading");
69+
el.setAttribute("aria-level", level);
70+
}
71+
72+
function makeRegion(el, label) {
73+
el.setAttribute("role", "region");
74+
el.setAttribute("aria-label", label);
75+
}
76+
77+
function makeButton(el, label) {
78+
el.setAttribute("role", "button");
79+
if (label) {
80+
el.setAttribute("aria-label", label);
81+
}
82+
}
83+
84+
function setRole(el, role) {
85+
el.setAttribute('role', role);
86+
}
87+
88+
function makePresentational(el) {
89+
el.setAttribute("role", "presentation");
90+
}
91+
92+
function setLabel(el, label) {
93+
el.setAttribute("aria-label", label);
94+
}
95+
96+
function makeHidden(el) {
97+
el.setAttribute("aria-hidden", "true");
98+
}
99+
100+
function setExpanded(el, expanded) {
101+
el.setAttribute("aria-expanded", expanded ? "true" : "false");
102+
}
103+
104+
var idCounter = 0;
105+
// Get a node's id. If it doesn't have one, make and set one first.
106+
function setAriaIdIfNecessary(elem) {
107+
if (!elem.id) {
108+
elem.setAttribute("id", "axsg-" + idCounter++);
109+
}
110+
return elem.id;
111+
}
112+
113+
function makeElementOwn(parentElement, listOfNodes) {
114+
ids = [];
115+
for (let node of listOfNodes) {
116+
ids.push(setAriaIdIfNecessary(node));
117+
}
118+
parentElement.setAttribute("aria-owns", ids.join(" "));
119+
}
120+
121+
// Focus something even if it wasn't made focusable by the author.
122+
function forceFocus(el) {
123+
let focusable = el.hasAttribute("tabindex");
124+
if (focusable) {
125+
el.focus();
126+
return;
127+
}
128+
el.setAttribute("tabindex", "-1");
129+
el.focus();
130+
}
131+
132+
/*** Code to apply the tweaks when appropriate. ***/
133+
134+
function applyTweak(el, tweak) {
135+
if (Array.isArray(tweak.tweak)) {
136+
let [func, ...args] = tweak.tweak;
137+
func(el, ...args);
138+
} else {
139+
tweak.tweak(el);
140+
}
141+
}
142+
143+
function applyTweaks(root, tweaks, checkRoot) {
144+
for (let tweak of tweaks) {
145+
for (let el of root.querySelectorAll(tweak.selector)) {
146+
try {
147+
applyTweak(el, tweak);
148+
} catch (e) {
149+
console.log("Exception while applying tweak for '" + tweak.selector + "': " + e);
150+
}
151+
}
152+
if (checkRoot && root.matches(tweak.selector)) {
153+
try {
154+
applyTweak(root, tweak);
155+
} catch (e) {
156+
console.log("Exception while applying tweak for '" + tweak.selector + "': " + e);
157+
}
158+
}
159+
}
160+
}
161+
162+
let observer = new MutationObserver(function (mutations) {
163+
for (let mutation of mutations) {
164+
try {
165+
if (mutation.type === "childList") {
166+
for (let node of mutation.addedNodes) {
167+
if (node.nodeType != Node.ELEMENT_NODE) {
168+
continue;
169+
}
170+
applyTweaks(node, DYNAMIC_TWEAKS, true);
171+
}
172+
} else if (mutation.type === "attributes") {
173+
applyTweaks(mutation.target, DYNAMIC_TWEAKS, true);
174+
}
175+
} catch (e) {
176+
// Catch exceptions for individual mutations so other mutations are still handled.
177+
console.log("Exception while handling mutation: " + e);
178+
}
179+
}
180+
});
181+
182+
function init() {
183+
applyTweaks(document, LOAD_TWEAKS, false);
184+
applyTweaks(document, DYNAMIC_TWEAKS, false);
185+
options = { childList: true, subtree: true };
186+
if (DYNAMIC_TWEAK_ATTRIBS.length > 0) {
187+
options.attributes = true;
188+
options.attributeFilter = DYNAMIC_TWEAK_ATTRIBS;
189+
}
190+
observer.observe(document, options);
191+
}
192+
193+
/*** Define the actual tweaks. ***/
194+
195+
// Tweaks that only need to be applied on load.
196+
const LOAD_TWEAKS = [
197+
{
198+
selector: "#op_link",
199+
tweak: el => {
200+
const table = el.closest('table');
201+
setRole(table, 'banner');
202+
// Because I can, make the tbody a list, and each td a list item.
203+
setRole(table.firstElementChild, 'list');
204+
for (let pres of table.firstElementChild.children) {
205+
makePresentational(pres);
206+
}
207+
// td's become listitems
208+
Array.from(table.querySelectorAll('td')).forEach((e) => setRole(e, e.innerText ? 'listitem' : 'none'));
209+
},
210+
},
211+
];
212+
213+
// Attributes that should be watched for changes and cause dynamic tweaks to be
214+
// applied.
215+
const DYNAMIC_TWEAK_ATTRIBS = [];
216+
217+
// Tweaks that must be applied whenever an element is added/changed.
218+
const DYNAMIC_TWEAKS = [
219+
{
220+
selector: '.menu_Desc',
221+
tweak: [setRole, 'link'],
222+
},
223+
{
224+
selector: '.menu_Split',
225+
tweak: [makeHeading, 2],
226+
},
227+
{
228+
selector: '#mainMenu',
229+
tweak: [makeRegion, 'main navigation'],
230+
},
231+
{
232+
selector: '#tabMenu',
233+
tweak: [makeRegion, 'secondary navigation'],
234+
},
235+
{
236+
selector: '#tabMenu td',
237+
tweak: [setRole, 'link'],
238+
},
239+
{
240+
selector: '.formfonttitle',
241+
tweak: [makeHeading, 1],
242+
},
243+
{
244+
selector: 'img[src="/switcherplugin/iphone_switch_container_on.png"]',
245+
tweak: e => {
246+
e.alt = 'on';
247+
},
248+
},
249+
{
250+
selector: "#overDiv_table1",
251+
tweak: e => {
252+
// on rare occasions, this is delayed while the table renders, so
253+
// we wait a quarter second. Also kind of mimics a tutor help
254+
// with most screen readers.
255+
setTimeout(() => {
256+
announce(e.innerText, 'tutor');
257+
}, 250);
258+
},
259+
},
260+
];
261+
262+
/** Add your specific initialization here, so that if you ever update the framework from new skeleton your inits are not overridden. */
263+
function userInit() { }
264+
265+
/*** Lights, camera, action! ***/
266+
init();

framework/axSGreaseSkeleton.js

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,57 @@
1111

1212
/*** Functions for common tweaks. ***/
1313

14+
/**
15+
* Adds text to the given live region, and clears it a second later so it's no
16+
* longer perceivable.
17+
* @param {string} regionid an id of a region.
18+
*/
19+
function announce(text, regionId) {
20+
getLiveRegion(regionId)
21+
.then((region) => {
22+
region.innerText = text;
23+
setTimeout(() => {
24+
region.innerText = '';
25+
}, 1000);
26+
});
27+
}
28+
29+
/**
30+
* create or fetch a live region that can be used with announce(). Returns a promise with the region.
31+
* @param {string} id the name of the new live region. This is an html id.
32+
* @return {!Promise<HTMLElement>} a div that contains the live region. This can typically be ignored, this exists to aid in chaining creation of non-existant regions.
33+
*/
34+
function getLiveRegion(id) {
35+
const updatePromise = new Promise((resolve, reject) => {
36+
if (!id) {
37+
reject('Need a valid id!');
38+
return;
39+
}
40+
const existingRegion = document.getElementById(id);
41+
if (existingRegion) {
42+
resolve(existingRegion);
43+
return;
44+
}
45+
const region = document.createElement('div');
46+
region.id = id;
47+
region.setAttribute('aria-live', 'polite');
48+
region.setAttribute('aria-atomic', 'true');
49+
region.style.position = 'absolute';
50+
region.style.width = '50px';
51+
region.style.height = '50px';
52+
region.style.opasity = 0;
53+
document.body.appendChild(region);
54+
// we need to delay a little to get the new region to actually read contents.
55+
// A11y APIs probably don't treat the relevant changes as "additions" until
56+
//an annimation frame has passed. It may, in reality be more like 2-4
57+
// annimation frames, so delay 134 ms to be safe.
58+
setTimeout(() => {
59+
resolve(region);
60+
}, 134);
61+
});
62+
return updatePromise;
63+
}
64+
1465
function makeHeading(el, level) {
1566
el.setAttribute("role", "heading");
1667
el.setAttribute("aria-level", level);
@@ -53,9 +104,9 @@ function setAriaIdIfNecessary(elem) {
53104
return elem.id;
54105
}
55106

56-
function makeElementOwn(parentElement, listOfNodes){
107+
function makeElementOwn(parentElement, listOfNodes) {
57108
ids = [];
58-
for(let node of listOfNodes){
109+
for (let node of listOfNodes) {
59110
ids.push(setAriaIdIfNecessary(node));
60111
}
61112
parentElement.setAttribute("aria-owns", ids.join(" "));
@@ -83,7 +134,7 @@ function applyTweak(el, tweak) {
83134
}
84135
}
85136

86-
function applyTweaks(root, tweaks, checkRoot, forAttrChange=false) {
137+
function applyTweaks(root, tweaks, checkRoot, forAttrChange = false) {
87138
for (let tweak of tweaks) {
88139
if (!forAttrChange || tweak.whenAttrChangedOnAncestor !== false) {
89140
for (let el of root.querySelectorAll(tweak.selector)) {
@@ -104,7 +155,7 @@ function applyTweaks(root, tweaks, checkRoot, forAttrChange=false) {
104155
}
105156
}
106157

107-
let observer = new MutationObserver(function(mutations) {
158+
let observer = new MutationObserver(function (mutations) {
108159
for (let mutation of mutations) {
109160
try {
110161
if (mutation.type === "childList") {
@@ -127,7 +178,7 @@ let observer = new MutationObserver(function(mutations) {
127178
function init() {
128179
applyTweaks(document, LOAD_TWEAKS, false);
129180
applyTweaks(document, DYNAMIC_TWEAKS, false);
130-
options = {childList: true, subtree: true};
181+
options = { childList: true, subtree: true };
131182
if (DYNAMIC_TWEAK_ATTRIBS.length > 0) {
132183
options.attributes = true;
133184
options.attributeFilter = DYNAMIC_TWEAK_ATTRIBS;
@@ -149,5 +200,9 @@ const DYNAMIC_TWEAK_ATTRIBS = [];
149200
const DYNAMIC_TWEAKS = [
150201
];
151202

203+
/** add your specific initialization here, so that if you ever update the framework from new skeleton your inits are not overridden. */
204+
function userInit(){}
205+
152206
/*** Lights, camera, action! ***/
153207
init();
208+
userInit();

readme.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@ Some newer scripts are missing, some older scripts should be removed, etc.
2020

2121
Following is information about each script.
2222

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+
2334
### Bugzilla Accessibility Fixes
2435
[Download Bugzilla Accessibility Fixes](https://github.com/jcsteh/axSGrease/raw/master/BugzillaA11yFixes.user.js)
2536

0 commit comments

Comments
 (0)