Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
266 changes: 266 additions & 0 deletions AsusRouterA11yFixes.user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
// ==UserScript==
// @name asus router interface Accessibility Fixes
// @grant unsafeWindow
// @namespace http://axSgrease.derekriemer.org/
// @description Improves the accessibility of the asus router management interface
// @author James Teh <jteh@mozilla.com>, derek riemer <git@derekriemer.com>
// @copyright 2019-2024 Mozilla Corporation, Derek Riemer
// @license Mozilla Public License version 2.0
// @version 2024.1
// @include http://asusrouter.com/*
// @include http://www.asusrouter.com/*
// ==/UserScript==

/*** Functions for common tweaks. ***/

/**
* Adds text to the given live region, and clears it a second later so it's no
* longer perceivable.
* @param {string} regionid an id of a region.
*/
function announce(text, regionId) {
getLiveRegion(regionId)
.then((region) => {
region.innerText = text;
setTimeout(() => {
region.innerText = '';
}, 1000);
});
}

/**
* create or fetch a live region that can be used with announce(). Returns a promise with the region.
* @param {string} id the name of the new live region. This is an html id.
* @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.
*/
function getLiveRegion(id) {
const updatePromise = new Promise((resolve, reject) => {
if (!id) {
reject('Need a valid id!');
return;
}
const existingRegion = document.getElementById(id);
if (existingRegion) {
resolve(existingRegion);
return;
}
const region = document.createElement('div');
region.id = id;
region.setAttribute('aria-live', 'polite');
region.setAttribute('aria-atomic', 'true');
region.style.position = 'absolute';
region.style.width = '50px';
region.style.height = '50px';
region.style.opasity = 0;
document.body.appendChild(region);
// we need to delay a little to get the new region to actually read contents.
// A11y APIs probably don't considder the relevant changes, additions, until
//an annimation frame has passed. It may, in reality be more like 2-4
// annimation frames, so delay 134 ms to be safe.
setTimeout(() => {
resolve(region);
}, 134);
});
return updatePromise;
}

function makeHeading(el, level) {
el.setAttribute("role", "heading");
el.setAttribute("aria-level", level);
}

function makeRegion(el, label) {
el.setAttribute("role", "region");
el.setAttribute("aria-label", label);
}

function makeButton(el, label) {
el.setAttribute("role", "button");
if (label) {
el.setAttribute("aria-label", label);
}
}

function setRole(el, role) {
el.setAttribute('role', role);
}

function makePresentational(el) {
el.setAttribute("role", "presentation");
}

function setLabel(el, label) {
el.setAttribute("aria-label", label);
}

function makeHidden(el) {
el.setAttribute("aria-hidden", "true");
}

function setExpanded(el, expanded) {
el.setAttribute("aria-expanded", expanded ? "true" : "false");
}

var idCounter = 0;
// Get a node's id. If it doesn't have one, make and set one first.
function setAriaIdIfNecessary(elem) {
if (!elem.id) {
elem.setAttribute("id", "axsg-" + idCounter++);
}
return elem.id;
}

function makeElementOwn(parentElement, listOfNodes) {
ids = [];
for (let node of listOfNodes) {
ids.push(setAriaIdIfNecessary(node));
}
parentElement.setAttribute("aria-owns", ids.join(" "));
}

// Focus something even if it wasn't made focusable by the author.
function forceFocus(el) {
let focusable = el.hasAttribute("tabindex");
if (focusable) {
el.focus();
return;
}
el.setAttribute("tabindex", "-1");
el.focus();
}

/*** Code to apply the tweaks when appropriate. ***/

function applyTweak(el, tweak) {
if (Array.isArray(tweak.tweak)) {
let [func, ...args] = tweak.tweak;
func(el, ...args);
} else {
tweak.tweak(el);
}
}

function applyTweaks(root, tweaks, checkRoot) {
for (let tweak of tweaks) {
for (let el of root.querySelectorAll(tweak.selector)) {
try {
applyTweak(el, tweak);
} catch (e) {
console.log("Exception while applying tweak for '" + tweak.selector + "': " + e);
}
}
if (checkRoot && root.matches(tweak.selector)) {
try {
applyTweak(root, tweak);
} catch (e) {
console.log("Exception while applying tweak for '" + tweak.selector + "': " + e);
}
}
}
}

