Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script setup lang="ts">
import type { SelectMenuItem, BadgeProps } from '@nuxt/ui'
const items = ref([
{
label: 'bug',
value: 'bug',
badge: {
label: '4',
color: 'error'
}
},
{
label: 'feature',
value: 'feature',
badge: {
label: '2',
color: 'success'
}
},
{
label: 'enhancement',
value: 'enhancement',
badge: {
label: '1',
color: 'info'
}
}
] satisfies SelectMenuItem[])
const value = ref(items.value[0])
</script>

<template>
<USelectMenu v-model="value" :items="items" class="w-48">
<template #leading="{ modelValue, ui }">
<UBadge
v-if="modelValue"
variant="soft"
v-bind="modelValue.badge"
:size="(ui.itemBadgeSize() as BadgeProps['size'])"
:class="ui.itemBadge()"
/>
</template>
</USelectMenu>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<script setup lang="ts">
import type { SelectItem, BadgeProps } from '@nuxt/ui'

const items = ref([
{
label: 'bug',
value: 'bug',
badge: {
label: '4',
color: 'error'
}
},
{
label: 'feature',
value: 'feature',
badge: {
label: '2',
color: 'success'
}
},
{
label: 'enhancement',
value: 'enhancement',
badge: {
label: '1',
color: 'info'
}
}
] satisfies SelectItem[])

const value = ref(items.value[0]?.value)

function getBadge(value: string) {
return items.value.find(item => item.value === value)?.badge
}
</script>

<template>
<USelect v-model="value" :items="items" value-key="value" class="w-48">
<template #leading="{ modelValue, ui }">
<UBadge
v-if="modelValue"
variant="soft"
v-bind="getBadge(modelValue)"
:size="(ui.itemBadgeSize() as BadgeProps['size'])"
:class="ui.itemBadge()"
/>
</template>
</USelect>
</template>
15 changes: 15 additions & 0 deletions docs/content/docs/2.components/select-menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,21 @@ name: 'select-menu-items-avatar-example'
You can also use the `#leading` slot to display the selected avatar.
::

### With badge in items

You can use the `badge` property to display a [Badge](/docs/components/badge) inside the items.

::component-example
---
collapse: true
name: 'select-menu-items-badge-example'
---
::

::tip
You can also use the `#leading` slot to display the selected badge.
::
Comment thread
coderabbitai[bot] marked this conversation as resolved.

### With chip in items

You can use the `chip` property to display a [Chip](/docs/components/chip) inside the items.
Expand Down
15 changes: 15 additions & 0 deletions docs/content/docs/2.components/select.md
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,21 @@ In this example, the avatar is computed from the `value` property of the selecte
You can also use the `#leading` slot to display the selected avatar.
::

### With badge in items

You can use the `badge` property to display a [Badge](/docs/components/badge) inside the items.

::component-example
---
collapse: true
name: 'select-items-badge-example'
---
::

Comment thread
coderabbitai[bot] marked this conversation as resolved.
::tip
You can also use the `#leading` slot to display the selected badge.
::

### With chip in items

You can use the `chip` property to display a [Chip](/docs/components/chip) inside the items.
Expand Down
12 changes: 12 additions & 0 deletions playgrounds/nuxt/app/pages/components/select-menu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const fruits = ['Apple', 'Banana', 'Blueberry', 'Grapes', 'Pineapple']
const vegetables = ['Aubergine', 'Broccoli', 'Carrot', 'Courgette', 'Leek']

const items = [[{ label: 'Fruits', type: 'label' }, ...fruits], [{ label: 'Vegetables', type: 'label' }, ...vegetables]] satisfies SelectMenuItem[][]
const itemsBadges = [
[{ label: 'Fruits', type: 'label' }, ...fruits.map(f => ({ label: f, badge: Math.floor(Math.random() * 10) + 1 }))],
[{ label: 'Vegetables', type: 'label' }, ...vegetables.map(f => ({ label: f, badge: Math.floor(Math.random() * 10) + 1 }))]
] satisfies SelectMenuItem[][]

