feat(docsearch): trap focus in modal
This commit is contained in:
parent
a73d050c80
commit
ec7df43e80
2 changed files with 46 additions and 0 deletions
|
|
@ -14,6 +14,7 @@ import {
|
||||||
} from './types';
|
} from './types';
|
||||||
import { createSearchClient, groupBy, noop } from './utils';
|
import { createSearchClient, groupBy, noop } from './utils';
|
||||||
import { createStoredSearches } from './stored-searches';
|
import { createStoredSearches } from './stored-searches';
|
||||||
|
import { useTrapFocus } from './useTrapFocus';
|
||||||
import { Hit } from './Hit';
|
import { Hit } from './Hit';
|
||||||
import { SearchBox } from './SearchBox';
|
import { SearchBox } from './SearchBox';
|
||||||
import { ScreenState } from './ScreenState';
|
import { ScreenState } from './ScreenState';
|
||||||
|
|
@ -50,6 +51,7 @@ export function DocSearch({
|
||||||
suggestions: [],
|
suggestions: [],
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
|
const containerRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
const searchBoxRef = React.useRef<HTMLDivElement | null>(null);
|
const searchBoxRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
const dropdownRef = React.useRef<HTMLDivElement | null>(null);
|
const dropdownRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
const inputRef = React.useRef<HTMLInputElement | null>(null);
|
const inputRef = React.useRef<HTMLInputElement | null>(null);
|
||||||
|
|
@ -63,6 +65,8 @@ export function DocSearch({
|
||||||
: ''
|
: ''
|
||||||
).current;
|
).current;
|
||||||
|
|
||||||
|
useTrapFocus({ container: containerRef.current });
|
||||||
|
|
||||||
const searchClient = React.useMemo(() => createSearchClient(appId, apiKey), [
|
const searchClient = React.useMemo(() => createSearchClient(appId, apiKey), [
|
||||||
appId,
|
appId,
|
||||||
apiKey,
|
apiKey,
|
||||||
|
|
@ -320,6 +324,7 @@ export function DocSearch({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={containerRef}
|
||||||
className={[
|
className={[
|
||||||
'DocSearch-Container',
|
'DocSearch-Container',
|
||||||
state.status === 'stalled' && 'DocSearch-Container--Stalled',
|
state.status === 'stalled' && 'DocSearch-Container--Stalled',
|
||||||
|
|
|
||||||
41
src/useTrapFocus.ts
Normal file
41
src/useTrapFocus.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface UseTrapFocusProps {
|
||||||
|
container: HTMLElement | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTrapFocus({ container }: UseTrapFocusProps) {
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!container) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusableElements = container.querySelectorAll<HTMLElement>(
|
||||||
|
'a[href]:not([disabled]), button:not([disabled]), input:not([disabled])'
|
||||||
|
);
|
||||||
|
const firstElement = focusableElements[0];
|
||||||
|
const lastElement = focusableElements[focusableElements.length - 1];
|
||||||
|
|
||||||
|
function trapFocus(event: KeyboardEvent) {
|
||||||
|
if (event.key !== 'Tab') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.shiftKey) {
|
||||||
|
if (document.activeElement === firstElement) {
|
||||||
|
event.preventDefault();
|
||||||
|
lastElement.focus();
|
||||||
|
}
|
||||||
|
} else if (document.activeElement === lastElement) {
|
||||||
|
event.preventDefault();
|
||||||
|
firstElement.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
container.addEventListener('keydown', trapFocus);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
container.removeEventListener('keydown', trapFocus);
|
||||||
|
};
|
||||||
|
}, [container]);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue