feat: dropdown components

This commit is contained in:
zernonia 2023-09-04 22:52:41 +08:00
parent 77b96ccbd7
commit e168f23aea
13 changed files with 247 additions and 64 deletions

View File

@ -0,0 +1,56 @@
---
title: Dropdown 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/dropdown-menu
primitive: https://www.radix-vue.com/components/dropdown-menu.html
---
<ComponentPreview name="DropdownMenuDemo" />
## Installation
```bash
npx shadcn-vue@latest add dropdown-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 {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/lib/registry/default/ui/dropdown-menu'
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger>Open</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Billing</DropdownMenuItem>
<DropdownMenuItem>Team</DropdownMenuItem>
<DropdownMenuItem>Subscription</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>
```

View File

@ -0,0 +1,124 @@
<script setup lang="ts">
import {
Cloud,
CreditCard,
Github,
Keyboard,
LifeBuoy,
LogOut,
Mail,
MessageSquare,
Plus,
PlusCircle,
Settings,
User,
UserPlus,
Users,
} from 'lucide-vue-next'
import { Button } from '@/lib/registry/default/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/lib/registry/default/ui/dropdown-menu'
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline">
Open
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent class="w-56">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<User class="mr-2 h-4 w-4" />
<span>Profile</span>
<DropdownMenuShortcut>P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCard class="mr-2 h-4 w-4" />
<span>Billing</span>
<DropdownMenuShortcut>B</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<Settings class="mr-2 h-4 w-4" />
<span>Settings</span>
<DropdownMenuShortcut>S</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<Keyboard class="mr-2 h-4 w-4" />
<span>Keyboard shortcuts</span>
<DropdownMenuShortcut>K</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Users class="mr-2 h-4 w-4" />
<span>Team</span>
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<UserPlus class="mr-2 h-4 w-4" />
<span>Invite users</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem>
<Mail class="mr-2 h-4 w-4" />
<span>Email</span>
</DropdownMenuItem>
<DropdownMenuItem>
<MessageSquare class="mr-2 h-4 w-4" />
<span>Message</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<PlusCircle class="mr-2 h-4 w-4" />
<span>More...</span>
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuItem>
<Plus class="mr-2 h-4 w-4" />
<span>New Team</span>
<DropdownMenuShortcut>+T</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Github class="mr-2 h-4 w-4" />
<span>GitHub</span>
</DropdownMenuItem>
<DropdownMenuItem>
<LifeBuoy class="mr-2 h-4 w-4" />
<span>Support</span>
</DropdownMenuItem>
<DropdownMenuItem disabled>
<Cloud class="mr-2 h-4 w-4" />
<span>API</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut class="mr-2 h-4 w-4" />
<span>Log out</span>
<DropdownMenuShortcut>Q</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>

View File

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

View File

@ -5,31 +5,26 @@ import {
type DropdownMenuCheckboxItemProps, type DropdownMenuCheckboxItemProps,
DropdownMenuItemIndicator, DropdownMenuItemIndicator,
} 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<DropdownMenuCheckboxItemProps & { class?: string }>() const props = defineProps<DropdownMenuCheckboxItemProps & { class?: string }>()
const emits = defineEmits<DropdownMenuCheckboxItemEmits>() const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
</script> </script>
<template> <template>
<DropdownMenuCheckboxItem <DropdownMenuCheckboxItem
v-bind="props" v-bind="{ ...props, ...useEmitAsProps(emits) }"
:class="[ :class=" cn(
cn( 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'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', props.class,
props.class, )"
),
]"
@update:checked="emits('update:checked', $event)"
@select="emits('select', $event)"
> >
<DropdownMenuItemIndicator <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" <DropdownMenuItemIndicator>
> <Check class="w-4 h-4" />
<RadixIconsCheck /> </DropdownMenuItemIndicator>
</DropdownMenuItemIndicator> </span>
<slot /> <slot />
</DropdownMenuCheckboxItem> </DropdownMenuCheckboxItem>
</template> </template>

View File

@ -9,13 +9,13 @@ import { cn } from '@/lib/utils'
const props = withDefaults( const props = withDefaults(
defineProps<DropdownMenuContentProps & { class?: string }>(), defineProps<DropdownMenuContentProps & { class?: string }>(),
{ {
sideOffset: 6, sideOffset: 4,
}, },
) )
</script> </script>
<template> <template>
<DropdownMenuPortal force-mount> <DropdownMenuPortal>
<DropdownMenuContent <DropdownMenuContent
:loop="props.loop" :loop="props.loop"
:as-child="props.asChild" :as-child="props.asChild"
@ -25,7 +25,7 @@ const props = withDefaults(
:align-offset="props.alignOffset" :align-offset="props.alignOffset"
: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 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,
), ),
]" ]"

