Files
blog-eleventy-indiekit/js/lightbox.js
Ricardo e236b4bf65 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
2026-03-07 18:58:08 +01:00

102 lines
2.9 KiB
JavaScript

/**
* Alpine.js lightbox component for article images.
* Registers via alpine:init so it's available before Alpine starts.
* Click any image inside .e-content to view fullscreen.
* Navigate with arrow keys, close with Escape or click outside.
*/
document.addEventListener("alpine:init", () => {
Alpine.data("lightbox", () => ({
open: false,
src: "",
alt: "",
images: [],
currentIndex: 0,
triggerElement: null,
init() {
const container = this.$root;
const imgs = container.querySelectorAll(
".e-content img:not(.u-photo), .photo-gallery img.u-photo"
);
this.images = Array.from(imgs);
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
const picture = img.closest("picture");
if (picture) {
const source = picture.querySelector("source");
if (source) {
// Extract the URL from srcset (strip width descriptor)
const srcset = source.getAttribute("srcset") || "";
this.src = srcset.split(/\s+/)[0] || img.src;
} else {
this.src = img.src;
}
} else {
this.src = img.src;
}
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() {
if (this.images.length > 1) {
this.show((this.currentIndex + 1) % this.images.length);
}
},
prev() {
if (this.images.length > 1) {
this.show(
(this.currentIndex - 1 + this.images.length) % this.images.length
);
}
},
onKeydown(e) {
if (!this.open) return;
if (e.key === "Escape") this.close();
if (e.key === "ArrowRight") this.next();
if (e.key === "ArrowLeft") this.prev();
},
}));
});