From 8f2e7c8016c72d41e43f49771884e9b3cfb7ff07 Mon Sep 17 00:00:00 2001 From: Dany Date: Sat, 11 Apr 2026 14:26:07 +0200 Subject: [PATCH 1/8] feat(Select/SelectMenu): add `badge` prop --- .../nuxt/app/pages/components/select-menu.vue | 12 ++++++++++++ .../nuxt/app/pages/components/select.vue | 7 ++++++- src/runtime/components/Select.vue | 18 ++++++++++++++++-- src/runtime/components/SelectMenu.vue | 18 ++++++++++++++++-- src/theme/select.ts | 6 ++++-- 5 files changed, 54 insertions(+), 7 deletions(-) diff --git a/playgrounds/nuxt/app/pages/components/select-menu.vue b/playgrounds/nuxt/app/pages/components/select-menu.vue index 237daa1d94..8aae11dff9 100644 --- a/playgrounds/nuxt/app/pages/components/select-menu.vue +++ b/playgrounds/nuxt/app/pages/components/select-menu.vue @@ -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', @@ -80,6 +84,14 @@ const valueMultiple = ref([fruits[0]!, vegetables[0]!]) v-bind="props" clear /> + diff --git a/playgrounds/nuxt/app/pages/components/select.vue b/playgrounds/nuxt/app/pages/components/select.vue index fd59891f2c..b5df2ffc94 100644 --- a/playgrounds/nuxt/app/pages/components/select.vue +++ b/playgrounds/nuxt/app/pages/components/select.vue @@ -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', @@ -76,6 +80,7 @@ const valueMultiple = ref([fruits[0]!, vegetables[0]!]) + diff --git a/src/runtime/components/Select.vue b/src/runtime/components/Select.vue index b26aed2048..1b26d2fb55 100644 --- a/src/runtime/components/Select.vue +++ b/src/runtime/components/Select.vue @@ -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' @@ -23,6 +23,11 @@ export type SelectItem = SelectValue | { icon?: IconProps['name'] avatar?: AvatarProps chip?: ChipProps + /** + * Display a badge on the item. + * `{ color: 'neutral', variant: 'outline', size: 'sm' }`{lang="ts-type"} + */ + badge?: string | number | BadgeProps /** * The item type. * @defaultValue 'item' @@ -32,7 +37,7 @@ export type SelectItem = SelectValue | { disabled?: boolean onSelect?: (e: Event) => void class?: any - ui?: Pick + ui?: Pick [key: string]: any } @@ -380,6 +385,15 @@ defineExpose({ {{ isSelectItem(item) ? get(item, props.labelKey as string) : item }} + diff --git a/src/runtime/components/SelectMenu.vue b/src/runtime/components/SelectMenu.vue index 052f1ff8fc..4b5ca9329c 100644 --- a/src/runtime/components/SelectMenu.vue +++ b/src/runtime/components/SelectMenu.vue @@ -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' 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' @@ -23,6 +23,11 @@ export type SelectMenuItem = SelectMenuValue | { icon?: IconProps['name'] avatar?: AvatarProps chip?: ChipProps + /** + * Display a badge on the item. + * `{ color: 'neutral', variant: 'outline', size: 'sm' }`{lang="ts-type"} + */ + badge?: string | number | BadgeProps /** * The item type. * @defaultValue 'item' @@ -31,7 +36,7 @@ export type SelectMenuItem = SelectMenuValue | { disabled?: boolean onSelect?: (e: Event) => void class?: any - ui?: Pick + ui?: Pick [key: string]: any } @@ -543,6 +548,15 @@ defineExpose({ {{ isSelectItem(item) ? get(item, props.labelKey as string) : item }} + diff --git a/src/theme/select.ts b/src/theme/select.ts index 8c914f76ca..aeef7e99cf 100644 --- a/src/theme/select.ts +++ b/src/theme/select.ts @@ -26,8 +26,10 @@ export default (options: Required) => { 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, From f721801fcf5696fb28006c725cfa9f2fba7bc791 Mon Sep 17 00:00:00 2001 From: Dany Date: Sat, 11 Apr 2026 14:54:40 +0200 Subject: [PATCH 2/8] update tests --- test/components/Select.spec.ts | 8 + test/components/SelectMenu.spec.ts | 8 + .../__snapshots__/Select-vue.spec.ts.snap | 452 ++++++++------- .../__snapshots__/Select.spec.ts.snap | 452 ++++++++------- .../__snapshots__/SelectMenu-vue.spec.ts.snap | 531 ++++++++++-------- .../__snapshots__/SelectMenu.spec.ts.snap | 531 ++++++++++-------- 6 files changed, 1090 insertions(+), 892 deletions(-) diff --git a/test/components/Select.spec.ts b/test/components/Select.spec.ts index 77b0399ba4..af8def3d11 100644 --- a/test/components/Select.spec.ts +++ b/test/components/Select.spec.ts @@ -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' } }], diff --git a/test/components/SelectMenu.spec.ts b/test/components/SelectMenu.spec.ts index f66272b680..ddd097b850 100644 --- a/test/components/SelectMenu.spec.ts +++ b/test/components/SelectMenu.spec.ts @@ -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', content: 'B' } + })) + ] + 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' } }], diff --git a/test/components/__snapshots__/Select-vue.spec.ts.snap b/test/components/__snapshots__/Select-vue.spec.ts.snap index fb49734728..9fe47d2df5 100644 --- a/test/components/__snapshots__/Select-vue.spec.ts.snap +++ b/test/components/__snapshots__/Select-vue.spec.ts.snap @@ -12,19 +12,19 @@ exports[`Select > renders with ariaLabel correctly 1`] = `