refactor(ui): consolidate 7 hand-rolled kebabs into one KebabMenu (DRY pattern sweep)
First pattern-consistency DRY pass (process #594). The overflow kebab was hand-rolled 7 ways in two divergent activator strategies — Pattern A (#activator + v-bind) which silently breaks inside the teleported image modal (#711), and Pattern B (manual v-model + activator=parent + open-on-click=false + z-index 2400) the modal kebabs needed as a workaround. New <KebabMenu> (components/common) bakes in the modal-safe strategy UNIVERSALLY, so every kebab works in modal and non-modal contexts — folding the latent #711-class bug fix into all five Pattern-A sites. Menu items go in the default slot; variations (size/variant/location/label/min-width) are props. Adopted across all 7: TagChip, SuggestionItem, TagCard, SeriesView card, SeriesManageView, BackupRunsTable, SourceActions. Exhaustiveness (§8b): mdi-dots-vertical now lives only in KebabMenu. Labeled dropdowns / nav menus / filter popovers are a different concept and left alone. Seeded the pattern catalog so new code reuses the primitive. Test: KebabMenu renders slot items + trigger label/glyph + presentational props. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<!-- Canonical kebab (overflow ⋮) menu — the single primitive every kebab in
|
||||
the app uses (DRY pattern-consistency sweep 2026-06-09). It bakes in the
|
||||
modal-safe activator strategy that the image-modal kebabs needed (#711):
|
||||
a manual v-model toggled by @click.stop, `activator="parent"` for
|
||||
positioning only, `:open-on-click="false"`, and a z-index above the modal
|
||||
(2000) — so a kebab works identically inside a teleported modal and on a
|
||||
plain page. Menu items go in the default slot; close-on-content-click
|
||||
(Vuetify default) closes the menu when one is chosen. -->
|
||||
<span class="fc-kebab">
|
||||
<v-btn
|
||||
:icon="icon"
|
||||
:size="size"
|
||||
:variant="variant"
|
||||
:aria-label="label"
|
||||
@click.stop="open = !open"
|
||||
/>
|
||||
<v-menu
|
||||
v-model="open"
|
||||
activator="parent"
|
||||
:open-on-click="false"
|
||||
:location="location"
|
||||
:z-index="2400"
|
||||
>
|
||||
<v-list density="compact" :min-width="minWidth">
|
||||
<slot />
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
icon: { type: String, default: 'mdi-dots-vertical' },
|
||||
size: { type: String, default: 'x-small' },
|
||||
variant: { type: String, default: 'text' },
|
||||
location: { type: String, default: 'bottom end' },
|
||||
// aria-label for the trigger (every kebab should name what it acts on).
|
||||
label: { type: String, default: 'More actions' },
|
||||
minWidth: { type: [String, Number], default: undefined },
|
||||
})
|
||||
|
||||
const open = ref(false)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-kebab { display: inline-flex; align-items: center; }
|
||||
</style>
|
||||
@@ -46,34 +46,25 @@
|
||||
<v-chip size="x-small" label>{{ card.kind }}</v-chip>
|
||||
<div class="fc-tagcard__meta-right">
|
||||
<span class="fc-tagcard__count">{{ card.image_count }}</span>
|
||||
<v-menu>
|
||||
<template #activator="{ props: act }">
|
||||
<v-btn
|
||||
class="fc-tagcard__menu"
|
||||
icon="mdi-dots-vertical" size="x-small" variant="text"
|
||||
v-bind="act" @click.stop
|
||||
/>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item
|
||||
v-if="card.kind === 'character'"
|
||||
title="Set fandom…"
|
||||
prepend-icon="mdi-book-open-page-variant"
|
||||
@click="$emit('set-fandom', card)"
|
||||
/>
|
||||
<v-list-item
|
||||
title="Merge with…"
|
||||
prepend-icon="mdi-call-merge"
|
||||
@click="$emit('merge-with', card)"
|
||||
/>
|
||||
<v-list-item
|
||||
title="Delete tag"
|
||||
prepend-icon="mdi-delete"
|
||||
base-color="error"
|
||||
@click="$emit('delete', card)"
|
||||
/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<KebabMenu class="fc-tagcard__menu" :label="`Actions for ${card.name}`">
|
||||
<v-list-item
|
||||
v-if="card.kind === 'character'"
|
||||
title="Set fandom…"
|
||||
prepend-icon="mdi-book-open-page-variant"
|
||||
@click="$emit('set-fandom', card)"
|
||||
/>
|
||||
<v-list-item
|
||||
title="Merge with…"
|
||||
prepend-icon="mdi-call-merge"
|
||||
@click="$emit('merge-with', card)"
|
||||
/>
|
||||
<v-list-item
|
||||
title="Delete tag"
|
||||
prepend-icon="mdi-delete"
|
||||
base-color="error"
|
||||
@click="$emit('delete', card)"
|
||||
/>
|
||||
</KebabMenu>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
@@ -82,6 +73,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import KebabMenu from '../common/KebabMenu.vue'
|
||||
|
||||
const props = defineProps({ card: { type: Object, required: true } })
|
||||
const emit = defineEmits([
|
||||
|
||||
@@ -19,46 +19,29 @@
|
||||
>
|
||||
Accept
|
||||
</v-btn>
|
||||
<!-- Operator-flagged 2026-06-04: the kebab still wasn't opening. The
|
||||
prior `#activator` + `v-bind="props"` path never toggled the menu
|
||||
inside this teleported modal, while v-model-driven overlays (the
|
||||
dialogs in this modal) work fine. So drive the menu explicitly:
|
||||
the button toggles `menuOpen` with @click.stop (also shields any
|
||||
parent), and `activator="parent"` anchors the menu for positioning
|
||||
only — `:open-on-click="false"` keeps Vuetify's activator-click out
|
||||
of it, so there's a single, reliable opener. -->
|
||||
<span class="fc-suggestion__menu-wrap">
|
||||
<v-btn
|
||||
class="fc-suggestion__menu"
|
||||
icon="mdi-dots-vertical" size="small"
|
||||
variant="outlined" density="compact"
|
||||
:aria-label="`More actions for ${suggestion.display_name}`"
|
||||
@click.stop="menuOpen = !menuOpen"
|
||||
/>
|
||||
<v-menu
|
||||
v-model="menuOpen" activator="parent" :open-on-click="false"
|
||||
:z-index="2400"
|
||||
>
|
||||
<v-list density="compact">
|
||||
<v-list-item @click="$emit('alias', suggestion)">
|
||||
<v-list-item-title>Treat as alias for…</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="$emit('dismiss', suggestion)">
|
||||
<v-list-item-title>Dismiss for this image</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</span>
|
||||
<!-- Modal-safe kebab is baked into KebabMenu (this row lives in the
|
||||
teleported image modal — #711). -->
|
||||
<KebabMenu
|
||||
class="fc-suggestion__menu" size="small" variant="outlined"
|
||||
:label="`More actions for ${suggestion.display_name}`"
|
||||
>
|
||||
<v-list-item @click="$emit('alias', suggestion)">
|
||||
<v-list-item-title>Treat as alias for…</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="$emit('dismiss', suggestion)">
|
||||
<v-list-item-title>Dismiss for this image</v-list-item-title>
|
||||
</v-list-item>
|
||||
</KebabMenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import KebabMenu from '../common/KebabMenu.vue'
|
||||
|
||||
const props = defineProps({ suggestion: { type: Object, required: true } })
|
||||
defineEmits(['accept', 'alias', 'dismiss'])
|
||||
|
||||
const menuOpen = ref(false)
|
||||
const scorePct = computed(() => `${Math.round(props.suggestion.score * 100)}%`)
|
||||
</script>
|
||||
|
||||
@@ -102,11 +85,6 @@ const scorePct = computed(() => `${Math.round(props.suggestion.score * 100)}%`)
|
||||
.fc-suggestion__accept :deep(.v-btn__content) {
|
||||
font-size: 12px; letter-spacing: 0.02em;
|
||||
}
|
||||
.fc-suggestion__menu-wrap {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
.fc-suggestion__menu {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
<template>
|
||||
<!-- One tag chip + its kebab. The kebab uses the SAME explicit-menu pattern
|
||||
as SuggestionItem (activator="parent" + :open-on-click="false" + a manual
|
||||
v-model), because the #activator / v-bind="props" pattern never toggles a
|
||||
v-menu inside the teleported ImageViewer modal (#711, re-fixed 2026-06-07
|
||||
after the first attempt used the broken pattern). -->
|
||||
<span class="fc-tag-chip">
|
||||
<v-chip
|
||||
size="small" closable
|
||||
@@ -16,41 +11,28 @@
|
||||
:title="tag.fandom_name ? `Fandom: ${tag.fandom_name}` : 'Has a fandom'"
|
||||
>→<template v-if="fandomLabel"> {{ fandomLabel }}</template></span>
|
||||
</v-chip>
|
||||
<span class="fc-tag-chip__menu-wrap">
|
||||
<v-btn
|
||||
class="fc-tag-chip__kebab"
|
||||
icon="mdi-dots-vertical" size="x-small"
|
||||
variant="text" density="comfortable"
|
||||
:aria-label="`More actions for ${tag.name}`"
|
||||
@click.stop="menuOpen = !menuOpen"
|
||||
/>
|
||||
<!-- :z-index above the modal's 2000 — the teleported menu otherwise lands
|
||||
BEHIND .fc-viewer and gets blurred by its backdrop-filter (operator
|
||||
saw it ghosted behind the sidebar, 2026-06-07). THIS is what made the
|
||||
kebab look dead the whole time: it opened, just underneath. -->
|
||||
<v-menu v-model="menuOpen" activator="parent" :open-on-click="false" :z-index="2400">
|
||||
<v-list density="compact">
|
||||
<v-list-item @click="$emit('rename', tag)">
|
||||
<v-list-item-title>Rename…</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="tag.kind === 'character'" @click="$emit('set-fandom', tag)">
|
||||
<v-list-item-title>Set fandom…</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</span>
|
||||
<!-- Modal-safe kebab is baked into KebabMenu (this chip lives in the
|
||||
teleported image modal — #711). -->
|
||||
<KebabMenu class="fc-tag-chip__kebab" :label="`More actions for ${tag.name}`">
|
||||
<v-list-item @click="$emit('rename', tag)">
|
||||
<v-list-item-title>Rename…</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="tag.kind === 'character'" @click="$emit('set-fandom', tag)">
|
||||
<v-list-item-title>Set fandom…</v-list-item-title>
|
||||
</v-list-item>
|
||||
</KebabMenu>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useTagStore } from '../../stores/tags.js'
|
||||
import KebabMenu from '../common/KebabMenu.vue'
|
||||
|
||||
const props = defineProps({ tag: { type: Object, required: true } })
|
||||
defineEmits(['remove', 'rename', 'set-fandom'])
|
||||
|
||||
const store = useTagStore()
|
||||
const menuOpen = ref(false)
|
||||
|
||||
// Show a character's fandom inline (truncated). Falls back to a bare arrow when
|
||||
// only fandom_id is known but the name wasn't resolved (older payloads).
|
||||
@@ -71,7 +53,6 @@ function iconFor (k) { return KIND_ICONS[k] || 'mdi-tag' }
|
||||
|
||||
<style scoped>
|
||||
.fc-tag-chip { display: inline-flex; align-items: center; gap: 1px; }
|
||||
.fc-tag-chip__menu-wrap { display: inline-flex; align-items: center; }
|
||||
.fc-tag-chip__kebab { opacity: 0.7; }
|
||||
.fc-tag-chip:hover .fc-tag-chip__kebab { opacity: 1; }
|
||||
.fc-tag-chip__fandom { opacity: 0.7; font-size: 0.85em; }
|
||||
|
||||
@@ -30,25 +30,20 @@
|
||||
<span v-else class="fc-muted">—</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<v-menu>
|
||||
<template #activator="{ props: act }">
|
||||
<v-btn icon="mdi-dots-vertical" size="x-small" variant="text" v-bind="act" />
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item
|
||||
:title="r.tag ? 'Untag' : 'Tag…'"
|
||||
@click="$emit('tag', r)"
|
||||
/>
|
||||
<v-list-item
|
||||
title="Restore…" :disabled="r.status !== 'ok'"
|
||||
@click="$emit('restore', r)"
|
||||
/>
|
||||
<v-list-item
|
||||
title="Delete…"
|
||||
@click="$emit('delete', r)"
|
||||
/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<KebabMenu :label="`Actions for backup ${r.id}`">
|
||||
<v-list-item
|
||||
:title="r.tag ? 'Untag' : 'Tag…'"
|
||||
@click="$emit('tag', r)"
|
||||
/>
|
||||
<v-list-item
|
||||
title="Restore…" :disabled="r.status !== 'ok'"
|
||||
@click="$emit('restore', r)"
|
||||
/>
|
||||
<v-list-item
|
||||
title="Delete…"
|
||||
@click="$emit('delete', r)"
|
||||
/>
|
||||
</KebabMenu>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!runs.length">
|
||||
@@ -60,6 +55,7 @@
|
||||
|
||||
<script setup>
|
||||
import { formatRelative as fmtRelative } from '../../utils/date.js'
|
||||
import KebabMenu from '../common/KebabMenu.vue'
|
||||
|
||||
defineProps({ runs: { type: Array, default: () => [] } })
|
||||
defineEmits(['restore', 'delete', 'tag'])
|
||||
|
||||
@@ -26,40 +26,33 @@
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<v-menu location="bottom end">
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-btn icon size="small" variant="text" v-bind="menuProps" @click.stop>
|
||||
<v-icon>mdi-dots-vertical</v-icon>
|
||||
<v-tooltip activator="parent" location="top">More actions</v-tooltip>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list density="compact" min-width="260">
|
||||
<v-list-item
|
||||
v-if="isPatreon && !running"
|
||||
prepend-icon="mdi-backup-restore"
|
||||
@click="emit('recover', source)"
|
||||
>
|
||||
<v-list-item-title>Recover dropped near-duplicates</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
Re-fetch images previously dropped as near-dups and re-judge them
|
||||
under the current similarity threshold
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-divider v-if="isPatreon && !running" />
|
||||
<v-list-item
|
||||
base-color="error"
|
||||
prepend-icon="mdi-delete-outline"
|
||||
@click="emit('remove', source)"
|
||||
>
|
||||
<v-list-item-title>Remove source</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<KebabMenu size="small" :min-width="260" label="More actions">
|
||||
<v-list-item
|
||||
v-if="isPatreon && !running"
|
||||
prepend-icon="mdi-backup-restore"
|
||||
@click="emit('recover', source)"
|
||||
>
|
||||
<v-list-item-title>Recover dropped near-duplicates</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
Re-fetch images previously dropped as near-dups and re-judge them
|
||||
under the current similarity threshold
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-divider v-if="isPatreon && !running" />
|
||||
<v-list-item
|
||||
base-color="error"
|
||||
prepend-icon="mdi-delete-outline"
|
||||
@click="emit('remove', source)"
|
||||
>
|
||||
<v-list-item-title>Remove source</v-list-item-title>
|
||||
</v-list-item>
|
||||
</KebabMenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import KebabMenu from '../common/KebabMenu.vue'
|
||||
|
||||
const props = defineProps({
|
||||
source: { type: Object, required: true },
|
||||
|
||||
@@ -67,37 +67,29 @@
|
||||
@click="openPicker(ch.id)"
|
||||
>Add pages</v-btn>
|
||||
|
||||
<v-menu location="bottom end">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props" size="small" variant="text"
|
||||
icon="mdi-dots-vertical" title="Part actions"
|
||||
/>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item
|
||||
prepend-icon="mdi-chevron-up" title="Move up"
|
||||
:disabled="ci === 0"
|
||||
@click="store.moveChapter(ch.id, -1)"
|
||||
/>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-chevron-down" title="Move down"
|
||||
:disabled="ci === store.chapters.length - 1"
|
||||
@click="store.moveChapter(ch.id, 1)"
|
||||
/>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-arrow-collapse-up" title="Merge into previous"
|
||||
:disabled="ci === 0"
|
||||
@click="store.mergeChapter(ch.id, store.chapters[ci - 1].id)"
|
||||
/>
|
||||
<v-divider />
|
||||
<v-list-item
|
||||
prepend-icon="mdi-delete-outline" title="Delete part"
|
||||
base-color="error"
|
||||
@click="confirmDelete(ch)"
|
||||
/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<KebabMenu size="small" label="Part actions">
|
||||
<v-list-item
|
||||
prepend-icon="mdi-chevron-up" title="Move up"
|
||||
:disabled="ci === 0"
|
||||
@click="store.moveChapter(ch.id, -1)"
|
||||
/>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-chevron-down" title="Move down"
|
||||
:disabled="ci === store.chapters.length - 1"
|
||||
@click="store.moveChapter(ch.id, 1)"
|
||||
/>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-arrow-collapse-up" title="Merge into previous"
|
||||
:disabled="ci === 0"
|
||||
@click="store.mergeChapter(ch.id, store.chapters[ci - 1].id)"
|
||||
/>
|
||||
<v-divider />
|
||||
<v-list-item
|
||||
prepend-icon="mdi-delete-outline" title="Delete part"
|
||||
base-color="error"
|
||||
@click="confirmDelete(ch)"
|
||||
/>
|
||||
</KebabMenu>
|
||||
</header>
|
||||
|
||||
<div class="fc-part__statedrow">
|
||||
@@ -230,6 +222,7 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useSeriesManageStore, moveItem } from '../stores/seriesManage.js'
|
||||
import { useInfiniteScroll } from '../composables/useInfiniteScroll.js'
|
||||
import KebabMenu from '../components/common/KebabMenu.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const store = useSeriesManageStore()
|
||||
|
||||
@@ -60,27 +60,20 @@
|
||||
<span v-if="s.has_gap" class="fc-sbcard__gap" title="Has a missing-page gap">
|
||||
<v-icon size="x-small">mdi-alert-outline</v-icon> gap
|
||||
</span>
|
||||
<v-menu>
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-btn
|
||||
v-bind="menuProps"
|
||||
icon="mdi-dots-vertical" size="x-small" variant="flat"
|
||||
class="fc-sbcard__kebab" :aria-label="`Actions for ${s.name}`"
|
||||
@click.stop
|
||||
/>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item prepend-icon="mdi-pencil" @click.stop="openRename(s)">
|
||||
<v-list-item-title>Rename</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-delete" base-color="error"
|
||||
@click.stop="openDelete(s)"
|
||||
>
|
||||
<v-list-item-title>Delete</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<KebabMenu
|
||||
class="fc-sbcard__kebab" variant="flat"
|
||||
:label="`Actions for ${s.name}`"
|
||||
>
|
||||
<v-list-item prepend-icon="mdi-pencil" @click.stop="openRename(s)">
|
||||
<v-list-item-title>Rename</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-delete" base-color="error"
|
||||
@click.stop="openDelete(s)"
|
||||
>
|
||||
<v-list-item-title>Delete</v-list-item-title>
|
||||
</v-list-item>
|
||||
</KebabMenu>
|
||||
</div>
|
||||
<div class="fc-sbcard__body">
|
||||
<h3 class="fc-sbcard__name">{{ s.name }}</h3>
|
||||
@@ -210,6 +203,7 @@ import { useRouter } from 'vue-router'
|
||||
import { useSeriesBrowseStore } from '../stores/seriesBrowse.js'
|
||||
import { useSeriesSuggestionsStore } from '../stores/seriesSuggestions.js'
|
||||
import TagRenameDialog from '../components/modal/TagRenameDialog.vue'
|
||||
import KebabMenu from '../components/common/KebabMenu.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useSeriesBrowseStore()
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
import KebabMenu from '../../src/components/common/KebabMenu.vue'
|
||||
|
||||
// Canonical kebab primitive (DRY pattern-consistency sweep). Vuetify components
|
||||
// are left unresolved by the test harness, so we assert the wiring the consumers
|
||||
// rely on: the trigger's aria-label, the default kebab glyph, and that the
|
||||
// consumer's menu items (default slot) are rendered inside the menu.
|
||||
function mountKebab (props = {}) {
|
||||
return mount(KebabMenu, {
|
||||
props,
|
||||
slots: {
|
||||
default: '<v-list-item>Rename</v-list-item><v-list-item>Delete</v-list-item>',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('KebabMenu', () => {
|
||||
it('renders the trigger with the provided aria-label and the slotted items', () => {
|
||||
const w = mountKebab({ label: 'Actions for Foo' })
|
||||
expect(w.text()).toContain('Rename')
|
||||
expect(w.text()).toContain('Delete')
|
||||
expect(w.html()).toContain('Actions for Foo')
|
||||
})
|
||||
|
||||
it('defaults the trigger glyph to the kebab icon', () => {
|
||||
expect(mountKebab().html()).toContain('mdi-dots-vertical')
|
||||
})
|
||||
|
||||
it('passes presentational props through to the trigger/menu', () => {
|
||||
const w = mountKebab({ size: 'small', variant: 'flat', location: 'bottom end' })
|
||||
const html = w.html()
|
||||
expect(html).toContain('small')
|
||||
expect(html).toContain('flat')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user