feat(voyage): build A11Y-panel from DS-primitives (greenfield)

This commit is contained in:
Kjell Tore Guttormsen 2026-05-10 17:56:49 +02:00
commit b70b480d0d
2 changed files with 113 additions and 1 deletions

View file

@ -883,10 +883,51 @@
</div>
</section>
<!-- v4.3 Step 22 — A11Y-panel built from DS primitives (greenfield).
Empty placeholder until Wave 7 axe-core spec populates it via
window.__voyage hooks (Step 23). Severity counters use key-stats
severity modifiers (critical/high/medium/low) mapping axe-core's
critical/serious/moderate/minor enum. -->
<aside
id="voyage-a11y-panel"
class="guide-panel guide-panel--info"
role="complementary"
aria-label="A11Y-rapport (axe-core)"
hidden
>
<div class="guide-panel__title">A11Y-rapport</div>
<div class="guide-panel__body">
<div class="key-stats" role="group" aria-label="Axe-core severity-summary">
<div class="key-stat key-stat--critical">
<div class="key-stat__value" data-a11y-stat="critical">0</div>
<div class="key-stat__label">Critical</div>
</div>
<div class="key-stat key-stat--high">
<div class="key-stat__value" data-a11y-stat="serious">0</div>
<div class="key-stat__label">Serious</div>
</div>
<div class="key-stat key-stat--medium">
<div class="key-stat__value" data-a11y-stat="moderate">0</div>
<div class="key-stat__label">Moderate</div>
</div>
<div class="key-stat key-stat--low">
<div class="key-stat__value" data-a11y-stat="minor">0</div>
<div class="key-stat__label">Minor</div>
</div>
</div>
<ol class="findings__items" id="voyage-a11y-findings" aria-label="Axe-violations">
<li class="findings__item" aria-disabled="true">
<span class="findings__item-title">Kjør axe-spec for å fylle.</span>
<span class="findings__item-meta">tests/e2e/voyage-playground-a11y.spec.mjs (Wave 7)</span>
</li>
</ol>
</div>
</aside>
<!-- v4.3 Step 14 — Project dashboard mount slot. Hidden until
loadProjectDirectory completes; renderDashboard fills with a
fleet-grid of fleet-tiles (one per artifact: brief / plan /
review / research / progress) plus status vocabulary badges. -->
review / progress) plus status vocabulary badges. -->
<section id="voyage-dashboard" class="voyage-dashboard__page" aria-label="Project dashboard" hidden></section>
<!-- v4.3 Step 15 — Artifact-detail mount slot. Hidden until a
@ -1248,6 +1289,10 @@ playground first-run shows a complete round-trip-able artifact.
'<div class="app-header__spacer"></div>' +
'<div class="app-header__actions" role="group" aria-label="Hovednavigasjon">' +
'<button type="button" class="btn btn--primary" data-action="open-project-picker">Velg prosjektmappe</button>' +
// v4.3 Step 22 — A11Y panel toggle. Greenfield component built from
// DS-primitives (guide-panel--info + key-stats + findings__item).
// Initial state: empty placeholder; Wave 7 axe-spec populates it.
'<button type="button" class="btn btn--ghost" data-action="toggle-a11y-panel" aria-controls="voyage-a11y-panel" aria-expanded="false" aria-label="Vis/skjul A11Y-rapport">A11Y</button>' +
'<button type="button" class="theme-toggle" data-action="toggle-theme" aria-label="Bytt tema">' +
'<span data-theme-label aria-hidden="true"></span>' +
'</button>' +
@ -2674,6 +2719,9 @@ playground first-run shows a complete round-trip-able artifact.
// data-theme + colorScheme, persists to localStorage('voyage-theme').
wireThemeToggle();
// Step 22 (v4.3) — A11Y-panel toggle (delegated data-action handler).
wireA11yToggle();
// Step 8 (v4.3) — initial topbar render with single-crumb (voyage root).
// renderDashboard / drill-down (Wave 3) re-renders with deeper crumbs.
renderTopbar([{ label: 'Hjem' }]);
@ -2758,6 +2806,25 @@ playground first-run shows a complete round-trip-able artifact.
if (lbl) lbl.textContent = theme === 'light' ? '☾' : '☀';
}
// v4.3 Step 22 — A11Y-panel toggle. Click on data-action="toggle-a11y-panel"
// toggles the hidden attribute on #voyage-a11y-panel and updates aria-expanded
// on the toggle button. Panel is empty placeholder until Wave 7 axe-core
// spec calls window.__voyage.scheduleRender({ a11yViolations }) to populate.
function wireA11yToggle() {
document.addEventListener('click', function (e) {
var btn = e.target && e.target.closest && e.target.closest('[data-action="toggle-a11y-panel"]');
if (!btn) return;
var panel = document.getElementById('voyage-a11y-panel');
if (!panel) return;
var willOpen = panel.hidden;
panel.hidden = !willOpen;
btn.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
if (typeof announce === 'function') {
announce(willOpen ? 'A11Y-rapport vist.' : 'A11Y-rapport skjult.');
}
});
}
function wireThemeToggle() {
var initial = document.documentElement.getAttribute('data-theme') || 'dark';
setThemeLabel(initial);

View file

@ -337,3 +337,48 @@ test('voyage-playground.html setActiveAnchor toggles data-active on badges (v4.3
// injectAnchorBadges must propagate resolved state to badge data-resolved
assert.match(text, /setAttribute\('data-resolved',\s*'true'\)/, 'data-resolved set on resolved badge required');
});
// v4.3 Step 22 — A11Y-panel built from DS-primitives (greenfield)
test('voyage-playground.html declares voyage-a11y-panel with guide-panel--info (v4.3 Step 22)', () => {
const text = readFileSync(HTML, 'utf-8');
assert.match(text, /id="voyage-a11y-panel"[^>]*guide-panel guide-panel--info/, 'voyage-a11y-panel with guide-panel--info required');
// Must be hidden by default (placeholder until Wave 7)
assert.match(text, /id="voyage-a11y-panel"[\s\S]{0,300}\bhidden\b/, 'voyage-a11y-panel hidden by default required');
});
test('voyage-playground.html declares data-action="toggle-a11y-panel" toggle-button (v4.3 Step 22)', () => {
const text = readFileSync(HTML, 'utf-8');
assert.match(text, /data-action="toggle-a11y-panel"/, 'toggle-a11y-panel button required');
// aria-controls must point at the panel id
assert.match(text, /data-action="toggle-a11y-panel"[\s\S]*?aria-controls="voyage-a11y-panel"/, 'aria-controls binding required');
});
test('voyage-playground.html A11Y-panel uses key-stats severity grid (v4.3 Step 22)', () => {
const text = readFileSync(HTML, 'utf-8');
// key-stats grid with critical/high/medium/low severity modifiers
assert.match(text, /class="key-stat key-stat--critical"/, 'key-stat--critical required');
assert.match(text, /class="key-stat key-stat--high"/, 'key-stat--high (serious) required');
assert.match(text, /class="key-stat key-stat--medium"/, 'key-stat--medium (moderate) required');
assert.match(text, /class="key-stat key-stat--low"/, 'key-stat--low (minor) required');
// axe-core severity vocabulary on data-a11y-stat
assert.match(text, /data-a11y-stat="critical"/, 'data-a11y-stat="critical" required');
assert.match(text, /data-a11y-stat="serious"/, 'data-a11y-stat="serious" required');
assert.match(text, /data-a11y-stat="moderate"/, 'data-a11y-stat="moderate" required');
assert.match(text, /data-a11y-stat="minor"/, 'data-a11y-stat="minor" required');
});
test('voyage-playground.html A11Y-panel uses findings__items placeholder list (v4.3 Step 22)', () => {
const text = readFileSync(HTML, 'utf-8');
// Match either attribute order (class= or id= first); just confirm both live on the same <ol>.
assert.match(text, /<ol[^>]*class="findings__items"[^>]*id="voyage-a11y-findings"|<ol[^>]*id="voyage-a11y-findings"[^>]*class="findings__items"/, 'findings__items list (id=voyage-a11y-findings) required');
// Placeholder line referencing the Wave 7 Playwright spec
assert.match(text, /Kjør axe-spec/, 'placeholder hint "Kjør axe-spec" required');
});
test('voyage-playground.html declares wireA11yToggle JS function (v4.3 Step 22)', () => {
const text = readFileSync(HTML, 'utf-8');
assert.match(text, /function\s+wireA11yToggle\s*\(\s*\)/, 'wireA11yToggle() function required');
// Toggle must flip hidden + aria-expanded
assert.match(text, /panel\.hidden\s*=\s*!willOpen/, 'panel.hidden toggle required');
assert.match(text, /setAttribute\('aria-expanded'/, 'aria-expanded update required');
});