1
1
<!-- eslint-disable vue/block-tag-newline -->
2
2
<script lang="ts">
3
- import type { NavigationMenuRootProps , NavigationMenuRootEmits , NavigationMenuContentProps , NavigationMenuContentEmits , CollapsibleRootProps } from ' reka-ui'
3
+ import type { NavigationMenuRootProps , NavigationMenuRootEmits , NavigationMenuContentProps , NavigationMenuContentEmits , AccordionRootProps } from ' reka-ui'
4
4
import type { AppConfig } from ' @nuxt/schema'
5
5
import theme from ' #build/ui/navigation-menu'
6
6
import type { AvatarProps , BadgeProps , LinkProps , TooltipProps } from ' ../types'
@@ -14,7 +14,7 @@ export interface NavigationMenuChildItem extends Omit<NavigationMenuItem, 'type'
14
14
[key : string ]: any
15
15
}
16
16
17
- export interface NavigationMenuItem extends Omit <LinkProps , ' type' | ' raw' | ' custom' >, Pick < CollapsibleRootProps , ' defaultOpen ' | ' open ' > {
17
+ export interface NavigationMenuItem extends Omit <LinkProps , ' type' | ' raw' | ' custom' > {
18
18
label? : string
19
19
/**
20
20
* @IconifyIcon
@@ -49,13 +49,15 @@ export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
49
49
*/
50
50
value? : string
51
51
children? : NavigationMenuChildItem []
52
+ defaultOpen? : boolean
53
+ open? : boolean
52
54
onSelect? (e : Event ): void
53
55
class? : any
54
56
ui? : Pick <NavigationMenu [' slots' ], ' item' | ' linkLeadingAvatarSize' | ' linkLeadingAvatar' | ' linkLeadingIcon' | ' linkLabel' | ' linkLabelExternalIcon' | ' linkTrailing' | ' linkTrailingBadgeSize' | ' linkTrailingBadge' | ' linkTrailingIcon' | ' label' | ' link' | ' content' | ' childList' | ' childItem' | ' childLink' | ' childLinkIcon' | ' childLinkWrapper' | ' childLinkLabel' | ' childLinkLabelExternalIcon' | ' childLinkDescription' >
55
57
[key : string ]: any
56
58
}
57
59
58
- export interface NavigationMenuProps <T extends ArrayOrNested <NavigationMenuItem > = ArrayOrNested <NavigationMenuItem >> extends Pick <NavigationMenuRootProps , ' modelValue' | ' defaultValue' | ' delayDuration' | ' disableClickTrigger' | ' disableHoverTrigger' | ' skipDelayDuration' | ' disablePointerLeaveClose' | ' unmountOnHide' > {
60
+ export interface NavigationMenuProps <T extends ArrayOrNested <NavigationMenuItem > = ArrayOrNested <NavigationMenuItem >> extends Pick <NavigationMenuRootProps , ' modelValue' | ' defaultValue' | ' delayDuration' | ' disableClickTrigger' | ' disableHoverTrigger' | ' skipDelayDuration' | ' disablePointerLeaveClose' | ' unmountOnHide' >, Pick < AccordionRootProps , ' disabled ' | ' type ' | ' collapsible ' > {
59
61
/**
60
62
* The element or component this component should render as.
61
63
* @defaultValue 'div'
@@ -143,8 +145,8 @@ export type NavigationMenuSlots<
143
145
144
146
<script setup lang="ts" generic =" T extends ArrayOrNested <NavigationMenuItem >" >
145
147
import { computed , toRef } from ' vue'
146
- import { NavigationMenuRoot , NavigationMenuList , NavigationMenuItem , NavigationMenuTrigger , NavigationMenuContent , NavigationMenuLink , NavigationMenuIndicator , NavigationMenuViewport , useForwardPropsEmits } from ' reka-ui'
147
- import { createReusableTemplate } from ' @vueuse/core'
148
+ import { NavigationMenuRoot , NavigationMenuList , NavigationMenuItem , NavigationMenuTrigger , NavigationMenuContent , NavigationMenuLink , NavigationMenuIndicator , NavigationMenuViewport , AccordionRoot , AccordionItem , AccordionTrigger , AccordionContent , useForwardPropsEmits } from ' reka-ui'
149
+ import { reactivePick , createReusableTemplate } from ' @vueuse/core'
148
150
import { useAppConfig } from ' #imports'
149
151
import { get , isArrayOfArray } from ' ../utils'
150
152
import { tv } from ' ../utils/tv'
@@ -154,14 +156,15 @@ import ULink from './Link.vue'
154
156
import UAvatar from ' ./Avatar.vue'
155
157
import UIcon from ' ./Icon.vue'
156
158
import UBadge from ' ./Badge.vue'
157
- import UCollapsible from ' ./Collapsible.vue'
158
159
import UTooltip from ' ./Tooltip.vue'
159
160
160
161
const props = withDefaults (defineProps <NavigationMenuProps <T >>(), {
161
162
orientation: ' horizontal' ,
162
163
contentOrientation: ' horizontal' ,
163
164
externalIcon: true ,
164
165
delayDuration: 0 ,
166
+ type: ' multiple' ,
167
+ collapsible: true ,
165
168
unmountOnHide: true ,
166
169
labelKey: ' label'
167
170
})
@@ -182,6 +185,7 @@ const rootProps = useForwardPropsEmits(computed(() => ({
182
185
disablePointerLeaveClose: props .disablePointerLeaveClose ,
183
186
unmountOnHide: props .unmountOnHide
184
187
})), emits )
188
+ const accordionProps = useForwardPropsEmits (reactivePick (props , ' collapsible' , ' disabled' , ' type' , ' unmountOnHide' ), emits )
185
189
const contentProps = toRef (() => props .content )
186
190
187
191
const [DefineLinkTemplate, ReuseLinkTemplate] = createReusableTemplate <{ item: NavigationMenuItem , index: number , active? : boolean }>()
@@ -195,7 +199,7 @@ const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: N
195
199
196
200
const ui = computed (() => tv ({ extend: tv (theme ), ... (appConfig .ui ?.navigationMenu || {}) })({
197
201
orientation: props .orientation ,
198
- contentOrientation: props .contentOrientation ,
202
+ contentOrientation: props .orientation === ' vertical ' ? undefined : props . contentOrientation ,
199
203
collapsed: props .collapsed ,
200
204
color: props .color ,
201
205
variant: props .variant ,
@@ -210,6 +214,24 @@ const lists = computed<NavigationMenuItem[][]>(() =>
210
214
: [props .items ]
211
215
: []
212
216
)
217
+
218
+ function getAccordionDefaultValue(list : NavigationMenuItem []) {
219
+ function findItemsWithDefaultOpen(items : NavigationMenuItem [], level = 0 ): string [] {
220
+ return items .reduce ((acc : string [], item , index ) => {
221
+ if (item .defaultOpen || item .open ) {
222
+ acc .push (item .value || (level > 0 ? ` item-${level }-${index } ` : ` item-${index } ` ))
223
+ }
224
+ if (item .children ?.length ) {
225
+ acc .push (... findItemsWithDefaultOpen (item .children , level + 1 ))
226
+ }
227
+ return acc
228
+ }, [])
229
+ }
230
+
231
+ const indexes = findItemsWithDefaultOpen (list )
232
+
233
+ return props .type === ' single' ? indexes [0 ] : indexes
234
+ }
213
235
</script >
214
236
215
237
<template >
@@ -231,7 +253,7 @@ const lists = computed<NavigationMenuItem[][]>(() =>
231
253
<UIcon v-if =" item.target === '_blank' && externalIcon !== false" :name =" typeof externalIcon === 'string' ? externalIcon : appConfig.ui.icons.external" :class =" ui.linkLabelExternalIcon({ class: [props.ui?.linkLabelExternalIcon, item.ui?.linkLabelExternalIcon], active })" />
232
254
</span >
233
255
234
- <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, item.ui?.linkTrailing] })" >
256
+ <component :is = " orientation === 'vertical' && item.children?.length ? AccordionTrigger : ' 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>])" as = " span " :class =" ui.linkTrailing({ class: [props.ui?.linkTrailing, item.ui?.linkTrailing] })" @click.stop.prevent >
235
257
<slot :name =" ((item.slot ? `${item.slot}-trailing` : 'item-trailing') as keyof NavigationMenuSlots<T>)" :item =" item" :active =" active" :index =" index" >
236
258
<UBadge
237
259
v-if =" item.badge"
@@ -245,37 +267,34 @@ const lists = computed<NavigationMenuItem[][]>(() =>
245
267
<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, item.ui?.linkTrailingIcon], active })" />
246
268
<UIcon v-else-if =" item.trailingIcon" :name =" item.trailingIcon" :class =" ui.linkTrailingIcon({ class: [props.ui?.linkTrailingIcon, item.ui?.linkTrailingIcon], active })" />
247
269
</slot >
248
- </span >
270
+ </component >
249
271
</slot >
250
272
</DefineLinkTemplate >
251
273
252
274
<DefineItemTemplate v-slot =" { item, index, level = 0 }" >
253
275
<component
254
- :is =" (orientation === 'vertical' && item.children?.length && !collapsed ) ? UCollapsible : NavigationMenuItem"
276
+ :is =" (orientation === 'vertical' && item.children?.length) ? AccordionItem : NavigationMenuItem"
255
277
as =" li"
256
- :value =" item.value || `item-${index}`"
257
- :default-open =" item.defaultOpen"
258
- :unmount-on-hide =" (orientation === 'vertical' && item.children?.length && !collapsed) ? unmountOnHide : undefined"
259
- :open =" item.open"
278
+ :value =" item.value || (level > 0 ? `item-${level}-${index}` : `item-${index}`)"
260
279
>
261
280
<div v-if =" orientation === 'vertical' && item.type === 'label'" :class =" ui.label({ class: [props.ui?.label, item.ui?.label, item.class] })" >
262
281
<ReuseLinkTemplate :item =" item" :index =" index" />
263
282
</div >
264
- <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 >
283
+ <ULink v-else-if =" item.type !== 'label'" v-slot =" { active, ...slotProps }" v-bind =" pickLinkProps(item as Omit<NavigationMenuItem, 'type'>)" custom >
265
284
<component
266
- :is =" (orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) ? NavigationMenuTrigger : NavigationMenuLink"
285
+ :is =" (orientation === 'horizontal' && (item.children?.length || !!slots[(item.slot ? `${item.slot}-content` : 'item-content') as keyof NavigationMenuSlots<T>])) ? NavigationMenuTrigger : ((orientation === 'vertical' && item.children?.length && !(slotProps as any).href) ? AccordionTrigger : NavigationMenuLink) "
267
286
as-child
268
- :active =" active || item.active "
287
+ :active =" active"
269
288
:disabled =" item.disabled"
270
289
@select =" item.onSelect"
271
290
>
272
291
<UTooltip v-if =" !!item.tooltip" :content =" { side: 'right' }" v-bind =" item.tooltip" >
273
- <ULinkBase v-bind =" slotProps" :class =" ui.link({ class: [props.ui?.link, item.ui?.link, item.class], active: active || item.active , disabled: !!item.disabled, level: orientation === 'horizontal' || level > 0 })" >
274
- <ReuseLinkTemplate :item =" item" :active =" active || item.active " :index =" index" />
292
+ <ULinkBase v-bind =" slotProps" :class =" ui.link({ class: [props.ui?.link, item.ui?.link, item.class], active, disabled: !!item.disabled, level: orientation === 'horizontal' || level > 0 })" >
293
+ <ReuseLinkTemplate :item =" item" :active =" active" :index =" index" />
275
294
</ULinkBase >
276
295
</UTooltip >
277
- <ULinkBase v-else v-bind =" slotProps" :class =" ui.link({ class: [props.ui?.link, item.ui?.link, item.class], active: active || item.active , disabled: !!item.disabled, level: orientation === 'horizontal' || level > 0 })" >
278
- <ReuseLinkTemplate :item =" item" :active =" active || item.active " :index =" index" />
296
+ <ULinkBase v-else v-bind =" slotProps" :class =" ui.link({ class: [props.ui?.link, item.ui?.link, item.class], active, disabled: !!item.disabled, level: orientation === 'horizontal' || level > 0 })" >
297
+ <ReuseLinkTemplate :item =" item" :active =" active" :index =" index" />
279
298
</ULinkBase >
280
299
</component >
281
300
@@ -307,7 +326,7 @@ const lists = computed<NavigationMenuItem[][]>(() =>
307
326
</NavigationMenuContent >
308
327
</ULink >
309
328
310
- <template v-if =" orientation === ' vertical' && item .children ?.length && ! collapsed " # content >
329
+ <AccordionContent v-if =" orientation === 'vertical' && item.children?.length && !collapsed" :class = " ui. content({ class: [props.ui?.content, item.ui?.content] }) " >
311
330
<ul :class =" ui.childList({ class: props.ui?.childList })" >
312
331
<ReuseItemTemplate
313
332
v-for =" (childItem, childIndex) in item.children"
@@ -318,17 +337,25 @@ const lists = computed<NavigationMenuItem[][]>(() =>
318
337
:class =" ui.childItem({ class: [props.ui?.childItem, childItem.ui?.childItem] })"
319
338
/>
320
339
</ul >
321
- </template >
340
+ </AccordionContent >
322
341
</component >
323
342
</DefineItemTemplate >
324
343
325
344
<NavigationMenuRoot v-bind =" rootProps" :data-collapsed =" collapsed" :class =" ui.root({ class: [props.ui?.root, props.class] })" >
326
345
<slot name =" list-leading" />
327
346
328
347
<template v-for =" (list , listIndex ) in lists " :key =" ` list-${listIndex } ` " >
329
- <NavigationMenuList :class =" ui.list({ class: props.ui?.list })" >
348
+ <component
349
+ v-bind =" orientation === 'vertical' ? {
350
+ ...accordionProps,
351
+ defaultValue: getAccordionDefaultValue(list)
352
+ } : {}"
353
+ :is =" orientation === 'vertical' ? AccordionRoot : NavigationMenuList"
354
+ as =" ul"
355
+ :class =" ui.list({ class: props.ui?.list })"
356
+ >
330
357
<ReuseItemTemplate v-for =" (item, index) in list" :key =" `list-${listIndex}-${index}`" :item =" item" :index =" index" :class =" ui.item({ class: [props.ui?.item, item.ui?.item] })" />
331
- </NavigationMenuList >
358
+ </component >
332
359
333
360
<div v-if =" orientation === 'vertical' && listIndex < lists.length - 1" :class =" ui.separator({ class: props.ui?.separator })" />
334
361
</template >
0 commit comments