feat: crisis overlay full Tab cycle + Escape dismiss (closes #95) #105

Closed
Rockachopa wants to merge 1 commits from fix/95-overlay-keyboard-nav into main

View File

@@ -1001,6 +1001,13 @@ Sovereignty and service always.`;
var first = focusable[0];
var last = focusable[focusable.length - 1];
// If focus escaped outside the overlay (e.g. to body), redirect to first
if (!crisisOverlay.contains(document.activeElement)) {
e.preventDefault();
first.focus();
return;
}
if (e.shiftKey) {
// Shift+Tab: if on first, wrap to last
if (document.activeElement === first) {
@@ -1050,38 +1057,55 @@ Sovereignty and service always.`;
}
}, 1000);
overlayDismissBtn.focus();
// Focus the first focusable element (call link) — dismiss button is still disabled
var firstFocusable = crisisOverlay.querySelector('a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])');
if (firstFocusable) {
firstFocusable.focus();
}
}
// Register focus trap on document (always listening, gated by class check)
document.addEventListener('keydown', trapFocusInOverlay);
function dismissOverlay() {
crisisOverlay.classList.remove('active');
if (overlayTimer) {
clearInterval(overlayTimer);
overlayTimer = null;
}
// Re-enable background interaction
var mainApp = document.querySelector('.app');
if (mainApp) mainApp.removeAttribute('inert');
var chatSection = document.getElementById('chat');
if (chatSection) chatSection.removeAttribute('aria-hidden');
var footerEl = document.querySelector('footer');
if (footerEl) footerEl.removeAttribute('aria-hidden');
// Restore focus to the element that had it before the overlay opened
if (_preOverlayFocusElement && typeof _preOverlayFocusElement.focus === 'function') {
_preOverlayFocusElement.focus();
} else {
msgInput.focus();
}
_preOverlayFocusElement = null;
}
overlayDismissBtn.addEventListener('click', function() {
if (!overlayDismissBtn.disabled) {
crisisOverlay.classList.remove('active');
if (overlayTimer) {
clearInterval(overlayTimer);
overlayTimer = null;
}
// Re-enable background interaction
var mainApp = document.querySelector('.app');
if (mainApp) mainApp.removeAttribute('inert');
var chatSection = document.getElementById('chat');
if (chatSection) chatSection.removeAttribute('aria-hidden');
var footerEl = document.querySelector('footer');
if (footerEl) footerEl.removeAttribute('aria-hidden');
// Restore focus to the element that had it before the overlay opened
if (_preOverlayFocusElement && typeof _preOverlayFocusElement.focus === 'function') {
_preOverlayFocusElement.focus();
} else {
msgInput.focus();
}
_preOverlayFocusElement = null;
dismissOverlay();
}
});
// Escape key closes crisis overlay (only after dismiss button is enabled)
document.addEventListener('keydown', function(e) {
if (e.key !== 'Escape') return;
if (!crisisOverlay.classList.contains('active')) return;
if (overlayDismissBtn.disabled) return; // Don't bypass countdown
e.preventDefault();
dismissOverlay();
});
// ===== MESSAGE RENDERING =====
function addMessage(role, text, skipSave) {
var div = document.createElement('div');