Skip to content

Commit a12c22d

Browse files
committed
Add Messenger.
1. Pressing alt+1 focuses the first chat in the Chats list. 2. Pressing alt+2 focuses the last message in the active chat. 3. Pressing alt+3 focuses the message composer text box.
1 parent 973022e commit a12c22d

File tree

1 file changed

+236
-0
lines changed

1 file changed

+236
-0
lines changed
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
// ==UserScript==
2+
// @name Messenger Accessibility Fixes
3+
// @namespace http://axSgrease.nvaccess.org/
4+
// @description Improves the accessibility of Facebook Messenger.
5+
// @author James Teh <jamie@jantrid.net>
6+
// @copyright 2019-2025 James Teh, Mozilla Corporation, Derek Riemer
7+
// @license Mozilla Public License version 2.0
8+
// @version 2025.1
9+
// @include https://www.messenger.com/*
10+
// ==/UserScript==
11+
12+
/*** Functions for common tweaks. ***/
13+
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+
65+
function makeHeading(el, level) {
66+
el.setAttribute("role", "heading");
67+
el.setAttribute("aria-level", level);
68+
}
69+
70+
function makeRegion(el, label) {
71+
el.setAttribute("role", "region");
72+
el.setAttribute("aria-label", label);
73+
}
74+
75+
function makeButton(el, label) {
76+
el.setAttribute("role", "button");
77+
if (label) {
78+
el.setAttribute("aria-label", label);
79+
}
80+
}
81+
82+
function makePresentational(el) {
83+
el.setAttribute("role", "presentation");
84+
}
85+
86+
function setLabel(el, label) {
87+
el.setAttribute("aria-label", label);
88+
}
89+
90+
function makeHidden(el) {
91+
el.setAttribute("aria-hidden", "true");
92+
}
93+
94+
function setExpanded(el, expanded) {
95+
el.setAttribute("aria-expanded", expanded ? "true" : "false");
96+
}
97+
98+
var idCounter = 0;
99+
// Get a node's id. If it doesn't have one, make and set one first.
100+
function setAriaIdIfNecessary(elem) {
101+
if (!elem.id) {
102+
elem.setAttribute("id", "axsg-" + idCounter++);
103+
}
104+
return elem.id;
105+
}
106+
107+
function makeElementOwn(parentElement, listOfNodes) {
108+
ids = [];
109+
for (let node of listOfNodes) {
110+
ids.push(setAriaIdIfNecessary(node));
111+
}
112+
parentElement.setAttribute("aria-owns", ids.join(" "));
113+
}
114+
115+
// Focus something even if it wasn't made focusable by the author.
116+
function forceFocus(el) {
117+
let focusable = el.hasAttribute("tabindex");
118+
if (focusable) {
119+
el.focus();
120+
return;
121+
}
122+
el.setAttribute("tabindex", "-1");
123+
el.focus();
124+
}
125+
126+
/*** Code to apply the tweaks when appropriate. ***/
127+
128+
function applyTweak(el, tweak) {
129+
if (Array.isArray(tweak.tweak)) {
130+
let [func, ...args] = tweak.tweak;
131+
func(el, ...args);
132+
} else {
133+
tweak.tweak(el);
134+
}
135+
}
136+
137+
function applyTweaks(root, tweaks, checkRoot, forAttrChange = false) {
138+
for (let tweak of tweaks) {
139+
if (!forAttrChange || tweak.whenAttrChangedOnAncestor !== false) {
140+
for (let el of root.querySelectorAll(tweak.selector)) {
141+
try {
142+
applyTweak(el, tweak);
143+
} catch (e) {
144+
console.log("Exception while applying tweak for '" + tweak.selector + "': " + e);
145+
}
146+
}
147+
}
148+
if (checkRoot && root.matches(tweak.selector)) {
149+
try {
150+
applyTweak(root, tweak);
151+
} catch (e) {
152+
console.log("Exception while applying tweak for '" + tweak.selector + "': " + e);
153+
}
154+
}
155+
}
156+
}
157+
158+
let observer = new MutationObserver(function (mutations) {
159+
for (let mutation of mutations) {
160+
try {
161+
if (mutation.type === "childList") {
162+
for (let node of mutation.addedNodes) {
163+
if (node.nodeType != Node.ELEMENT_NODE) {
164+
continue;
165+
}
166+
applyTweaks(node, DYNAMIC_TWEAKS, true);
167+
}
168+
} else if (mutation.type === "attributes") {
169+
applyTweaks(mutation.target, DYNAMIC_TWEAKS, true, true);
170+
}
171+
} catch (e) {
172+
// Catch exceptions for individual mutations so other mutations are still handled.
173+
console.log("Exception while handling mutation: " + e);
174+
}
175+
}
176+
});
177+
178+
function init() {
179+
applyTweaks(document, LOAD_TWEAKS, false);
180+
applyTweaks(document, DYNAMIC_TWEAKS, false);
181+
options = { childList: true, subtree: true };
182+
if (DYNAMIC_TWEAK_ATTRIBS.length > 0) {
183+
options.attributes = true;
184+
options.attributeFilter = DYNAMIC_TWEAK_ATTRIBS;
185+
}
186+
observer.observe(document, options);
187+
}
188+
189+
/*** Define the actual tweaks. ***/
190+
191+
// Tweaks that only need to be applied on load.
192+
const LOAD_TWEAKS = [
193+
];
194+
195+
// Attributes that should be watched for changes and cause dynamic tweaks to be
196+
// applied.
197+
const DYNAMIC_TWEAK_ATTRIBS = [];
198+
199+
// Tweaks that must be applied whenever an element is added/changed.
200+
const DYNAMIC_TWEAKS = [
201+
];
202+
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+
document.addEventListener("keydown", event => {
206+
// Make alt+1 focus the first chat in the Chats list.
207+
if (event.altKey && event.key == "1") {
208+
const firstChat = document.querySelector('[role=navigation] [role=gridcell] a');
209+
if (firstChat) {
210+
firstChat.focus();
211+
}
212+
return;
213+
}
214+
// Make alt+2 focus the last message in the active chat.
215+
if (event.altKey && event.key == "2") {
216+
const messages = document.querySelectorAll('[role=main] [role=row] > div > [role=gridcell]');
217+
if (messages.length > 0) {
218+
const lastMessage = messages[messages.length - 1];
219+
lastMessage.focus();
220+
}
221+
return;
222+
}
223+
// Make alt+3 focus the message composer.
224+
if (event.altKey && event.key == "3") {
225+
const composer = document.querySelector('[role=main] [role=textbox]');
226+
if (composer) {
227+
composer.focus();
228+
}
229+
return;
230+
}
231+
});
232+
}
233+
234+
/*** Lights, camera, action! ***/
235+
init();
236+
userInit();

0 commit comments

Comments
 (0)