25 components register 31 global Escape handlers on window/document, and none calls stopPropagation or checks defaultPrevented. A context menu or header popover open over a dialog closes together with the dialog.
No Escape handling at all: VerificationDialog, the MembershipGate rules dialog, the CallStage screen picker (CallStage.tsx:370), and the full-page Settings, Accounts, Downloads and Join panels - whose only exit is the X in the header (App.tsx:224-230). Discord's settings close on Esc.
Fix: a single layer stack (lib/layers.ts): each popup/dialog/panel pushes itself, and only the topmost one handles Escape. Migrate all 31 handlers to it.
Part of the v1.0 UI pass.
25 components register 31 global Escape handlers on `window`/`document`, and none calls `stopPropagation` or checks `defaultPrevented`. A context menu or header popover open over a dialog closes together with the dialog.
No Escape handling at all: VerificationDialog, the MembershipGate rules dialog, the CallStage screen picker (CallStage.tsx:370), and the full-page Settings, Accounts, Downloads and Join panels - whose only exit is the X in the header (App.tsx:224-230). Discord's settings close on Esc.
**Fix:** a single layer stack (`lib/layers.ts`): each popup/dialog/panel pushes itself, and only the topmost one handles Escape. Migrate all 31 handlers to it.
_Part of the v1.0 UI pass._
Salastil
added this to the v1.0 milestone 2026-10-07 20:42:53 -04:00
Salastil
added the bug label 2026-10-07 20:42:53 -04:00
No picture - the change is in what Escape does. Each popup, dialog and panel now registers in the order it opened (lib/layers.ts, useEscapeLayer), and one window listener sends Escape to the last - the one on top - and to nothing else. About twenty window-level listeners moved onto it: the context menu, header popovers, emoji picker, lightbox, profile card, forward picker, reason prompt, export dialog, leave confirmation, event source, space rooms, room search, Discord modals and events, file drop, the call view's panels, the mentions inbox, the reader list, Kick points and add-to-conversation. A layer's place in the order is when it became active, not when it last re-rendered (its handler is read through a ref).
The full-page panels (Settings, Accounts, Downloads, Join) now close on Escape too, underneath whatever opened over them. A field that handles Escape itself - closing a suggestion list, cancelling a reply - still wins, because it prevents the default first. The message-read shortcut from #286 stands down when any layer is open.
Checked in the isolated test copy with real key events: with Settings open and the cog's menu opened over it, Escape 1 closed only the menu (panel still open), Escape 2 closed the panel; with the export dialog open, Escape closed it. Not changed: the handlers that live on a single input (the member filter, the thread reply box) - they only fire when that field has focus.
Finished: [c5e7619](https://git.salastil.com/Salastil/moho/commit/c5e761924aafcf8673c3972ecb1439c21f881df9).
No picture - the change is in what Escape does. Each popup, dialog and panel now **registers in the order it opened** (`lib/layers.ts`, `useEscapeLayer`), and one window listener sends Escape to the **last** - the one on top - and to nothing else. About twenty window-level listeners moved onto it: the context menu, header popovers, emoji picker, lightbox, profile card, forward picker, reason prompt, export dialog, leave confirmation, event source, space rooms, room search, Discord modals and events, file drop, the call view's panels, the mentions inbox, the reader list, Kick points and add-to-conversation. A layer's place in the order is when it became active, not when it last re-rendered (its handler is read through a ref).
**The full-page panels** (Settings, Accounts, Downloads, Join) now close on Escape too, underneath whatever opened over them. A field that handles Escape itself - closing a suggestion list, cancelling a reply - still wins, because it prevents the default first. The message-read shortcut from #286 stands down when any layer is open.
Checked in the isolated test copy with real key events: with Settings open and the cog's menu opened over it, **Escape 1 closed only the menu** (panel still open), **Escape 2 closed the panel**; with the export dialog open, Escape closed it. Not changed: the handlers that live on a single input (the member filter, the thread reply box) - they only fire when that field has focus.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
25 components register 31 global Escape handlers on
window/document, and none callsstopPropagationor checksdefaultPrevented. A context menu or header popover open over a dialog closes together with the dialog.No Escape handling at all: VerificationDialog, the MembershipGate rules dialog, the CallStage screen picker (CallStage.tsx:370), and the full-page Settings, Accounts, Downloads and Join panels - whose only exit is the X in the header (App.tsx:224-230). Discord's settings close on Esc.
Fix: a single layer stack (
lib/layers.ts): each popup/dialog/panel pushes itself, and only the topmost one handles Escape. Migrate all 31 handlers to it.Part of the v1.0 UI pass.
Finished: c5e7619.
No picture - the change is in what Escape does. Each popup, dialog and panel now registers in the order it opened (
lib/layers.ts,useEscapeLayer), and one window listener sends Escape to the last - the one on top - and to nothing else. About twenty window-level listeners moved onto it: the context menu, header popovers, emoji picker, lightbox, profile card, forward picker, reason prompt, export dialog, leave confirmation, event source, space rooms, room search, Discord modals and events, file drop, the call view's panels, the mentions inbox, the reader list, Kick points and add-to-conversation. A layer's place in the order is when it became active, not when it last re-rendered (its handler is read through a ref).The full-page panels (Settings, Accounts, Downloads, Join) now close on Escape too, underneath whatever opened over them. A field that handles Escape itself - closing a suggestion list, cancelling a reply - still wins, because it prevents the default first. The message-read shortcut from #286 stands down when any layer is open.
Checked in the isolated test copy with real key events: with Settings open and the cog's menu opened over it, Escape 1 closed only the menu (panel still open), Escape 2 closed the panel; with the export dialog open, Escape closed it. Not changed: the handlers that live on a single input (the member filter, the thread reply box) - they only fire when that field has focus.