From 22dc516dc77bbcb1a90af6780487b1a70c88fd11 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 6 Jun 2026 23:42:30 -0400 Subject: [PATCH 1/8] fix(modal): tag-chip kebab + ESC-after-accept + autocomplete scroll-into-view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two regressions the operator re-flagged (the earlier "fixes" didn't work): #711 tag-chip kebab: TagPanel's kebab used the #activator + v-bind="props" v-menu pattern — the exact pattern SuggestionItem's own comment documents as NEVER toggling inside the teleported ImageViewer modal. Extracted TagChip.vue using the proven explicit pattern (activator="parent" + :open-on-click="false" + a manual v-model), mirroring the working suggestion kebab. Now opens. #700 ESC-after-accept: the guard suppressed close whenever ANY non-tooltip overlay was active anywhere in the DOM, so a stray overlay after accepting a suggestion (focus drops to ) blocked Esc. Now key off the event origin — only defer to an overlay when Esc is pressed from INSIDE its content (ev.target.closest('.v-overlay__content')); a stray overlay no longer traps the modal, and dialogs/menus still handle their own Esc. A1: TagAutocomplete arrow-nav now scrollIntoView's the highlighted row — the list is capped at 240px and arrowing past the fold left the active item off-screen (operator-flagged). Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/components/modal/ImageViewer.vue | 28 ++++----- .../src/components/modal/TagAutocomplete.vue | 10 +++ frontend/src/components/modal/TagChip.vue | 61 +++++++++++++++++++ frontend/src/components/modal/TagPanel.vue | 53 ++-------------- 4 files changed, 90 insertions(+), 62 deletions(-) create mode 100644 frontend/src/components/modal/TagChip.vue diff --git a/frontend/src/components/modal/ImageViewer.vue b/frontend/src/components/modal/ImageViewer.vue index 53e17be..d052bbd 100644 --- a/frontend/src/components/modal/ImageViewer.vue +++ b/frontend/src/components/modal/ImageViewer.vue @@ -97,20 +97,20 @@ let prevBodyOverflow = null // own keystrokes. function onKeyDown(ev) { if (ev.key === 'Escape') { - // Escape closes the modal even from inside a text input — that's - // the universal "get me out of here" expectation, and the - // autofocused tag-entry field would otherwise trap focus with no - // visible escape (operator-flagged 2026-06-01). EXCEPTION: when a - // nested Vuetify overlay is open (v-menu autocomplete dropdown, - // FandomPicker v-dialog, per-suggestion 3-dot menu), let that - // overlay's own Esc handling fire instead of closing the whole - // modal mid-interaction. Vuetify marks open overlays with - // `.v-overlay--active`. - // EXCLUDE tooltips (`.v-tooltip`): they're also `.v-overlay--active` while - // shown, so one lingering after a hover/click (e.g. just after accepting a - // suggested tag) wrongly suppressed the close (#700). Only real interactive - // overlays (menus/dialogs) should keep ESC from closing the modal. - if (document.querySelector('.v-overlay--active:not(.v-tooltip)')) return + // Escape closes the modal even from inside a text input — the universal + // "get me out of here" expectation; the autofocused tag field would + // otherwise trap focus (operator-flagged 2026-06-01). EXCEPTION: when the + // keystroke originates INSIDE an open Vuetify overlay's content (a rename/ + // fandom/alias dialog, or a kebab menu), let that overlay handle its own + // Esc and don't close the whole modal. + // + // #700 re-fix (2026-06-07): the prior guard queried for ANY active overlay + // anywhere in the DOM and suppressed the close — so a lingering overlay + // after accepting a suggestion (focus drops to , not into any + // overlay) wrongly blocked Esc. Keying off the event's origin instead means + // a stray overlay no longer traps the modal: only an Esc pressed from + // within overlay content defers to that overlay. + if (ev.target?.closest?.('.v-overlay__content')) return ev.preventDefault() emit('close') } else if (ev.key === 'ArrowLeft') { diff --git a/frontend/src/components/modal/TagAutocomplete.vue b/frontend/src/components/modal/TagAutocomplete.vue index ec6a258..4e671f3 100644 --- a/frontend/src/components/modal/TagAutocomplete.vue +++ b/frontend/src/components/modal/TagAutocomplete.vue @@ -12,6 +12,7 @@ /> { const query = ref('') const hits = ref([]) const highlight = ref(0) +const listRef = ref(null) const fandomDialog = ref(false) let pendingNewName = null @@ -137,6 +139,14 @@ function moveHighlight (delta) { const total = hits.value.length + (allowCreate.value ? 1 : 0) if (total === 0) return highlight.value = (highlight.value + delta + total) % total + // Keep the highlighted row visible — the list is capped at 240px and arrowing + // past the fold otherwise left the active item off-screen (operator-flagged + // 2026-06-07). block:'nearest' scrolls the minimum needed. + nextTick(() => { + listRef.value?.$el + ?.querySelector('.v-list-item--active') + ?.scrollIntoView({ block: 'nearest' }) + }) } function onPick (hit) { emit('pick-existing', hit); reset() } diff --git a/frontend/src/components/modal/TagChip.vue b/frontend/src/components/modal/TagChip.vue new file mode 100644 index 0000000..f06c18a --- /dev/null +++ b/frontend/src/components/modal/TagChip.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/frontend/src/components/modal/TagPanel.vue b/frontend/src/components/modal/TagPanel.vue index 4123c18..1d913f4 100644 --- a/frontend/src/components/modal/TagPanel.vue +++ b/frontend/src/components/modal/TagPanel.vue @@ -2,43 +2,11 @@