a11y: exported widget ARIA (slider/tabs/gallery)

ContentSlider, Tabs, and Gallery toHtml exports and their inline scripts
now carry ARIA semantics and keyboard support:

- ContentSlider: prev/next arrows and dot buttons get aria-labels, the
  slide stack is wrapped in an aria-live="polite" region, and the
  decorative chevron icons are aria-hidden.
- Tabs: tablist/tab/tabpanel roles, aria-selected/aria-controls/
  aria-labelledby, and Left/Right/Home/End arrow-key navigation in the
  inline script. The tab<->panel id scope is now derived from a
  deterministic hash of anchorId/labels instead of Math.random, since
  the ARIA linking ids must be stable across export runs.
- Gallery: the lightbox overlay gets role="dialog"/aria-modal/aria-label,
  Escape closes it, and thumbnails are keyboard-operable (role="button"
  tabindex="0" plus Enter/Space handling in the existing delegated
  listener).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 14:18:49 -07:00
parent c83db99ae4
commit 9b532e36e8
6 changed files with 188 additions and 13 deletions
+18 -6
View File
@@ -144,7 +144,7 @@ Gallery.craft = {
// inline onclick with an interpolated src -- a single delegated click
// listener below reads it, so a src containing a quote can't break out
// of a per-item event-handler string.
const lbAttr = lightbox ? ` data-lb-src="${escapeAttr(safeUrl(img.src || ''))}"` : '';
const lbAttr = lightbox ? ` data-lb-src="${escapeAttr(safeUrl(img.src || ''))}" role="button" tabindex="0"` : '';
const itemStyle = lightbox ? 'cursor:pointer;position:relative;overflow:hidden;border-radius:8px' : 'position:relative;overflow:hidden;border-radius:8px';
return `<div${lbAttr} style="${itemStyle}">
<img src="${escapeAttr(safeUrl(img.src || ''))}" alt="${escapeAttr(img.alt)}" style="width:100%;height:200px;object-fit:cover;display:block;border-radius:8px;background-color:#f1f5f9" />
@@ -157,18 +157,30 @@ Gallery.craft = {
if (lightbox) {
gridIdAttr = ` id="${galleryId}_grid"`;
lightboxHtml = `
<div id="${galleryId}_overlay" 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">
<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">
<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';}
document.getElementById('${galleryId}_grid').addEventListener('click', function(e){
var t = e.target.closest('[data-lb-src]');
if(!t) return;
var src = t.getAttribute('data-lb-src');
function ${galleryId}_open(src){
var o = document.getElementById('${galleryId}_overlay');
document.getElementById('${galleryId}_img').src = src;
o.style.display = 'flex';
}
document.getElementById('${galleryId}_grid').addEventListener('click', function(e){
var t = e.target.closest('[data-lb-src]');
if(!t) return;
${galleryId}_open(t.getAttribute('data-lb-src'));
});
document.getElementById('${galleryId}_grid').addEventListener('keydown', function(e){
if(e.key!=='Enter' && e.key!==' ') return;
var t = e.target.closest('[data-lb-src]');
if(!t) return;
e.preventDefault();
${galleryId}_open(t.getAttribute('data-lb-src'));
});
document.addEventListener('keydown', function(e){
if(e.key==='Escape'){ ${galleryId}_close(); }
});
</script>`;
}