fix(fandom): match change-fandom modal focus + Enter to FandomPicker
The 'Fandom for <character>' dialog (FandomSetDialog) used plain autofocus and had no Enter handling, so Enter re-opened the dropdown instead of submitting — the same bug FandomPicker already fixed. Mirror that flow: parent v-dialogs focus the field via @after-enter→focusSearch (reliable past the focus-trap); capture-phase Enter Saves the changed selection instead of re-opening the menu; Tab jumps to the new-fandom field; creating a fandom returns focus to the dropdown so a single Enter saves it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,20 +3,29 @@
|
||||
<v-card-title class="text-body-1">Fandom for “{{ tag.name }}”</v-card-title>
|
||||
<v-card-text>
|
||||
<template v-if="!collision">
|
||||
<!-- Keyboard flow matches FandomPicker (operator-specified 2026-06-07):
|
||||
focus lands here via the parent dialog's @after-enter → focusSearch
|
||||
(autofocus is unreliable inside a v-dialog focus-trap); Tab moves to
|
||||
the new-fandom field; a SECOND Enter (menu closed) accepts the choice
|
||||
by Saving instead of re-opening the dropdown. -->
|
||||
<v-autocomplete
|
||||
ref="fandomRef"
|
||||
v-model="selectedId"
|
||||
v-model:menu="menuOpen"
|
||||
:items="store.fandomCache"
|
||||
:item-title="(f) => f.name" :item-value="(f) => f.id"
|
||||
label="Fandom" clearable density="compact"
|
||||
autofocus
|
||||
:hint="selectedId == null
|
||||
? 'No fandom — the character will be unassigned.' : ''"
|
||||
persistent-hint
|
||||
@keydown.enter.capture="onSearchEnter"
|
||||
@keydown.tab="onSearchTab"
|
||||
/>
|
||||
<v-divider class="my-3" />
|
||||
<p class="text-caption mb-2">Or create a new fandom:</p>
|
||||
<div class="d-flex" style="gap: 8px;">
|
||||
<v-text-field
|
||||
ref="newNameRef"
|
||||
v-model="newName" placeholder="New fandom name"
|
||||
density="compact" hide-details
|
||||
@keydown.enter.prevent="onCreate"
|
||||
@@ -71,7 +80,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
import { useTagStore } from '../../stores/tags.js'
|
||||
|
||||
const props = defineProps({ tag: { type: Object, required: true } })
|
||||
@@ -84,10 +93,25 @@ const newName = ref('')
|
||||
const busy = ref(false)
|
||||
const error = ref(null)
|
||||
const collision = ref(null)
|
||||
const menuOpen = ref(false)
|
||||
const fandomRef = ref(null)
|
||||
const newNameRef = ref(null)
|
||||
|
||||
// Always refresh on open so the list reflects fandoms created elsewhere (#712).
|
||||
onMounted(() => store.loadFandoms())
|
||||
|
||||
// Exposed so the parent dialog can focus the search field on @after-enter —
|
||||
// the reliable point to grab focus (the dialog transition + focus-trap are done).
|
||||
function focusSearch() {
|
||||
nextTick(() => {
|
||||
fandomRef.value?.focus?.()
|
||||
// Keep the menu closed after a programmatic focus so the very next Enter
|
||||
// accepts the value (Saves) instead of being swallowed as a menu interaction.
|
||||
menuOpen.value = false
|
||||
})
|
||||
}
|
||||
defineExpose({ focusSearch })
|
||||
|
||||
async function onCreate() {
|
||||
const name = newName.value.trim()
|
||||
if (!name) return
|
||||
@@ -97,6 +121,9 @@ async function onCreate() {
|
||||
const f = await store.createFandom(name)
|
||||
selectedId.value = f.id
|
||||
newName.value = ''
|
||||
// The created fandom is now the selection; hand focus back to the dropdown
|
||||
// so a single Enter saves it (matches FandomPicker's flow).
|
||||
focusSearch()
|
||||
} catch (e) {
|
||||
error.value = e.message || String(e)
|
||||
} finally {
|
||||
@@ -104,6 +131,29 @@ async function onCreate() {
|
||||
}
|
||||
}
|
||||
|
||||
// Enter on the search field — bound in the CAPTURE phase so it runs BEFORE
|
||||
// Vuetify's own handler (which opens the menu on Enter). Menu open → let Vuetify
|
||||
// pick the highlighted item. Menu closed with a changed selection → Save and stop
|
||||
// the event so Vuetify never (re)opens the dropdown (the bug this fixes).
|
||||
function onSearchEnter(e) {
|
||||
if (menuOpen.value) return
|
||||
if (busy.value) return
|
||||
if (selectedId.value !== (props.tag.fandom_id ?? null)) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onSave()
|
||||
}
|
||||
}
|
||||
|
||||
// Tab from the search field goes to the "new fandom" text field (operator-
|
||||
// specified). Shift+Tab keeps normal reverse traversal.
|
||||
function onSearchTab(e) {
|
||||
if (e.shiftKey) return
|
||||
e.preventDefault()
|
||||
menuOpen.value = false
|
||||
nextTick(() => newNameRef.value?.focus?.())
|
||||
}
|
||||
|
||||
async function save(merge) {
|
||||
busy.value = true
|
||||
error.value = null
|
||||
|
||||
@@ -34,8 +34,12 @@
|
||||
/>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog v-model="fandomDialog" max-width="460">
|
||||
<v-dialog
|
||||
v-model="fandomDialog" max-width="460"
|
||||
@after-enter="fandomSetRef?.focusSearch?.()"
|
||||
>
|
||||
<FandomSetDialog
|
||||
ref="fandomSetRef"
|
||||
v-if="fandomTarget" :tag="fandomTarget"
|
||||
@updated="onFandomUpdated" @cancel="fandomDialog = false"
|
||||
/>
|
||||
@@ -98,6 +102,7 @@ async function onRenamed() {
|
||||
|
||||
const fandomDialog = ref(false)
|
||||
const fandomTarget = ref(null)
|
||||
const fandomSetRef = ref(null)
|
||||
function openSetFandom(tag) {
|
||||
fandomTarget.value = tag
|
||||
fandomDialog.value = true
|
||||
|
||||
@@ -87,8 +87,12 @@
|
||||
@confirm="onDeleteTagConfirm"
|
||||
/>
|
||||
|
||||
<v-dialog v-model="fandomDialogOpen" max-width="460">
|
||||
<v-dialog
|
||||
v-model="fandomDialogOpen" max-width="460"
|
||||
@after-enter="fandomSetRef?.focusSearch?.()"
|
||||
>
|
||||
<FandomSetDialog
|
||||
ref="fandomSetRef"
|
||||
v-if="fandomTarget" :tag="fandomTarget"
|
||||
@updated="onFandomUpdated" @cancel="fandomDialogOpen = false"
|
||||
/>
|
||||
@@ -152,6 +156,7 @@ function openTag(tagId) {
|
||||
// Character fandom editing (dots-menu → FandomSetDialog).
|
||||
const fandomDialogOpen = ref(false)
|
||||
const fandomTarget = ref(null)
|
||||
const fandomSetRef = ref(null)
|
||||
function onSetFandom(card) {
|
||||
fandomTarget.value = card
|
||||
fandomDialogOpen.value = true
|
||||
|
||||
Reference in New Issue
Block a user