Skip to content

Commit ec7df43

Browse files
feat(docsearch): trap focus in modal
1 parent a73d050 commit ec7df43

2 files changed

Lines changed: 46 additions & 0 deletions

File tree

src/DocSearch.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from './types';
1515
import { createSearchClient, groupBy, noop } from './utils';
1616
import { createStoredSearches } from './stored-searches';
17+
import { useTrapFocus } from './useTrapFocus';
1718
import { Hit } from './Hit';
1819
import { SearchBox } from './SearchBox';
1920
import { ScreenState } from './ScreenState';
@@ -50,6 +51,7 @@ export function DocSearch({
5051
suggestions: [],
5152
} as any);
5253

54+
const containerRef = React.useRef<HTMLDivElement | null>(null);
5355
const searchBoxRef = React.useRef<HTMLDivElement | null>(null);
5456
const dropdownRef = React.useRef<HTMLDivElement | null>(null);
5557
const inputRef = React.useRef<HTMLInputElement | null>(null);
@@ -63,6 +65,8 @@ export function DocSearch({
6365
: ''
6466
).current;
6567

68+
useTrapFocus({ container: containerRef.current });
69+
6670
const searchClient = React.useMemo(() => createSearchClient(appId, apiKey), [
6771
appId,
6872
apiKey,
@@ -320,6 +324,7 @@ export function DocSearch({
320324

321325
return (
322326
<div
327+
ref={containerRef}
323328
className={[
324329
'DocSearch-Container',
325330
state.status === 'stalled' && 'DocSearch-Container--Stalled',

src/useTrapFocus.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import React from 'react';
2+
3+
interface UseTrapFocusProps {
4+
container: HTMLElement | null;
5+
}
6+
7+
export function useTrapFocus({ container }: UseTrapFocusProps) {
8+
React.useEffect(() => {
9+
if (!container) {
10+
return undefined;
11+
}
12+
13+
const focusableElements = container.querySelectorAll<HTMLElement>(
14+
'a[href]:not([disabled]), button:not([disabled]), input:not([disabled])'
15+
);
16+
const firstElement = focusableElements[0];
17+
const lastElement = focusableElements[focusableElements.length - 1];
18+
19+
function trapFocus(event: KeyboardEvent) {
20+
if (event.key !== 'Tab') {
21+
return;
22+
}
23+
24+
if (event.shiftKey) {
25+
if (document.activeElement === firstElement) {
26+
event.preventDefault();
27+
lastElement.focus();
28+
}
29+
} else if (document.activeElement === lastElement) {
30+
event.preventDefault();
31+
firstElement.focus();
32+
}
33+
}
34+
35+
container.addEventListener('keydown', trapFocus);
36+
37+
return () => {
38+
container.removeEventListener('keydown', trapFocus);
39+
};
40+
}, [container]);
41+
}

0 commit comments

Comments
 (0)