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
@@ -289,19 +289,21 @@ ContentSlider.craft = {
}).join('\n ');
const arrowsHtml = showArrows && items.length > 1
? `<button onclick="${uid}_prev()" style="position:absolute;top:50%;left:16px;transform:translateY(-50%);width:40px;height:40px;border-radius:50%;border:none;background:rgba(255,255,255,0.9);color:#18181b;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:2;box-shadow:0 2px 8px rgba(0,0,0,0.15)"><i class="fa fa-chevron-left"></i></button>
<button onclick="${uid}_next()" style="position:absolute;top:50%;right:16px;transform:translateY(-50%);width:40px;height:40px;border-radius:50%;border:none;background:rgba(255,255,255,0.9);color:#18181b;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:2;box-shadow:0 2px 8px rgba(0,0,0,0.15)"><i class="fa fa-chevron-right"></i></button>`
? `<button onclick="${uid}_prev()" aria-label="Previous slide" style="position:absolute;top:50%;left:16px;transform:translateY(-50%);width:40px;height:40px;border-radius:50%;border:none;background:rgba(255,255,255,0.9);color:#18181b;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:2;box-shadow:0 2px 8px rgba(0,0,0,0.15)"><i class="fa fa-chevron-left" aria-hidden="true"></i></button>
<button onclick="${uid}_next()" aria-label="Next slide" style="position:absolute;top:50%;right:16px;transform:translateY(-50%);width:40px;height:40px;border-radius:50%;border:none;background:rgba(255,255,255,0.9);color:#18181b;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:2;box-shadow:0 2px 8px rgba(0,0,0,0.15)"><i class="fa fa-chevron-right" aria-hidden="true"></i></button>`
: '';
const dotsHtml = showDots && items.length > 1
? `<div style="position:absolute;bottom:16px;left:50%;transform:translateX(-50%);display:flex;gap:8px;z-index:2">
${items.map((_, i) => `<button onclick="${uid}_go(${i})" id="${uid}_d${i}" style="width:10px;height:10px;border-radius:50%;border:none;cursor:pointer;background-color:${i === 0 ? '#ffffff' : 'rgba(255,255,255,0.5)'};transition:background-color 0.3s"></button>`).join('\n ')}
${items.map((_, i) => `<button onclick="${uid}_go(${i})" id="${uid}_d${i}" aria-label="Go to slide ${i + 1}" style="width:10px;height:10px;border-radius:50%;border:none;cursor:pointer;background-color:${i === 0 ? '#ffffff' : 'rgba(255,255,255,0.5)'};transition:background-color 0.3s"></button>`).join('\n ')}
</div>`
: '';
return {
html: `<section${sectionStyle ? ` style="${sectionStyle}"` : ''}>
<div aria-live="polite">
${slidesHtml}
</div>
${arrowsHtml}
${dotsHtml}
<script>