diff --git a/src/frontend/README.md b/src/frontend/README.md index 0d6babed..7e12a02e 100644 --- a/src/frontend/README.md +++ b/src/frontend/README.md @@ -28,3 +28,36 @@ export default { - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list + +## Side Panel Focus Pattern + +We use a consistent focus management pattern for side panels: + +- **Open**: focus the first actionable element inside the panel. +- **Close**: restore focus to the button that opened the panel. + +Implementation summary: + +1. A provider stores a `panelRef` and a registry of trigger refs (`setTrigger/getTrigger`). +2. Each trigger button registers itself with `setTrigger("key", el)`. +3. Panel content uses `useRestoreFocus` with: + - `resolveTrigger` → returns `getTrigger("key")`. + - `onOpened` → finds the first actionable element inside `panelRef`. + +Example: + +```tsx +// Trigger button +; setTrigger('tools', el)} /> + +// Panel content +useRestoreFocus(isOpen, { + resolveTrigger: (activeEl) => getTrigger('tools') ?? activeEl, + onOpened: () => { + const first = panelRef.current?.querySelector( + '[data-attr="tools-list"] button' + ) + ;(first as HTMLElement | null)?.focus({ preventScroll: true }) + }, +}) +```