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,69 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import { defineComponent, h, nextTick, ref } from "vue";
|
|
4
|
+
import { useTooltipsGuide } from "../useTooltips";
|
|
5
|
+
import DisplayTooltipDefined from "~/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue";
|
|
6
|
+
|
|
7
|
+
const flush = async () => {
|
|
8
|
+
for (let i = 0; i < 5; i++) await nextTick();
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const mountGuide = () =>
|
|
12
|
+
mountSuspended(
|
|
13
|
+
defineComponent({
|
|
14
|
+
setup() {
|
|
15
|
+
const containerRef = ref<HTMLElement | null>(null);
|
|
16
|
+
const guide = useTooltipsGuide(containerRef, { autoStart: false, startDelay: 0 });
|
|
17
|
+
return { containerRef, guide };
|
|
18
|
+
},
|
|
19
|
+
render() {
|
|
20
|
+
return h("div", { ref: "containerRef" }, [
|
|
21
|
+
h(DisplayTooltipDefined, { tooltipId: "one", contentText: { tooltipTitle: { tag: "h4", text: "One" } } }),
|
|
22
|
+
h(DisplayTooltipDefined, { tooltipId: "two", contentText: { tooltipTitle: { tag: "h4", text: "Two" } } }),
|
|
23
|
+
]);
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
{ attachTo: document.body }
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
describe("useTooltipsGuide without the Popover API", () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
Object.defineProperty(HTMLElement.prototype, "showPopover", { value: undefined, writable: true, configurable: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
delete (HTMLElement.prototype as unknown as Record<string, unknown>)["showPopover"];
|
|
36
|
+
document.body.innerHTML = "";
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("steps through each tooltip as its close button is clicked", async () => {
|
|
40
|
+
const wrapper = await mountGuide();
|
|
41
|
+
const vm = wrapper.vm as unknown as { guide: ReturnType<typeof useTooltipsGuide> };
|
|
42
|
+
const popovers = () => wrapper.findAll(".display-tooltip-popover");
|
|
43
|
+
|
|
44
|
+
vm.guide.initializePopovers();
|
|
45
|
+
void vm.guide.startGuide();
|
|
46
|
+
await flush();
|
|
47
|
+
expect(popovers()[0]!.classes()).toContain("display-tooltip-popover-open");
|
|
48
|
+
|
|
49
|
+
await wrapper.findAll(".display-tooltip-close-button")[0]!.trigger("click");
|
|
50
|
+
await flush();
|
|
51
|
+
expect(popovers()[0]!.classes()).not.toContain("display-tooltip-popover-open");
|
|
52
|
+
expect(popovers()[1]!.classes()).toContain("display-tooltip-popover-open");
|
|
53
|
+
expect(vm.guide.currentTooltipIndex.value).toBe(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("stopGuide closes the open tooltip without calling Popover API methods", async () => {
|
|
57
|
+
const wrapper = await mountGuide();
|
|
58
|
+
const vm = wrapper.vm as unknown as { guide: ReturnType<typeof useTooltipsGuide> };
|
|
59
|
+
|
|
60
|
+
vm.guide.initializePopovers();
|
|
61
|
+
void vm.guide.startGuide();
|
|
62
|
+
await flush();
|
|
63
|
+
|
|
64
|
+
expect(() => vm.guide.stopGuide()).not.toThrow();
|
|
65
|
+
await flush();
|
|
66
|
+
expect(wrapper.find(".display-tooltip-popover-open").exists()).toBe(false);
|
|
67
|
+
expect(vm.guide.isGuideRunning.value).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { MaybeRefOrGetter, Ref } from "vue";
|
|
2
|
+
|
|
3
|
+
interface AnchoredPopoverOptions {
|
|
4
|
+
rootRef: Readonly<Ref<HTMLElement | null>>;
|
|
5
|
+
triggerRef: Readonly<Ref<HTMLElement | null>>;
|
|
6
|
+
popoverRef: Readonly<Ref<HTMLElement | null>>;
|
|
7
|
+
side?: MaybeRefOrGetter<AnchoredPopoverSide>;
|
|
8
|
+
onOpen?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type AnchoredPopoverSide = "top" | "right" | "bottom" | "left";
|
|
12
|
+
|
|
13
|
+
type PopoverToggleEvent = Event & { newState?: string };
|
|
14
|
+
|
|
15
|
+
const OPPOSITE_SIDE: Record<AnchoredPopoverSide, AnchoredPopoverSide> = {
|
|
16
|
+
top: "bottom",
|
|
17
|
+
bottom: "top",
|
|
18
|
+
left: "right",
|
|
19
|
+
right: "left",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Native popover + anchor positioning where supported; JS positioning without anchor positioning
|
|
23
|
+
// (Safari 17-18); JS open/close, outside click and Escape without the Popover API (Safari 16).
|
|
24
|
+
export function useAnchoredPopover({ rootRef, triggerRef, popoverRef, side = "bottom", onOpen }: AnchoredPopoverOptions) {
|
|
25
|
+
const isOpen = ref(false);
|
|
26
|
+
const usesFallbackPopover = ref(false);
|
|
27
|
+
const needsPositioning = ref(false);
|
|
28
|
+
const placement = ref<AnchoredPopoverSide>(toValue(side));
|
|
29
|
+
const anchor = ref({ top: 0, bottom: 0, left: 0, right: 0, viewportWidth: 0, viewportHeight: 0 });
|
|
30
|
+
|
|
31
|
+
// `--_anchor-{edge}` mirrors anchor({edge}) for top/left; `-inverse` is the same edge measured
|
|
32
|
+
// from the viewport's bottom/right, for bottom/right.
|
|
33
|
+
const positionStyle = computed(() => {
|
|
34
|
+
if (!needsPositioning.value) return undefined;
|
|
35
|
+
const { top, bottom, left, right, viewportWidth, viewportHeight } = anchor.value;
|
|
36
|
+
return {
|
|
37
|
+
"--_anchor-top": `${top}px`,
|
|
38
|
+
"--_anchor-bottom": `${bottom}px`,
|
|
39
|
+
"--_anchor-left": `${left}px`,
|
|
40
|
+
"--_anchor-right": `${right}px`,
|
|
41
|
+
"--_anchor-top-inverse": `${viewportHeight - top}px`,
|
|
42
|
+
"--_anchor-bottom-inverse": `${viewportHeight - bottom}px`,
|
|
43
|
+
"--_anchor-left-inverse": `${viewportWidth - left}px`,
|
|
44
|
+
"--_anchor-right-inverse": `${viewportWidth - right}px`,
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const popoverPlacement = computed(() => (needsPositioning.value ? placement.value : undefined));
|
|
49
|
+
|
|
50
|
+
const updatePosition = () => {
|
|
51
|
+
const trigger = triggerRef.value;
|
|
52
|
+
if (!trigger) return;
|
|
53
|
+
const rect = trigger.getBoundingClientRect();
|
|
54
|
+
const viewportWidth = document.documentElement.clientWidth;
|
|
55
|
+
const viewportHeight = document.documentElement.clientHeight;
|
|
56
|
+
const width = popoverRef.value?.offsetWidth ?? 0;
|
|
57
|
+
const height = popoverRef.value?.offsetHeight ?? 0;
|
|
58
|
+
|
|
59
|
+
const fits: Record<AnchoredPopoverSide, boolean> = {
|
|
60
|
+
bottom: rect.bottom + height <= viewportHeight,
|
|
61
|
+
top: rect.top - height >= 0,
|
|
62
|
+
right: rect.right + width <= viewportWidth,
|
|
63
|
+
left: rect.left - width >= 0,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
anchor.value = { top: rect.top, bottom: rect.bottom, left: rect.left, right: rect.right, viewportWidth, viewportHeight };
|
|
67
|
+
const preferred = toValue(side);
|
|
68
|
+
placement.value = fits[preferred] || !fits[OPPOSITE_SIDE[preferred]] ? preferred : OPPOSITE_SIDE[preferred];
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
let frame = 0;
|
|
72
|
+
const schedulePositionUpdate = () => {
|
|
73
|
+
if (frame) return;
|
|
74
|
+
frame = requestAnimationFrame(() => {
|
|
75
|
+
frame = 0;
|
|
76
|
+
updatePosition();
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleDocumentPointerdown = (event: PointerEvent) => {
|
|
81
|
+
if (!rootRef.value?.contains(event.target as Node)) hide();
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleDocumentKeydown = (event: KeyboardEvent) => {
|
|
85
|
+
if (event.key !== "Escape") return;
|
|
86
|
+
hide();
|
|
87
|
+
triggerRef.value?.focus();
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const addListeners = () => {
|
|
91
|
+
if (needsPositioning.value) {
|
|
92
|
+
window.addEventListener("scroll", schedulePositionUpdate, { capture: true, passive: true });
|
|
93
|
+
window.addEventListener("resize", schedulePositionUpdate, { passive: true });
|
|
94
|
+
}
|
|
95
|
+
if (usesFallbackPopover.value) {
|
|
96
|
+
document.addEventListener("pointerdown", handleDocumentPointerdown);
|
|
97
|
+
document.addEventListener("keydown", handleDocumentKeydown);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const removeListeners = () => {
|
|
102
|
+
window.removeEventListener("scroll", schedulePositionUpdate, { capture: true });
|
|
103
|
+
window.removeEventListener("resize", schedulePositionUpdate);
|
|
104
|
+
document.removeEventListener("pointerdown", handleDocumentPointerdown);
|
|
105
|
+
document.removeEventListener("keydown", handleDocumentKeydown);
|
|
106
|
+
if (frame) cancelAnimationFrame(frame);
|
|
107
|
+
frame = 0;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const afterOpen = () => {
|
|
111
|
+
if (needsPositioning.value) updatePosition();
|
|
112
|
+
addListeners();
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const show = () => {
|
|
116
|
+
if (!usesFallbackPopover.value) {
|
|
117
|
+
popoverRef.value?.showPopover();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (isOpen.value) return;
|
|
121
|
+
if (needsPositioning.value) updatePosition();
|
|
122
|
+
isOpen.value = true;
|
|
123
|
+
nextTick(() => {
|
|
124
|
+
afterOpen();
|
|
125
|
+
onOpen?.();
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const hide = () => {
|
|
130
|
+
if (!usesFallbackPopover.value) {
|
|
131
|
+
popoverRef.value?.hidePopover();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (!isOpen.value) return;
|
|
135
|
+
// Matches native popovers, which return focus to the invoker when focus was inside.
|
|
136
|
+
if (popoverRef.value?.contains(document.activeElement)) triggerRef.value?.focus();
|
|
137
|
+
isOpen.value = false;
|
|
138
|
+
removeListeners();
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/** Bound to the trigger's click. A no-op where `popovertarget` already toggles natively. */
|
|
142
|
+
const handleTriggerClick = () => {
|
|
143
|
+
if (!usesFallbackPopover.value) return;
|
|
144
|
+
if (isOpen.value) hide();
|
|
145
|
+
else show();
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** Bound to the popover's `beforetoggle`, so the first open frame is already in place. */
|
|
149
|
+
const handleBeforeToggle = (event: Event) => {
|
|
150
|
+
if (needsPositioning.value && (event as PopoverToggleEvent).newState === "open") updatePosition();
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** Bound to the popover's `toggle`. */
|
|
154
|
+
const handleToggle = (event: Event) => {
|
|
155
|
+
isOpen.value = (event as PopoverToggleEvent).newState === "open";
|
|
156
|
+
if (isOpen.value) {
|
|
157
|
+
afterOpen();
|
|
158
|
+
onOpen?.();
|
|
159
|
+
} else {
|
|
160
|
+
removeListeners();
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
onMounted(() => {
|
|
165
|
+
usesFallbackPopover.value = typeof HTMLElement.prototype.showPopover !== "function";
|
|
166
|
+
needsPositioning.value = typeof CSS !== "undefined" && typeof CSS.supports === "function" && !CSS.supports("anchor-name: --a");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
onBeforeUnmount(removeListeners);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
isOpen,
|
|
173
|
+
usesFallbackPopover,
|
|
174
|
+
needsPositioning,
|
|
175
|
+
positionStyle,
|
|
176
|
+
popoverPlacement,
|
|
177
|
+
show,
|
|
178
|
+
hide,
|
|
179
|
+
handleTriggerClick,
|
|
180
|
+
handleBeforeToggle,
|
|
181
|
+
handleToggle,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -32,23 +32,40 @@ export const useTooltipsGuide = (
|
|
|
32
32
|
})
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
const findTrigger = (popover: HTMLElement) =>
|
|
36
|
+
document.querySelector<HTMLElement>(`[popovertarget="${popover.id}"][popovertargetaction="toggle"]`)
|
|
37
|
+
|
|
38
|
+
// Without the Popover API (Safari 16) there's no togglePopover() and :popover-open throws, so
|
|
39
|
+
// tooltips are driven through their trigger, whose aria-expanded tracks the open state everywhere.
|
|
40
|
+
const closePopover = (popover: HTMLElement) => {
|
|
41
|
+
const trigger = findTrigger(popover)
|
|
42
|
+
if (trigger?.getAttribute("aria-expanded") === "true") {
|
|
43
|
+
trigger.click()
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
if (typeof popover.hidePopover !== "function") return
|
|
47
|
+
try {
|
|
48
|
+
if (popover.matches(":popover-open")) popover.hidePopover()
|
|
49
|
+
} catch {
|
|
50
|
+
// :popover-open unsupported
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
35
54
|
/**
|
|
36
55
|
* Show a tooltip and wait for user dismissal
|
|
37
56
|
*/
|
|
38
57
|
const showTooltipAndWaitForDismiss = (popover: HTMLElement): Promise<void> => {
|
|
39
58
|
return new Promise((resolve) => {
|
|
40
|
-
|
|
41
|
-
const popoverId = popover.id
|
|
42
|
-
const triggerButton = document.querySelector<HTMLElement>(
|
|
43
|
-
`[popovertarget="${popoverId}"][popovertargetaction="toggle"]`
|
|
44
|
-
)
|
|
59
|
+
const triggerButton = findTrigger(popover)
|
|
45
60
|
|
|
46
61
|
// Use the trigger button to show the popover to maintain proper anchor relationship
|
|
47
62
|
if (triggerButton) {
|
|
48
63
|
triggerButton.click()
|
|
49
|
-
} else {
|
|
50
|
-
// Fallback if no trigger button found
|
|
64
|
+
} else if (typeof popover.togglePopover === "function") {
|
|
51
65
|
popover.togglePopover(true)
|
|
66
|
+
} else {
|
|
67
|
+
resolve()
|
|
68
|
+
return
|
|
52
69
|
}
|
|
53
70
|
|
|
54
71
|
// Find the close button within this popover
|
|
@@ -62,10 +79,18 @@ export const useTooltipsGuide = (
|
|
|
62
79
|
}
|
|
63
80
|
|
|
64
81
|
closeButton.addEventListener("click", handleClose)
|
|
82
|
+
} else if (triggerButton) {
|
|
83
|
+
// No close button: resolve once the trigger reports the tooltip closed
|
|
84
|
+
const observer = new MutationObserver(() => {
|
|
85
|
+
if (triggerButton.getAttribute("aria-expanded") === "false") {
|
|
86
|
+
observer.disconnect()
|
|
87
|
+
resolve()
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
observer.observe(triggerButton, { attributes: true, attributeFilter: ["aria-expanded"] })
|
|
65
91
|
} else {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (!popover.matches(":popover-open")) {
|
|
92
|
+
const handleToggle = (event: Event) => {
|
|
93
|
+
if ((event as Event & { newState?: string }).newState === "closed") {
|
|
69
94
|
popover.removeEventListener("toggle", handleToggle)
|
|
70
95
|
resolve()
|
|
71
96
|
}
|
|
@@ -105,11 +130,7 @@ export const useTooltipsGuide = (
|
|
|
105
130
|
if (isGuideRunning.value) return
|
|
106
131
|
|
|
107
132
|
// Close any currently open popovers
|
|
108
|
-
popovers.forEach(
|
|
109
|
-
if (popover.matches(":popover-open")) {
|
|
110
|
-
popover.togglePopover(false)
|
|
111
|
-
}
|
|
112
|
-
})
|
|
133
|
+
popovers.forEach(closePopover)
|
|
113
134
|
|
|
114
135
|
// Reset state and start the guide
|
|
115
136
|
autoRunGuide.value = true
|
|
@@ -123,11 +144,7 @@ export const useTooltipsGuide = (
|
|
|
123
144
|
if (!isGuideRunning.value) return
|
|
124
145
|
|
|
125
146
|
// Close any currently open popovers
|
|
126
|
-
popovers.forEach(
|
|
127
|
-
if (popover.matches(":popover-open")) {
|
|
128
|
-
popover.togglePopover(false)
|
|
129
|
-
}
|
|
130
|
-
})
|
|
147
|
+
popovers.forEach(closePopover)
|
|
131
148
|
|
|
132
149
|
isGuideRunning.value = false
|
|
133
150
|
autoRunGuide.value = false
|
|
@@ -8,8 +8,29 @@ import type {
|
|
|
8
8
|
|
|
9
9
|
const SCHEMA_DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// "HH:MM" from 00:00 to 23:59, else null so the caller can show the raw value instead of throwing
|
|
12
|
+
export const parseOpeningTime = (time: string): { hours: number; minutes: number } | null => {
|
|
13
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(time.trim());
|
|
14
|
+
if (!match) return null;
|
|
15
|
+
const hours = Number(match[1]);
|
|
16
|
+
const minutes = Number(match[2]);
|
|
17
|
+
return hours < 24 && minutes < 60 ? { hours, minutes } : null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// A real calendar date in "YYYY-MM-DD" form, as a UTC midnight Date, else null
|
|
21
|
+
export const parseOpeningDate = (iso: string): Date | null => {
|
|
22
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso.trim());
|
|
23
|
+
if (!match) return null;
|
|
24
|
+
const [y, m, d] = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
25
|
+
const date = new Date(Date.UTC(y, m - 1, d));
|
|
26
|
+
return date.getUTCFullYear() === y && date.getUTCMonth() === m - 1 && date.getUTCDate() === d ? date : null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// "open" with no sessions has nothing to show, so it's treated as closed
|
|
30
|
+
export const resolveOpeningStatus = (entry: { status?: OpeningStatus; sessions?: OpeningSession[] }): OpeningStatus => {
|
|
31
|
+
const status = entry.status ?? "open";
|
|
32
|
+
return status === "open" && !entry.sessions?.length ? "closed" : status;
|
|
33
|
+
};
|
|
13
34
|
|
|
14
35
|
const specsFor = (
|
|
15
36
|
entry: { status?: OpeningStatus; sessions?: OpeningSession[] },
|