Commit 685f410
committed
fix(admin/spa): split Modal effect to stop focus flicker on busy toggle
Claude review on b36cfed caught a real focus-management bug: the
single useEffect in Modal.tsx mixed two invariants — focus
capture/restore (open) and the keyboard handler (open + busy +
onClose) — into one effect with deps [open, busy, onClose].
Concrete failure: user clicks Save, parent flips busy=true. React
runs the cleanup, which removes the keydown listener AND restores
focus to the previously-focused element (the trigger button, outside
the dialog). The effect then re-runs, captures the trigger button
as the NEW restore target, and snaps focus back. Net: focus
flickers out and back on every busy toggle, previouslyFocusedRef
gets clobbered with the wrong element, and screen readers announce
the dialog as briefly "exited" mid-operation.
Fix: split into two effects.
- Focus capture/restore — deps [open] only. Runs once on open, once
on close. previouslyFocusedRef stays valid for the whole dialog
lifetime.
- Keyboard handler — deps [open, busy, onClose]. Re-binds when
any of those change; only observable side effect is one window
listener swap.
All three automated reviewers (Claude, Gemini, Codex) flagged this.
The split is the standard React idiom — each invariant owns its own
state slice.
The dist/ placeholder index.html is intentionally NOT regenerated
(per .gitignore: assets are built at deploy time, not committed).1 parent b36cfed commit 685f410
1 file changed
Lines changed: 31 additions & 17 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
33 | 42 | | |
34 | 43 | | |
35 | | - | |
36 | 44 | | |
37 | | - | |
38 | | - | |
39 | | - | |
40 | | - | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
41 | 49 | | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
42 | 61 | | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
43 | 68 | | |
44 | 69 | | |
45 | 70 | | |
| |||
65 | 90 | | |
66 | 91 | | |
67 | 92 | | |
68 | | - | |
69 | 93 | | |
70 | | - | |
71 | | - | |
72 | | - | |
73 | | - | |
74 | | - | |
75 | | - | |
76 | | - | |
77 | | - | |
78 | | - | |
79 | | - | |
80 | | - | |
| 94 | + | |
81 | 95 | | |
82 | 96 | | |
83 | 97 | | |
| |||
0 commit comments