Content Menu Content
Demo
Right click here
Related Components
This requires the following components to be installed:
Related Composables
This requires the following composables to be installed:
Related Utils
This requires the following utils to be installed:
Related Types
This requires the following types to be installed:
Related Theme
This requires the following theme to be installed:
Component
vue
<script lang="ts">
import type { AvatarProps } from '@/ui/components/Avatar.vue'
import type { ContextMenuItem, ContextMenuSlots } from '@/ui/components/ContextMenu.vue'
import type { KbdProps } from '@/ui/components/Kbd.vue'
import type theme from '@/ui/theme/context-menu'
import type { ArrayOrNested, ComponentConfig, NestedItem } from '@/ui/utils/utils'
import type { ContextMenuContentEmits as RekaContextMenuContentEmits, ContextMenuContentProps as RekaContextMenuContentProps } from 'reka-ui'
import Avatar from '@/ui/components/Avatar.vue'
import ContextMenuContent from '@/ui/components/ContextMenuContent.vue'
import Icon from '@/ui/components/Icon.vue'
import Kbd from '@/ui/components/Kbd.vue'
import Link from '@/ui/components/Link.vue'
import { usePortal } from '@/ui/composables/usePortal'
import { checkIcon, chevronRightIcon, externalIcon, loadingIcon } from '@/ui/icons'
import { get } from '@/ui/utils/get'
import { isArrayOfArray } from '@/ui/utils/is-array-of-array'
import { omit } from '@/ui/utils/omit'
import { pickLinkProps } from '@/ui/utils/pick-link-props'
import { createReusableTemplate, reactiveOmit } from '@vueuse/core'
import { useForwardPropsEmits } from 'reka-ui'
import { ContextMenu as RekaContextMenu } from 'reka-ui/namespaced'
import { computed, toRef } from 'vue'
type ContextMenu = ComponentConfig<typeof theme>
interface ContextMenuContentProps<T extends ArrayOrNested<ContextMenuItem>> extends Omit<RekaContextMenuContentProps, 'as' | 'asChild' | 'forceMount'> {
items?: T
portal?: boolean | string | HTMLElement
sub?: boolean
labelKey: keyof NestedItem<T>
class?: any
ui: { [K in keyof Required<ContextMenu['slots']>]: (props?: Record<string, any>) => string }
uiOverride?: ContextMenu['slots']
}
interface ContextMenuContentEmits extends RekaContextMenuContentEmits {}
</script>
<script setup lang="ts" generic="T extends ArrayOrNested<ContextMenuItem>">
const props = defineProps<ContextMenuContentProps<T>>()
const emits = defineEmits<ContextMenuContentEmits>()
const slots = defineSlots<ContextMenuSlots<T>>()
const portalProps = usePortal(toRef(() => props.portal))
const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'labelKey', 'class', 'ui', 'uiOverride'), emits)
const proxySlots = omit(slots, ['default'])
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: ContextMenuItem, active?: boolean, index: number }>()
const groups = computed<ContextMenuItem[][]>(() =>
props.items?.length
? isArrayOfArray(props.items)
? props.items
: [props.items]
: [],
)
</script>
<template>
<DefineItemTemplate v-slot="{ item, active, index }">
<slot :name="((item.slot || 'item') as keyof ContextMenuSlots<T>)" :item="item" :index="index">
<slot :name="((item.slot ? `${item.slot}-leading` : 'item-leading') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
<Icon v-if="item.loading" :name="loadingIcon" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, loading: true })" />
<Icon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, active })" />
<Avatar v-else-if="item.avatar" :size="((props.uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: uiOverride?.itemLeadingAvatar, active })" />
</slot>
<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label` : 'item-label') as keyof ContextMenuSlots<T>]" :class="ui.itemLabel({ class: uiOverride?.itemLabel, active })">
<slot :name="((item.slot ? `${item.slot}-label` : 'item-label') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
{{ get(item, props.labelKey as string) }}
</slot>
<Icon v-if="item.target === '_blank'" :name="externalIcon" :class="ui.itemLabelExternalIcon({ class: uiOverride?.itemLabelExternalIcon, color: item?.color, active })" />
</span>
<span :class="ui.itemTrailing({ class: uiOverride?.itemTrailing })">
<slot :name="((item.slot ? `${item.slot}-trailing` : 'item-trailing') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
<Icon v-if="item.children?.length" :name="chevronRightIcon" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, color: item?.color, active })" />
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: uiOverride?.itemTrailingKbds })">
<Kbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((props.uiOverride?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
</span>
</slot>
<RekaContextMenu.ItemIndicator as-child>
<Icon :name="checkIcon" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, color: item?.color })" />
</RekaContextMenu.ItemIndicator>
</span>
</slot>
</DefineItemTemplate>
<RekaContextMenu.Portal v-bind="portalProps">
<component :is="sub ? RekaContextMenu.SubContent : RekaContextMenu.Content" :class="props.class" v-bind="contentProps">
<slot name="content-top" />
<RekaContextMenu.Group v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: uiOverride?.group })">
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
<RekaContextMenu.Label v-if="item.type === 'label'" :class="ui.label({ class: uiOverride?.label })">
<ReuseItemTemplate :item="item" :index="index" />
</RekaContextMenu.Label>
<RekaContextMenu.Separator v-else-if="item.type === 'separator'" :class="ui.separator({ class: uiOverride?.separator })" />
<RekaContextMenu.Sub v-else-if="item?.children?.length" :open="item.open" :default-open="item.defaultOpen">
<RekaContextMenu.SubTrigger
as="button"
type="button"
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
:class="ui.item({ class: uiOverride?.item, color: item?.color })"
>
<ReuseItemTemplate :item="item" :index="index" />
</RekaContextMenu.SubTrigger>
<ContextMenuContent
sub
:class="props.class"
:ui="ui"
:ui-override="uiOverride"
:portal="portal"
:items="(item.children as T)"
:align-offset="-4"
:label-key="labelKey"
v-bind="item.content"
>
<template v-for="(_, name) in proxySlots" #[name]="slotData">
<slot :name="(name as keyof ContextMenuSlots<T>)" v-bind="slotData" />
</template>
</ContextMenuContent>
</RekaContextMenu.Sub>
<RekaContextMenu.CheckboxItem
v-else-if="item.type === 'checkbox'"
:model-value="item.checked"
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
:class="ui.item({ class: [uiOverride?.item, item.class], color: item?.color })"
@update:model-value="item.onUpdateChecked"
@select="item.onSelect"
>
<ReuseItemTemplate :item="item" :index="index" />
</RekaContextMenu.CheckboxItem>
<RekaContextMenu.Item
v-else
as-child
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
@select="item.onSelect"
>
<Link v-bind="pickLinkProps(item as Omit<ContextMenuItem, 'type'>)" :class="ui.item({ class: [uiOverride?.item, item.class], active: item.active, color: item?.color })" raw>
<ReuseItemTemplate :item="item" :active="item.active" :index="index" />
</Link>
</RekaContextMenu.Item>
</template>
</RekaContextMenu.Group>
<slot />
<slot name="content-bottom" />
</component>
</RekaContextMenu.Portal>
</template>vue
<script lang="ts">
import type { AvatarProps } from '@/UI/Components/Avatar.vue'
import type { ContextMenuItem, ContextMenuSlots } from '@/UI/Components/ContextMenu.vue'
import type { KbdProps } from '@/UI/Components/Kbd.vue'
import type theme from '@/UI/Theme/context-menu'
import type { ArrayOrNested, ComponentConfig, NestedItem } from '@/UI/Utils/utils'
import type { ContextMenuContentEmits as RekaContextMenuContentEmits, ContextMenuContentProps as RekaContextMenuContentProps } from 'reka-ui'
import Avatar from '@/UI/Components/Avatar.vue'
import ContextMenuContent from '@/UI/Components/ContextMenuContent.vue'
import Icon from '@/UI/Components/Icon.vue'
import Kbd from '@/UI/Components/Kbd.vue'
import Link from '@/UI/Components/Link.vue'
import { usePortal } from '@/UI/Composables/usePortal'
import { checkIcon, chevronRightIcon, externalIcon, loadingIcon } from '@/UI/icons'
import { get } from '@/UI/Utils/get'
import { isArrayOfArray } from '@/UI/Utils/is-array-of-array'
import { omit } from '@/UI/Utils/omit'
import { pickLinkProps } from '@/UI/Utils/pick-link-props'
import { createReusableTemplate, reactiveOmit } from '@vueuse/core'
import { useForwardPropsEmits } from 'reka-ui'
import { ContextMenu as RekaContextMenu } from 'reka-ui/namespaced'
import { computed, toRef } from 'vue'
type ContextMenu = ComponentConfig<typeof theme>
interface ContextMenuContentProps<T extends ArrayOrNested<ContextMenuItem>> extends Omit<RekaContextMenuContentProps, 'as' | 'asChild' | 'forceMount'> {
items?: T
portal?: boolean | string | HTMLElement
sub?: boolean
labelKey: keyof NestedItem<T>
class?: any
ui: { [K in keyof Required<ContextMenu['slots']>]: (props?: Record<string, any>) => string }
uiOverride?: ContextMenu['slots']
}
interface ContextMenuContentEmits extends RekaContextMenuContentEmits {}
</script>
<script setup lang="ts" generic="T extends ArrayOrNested<ContextMenuItem>">
const props = defineProps<ContextMenuContentProps<T>>()
const emits = defineEmits<ContextMenuContentEmits>()
const slots = defineSlots<ContextMenuSlots<T>>()
const portalProps = usePortal(toRef(() => props.portal))
const contentProps = useForwardPropsEmits(reactiveOmit(props, 'sub', 'items', 'portal', 'labelKey', 'class', 'ui', 'uiOverride'), emits)
const proxySlots = omit(slots, ['default'])
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: ContextMenuItem, active?: boolean, index: number }>()
const groups = computed<ContextMenuItem[][]>(() =>
props.items?.length
? isArrayOfArray(props.items)
? props.items
: [props.items]
: [],
)
</script>
<template>
<DefineItemTemplate v-slot="{ item, active, index }">
<slot :name="((item.slot || 'item') as keyof ContextMenuSlots<T>)" :item="item" :index="index">
<slot :name="((item.slot ? `${item.slot}-leading` : 'item-leading') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
<Icon v-if="item.loading" :name="loadingIcon" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, loading: true })" />
<Icon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, color: item?.color, active })" />
<Avatar v-else-if="item.avatar" :size="((props.uiOverride?.itemLeadingAvatarSize || ui.itemLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.itemLeadingAvatar({ class: uiOverride?.itemLeadingAvatar, active })" />
</slot>
<span v-if="get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label` : 'item-label') as keyof ContextMenuSlots<T>]" :class="ui.itemLabel({ class: uiOverride?.itemLabel, active })">
<slot :name="((item.slot ? `${item.slot}-label` : 'item-label') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
{{ get(item, props.labelKey as string) }}
</slot>
<Icon v-if="item.target === '_blank'" :name="externalIcon" :class="ui.itemLabelExternalIcon({ class: uiOverride?.itemLabelExternalIcon, color: item?.color, active })" />
</span>
<span :class="ui.itemTrailing({ class: uiOverride?.itemTrailing })">
<slot :name="((item.slot ? `${item.slot}-trailing` : 'item-trailing') as keyof ContextMenuSlots<T>)" :item="item" :active="active" :index="index">
<Icon v-if="item.children?.length" :name="chevronRightIcon" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, color: item?.color, active })" />
<span v-else-if="item.kbds?.length" :class="ui.itemTrailingKbds({ class: uiOverride?.itemTrailingKbds })">
<Kbd v-for="(kbd, kbdIndex) in item.kbds" :key="kbdIndex" :size="((props.uiOverride?.itemTrailingKbdsSize || ui.itemTrailingKbdsSize()) as KbdProps['size'])" v-bind="typeof kbd === 'string' ? { value: kbd } : kbd" />
</span>
</slot>
<RekaContextMenu.ItemIndicator as-child>
<Icon :name="checkIcon" :class="ui.itemTrailingIcon({ class: uiOverride?.itemTrailingIcon, color: item?.color })" />
</RekaContextMenu.ItemIndicator>
</span>
</slot>
</DefineItemTemplate>
<RekaContextMenu.Portal v-bind="portalProps">
<component :is="sub ? RekaContextMenu.SubContent : RekaContextMenu.Content" :class="props.class" v-bind="contentProps">
<slot name="content-top" />
<RekaContextMenu.Group v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: uiOverride?.group })">
<template v-for="(item, index) in group" :key="`group-${groupIndex}-${index}`">
<RekaContextMenu.Label v-if="item.type === 'label'" :class="ui.label({ class: uiOverride?.label })">
<ReuseItemTemplate :item="item" :index="index" />
</RekaContextMenu.Label>
<RekaContextMenu.Separator v-else-if="item.type === 'separator'" :class="ui.separator({ class: uiOverride?.separator })" />
<RekaContextMenu.Sub v-else-if="item?.children?.length" :open="item.open" :default-open="item.defaultOpen">
<RekaContextMenu.SubTrigger
as="button"
type="button"
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
:class="ui.item({ class: uiOverride?.item, color: item?.color })"
>
<ReuseItemTemplate :item="item" :index="index" />
</RekaContextMenu.SubTrigger>
<ContextMenuContent
sub
:class="props.class"
:ui="ui"
:ui-override="uiOverride"
:portal="portal"
:items="(item.children as T)"
:align-offset="-4"
:label-key="labelKey"
v-bind="item.content"
>
<template v-for="(_, name) in proxySlots" #[name]="slotData">
<slot :name="(name as keyof ContextMenuSlots<T>)" v-bind="slotData" />
</template>
</ContextMenuContent>
</RekaContextMenu.Sub>
<RekaContextMenu.CheckboxItem
v-else-if="item.type === 'checkbox'"
:model-value="item.checked"
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
:class="ui.item({ class: [uiOverride?.item, item.class], color: item?.color })"
@update:model-value="item.onUpdateChecked"
@select="item.onSelect"
>
<ReuseItemTemplate :item="item" :index="index" />
</RekaContextMenu.CheckboxItem>
<RekaContextMenu.Item
v-else
as-child
:disabled="item.disabled"
:text-value="get(item, props.labelKey as string)"
@select="item.onSelect"
>
<Link v-bind="pickLinkProps(item as Omit<ContextMenuItem, 'type'>)" :class="ui.item({ class: [uiOverride?.item, item.class], active: item.active, color: item?.color })" raw>
<ReuseItemTemplate :item="item" :active="item.active" :index="index" />
</Link>
</RekaContextMenu.Item>
</template>
</RekaContextMenu.Group>
<slot />
<slot name="content-bottom" />
</component>
</RekaContextMenu.Portal>
</template>Theme
ts
// Use 'context-menu.ts'
export default {}View Nuxt UI theme
ts
// Use 'context-menu.ts'
export default {}Test
To test this component, you can use the following test file:
ts
import { expect, it } from 'vitest'
// No tests for this component
it('passes', () => {
expect(true).toBe(true)
})ts
import { expect, it } from 'vitest'
// No tests for this component
it('passes', () => {
expect(true).toBe(true)
})