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 emits = defineEmits<CollapsibleRootEmits>()
const emitsAsProps = useEmitAsProps(emits)
</script>
<template>
<CollapsibleRoot v-slot="{ open }" v-bind="{ ...props, ...emitsAsProps }">
<CollapsibleRoot v-slot="{ open }" v-bind="{ ...props, ...useEmitAsProps(emits) }">
<slot :open="open" />
</CollapsibleRoot>
</template>

View File

@ -1,11 +1,14 @@
<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 emits = defineEmits<ContextMenuRootEmits>()
</script>
<template>
<ContextMenuRoot v-bind="props">
<ContextMenuRoot v-bind="{ ...props, ...useEmitAsProps(emits) }">
<slot />
</ContextMenuRoot>
</template>

View File

@ -5,30 +5,30 @@ import {
type ContextMenuCheckboxItemProps,
ContextMenuItemIndicator,
} from 'radix-vue'
import { cn } from '@/lib/utils'
import RadixIconsCheck from '~icons/radix-icons/check'
import { Check } from 'lucide-vue-next'
import { cn, useEmitAsProps } from '@/lib/utils'
const props = defineProps<ContextMenuCheckboxItemProps & { class?: string }>()
const emits = defineEmits<ContextMenuCheckboxItemEmits>()
</script>
<template>
<ContextMenuCheckboxItem
v-bind="props"
v-bind="{ ...props, ...useEmitAsProps(emits) }"
:class="[
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,
),
]"
@update:checked="emits('update:checked', $event)"
>
<ContextMenuItemIndicator
class="absolute left-1.5 inline-flex w-4 h-4 items-center justify-center"
>
<RadixIconsCheck />
</ContextMenuItemIndicator>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuItemIndicator
class="absolute left-1.5 inline-flex w-4 h-4 items-center justify-center"
>
<Check class="h-4 h-w" />
</ContextMenuItemIndicator>
</span>
<slot />
</ContextMenuCheckboxItem>
</template>

View File

@ -5,25 +5,23 @@ import {
type ContextMenuContentProps,
ContextMenuPortal,
} from 'radix-vue'
import { cn } from '@/lib/utils'
import { cn, useEmitAsProps } from '@/lib/utils'
const props = defineProps<ContextMenuContentProps & { class?: string }>()
const emits = defineEmits<ContextMenuContentEmits>()
</script>
<template>
<ContextMenuPortal force-mount>
<ContextMenuPortal>
<ContextMenuContent
:loop="props.loop"
:align-offset="props.alignOffset"
:as-child="props.asChild"
:class="[
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,
),
]"
v-bind="{ ...props, ...useEmitAsProps(emits) }"
>
<slot />
</ContextMenuContent>

View File

@ -4,23 +4,22 @@ import {
type ContextMenuItemEmits,
type ContextMenuItemProps,
} from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<ContextMenuItemProps & { class?: string }>()
import { cn, useEmitAsProps } from '@/lib/utils'
const props = defineProps<ContextMenuItemProps & { class?: string; inset?: boolean }>()
const emits = defineEmits<ContextMenuItemEmits>()
</script>
<template>
<ContextMenuItem
v-bind="props"
v-bind="{ ...props, ...useEmitAsProps(emits) }"
:class="[
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,
),
]"
@select="emits('select', $event)"
>
<slot />
</ContextMenuItem>

View File

@ -1,13 +1,18 @@
<script setup lang="ts">
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>
<template>
<div class="px-2 py-1.5 text-sm font-semibold">
<ContextMenuLabel v-bind="props">
<slot />
</ContextMenuLabel>
</div>
<ContextMenuLabel
v-bind="props"
:class="
cn('px-2 py-1.5 text-sm font-semibold text-foreground',
inset && 'pl-8', props.class ?? '',
)"
>
<slot />
</ContextMenuLabel>
</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 ContextMenuRadioItemProps,
} from 'radix-vue'
import { cn } from '@/lib/utils'
import RiCheckboxBlankCircleFill from '~icons/ri/checkbox-blank-circle-fill'
import { Circle } from 'lucide-vue-next'
import { cn, useEmitAsProps } from '@/lib/utils'
const props = defineProps<ContextMenuRadioItemProps & { class?: string }>()
const emits = defineEmits<ContextMenuRadioItemEmits>()
</script>
<template>
<ContextMenuRadioItem
v-bind="props"
v-bind="{ ...props, ...useEmitAsProps(emits) }"
:class="[
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,
),
]"
@select="emits('select', $event)"
>
<ContextMenuItemIndicator
class="absolute left-2 inline-flex w-2 h-2 items-center justify-center"
>
<RiCheckboxBlankCircleFill class="text-foreground" />
</ContextMenuItemIndicator>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuItemIndicator>
<Circle class="h-2 w-2 fill-current" />
</ContextMenuItemIndicator>
</span>
<slot />
</ContextMenuRadioItem>
</template>

View File

@ -3,10 +3,11 @@ import {
ContextMenuSeparator,
type ContextMenuSeparatorProps,
} from 'radix-vue'
import { cn } from '@/lib/utils'
const props = defineProps<ContextMenuSeparatorProps>()
</script>
<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>

View File

@ -1,5 +1,9 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
</script>
<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 />
</div>
</span>
</template>

View File

@ -1,33 +1,25 @@
<script setup lang="ts">
import {
ContextMenuPortal,
ContextMenuSubContent,
type DropdownMenuSubContentEmits,
type DropdownMenuSubContentProps,
} from 'radix-vue'
import { cn } from '@/lib/utils'
import { cn, useEmitAsProps } from '@/lib/utils'
const props = defineProps<DropdownMenuSubContentProps & { class?: string }>()
const emits = defineEmits<DropdownMenuSubContentEmits>()
</script>
<template>
<ContextMenuPortal force-mount>
<ContextMenuSubContent
:loop="props.loop"
:align-offset="props.alignOffset"
:side="props.side"
:side-offset="props.sideOffset"
: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',
props.class,
)
"
>
<slot />
</ContextMenuSubContent>
</ContextMenuPortal>
<ContextMenuSubContent
v-bind="{ ...props, ...useEmitAsProps(emits) }"
:class="
cn(
'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',
props.class,
)
"
>
<slot />
</ContextMenuSubContent>
</template>

View File

@ -3,9 +3,10 @@ import {
ContextMenuSubTrigger,
type ContextMenuSubTriggerProps,
} from 'radix-vue'
import { ChevronRight } from 'lucide-vue-next'
import { cn } from '@/lib/utils'
const props = defineProps<ContextMenuSubTriggerProps & { class?: string }>()
const props = defineProps<ContextMenuSubTriggerProps & { class?: string; inset?: boolean }>()
</script>
<template>
@ -13,11 +14,13 @@ const props = defineProps<ContextMenuSubTriggerProps & { class?: string }>()
v-bind="props"
:class="[
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,
),
]"
>
<slot />
<ChevronRight class="ml-auto h-4 w-4" />
</ContextMenuSubTrigger>
</template>