From 9b532e36e873e804edafd879c9e869c4c574b55d Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sun, 12 Jul 2026 14:18:49 -0700 Subject: [PATCH] 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) --- .../sections/ContentSlider.toHtml.test.ts | 36 +++++++++++++ .../src/components/sections/ContentSlider.tsx | 8 +-- .../sections/Gallery.toHtml.test.ts | 30 +++++++++++ craft/src/components/sections/Gallery.tsx | 24 ++++++--- .../components/sections/Tabs.toHtml.test.ts | 53 +++++++++++++++++++ craft/src/components/sections/Tabs.tsx | 50 +++++++++++++++-- 6 files changed, 188 insertions(+), 13 deletions(-) create mode 100644 craft/src/components/sections/ContentSlider.toHtml.test.ts create mode 100644 craft/src/components/sections/Tabs.toHtml.test.ts diff --git a/craft/src/components/sections/ContentSlider.toHtml.test.ts b/craft/src/components/sections/ContentSlider.toHtml.test.ts new file mode 100644 index 0000000..39857f7 --- /dev/null +++ b/craft/src/components/sections/ContentSlider.toHtml.test.ts @@ -0,0 +1,36 @@ +import { describe, test, expect } from 'vitest'; +import { ContentSlider } from './ContentSlider'; + +const toHtml = (ContentSlider as any).toHtml; + +const slides = [ + { type: 'image' as const, heading: 'One' }, + { type: 'image' as const, heading: 'Two' }, + { type: 'image' as const, heading: 'Three' }, +]; + +describe('ContentSlider.toHtml accessibility (F1.1)', () => { + test('prev/next arrows get aria-labels and are real buttons', () => { + const { html } = toHtml({ slides, showArrows: true }, ''); + expect(html).toMatch(/]*aria-label="Previous slide"/); + expect(html).toMatch(/]*aria-label="Next slide"/); + }); + + test('dot buttons get "Go to slide N" aria-labels', () => { + const { html } = toHtml({ slides, showDots: true }, ''); + expect(html).toMatch(/]*aria-label="Go to slide 1"/); + expect(html).toMatch(/]*aria-label="Go to slide 2"/); + expect(html).toMatch(/]*aria-label="Go to slide 3"/); + }); + + test('slides are wrapped in an aria-live="polite" region', () => { + const { html } = toHtml({ slides }, ''); + expect(html).toContain('aria-live="polite"'); + }); + + test('decorative chevron icons in arrows are aria-hidden', () => { + const { html } = toHtml({ slides, showArrows: true }, ''); + expect(html).toMatch(/