Files
tools/vue/primitives/src/navigation-menu/NavigationMenuIndicator.vue
T
robonen 626fbc70d8 fix(primitives): eslint/tsconfig migration, asChild refactor, type fixes
- Migrate to eslint flat config + composite tsconfig.
- Complete the asChild→as="template" refactor (remove asChild prop + :as-child
  bindings across components, matching Primitive's slot model).
- Fix test type errors and source type-safety (useGraceArea hull/point math,
  FocusScope/util ref typing).

Note: ~53 vue-tsc errors remain (HTML attr/event passthrough typing on
transparent wrapper components + a couple of duplicate-export naming
collisions) — not gated by CI (build/lint/test green); pending a
component-attribute-typing design decision.
2026-06-07 16:29:56 +07:00

88 lines
2.5 KiB
Vue

<script lang="ts">
export interface NavigationMenuIndicatorProps {
forceMount?: boolean;
}
</script>
<script setup lang="ts">
import { computed, onScopeDispose, ref, watch } from 'vue';
import { useForwardExpose } from '@robonen/vue';
import { Presence } from '../presence';
import { Primitive } from '../primitive';
import { useNavigationMenuContext } from './context';
defineOptions({ inheritAttrs: false });
const { forceMount = false } = defineProps<NavigationMenuIndicatorProps>();
const menuContext = useNavigationMenuContext();
const { forwardRef } = useForwardExpose();
const isVisible = computed(() => menuContext.modelValue.value !== '');
const isHorizontal = computed(() => menuContext.orientation === 'horizontal');
const rect = ref<{ size: number; position: number } | undefined>();
let triggerObserver: ResizeObserver | undefined;
let trackObserver: ResizeObserver | undefined;
function recompute() {
const trigger = menuContext.activeTrigger.value;
if (!trigger) return;
if (isHorizontal.value) {
rect.value = { size: trigger.offsetWidth, position: trigger.offsetLeft };
}
else {
rect.value = { size: trigger.offsetHeight, position: trigger.offsetTop };
}
}
watch(
() => [menuContext.activeTrigger.value, menuContext.indicatorTrack.value, isHorizontal.value],
() => {
triggerObserver?.disconnect();
trackObserver?.disconnect();
const trigger = menuContext.activeTrigger.value;
const track = menuContext.indicatorTrack.value;
if (!trigger || !track) return;
triggerObserver = new ResizeObserver(recompute);
trackObserver = new ResizeObserver(recompute);
triggerObserver.observe(trigger);
trackObserver.observe(track);
recompute();
},
{ immediate: true },
);
onScopeDispose(() => {
triggerObserver?.disconnect();
trackObserver?.disconnect();
});
const indicatorStyle = computed(() => {
if (!rect.value) return {};
return {
'--primitives-navigation-menu-indicator-size': `${rect.value.size}px`,
'--primitives-navigation-menu-indicator-position': `${rect.value.position}px`,
};
});
</script>
<template>
<Teleport v-if="menuContext.indicatorTrack.value" :to="menuContext.indicatorTrack.value">
<Presence :present="isVisible" :force-mount="forceMount">
<Primitive
:ref="forwardRef"
:data-state="isVisible ? 'visible' : 'hidden'"
:data-orientation="menuContext.orientation"
data-primitives-navigation-menu-indicator
:style="indicatorStyle"
v-bind="$attrs"
>
<slot />
</Primitive>
</Presence>
</Teleport>
</template>