feat: context menu

This commit is contained in:
zernonia 2023-08-30 23:30:12 +08:00
parent b34e1dbc62
commit 9d43d0b6e7
14 changed files with 221 additions and 70 deletions

View File

@ -0,0 +1,66 @@
---
title: Context Menu
description: Displays a menu to the user — such as a set of actions or functions — triggered by a button.
source: https://github.com/radix-vue/shadcn-vue/tree/main/apps/www/src/lib/registry/default/ui/context-menu
primitive: https://www.radix-vue.com/components/context-menu.html
---
<ComponentPreview name="ContextMenuDemo" >
<<< ../../../lib/registry/default/examples/ContextMenuDemo.vue
</ComponentPreview>
## Installation
```bash
npx shadcn-vue@latest add context-menu
```
<ManualInstall>
1. Install `radix-vue`:
```bash
npm install radix-vue
```
2. Copy and paste the component source files linked at the top of this page into your project.
</ManualInstall>
## Usage
```vue
<script setup lang="ts">
import {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuItem,
ContextMenuLabel,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from '@/lib/registry/default/ui/context-menu'
</script>
<template>
<ContextMenu>
<ContextMenuTrigger>Right click</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>Profile</ContextMenuItem>
<ContextMenuItem>Billing</ContextMenuItem>
<ContextMenuItem>Team</ContextMenuItem>
<ContextMenuItem>Subscription</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</template>
```

View File

