Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 33 additions & 8 deletions packages/vue/src/components/MenuItems.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<script setup lang="ts">
import { MenuItems } from "@headlessui/vue";
import { computePosition, flip, offset, shift } from "@floating-ui/dom";
import { autoUpdate, computePosition, flip, offset, shift } from "@floating-ui/dom";
import clsx from "clsx";
import { menu as menuVariants } from "@libretexts/davis-core";
import { inject, onMounted, ref, type Ref } from "vue";
import { inject, onBeforeUnmount, onMounted, ref, watch, type Ref } from "vue";

type MenuAlign = "left" | "right";
type MenuWidth = "auto" | "sm" | "md" | "lg" | "full";
Expand All @@ -25,23 +25,48 @@ const { items } = menuVariants({ align: props.align, width: props.width });
const triggerEl = inject<Ref<HTMLElement | null>>('davis:triggerEl');
const floatingEl = ref<{ $el: HTMLElement } | null>(null);
const floatingStyle = ref<Record<string, string>>({ position: 'fixed', top: '0px', left: '0px' });
const isMounted = ref(false);

onMounted(async () => {
let stopAutoUpdate: (() => void) | null = null;

function startPositioning() {
const reference = triggerEl?.value;
const floating = floatingEl.value?.$el;
if (!reference || !floating) return;

const placement = props.align === 'right' ? 'bottom-end' : 'bottom-start';
const { x, y } = await computePosition(reference, floating, {
placement,
middleware: [offset(8), flip(), shift({ padding: 8 })],
stopAutoUpdate = autoUpdate(reference, floating, async () => {
const { x, y } = await computePosition(reference, floating, {
placement,
middleware: [offset(8), flip(), shift({ padding: 8 })],
});
floatingStyle.value = { position: 'fixed', top: `${y}px`, left: `${x}px` };
});
floatingStyle.value = { position: 'fixed', top: `${y}px`, left: `${x}px` };
}

function stopPositioning() {
stopAutoUpdate?.();
stopAutoUpdate = null;
}

onMounted(() => {
isMounted.value = true;
});

// HeadlessUI keeps <MenuItems> mounted and toggles only its inner DOM element
// (exposed as $el) as the menu opens/closes. Watch that element — not the
// component ref, whose identity never changes — so we reposition on every open
// and tear the listeners down on close.
watch(() => floatingEl.value?.$el ?? null, (el) => {
stopPositioning();
if (el) startPositioning();
}, { flush: 'post' });

onBeforeUnmount(stopPositioning);
</script>

<template>
<Teleport to="body">
<Teleport to="body" :disabled="!isMounted">
<transition
enter-active-class="transition ease-out duration-100"
enter-from-class="opacity-0 scale-95"
Expand Down
32 changes: 20 additions & 12 deletions packages/vue/src/components/NotificationContainer.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
import clsx from "clsx";
import Notification from "./Notification.vue";
import type { NotificationItem, NotificationPosition } from "./Notification.vue";

const isMounted = ref(false);
onMounted(() => {
isMounted.value = true;
});

const POSITION_CLASSES: Record<NotificationPosition, string> = {
"top-right": "top-4 right-4 flex-col items-end",
"top-center": "top-4 left-1/2 -translate-x-1/2 flex-col items-center",
Expand All @@ -28,16 +34,18 @@ const emit = defineEmits<{
</script>

<template>
<div
aria-live="polite"
aria-atomic="false"
:class="clsx('fixed z-[9999] flex gap-3 pointer-events-none', POSITION_CLASSES[props.position])"
>
<Notification
v-for="n in props.notifications"
:key="n.id"
:notification="n"
@remove="emit('remove', $event)"
/>
</div>
<Teleport to="body" :disabled="!isMounted">
<div
aria-live="polite"
aria-atomic="false"
:class="clsx('fixed z-[9999] flex gap-3 pointer-events-none', POSITION_CLASSES[props.position])"
>
<Notification
v-for="n in props.notifications"
:key="n.id"
:notification="n"
@remove="emit('remove', $event)"
/>
</div>
</Teleport>
</template>
Loading