a11y/security: scope Navbar ids + hover styles, Gallery lightbox focus trap

M-1: Navbar.toHtml emitted a fixed id="navbar-links" and unscoped
.navbar-link/.navbar-cta :hover selectors -- two Navbars on one page
collided on the duplicate id and cross-applied each other's hover
colors (later <style> block wins in the cascade). Scope both on the
Craft node id via scopeId(), matching the Menu/Tabs pattern: the links
container gets a unique id, aria-controls/the hamburger toggle script
reference it, and the hover rules are prefixed with a per-instance
class on the <nav> root.

M-2: Gallery lightbox had no focus management -- opening it left focus
wherever it was (behind the now-visible overlay) and closing it never
restored it. The inline script now stashes document.activeElement on
open, moves focus to a new accessible close button, traps Tab on the
close button while the dialog is open, and restores the saved focus on
close (Escape, backdrop click, or the close button).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 18:30:22 -07:00
parent 92841e3f35
commit 0cbc58f8d1
4 changed files with 148 additions and 19 deletions
+30 -2
View File
@@ -166,16 +166,37 @@ Gallery.craft = {
let gridIdAttr = '';
if (lightbox) {
gridIdAttr = ` id="${galleryId}_grid"`;
// M-2: focus management for the lightbox dialog.
// - OPEN: stash `document.activeElement` (the thumbnail that triggered
// the open) in a module-scoped var, then move focus onto the close
// button -- so a screen-reader/keyboard user lands inside the dialog
// instead of focus staying on (or silently falling back to <body>)
// behind the now-visible overlay.
// - Tab trap: while the overlay is open, every Tab keypress is
// intercepted and refocuses the close button (the dialog's only
// focusable control besides Escape/click-to-close), so focus can
// never wander out into the page content hidden behind the overlay.
// - CLOSE (Escape, backdrop click, or the close button): restore focus
// to the element stashed on open.
lightboxHtml = `
<div id="${galleryId}_overlay" role="dialog" aria-modal="true" aria-label="Image preview" onclick="${galleryId}_close()" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);z-index:9999;justify-content:center;align-items:center;cursor:pointer">
<button type="button" id="${galleryId}_closebtn" aria-label="Close preview" tabindex="-1" onclick="event.stopPropagation();${galleryId}_close()" style="position:absolute;top:16px;right:16px;width:36px;height:36px;border-radius:50%;border:none;background:rgba(255,255,255,0.15);color:#ffffff;font-size:20px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center">&times;</button>
<img id="${galleryId}_img" src="" alt="" style="max-width:90%;max-height:90%;object-fit:contain;border-radius:8px" />
</div>
<script>
function ${galleryId}_close(){document.getElementById('${galleryId}_overlay').style.display='none';}
var ${galleryId}_lastFocus = null;
function ${galleryId}_close(){
document.getElementById('${galleryId}_overlay').style.display='none';
if(${galleryId}_lastFocus && ${galleryId}_lastFocus.focus) ${galleryId}_lastFocus.focus();
${galleryId}_lastFocus = null;
}
function ${galleryId}_open(src){
${galleryId}_lastFocus = document.activeElement;
var o = document.getElementById('${galleryId}_overlay');
document.getElementById('${galleryId}_img').src = src;
o.style.display = 'flex';
var c = document.getElementById('${galleryId}_closebtn');
if(c) c.focus();
}
document.getElementById('${galleryId}_grid').addEventListener('click', function(e){
var t = e.target.closest('[data-lb-src]');
@@ -190,7 +211,14 @@ document.getElementById('${galleryId}_grid').addEventListener('keydown', functio
${galleryId}_open(t.getAttribute('data-lb-src'));
});
document.addEventListener('keydown', function(e){
if(e.key==='Escape'){ ${galleryId}_close(); }
var o = document.getElementById('${galleryId}_overlay');
if(!o || o.style.display==='none') return;
if(e.key==='Escape'){ ${galleryId}_close(); return; }
if(e.key==='Tab'){
e.preventDefault();
var c = document.getElementById('${galleryId}_closebtn');
if(c) c.focus();
}
});
</script>`;
}