Skip to content

Commit 196c661

Browse files
committed
Add script for VentraIP VIPControl.
1 parent ee6da70 commit 196c661

File tree

1 file changed

+176
-0
lines changed

1 file changed

+176
-0
lines changed

VentraIPControl.user.js

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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 <jamie@jantrid.net>
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, .sharedTable__column, .sharedTable__details--actions',
159+
tweak: el => el.setAttribute("role", "cell")},
160+
// IconButton is a <button> wrapping an icon, but the <button> doesn't
161+
// handle clicks. We make the icon itself a button below. role="presentation"
162+
// doesn't work because it's focusable, so we hackily use role="group" instead.
163+
{selector: '.IconButton',
164+
tweak: el => el.setAttribute("role", "group")},
165+
{selector: '.icon-edit',
166+
tweak: [makeButton, "Edit"]},
167+
{selector: '.icon-delete',
168+
tweak: [makeButton, "Delete"]},
169+
{selector: '.icon-check',
170+
tweak: [makeButton, "Confirm"]},
171+
{selector: '.icon-x',
172+
tweak: [makeButton, "Cancel"]},
173+
];
174+
175+
/*** Lights, camera, action! ***/
176+
init();

0 commit comments

Comments
 (0)