feat(Accordion/Breadcrumb/CommandPalette/ContextMenu/DropdownMenu/NavigationMenu/Tabs): add labelKey prop

This commit is contained in:
Benjamin Canac
2024-10-11 14:39:44 +02:00
parent f6f9823b15
commit acfc6cef2d
23 changed files with 342 additions and 50 deletions

View File

@@ -33,6 +33,11 @@ export interface AccordionProps<T> extends Pick<AccordionRootProps, 'collapsible
* @defaultValue appConfig.ui.icons.chevronDown
*/
trailingIcon?: string
/**
* The key used to get the label from the item.
* @defaultValue 'label'
*/
labelKey?: string
class?: any
ui?: Partial<typeof accordion.slots>
}
@@ -56,10 +61,12 @@ import { computed } from 'vue'
import { AccordionRoot, AccordionItem, AccordionHeader, AccordionTrigger, AccordionContent, useForwardPropsEmits } from 'radix-vue'
import { reactivePick } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { get } from '../utils'
const props = withDefaults(defineProps<AccordionProps<T>>(), {
type: 'single',
collapsible: true
collapsible: true,
labelKey: 'label'
})
const emits = defineEmits<AccordionEmits>()
const slots = defineSlots<AccordionSlots<T>>()
@@ -88,8 +95,8 @@ const ui = computed(() => accordion({
<UIcon v-if="item.icon" :name="item.icon" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
</slot>
<span v-if="item.label || !!slots.default" :class="ui.label({ class: props.ui?.label })">
<slot :item="item" :index="index" :open="open">{{ item.label }}</slot>
<span v-if="get(item, props.labelKey as string) || !!slots.default" :class="ui.label({ class: props.ui?.label })">
<slot :item="item" :index="index" :open="open">{{ get(item, props.labelKey as string) }}</slot>
</span>
<slot name="trailing" :item="item" :index="index" :open="open">

View File

@@ -30,6 +30,11 @@ export interface BreadcrumbProps<T> {
* @defaultValue appConfig.ui.icons.chevronRight
*/
separatorIcon?: string
/**
* The key used to get the label from the item.
* @defaultValue 'label'
*/
labelKey?: string
class?: any
ui?: PartialString<typeof breadcrumb.slots>
}
@@ -49,13 +54,16 @@ export type BreadcrumbSlots<T extends { slot?: string }> = {
<script setup lang="ts" generic="T extends BreadcrumbItem">
import { Primitive } from 'radix-vue'
import { useAppConfig } from '#imports'
import { get } from '../utils'
import { pickLinkProps } from '../utils/link'
import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
import ULinkBase from './LinkBase.vue'
import ULink from './Link.vue'
const props = defineProps<BreadcrumbProps<T>>()
const props = withDefaults(defineProps<BreadcrumbProps<T>>(), {
labelKey: 'label'
})
const slots = defineSlots<BreadcrumbSlots<T>>()
const appConfig = useAppConfig()
@@ -77,9 +85,9 @@ const ui = breadcrumb()
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active: index === items!.length - 1 })" />
</slot>
<span v-if="item.label || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
<span v-if="get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
<slot :name="item.slot ? `${item.slot}-label`: 'item-label'" :item="item" :active="index === items!.length - 1" :index="index">
{{ item.label }}
{{ get(item, props.labelKey as string) }}
</slot>
</span>

View File

@@ -90,6 +90,11 @@ export interface CommandPaletteProps<G, T> extends Pick<ComboboxRootProps, 'mult
}
*/
fuse?: UseFuseOptions<T>
/**
* The key used to get the label from the item.
* @defaultValue 'label'
*/
labelKey?: string
class?: any
ui?: PartialString<typeof commandPalette.slots>
}
@@ -116,7 +121,7 @@ import { defu } from 'defu'
import { reactivePick } from '@vueuse/core'
import { useFuse } from '@vueuse/integrations/useFuse'
import { useAppConfig } from '#imports'
import { omit } from '../utils'
import { omit, get } from '../utils'
import { highlight } from '../utils/fuse'
import UIcon from './Icon.vue'
import UAvatar from './Avatar.vue'
@@ -126,7 +131,8 @@ import UInput from './Input.vue'
const props = withDefaults(defineProps<CommandPaletteProps<G, T>>(), {
modelValue: '',
placeholder: 'Type a command or search...'
placeholder: 'Type a command or search...',
labelKey: 'label'
})
const emits = defineEmits<CommandPaletteEmits<T>>()
const slots = defineSlots<CommandPaletteSlots<G, T>>()
@@ -144,7 +150,7 @@ const fuse = computed(() => defu({}, props.fuse, {
fuseOptions: {
ignoreLocation: true,
threshold: 0.1,
keys: ['label', 'suffix']
keys: [props.labelKey, 'suffix']
},
resultLimit: 12,
matchAllWhenSearchEmpty: true
@@ -175,8 +181,8 @@ function getGroupWithItems(group: G, items: (T & { matches?: FuseResult<T>['matc
items: items.slice(0, fuse.value.resultLimit).map((item) => {
return {
...item,
labelHtml: highlight<T>(item, searchTerm.value, 'label'),
suffixHtml: highlight<T>(item, searchTerm.value, undefined, ['label'])
labelHtml: highlight<T>(item, searchTerm.value, props.labelKey),
suffixHtml: highlight<T>(item, searchTerm.value, undefined, [props.labelKey])
}
})
}
@@ -255,8 +261,8 @@ const groups = computed(() => {
<ComboboxViewport :class="ui.viewport({ class: props.ui?.viewport })">
<ComboboxGroup v-for="(group, groupIndex) in groups" :key="`group-${groupIndex}`" :class="ui.group({ class: props.ui?.group })">
<ComboboxLabel v-if="group.label" :class="ui.label({ class: props.ui?.label })">
{{ group.label }}
<ComboboxLabel v-if="get(group, props.labelKey as string)" :class="ui.label({ class: props.ui?.label })">
{{ get(group, props.labelKey as string) }}
</ComboboxLabel>
<ComboboxItem
@@ -281,11 +287,11 @@ const groups = computed(() => {
/>
</slot>
<span v-if="item.label || !!slots[item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`]" :class="ui.itemLabel({ class: props.ui?.itemLabel })">
<span v-if="item.labelHtml || get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`]" :class="ui.itemLabel({ class: props.ui?.itemLabel })">
<slot :name="item.slot ? `${item.slot}-label` : group.slot ? `${group.slot}-label` : `item-label`" :item="item" :index="index">
<span v-if="item.prefix" :class="ui.itemLabelPrefix({ class: props.ui?.itemLabelPrefix })">{{ item.prefix }}</span>
<span :class="ui.itemLabelBase({ class: props.ui?.itemLabelBase })" v-html="item.labelHtml || item.label" />
<span :class="ui.itemLabelBase({ class: props.ui?.itemLabelBase })" v-html="item.labelHtml || get(item, props.labelKey as string)" />
<span :class="ui.itemLabelSuffix({ class: props.ui?.itemLabelSuffix })" v-html="item.suffixHtml || item.suffix" />
</slot>

View File

@@ -42,6 +42,11 @@ export interface ContextMenuProps<T> extends Omit<ContextMenuRootProps, 'dir'>,
* @defaultValue true
*/
portal?: boolean
/**
* The key used to get the label from the item.
* @defaultValue 'label'
*/
labelKey?: string
class?: any
ui?: PartialString<typeof contextMenu.slots>
}
@@ -69,7 +74,8 @@ import UContextMenuContent from './ContextMenuContent.vue'
const props = withDefaults(defineProps<ContextMenuProps<T>>(), {
portal: true,
modal: true
modal: true,
labelKey: 'label'
})
const emits = defineEmits<ContextMenuEmits>()
const slots = defineSlots<ContextMenuSlots<T>>()
@@ -96,6 +102,7 @@ const ui = computed(() => contextMenu({
v-bind="contentProps"
:items="items"
:portal="portal"
:label-key="labelKey"
>
<template v-for="(_, name) in proxySlots" #[name]="slotData: any">
<slot :name="name" v-bind="slotData" />

View File

@@ -10,6 +10,7 @@ interface ContextMenuContentProps<T> extends Omit<RadixContextMenuContentProps,
items?: T[] | T[][]
portal?: boolean
sub?: boolean
labelKey: string
class?: any
ui: typeof _contextMenu
uiOverride?: any
@@ -24,7 +25,7 @@ import { ContextMenu } from 'radix-vue/namespaced'
import { useForwardPropsEmits } from 'radix-vue'
import { reactiveOmit, createReusableTemplate } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { omit } from '../utils'
import { omit, get } from '../utils'
import { pickLinkProps } from '../utils/link'
import ULinkBase from './LinkBase.vue'
import ULink from './Link.vue'
@@ -53,9 +54,9 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, active })" />
</slot>
<span v-if="item.label || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.itemLabel({ class: uiOverride?.itemLabel, active })">
<span v-if="get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.itemLabel({ class: uiOverride?.itemLabel, active })">
<slot :name="item.slot ? `${item.slot}-label`: 'item-label'" :item="(item as T)" :active="active" :index="index">
{{ item.label }}
{{ get(item, props.labelKey as string) }}
</slot>
<UIcon v-if="item.target === '_blank'" :name="appConfig.ui.icons.external" :class="ui.itemLabelExternalIcon({ class: uiOverride?.itemLabelExternalIcon, active })" />
@@ -85,7 +86,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
as="button"
type="button"
:disabled="item.disabled"
:text-value="item.label"
:text-value="get(item, props.labelKey as string)"
:class="ui.item({ class: uiOverride?.item })"
>
<ReuseItemTemplate :item="item" :index="index" />
@@ -99,6 +100,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
:portal="portal"
:items="item.children"
:align-offset="-4"
:label-key="labelKey"
v-bind="item.content"
>
<template v-for="(_, name) in proxySlots" #[name]="slotData: any">
@@ -106,7 +108,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
</template>
</UContextMenuContent>
</ContextMenu.Sub>
<ContextMenu.Item v-else as-child :disabled="item.disabled" :text-value="item.label" @select="item.select">
<ContextMenu.Item v-else as-child :disabled="item.disabled" :text-value="get(item, props.labelKey as string)" @select="item.select">
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item as Omit<ContextMenuItem, 'type'>)" custom>
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [uiOverride?.item, item.class], active })">
<ReuseItemTemplate :item="item" :active="active" :index="index" />

View File

@@ -50,6 +50,11 @@ export interface DropdownMenuProps<T> extends Omit<DropdownMenuRootProps, 'dir'>
* @defaultValue true
*/
portal?: boolean
/**
* The key used to get the label from the item.
* @defaultValue 'label'
*/
labelKey?: string
class?: any
ui?: PartialString<typeof dropdownMenu.slots>
}
@@ -78,7 +83,8 @@ import UDropdownMenuContent from './DropdownMenuContent.vue'
const props = withDefaults(defineProps<DropdownMenuProps<T>>(), {
portal: true,
modal: true
modal: true,
labelKey: 'label'
})
const emits = defineEmits<DropdownMenuEmits>()
const slots = defineSlots<DropdownMenuSlots<T>>()
@@ -106,6 +112,7 @@ const ui = computed(() => dropdownMenu({
v-bind="contentProps"
:items="items"
:portal="portal"
:label-key="labelKey"
>
<template v-for="(_, name) in proxySlots" #[name]="slotData: any">
<slot :name="name" v-bind="slotData" />

View File

@@ -10,6 +10,7 @@ interface DropdownMenuContentProps<T> extends Omit<RadixDropdownMenuContentProps
items?: T[] | T[][]
portal?: boolean
sub?: boolean
labelKey: string
class?: any
ui: typeof _dropdownMenu
uiOverride?: any
@@ -28,7 +29,7 @@ import { DropdownMenu } from 'radix-vue/namespaced'
import { useForwardPropsEmits } from 'radix-vue'
import { reactiveOmit, createReusableTemplate } from '@vueuse/core'
import { useAppConfig } from '#imports'
import { omit } from '../utils'
import { omit, get } from '../utils'
import { pickLinkProps } from '../utils/link'
import ULinkBase from './LinkBase.vue'
import ULink from './Link.vue'
@@ -57,9 +58,9 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.itemLeadingIcon({ class: uiOverride?.itemLeadingIcon, active })" />
</slot>
<span v-if="item.label || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.itemLabel({ class: uiOverride?.itemLabel, active })">
<span v-if="get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.itemLabel({ class: uiOverride?.itemLabel, active })">
<slot :name="item.slot ? `${item.slot}-label`: 'item-label'" :item="(item as T)" :active="active" :index="index">
{{ item.label }}
{{ get(item, props.labelKey as string) }}
</slot>
<UIcon v-if="item.target === '_blank'" :name="appConfig.ui.icons.external" :class="ui.itemLabelExternalIcon({ class: uiOverride?.itemLabelExternalIcon, active })" />
@@ -89,7 +90,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
as="button"
type="button"
:disabled="item.disabled"
:text-value="item.label"
:text-value="get(item, props.labelKey as string)"
:class="ui.item({ class: uiOverride?.item })"
>
<ReuseItemTemplate :item="item" :index="index" />
@@ -106,6 +107,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
align="start"
:align-offset="-4"
:side-offset="3"
:label-key="labelKey"
v-bind="item.content"
>
<template v-for="(_, name) in proxySlots" #[name]="slotData: any">
@@ -113,7 +115,7 @@ const groups = computed(() => props.items?.length ? (Array.isArray(props.items[0
</template>
</UDropdownMenuContent>
</DropdownMenu.Sub>
<DropdownMenu.Item v-else as-child :disabled="item.disabled" :text-value="item.label" @select="item.select">
<DropdownMenu.Item v-else as-child :disabled="item.disabled" :text-value="get(item, props.labelKey as string)" @select="item.select">
<ULink v-slot="{ active, ...slotProps }" v-bind="pickLinkProps(item as Omit<DropdownMenuItem, 'type'>)" custom>
<ULinkBase v-bind="slotProps" :class="ui.item({ class: [uiOverride?.item, item.class], active })">
<ReuseItemTemplate :item="item" :active="active" :index="index" />

View File

@@ -61,6 +61,11 @@ export interface NavigationMenuProps<T> extends Pick<NavigationMenuRootProps, 'd
* @defaultValue false
*/
arrow?: boolean
/**
* The key used to get the label from the item.
* @defaultValue 'label'
*/
labelKey?: string
class?: any
ui?: PartialString<typeof navigationMenu.slots>
}
@@ -82,6 +87,7 @@ export type NavigationMenuSlots<T extends { slot?: string }> = {
import { computed, toRef } from 'vue'
import { NavigationMenuRoot, NavigationMenuList, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink, NavigationMenuIndicator, NavigationMenuViewport, useForwardPropsEmits } from 'radix-vue'
import { reactivePick } from '@vueuse/core'
import { get } from '../utils'
import { pickLinkProps } from '../utils/link'
import ULinkBase from './LinkBase.vue'
import ULink from './Link.vue'
@@ -91,7 +97,8 @@ import UBadge from './Badge.vue'
const props = withDefaults(defineProps<NavigationMenuProps<T>>(), {
orientation: 'horizontal',
delayDuration: 0
delayDuration: 0,
labelKey: 'label'
})
const emits = defineEmits<NavigationMenuEmits>()
const slots = defineSlots<NavigationMenuSlots<T>>()
@@ -130,9 +137,9 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active, disabled: !!item.disabled })" />
</slot>
<span v-if="item.label || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
<span v-if="get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label`: 'item-label']" :class="ui.linkLabel({ class: props.ui?.linkLabel })">
<slot :name="item.slot ? `${item.slot}-label`: 'item-label'" :item="item" :active="active" :index="index">
{{ item.label }}
{{ get(item, props.labelKey as string) }}
</slot>
<UIcon v-if="item.target === '_blank'" :name="appConfig.ui.icons.external" :class="ui.linkLabelExternalIcon({ class: props.ui?.linkLabelExternalIcon, active })" />
@@ -165,7 +172,7 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
<div :class="ui.childLinkWrapper({ class: props.ui?.childLinkWrapper })">
<p :class="ui.childLinkLabel({ class: props.ui?.childLinkLabel, active: childActive })">
{{ childItem.label }}
{{ get(childItem, props.labelKey as string) }}
<UIcon v-if="childItem.target === '_blank'" :name="appConfig.ui.icons.external" :class="ui.childLinkLabelExternalIcon({ class: props.ui?.childLinkLabelExternalIcon, active: childActive })" />
</p>

View File

@@ -44,6 +44,11 @@ export interface TabsProps<T> extends Pick<TabsRootProps<string | number>, 'defa
* @defaultValue true
*/
content?: boolean | Omit<TabsContentProps, 'as' | 'asChild' | 'value'>
/**
* The key used to get the label from the item.
* @defaultValue 'label'
*/
labelKey?: string
class?: any
ui?: PartialString<typeof tabs.slots>
}
@@ -66,11 +71,13 @@ import { computed, toRef } from 'vue'
import { defu } from 'defu'
import { TabsRoot, TabsList, TabsIndicator, TabsTrigger, TabsContent, useForwardPropsEmits } from 'radix-vue'
import { reactivePick } from '@vueuse/core'
import { get } from '../utils'
const props = withDefaults(defineProps<TabsProps<T>>(), {
content: true,
defaultValue: '0',
orientation: 'horizontal'
orientation: 'horizontal',
labelKey: 'label'
})
const emits = defineEmits<TabsEmits>()
const slots = defineSlots<TabsSlots<T>>()
@@ -97,8 +104,8 @@ const ui = computed(() => tabs({
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" />
</slot>
<span v-if="item.label || !!slots.default" :class="ui.label({ class: props.ui?.label })">
<slot :item="item" :index="index">{{ item.label }}</slot>
<span v-if="get(item, props.labelKey as string) || !!slots.default" :class="ui.label({ class: props.ui?.label })">
<slot :item="item" :index="index">{{ get(item, props.labelKey as string) }}</slot>
</span>
<slot name="trailing" :item="item" :index="index" />