- 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:
+66
-10
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,15 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
html {
|
||||
min-height: 100%;
|
||||
}
|
||||
@@ -67,6 +76,15 @@ a:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
a:focus-visible,
|
||||
button:focus-visible,
|
||||
input:focus-visible,
|
||||
textarea:focus-visible,
|
||||
[tabindex="-1"]:focus-visible {
|
||||
outline: 3px solid color-mix(in srgb, var(--accent) 72%, white);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
textarea {
|
||||
@@ -94,6 +112,26 @@ button:hover {
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
|
||||
}
|
||||
|
||||
.skip-link {
|
||||
position: fixed;
|
||||
top: 0.75rem;
|
||||
left: 0.75rem;
|
||||
z-index: 2000;
|
||||
transform: translateY(-160%);
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 72%, white);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.55rem 0.75rem;
|
||||
color: #06100b;
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
||||
font-weight: 800;
|
||||
text-decoration: none;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
width: 100%;
|
||||
@@ -221,6 +259,14 @@ textarea {
|
||||
.swatch--purple { background: #a084ff; }
|
||||
.swatch--orange { background: #ff9f43; }
|
||||
|
||||
.mode-toggle[aria-pressed="true"],
|
||||
.swatch[aria-pressed="true"] {
|
||||
border-color: color-mix(in srgb, var(--accent) 80%, white);
|
||||
box-shadow:
|
||||
0 0 0 3px color-mix(in srgb, var(--accent) 24%, transparent),
|
||||
0 0 18px color-mix(in srgb, var(--accent) 34%, transparent);
|
||||
}
|
||||
|
||||
.site-shell {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 280px;
|
||||
|
||||
Reference in New Issue
Block a user