- I made both surfaces friendlier to screen readers.

CMS changes landed in 
[layout.php](/Users/tyemeclifford/Documents/GH/blog/themes/neon/layout.php), 
[style.css](/Users/tyemeclifford/Documents/GH/blog/themes/neon/assets/style.css), 
[app.js](/Users/tyemeclifford/Documents/GH/blog/themes/neon/assets/app.js), 
and 
[lone-embed.php](/Users/tyemeclifford/Documents/GH/blog/lone-embed.php): 
skip link, named search/forms, current page state, live notices/empty 
states, labelled media/embed controls, visible focus states, theme 
control pressed states, and a focus-trapped/lightbox dialog with proper 
ARIA.
Mac app changes landed in 
[main.m](/Users/tyemeclifford/Documents/GH/blog/macclient/AppKitClient/main.m), 
and I rebuilt 
[TCMS](/Users/tyemeclifford/Documents/GH/blog/macclient/TCMS.app/Contents/MacOS/TCMS): 
AppKit controls/tables/fields/previews now get accessibility 
labels/help/value text, workspace sections are named, media/comment 
tables expose useful context, and status messages are announced via 
macOS accessibility notifications.
This commit is contained in:
Ty Clifford
2026-07-05 23:36:27 -04:00
parent 0b28e719ee
commit 75d38d4eb8
6 changed files with 298 additions and 64 deletions
+66 -10
View File
@@ -3,19 +3,33 @@
var storedMode = localStorage.getItem('neon-mode') || 'dark';
var storedAccent = localStorage.getItem('neon-accent') || 'green';
var modeButton = document.querySelector('[data-mode-toggle]');
var accentButtons = document.querySelectorAll('[data-accent]');
function labelAccent(accent) {
return accent.charAt(0).toUpperCase() + accent.slice(1);
}
function applyMode(mode) {
root.dataset.mode = mode;
localStorage.setItem('neon-mode', mode);
if (modeButton) {
modeButton.textContent = mode === 'light' ? 'Dark' : 'Light';
modeButton.setAttribute('aria-label', mode === 'light' ? 'Use dark mode' : 'Use light mode');
modeButton.textContent = 'Light';
modeButton.setAttribute('aria-label', 'Light mode');
modeButton.setAttribute('aria-pressed', mode === 'light' ? 'true' : 'false');
modeButton.title = mode === 'light' ? 'Disable light mode' : 'Enable light mode';
}
}
function applyAccent(accent) {
root.dataset.accent = accent;
localStorage.setItem('neon-accent', accent);
accentButtons.forEach(function (button) {
var selected = button.dataset.accent === accent;
var name = labelAccent(button.dataset.accent || 'accent');
button.setAttribute('aria-pressed', selected ? 'true' : 'false');
button.setAttribute('aria-label', selected ? name + ' accent selected' : 'Use ' + name.toLowerCase() + ' accent');
button.title = selected ? name + ' accent selected' : 'Use ' + name.toLowerCase() + ' accent';
});
}
applyMode(storedMode);
@@ -27,7 +41,7 @@
});
}
document.querySelectorAll('[data-accent]').forEach(function (button) {
accentButtons.forEach(function (button) {
button.addEventListener('click', function () {
applyAccent(button.dataset.accent || 'green');
});
@@ -145,14 +159,15 @@
lightbox.hidden = true;
lightbox.setAttribute('role', 'dialog');
lightbox.setAttribute('aria-modal', 'true');
lightbox.setAttribute('aria-label', 'Post media slideshow');
lightbox.setAttribute('aria-labelledby', 'tcms-lightbox-title');
lightbox.setAttribute('aria-describedby', 'tcms-lightbox-caption');
lightbox.innerHTML = [
'<button class="tcms-lightbox__backdrop" type="button" data-lightbox-close aria-label="Close media viewer"></button>',
'<section class="tcms-lightbox__dialog" role="document">',
'<header class="tcms-lightbox__bar">',
'<strong class="tcms-lightbox__title"></strong>',
'<span class="tcms-lightbox__count"></span>',
'<strong class="tcms-lightbox__title" id="tcms-lightbox-title"></strong>',
'<span class="tcms-lightbox__count" aria-live="polite"></span>',
'<button class="tcms-lightbox__close" type="button" data-lightbox-close aria-label="Close">Close</button>',
'</header>',
'<div class="tcms-lightbox__stage">',
@@ -160,7 +175,7 @@
'<div class="tcms-lightbox__media"></div>',
'<button class="tcms-lightbox__nav tcms-lightbox__nav--next" type="button" data-lightbox-next aria-label="Next media">Next</button>',
'</div>',
'<p class="tcms-lightbox__caption"></p>',
'<p class="tcms-lightbox__caption" id="tcms-lightbox-caption"></p>',
'</section>'
].join('');
@@ -198,6 +213,7 @@
mediaFrame.textContent = '';
titleNode.textContent = entry.title;
countNode.textContent = (currentIndex + 1) + ' of ' + entries.length;
countNode.setAttribute('aria-label', 'Media ' + (currentIndex + 1) + ' of ' + entries.length);
captionNode.textContent = entry.title;
if (entry.type === 'image') {
@@ -219,6 +235,7 @@
video.controls = true;
video.playsInline = true;
video.preload = 'metadata';
video.setAttribute('aria-label', entry.title);
if (entry.poster) {
video.poster = entry.poster;
}
@@ -253,11 +270,47 @@
lightbox.hidden = true;
mediaFrame.textContent = '';
document.body.classList.remove('tcms-lightbox-open');
if (lastFocus && typeof lastFocus.focus === 'function') {
if (lastFocus && document.contains(lastFocus) && typeof lastFocus.focus === 'function') {
lastFocus.focus();
}
}
function trapLightboxFocus(event) {
var focusable;
var first;
var last;
if (!lightbox || lightbox.hidden || event.key !== 'Tab') {
return;
}
focusable = Array.prototype.slice.call(lightbox.querySelectorAll([
'button:not([disabled])',
'a[href]',
'iframe',
'video[controls]',
'[tabindex]:not([tabindex="-1"])'
].join(','))).filter(function (element) {
return !element.hidden && element.offsetParent !== null;
});
if (!focusable.length) {
event.preventDefault();
closeButton.focus();
return;
}
first = focusable[0];
last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
function openFromEvent(event, index) {
event.preventDefault();
event.stopPropagation();
@@ -277,8 +330,9 @@
}
if (tag === 'img') {
trigger.setAttribute('role', 'button');
trigger.setAttribute('aria-label', 'Open image viewer');
trigger.setAttribute('aria-label', 'Open image viewer for ' + entry.title);
}
trigger.setAttribute('aria-haspopup', 'dialog');
trigger.addEventListener('click', function (event) {
openFromEvent(event, index);
});
@@ -294,7 +348,8 @@
button.className = 'tcms-lightbox-trigger';
button.type = 'button';
button.textContent = 'View';
button.setAttribute('aria-label', 'Open media viewer');
button.setAttribute('aria-haspopup', 'dialog');
button.setAttribute('aria-label', 'Open media viewer for ' + entry.title);
button.addEventListener('click', function (event) {
openFromEvent(event, index);
});
@@ -319,6 +374,7 @@
} else if (event.key === 'ArrowLeft') {
showLightboxItem(currentIndex - 1);
}
trapLightboxFocus(event);
});
}