let observer = new MutationObserver(function (mutations) {
for (let mutation of mutations) {
try {
if (mutation.type === "childList") {
for (let node of mutation.addedNodes) {
if (node.nodeType != Node.ELEMENT_NODE) {
continue;
}
applyTweaks(node, DYNAMIC_TWEAKS, true);
}
} else if (mutation.type === "attributes") {
applyTweaks(mutation.target, DYNAMIC_TWEAKS, true);
}
} catch (e) {
// Catch exceptions for individual mutations so other mutations are still handled.
console.log("Exception while handling mutation: " + e);
}
}
});

function init() {
applyTweaks(document, LOAD_TWEAKS, false);
applyTweaks(document, DYNAMIC_TWEAKS, false);
options = { childList: true, subtree: true };
if (DYNAMIC_TWEAK_ATTRIBS.length > 0) {
options.attributes = true;
options.attributeFilter = DYNAMIC_TWEAK_ATTRIBS;
}
observer.observe(document, options);
}

/*** Define the actual tweaks. ***/

// Tweaks that only need to be applied on load.
const LOAD_TWEAKS = [
{
selector: "#op_link",
tweak: el => {
const table = el.closest('table');
setRole(table, 'banner');
// Because I can, make the tbody a list, and each td a list item.
setRole(table.firstElementChild, 'list');
for (let pres of table.firstElementChild.children) {
makePresentational(pres);
}
// td's become listitems
Array.from(table.querySelectorAll('td')).forEach((e) => setRole(e, e.innerText ? 'listitem' : 'none'));
},
},
];

// Attributes that should be watched for changes and cause dynamic tweaks to be
// applied.
const DYNAMIC_TWEAK_ATTRIBS = [];

// Tweaks that must be applied whenever an element is added/changed.
const DYNAMIC_TWEAKS = [
{
selector: '.menu_Desc',
tweak: [setRole, 'link'],
},
{
selector: '.menu_Split',
tweak: [makeHeading, 2],
},
{
selector: '#mainMenu',
tweak: [makeRegion, 'main navigation'],
},
{
selector: '#tabMenu',
tweak: [makeRegion, 'secondary navigation'],
},
{
selector: '#tabMenu td',
tweak: [setRole, 'link'],
},
{
selector: '.formfonttitle',
tweak: [makeHeading, 1],
},
{
selector: 'img[src="/switcherplugin/iphone_switch_container_on.png"]',
tweak: e => {
e.alt = 'on';
},
},
{
selector: "#overDiv_table1",
tweak: e => {
// on rare occasions, this is delayed while the table renders, so
// we wait a quarter second. Also kind of mimics a tutor help
// with most screen readers.
setTimeout(() => {
announce(e.innerText, 'tutor');
}, 250);
},
},
];

/** Add your specific initialization here, so that if you ever update the framework from new skeleton your inits are not overridden. */
function userInit() { }

/*** Lights, camera, action! ***/
init();
65 changes: 60 additions & 5 deletions framework/axSGreaseSkeleton.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,57 @@

/*** Functions for common tweaks. ***/

/**
* Adds text to the given live region, and clears it a second later so it's no
* longer perceivable.
* @param {string} regionid an id of a region.
*/
function announce(text, regionId) {
getLiveRegion(regionId)
.then((region) => {
region.innerText = text;
setTimeout(() => {
region.innerText = '';
}, 1000);
});
}

