Skip to content

Commit 4a17d73

Browse files
michaelmcneesclaude
andcommitted
fix: search modal focus trap, restore focus, invalidate on close
- Focus trap: Tab cycles within modal when open (first/last wrapping) - Restore focus: previously focused trigger gets focus back on close - Invalidate requests: searchRequestId incremented on open AND close, preventing stale responses from populating after modal reset Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3427e1a commit 4a17d73

1 file changed

Lines changed: 35 additions & 1 deletion

File tree

site/src/components/SearchModal.astro

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,15 @@
5353
r.appendChild(p);
5454
}
5555

56+
var previousFocus = null;
57+
var searchRequestId = 0;
58+
5659
function openSearch() {
5760
var modal = getModal();
5861
var input = getInput();
5962
if (!modal || !input) return;
63+
previousFocus = document.activeElement;
64+
searchRequestId++; // invalidate any pending requests
6065
modal.classList.remove('hidden');
6166
input.value = '';
6267
input.focus();
@@ -66,8 +71,38 @@
6671
function closeSearch() {
6772
var modal = getModal();
6873
if (modal) modal.classList.add('hidden');
74+
searchRequestId++; // invalidate any pending requests
75+
if (previousFocus && previousFocus.focus) {
76+
previousFocus.focus();
77+
previousFocus = null;
78+
}
6979
}
7080

81+
// Trap focus inside the modal when open
82+
document.addEventListener('keydown', function(e) {
83+
if (e.key !== 'Tab') return;
84+
var modal = getModal();
85+
if (!modal || modal.classList.contains('hidden')) return;
86+
87+
var focusable = modal.querySelectorAll('input, a, button, [tabindex]:not([tabindex="-1"])');
88+
if (focusable.length === 0) return;
89+
90+
var first = focusable[0];
91+
var last = focusable[focusable.length - 1];
92+
93+
if (e.shiftKey) {
94+
if (document.activeElement === first) {
95+
e.preventDefault();
96+
last.focus();
97+
}
98+
} else {
99+
if (document.activeElement === last) {
100+
e.preventDefault();
101+
first.focus();
102+
}
103+
}
104+
});
105+
71106
// Recursively sanitize Pagefind excerpt: preserve only <mark> tags, strip everything else
72107
function sanitizeExcerpt(html) {
73108
var parser = new DOMParser();
@@ -109,7 +144,6 @@
109144
});
110145

111146
var debounceTimer;
112-
var searchRequestId = 0;
113147
document.addEventListener('input', function(e) {
114148
if (!e.target || !e.target.hasAttribute('data-search-input')) return;
115149
clearTimeout(debounceTimer);

0 commit comments

Comments
 (0)