Files
tools/vue/primitives/src/menus/navigation-menu/NavigationMenuSub.vue
T
robonen eefd7abf83 feat(primitives): media-editor components, category reorg, perf + type cleanup
Reorganize components into category folders (forms/canvas/overlays/etc.); add the
media-editor headless family (timeline, curve-editor, waveform, crop, color
picker, etc.); apply perf fixes (O(1) collection lookups, plain-object drag
state, gesture-leak teardown, shallowRef color state, rect caching) and replace
source `any` with proper types.
2026-06-15 16:54:29 +07:00

126 lines
3.6 KiB
Vue

<script lang="ts">
import type { PrimitiveProps } from '../../internal/primitive';
import type { Orientation } from '../../utilities/roving-focus';
/**
* Nests a second navigation menu inside a `NavigationMenuContent` panel, with its
* own independent active value while inheriting the parent's timing and direction.
* Use it to build multi-level menus where a content panel itself contains a list of
* triggers and sub-panels.
*/
export interface NavigationMenuSubProps extends PrimitiveProps {
/** Uncontrolled initial value. */
defaultValue?: string;
/** Submenu orientation. @default 'horizontal' */
orientation?: Orientation;
}
export interface NavigationMenuSubEmits {
'update:modelValue': [value: string];
}
</script>
<script setup lang="ts">
import type { Ref } from 'vue';
import { ref, shallowRef, watchEffect } from 'vue';
import { useForwardExpose, useId } from '@robonen/vue';
import { useCollectionProvider } from '../../utilities/collection';
import { Primitive } from '../../internal/primitive';
import { provideNavigationMenuContext, useNavigationMenuContext } from './context';
import { NAVIGATION_MENU_COLLECTION_KEY } from './utils';
defineOptions({ inheritAttrs: false });
const { defaultValue, orientation = 'horizontal', as = 'div' } = defineProps<NavigationMenuSubProps>();
defineEmits<NavigationMenuSubEmits>();
defineSlots<{
default?: (props: { modelValue: string }) => unknown;
}>();
const localValue = ref<string>(defaultValue ?? '');
/** Controlled active value of the submenu. Use `v-model`. */
const modelValue = defineModel<string | undefined>({
default: undefined,
get: v => v ?? localValue.value,
set: (v) => {
const next = v ?? '';
localValue.value = next;
return next;
},
}) as unknown as Ref<string>;
const previousValue = ref<string>('');
const parentContext = useNavigationMenuContext();
const { forwardRef, currentElement } = useForwardExpose();
const indicatorTrack = shallowRef<HTMLElement | undefined>(undefined);
const viewport = shallowRef<HTMLElement | undefined>(undefined);
const activeTrigger = shallowRef<HTMLElement | undefined>(undefined);
const { getItems, CollectionSlot } = useCollectionProvider<{ value: string }>(NAVIGATION_MENU_COLLECTION_KEY);
const baseId = useId(undefined, 'primitives-navigation-menu-sub');
watchEffect(() => {
if (!modelValue.value) return;
const items = getItems().map(i => i.ref);
const matched = items.find(item => item.id.includes(`-trigger-${modelValue.value}`));
if (matched) activeTrigger.value = matched;
});
provideNavigationMenuContext({
...parentContext,
isRootMenu: false,
modelValue,
previousValue,
baseId,
orientation,
rootNavigationMenu: currentElement,
activeTrigger,
onActiveTriggerChange: (el) => { activeTrigger.value = el; },
indicatorTrack,
onIndicatorTrackChange: (el) => { indicatorTrack.value = el; },
viewport,
onViewportChange: (el) => { viewport.value = el; },
onTriggerEnter: (val) => {
modelValue.value = val;
},
onTriggerLeave: () => {
/* submenus don't auto-close on trigger leave */
},
onContentEnter: () => {
/* no-op for submenus */
},
onContentLeave: () => {
/* no-op for submenus */
},
onItemSelect: (val) => {
previousValue.value = modelValue.value;
modelValue.value = val;
},
onItemDismiss: () => {
previousValue.value = modelValue.value;
modelValue.value = '';
},
});
</script>
<template>
<CollectionSlot>
<Primitive
:ref="forwardRef"
:as="as"
:data-orientation="orientation"
data-primitives-navigation-menu
v-bind="$attrs"
>
<slot :model-value="modelValue" />
</Primitive>
</CollectionSlot>
</template>