From 47c29bd04e977a1b96c8e74049a8386131b47add Mon Sep 17 00:00:00 2001 From: r-zakarya <82443831+r-zakarya@users.noreply.github.com> Date: Thu, 23 Jul 2026 21:17:53 +0100 Subject: [PATCH] fix(esc): reauth / picker / quick-search were missing from the Esc chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three static modals had no entry in the fallback Esc handler, so Esc over them fell through and closed the editor behind (or did nothing when no editor was open — reauth and the autofill picker could only be dismissed by mouse). Also scopes the stray-modal catch added in a95becd to [data-dynamic]. As written it matched ANY open .modal, so an Esc over reauth / quick-search / the autofill picker would have removed those static nodes from the DOM for good, breaking the feature until restart. Only the runtime-built folder modal carries the marker. Co-Authored-By: Claude Opus 4.8 --- TEST_SCENARIOS.md | 4 ++-- js/app.js | 34 +++++++++++++++++++++++++--------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/TEST_SCENARIOS.md b/TEST_SCENARIOS.md index acb5074..0a8a5c8 100644 --- a/TEST_SCENARIOS.md +++ b/TEST_SCENARIOS.md @@ -62,11 +62,11 @@ Préparer : 3 entrées `A`, `B`, `C`, dont `A` dans le dossier `Work` avec le ta ### A9 · favori / épingle pendant l'édition (non-régression) - **ACTION** : `A` ouverte + dirty → cliquer ★ et 📌 sur la ligne `A` → Save -- **ATTENDU** : l'entrée reste favorite et épinglée (ces colonnes ne sont pas dans le PUT de l'éditeur). +- **ATTENDU** : l'entrée reste favorite et épinglée (ces colonnes ne sont pas dans le PUT de l'éditeur).✅ ### A10 · icône / pièce jointe pendant l'édition (non-régression) - **ACTION** : `A` ouverte + dirty → Settings → *Fetch missing* favicons (ou upload d'icône) → Save -- **ATTENDU** : l'icône survit au Save (endpoint dédié, hors payload éditeur). +- **ATTENDU** : l'icône survit au Save (endpoint dédié, hors payload éditeur).✅ icon not refreshed in edit ### A11 · sync / import pendant que l'éditeur est ouvert — **trou connu** - **ACTION** : `A` ouverte + dirty → lancer *Sync now* (ou importer un JSON en mode overwrite) qui met `A` à jour → Save diff --git a/js/app.js b/js/app.js index 0680c57..e65aae6 100644 --- a/js/app.js +++ b/js/app.js @@ -5592,7 +5592,9 @@ function openFolderModal(existing) { const old = document.getElementById('folderModal'); if (old) old.remove(); - const modal = el('div', { id: 'folderModal', class: 'modal' }); + // data-dynamic: built at runtime, so the Esc fallback may close it by + // removing the node (static modals must only ever be hidden). + const modal = el('div', { id: 'folderModal', class: 'modal', 'data-dynamic': '1' }); const backdrop = el('div', { class: 'modal-backdrop' }); backdrop.addEventListener('click', () => modal.remove()); modal.appendChild(backdrop); @@ -8003,14 +8005,28 @@ async function init() { closeGen(); return; } - // Any other open modal — the folder modal is built dynamically and - // has no id in this list, so without this Esc fell through to the - // slideover close below and shut the editor behind it. Closing a - // stray .modal is just .remove() (how its backdrop click closes). - // The static modals above never reach here (their id checks return - // first), so this only ever catches the dynamic ones. - const strayModal = document.querySelector('.modal:not(.is-hidden)'); - if (strayModal) { strayModal.remove(); return; } + if (!$('#reauthModal').classList.contains('is-hidden')) { + closeReauth(false); + return; + } + if (!$('#autofillPickerModal').classList.contains('is-hidden')) { + closeAutofillPicker(); + return; + } + if (!$('#quickSearchModal').classList.contains('is-hidden')) { + // Reachable when focus left the search input (clicked a row), + // so the input's own Esc handler doesn't fire. + closeQuickSearchModal(); + return; + } + // Modals built at runtime (the folder modal) have no id to test + // here, and without this Esc fell through to the slideover close + // below and shut the editor behind them. Removing the node IS how + // they close — their backdrop click does the same. Scoped to + // [data-dynamic] so it can never delete a static modal from the + // DOM, which would break that feature until the app restarts. + const dynModal = document.querySelector('.modal[data-dynamic]:not(.is-hidden)'); + if (dynModal) { dynModal.remove(); return; } if ($('#slideover').classList.contains('is-open')) { requestCloseSlideOver(); return;