a11y: exported semantics (forms/rating/nav/iframe/icons)
- InputField/TextareaField/ContactForm: every control gets a
deterministic id (slugId() in utils/escape.ts, derived from the
field's name/label + index for ContactForm's looped fields -- no
Math.random) with a matching <label for=>; fields with no visible
label get an aria-label from the placeholder/name instead.
- StarRating: wrapped in role="img" aria-label="Rating: N out of M",
individual star glyphs marked aria-hidden.
- Navbar: the mobile hamburger toggle gets aria-label="Toggle
navigation menu", aria-controls="navbar-links", and aria-expanded
wired to flip true/false in the inline onclick handler.
- VideoBlock and MapEmbed: every exported <iframe> gets a title
(generic "Embedded video", or "Map of {address}" for MapEmbed).
- Decorative Font Awesome icons (ContentSlider arrows already covered
in the prior commit; SocialLinks, SearchBar, Testimonials stars) are
aria-hidden; SocialLinks' icon-only links get an aria-label naming
the platform alongside the existing title tooltip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -63,3 +63,28 @@ describe('ContactForm.toHtml successMessage', () => {
|
||||
expect(html).not.toContain('onerror="alert(1)"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ContactForm.toHtml accessibility (F2.1)', () => {
|
||||
const fields = [
|
||||
{ type: 'text' as const, label: 'Name', name: 'name', placeholder: 'Your name', required: true },
|
||||
{ type: 'email' as const, label: 'Email', name: 'email', placeholder: 'you@example.com', required: true },
|
||||
];
|
||||
|
||||
test('each field label for= matches its control id=, and ids are unique', () => {
|
||||
const { html } = toHtml({ fields }, '');
|
||||
const labelIds = [...html.matchAll(/<label for="([^"]+)"/g)].map((m) => m[1]);
|
||||
const controlIds = [...html.matchAll(/<(?:input|textarea|select) id="([^"]+)"/g)].map((m) => m[1]);
|
||||
expect(labelIds.length).toBe(2);
|
||||
expect(controlIds.length).toBe(2);
|
||||
expect(labelIds).toEqual(controlIds);
|
||||
expect(new Set(controlIds).size).toBe(2);
|
||||
});
|
||||
|
||||
test('ids are deterministic across repeated calls with the same fields', () => {
|
||||
const { html: html1 } = toHtml({ fields }, '');
|
||||
const { html: html2 } = toHtml({ fields }, '');
|
||||
const ids1 = [...html1.matchAll(/<input id="([^"]+)"/g)].map((m) => m[1]);
|
||||
const ids2 = [...html2.matchAll(/<input id="([^"]+)"/g)].map((m) => m[1]);
|
||||
expect(ids1).toEqual(ids2);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user