From a8af92dd13e047a2efa7866f5c8c2c02354ef771 Mon Sep 17 00:00:00 2001 From: James Teh Date: Sat, 9 Aug 2025 14:42:00 +1000 Subject: [PATCH 01/11] GitHub: Make the add line comment buttons always visible. Recently, GitHub changed these so that they only appear on hover, which is nasty for screen reader users. Fixes #30. --- GitHubA11yFixes.user.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/GitHubA11yFixes.user.js b/GitHubA11yFixes.user.js index 4775409..dbe1f19 100644 --- a/GitHubA11yFixes.user.js +++ b/GitHubA11yFixes.user.js @@ -3,9 +3,9 @@ // @namespace http://axSgrease.nvaccess.org/ // @description Improves the accessibility of GitHub. // @author James Teh -// @copyright 2019-2024 Mozilla Corporation, Derek Riemer +// @copyright 2019-2025 Mozilla Corporation, Derek Riemer // @license Mozilla Public License version 2.0 -// @version 2024.2 +// @version 2025.1 // @include https://github.com/* // ==/UserScript== @@ -136,6 +136,11 @@ const DYNAMIC_TWEAKS = [ if (code && comment) { makeElementOwn(cell, [code, comment]); } + if (comment) { + // These buttons only appear on hover, which sucks for screen reader users. + // Make them always visible. + comment.style.display = "block"; + } }}, // Make non-comment events into headings; e.g. closing/referencing an issue, // approving/requesting changes to a PR, merging a PR. Exclude commits and From 2b9a3c55871ee2f6c93474fb1e619e8927ac90db Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Sat, 9 Aug 2025 20:35:43 +1000 Subject: [PATCH 02/11] Make PR filenames headings and label the file contents (#29) - Makes the filenames in diff viewers level 2 headings. - Moves the filename first, rather than after a bunch of controls for that file. - Makes the file contents a region labelled by the filename. Useful when navigating by table rather than by heading. --- GitHubA11yFixes.user.js | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/GitHubA11yFixes.user.js b/GitHubA11yFixes.user.js index dbe1f19..f7a3fef 100644 --- a/GitHubA11yFixes.user.js +++ b/GitHubA11yFixes.user.js @@ -2,10 +2,10 @@ // @name GitHub Accessibility Fixes // @namespace http://axSgrease.nvaccess.org/ // @description Improves the accessibility of GitHub. -// @author James Teh -// @copyright 2019-2025 Mozilla Corporation, Derek Riemer +// @author James Teh , Sascha Cowley +// @copyright 2019-2025 Mozilla Corporation, Derek Riemer, Sascha Cowley // @license Mozilla Public License version 2.0 -// @version 2025.1 +// @version 2025.2 // @include https://github.com/* // ==/UserScript== @@ -168,6 +168,27 @@ const DYNAMIC_TWEAKS = [ // Remove headings from folder and file lists. {selector: 'table[aria-labelledby=folders-and-files] :is(h2, h3)', tweak: makePresentational}, + // Make file viewer filenames headings, and the first item in the file viewer. + {selector: '.file-header .file-info .Truncate:has(.Link--primary)', + tweak: el => { + makeHeading(el, 2); + const headerRow = el.parentElement; + const children = Array.from(headerRow.children); + // Filename is the last child of .file-info, make it the first + children.unshift(children.pop()); + if (headerRow) { + makeElementOwn(headerRow, children); + } + }}, + // Label diffs and the like with their filename. + {selector: '.file', + tweak: el => { + const label = el.querySelector(".Link--primary"); + const file = el.querySelector(".js-file-content"); + if (label && file) { + makeRegion(file, label.textContent); + } + }}, ]; /*** Lights, camera, action! ***/ From fd1921608aaa61d8b3bc819fa2b1ce4da2031a04 Mon Sep 17 00:00:00 2001 From: James Teh Date: Tue, 9 Sep 2025 16:05:08 +1000 Subject: [PATCH 03/11] Phabricator: Make reviewer status icons accessible (remove aria-hidden, set role, set label). --- PhabricatorA11yFixes.user.js | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/PhabricatorA11yFixes.user.js b/PhabricatorA11yFixes.user.js index 1377201..652055e 100644 --- a/PhabricatorA11yFixes.user.js +++ b/PhabricatorA11yFixes.user.js @@ -3,9 +3,9 @@ // @namespace http://axSgrease.nvaccess.org/ // @description Improves the accessibility of Phabricator. // @author James Teh -// @copyright 2018 Mozilla Corporation +// @copyright 2018-2025 Mozilla Corporation // @license Mozilla Public License version 2.0 -// @version 2018.2 +// @version 2025.1 // @grant GM_log // @include https://phabricator.services.mozilla.com/D* // ==/UserScript== @@ -35,6 +35,12 @@ function setLabel(el, label) { el.setAttribute("aria-label", label); } +function labelHiddenStatusIcon(el, label) { + el.removeAttribute("aria-hidden"); + el.setAttribute("role", "image"); + setLabel(el, label); +} + /*** Code to apply the tweaks when appropriate. ***/ function applyTweaks(root, tweaks) { @@ -90,6 +96,21 @@ const LOAD_TWEAKS = [ // The diff is an h1, so the files inside the diff should be an h2, not an h1. {selector: '.differential-file-icon-header', tweak: [makeHeading, 2]}, + // Reviewer status icons. + {selector: '.phui-status-item-target .fa-circle-o', + tweak: [labelHiddenStatusIcon, "pending"]}, + {selector: '.phui-status-item-target .fa-minus-circle', + tweak: [labelHiddenStatusIcon, "pending blocking"]}, + {selector: '.phui-status-item-target .fa-check-circle', + tweak: [labelHiddenStatusIcon, "accepted"]}, + {selector: '.phui-status-item-target .fa-check-circle-o', + tweak: [labelHiddenStatusIcon, "accepted prior"]}, + {selector: '.phui-status-item-target .fa-times-circle', + tweak: [labelHiddenStatusIcon, "requested changes"]}, + {selector: '.phui-status-item-target .fa-times-circle-o', + tweak: [labelHiddenStatusIcon, "requested changes to prior"]}, + {selector: '.phui-status-item-target .fa-comment', + tweak: [labelHiddenStatusIcon, "comment"]}, ] // Tweaks that must be applied whenever a node is added. From 9d21ed0eeb6e5a432bf137bb9366e77907c64f88 Mon Sep 17 00:00:00 2001 From: James Teh Date: Sun, 2 Nov 2025 20:47:39 +1000 Subject: [PATCH 04/11] Add WhatsApp. 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 the context menu key opens the context menu for the current chat or message. 4. With NVDA, the reaction picker no longer switches to browse mode and starts reading all the reactions. --- WhatsAppAccessibilityFixes.user.js | 261 +++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 WhatsAppAccessibilityFixes.user.js diff --git a/WhatsAppAccessibilityFixes.user.js b/WhatsAppAccessibilityFixes.user.js new file mode 100644 index 0000000..60605d0 --- /dev/null +++ b/WhatsAppAccessibilityFixes.user.js @@ -0,0 +1,261 @@ +// ==UserScript== +// @name WhatsApp Web Accessibility Fixes +// @namespace http://axSgrease.nvaccess.org/ +// @description Improves the accessibility of WhatsApp Web. +// @author James Teh +// @copyright 2019-2025 Mozilla Corporation, Derek Riemer, James Teh +// @license Mozilla Public License version 2.0 +// @version 2025.1 +// @include https://web.whatsapp.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} 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); + }); + 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 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, forAttrChange = false) { + for (let tweak of tweaks) { + if (!forAttrChange || tweak.whenAttrChangedOnAncestor !== false) { + 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, 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 = [ +]; + +// 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: '[role=dialog]', + tweak: el => { + if (el.querySelector('[role=button][aria-pressed] img')) { + // This is the reaction picker. It contains buttons which wilh switch to + // browse mode, but we want to use WhatsApp's own arrow key navigation + // here. Therefore, use role="application". Menu would be more appropriate, + // but that's tricky because WhatsApp uses aria-pressed on the buttons. + el.role = "application"; + } + } + }, +]; + +/** add your specific initialization here, so that if you ever update the framework from new skeleton your inits are not overridden. */ +function userInit(){ + document.addEventListener("keydown", event => { + // Make alt+1 focus the first chat in the Chats list. + if (event.altKey && event.key == "1") { + const firstChat = document.querySelector('[role=grid] [role=gridcell] [aria-selected]'); + if (firstChat) { + firstChat.focus(); + } + return; + } + // Make alt+2 focus the last message in the active chat. + if (event.altKey && event.key == "2") { + const messages = document.querySelectorAll(".focusable-list-item"); + if (messages.length > 0) { + const lastMessage = messages[messages.length - 1]; + if (!lastMessage.hasAttribute("tabindex")) { + // Messages don't initially have the tabindex attribute, but they gain it + // once you navigate to them with the keyboard. + lastMessage.setAttribute("tabindex", 0); + } + lastMessage.focus(); + } + return; + } + }); + document.addEventListener("contextmenu", event => { + // The context menu key doesn't work properly for chat and message items. + // Fix that. + const focus = document.activeElement; + const button = focus.querySelector( + focus.classList.contains("focusable-list-item") ? + // Message + '[role=button][aria-expanded]' : + // Chat + 'button' + ); + if (button) { + event.preventDefault(); + event.stopPropagation(); + button.click(); + } + }, { capture: true }); +} + +/*** Lights, camera, action! ***/ +init(); +userInit(); From 973022e27ce6e6fe25e75ecbcb8e0364df2031e5 Mon Sep 17 00:00:00 2001 From: James Teh Date: Mon, 3 Nov 2025 08:30:05 +1000 Subject: [PATCH 05/11] GitHub: Don't make an entire pull request review into a heading. --- GitHubA11yFixes.user.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GitHubA11yFixes.user.js b/GitHubA11yFixes.user.js index f7a3fef..4eeb1f0 100644 --- a/GitHubA11yFixes.user.js +++ b/GitHubA11yFixes.user.js @@ -5,7 +5,7 @@ // @author James Teh , Sascha Cowley // @copyright 2019-2025 Mozilla Corporation, Derek Riemer, Sascha Cowley // @license Mozilla Public License version 2.0 -// @version 2025.2 +// @version 2025.3 // @include https://github.com/* // ==/UserScript== @@ -146,7 +146,7 @@ const DYNAMIC_TWEAKS = [ // approving/requesting changes to a PR, merging a PR. Exclude commits and // commit references because these contain too much detail and there's no // way to separate the header from the body. - {selector: '.TimelineItem:not(.js-commit) .TimelineItem-body:not(.my-0):not(.discussion-comment):not([id^="ref-commit-"])', + {selector: '.TimelineItem:not(.js-commit, .pt-0) .TimelineItem-body:not(.my-0):not(.discussion-comment):not([id^="ref-commit-"])', tweak: [makeHeading, 3]}, // Issue listing tables. {selector: '.js-navigation-container:not(.commits-listing)', From a12c22da28bd624b3d6098e0b7aa1f8216782d66 Mon Sep 17 00:00:00 2001 From: James Teh Date: Mon, 3 Nov 2025 12:12:06 +1000 Subject: [PATCH 06/11] 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. --- MessengerAccessibilityFixes.user.js | 236 ++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 MessengerAccessibilityFixes.user.js diff --git a/MessengerAccessibilityFixes.user.js b/MessengerAccessibilityFixes.user.js new file mode 100644 index 0000000..c94339a --- /dev/null +++ b/MessengerAccessibilityFixes.user.js @@ -0,0 +1,236 @@ +// ==UserScript== +// @name Messenger Accessibility Fixes +// @namespace http://axSgrease.nvaccess.org/ +// @description Improves the accessibility of Facebook Messenger. +// @author James Teh +// @copyright 2019-2025 James Teh, Mozilla Corporation, Derek Riemer +// @license Mozilla Public License version 2.0 +// @version 2025.1 +// @include https://www.messenger.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} 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); + }); + 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 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, forAttrChange = false) { + for (let tweak of tweaks) { + if (!forAttrChange || tweak.whenAttrChangedOnAncestor !== false) { + 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, 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 = [ +]; + +// 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 = [ +]; + +/** add your specific initialization here, so that if you ever update the framework from new skeleton your inits are not overridden. */ +function userInit(){ + document.addEventListener("keydown", event => { + // Make alt+1 focus the first chat in the Chats list. + if (event.altKey && event.key == "1") { + const firstChat = document.querySelector('[role=navigation] [role=gridcell] a'); + if (firstChat) { + firstChat.focus(); + } + return; + } + // Make alt+2 focus the last message in the active chat. + if (event.altKey && event.key == "2") { + const messages = document.querySelectorAll('[role=main] [role=row] > div > [role=gridcell]'); + if (messages.length > 0) { + const lastMessage = messages[messages.length - 1]; + lastMessage.focus(); + } + return; + } + // Make alt+3 focus the message composer. + if (event.altKey && event.key == "3") { + const composer = document.querySelector('[role=main] [role=textbox]'); + if (composer) { + composer.focus(); + } + return; + } + }); +} + +/*** Lights, camera, action! ***/ +init(); +userInit(); From 5e6f77904a4b2d6041383804258e8e2ccb9ce4bf Mon Sep 17 00:00:00 2001 From: James Teh Date: Fri, 7 Nov 2025 13:03:08 +1000 Subject: [PATCH 07/11] Rename WhatsApp and Messenger for consistency. --- MessengerAccessibilityFixes.user.js => MessengerA11yFixes.user.js | 0 WhatsAppAccessibilityFixes.user.js => WhatsAppA11yFixes.user.js | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename MessengerAccessibilityFixes.user.js => MessengerA11yFixes.user.js (100%) rename WhatsAppAccessibilityFixes.user.js => WhatsAppA11yFixes.user.js (100%) diff --git a/MessengerAccessibilityFixes.user.js b/MessengerA11yFixes.user.js similarity index 100% rename from MessengerAccessibilityFixes.user.js rename to MessengerA11yFixes.user.js diff --git a/WhatsAppAccessibilityFixes.user.js b/WhatsAppA11yFixes.user.js similarity index 100% rename from WhatsAppAccessibilityFixes.user.js rename to WhatsAppA11yFixes.user.js From 7bc0551e52f0e326b6896439dfe1d1d683dd82a1 Mon Sep 17 00:00:00 2001 From: James Teh Date: Sat, 20 Dec 2025 09:42:00 +1000 Subject: [PATCH 08/11] WhatsApp: Stop WhatsApp from forcing focus into the message list when switching away from the browser tab. --- WhatsAppA11yFixes.user.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/WhatsAppA11yFixes.user.js b/WhatsAppA11yFixes.user.js index 60605d0..58bb190 100644 --- a/WhatsAppA11yFixes.user.js +++ b/WhatsAppA11yFixes.user.js @@ -5,7 +5,7 @@ // @author James Teh // @copyright 2019-2025 Mozilla Corporation, Derek Riemer, James Teh // @license Mozilla Public License version 2.0 -// @version 2025.1 +// @version 2025.2 // @include https://web.whatsapp.com/ // ==/UserScript== @@ -254,6 +254,15 @@ function userInit(){ button.click(); } }, { capture: true }); + document.addEventListener("focusout", event => { + // When the tab loses focus, WhatsApp forces focus into the message list, + // regardless of where it was before. Override that absurdity. + setTimeout(() => { + if (!document.hasFocus() && document.activeElement != event.target) { + event.target.focus(); + } + }, 50); + }); } /*** Lights, camera, action! ***/ From 7e537e720e079418d8f64345e2dbceb420bf40de Mon Sep 17 00:00:00 2001 From: James Teh Date: Sat, 7 Mar 2026 10:31:18 +1000 Subject: [PATCH 09/11] WhatsApp: Remove phone numbers from the labels of messages from unknown contacts. --- WhatsAppA11yFixes.user.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/WhatsAppA11yFixes.user.js b/WhatsAppA11yFixes.user.js index 58bb190..c1ec165 100644 --- a/WhatsAppA11yFixes.user.js +++ b/WhatsAppA11yFixes.user.js @@ -3,9 +3,9 @@ // @namespace http://axSgrease.nvaccess.org/ // @description Improves the accessibility of WhatsApp Web. // @author James Teh -// @copyright 2019-2025 Mozilla Corporation, Derek Riemer, James Teh +// @copyright 2019-2026 Mozilla Corporation, Derek Riemer, James Teh // @license Mozilla Public License version 2.0 -// @version 2025.2 +// @version 2026.1 // @include https://web.whatsapp.com/ // ==/UserScript== @@ -194,7 +194,7 @@ const LOAD_TWEAKS = [ // Attributes that should be watched for changes and cause dynamic tweaks to be // applied. -const DYNAMIC_TWEAK_ATTRIBS = []; +const DYNAMIC_TWEAK_ATTRIBS = ["aria-label"]; // Tweaks that must be applied whenever an element is added/changed. const DYNAMIC_TWEAKS = [ @@ -209,6 +209,17 @@ const DYNAMIC_TWEAKS = [ } } }, + {selector: '.message-in[aria-label]', + tweak: el => { + // Remove phone numbers for unknown contacts. These have a slightly different + // format for normal chats and group chats. + const m = el.ariaLabel.match( + /^(?:Maybe (.*? ))?\+\d{1,2} [\d ]+ (.*)$/s + ); + if (m) { + el.ariaLabel = `Maybe ${m[1] || ""} ${m[2]}`; + } + }}, ]; /** add your specific initialization here, so that if you ever update the framework from new skeleton your inits are not overridden. */ From ac05b45e70db6a6b23b4dcba282a6887d22cf57c Mon Sep 17 00:00:00 2001 From: James Teh Date: Sat, 7 Mar 2026 20:52:08 +1000 Subject: [PATCH 10/11] WhatsApp: Fix unknown number stripping for country codes with 3 digits and non-English text. --- WhatsAppA11yFixes.user.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WhatsAppA11yFixes.user.js b/WhatsAppA11yFixes.user.js index c1ec165..6b24389 100644 --- a/WhatsAppA11yFixes.user.js +++ b/WhatsAppA11yFixes.user.js @@ -214,10 +214,10 @@ const DYNAMIC_TWEAKS = [ // Remove phone numbers for unknown contacts. These have a slightly different // format for normal chats and group chats. const m = el.ariaLabel.match( - /^(?:Maybe (.*? ))?\+\d{1,2} [\d ]+ (.*)$/s + /^(?:[^\d ]+ (.*? ))?\+\d{1,3} [\d ]+ (.*)$/s ); if (m) { - el.ariaLabel = `Maybe ${m[1] || ""} ${m[2]}`; + el.ariaLabel = `~ ${m[1] || ""} ${m[2]}`; } }}, ]; From 306df1cffddf0109c30788a353521ed5353b2c7c Mon Sep 17 00:00:00 2001 From: Brandon <9094821+serrebi@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:32:08 -0700 Subject: [PATCH 11/11] Add support for facebook.com/messages in MessengerA11yFixes (#32) Facebook Messenger is also accessible at facebook.com/messages, which uses the same UI and ARIA roles as messenger.com. This adds an @include so the accessibility fixes and keyboard shortcuts (Alt+1/2/3) also work on that URL. --- MessengerA11yFixes.user.js | 1 + 1 file changed, 1 insertion(+) diff --git a/MessengerA11yFixes.user.js b/MessengerA11yFixes.user.js index c94339a..ec97d60 100644 --- a/MessengerA11yFixes.user.js +++ b/MessengerA11yFixes.user.js @@ -7,6 +7,7 @@ // @license Mozilla Public License version 2.0 // @version 2025.1 // @include https://www.messenger.com/* +// @include https://www.facebook.com/messages/* // ==/UserScript== /*** Functions for common tweaks. ***/