@ -0,0 +1,73 @@
<script setup lang="ts">
import {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuItem,
ContextMenuLabel,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from '@/lib/registry/default/ui/context-menu'
</script>
<template>
<ContextMenu>
<ContextMenuTrigger class="flex h-[150px] w-[300px] items-center justify-center rounded-md border border-dashed text-sm">
Right click here
</ContextMenuTrigger>
<ContextMenuContent class="w-64">
<ContextMenuItem inset>
Back
<ContextMenuShortcut>[</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem inset disabled>
Forward
<ContextMenuShortcut>]</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem inset>
Reload
<ContextMenuShortcut>R</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuSub>
<ContextMenuSubTrigger inset>
More Tools
</ContextMenuSubTrigger>
<ContextMenuSubContent class="w-48">
<ContextMenuItem>
Save Page As...
<ContextMenuShortcut>S</ContextMenuShortcut>
</ContextMenuItem>
<ContextMenuItem>Create Shortcut...</ContextMenuItem>
<ContextMenuItem>Name Window...</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem>Developer Tools</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSeparator />
<ContextMenuCheckboxItem checked>
Show Bookmarks Bar
<ContextMenuShortcut>B</ContextMenuShortcut>
</ContextMenuCheckboxItem>
<ContextMenuCheckboxItem>Show Full URLs</ContextMenuCheckboxItem>
<ContextMenuSeparator />
<ContextMenuRadioGroup value="pedro">
<ContextMenuLabel inset>
People
</ContextMenuLabel>
<ContextMenuSeparator />
<ContextMenuRadioItem value="pedro">
Pedro Duarte
</ContextMenuRadioItem>
<ContextMenuRadioItem value="colm">
Colm Tuite
</ContextMenuRadioItem>
</ContextMenuRadioGroup>
</ContextMenuContent>
</ContextMenu>
</template>

View File

@ -5,12 +5,10 @@ import { useEmitAsProps } from '@/lib/utils'
const props = defineProps<CollapsibleRootProps>() const props = defineProps<CollapsibleRootProps>()
const emits = defineEmits<CollapsibleRootEmits>() const emits = defineEmits<CollapsibleRootEmits>()
const emitsAsProps = useEmitAsProps(emits)
</script> </script>
<template> <template>
<CollapsibleRoot v-slot="{ open }" v-bind="{ ...props, ...emitsAsProps }"> <CollapsibleRoot v-slot="{ open }" v-bind="{ ...props, ...useEmitAsProps(emits) }">
<slot :open="open" /> <slot :open="open" />
</CollapsibleRoot> </CollapsibleRoot>
</template> </template>

View File

@ -1,11 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { ContextMenuRoot, type ContextMenuRootProps } from 'radix-vue' import { ContextMenuRoot } from 'radix-vue'
import type { ContextMenuRootEmits, ContextMenuRootProps } from 'radix-vue'
import { useEmitAsProps } from '@/lib/utils'
const props = defineProps<ContextMenuRootProps>() const props = defineProps<ContextMenuRootProps>()
const emits = defineEmits<ContextMenuRootEmits>()
</script> </script>
<template> <template>
<ContextMenuRoot v-bind="props"> <ContextMenuRoot v-bind="{ ...props, ...useEmitAsProps(emits) }">
<slot /> <slot />
</ContextMenuRoot> </ContextMenuRoot>
</template> </template>

View File

@ -5,30 +5,30 @@ import {
type ContextMenuCheckboxItemProps, type ContextMenuCheckboxItemProps,
ContextMenuItemIndicator, ContextMenuItemIndicator,
} from 'radix-vue' } from 'radix-vue'
import { cn } from '@/lib/utils' import { Check } from 'lucide-vue-next'
import RadixIconsCheck from '~icons/radix-icons/check' import { cn, useEmitAsProps } from '@/lib/utils'
const props = defineProps<ContextMenuCheckboxItemProps & { class?: string }>() const props = defineProps<ContextMenuCheckboxItemProps & { class?: string }>()
const emits = defineEmits<ContextMenuCheckboxItemEmits>() const emits = defineEmits<ContextMenuCheckboxItemEmits>()
</script> </script>
<template> <template>
<ContextMenuCheckboxItem <ContextMenuCheckboxItem
v-bind="props" v-bind="{ ...props, ...useEmitAsProps(emits) }"
:class="[ :class="[
cn( cn(
'flex relative items-center rounded-md transition-colors data-[disabled]:opacity-50 data-[disabled]:pointer-events-none data-[highlighted]:bg-outline-hover pl-7 py-1.5 text-sm outline-none select-none cursor-default', 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class, props.class,
), ),
]" ]"
@update:checked="emits('update:checked', $event)"
> >
<ContextMenuItemIndicator <span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
class="absolute left-1.5 inline-flex w-4 h-4 items-center justify-center" <ContextMenuItemIndicator
> class="absolute left-1.5 inline-flex w-4 h-4 items-center justify-center"
<RadixIconsCheck /> >
</ContextMenuItemIndicator> <Check class="h-4 h-w" />
</ContextMenuItemIndicator>
</span>
<slot /> <slot />
</ContextMenuCheckboxItem> </ContextMenuCheckboxItem>
</template> </template>

View File

@ -5,25 +5,23 @@ import {
type ContextMenuContentProps, type ContextMenuContentProps,
ContextMenuPortal, ContextMenuPortal,
} from 'radix-vue' } from 'radix-vue'
import { cn } from '@/lib/utils' import { cn, useEmitAsProps } from '@/lib/utils'
const props = defineProps<ContextMenuContentProps & { class?: string }>() const props = defineProps<ContextMenuContentProps & { class?: string }>()
const emits = defineEmits<ContextMenuContentEmits>() const emits = defineEmits<ContextMenuContentEmits>()
</script> </script>
<template> <template>
<ContextMenuPortal force-mount> <ContextMenuPortal>
<ContextMenuContent <ContextMenuContent
:loop="props.loop"
:align-offset="props.alignOffset" :align-offset="props.alignOffset"
:as-child="props.asChild"
:class="[ :class="[
cn( cn(
'bg-background focus:outline-none outline-none border border-border p-1 z-50 min-w-[8rem] rounded-md shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class, props.class,
), ),
]" ]"
v-bind="{ ...props, ...useEmitAsProps(emits) }"
> >
<slot /> <slot />
</ContextMenuContent> </ContextMenuContent>