const statuses = [{
label: 'Backlog',
Expand Down Expand Up @@ -80,6 +84,14 @@ const valueMultiple = ref([fruits[0]!, vegetables[0]!])
v-bind="props"
clear
/>
<USelectMenu
multiple
value-key="label"
placeholder="Multiple with badges"
:items="itemsBadges"
v-bind="props"
clear
/>
<USelectMenu placeholder="Highlight" highlight :items="items" v-bind="props" />
<USelectMenu placeholder="Disabled" disabled :items="items" v-bind="props" />
<USelectMenu placeholder="Required" required :items="items" v-bind="props" />
Expand Down
7 changes: 6 additions & 1 deletion playgrounds/nuxt/app/pages/components/select.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ const attrs = reactive({
const fruits = ['Apple', 'Banana', 'Blueberry', 'Grapes', 'Pineapple']
const vegetables = ['Aubergine', 'Broccoli', 'Carrot', 'Courgette', 'Leek']

const items = [[{ label: 'Fruits', type: 'label' as const }, ...fruits], [{ label: 'Vegetables', type: 'label' as const }, ...vegetables]]
const items = [[{ label: 'Fruits', type: 'label' as const }, ...fruits], [{ label: 'Vegetables', type: 'label' as const }, ...vegetables]] satisfies SelectItem[][]
const itemsBadges = [
[{ label: 'Fruits', type: 'label' }, ...fruits.map(f => ({ label: f, badge: Math.floor(Math.random() * 10) + 1 }))],
[{ label: 'Vegetables', type: 'label' }, ...vegetables.map(f => ({ label: f, badge: Math.floor(Math.random() * 10) + 1 }))]
] satisfies SelectItem[][]

const statuses = [{
label: 'Backlog',
Expand Down Expand Up @@ -76,6 +80,7 @@ const valueMultiple = ref([fruits[0]!, vegetables[0]!])
<USelect :default-value="value" :items="items" v-bind="props" />
<USelect v-model="valueMultiple" multiple placeholder="Multiple" :items="items" v-bind="props" />
<USelect :default-value="valueMultiple" multiple placeholder="Multiple" :items="items" v-bind="props" />
<USelect multiple placeholder="Multiple with badges" :items="itemsBadges" v-bind="props" />
<USelect placeholder="Highlight" highlight :items="items" v-bind="props" />
<USelect placeholder="Disabled" disabled :items="items" v-bind="props" />
<USelect placeholder="Required" required :items="items" v-bind="props" />
Expand Down
18 changes: 16 additions & 2 deletions src/runtime/components/Select.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { VNode } from 'vue'
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/select'
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
import type { AvatarProps, ChipProps, IconProps, InputProps } from '../types'
import type { AvatarProps, BadgeProps, ChipProps, IconProps, InputProps } from '../types'
import type { ModelModifiers, ApplyModifiers } from '../types/input'
import type { ButtonHTMLAttributes } from '../types/html'
import type { AcceptableValue, ArrayOrNested, GetItemKeys, GetModelValue, NestedItem, EmitsToProps } from '../types/utils'
Expand All @@ -23,6 +23,11 @@ export type SelectItem = SelectValue | {
icon?: IconProps['name']
avatar?: AvatarProps
chip?: ChipProps
/**
* Display a badge on the item.
* `{ color: 'neutral', variant: 'soft', size: 'sm' }`{lang="ts-type"}
*/
badge?: string | number | BadgeProps
/**
* The item type.
* @defaultValue 'item'
Expand All @@ -32,7 +37,7 @@ export type SelectItem = SelectValue | {
disabled?: boolean
onSelect?: (e: Event) => void
class?: any
ui?: Pick<Select['slots'], 'label' | 'separator' | 'item' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLeadingChipSize' | 'itemLeadingChip' | 'itemWrapper' | 'itemLabel' | 'itemDescription' | 'itemTrailing' | 'itemTrailingIcon'>
ui?: Pick<Select['slots'], 'label' | 'separator' | 'item' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLeadingChipSize' | 'itemLeadingChip' | 'itemWrapper' | 'itemLabel' | 'itemDescription' | 'itemTrailing' | 'itemTrailingIcon' | 'itemBadge' | 'itemBadgeSize'>
[key: string]: any
}

Expand Down Expand Up @@ -380,6 +385,15 @@ defineExpose({
<slot name="item-label" :item="(item as NestedItem<T>)" :index="index">
{{ isSelectItem(item) ? get(item, props.labelKey as string) : item }}
</slot>
<UBadge
v-if="isSelectItem(item) && item.badge !== undefined && item.badge !== null"
color="neutral"
variant="outline"
:size="((item.ui?.itemBadgeSize || uiProp?.itemBadgeSize || ui.itemBadgeSize()) as BadgeProps['size'])"
v-bind="(typeof item.badge === 'string' || typeof item.badge === 'number') ? { label: item.badge } : item.badge"
data-slot="itemBadge"
:class="ui.itemBadge({ class: [uiProp?.itemBadge, item.ui?.itemBadge] })"
/>
</SelectItemText>

<span v-if="isSelectItem(item) && (get(item, props.descriptionKey as string) || !!slots['item-description'])" data-slot="itemDescription" :class="ui.itemDescription({ class: [uiProp?.itemDescription, isSelectItem(item) && item.ui?.itemDescription] })">
Expand Down
18 changes: 16 additions & 2 deletions src/runtime/components/SelectMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { VNode } from 'vue'
import type { AppConfig } from '@nuxt/schema'
import theme from '#build/ui/select-menu'
import type { UseComponentIconsProps } from '../composables/useComponentIcons'
import type { AvatarProps, ButtonProps, ChipProps, IconProps, InputProps, LinkPropsKeys } from '../types'
import type { AvatarProps, ButtonProps, ChipProps, IconProps, InputProps, LinkPropsKeys, BadgeProps } from '../types'
Comment thread
zAlweNy26 marked this conversation as resolved.
import type { ModelModifiers, ApplyModifiers } from '../types/input'
import type { ButtonHTMLAttributes } from '../types/html'
import type { AcceptableValue, ArrayOrNested, GetItemKeys, GetItemValue, GetModelValue, NestedItem, EmitsToProps } from '../types/utils'
Expand All @@ -23,6 +23,11 @@ export type SelectMenuItem = SelectMenuValue | {
icon?: IconProps['name']
avatar?: AvatarProps
chip?: ChipProps
/**
* Display a badge on the item.
* `{ color: 'neutral', variant: 'soft', size: 'sm' }`{lang="ts-type"}
*/
badge?: string | number | BadgeProps
/**
* The item type.
* @defaultValue 'item'
Expand All @@ -31,7 +36,7 @@ export type SelectMenuItem = SelectMenuValue | {
disabled?: boolean
onSelect?: (e: Event) => void
class?: any
ui?: Pick<SelectMenu['slots'], 'label' | 'separator' | 'item' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLeadingChipSize' | 'itemLeadingChip' | 'itemWrapper' | 'itemLabel' | 'itemDescription' | 'itemTrailing' | 'itemTrailingIcon'>
ui?: Pick<SelectMenu['slots'], 'label' | 'separator' | 'item' | 'itemLeadingIcon' | 'itemLeadingAvatarSize' | 'itemLeadingAvatar' | 'itemLeadingChipSize' | 'itemLeadingChip' | 'itemWrapper' | 'itemLabel' | 'itemDescription' | 'itemTrailing' | 'itemTrailingIcon' | 'itemBadgeSize' | 'itemBadge'>
[key: string]: any
}

Expand Down Expand Up @@ -543,6 +548,15 @@ defineExpose({
<slot name="item-label" :item="(item as NestedItem<T>)" :index="index">
{{ isSelectItem(item) ? get(item, props.labelKey as string) : item }}
</slot>
<UBadge
v-if="isSelectItem(item) && item.badge !== undefined && item.badge !== null"
color="neutral"
variant="soft"
:size="((item.ui?.itemBadgeSize || uiProp?.itemBadgeSize || ui.itemBadgeSize()) as BadgeProps['size'])"
v-bind="(typeof item.badge === 'string' || typeof item.badge === 'number') ? { label: item.badge } : item.badge"
data-slot="itemLeadingBadge"
:class="ui.itemBadge({ class: [uiProp?.itemBadge, item.ui?.itemBadge] })"
/>
</span>

<span v-if="isSelectItem(item) && (get(item, props.descriptionKey as string) || !!slots['item-description'])" data-slot="itemDescription" :class="ui.itemDescription({ class: [uiProp?.itemDescription, isSelectItem(item) && item.ui?.itemDescription] })">
Expand Down
6 changes: 4 additions & 2 deletions src/theme/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ export default (options: Required<ModuleOptions>) => {
itemTrailing: 'ms-auto inline-flex gap-1.5 items-center',
itemTrailingIcon: 'shrink-0',
itemWrapper: 'flex-1 flex flex-col min-w-0',
itemLabel: 'truncate',
itemDescription: 'truncate text-muted'
itemLabel: 'truncate flex items-center gap-1',
itemDescription: 'truncate text-muted',
itemBadge: 'shrink-0',
itemBadgeSize: 'sm'
},
variants: {
...fieldGroupVariant,
Expand Down
8 changes: 8 additions & 0 deletions test/components/Select.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,20 @@ describe('Select', () => {

const itemsWithDescription = [...items.map(item => ({ ...item, description: 'Description' }))]

const itemsWithBadge = [
...items.map((item, i) => ({
...item,
badge: i % 2 === 0 ? 'Badge' : { color: 'primary', variant: 'solid', size: 'sm', label: 'Badge' }
}))
]

const props = { open: true, portal: false, items }

renderEach(Select, [
// Props
['with items', { props }],
['with items with description', { props: { ...props, items: itemsWithDescription } }],
['with items with badge', { props: { ...props, items: itemsWithBadge } }],
['with modelValue', { props: { ...props, modelValue: items[0]?.value } }],
['with defaultValue', { props: { ...props, defaultValue: items[0]?.value } }],
['with valueKey', { props: { ...props, valueKey: 'label', defaultValue: 'Backlog' } }],
Expand Down
8 changes: 8 additions & 0 deletions test/components/SelectMenu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,20 @@ describe('SelectMenu', () => {

const itemsWithDescription = [...items.map(item => ({ ...item, description: 'Description' }))]

const itemsWithBadge = [
...items.map((item, i) => ({
...item,
badge: i % 2 === 0 ? 'Badge' : { color: 'primary', variant: 'solid', size: 'sm', label: 'Badge' }
}))
]
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const props = { open: true, portal: false, items }

renderEach(SelectMenu, [
// Props
['with items', { props }],
['with items with description', { props: { ...props, items: itemsWithDescription } }],
['with items with badge', { props: { ...props, items: itemsWithBadge } }],
['with modelValue', { props: { ...props, modelValue: items[0] } }],
['with defaultValue', { props: { ...props, defaultValue: items[0] } }],
['with valueKey', { props: { ...props, valueKey: 'label', defaultValue: 'Backlog' } }],
Expand Down
Loading
Loading