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
@@ -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(/<button[^>]*aria-label="Previous slide"/);
expect(html).toMatch(/<button[^>]*aria-label="Next slide"/);
});
test('dot buttons get "Go to slide N" aria-labels', () => {
const { html } = toHtml({ slides, showDots: true }, '');
expect(html).toMatch(/<button[^>]*aria-label="Go to slide 1"/);
expect(html).toMatch(/<button[^>]*aria-label="Go to slide 2"/);
expect(html).toMatch(/<button[^>]*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(/<i class="fa fa-chevron-left" aria-hidden="true"><\/i>/);
expect(html).toMatch(/<i class="fa fa-chevron-right" aria-hidden="true"><\/i>/);
});
});