mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-19 06:21:46 +01:00
fix(components): improve generic types (#3331)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
@@ -7,15 +7,23 @@ import _appConfig from '#build/app.config'
|
||||
import theme from '#build/ui/navigation-menu'
|
||||
import { tv } from '../utils/tv'
|
||||
import type { AvatarProps, BadgeProps, LinkProps } from '../types'
|
||||
import type { DynamicSlots, MaybeArrayOfArray, MaybeArrayOfArrayItem, PartialString, EmitsToProps } from '../types/utils'
|
||||
import type {
|
||||
ArrayOrNested,
|
||||
DynamicSlots,
|
||||
MergeTypes,
|
||||
NestedItem,
|
||||
PartialString,
|
||||
EmitsToProps
|
||||
} from '../types/utils'
|
||||
|
||||
const appConfigNavigationMenu = _appConfig as AppConfig & { ui: { navigationMenu: Partial<typeof theme> } }
|
||||
|
||||
const navigationMenu = tv({ extend: tv(theme), ...(appConfigNavigationMenu.ui?.navigationMenu || {}) })
|
||||
|
||||
export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'children' | 'type'> {
|
||||
export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'type'> {
|
||||
/** Description is only used when `orientation` is `horizontal`. */
|
||||
description?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'custom'>, Pick<CollapsibleRootProps, 'defaultOpen' | 'open'> {
|
||||
@@ -44,11 +52,12 @@ export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
|
||||
value?: string
|
||||
children?: NavigationMenuChildItem[]
|
||||
onSelect?(e: Event): void
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
type NavigationMenuVariants = VariantProps<typeof navigationMenu>
|
||||
|
||||
export interface NavigationMenuProps<T> extends Pick<NavigationMenuRootProps, 'modelValue' | 'defaultValue' | 'delayDuration' | 'disableClickTrigger' | 'disableHoverTrigger' | 'skipDelayDuration' | 'disablePointerLeaveClose' | 'unmountOnHide'> {
|
||||
export interface NavigationMenuProps<T extends ArrayOrNested<NavigationMenuItem> = ArrayOrNested<NavigationMenuItem>> extends Pick<NavigationMenuRootProps, 'modelValue' | 'defaultValue' | 'delayDuration' | 'disableClickTrigger' | 'disableHoverTrigger' | 'skipDelayDuration' | 'disablePointerLeaveClose' | 'unmountOnHide'> {
|
||||
/**
|
||||
* The element or component this component should render as.
|
||||
* @defaultValue 'div'
|
||||
@@ -110,31 +119,34 @@ export interface NavigationMenuProps<T> extends Pick<NavigationMenuRootProps, 'm
|
||||
* The key used to get the label from the item.
|
||||
* @defaultValue 'label'
|
||||
*/
|
||||
labelKey?: string
|
||||
labelKey?: keyof NestedItem<T>
|
||||
class?: any
|
||||
ui?: PartialString<typeof navigationMenu.slots>
|
||||
}
|
||||
|
||||
export interface NavigationMenuEmits extends NavigationMenuRootEmits {}
|
||||
|
||||
type SlotProps<T> = (props: { item: T, index: number, active?: boolean }) => any
|
||||
type SlotProps<T extends NavigationMenuItem> = (props: { item: T, index: number, active?: boolean }) => any
|
||||
|
||||
export type NavigationMenuSlots<T extends { slot?: string }> = {
|
||||
export type NavigationMenuSlots<
|
||||
A extends ArrayOrNested<NavigationMenuItem> = ArrayOrNested<NavigationMenuItem>,
|
||||
T extends NestedItem<A> = NestedItem<A>
|
||||
> = {
|
||||
'item': SlotProps<T>
|
||||
'item-leading': SlotProps<T>
|
||||
'item-label': SlotProps<T>
|
||||
'item-trailing': SlotProps<T>
|
||||
'item-content': SlotProps<T>
|
||||
} & DynamicSlots<T, SlotProps<T>>
|
||||
} & DynamicSlots<MergeTypes<T>, 'leading' | 'label' | 'trailing' | 'content', { index: number, active?: boolean }>
|
||||
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="T extends MaybeArrayOfArrayItem<I>, I extends MaybeArrayOfArray<NavigationMenuItem>">
|
||||
<script setup lang="ts" generic="T extends ArrayOrNested<NavigationMenuItem>">
|
||||
import { computed, toRef } from 'vue'
|
||||
import { NavigationMenuRoot, NavigationMenuList, NavigationMenuItem, NavigationMenuTrigger, NavigationMenuContent, NavigationMenuLink, NavigationMenuIndicator, NavigationMenuViewport, useForwardPropsEmits } from 'reka-ui'
|
||||
import { createReusableTemplate } from '@vueuse/core'
|
||||
import { useAppConfig } from '#imports'
|
||||
import { get } from '../utils'
|
||||
import { get, isArrayOfArray } from '../utils'
|
||||
import { pickLinkProps } from '../utils/link'
|
||||
import ULinkBase from './LinkBase.vue'
|
||||
import ULink from './Link.vue'
|
||||
@@ -143,7 +155,7 @@ import UIcon from './Icon.vue'
|
||||
import UBadge from './Badge.vue'
|
||||
import UCollapsible from './Collapsible.vue'
|
||||
|
||||
const props = withDefaults(defineProps<NavigationMenuProps<I>>(), {
|
||||
const props = withDefaults(defineProps<NavigationMenuProps<T>>(), {
|
||||
orientation: 'horizontal',
|
||||
contentOrientation: 'horizontal',
|
||||
externalIcon: true,
|
||||
@@ -170,8 +182,14 @@ const rootProps = useForwardPropsEmits(computed(() => ({
|
||||
const contentProps = toRef(() => props.content)
|
||||
|
||||
const appConfig = useAppConfig()
|
||||
const [DefineLinkTemplate, ReuseLinkTemplate] = createReusableTemplate<{ item: NavigationMenuItem, index: number, active?: boolean }>()
|
||||
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: NavigationMenuItem, index: number, level?: number }>({
|
||||
const [DefineLinkTemplate, ReuseLinkTemplate] = createReusableTemplate<
|
||||
{ item: NavigationMenuItem, index: number, active?: boolean },
|
||||
NavigationMenuSlots<T>
|
||||
>()
|
||||
const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<
|
||||
{ item: NavigationMenuItem, index: number, level?: number },
|
||||
NavigationMenuSlots<T>
|
||||
>({
|
||||
props: {
|
||||
item: Object,
|
||||
index: Number,
|
||||
@@ -189,30 +207,36 @@ const ui = computed(() => navigationMenu({
|
||||
highlightColor: props.highlightColor || props.color
|
||||
}))
|
||||
|
||||
const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]) ? props.items : [props.items]) as T[][] : [])
|
||||
const lists = computed<NavigationMenuItem[][]>(() =>
|
||||
props.items?.length
|
||||
? isArrayOfArray(props.items)
|
||||
? props.items
|
||||
: [props.items]
|
||||
: []
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DefineLinkTemplate v-slot="{ item, active, index }">
|
||||
<slot :name="item.slot || 'item'" :item="(item as T)" :index="index">
|
||||
<slot :name="item.slot ? `${item.slot}-leading` : 'item-leading'" :item="(item as T)" :active="active" :index="index">
|
||||
<slot :name="((item.slot || 'item') as keyof NavigationMenuSlots<T>)" :item="item" :index="index">
|
||||
<slot :name="((item.slot ? `${item.slot}-leading` : 'item-leading') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||
<UAvatar v-if="item.avatar" :size="((props.ui?.linkLeadingAvatarSize || ui.linkLeadingAvatarSize()) as AvatarProps['size'])" v-bind="item.avatar" :class="ui.linkLeadingAvatar({ class: props.ui?.linkLeadingAvatar, active, disabled: !!item.disabled })" />
|
||||
<UIcon v-else-if="item.icon" :name="item.icon" :class="ui.linkLeadingIcon({ class: props.ui?.linkLeadingIcon, active, disabled: !!item.disabled })" />
|
||||
</slot>
|
||||
|
||||
<span
|
||||
v-if="(!collapsed || orientation !== 'vertical') && (get(item, props.labelKey as string) || !!slots[item.slot ? `${item.slot}-label` : 'item-label'])"
|
||||
v-if="(!collapsed || orientation !== 'vertical') && (get(item, props.labelKey as string) || !!slots[(item.slot ? `${item.slot}-label` : 'item-label') as keyof NavigationMenuSlots<T>])"
|
||||
:class="ui.linkLabel({ class: props.ui?.linkLabel })"
|
||||
>
|
||||
<slot :name="item.slot ? `${item.slot}-label` : 'item-label'" :item="(item as T)" :active="active" :index="index">
|
||||
<slot :name="((item.slot ? `${item.slot}-label` : 'item-label') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||
{{ get(item, props.labelKey as string) }}
|
||||
</slot>
|
||||
|
||||
<UIcon v-if="item.target === '_blank' && externalIcon !== false" :name="typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class="ui.linkLabelExternalIcon({ class: props.ui?.linkLabelExternalIcon, active })" />
|
||||
</span>
|
||||
|
||||
<span v-if="(!collapsed || orientation !== 'vertical') && (item.badge || (orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])) || (orientation === 'vertical' && item.children?.length) || item.trailingIcon || !!slots[item.slot ? `${item.slot}-trailing` : 'item-trailing'])" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
|
||||
<slot :name="item.slot ? `${item.slot}-trailing` : 'item-trailing'" :item="(item as T)" :active="active" :index="index">
|
||||
<span v-if="(!collapsed || orientation !== 'vertical') && (item.badge || (orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && item.children?.length) || item.trailingIcon || !!slots[(item.slot ? `${item.slot}-trailing` : 'item-trailing') as keyof NavigationMenuSlots<T>])" :class="ui.linkTrailing({ class: props.ui?.linkTrailing })">
|
||||
<slot :name="((item.slot ? `${item.slot}-trailing` : 'item-trailing') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||
<UBadge
|
||||
v-if="item.badge"
|
||||
color="neutral"
|
||||
@@ -222,7 +246,7 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
|
||||
:class="ui.linkTrailingBadge({ class: props.ui?.linkTrailingBadge })"
|
||||
/>
|
||||
|
||||
<UIcon v-if="(orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])) || (orientation === 'vertical' && item.children?.length)" :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
|
||||
<UIcon v-if="(orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) || (orientation === 'vertical' && item.children?.length)" :name="item.trailingIcon || trailingIcon || appConfig.ui.icons.chevronDown" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
|
||||
<UIcon v-else-if="item.trailingIcon" :name="item.trailingIcon" :class="ui.linkTrailingIcon({ class: props.ui?.linkTrailingIcon, active })" />
|
||||
</slot>
|
||||
</span>
|
||||
@@ -239,23 +263,23 @@ const lists = computed(() => props.items?.length ? (Array.isArray(props.items[0]
|
||||
:open="item.open"
|
||||
>
|
||||
<div v-if="orientation === 'vertical' && item.type === 'label'" :class="ui.label({ class: props.ui?.label })">
|
||||
<ReuseLinkTemplate :item="(item as T)" :index="index" />
|
||||
<ReuseLinkTemplate :item="item" :index="index" />
|
||||
</div>
|
||||
<ULink v-else-if="item.type !== 'label'" v-slot="{ active, ...slotProps }" v-bind="(orientation === 'vertical' && item.children?.length && !collapsed) ? {} : pickLinkProps(item as Omit<NavigationMenuItem, 'type'>)" custom>
|
||||
<component
|
||||
:is="(orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])) ? NavigationMenuTrigger : NavigationMenuLink"
|
||||
:is="(orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) ? NavigationMenuTrigger : NavigationMenuLink"
|
||||
as-child
|
||||
:active="active || item.active"
|
||||
:disabled="item.disabled"
|
||||
@select="item.onSelect"
|
||||
>
|
||||
<ULinkBase v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.class], active: active || item.active, disabled: !!item.disabled, level: orientation === 'horizontal' || level > 0 })">
|
||||
<ReuseLinkTemplate :item="(item as T)" :active="active || item.active" :index="index" />
|
||||
<ReuseLinkTemplate :item="item" :active="active || item.active" :index="index" />
|
||||
</ULinkBase>
|
||||
</component>
|
||||
|
||||
<NavigationMenuContent v-if="orientation === 'horizontal' && (item.children?.length || !!slots[item.slot ? `${item.slot}-content` : 'item-content'])" v-bind="contentProps" :class="ui.content({ class: props.ui?.content })">
|
||||
<slot :name="item.slot ? `${item.slot}-content` : 'item-content'" :item="(item as T)" :active="active" :index="index">
|
||||
<NavigationMenuContent v-if="orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])" v-bind="contentProps" :class="ui.content({ class: props.ui?.content })">
|
||||
<slot :name="((item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>)" :item="item" :active="active" :index="index">
|
||||
<ul :class="ui.childList({ class: props.ui?.childList })">
|
||||
<li v-for="(childItem, childIndex) in item.children" :key="childIndex" :class="ui.childItem({ class: props.ui?.childItem })">
|
||||
<ULink v-slot="{ active: childActive, ...childSlotProps }" v-bind="pickLinkProps(childItem)" custom>
|
||||
|
||||
Reference in New Issue
Block a user