Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions assets/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ html, body {
z-index: 100; display: flex; flex-direction: column; gap: 6px;
}
.slide-nav-dot {
/* Reset the native button so the <button> dot matches the old <div> dot. */
appearance: none; -webkit-appearance: none; border: 0; padding: 0; margin: 0;
width: 6px; height: 6px; border-radius: 50%;
background: rgba(255,255,255,0.15); cursor: pointer; transition: all 0.3s ease;
}
Expand All @@ -83,6 +85,14 @@ html, body {
box-shadow: 0 0 8px var(--accent-blue);
}
.slide-nav-dot:hover { background: rgba(255,255,255,0.4); transform: scale(1.3); }
/* Keyboard-only focus ring, suppressed for pointer users. */
.slide-nav-dot:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 3px; }

/* Visually hidden, still read by screen readers. Used for the live region. */
.sr-only {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* -- Typography -- */
.slide-tag {
Expand Down
36 changes: 33 additions & 3 deletions assets/slides-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,54 @@ let current = 0;
const total = slides.length;

slides.forEach((_, i) => {
const dot = document.createElement('div');
// Buttons give keyboard focus and Enter/Space activation for free.
const dot = document.createElement('button');
dot.type = 'button';
dot.className = 'slide-nav-dot' + (i===0?' active':'');
dot.setAttribute('aria-label', 'Go to slide ' + (i+1) + ' of ' + total);
if (i===0) dot.setAttribute('aria-current', 'true');
dot.addEventListener('click', () => goTo(i));
slideNav.appendChild(dot);
});

// Visually hidden region that announces slide changes to screen readers.
const liveRegion = document.createElement('div');
liveRegion.className = 'sr-only';
liveRegion.setAttribute('aria-live', 'polite');
document.body.appendChild(liveRegion);

// Inactive slides start outside the accessibility tree and tab order.
slides.forEach((s, i) => {
s.setAttribute('tabindex', '-1');
if (i !== current) {
s.setAttribute('inert', '');
s.setAttribute('aria-hidden', 'true');
}
});

function updateUI() {
progress.style.width = ((current+1)/total*100)+'%';
counter.textContent = `${current+1} / ${total}`;
document.querySelectorAll('.slide-nav-dot').forEach((d,i) => d.classList.toggle('active', i===current));
document.querySelectorAll('.slide-nav-dot').forEach((d,i) => {
d.classList.toggle('active', i===current);
if (i===current) d.setAttribute('aria-current', 'true');
else d.removeAttribute('aria-current');
});
}

function goTo(index) {
if (index<0 || index>=total || index===current) return;
const prev = current;
current = index;
slides.forEach((s,i) => s.classList.toggle('active', i===current));
slides.forEach((s,i) => {
s.classList.toggle('active', i===current);
// Keep inactive slides out of the reading order and tab order.
if (i===current) { s.removeAttribute('inert'); s.removeAttribute('aria-hidden'); }
else { s.setAttribute('inert', ''); s.setAttribute('aria-hidden', 'true'); }
});
updateUI();
slides[current].focus();
liveRegion.textContent = 'Slide ' + (current+1) + ' of ' + total;
showSpeakerNotes(current);

// Chart lifecycle: destroy on exit, create on entry
Expand Down
43 changes: 42 additions & 1 deletion references/html-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@ Every generated HTML file **must** comply with these rules:
.reveal:nth-child(3) { transition-delay: 0.3s; }
.reveal:nth-child(4) { transition-delay: 0.4s; }

/* ===========================================
ACCESSIBILITY
=========================================== */
/* Reset the native button so the <button> dot matches the old <div> dot. */
.slide-nav-dot { appearance: none; -webkit-appearance: none; border: 0; padding: 0; margin: 0; }

/* Keyboard-only focus ring, suppressed for pointer users. */
.slide-nav-dot:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 3px; }

/* Visually hidden, still read by screen readers. Used for the live region. */
.sr-only {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* ... preset-specific styles ... */
</style>
</head>
Expand Down Expand Up @@ -131,9 +146,14 @@ Every generated HTML file **must** comply with these rules:
current = index;
slides.forEach(function(s, i) {
s.classList.toggle('active', i === current);
// Keep inactive slides out of the reading order and tab order.
if (i === current) { s.removeAttribute('inert'); s.removeAttribute('aria-hidden'); }
else { s.setAttribute('inert', ''); s.setAttribute('aria-hidden', 'true'); }
});
slides[current].scrollIntoView({ behavior: 'smooth' });
updateUI();
slides[current].focus();
liveRegion.textContent = 'Slide ' + (current + 1) + ' of ' + total;
showSpeakerNotes(current);
}

Expand All @@ -152,6 +172,8 @@ Every generated HTML file **must** comply with these rules:
// Nav dots
document.querySelectorAll('.slide-nav-dot').forEach(function(d, i) {
d.classList.toggle('active', i === current);
if (i === current) d.setAttribute('aria-current', 'true');
else d.removeAttribute('aria-current');
});
}

Expand Down Expand Up @@ -183,13 +205,32 @@ Every generated HTML file **must** comply with these rules:
var slideNav = document.getElementById('slideNav');
if (slideNav) {
slides.forEach(function(_, i) {
var dot = document.createElement('div');
// Buttons give keyboard focus and Enter/Space activation for free.
var dot = document.createElement('button');
dot.type = 'button';
dot.className = 'slide-nav-dot' + (i === 0 ? ' active' : '');
dot.setAttribute('aria-label', 'Go to slide ' + (i + 1) + ' of ' + total);
if (i === 0) dot.setAttribute('aria-current', 'true');
dot.addEventListener('click', function() { goTo(i); });
slideNav.appendChild(dot);
});
}

// Visually hidden region that announces slide changes to screen readers.
var liveRegion = document.createElement('div');
liveRegion.className = 'sr-only';
liveRegion.setAttribute('aria-live', 'polite');
document.body.appendChild(liveRegion);

// Inactive slides start outside the accessibility tree and tab order.
slides.forEach(function(s, i) {
s.setAttribute('tabindex', '-1');
if (i !== current) {
s.setAttribute('inert', '');
s.setAttribute('aria-hidden', 'true');
}
});

updateUI();

/* ===========================================
Expand Down
9 changes: 9 additions & 0 deletions references/presentation-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ The presentation must support:

Build navigation inline following the pattern in [html-template.md](html-template.md). Generate `goTo()`, `next()`, `prev()` as global functions, plus speaker notes console logging on each slide change.

## Accessibility Contract

The navigation runtime holds to the following contract so keyboard and screen-reader users can drive the deck:

- Nav dots are `<button type="button">` elements, so each dot is reachable by Tab and activates on Enter or Space. Every dot carries an `aria-label` of the form "Go to slide N of M".
- The active dot carries `aria-current="true"`, and the others carry none. `updateUI()` keeps this in sync on every slide change.
- Only the active slide is in the accessibility tree. Inactive slides carry `inert` and `aria-hidden="true"`, so a screen reader reads just the current slide and Tab skips controls inside hidden slides. Each slide also carries `tabindex="-1"` so it can receive focus.
- On every slide change, focus moves to the active slide, and a visually hidden `aria-live="polite"` region announces "Slide N of M". The region is created once at startup and uses the `.sr-only` utility.

## Generator Meta Tag

Every presentation must include in `<head>`:
Expand Down