View File

@ -2,7 +2,7 @@
import { DropdownMenuItem, type DropdownMenuItemProps } from 'radix-vue' import { DropdownMenuItem, type DropdownMenuItemProps } from 'radix-vue'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
const props = defineProps<DropdownMenuItemProps & { class?: string }>() const props = defineProps<DropdownMenuItemProps & { inset?: boolean; class?: string }>()
</script> </script>
<template> <template>
@ -10,7 +10,8 @@ const props = defineProps<DropdownMenuItemProps & { 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 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 transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
props.class, props.class,
), ),
]" ]"

View File

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

View File

@ -5,8 +5,8 @@ import {
type DropdownMenuRadioItemEmits, type DropdownMenuRadioItemEmits,
type DropdownMenuRadioItemProps, type DropdownMenuRadioItemProps,
} 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<DropdownMenuRadioItemProps & { class?: string }>() const props = defineProps<DropdownMenuRadioItemProps & { class?: string }>()
@ -15,20 +15,18 @@ const emits = defineEmits<DropdownMenuRadioItemEmits>()
<template> <template>
<DropdownMenuRadioItem <DropdownMenuRadioItem
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',
'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', props.class,
props.class, )"
),
]"
@select="emits('select', $event)"
> >
<DropdownMenuItemIndicator <span className="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"
> <DropdownMenuItemIndicator>
<RiCheckboxBlankCircleFill class="text-foreground" /> <Circle class="h-2 w-2 fill-current" />
</DropdownMenuItemIndicator> </DropdownMenuItemIndicator>
</span>
<slot /> <slot />
</DropdownMenuRadioItem> </DropdownMenuRadioItem>
</template> </template>

View File

@ -8,5 +8,5 @@ const props = defineProps<DropdownMenuSeparatorProps>()
</script> </script>
<template> <template>
<DropdownMenuSeparator v-bind="props" class="-mx-1 my-1 h-px bg-secondary" /> <DropdownMenuSeparator v-bind="props" class="-mx-1 my-1 h-px bg-muted" />
</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 opacity-60', $attrs.class ?? '')">
<slot /> <slot />
</div> </span>
</template> </template>

View File

@ -1,28 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
DropdownMenuPortal,
DropdownMenuSubContent, DropdownMenuSubContent,
type DropdownMenuSubContentEmits, type DropdownMenuSubContentEmits,
type DropdownMenuSubContentProps, type DropdownMenuSubContentProps,
} from 'radix-vue' } from 'radix-vue'
import { cn, useEmitAsProps } from '@/lib/utils'
const props = defineProps<DropdownMenuSubContentProps>() const props = defineProps<DropdownMenuSubContentProps>()
const emits = defineEmits<DropdownMenuSubContentEmits>() const emits = defineEmits<DropdownMenuSubContentEmits>()
</script> </script>
<template> <template>
<DropdownMenuPortal force-mount> <DropdownMenuSubContent
<DropdownMenuSubContent v-bind="{ ...props, ...useEmitAsProps(emits) }"
:loop="props.loop" :class="cn('z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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', $attrs.class ?? '')"
:as-child="props.asChild" >
:side-offset="props.sideOffset" <slot />
:side="props.side" </DropdownMenuSubContent>
:align="props.align"
:align-offset="props.alignOffset"
class="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 />
</DropdownMenuSubContent>
</DropdownMenuPortal>
</template> </template>

View File

@ -3,6 +3,7 @@ import {
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
type DropdownMenuSubTriggerProps, type DropdownMenuSubTriggerProps,
} 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<DropdownMenuSubTriggerProps & { class?: string }>() const props = defineProps<DropdownMenuSubTriggerProps & { class?: string }>()
@ -13,11 +14,12 @@ const props = defineProps<DropdownMenuSubTriggerProps & { 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 data-[state=open]:bg-accent',
props.class, props.class,
), ),
]" ]"
> >
<slot /> <slot />
<ChevronRight class="ml-auto h-4 w-4" />
</DropdownMenuSubTrigger> </DropdownMenuSubTrigger>
</template> </template>

View File

@ -1,3 +1,5 @@
export { DropdownMenuPortal } from 'radix-vue'
export { default as DropdownMenu } from './DropdownMenu.vue' export { default as DropdownMenu } from './DropdownMenu.vue'
export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue' export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue'
export { default as DropdownMenuContent } from './DropdownMenuContent.vue' export { default as DropdownMenuContent } from './DropdownMenuContent.vue'