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
31 changes: 20 additions & 11 deletions src/Menu/MenuContainer.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<Menu as="div" class="inline-block text-left">
<Menu as="div" class="inline-block text-left" v-slot="{ open }">
<MenuButton class="focus:outline-none" ref="reference">
<slot name="button">
<D9Icon class="fill-current" name="ellipsis-h" />
Expand All @@ -13,24 +13,31 @@
:to="typeof usePortal === 'string' ? usePortal : 'body'"
:disabled="false || !usePortal"
>
<div class="w-56 z-20" :style="floatingStyles" ref="floating">
<transition
enter-active-class="transition duration-200 ease-in-out"
enter-from-class="transform -translate-y-4 opacity-0"
enter-to-class="transform opacity-100"
leave-active-class="transition duration-100 ease-in-out"
leave-from-class="transform opacity-100"
leave-to-class="transform -translate-y-2 opacity-0"
<!-- Mounted only while open, so autoUpdate doesn't track closed menus -->
<transition
enter-active-class="transition duration-200 ease-in-out"
enter-from-class="transform -translate-y-4 opacity-0"
enter-to-class="transform opacity-100"
leave-active-class="transition duration-100 ease-in-out"
leave-from-class="transform opacity-100"
leave-to-class="transform -translate-y-2 opacity-0"
>
<div
v-if="open"
class="w-56 z-20"
:style="floatingStyles"
ref="floating"
>
<MenuItems
static
:class="[
'z-40 mt-1 p-1 bg-white rounded shadow-lg text-grey-700 focus:outline-none w-full',
]"
>
<slot></slot>
</MenuItems>
</transition>
</div>
</div>
</transition>
</teleport>
</Menu>
</template>
Expand Down Expand Up @@ -76,6 +83,8 @@ const placement = props.position === "left" ? "bottom-end" : "bottom-start";

const { floatingStyles } = useFloating(reference, floating, {
placement,
// left/top instead of a transform, so the transition's translate isn't overridden
transform: false,
whileElementsMounted: autoUpdate,
middleware: [offset(6), flip(), shift({ padding: 6 })],
});
Expand Down
73 changes: 73 additions & 0 deletions tests/Menu.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { mount } from "@vue/test-utils";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { h } from "vue";
import { D9Menu, D9MenuLink } from "../src";

// jsdom has neither observer, so count the live ones floating-ui's autoUpdate creates.
let observers = 0;
class FakeObserver {
constructor() {
observers++;
}
observe() {}
unobserve() {}
disconnect() {
observers--;
}
}

// In jsdom, window is the only scroll ancestor autoUpdate listens on.
let listeners = 0;
const countListeners = (method: "addEventListener" | "removeEventListener") => {
const original = window[method];
vi.spyOn(window, method).mockImplementation((...args) => {
if (args[0] === "scroll" || args[0] === "resize") {
listeners += method === "addEventListener" ? 1 : -1;
}
return original.apply(window, args);
});
};

beforeEach(() => {
observers = 0;
listeners = 0;
vi.stubGlobal("ResizeObserver", FakeObserver);
vi.stubGlobal("IntersectionObserver", FakeObserver);
// autoUpdate only watches layout shifts of a reference that has a size.
vi.spyOn(Element.prototype, "getBoundingClientRect").mockReturnValue(
new DOMRect(0, 0, 20, 20)
);
countListeners("addEventListener");
countListeners("removeEventListener");
});

afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});

// useFloating styles the floating wrapper inline.
const floating = () => document.querySelectorAll('[style*="position"]').length;

describe("D9Menu", () => {
it("tracks its position only while open", async () => {
const menus = [1, 2, 3].map(() =>
mount(D9Menu, {
props: { position: "right", usePortal: true },
slots: { default: () => h(D9MenuLink, { label: "Account" }) },
attachTo: document.body,
})
);
expect([floating(), observers, listeners]).toEqual([0, 0, 0]);

await menus[0].find("button").trigger("click");
expect(floating()).toBe(1);
expect(observers).toBeGreaterThan(0);
expect(listeners).toBeGreaterThan(0);

await menus[0].find("button").trigger("click");
expect([floating(), observers, listeners]).toEqual([0, 0, 0]);

menus.forEach((menu) => menu.unmount());
});
});
Loading