From a863dedc6aff3868e051ca7385a1e33f51158fe1 Mon Sep 17 00:00:00 2001 From: Cyril Date: Thu, 15 Jan 2026 12:18:00 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D(docs)=20add=20side=20panel=20focus?= =?UTF-8?q?=20pattern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document trigger ref and panel ref focus workflow --- src/frontend/README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) 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 }) + }, +}) +```