a11y: comprehensive WCAG 2.1 Level AA accessibility audit

- Add skip-to-main-content link and main content ID target
- Add prefers-reduced-motion media queries for all animations
- Enhance visible focus indicators (2px offset, high-contrast ring)
- Replace ~160 text-surface-500 instances with text-surface-600/dark:text-surface-400
  for 4.5:1+ contrast ratio compliance
- Add aria-hidden="true" to ~30+ decorative SVG icons across sidebars/widgets
- Convert facepile containers from div to semantic ul/li with role="list"
- Add aria-label to icon-only buttons (share, sort controls)
- Add sr-only labels to form inputs (webmention, search)
- Add aria-live="polite" to dynamically loaded webmentions
- Add aria-label with relative+absolute date to time-difference component
- Add keyboard handlers (Enter/Space) to custom interactive elements
- Add aria-label to nav landmarks (table of contents)
- Fix modal focus trap and dialog accessibility
- Fix lightbox keyboard navigation and screen reader announcements

Confab-Link: http://localhost:8080/sessions/edb1b7b0-da66-4486-bd9c-d1cfa7553b88
This commit is contained in:
Ricardo
2026-03-07 18:58:08 +01:00
parent db75bd05ea
commit e236b4bf65
77 changed files with 638 additions and 505 deletions

View File

@@ -11,6 +11,7 @@ document.addEventListener("alpine:init", () => {
alt: "",
images: [],
currentIndex: 0,
triggerElement: null,
init() {
const container = this.$root;
@@ -21,14 +22,24 @@ document.addEventListener("alpine:init", () => {
this.images.forEach((img, i) => {
img.style.cursor = "zoom-in";
img.setAttribute("tabindex", "0");
img.setAttribute("role", "button");
img.setAttribute("aria-label", (img.alt || "Image") + " — click to enlarge");
img.addEventListener("click", (e) => {
e.preventDefault();
this.show(i);
});
img.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
this.show(i);
}
});
});
},
show(index) {
this.triggerElement = this.images[index];
this.currentIndex = index;
const img = this.images[index];
// Use the largest source available
@@ -48,12 +59,22 @@ document.addEventListener("alpine:init", () => {
this.alt = img.alt || "";
this.open = true;
document.body.style.overflow = "hidden";
// Move focus to close button for keyboard users
this.$nextTick(() => {
const closeBtn = document.querySelector('[x-ref="closeBtn"]');
if (closeBtn) closeBtn.focus();
});
},
close() {
this.open = false;
this.src = "";
document.body.style.overflow = "";
// Return focus to the image that triggered the lightbox
if (this.triggerElement) {
this.triggerElement.focus();
this.triggerElement = null;
}
},
next() {

View File

@@ -78,9 +78,12 @@ class TimeDifference extends HTMLElement {
const relative = rtf.format(value, unit);
// Store original text as title for hover tooltip
const originalText = time.textContent.trim();
if (!time.hasAttribute("title")) {
time.setAttribute("title", time.textContent.trim());
time.setAttribute("title", originalText);
}
// aria-label provides the full context: "2 days ago (March 5, 2026)"
time.setAttribute("aria-label", relative + " (" + originalText + ")");
time.textContent = relative;
} catch {
// Intl.RelativeTimeFormat not supported, keep static text

View File

@@ -197,22 +197,26 @@
items.forEach((item) => {
const author = item.author || {};
const li = document.createElement('li');
li.className = 'inline';
const link = document.createElement('a');
link.href = author.url || '#';
link.className = 'facepile-avatar';
link.title = author.name || 'Anonymous';
link.setAttribute('aria-label', (author.name || 'Anonymous') + ' (opens in new tab)');
link.target = '_blank';
link.rel = 'noopener';
link.dataset.new = 'true';
const img = document.createElement('img');
img.src = author.photo || '/images/default-avatar.svg';
img.alt = author.name || 'Anonymous';
img.alt = '';
img.className = 'w-8 h-8 rounded-full ring-2 ring-white dark:ring-surface-900';
img.loading = 'lazy';
link.appendChild(img);
row.appendChild(link);
li.appendChild(link);
row.appendChild(li);
});
}
@@ -278,7 +282,7 @@
const dateLink = document.createElement('a');
dateLink.href = item.url || '#';
dateLink.className = 'text-xs text-surface-500 hover:underline';
dateLink.className = 'text-xs text-surface-600 dark:text-surface-400 hover:underline';
dateLink.target = '_blank';
dateLink.rel = 'noopener';
@@ -396,8 +400,9 @@
header.className = 'text-sm font-semibold text-surface-600 dark:text-surface-400 uppercase tracking-wide mb-3';
header.textContent = `0 ${type === 'likes' ? 'Likes' : 'Reposts'}`;
const row = document.createElement('div');
const row = document.createElement('ul');
row.className = 'facepile';
row.setAttribute('role', 'list');
section.appendChild(header);
section.appendChild(row);
@@ -446,6 +451,8 @@
const section = document.createElement('section');
section.className = 'webmentions mt-8 pt-8 border-t border-surface-200 dark:border-surface-700';
section.id = 'webmentions';
section.setAttribute('aria-live', 'polite');
section.setAttribute('aria-label', 'Webmentions');
const header = document.createElement('h2');
header.className = 'text-xl font-bold text-surface-900 dark:text-surface-100 mb-6';