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:
2026-07-12 14:19:01 -07:00
co-authored by Claude Opus 4.8
parent 9b532e36e8
commit 9969adca72
21 changed files with 255 additions and 27 deletions
@@ -0,0 +1,23 @@
import { describe, test, expect } from 'vitest';
import { StarRating } from './StarRating';
const toHtml = (StarRating as any).toHtml;
describe('StarRating.toHtml accessibility (F2.2)', () => {
test('wrapper has role="img" and a "Rating: N out of maxStars" aria-label', () => {
const { html } = toHtml({ rating: 4.5, maxStars: 5 }, '');
expect(html).toMatch(/<span role="img" aria-label="Rating: 4\.5 out of 5"/);
});
test('individual star glyphs are aria-hidden', () => {
const { html } = toHtml({ rating: 3, maxStars: 5 }, '');
const glyphs = html.match(/<i class="fa fa-star"[^>]*>/g) || [];
expect(glyphs.length).toBeGreaterThan(0);
glyphs.forEach((tag: string) => expect(tag).toContain('aria-hidden="true"'));
});
test('respects custom maxStars in the aria-label', () => {
const { html } = toHtml({ rating: 2, maxStars: 10 }, '');
expect(html).toContain('aria-label="Rating: 2 out of 10"');
});
});