Skip to content

Transient dropdown menus stop working until a page refresh (orphaned outside-click listeners) #4686

Description

@Mocchibird

Prerequisites

  • I searched open issues and discussions and did not find an existing report of this bug.
  • This is not a security vulnerability.
  • I am running the latest code from the dev branch and the bug still reproduces there.

Install Method

Docker (docker compose up)

Operating System

Linux

Steps to Reproduce

  1. Open the email library and open an email so the reader view shows.
  2. Go back to the list, then permanently delete that email (not trash).
  3. Click the "..." (more / kebab) button on any other email row.
  4. Observe: the dropdown does not open — the button is dead until the page is reloaded.

This is one instance of a general pattern. The same stale-listener breakage can be triggered on most of the app's ad-hoc dropdown/context menus (gallery bulk menu, notes corner menu, the document export menu, the skills card menu, the tasks dropdown, the settings integration dropdowns, and more) any time a menu is dismissed by something other than an outside click.

Expected Behaviour

After a menu closes by any means — clicking an item, a Cancel button, Escape, or opening a different menu — every other menu trigger keeps working normally. No page refresh should ever be required to "unstick" a menu button.

Actual Behaviour

Each of these menus wires its own document-level outside-click listener, roughly:

const close = (ev) => { if (menu.contains(ev.target)) return; menu.remove(); document.removeEventListener('click', close, true); };
setTimeout(() => document.addEventListener('click', close, true), 10);

That listener only removes itself on an outside click. Every other dismissal path — an item click that calls menu.remove() directly, a Cancel button, Escape, or the "close the previously-open menu" reopen sweep — tears the node down without unregistering the listener, leaving it orphaned on document. The stale listener then interferes with the next menu interaction, so the trigger appears dead until a reload. No console error is produced, which is why it is easy to miss.

The repo already ships the right tool for this: the escMenuStack helper (bindMenuDismiss / dismissOrRemove) introduced in #684 and already used correctly by documentLibrary.js _showLibDropdown — but roughly 22 other menus were never migrated onto it.

Logs / Screenshots

No console error or stack trace is produced; the failure is silent — the menu trigger simply does nothing on the next click until the page is reloaded.

Model / Backend (if relevant)

N/A — this is a front-end (browser) UI bug, independent of the model backend or OS.

Are you willing to submit a fix?

Yes — I can open a PR

Additional Information

Root cause: the orphaned document click listener. Fix: route these menus through the existing escMenuStack helper so a single idempotent close() owns teardown on every dismissal path (item click, Cancel, Escape, outside click, reopen sweep).

A draft PR for the 16 mechanical (clean 1:1 migration) menus is up at #4684, left as a draft pending end-to-end verification. Six menus with more involved lifecycles (the notes reminder menu, the sessions long-press menu, the document markdown-toolbar menu, the emoji picker, and the compare model-selector) are noted there as a follow-up.

Transparency: this bug and the proposed fix were prepared with the assistance of an LLM agent (Claude Code). Per CONTRIBUTING.md's "Auto-generated PRs" note I am filing this issue first and leaving the PR as a draft for your call on whether to proceed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready for reviewDescription complete — ready for maintainer review

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions