110 lines
3.1 KiB
TypeScript
110 lines
3.1 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
||
|
||
interface HelpPanelProps {
|
||
open: boolean;
|
||
onClose: () => void;
|
||
}
|
||
|
||
export function HelpPanel({ open, onClose }: HelpPanelProps) {
|
||
const panel = useRef<HTMLElement>(null);
|
||
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
const returnFocus =
|
||
document.activeElement instanceof HTMLElement
|
||
? document.activeElement
|
||
: null;
|
||
const dialog = panel.current;
|
||
const focusable = () =>
|
||
dialog
|
||
? [...dialog.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)]
|
||
: [];
|
||
focusable()[0]?.focus();
|
||
|
||
const handleKeyDown = (event: KeyboardEvent) => {
|
||
if (event.key === 'Escape') {
|
||
event.preventDefault();
|
||
onClose();
|
||
return;
|
||
}
|
||
if (event.key !== 'Tab') return;
|
||
const elements = focusable();
|
||
const first = elements[0];
|
||
const last = elements.at(-1);
|
||
if (!first || !last) return;
|
||
if (event.shiftKey && document.activeElement === first) {
|
||
event.preventDefault();
|
||
last.focus();
|
||
} else if (!event.shiftKey && document.activeElement === last) {
|
||
event.preventDefault();
|
||
first.focus();
|
||
}
|
||
};
|
||
document.addEventListener('keydown', handleKeyDown);
|
||
return () => {
|
||
document.removeEventListener('keydown', handleKeyDown);
|
||
returnFocus?.focus();
|
||
};
|
||
}, [open, onClose]);
|
||
|
||
if (!open) return null;
|
||
return (
|
||
<div
|
||
className="dialog-backdrop"
|
||
role="presentation"
|
||
onMouseDown={(event) => event.target === event.currentTarget && onClose()}
|
||
>
|
||
<section
|
||
ref={panel}
|
||
className="help-panel"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="help-title"
|
||
tabIndex={-1}
|
||
>
|
||
<header>
|
||
<div>
|
||
<p className="eyebrow">Toolbox guide</p>
|
||
<h2 id="help-title">Choose and arrange your tools</h2>
|
||
</div>
|
||
<button
|
||
className="close-button"
|
||
type="button"
|
||
aria-label="Close help"
|
||
onClick={onClose}
|
||
>
|
||
×
|
||
</button>
|
||
</header>
|
||
<div className="help-steps">
|
||
<p>
|
||
<strong>Open:</strong> click anywhere on a tool tile that is not a
|
||
control.
|
||
</p>
|
||
<p>
|
||
<strong>Filter:</strong> search, choose a category, or click a tag.
|
||
Click the selected tag again to clear it.
|
||
</p>
|
||
<p>
|
||
<strong>Arrange:</strong> pin frequently used tools into their own
|
||
section, then drag the grip to reorder. The grip also supports
|
||
keyboard dragging with Space, arrow keys, and Escape.
|
||
</p>
|
||
<p>
|
||
<strong>Privacy:</strong> each tile reports its declared processing,
|
||
uploads, telemetry, and browser requirements before you open it.
|
||
</p>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const FOCUSABLE_SELECTOR = [
|
||
'a[href]',
|
||
'button:not([disabled])',
|
||
'input:not([disabled])',
|
||
'select:not([disabled])',
|
||
'[tabindex]:not([tabindex="-1"])',
|
||
].join(',');
|