srcdev-nuxt-components 9.5.5 → 9.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/component-compliance-checklist.md +6 -2
- package/.claude/skills/components/action-menu.md +21 -15
- package/.claude/skills/components/deep-expanding-menu.md +18 -6
- package/.claude/skills/components/display-tooltip-defined.md +2 -1
- package/.claude/skills/components/display-tooltip.md +10 -4
- package/.claude/skills/components/navigation-items.md +1 -0
- package/.claude/skills/components/opening-hours.md +12 -0
- package/.claude/skills/components/pop-over.md +11 -8
- package/.claude/skills/components/price-list.md +10 -1
- package/.claude/skills/components/responsive-header.md +25 -0
- package/.claude/skills/components/select-menu.md +18 -8
- package/.claude/skills/components/site-header.md +2 -0
- package/.claude/skills/components/ui-block-decorated.md +7 -0
- package/.claude/skills/composable-anchored-popover.md +108 -0
- package/.claude/skills/composable-tooltips-guide.md +3 -1
- package/.claude/skills/index.md +1 -0
- package/.claude/skills/storybook-add-story.md +14 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +3 -1
- package/.vscode/srcdev-component-site-header.code-snippets +3 -1
- package/app/assets/styles/setup/06.utility-classes/animations/_animation-scroller-x.css +5 -3
- package/app/assets/styles/setup/06.utility-classes/animations/_auto-rotate.css +5 -3
- package/app/assets/styles/setup/06.utility-classes/animations/_entry-exit-blur.css +5 -3
- package/app/assets/styles/setup/06.utility-classes/animations/_entry-slide-in.css +5 -3
- package/app/assets/styles/setup/06.utility-classes/animations/_entry-zoom-reveal.css +5 -3
- package/app/components/01.atoms/animations/entry/CONSUMER-STYLING.md +6 -2
- package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +15 -3
- package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +62 -3
- package/app/components/01.atoms/display-tooltip/tests/DisplayTooltip.spec.ts +36 -1
- package/app/components/01.atoms/display-tooltip/tests/__snapshots__/DisplayTooltip.spec.ts.snap +1 -1
- package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +1 -1
- package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +9 -3
- package/app/components/01.atoms/pop-over/PopOver.vue +70 -9
- package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +53 -0
- package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +1 -1
- package/app/components/01.atoms/ui-block-decorated/CONSUMER-STYLING.md +12 -4
- package/app/components/01.atoms/ui-block-decorated/UiBlockDecorated.vue +20 -16
- package/app/components/01.atoms/ui-block-decorated/stories/UiBlockDecorated.stories.ts +124 -96
- package/app/components/02.molecules/action-menu/ActionMenu.vue +41 -16
- package/app/components/02.molecules/action-menu/CONSUMER-STYLING.md +16 -3
- package/app/components/02.molecules/action-menu/tests/ActionMenu.spec.ts +28 -0
- package/app/components/02.molecules/action-menu/tests/__snapshots__/ActionMenu.spec.ts.snap +3 -3
- package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +2 -1
- package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts +7 -4
- package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts +20 -1
- package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +1 -1
- package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +18 -4
- package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +87 -16
- package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts +83 -1
- package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +2 -2
- package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +1 -1
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +1 -1
- package/app/components/02.molecules/opening-hours/CONSUMER-STYLING.md +11 -1
- package/app/components/02.molecules/opening-hours/OpeningHours.vue +71 -43
- package/app/components/02.molecules/opening-hours/stories/OpeningHours.stories.ts +138 -6
- package/app/components/02.molecules/opening-hours/tests/OpeningHours.spec.ts +53 -0
- package/app/components/02.molecules/opening-hours/tests/__snapshots__/OpeningHours.spec.ts.snap +1 -1
- package/app/components/02.molecules/opening-hours/tests/buildOpeningHoursSpecification.spec.ts +37 -1
- package/app/components/02.molecules/price-list/CONSUMER-STYLING.md +17 -5
- package/app/components/02.molecules/price-list/PriceList.vue +32 -9
- package/app/components/02.molecules/price-list/stories/PriceList.stories.ts +125 -29
- package/app/components/02.molecules/price-list/tests/PriceList.spec.ts +20 -0
- package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +17 -5
- package/app/components/02.molecules/select-menu/SelectMenu.vue +51 -16
- package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +36 -0
- package/app/components/03.organisms/google-reviews/stories/GoogleReviews.stories.ts +9 -4
- package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +14 -0
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +23 -14
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +58 -30
- package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +149 -18
- package/app/components/03.organisms/responsive-header/tests/.nuxt/app.config.mjs +21 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/components.d.ts +66 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/imports.d.ts +30 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/manifest/meta/test.json +1 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/app.config.d.ts +35 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/build.d.ts +23 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/builder-env.d.ts +1 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/components.d.ts +71 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/imports.d.ts +386 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/middleware.d.ts +11 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/modules.d.ts +79 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-layouts.d.ts +17 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-nuxt.d.ts +64 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/plugins.d.ts +29 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/runtime-config.d.ts +32 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/shared-imports.d.ts +10 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/vue-shim.d.ts +0 -0
- package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +16 -0
- package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +91 -6
- package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +9 -9
- package/app/components/03.organisms/responsive-header/tests/node_modules/.cache/nuxt/chrome-workspace.json +1 -0
- package/app/components/03.organisms/site-header/SiteHeader.vue +8 -0
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +1 -1
- package/app/composables/tests/useAnchoredPopover.spec.ts +231 -0
- package/app/composables/tests/useTooltips.spec.ts +69 -0
- package/app/composables/useAnchoredPopover.ts +183 -0
- package/app/composables/useTooltips.ts +37 -20
- package/app/utils/opening-hours.ts +23 -2
- package/package.json +1 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
|
|
2
|
+
/// <reference path="nitro-layouts.d.ts" />
|
|
3
|
+
/// <reference path="app.config.d.ts" />
|
|
4
|
+
/// <reference path="runtime-config.d.ts" />
|
|
5
|
+
/// <reference path="../../../../../../../node_modules/@nuxt/nitro-server/dist/index.d.mts" />
|
|
6
|
+
/// <reference path="middleware.d.ts" />
|
|
7
|
+
|
|
8
|
+
import type { RuntimeConfig } from 'nuxt/schema'
|
|
9
|
+
import type { H3Event } from 'h3'
|
|
10
|
+
import type { LogObject } from 'consola'
|
|
11
|
+
import type { NuxtIslandContext, NuxtIslandResponse, NuxtRenderHTMLContext } from 'nuxt/app'
|
|
12
|
+
|
|
13
|
+
declare module 'nitropack' {
|
|
14
|
+
interface NitroRuntimeConfigApp {
|
|
15
|
+
buildAssetsDir: string
|
|
16
|
+
cdnURL: string
|
|
17
|
+
}
|
|
18
|
+
interface NitroRuntimeConfig extends RuntimeConfig {}
|
|
19
|
+
interface NitroRouteConfig {
|
|
20
|
+
ssr?: boolean
|
|
21
|
+
noScripts?: boolean
|
|
22
|
+
/** @deprecated Use `noScripts` instead */
|
|
23
|
+
experimentalNoScripts?: boolean
|
|
24
|
+
}
|
|
25
|
+
interface NitroRouteRules {
|
|
26
|
+
ssr?: boolean
|
|
27
|
+
noScripts?: boolean
|
|
28
|
+
/** @deprecated Use `noScripts` instead */
|
|
29
|
+
experimentalNoScripts?: boolean
|
|
30
|
+
appMiddleware?: Record<string, boolean>
|
|
31
|
+
appLayout?: string | false
|
|
32
|
+
}
|
|
33
|
+
interface NitroRuntimeHooks {
|
|
34
|
+
'dev:ssr-logs': (ctx: { logs: LogObject[], path: string }) => void | Promise<void>
|
|
35
|
+
'render:html': (htmlContext: NuxtRenderHTMLContext, context: { event: H3Event }) => void | Promise<void>
|
|
36
|
+
'render:island': (islandResponse: NuxtIslandResponse, context: { event: H3Event, islandContext: NuxtIslandContext }) => void | Promise<void>
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
declare module 'nitropack/types' {
|
|
40
|
+
interface NitroRuntimeConfigApp {
|
|
41
|
+
buildAssetsDir: string
|
|
42
|
+
cdnURL: string
|
|
43
|
+
}
|
|
44
|
+
interface NitroRuntimeConfig extends RuntimeConfig {}
|
|
45
|
+
interface NitroRouteConfig {
|
|
46
|
+
ssr?: boolean
|
|
47
|
+
noScripts?: boolean
|
|
48
|
+
/** @deprecated Use `noScripts` instead */
|
|
49
|
+
experimentalNoScripts?: boolean
|
|
50
|
+
}
|
|
51
|
+
interface NitroRouteRules {
|
|
52
|
+
ssr?: boolean
|
|
53
|
+
noScripts?: boolean
|
|
54
|
+
/** @deprecated Use `noScripts` instead */
|
|
55
|
+
experimentalNoScripts?: boolean
|
|
56
|
+
appMiddleware?: Record<string, boolean>
|
|
57
|
+
appLayout?: string | false
|
|
58
|
+
}
|
|
59
|
+
interface NitroRuntimeHooks {
|
|
60
|
+
'dev:ssr-logs': (ctx: { logs: LogObject[], path: string }) => void | Promise<void>
|
|
61
|
+
'render:html': (htmlContext: NuxtRenderHTMLContext, context: { event: H3Event }) => void | Promise<void>
|
|
62
|
+
'render:island': (islandResponse: NuxtIslandResponse, context: { event: H3Event, islandContext: NuxtIslandContext }) => void | Promise<void>
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Generated by Nuxt'
|
|
2
|
+
import type { Plugin } from '#app'
|
|
3
|
+
|
|
4
|
+
type Decorate<T extends Record<string, any>> = { [K in keyof T as K extends string ? `$${K}` : never]: T[K] }
|
|
5
|
+
|
|
6
|
+
type InjectionType<A extends Plugin> = A extends {default: Plugin<infer T>} ? Decorate<T> : unknown
|
|
7
|
+
|
|
8
|
+
type NuxtAppInjections =
|
|
9
|
+
InjectionType<typeof import("../../../../../../../node_modules/nuxt/dist/app/plugins/revive-payload.client.js")> &
|
|
10
|
+
InjectionType<typeof import("../../../../../../../node_modules/nuxt/dist/head/runtime/plugins/unhead.js")> &
|
|
11
|
+
InjectionType<typeof import("../../../../../../../node_modules/nuxt/dist/app/plugins/router.js")> &
|
|
12
|
+
InjectionType<typeof import("../../../../../../../node_modules/nuxt/dist/app/plugins/navigation-repaint.client.js")> &
|
|
13
|
+
InjectionType<typeof import("../../../../../../../node_modules/nuxt/dist/app/plugins/check-outdated-build.client.js")> &
|
|
14
|
+
InjectionType<typeof import("../../../../../../../node_modules/nuxt/dist/app/plugins/revive-payload.server.js")> &
|
|
15
|
+
InjectionType<typeof import("../../../../../../../node_modules/nuxt/dist/app/plugins/chunk-reload.client.js")>
|
|
16
|
+
|
|
17
|
+
declare module '#app' {
|
|
18
|
+
interface NuxtApp extends NuxtAppInjections { }
|
|
19
|
+
|
|
20
|
+
interface NuxtAppLiterals {
|
|
21
|
+
pluginName: 'nuxt:revive-payload:client' | 'nuxt:head' | 'nuxt:router' | 'nuxt:revive-payload:server' | 'nuxt:chunk-reload' | 'nuxt:global-components'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
declare module 'vue' {
|
|
26
|
+
interface ComponentCustomProperties extends NuxtAppInjections { }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { }
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { RuntimeConfig as UserRuntimeConfig, PublicRuntimeConfig as UserPublicRuntimeConfig } from 'nuxt/schema'
|
|
2
|
+
interface SharedRuntimeConfig {
|
|
3
|
+
app: {
|
|
4
|
+
buildId: string,
|
|
5
|
+
|
|
6
|
+
baseURL: string,
|
|
7
|
+
|
|
8
|
+
buildAssetsDir: string,
|
|
9
|
+
|
|
10
|
+
cdnURL: string,
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
nitro: {
|
|
14
|
+
envPrefix: string,
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
interface SharedPublicRuntimeConfig {
|
|
18
|
+
|
|
19
|
+
}
|
|
20
|
+
declare module '@nuxt/schema' {
|
|
21
|
+
interface RuntimeConfig extends UserRuntimeConfig {}
|
|
22
|
+
interface PublicRuntimeConfig extends UserPublicRuntimeConfig {}
|
|
23
|
+
}
|
|
24
|
+
declare module 'nuxt/schema' {
|
|
25
|
+
interface RuntimeConfig extends SharedRuntimeConfig {}
|
|
26
|
+
interface PublicRuntimeConfig extends SharedPublicRuntimeConfig {}
|
|
27
|
+
}
|
|
28
|
+
declare module 'vue' {
|
|
29
|
+
interface ComponentCustomProperties {
|
|
30
|
+
$config: UserRuntimeConfig
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Generated by auto imports
|
|
2
|
+
export {}
|
|
3
|
+
declare global {
|
|
4
|
+
const useRuntimeConfig: (event?: import('h3').H3Event) => import('nuxt/schema').RuntimeConfig
|
|
5
|
+
const useAppConfig: () => import('nuxt/schema').AppConfig
|
|
6
|
+
const defineAppConfig: <C extends import('nuxt/schema').AppConfigInput>(config: C) => C
|
|
7
|
+
const createError: typeof import('h3')['createError']
|
|
8
|
+
const setResponseStatus: typeof import('h3')['setResponseStatus']
|
|
9
|
+
|
|
10
|
+
}
|
|
File without changes
|
|
@@ -146,6 +146,22 @@ describe("NavigationItems", () => {
|
|
|
146
146
|
expect(details.attributes("open")).toBe("");
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
+
it("builds the submenu aria-label from submenuAriaLabel, falling back to name", async () => {
|
|
150
|
+
const wrapper = await mountSuspended(NavigationItems, {
|
|
151
|
+
props: {
|
|
152
|
+
submenuAriaLabel: "Untermenü {title}",
|
|
153
|
+
mainNavigationState: {
|
|
154
|
+
clonedNavLinks: { firstNav: [{ name: "Services", childLinks: [{ name: "A", path: "/a" }] }] },
|
|
155
|
+
navListVisibility: { firstNav: false },
|
|
156
|
+
hasSecondNav: false,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
const text = wrapper.find(".overflow-navigation-details .overflow-navigation-text");
|
|
161
|
+
expect(text.text()).toBe("Services");
|
|
162
|
+
expect(text.attributes("aria-label")).toBe("Untermenü Services");
|
|
163
|
+
});
|
|
164
|
+
|
|
149
165
|
it("renders correct HTML structure with default props", async () => {
|
|
150
166
|
const wrapper = await mountSuspended(NavigationItems, {
|
|
151
167
|
props: { mainNavigationState },
|
|
@@ -34,6 +34,19 @@ const triggerGeometryPass = async () => {
|
|
|
34
34
|
}
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
// Fake layout: the main nav ends at mainNavRight and each item is 100px wide, laid out in
|
|
38
|
+
// document order, so item n (0-based) ends at (n + 1) * 100.
|
|
39
|
+
const mockGeometry = (mainNavRight: number) =>
|
|
40
|
+
vi.spyOn(Element.prototype, "getBoundingClientRect").mockImplementation(function (this: Element) {
|
|
41
|
+
let right = 0;
|
|
42
|
+
if (this.classList.contains("main-navigation")) right = mainNavRight;
|
|
43
|
+
if (this.classList.contains("main-navigation-item")) {
|
|
44
|
+
const items = Array.from(this.closest(".main-navigation")?.querySelectorAll(".main-navigation-item") ?? []);
|
|
45
|
+
right = (items.indexOf(this) + 1) * 100;
|
|
46
|
+
}
|
|
47
|
+
return { left: 0, top: 0, bottom: 0, x: 0, y: 0, height: 0, right, width: right, toJSON: () => ({}) } as DOMRect;
|
|
48
|
+
});
|
|
49
|
+
|
|
37
50
|
const navLinks: ResponsiveHeaderProp = {
|
|
38
51
|
firstNav: [
|
|
39
52
|
{ name: "Home", path: "/" },
|
|
@@ -296,18 +309,90 @@ describe("ResponsiveHeader", () => {
|
|
|
296
309
|
expect(overflowNav.text()).toContain("UI Components");
|
|
297
310
|
});
|
|
298
311
|
|
|
299
|
-
it("
|
|
312
|
+
it("hides only the items that end past the main nav's right edge", async () => {
|
|
313
|
+
const geometry = mockGeometry(250);
|
|
300
314
|
const wrapper = await mountSuspended(ResponsiveHeader, {
|
|
301
315
|
props: { responsiveNavLinks: navLinks },
|
|
302
316
|
});
|
|
303
317
|
await triggerGeometryPass();
|
|
304
318
|
|
|
305
|
-
// jsdom reports every rect as 0×0, so `rect.right + margin + gap < wrapperRect.right`
|
|
306
|
-
// (0 + 0 + 12 < 0) is always false — every item correctly collapses into overflow.
|
|
307
319
|
expect(wrapper.find(".navigation").classes()).toContain("geometry-ready");
|
|
308
|
-
const
|
|
309
|
-
expect(
|
|
310
|
-
|
|
320
|
+
const hidden = wrapper.findAll(".main-navigation-item").map((item) => item.classes().includes("visually-hidden"));
|
|
321
|
+
expect(hidden).toEqual([false, false, true, true]);
|
|
322
|
+
expect(wrapper.find(".overflow-details").classes()).not.toContain("visually-hidden");
|
|
323
|
+
geometry.mockRestore();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("keeps an item that ends exactly at the main nav's right edge, so a flush-right last item isn't always hidden", async () => {
|
|
327
|
+
const geometry = mockGeometry(400);
|
|
328
|
+
const wrapper = await mountSuspended(ResponsiveHeader, {
|
|
329
|
+
props: { responsiveNavLinks: navLinks },
|
|
330
|
+
});
|
|
331
|
+
await triggerGeometryPass();
|
|
332
|
+
|
|
333
|
+
wrapper.findAll(".main-navigation-item").forEach((item) => expect(item.classes()).not.toContain("visually-hidden"));
|
|
334
|
+
expect(wrapper.find(".overflow-details").classes()).toContain("visually-hidden");
|
|
335
|
+
geometry.mockRestore();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("gives the icon-only overflow button an accessible name", async () => {
|
|
339
|
+
const wrapper = await mountSuspended(ResponsiveHeader, {
|
|
340
|
+
props: { responsiveNavLinks: navLinks },
|
|
341
|
+
});
|
|
342
|
+
expect(wrapper.find(".overflow-details-summary").attributes("aria-label")).toBe("More navigation");
|
|
343
|
+
await wrapper.setProps({ overflowButtonLabel: "Menü" });
|
|
344
|
+
expect(wrapper.find(".overflow-details-summary").attributes("aria-label")).toBe("Menü");
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("builds the submenu aria-label from submenuAriaLabel", async () => {
|
|
348
|
+
const wrapper = await mountSuspended(ResponsiveHeader, {
|
|
349
|
+
props: { responsiveNavLinks: navLinks, submenuAriaLabel: "Untermenü {title}" },
|
|
350
|
+
});
|
|
351
|
+
expect(wrapper.find(".main-navigation-details-summary").attributes("aria-label")).toBe("Untermenü UI Components");
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("falls back to name for a dropdown with no childLinksTitle", async () => {
|
|
355
|
+
const wrapper = await mountSuspended(ResponsiveHeader, {
|
|
356
|
+
props: { responsiveNavLinks: { firstNav: [{ name: "Services", childLinks: [{ name: "A", path: "/a" }] }] } },
|
|
357
|
+
});
|
|
358
|
+
const summary = wrapper.find(".main-navigation-details-summary");
|
|
359
|
+
expect(summary.text()).toBe("Services");
|
|
360
|
+
expect(summary.attributes("aria-label")).toBe("Services submenu");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("renders child links with duplicate names without key clashes", async () => {
|
|
364
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
365
|
+
const wrapper = await mountSuspended(ResponsiveHeader, {
|
|
366
|
+
props: {
|
|
367
|
+
responsiveNavLinks: {
|
|
368
|
+
firstNav: [
|
|
369
|
+
{ name: "Dup", childLinksTitle: "Dup", childLinks: [{ name: "Same", path: "/a" }, { name: "Same", path: "/b" }] },
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
expect(wrapper.findAll(".main-navigation-sub-nav-link")).toHaveLength(2);
|
|
375
|
+
expect(warn.mock.calls.some(([msg]) => String(msg).includes("Duplicate keys"))).toBe(false);
|
|
376
|
+
warn.mockRestore();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("shows the overflow button for any group key once an item is hidden", async () => {
|
|
380
|
+
const geometry = mockGeometry(150);
|
|
381
|
+
const wrapper = await mountSuspended(ResponsiveHeader, {
|
|
382
|
+
props: { responsiveNavLinks: { main: [{ name: "Home", path: "/" }, { name: "About", path: "/about" }] } },
|
|
383
|
+
});
|
|
384
|
+
await triggerGeometryPass();
|
|
385
|
+
expect(wrapper.find(".overflow-details").classes()).not.toContain("visually-hidden");
|
|
386
|
+
geometry.mockRestore();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("keeps the overflow button hidden when there are no nav items", async () => {
|
|
390
|
+
const wrapper = await mountSuspended(ResponsiveHeader, {
|
|
391
|
+
props: { responsiveNavLinks: {} },
|
|
392
|
+
});
|
|
393
|
+
await triggerGeometryPass();
|
|
394
|
+
expect(wrapper.find(".navigation").classes()).toContain("geometry-ready");
|
|
395
|
+
expect(wrapper.find(".overflow-details").classes()).toContain("visually-hidden");
|
|
311
396
|
});
|
|
312
397
|
|
|
313
398
|
it("renders correct HTML structure with default props", async () => {
|
|
@@ -4,11 +4,11 @@ exports[`ResponsiveHeader > renders correct HTML structure with default props 1`
|
|
|
4
4
|
"<div class="navigation loaded" role="banner">
|
|
5
5
|
<nav class="main-navigation is-animated" aria-label="Main navigation">
|
|
6
6
|
<ul class="main-navigation-list">
|
|
7
|
-
<li class="main-navigation-item visually-hidden is-active"
|
|
7
|
+
<li class="main-navigation-item visually-hidden is-active" data-group-key="firstNav" data-local-index="0"><a href="/" class="main-navigation-link">
|
|
8
8
|
<!--v-if--> Home
|
|
9
9
|
</a></li>
|
|
10
|
-
<li class="main-navigation-item visually-hidden"
|
|
11
|
-
<li class="main-navigation-item visually-hidden"
|
|
10
|
+
<li class="main-navigation-item visually-hidden" data-group-key="firstNav" data-local-index="1"><a href="/about" class="main-navigation-link has-icon"><span class="iconify i-mdi:home decorator-icon" aria-hidden="true"></span> About</a></li>
|
|
11
|
+
<li class="main-navigation-item visually-hidden" data-group-key="firstNav" data-local-index="2">
|
|
12
12
|
<details class="main-navigation-details" name="navigation-group">
|
|
13
13
|
<summary class="main-navigation-details-summary has-toggle-icon" aria-label="UI Components submenu"><span class="iconify i-mdi:chevron-down icon" aria-hidden="true"></span>
|
|
14
14
|
<!--v-if--> UI Components
|
|
@@ -23,7 +23,7 @@ exports[`ResponsiveHeader > renders correct HTML structure with default props 1`
|
|
|
23
23
|
</li>
|
|
24
24
|
</ul>
|
|
25
25
|
<ul class="main-navigation-list">
|
|
26
|
-
<li class="main-navigation-item visually-hidden"
|
|
26
|
+
<li class="main-navigation-item visually-hidden" data-group-key="secondNav" data-local-index="0"><a href="/contact" class="main-navigation-link">
|
|
27
27
|
<!--v-if--> Contact
|
|
28
28
|
</a></li>
|
|
29
29
|
</ul>
|
|
@@ -32,13 +32,13 @@ exports[`ResponsiveHeader > renders correct HTML structure with default props 1`
|
|
|
32
32
|
</nav>
|
|
33
33
|
<nav class="secondary-navigation" aria-label="Secondary navigation">
|
|
34
34
|
<details class="overflow-details visually-hidden" name="overflow-group">
|
|
35
|
-
<summary class="overflow-details-summary has-toggle-icon"><span class="iconify i-gravity-ui:ellipsis icon show" aria-hidden="true"></span><span class="iconify i-gravity-ui:bars icon" aria-hidden="true"></span></summary>
|
|
35
|
+
<summary class="overflow-details-summary has-toggle-icon" aria-label="More navigation"><span class="iconify i-gravity-ui:ellipsis icon show" aria-hidden="true"></span><span class="iconify i-gravity-ui:bars icon" aria-hidden="true"></span></summary>
|
|
36
36
|
<div class="overflow-details-nav" role="menu">
|
|
37
37
|
<div class="overflow-navigation-wrapper" role="menu" aria-label="Overflow navigation menu">
|
|
38
38
|
<ul class="overflow-navigation-list visible" style="--_overflow-navigation-list-min-width: 0px;" role="none">
|
|
39
|
-
<li class="overflow-navigation-item visible is-active"
|
|
40
|
-
<li class="overflow-navigation-item visible"
|
|
41
|
-
<li class="overflow-navigation-item visible"
|
|
39
|
+
<li class="overflow-navigation-item visible is-active" data-group-key="firstNav" data-local-index="0" role="none"><a href="/" class="overflow-navigation-link" role="menuitem"><span class="overflow-navigation-text">Home</span></a></li>
|
|
40
|
+
<li class="overflow-navigation-item visible" data-group-key="firstNav" data-local-index="1" role="none"><a href="/about" class="overflow-navigation-link" role="menuitem"><span class="overflow-navigation-text">About</span></a></li>
|
|
41
|
+
<li class="overflow-navigation-item visible" data-group-key="firstNav" data-local-index="2" role="none">
|
|
42
42
|
<div class="expanding-panel-classic overflow-navigation-details">
|
|
43
43
|
<details class="expanding-panel-classic-details" name="overflow-navigation-group">
|
|
44
44
|
<summary id="id-overflow-navigation-group-trigger" class="expanding-panel-classic-summary" aria-controls="id-overflow-navigation-group-content" aria-expanded="false">
|
|
@@ -61,7 +61,7 @@ exports[`ResponsiveHeader > renders correct HTML structure with default props 1`
|
|
|
61
61
|
</li>
|
|
62
62
|
</ul>
|
|
63
63
|
<ul class="overflow-navigation-list visible" style="--_overflow-navigation-list-min-width: 0px;" role="none">
|
|
64
|
-
<li class="overflow-navigation-item visible"
|
|
64
|
+
<li class="overflow-navigation-item visible" data-group-key="secondNav" data-local-index="0" role="none"><a href="/contact" class="overflow-navigation-link" role="menuitem"><span class="overflow-navigation-text">Contact</span></a></li>
|
|
65
65
|
</ul>
|
|
66
66
|
<div aria-hidden="true" class="overflow-nav-indicator-hovered"></div>
|
|
67
67
|
<div aria-hidden="true" class="overflow-nav-indicator-active"></div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"uuid":"21163df5-50d7-4cfc-ab56-45d835fb0c72"}
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
:main-nav-aria-label="mainNavAriaLabel"
|
|
21
21
|
:secondary-nav-aria-label="secondaryNavAriaLabel"
|
|
22
22
|
:overflow-menu-aria-label="overflowMenuAriaLabel"
|
|
23
|
+
:overflow-button-label="overflowButtonLabel"
|
|
24
|
+
:submenu-aria-label="submenuAriaLabel"
|
|
23
25
|
>
|
|
24
26
|
<template v-if="slots.secondaryNavigation" #secondaryNavigation>
|
|
25
27
|
<slot name="secondaryNavigation"></slot>
|
|
@@ -54,6 +56,10 @@ interface Props {
|
|
|
54
56
|
secondaryNavAriaLabel?: string;
|
|
55
57
|
/** Forwarded to ResponsiveHeader's overflowMenuAriaLabel — override for localisation. */
|
|
56
58
|
overflowMenuAriaLabel?: string;
|
|
59
|
+
/** Forwarded to ResponsiveHeader's overflowButtonLabel, the burger button's accessible name. */
|
|
60
|
+
overflowButtonLabel?: string;
|
|
61
|
+
/** Forwarded to ResponsiveHeader's submenuAriaLabel; {title} is replaced with the item's title. */
|
|
62
|
+
submenuAriaLabel?: string;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
withDefaults(defineProps<Props>(), {
|
|
@@ -73,6 +79,8 @@ withDefaults(defineProps<Props>(), {
|
|
|
73
79
|
mainNavAriaLabel: "Main navigation",
|
|
74
80
|
secondaryNavAriaLabel: "Secondary navigation",
|
|
75
81
|
overflowMenuAriaLabel: "Overflow navigation menu",
|
|
82
|
+
overflowButtonLabel: "More navigation",
|
|
83
|
+
submenuAriaLabel: "{title} submenu",
|
|
76
84
|
});
|
|
77
85
|
|
|
78
86
|
const slots = useSlots();
|
|
@@ -9,7 +9,7 @@ exports[`SiteHeader > renders correct HTML structure with default props 1`] = `
|
|
|
9
9
|
<nav class="skip-links__nav" aria-label="Skip navigation"><a href="#main-content" class="skip-links__link">Skip to main content</a><a href="#footer-content" class="skip-links__link">Skip to footer</a></nav>
|
|
10
10
|
</div>
|
|
11
11
|
</nav>
|
|
12
|
-
<div class="responsive-header-stub" main-nav-aria-label="Main navigation" secondary-nav-aria-label="Secondary navigation" overflow-menu-aria-label="Overflow navigation menu"></div>
|
|
12
|
+
<div class="responsive-header-stub" main-nav-aria-label="Main navigation" secondary-nav-aria-label="Secondary navigation" overflow-menu-aria-label="Overflow navigation menu" overflow-button-label="More navigation" submenu-aria-label="{title} submenu"></div>
|
|
13
13
|
</header>
|
|
14
14
|
</div>"
|
|
15
15
|
`;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import { defineComponent, h, nextTick, ref } from "vue";
|
|
4
|
+
import { useAnchoredPopover, type AnchoredPopoverSide } from "../useAnchoredPopover";
|
|
5
|
+
|
|
6
|
+
type Support = { popover: boolean; anchor: boolean };
|
|
7
|
+
|
|
8
|
+
const setSupport = ({ popover, anchor }: Support) => {
|
|
9
|
+
Object.defineProperty(HTMLElement.prototype, "showPopover", {
|
|
10
|
+
value: popover ? vi.fn() : undefined,
|
|
11
|
+
writable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
});
|
|
14
|
+
Object.defineProperty(HTMLElement.prototype, "hidePopover", {
|
|
15
|
+
value: popover ? vi.fn() : undefined,
|
|
16
|
+
writable: true,
|
|
17
|
+
configurable: true,
|
|
18
|
+
});
|
|
19
|
+
// happy-dom's CSS.supports can't be spied on, so swap the whole global and restore it in afterEach.
|
|
20
|
+
Object.defineProperty(globalThis, "CSS", { value: { supports: () => anchor }, writable: true, configurable: true });
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const originalCSS = globalThis.CSS;
|
|
24
|
+
|
|
25
|
+
const setViewport = (width: number, height: number) => {
|
|
26
|
+
Object.defineProperty(document.documentElement, "clientWidth", { value: width, configurable: true });
|
|
27
|
+
Object.defineProperty(document.documentElement, "clientHeight", { value: height, configurable: true });
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const mountHost = (side: AnchoredPopoverSide = "bottom") => {
|
|
31
|
+
const onOpen = vi.fn();
|
|
32
|
+
const mounted = mountSuspended(
|
|
33
|
+
defineComponent({
|
|
34
|
+
setup() {
|
|
35
|
+
const rootRef = ref<HTMLElement | null>(null);
|
|
36
|
+
const triggerRef = ref<HTMLElement | null>(null);
|
|
37
|
+
const popoverRef = ref<HTMLElement | null>(null);
|
|
38
|
+
return { rootRef, triggerRef, popoverRef, ...useAnchoredPopover({ rootRef, triggerRef, popoverRef, side, onOpen }) };
|
|
39
|
+
},
|
|
40
|
+
render() {
|
|
41
|
+
return h("div", [
|
|
42
|
+
h("div", { ref: "rootRef", class: "root" }, [
|
|
43
|
+
h("button", { ref: "triggerRef", class: "trigger", onClick: this.handleTriggerClick }),
|
|
44
|
+
h("div", { ref: "popoverRef", class: "popover" }),
|
|
45
|
+
]),
|
|
46
|
+
h("button", { class: "outside" }),
|
|
47
|
+
]);
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
{ attachTo: document.body }
|
|
51
|
+
);
|
|
52
|
+
return { mounted, onOpen };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const mockTriggerRect = (wrapper: Awaited<ReturnType<typeof mountHost>["mounted"]>, rect: Partial<DOMRect>) => {
|
|
56
|
+
const trigger = wrapper.find(".trigger").element as HTMLElement;
|
|
57
|
+
vi.spyOn(trigger, "getBoundingClientRect").mockReturnValue({ top: 0, bottom: 0, left: 0, right: 0, ...rect } as DOMRect);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const mockPopoverSize = (wrapper: Awaited<ReturnType<typeof mountHost>["mounted"]>, width: number, height: number) => {
|
|
61
|
+
const popover = wrapper.find(".popover").element;
|
|
62
|
+
Object.defineProperty(popover, "offsetWidth", { value: width, configurable: true });
|
|
63
|
+
Object.defineProperty(popover, "offsetHeight", { value: height, configurable: true });
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
describe("useAnchoredPopover", () => {
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
setViewport(1000, 800);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
vi.restoreAllMocks();
|
|
73
|
+
delete (HTMLElement.prototype as unknown as Record<string, unknown>)["showPopover"];
|
|
74
|
+
delete (HTMLElement.prototype as unknown as Record<string, unknown>)["hidePopover"];
|
|
75
|
+
delete (document.documentElement as unknown as Record<string, unknown>)["clientWidth"];
|
|
76
|
+
delete (document.documentElement as unknown as Record<string, unknown>)["clientHeight"];
|
|
77
|
+
Object.defineProperty(globalThis, "CSS", { value: originalCSS, writable: true, configurable: true });
|
|
78
|
+
document.body.innerHTML = "";
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("native Popover API and anchor positioning", () => {
|
|
82
|
+
beforeEach(() => setSupport({ popover: true, anchor: true }));
|
|
83
|
+
|
|
84
|
+
it("uses neither fallback", async () => {
|
|
85
|
+
const wrapper = await mountHost().mounted;
|
|
86
|
+
expect(wrapper.vm.usesFallbackPopover).toBe(false);
|
|
87
|
+
expect(wrapper.vm.needsPositioning).toBe(false);
|
|
88
|
+
expect(wrapper.vm.positionStyle).toBeUndefined();
|
|
89
|
+
expect(wrapper.vm.popoverPlacement).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("show and hide call the native methods", async () => {
|
|
93
|
+
const wrapper = await mountHost().mounted;
|
|
94
|
+
const popover = wrapper.find(".popover").element as HTMLElement;
|
|
95
|
+
wrapper.vm.show();
|
|
96
|
+
wrapper.vm.hide();
|
|
97
|
+
expect(popover.showPopover).toHaveBeenCalledOnce();
|
|
98
|
+
expect(popover.hidePopover).toHaveBeenCalledOnce();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("trigger click is left to popovertarget", async () => {
|
|
102
|
+
const wrapper = await mountHost().mounted;
|
|
103
|
+
await wrapper.find(".trigger").trigger("click");
|
|
104
|
+
expect(wrapper.vm.isOpen).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("tracks isOpen and calls onOpen from the toggle event", async () => {
|
|
108
|
+
const { mounted, onOpen } = mountHost();
|
|
109
|
+
const wrapper = await mounted;
|
|
110
|
+
wrapper.vm.handleToggle(Object.assign(new Event("toggle"), { newState: "open" }));
|
|
111
|
+
expect(wrapper.vm.isOpen).toBe(true);
|
|
112
|
+
expect(onOpen).toHaveBeenCalledOnce();
|
|
113
|
+
|
|
114
|
+
wrapper.vm.handleToggle(Object.assign(new Event("toggle"), { newState: "closed" }));
|
|
115
|
+
expect(wrapper.vm.isOpen).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("without the Popover API", () => {
|
|
120
|
+
beforeEach(() => setSupport({ popover: false, anchor: true }));
|
|
121
|
+
|
|
122
|
+
it("detects the fallback on mount", async () => {
|
|
123
|
+
const wrapper = await mountHost().mounted;
|
|
124
|
+
expect(wrapper.vm.usesFallbackPopover).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("toggles open and closed from the trigger, calling onOpen after render", async () => {
|
|
128
|
+
const { mounted, onOpen } = mountHost();
|
|
129
|
+
const wrapper = await mounted;
|
|
130
|
+
|
|
131
|
+
await wrapper.find(".trigger").trigger("click");
|
|
132
|
+
expect(wrapper.vm.isOpen).toBe(true);
|
|
133
|
+
await nextTick();
|
|
134
|
+
expect(onOpen).toHaveBeenCalledOnce();
|
|
135
|
+
|
|
136
|
+
await wrapper.find(".trigger").trigger("click");
|
|
137
|
+
expect(wrapper.vm.isOpen).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("closes on a pointerdown outside the root, not inside it", async () => {
|
|
141
|
+
const wrapper = await mountHost().mounted;
|
|
142
|
+
wrapper.vm.show();
|
|
143
|
+
await nextTick();
|
|
144
|
+
|
|
145
|
+
wrapper.find(".popover").element.dispatchEvent(new Event("pointerdown", { bubbles: true }));
|
|
146
|
+
expect(wrapper.vm.isOpen).toBe(true);
|
|
147
|
+
|
|
148
|
+
wrapper.find(".outside").element.dispatchEvent(new Event("pointerdown", { bubbles: true }));
|
|
149
|
+
expect(wrapper.vm.isOpen).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("closes on Escape and returns focus to the trigger", async () => {
|
|
153
|
+
const wrapper = await mountHost().mounted;
|
|
154
|
+
const focusSpy = vi.spyOn(wrapper.find(".trigger").element as HTMLElement, "focus");
|
|
155
|
+
wrapper.vm.show();
|
|
156
|
+
await nextTick();
|
|
157
|
+
|
|
158
|
+
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
|
|
159
|
+
expect(wrapper.vm.isOpen).toBe(false);
|
|
160
|
+
expect(focusSpy).toHaveBeenCalledOnce();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("stops listening once closed", async () => {
|
|
164
|
+
const wrapper = await mountHost().mounted;
|
|
165
|
+
const focusSpy = vi.spyOn(wrapper.find(".trigger").element as HTMLElement, "focus");
|
|
166
|
+
wrapper.vm.show();
|
|
167
|
+
await nextTick();
|
|
168
|
+
wrapper.vm.hide();
|
|
169
|
+
|
|
170
|
+
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
|
|
171
|
+
expect(focusSpy).not.toHaveBeenCalled();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("without anchor positioning", () => {
|
|
176
|
+
beforeEach(() => setSupport({ popover: true, anchor: false }));
|
|
177
|
+
|
|
178
|
+
const openWith = async (side: AnchoredPopoverSide, rect: Partial<DOMRect>, size = { width: 100, height: 200 }) => {
|
|
179
|
+
const wrapper = await mountHost(side).mounted;
|
|
180
|
+
mockTriggerRect(wrapper, rect);
|
|
181
|
+
mockPopoverSize(wrapper, size.width, size.height);
|
|
182
|
+
wrapper.vm.handleToggle(Object.assign(new Event("toggle"), { newState: "open" }));
|
|
183
|
+
return wrapper;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
it("writes every trigger edge, plain and measured from the viewport's far edge", async () => {
|
|
187
|
+
const wrapper = await mountHost().mounted;
|
|
188
|
+
mockTriggerRect(wrapper, { top: 100, bottom: 140, left: 50, right: 250 });
|
|
189
|
+
|
|
190
|
+
wrapper.vm.handleBeforeToggle(Object.assign(new Event("beforetoggle"), { newState: "open" }));
|
|
191
|
+
expect(wrapper.vm.needsPositioning).toBe(true);
|
|
192
|
+
expect(wrapper.vm.positionStyle).toEqual({
|
|
193
|
+
"--_anchor-top": "100px",
|
|
194
|
+
"--_anchor-bottom": "140px",
|
|
195
|
+
"--_anchor-left": "50px",
|
|
196
|
+
"--_anchor-right": "250px",
|
|
197
|
+
"--_anchor-top-inverse": "700px",
|
|
198
|
+
"--_anchor-bottom-inverse": "660px",
|
|
199
|
+
"--_anchor-left-inverse": "950px",
|
|
200
|
+
"--_anchor-right-inverse": "750px",
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("ignores beforetoggle when closing", async () => {
|
|
205
|
+
const wrapper = await mountHost().mounted;
|
|
206
|
+
mockTriggerRect(wrapper, { top: 100, bottom: 140, left: 50, right: 250 });
|
|
207
|
+
|
|
208
|
+
wrapper.vm.handleBeforeToggle(Object.assign(new Event("beforetoggle"), { newState: "closed" }));
|
|
209
|
+
expect(wrapper.vm.positionStyle).toMatchObject({ "--_anchor-top": "0px" });
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it.each([
|
|
213
|
+
["bottom", { top: 100, bottom: 140, left: 400, right: 450 }, "bottom"],
|
|
214
|
+
["bottom", { top: 700, bottom: 740, left: 400, right: 450 }, "top"],
|
|
215
|
+
["top", { top: 300, bottom: 340, left: 400, right: 450 }, "top"],
|
|
216
|
+
["top", { top: 100, bottom: 140, left: 400, right: 450 }, "bottom"],
|
|
217
|
+
["right", { top: 300, bottom: 340, left: 400, right: 450 }, "right"],
|
|
218
|
+
["right", { top: 300, bottom: 340, left: 850, right: 950 }, "left"],
|
|
219
|
+
["left", { top: 300, bottom: 340, left: 400, right: 450 }, "left"],
|
|
220
|
+
["left", { top: 300, bottom: 340, left: 50, right: 100 }, "right"],
|
|
221
|
+
] as const)("prefers %s and resolves to %s for %o", async (side, rect, expected) => {
|
|
222
|
+
const wrapper = await openWith(side, rect);
|
|
223
|
+
expect(wrapper.vm.popoverPlacement).toBe(expected);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("keeps the preferred side when neither side has room", async () => {
|
|
227
|
+
const wrapper = await openWith("bottom", { top: 100, bottom: 140, left: 50, right: 250 }, { width: 100, height: 900 });
|
|
228
|
+
expect(wrapper.vm.popoverPlacement).toBe("bottom");
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|