View File

@ -4,23 +4,22 @@ import {
type ContextMenuItemEmits, type ContextMenuItemEmits,
type ContextMenuItemProps, type ContextMenuItemProps,
} from 'radix-vue' } from 'radix-vue'
import { cn } from '@/lib/utils' import { cn, useEmitAsProps } from '@/lib/utils'
const props = defineProps<ContextMenuItemProps & { class?: string }>()
const props = defineProps<ContextMenuItemProps & { class?: string; inset?: boolean }>()
const emits = defineEmits<ContextMenuItemEmits>() const emits = defineEmits<ContextMenuItemEmits>()
</script> </script>
<template> <template>
<ContextMenuItem <ContextMenuItem
v-bind="props" v-bind="{ ...props, ...useEmitAsProps(emits) }"
:class="[ :class="[
cn( cn(
'flex items-center rounded-md transition-colors data-[disabled]:opacity-50 data-[disabled]:pointer-events-none focus:bg-outline-hover px-2 py-1.5 text-sm outline-none select-none cursor-default', 'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
props.class, props.class,
), ),
]" ]"
@select="emits('select', $event)"
> >
<slot /> <slot />
</ContextMenuItem> </ContextMenuItem>

View File

@ -1,13 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { ContextMenuLabel, type ContextMenuLabelProps } from 'radix-vue' import { ContextMenuLabel, type ContextMenuLabelProps } from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<ContextMenuLabelProps>() const props = defineProps<ContextMenuLabelProps & { class?: string; inset?: boolean }>()
</script> </script>
<template> <template>
<div class="px-2 py-1.5 text-sm font-semibold"> <ContextMenuLabel
<ContextMenuLabel v-bind="props"> v-bind="props"
<slot /> :class="
</ContextMenuLabel> cn('px-2 py-1.5 text-sm font-semibold text-foreground',
</div> inset && 'pl-8', props.class ?? '',
)"
>
<slot />
</ContextMenuLabel>
</template> </template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { ContextMenuPortal, type ContextMenuPortalProps } from 'radix-vue'
const props = defineProps<ContextMenuPortalProps>()
</script>
<template>
<ContextMenuPortal v-bind="props">
<slot />
</ContextMenuPortal>
</template>

View File

@ -5,30 +5,28 @@ import {
type ContextMenuRadioItemEmits, type ContextMenuRadioItemEmits,
type ContextMenuRadioItemProps, type ContextMenuRadioItemProps,
} from 'radix-vue' } from 'radix-vue'
import { cn } from '@/lib/utils' import { Circle } from 'lucide-vue-next'
import RiCheckboxBlankCircleFill from '~icons/ri/checkbox-blank-circle-fill' import { cn, useEmitAsProps } from '@/lib/utils'
const props = defineProps<ContextMenuRadioItemProps & { class?: string }>() const props = defineProps<ContextMenuRadioItemProps & { class?: string }>()
const emits = defineEmits<ContextMenuRadioItemEmits>() const emits = defineEmits<ContextMenuRadioItemEmits>()
</script> </script>
<template> <template>
<ContextMenuRadioItem <ContextMenuRadioItem
v-bind="props" v-bind="{ ...props, ...useEmitAsProps(emits) }"
:class="[ :class="[
cn( cn(
'flex relative items-center rounded-md transition-colors data-[disabled]:opacity-50 data-[disabled]:pointer-events-none data-[highlighted]:bg-outline-hover pl-7 py-1.5 text-sm outline-none select-none cursor-default', 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class, props.class,
), ),
]" ]"
@select="emits('select', $event)"
> >
<ContextMenuItemIndicator <span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
class="absolute left-2 inline-flex w-2 h-2 items-center justify-center" <ContextMenuItemIndicator>
> <Circle class="h-2 w-2 fill-current" />
<RiCheckboxBlankCircleFill class="text-foreground" /> </ContextMenuItemIndicator>
</ContextMenuItemIndicator> </span>
<slot /> <slot />
</ContextMenuRadioItem> </ContextMenuRadioItem>
</template> </template>

