Compare commits
1 Commits
burn/123-1
...
fix/65-mod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27ade94c80 |
105
index.html
105
index.html
@@ -1117,25 +1117,14 @@ Sovereignty and service always.`;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
safetyPlanBtn.addEventListener('click', function() {
|
||||
loadSafetyPlan();
|
||||
safetyPlanModal.classList.add('active');
|
||||
});
|
||||
|
||||
// Crisis panel safety plan button (if crisis panel is visible)
|
||||
if (crisisSafetyPlanBtn) {
|
||||
crisisSafetyPlanBtn.addEventListener('click', function() {
|
||||
loadSafetyPlan();
|
||||
safetyPlanModal.classList.add('active');
|
||||
});
|
||||
}
|
||||
|
||||
closeSafetyPlan.addEventListener('click', function() {
|
||||
safetyPlanModal.classList.remove('active');
|
||||
_restoreSafetyPlanFocus();
|
||||
});
|
||||
|
||||
cancelSafetyPlan.addEventListener('click', function() {
|
||||
safetyPlanModal.classList.remove('active');
|
||||
_restoreSafetyPlanFocus();
|
||||
});
|
||||
|
||||
saveSafetyPlan.addEventListener('click', function() {
|
||||
@@ -1149,12 +1138,101 @@ Sovereignty and service always.`;
|
||||
try {
|
||||
localStorage.setItem('timmy_safety_plan', JSON.stringify(plan));
|
||||
safetyPlanModal.classList.remove('active');
|
||||
_restoreSafetyPlanFocus();
|
||||
alert('Safety plan saved locally.');
|
||||
} catch (e) {
|
||||
alert('Error saving plan.');
|
||||
}
|
||||
});
|
||||
|
||||
// ===== SAFETY PLAN FOCUS TRAP (fix #65) =====
|
||||
// Focusable elements inside the modal, in tab order
|
||||
var _spFocusableIds = [
|
||||
'close-safety-plan',
|
||||
'sp-warning-signs',
|
||||
'sp-coping',
|
||||
'sp-distraction',
|
||||
'sp-help',
|
||||
'sp-environment',
|
||||
'cancel-safety-plan',
|
||||
'save-safety-plan'
|
||||
];
|
||||
var _spTriggerEl = null; // element that opened the modal
|
||||
|
||||
function _getSpFocusableEls() {
|
||||
return _spFocusableIds
|
||||
.map(function(id) { return document.getElementById(id); })
|
||||
.filter(function(el) { return el && !el.disabled; });
|
||||
}
|
||||
|
||||
function _trapSafetyPlanFocus(e) {
|
||||
if (e.key !== 'Tab') return;
|
||||
var els = _getSpFocusableEls();
|
||||
if (!els.length) return;
|
||||
var first = els[0];
|
||||
var last = els[els.length - 1];
|
||||
|
||||
if (e.shiftKey) {
|
||||
// Shift+Tab on first → wrap to last
|
||||
if (document.activeElement === first) {
|
||||
e.preventDefault();
|
||||
last.focus();
|
||||
}
|
||||
} else {
|
||||
// Tab on last → wrap to first
|
||||
if (document.activeElement === last) {
|
||||
e.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _trapSafetyPlanEscape(e) {
|
||||
if (e.key === 'Escape') {
|
||||
safetyPlanModal.classList.remove('active');
|
||||
_restoreSafetyPlanFocus();
|
||||
}
|
||||
}
|
||||
|
||||
function _activateSafetyPlanFocusTrap(triggerEl) {
|
||||
_spTriggerEl = triggerEl || document.activeElement;
|
||||
// Focus first textarea
|
||||
var firstInput = document.getElementById('sp-warning-signs');
|
||||
if (firstInput) firstInput.focus();
|
||||
// Add listeners
|
||||
document.addEventListener('keydown', _trapSafetyPlanFocus);
|
||||
document.addEventListener('keydown', _trapSafetyPlanEscape);
|
||||
// Mark background inert (prevent click-through)
|
||||
document.body.setAttribute('aria-hidden', 'true');
|
||||
safetyPlanModal.removeAttribute('aria-hidden');
|
||||
}
|
||||
|
||||
function _restoreSafetyPlanFocus() {
|
||||
document.removeEventListener('keydown', _trapSafetyPlanFocus);
|
||||
document.removeEventListener('keydown', _trapSafetyPlanEscape);
|
||||
document.body.removeAttribute('aria-hidden');
|
||||
if (_spTriggerEl && typeof _spTriggerEl.focus === 'function') {
|
||||
_spTriggerEl.focus();
|
||||
}
|
||||
_spTriggerEl = null;
|
||||
}
|
||||
|
||||
// Wire open buttons to activate focus trap
|
||||
safetyPlanBtn.addEventListener('click', function() {
|
||||
loadSafetyPlan();
|
||||
safetyPlanModal.classList.add('active');
|
||||
_activateSafetyPlanFocusTrap(safetyPlanBtn);
|
||||
});
|
||||
|
||||
// Crisis panel safety plan button (if crisis panel is visible)
|
||||
if (crisisSafetyPlanBtn) {
|
||||
crisisSafetyPlanBtn.addEventListener('click', function() {
|
||||
loadSafetyPlan();
|
||||
safetyPlanModal.classList.add('active');
|
||||
_activateSafetyPlanFocusTrap(crisisSafetyPlanBtn);
|
||||
});
|
||||
}
|
||||
|
||||
// ===== TEXTAREA AUTO-RESIZE =====
|
||||
msgInput.addEventListener('input', function() {
|
||||
this.style.height = 'auto';
|
||||
@@ -1300,6 +1378,7 @@ Sovereignty and service always.`;
|
||||
if (urlParams.get('safetyplan') === 'true') {
|
||||
loadSafetyPlan();
|
||||
safetyPlanModal.classList.add('active');
|
||||
_activateSafetyPlanFocusTrap(safetyPlanBtn);
|
||||
// Clean up URL
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
}
|
||||
|
||||
84
tests/focus-trap-test.html
Normal file
84
tests/focus-trap-test.html
Normal file
@@ -0,0 +1,84 @@
|
||||
<!-- Test: Safety plan modal focus trap (issue #65) -->
|
||||
<!-- Open this file in a browser to manually verify focus trap behavior -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Focus Trap Test</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; padding: 20px; }
|
||||
.test { margin: 10px 0; padding: 10px; border: 1px solid #ccc; }
|
||||
.pass { background: #d4edda; border-color: #28a745; }
|
||||
.fail { background: #f8d7da; border-color: #dc3545; }
|
||||
button { margin: 5px; padding: 8px 16px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Focus Trap Manual Test</h1>
|
||||
<p>Open <code>index.html</code> in a browser, then run these checks:</p>
|
||||
|
||||
<div class="test" id="test-1">
|
||||
<strong>Test 1: Tab wraps to first element</strong><br>
|
||||
1. Open safety plan modal<br>
|
||||
2. Tab through all elements until you reach "Save Plan"<br>
|
||||
3. Press Tab again → should wrap to close button (X)
|
||||
</div>
|
||||
|
||||
<div class="test" id="test-2">
|
||||
<strong>Test 2: Shift+Tab wraps to last element</strong><br>
|
||||
1. Open safety plan modal<br>
|
||||
2. Focus is on "Warning signs" textarea<br>
|
||||
3. Press Shift+Tab → should wrap to "Save Plan" button
|
||||
</div>
|
||||
|
||||
<div class="test" id="test-3">
|
||||
<strong>Test 3: Escape closes modal</strong><br>
|
||||
1. Open safety plan modal<br>
|
||||
2. Press Escape → modal closes<br>
|
||||
3. Focus returns to the button that opened it
|
||||
</div>
|
||||
|
||||
<div class="test" id="test-4">
|
||||
<strong>Test 4: Background not reachable</strong><br>
|
||||
1. Open safety plan modal<br>
|
||||
2. Try to Tab to the chat input behind the modal<br>
|
||||
3. Should NOT be able to reach it
|
||||
</div>
|
||||
|
||||
<div class="test" id="test-5">
|
||||
<strong>Test 5: Click buttons close + restore focus</strong><br>
|
||||
1. Open modal via "my safety plan" button<br>
|
||||
2. Click Cancel → modal closes, focus on "my safety plan" button<br>
|
||||
3. Open again, click Save → same behavior<br>
|
||||
4. Open again, click X → same behavior
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<h2>Automated checks (paste into DevTools console on index.html):</h2>
|
||||
<pre><code>
|
||||
// Test focus trap
|
||||
var modal = document.getElementById('safety-plan-modal');
|
||||
var openBtn = document.getElementById('safety-plan-btn');
|
||||
openBtn.click();
|
||||
console.assert(modal.classList.contains('active'), 'Modal should be open');
|
||||
|
||||
var lastEl = document.getElementById('save-safety-plan');
|
||||
lastEl.focus();
|
||||
var evt = new KeyboardEvent('keydown', {key: 'Tab', bubbles: true});
|
||||
document.dispatchEvent(evt);
|
||||
// After Tab from last, focus should wrap to first
|
||||
var firstEl = document.getElementById('close-safety-plan');
|
||||
console.log('Focus after wrap:', document.activeElement.id);
|
||||
console.assert(document.activeElement === firstEl || document.activeElement.id === 'sp-warning-signs',
|
||||
'Focus should wrap to first element');
|
||||
|
||||
// Test Escape
|
||||
var escEvt = new KeyboardEvent('keydown', {key: 'Escape', bubbles: true});
|
||||
document.dispatchEvent(escEvt);
|
||||
console.assert(!modal.classList.contains('active'), 'Modal should close on Escape');
|
||||
console.assert(document.activeElement === openBtn, 'Focus should return to open button');
|
||||
|
||||
console.log('All automated checks passed!');
|
||||
</code></pre>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user