Merge pull request 'fix: tutorial focus trap polish for #57' (#204) from fix/57 into main
Some checks failed
Smoke Test / smoke (push) Has been cancelled

This commit was merged in pull request #204.
This commit is contained in:
2026-04-21 15:29:59 +00:00
2 changed files with 127 additions and 13 deletions

View File

@@ -234,21 +234,56 @@ function nextTutorialStep() {
renderTutorialStep(_tutorialStep);
}
// Keyboard support: Enter/Right to advance, Escape to close
document.addEventListener('keydown', function tutorialKeyHandler(e) {
if (!document.getElementById('tutorial-overlay')) return;
if (e.key === 'Enter' || e.key === 'ArrowRight') {
function getTutorialFocusableElements(root = document) {
const overlay = root && typeof root.getElementById === 'function'
? root.getElementById('tutorial-overlay')
: null;
if (!overlay || typeof overlay.querySelectorAll !== 'function') return [];
return Array.from(
overlay.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
).filter(el => !el.disabled && !el.hidden && el.offsetParent !== null);
}
function trapTutorialFocus(e, root = document) {
if (!e || e.key !== 'Tab') return false;
const focusable = getTutorialFocusableElements(root);
if (focusable.length < 2) return false;
const active = root.activeElement;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && (active === first || !focusable.includes(active))) {
e.preventDefault();
if (_tutorialStep >= TUTORIAL_STEPS.length - 1) {
closeTutorial();
} else {
nextTutorialStep();
}
} else if (e.key === 'Escape') {
e.preventDefault();
closeTutorial();
last.focus();
return true;
}
});
if (!e.shiftKey && (active === last || !focusable.includes(active))) {
e.preventDefault();
first.focus();
return true;
}
return false;
}
// Keyboard support: Enter/Right to advance, Escape to close
if (typeof document !== 'undefined' && document.addEventListener) {
document.addEventListener('keydown', function tutorialKeyHandler(e) {
if (!document.getElementById('tutorial-overlay')) return;
if (trapTutorialFocus(e, document)) return;
if (e.key === 'Enter' || e.key === 'ArrowRight') {
e.preventDefault();
if (_tutorialStep >= TUTORIAL_STEPS.length - 1) {
closeTutorial();
} else {
nextTutorialStep();
}
} else if (e.key === 'Escape') {
e.preventDefault();
closeTutorial();
}
});
}
function closeTutorial() {
const overlay = document.getElementById('tutorial-overlay');
@@ -269,3 +304,19 @@ function startTutorial() {
// Small delay so the page renders first
setTimeout(() => renderTutorialStep(0), 300);
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
TUTORIAL_KEY,
TUTORIAL_STEPS,
isTutorialDone,
markTutorialDone,
createTutorialStyles,
renderTutorialStep,
nextTutorialStep,
getTutorialFocusableElements,
trapTutorialFocus,
closeTutorial,
startTutorial,
};
}