View File

@ -3,10 +3,11 @@ import {
ContextMenuSeparator, ContextMenuSeparator,
type ContextMenuSeparatorProps, type ContextMenuSeparatorProps,
} from 'radix-vue' } from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<ContextMenuSeparatorProps>() const props = defineProps<ContextMenuSeparatorProps>()
</script> </script>
<template> <template>
<ContextMenuSeparator v-bind="props" class="-mx-1 my-1 h-px bg-secondary" /> <ContextMenuSeparator v-bind="props" :class="cn('-mx-1 my-1 h-px bg-border', $attrs.class ?? '')" />
</template> </template>

View File

@ -1,5 +1,9 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
</script>
<template> <template>
<div class="text-xxs ml-auto tracking-widest opacity-50"> <span :class="cn('ml-auto text-xs tracking-widest text-muted-foreground', $attrs.class ?? '')">
<slot /> <slot />
</div> </span>
</template> </template>

View File

@ -1,33 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
ContextMenuPortal,
ContextMenuSubContent, ContextMenuSubContent,
type DropdownMenuSubContentEmits, type DropdownMenuSubContentEmits,
type DropdownMenuSubContentProps, type DropdownMenuSubContentProps,
} from 'radix-vue' } from 'radix-vue'
import { cn } from '@/lib/utils' import { cn, useEmitAsProps } from '@/lib/utils'
const props = defineProps<DropdownMenuSubContentProps & { class?: string }>() const props = defineProps<DropdownMenuSubContentProps & { class?: string }>()
const emits = defineEmits<DropdownMenuSubContentEmits>() const emits = defineEmits<DropdownMenuSubContentEmits>()
</script> </script>
<template> <template>
<ContextMenuPortal force-mount> <ContextMenuSubContent
<ContextMenuSubContent v-bind="{ ...props, ...useEmitAsProps(emits) }"
:loop="props.loop" :class="
:align-offset="props.alignOffset" cn(
:side="props.side" 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
:side-offset="props.sideOffset" props.class,
:as-child="props.asChild" )
:class=" "
cn( >
'bg-background focus:outline-none outline-none text-foreground border border-border p-1 z-50 min-w-[10rem] rounded-md shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', <slot />
props.class, </ContextMenuSubContent>
)
"
>
<slot />
</ContextMenuSubContent>
</ContextMenuPortal>
</template> </template>

View File

@ -3,9 +3,10 @@ import {
ContextMenuSubTrigger, ContextMenuSubTrigger,
type ContextMenuSubTriggerProps, type ContextMenuSubTriggerProps,
} from 'radix-vue' } from 'radix-vue'
import { ChevronRight } from 'lucide-vue-next'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
const props = defineProps<ContextMenuSubTriggerProps & { class?: string }>() const props = defineProps<ContextMenuSubTriggerProps & { class?: string; inset?: boolean }>()
</script> </script>
<template> <template>
@ -13,11 +14,13 @@ const props = defineProps<ContextMenuSubTriggerProps & { class?: string }>()
v-bind="props" v-bind="props"
:class="[ :class="[
cn( cn(
'flex items-center rounded-md transition-colors data-[disabled]:opacity-50 data-[disabled]:pointer-events-none data-[highlighted]:bg-outline-hover px-2 py-1.5 text-sm outline-none select-none cursor-default', 'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
inset && 'pl-8',
props.class, props.class,
), ),
]" ]"
> >
<slot /> <slot />
<ChevronRight class="ml-auto h-4 w-4" />
</ContextMenuSubTrigger> </ContextMenuSubTrigger>
</template> </template>