/**
* create or fetch a live region that can be used with announce(). Returns a promise with the region.
* @param {string} id the name of the new live region. This is an html id.
* @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.
*/
function getLiveRegion(id) {
const updatePromise = new Promise((resolve, reject) => {
if (!id) {
reject('Need a valid id!');
return;
}
const existingRegion = document.getElementById(id);
if (existingRegion) {
resolve(existingRegion);
return;
}
const region = document.createElement('div');
region.id = id;
region.setAttribute('aria-live', 'polite');
region.setAttribute('aria-atomic', 'true');
region.style.position = 'absolute';
region.style.width = '50px';
region.style.height = '50px';
region.style.opasity = 0;
document.body.appendChild(region);
// we need to delay a little to get the new region to actually read contents.
// A11y APIs probably don't treat the relevant changes as "additions" until
//an annimation frame has passed. It may, in reality be more like 2-4
// annimation frames, so delay 134 ms to be safe.
setTimeout(() => {
resolve(region);
}, 134);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just out of curiosity, why 134 (as opposed to 130 or similar)? Is 134 significant somehow?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1000/30 (frames per second) is 3.333... * 3 is 333 ...4 when rounded. Ballpark math tbh.

});
return updatePromise;
}

function makeHeading(el, level) {
el.setAttribute("role", "heading");
el.setAttribute("aria-level", level);
Expand Down Expand Up @@ -53,9 +104,9 @@ function setAriaIdIfNecessary(elem) {
return elem.id;
}

function makeElementOwn(parentElement, listOfNodes){
function makeElementOwn(parentElement, listOfNodes) {
ids = [];
for(let node of listOfNodes){
for (let node of listOfNodes) {
ids.push(setAriaIdIfNecessary(node));
}
parentElement.setAttribute("aria-owns", ids.join(" "));
Expand Down Expand Up @@ -83,7 +134,7 @@ function applyTweak(el, tweak) {
}
}

function applyTweaks(root, tweaks, checkRoot, forAttrChange=false) {
function applyTweaks(root, tweaks, checkRoot, forAttrChange = false) {
for (let tweak of tweaks) {
if (!forAttrChange || tweak.whenAttrChangedOnAncestor !== false) {
for (let el of root.querySelectorAll(tweak.selector)) {
Expand All @@ -104,7 +155,7 @@ function applyTweaks(root, tweaks, checkRoot, forAttrChange=false) {
}
}

let observer = new MutationObserver(function(mutations) {
let observer = new MutationObserver(function (mutations) {
for (let mutation of mutations) {
try {
if (mutation.type === "childList") {
Expand All @@ -127,7 +178,7 @@ let observer = new MutationObserver(function(mutations) {
function init() {
applyTweaks(document, LOAD_TWEAKS, false);
applyTweaks(document, DYNAMIC_TWEAKS, false);
options = {childList: true, subtree: true};
options = { childList: true, subtree: true };
if (DYNAMIC_TWEAK_ATTRIBS.length > 0) {
options.attributes = true;
options.attributeFilter = DYNAMIC_TWEAK_ATTRIBS;
Expand All @@ -149,5 +200,9 @@ const DYNAMIC_TWEAK_ATTRIBS = [];
const DYNAMIC_TWEAKS = [
];

/** add your specific initialization here, so that if you ever update the framework from new skeleton your inits are not overridden. */
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/** add your specific initialization here, so that if you ever update the framework from new skeleton your inits are not overridden. */
/** Add your specific initialization here, so that if you ever update the framework from new skeleton your inits are not overridden. */

function userInit(){}

/*** Lights, camera, action! ***/
init();
userInit();
11 changes: 11 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ Some newer scripts are missing, some older scripts should be removed, etc.

Following is information about each script.

### Asus Router Accessibility fixes

[Download Asus Router Accessibility Fixes](https://github.com/jcsteh/axSGrease/raw/master/AsusRouterA11yFixes.user.js)
This script improves the accessibility of the asus router firmware. (this has only been tested on RT-AX56U router). it does the following:

- makes tutor help messages automatically read.
- Creates a primary and secondary navigation region, and removes layout tables for navigation.
- Adds section headers to the nav menu, at heading level 2.
- Makes pages that have a title have an h1.
- Labels some unlabeled images.

### Bugzilla Accessibility Fixes
[Download Bugzilla Accessibility Fixes](https://github.com/jcsteh/axSGrease/raw/master/BugzillaA11yFixes.user.js)

Expand Down