Summary
When the start menu is open and the user switches themes, DOM focus ends up stranded on <body> after dismissal. Escape, click-outside, and app-launch all run through closeStartMenu(), whose opener-restoration silently no-ops because the captured opener element was destroyed with the old taskbar.
Found during review of PR #31 (WEB-0035 fix round, finding F3).
Reproduction
- Focus the Start button (or any taskbar control).
- Open the start menu —
openStartMenu captures the focused element into openerFocus (src/desktop/main.ts:471).
- Switch the theme (Settings → any other era) while the menu is open.
- Dismiss the menu (Escape / click outside / launch an app).
Expected: focus returns to a sensible control (the Start button).
Actual: document.activeElement is <body>.
Root cause
applyTheme (src/desktop/main.ts:81-82) calls installWebosTaskbar() — which destroys the old WebOSTaskbar together with its a11y mirrors (src/desktop/main.ts:415-418) — before closeStartMenu(). The PX-0077 restoration in closeStartMenu (src/desktop/main.ts:460-463) then calls .focus() on the captured opener, but that node is disconnected: focusing a detached element is a silent no-op, and the guard (activeElement === body) passes precisely because the destruction already dropped focus to <body>.
Two constraints make the fix non-trivial:
- Reordering alone does not help: closing the menu first restores focus onto the old Start mirror, which
installWebosTaskbar destroys immediately afterwards.
- An immediate fallback cannot target the new Start button either: the rebuilt taskbar's a11y mirror only exists after the engine's next a11y sync pass (which runs on the following frame).
Suggested direction
Re-validate the captured opener at restore time: if it is still connected, restore as today; otherwise fall back to the live taskbar's Start tile, deferring the actual .focus() past the next a11y sync (guarded so it yields if something else took focus meanwhile).
Summary
When the start menu is open and the user switches themes, DOM focus ends up stranded on
<body>after dismissal. Escape, click-outside, and app-launch all run throughcloseStartMenu(), whose opener-restoration silently no-ops because the captured opener element was destroyed with the old taskbar.Found during review of PR #31 (WEB-0035 fix round, finding F3).
Reproduction
openStartMenucaptures the focused element intoopenerFocus(src/desktop/main.ts:471).Expected: focus returns to a sensible control (the Start button).
Actual:
document.activeElementis<body>.Root cause
applyTheme(src/desktop/main.ts:81-82) callsinstallWebosTaskbar()— which destroys the oldWebOSTaskbartogether with its a11y mirrors (src/desktop/main.ts:415-418) — beforecloseStartMenu(). The PX-0077 restoration incloseStartMenu(src/desktop/main.ts:460-463) then calls.focus()on the captured opener, but that node is disconnected: focusing a detached element is a silent no-op, and the guard (activeElement === body) passes precisely because the destruction already dropped focus to<body>.Two constraints make the fix non-trivial:
installWebosTaskbardestroys immediately afterwards.Suggested direction
Re-validate the captured opener at restore time: if it is still connected, restore as today; otherwise fall back to the live taskbar's Start tile, deferring the actual
.focus()past the next a11y sync (guarded so it yields if something else took focus meanwhile).