mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-24 05:35:31 -04:00
fix(ui): route transient dropdown menus through escMenuStack to stop listener leaks (#4684)
The app's ad-hoc dropdown/context menus each wire their own document-level outside-click listener, but that listener only removes itself on an *outside* click. Every other dismissal path -- clicking a menu item (which calls el.remove() directly), a Cancel button, Escape, or the "close the previously-open menu" reopen sweep -- tears the node down without unregistering the listener, orphaning it on `document`. The stranded listener then lingers and can break the next menu interaction: the recurring "the button stops working until I refresh the page" class of bug (e.g. delete an email, then the kebab/more button is dead on the other rows). Route all 16 of these menus through the existing escMenuStack helper (bindMenuDismiss / dismissOrRemove), exactly as documentLibrary.js _showLibDropdown, cookbookRunning.js, and research/panel.js already do: a single idempotent close() owns the teardown and is released on every dismissal path, reopen sweeps use dismissOrRemove() instead of a bare .remove(), and Escape flows through the central LIFO esc-stack arbiter. Net -49 lines. Menus migrated: cookbook _showDepMenu; document export menu and _openDocAiReplyChoice; emailInbox _showEmailMenu; emailLibrary _showReaderMoreMenu / _showCardMenu / _showBulkActionsMenu; gallery _showGalleryBulkMenu; notes _pickCustomDate / _openNoteCornerMenu; settings (3 unified-integrations dropdowns); skills _openSkillMenu; tasks _showTaskDropdown; compare _toggleExportMenu. Per-menu semantics preserved (anchor-as-inside tests, the tasks 250ms ghost-click guard, emailLibrary's reader-more-active anchor class and the bulk-Cancel select-mode reset, settings' reused-vs-recreated lifecycles). Six menus with custom lifecycles (notes _openReminderMenu, sessions long-press, document markdown-toolbar, emojiPicker, compare model selector) are intentionally left for a follow-up -- each needs individual review. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+24
-43
@@ -23,6 +23,7 @@ import {
|
||||
} from './emailLibrary/signatureFold.js';
|
||||
import { state } from './emailLibrary/state.js';
|
||||
import { collapseSidebarToRail } from './modalSnap.js';
|
||||
import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js';
|
||||
|
||||
const API_BASE = window.location.origin;
|
||||
let _emailUnreadChipClickWired = false;
|
||||
@@ -5499,16 +5500,12 @@ function _showReaderMoreMenu(em, card, reader, anchor) {
|
||||
// Toggle: if a dropdown for THIS anchor is already open, close it.
|
||||
const existing = document.querySelector('.email-card-dropdown');
|
||||
if (existing && existing._anchor === anchor) {
|
||||
existing.remove();
|
||||
anchor.classList.remove('reader-more-active');
|
||||
dismissOrRemove(existing);
|
||||
return;
|
||||
}
|
||||
// Otherwise close any other open dropdown (and clear its anchor's active
|
||||
// state) before opening a fresh one.
|
||||
document.querySelectorAll('.email-card-dropdown').forEach(d => {
|
||||
if (d._anchor) d._anchor.classList.remove('reader-more-active');
|
||||
d.remove();
|
||||
});
|
||||
// Otherwise close any other open dropdown (its own teardown clears its
|
||||
// anchor's active state) before opening a fresh one.
|
||||
document.querySelectorAll('.email-card-dropdown').forEach(dismissOrRemove);
|
||||
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'email-card-dropdown';
|
||||
@@ -5721,8 +5718,7 @@ function _showReaderMoreMenu(em, card, reader, anchor) {
|
||||
_showLibRemindSubmenu(em, dropdown);
|
||||
return;
|
||||
}
|
||||
dropdown.remove();
|
||||
anchor.classList.remove('reader-more-active');
|
||||
close();
|
||||
a.action();
|
||||
});
|
||||
dropdown.appendChild(item);
|
||||
@@ -5735,25 +5731,20 @@ function _showReaderMoreMenu(em, card, reader, anchor) {
|
||||
cancelItem.innerHTML = _icon(_cancelIco) + '<span>Cancel</span>';
|
||||
cancelItem.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
dropdown.remove();
|
||||
anchor.classList.remove('reader-more-active');
|
||||
close();
|
||||
});
|
||||
dropdown.appendChild(cancelItem);
|
||||
|
||||
document.body.appendChild(dropdown);
|
||||
_fitEmailDropdown(dropdown, rect);
|
||||
const close = (ev) => {
|
||||
if (!dropdown.contains(ev.target) && ev.target !== anchor) {
|
||||
dropdown.remove();
|
||||
anchor.classList.remove('reader-more-active');
|
||||
document.removeEventListener('click', close, true);
|
||||
}
|
||||
};
|
||||
setTimeout(() => document.addEventListener('click', close, true), 10);
|
||||
const close = bindMenuDismiss(dropdown, () => {
|
||||
dropdown.remove();
|
||||
anchor.classList.remove('reader-more-active');
|
||||
}, (ev) => !dropdown.contains(ev.target) && ev.target !== anchor);
|
||||
}
|
||||
|
||||
function _showCardMenu(em, anchor) {
|
||||
document.querySelectorAll('.email-card-dropdown').forEach(d => d.remove());
|
||||
document.querySelectorAll('.email-card-dropdown').forEach(dismissOrRemove);
|
||||
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'email-card-dropdown';
|
||||
@@ -5918,8 +5909,7 @@ function _showCardMenu(em, anchor) {
|
||||
_showLibRemindSubmenu(em, dropdown);
|
||||
return;
|
||||
}
|
||||
dropdown.remove();
|
||||
anchor.classList.remove('reader-more-active');
|
||||
close();
|
||||
a.action();
|
||||
});
|
||||
dropdown.appendChild(item);
|
||||
@@ -5932,26 +5922,21 @@ function _showCardMenu(em, anchor) {
|
||||
cancelItem.innerHTML = _icon(_cancelIco) + '<span>Cancel</span>';
|
||||
cancelItem.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
dropdown.remove();
|
||||
anchor.classList.remove('reader-more-active');
|
||||
close();
|
||||
});
|
||||
dropdown.appendChild(cancelItem);
|
||||
|
||||
document.body.appendChild(dropdown);
|
||||
_fitEmailDropdown(dropdown, rect);
|
||||
const close = (ev) => {
|
||||
if (!dropdown.contains(ev.target) && ev.target !== anchor) {
|
||||
dropdown.remove();
|
||||
anchor.classList.remove('reader-more-active');
|
||||
document.removeEventListener('click', close, true);
|
||||
}
|
||||
};
|
||||
setTimeout(() => document.addEventListener('click', close, true), 10);
|
||||
const close = bindMenuDismiss(dropdown, () => {
|
||||
dropdown.remove();
|
||||
anchor.classList.remove('reader-more-active');
|
||||
}, (ev) => !dropdown.contains(ev.target) && ev.target !== anchor);
|
||||
}
|
||||
|
||||
// Bulk "Actions" dropdown for select mode — Delete is a separate visible button.
|
||||
function _showBulkActionsMenu(anchor) {
|
||||
document.querySelectorAll('.email-card-dropdown').forEach(d => d.remove());
|
||||
document.querySelectorAll('.email-card-dropdown').forEach(dismissOrRemove);
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'email-card-dropdown email-bulk-menu';
|
||||
const rect = anchor.getBoundingClientRect();
|
||||
@@ -5968,7 +5953,7 @@ function _showBulkActionsMenu(anchor) {
|
||||
const it = document.createElement('div');
|
||||
it.className = 'dropdown-item-compact' + (a.danger ? ' dropdown-item-danger' : '');
|
||||
it.innerHTML = `<span class="dropdown-icon">${a.icon}</span><span>${a.label}</span>`;
|
||||
it.addEventListener('click', (e) => { e.stopPropagation(); dropdown.remove(); a.action(); });
|
||||
it.addEventListener('click', (e) => { e.stopPropagation(); close(); a.action(); });
|
||||
dropdown.appendChild(it);
|
||||
}
|
||||
// Mobile-only Cancel — matches the per-card and sidebar dropdowns.
|
||||
@@ -5978,7 +5963,7 @@ function _showBulkActionsMenu(anchor) {
|
||||
cancelIt.innerHTML = `<span class="dropdown-icon">${_cancelIco2}</span><span>Cancel</span>`;
|
||||
cancelIt.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
dropdown.remove();
|
||||
close();
|
||||
// Cancel inside the bulk-Actions menu also exits select mode — matches the
|
||||
// documents bulk dropdown.
|
||||
state._selectMode = false;
|
||||
@@ -5989,13 +5974,9 @@ function _showBulkActionsMenu(anchor) {
|
||||
dropdown.appendChild(cancelIt);
|
||||
document.body.appendChild(dropdown);
|
||||
_fitEmailDropdown(dropdown, rect);
|
||||
const close = (ev) => {
|
||||
if (!dropdown.contains(ev.target) && ev.target !== anchor) {
|
||||
dropdown.remove();
|
||||
document.removeEventListener('click', close, true);
|
||||
}
|
||||
};
|
||||
setTimeout(() => document.addEventListener('click', close, true), 10);
|
||||
const close = bindMenuDismiss(dropdown, () => {
|
||||
dropdown.remove();
|
||||
}, (ev) => !dropdown.contains(ev.target) && ev.target !== anchor);
|
||||
}
|
||||
|
||||
function _updateBulkBar() {
|
||||
|
||||
Reference in New Issue
Block a user