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
|
@@ -1,7 +1,21 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import { computed } from "vue";
|
|
1
3
|
import UiBlockDecorated from "../UiBlockDecorated.vue";
|
|
2
|
-
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
interface UiBlockDecoratedStoryArgs {
|
|
6
|
+
tag: "div" | "p" | "span" | "section" | "article" | "aside" | "header" | "footer" | "main" | "nav" | "ul" | "ol";
|
|
7
|
+
borderStrength: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
8
|
+
shadowStrength: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
9
|
+
innerShadowStrength: 0 | 1 | 2 | 3 | 4;
|
|
10
|
+
label: string;
|
|
11
|
+
borderColour: string;
|
|
12
|
+
shadowColour: string;
|
|
13
|
+
innerShadowColour: string;
|
|
14
|
+
background: string;
|
|
15
|
+
textColour: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default {
|
|
5
19
|
title: "Atoms/UiBlockDecorated",
|
|
6
20
|
component: UiBlockDecorated,
|
|
7
21
|
argTypes: {
|
|
@@ -29,120 +43,134 @@ const meta: Meta<typeof UiBlockDecorated> = {
|
|
|
29
43
|
description: "Inner shadow strength level (0 = none)",
|
|
30
44
|
table: { category: "Basic" },
|
|
31
45
|
},
|
|
32
|
-
|
|
33
|
-
|
|
46
|
+
label: { control: "text", description: "Story only: block text", table: { category: "Story" } },
|
|
47
|
+
borderColour: {
|
|
48
|
+
control: "color",
|
|
49
|
+
description: "--ui-block-decorated-border-colour (empty = var(--theme-border))",
|
|
50
|
+
table: { category: "Tokens" },
|
|
51
|
+
},
|
|
52
|
+
shadowColour: {
|
|
53
|
+
control: "color",
|
|
54
|
+
description: "--ui-block-decorated-shadow-colour (empty = black). Each strength level mixes in its own opacity.",
|
|
55
|
+
table: { category: "Tokens" },
|
|
56
|
+
},
|
|
57
|
+
innerShadowColour: {
|
|
58
|
+
control: "color",
|
|
59
|
+
description: "--ui-block-decorated-inner-shadow-colour (empty = the shadow colour)",
|
|
60
|
+
table: { category: "Tokens" },
|
|
61
|
+
},
|
|
62
|
+
background: {
|
|
63
|
+
control: "color",
|
|
64
|
+
description: "Story only: block background, for checking contrast (empty = none)",
|
|
65
|
+
table: { category: "Story" },
|
|
66
|
+
},
|
|
67
|
+
textColour: {
|
|
68
|
+
control: "color",
|
|
69
|
+
description: "Story only: block text colour (empty = inherited)",
|
|
70
|
+
table: { category: "Story" },
|
|
34
71
|
},
|
|
35
72
|
},
|
|
73
|
+
args: {
|
|
74
|
+
tag: "div",
|
|
75
|
+
borderStrength: 0,
|
|
76
|
+
shadowStrength: 0,
|
|
77
|
+
innerShadowStrength: 0,
|
|
78
|
+
label: "No decoration",
|
|
79
|
+
borderColour: "",
|
|
80
|
+
shadowColour: "",
|
|
81
|
+
innerShadowColour: "",
|
|
82
|
+
background: "",
|
|
83
|
+
textColour: "",
|
|
84
|
+
},
|
|
36
85
|
parameters: {
|
|
37
86
|
docs: {
|
|
38
87
|
description: {
|
|
39
88
|
component:
|
|
40
|
-
"A plain block wrapper that applies independent border / drop-shadow / inner-shadow strength levels via CSS custom properties (see CONSUMER-STYLING.md).",
|
|
89
|
+
"A plain block wrapper that applies independent border / drop-shadow / inner-shadow strength levels via CSS custom properties (see CONSUMER-STYLING.md). Use the colour pickers under Tokens to recolour every strength level at once.",
|
|
41
90
|
},
|
|
42
91
|
},
|
|
43
92
|
},
|
|
44
|
-
}
|
|
93
|
+
} as Meta<UiBlockDecoratedStoryArgs>;
|
|
45
94
|
|
|
46
|
-
|
|
47
|
-
|
|
95
|
+
const Template: StoryFn<UiBlockDecoratedStoryArgs> = (args) => ({
|
|
96
|
+
components: { UiBlockDecorated },
|
|
97
|
+
setup() {
|
|
98
|
+
const blockStyle = computed(() => ({
|
|
99
|
+
padding: "2rem",
|
|
100
|
+
...(args.borderColour ? { "--ui-block-decorated-border-colour": args.borderColour } : {}),
|
|
101
|
+
...(args.shadowColour ? { "--ui-block-decorated-shadow-colour": args.shadowColour } : {}),
|
|
102
|
+
...(args.innerShadowColour ? { "--ui-block-decorated-inner-shadow-colour": args.innerShadowColour } : {}),
|
|
103
|
+
...(args.background ? { background: args.background } : {}),
|
|
104
|
+
...(args.textColour ? { color: args.textColour } : {}),
|
|
105
|
+
}));
|
|
106
|
+
return { args, blockStyle };
|
|
107
|
+
},
|
|
108
|
+
template: `
|
|
109
|
+
<UiBlockDecorated
|
|
110
|
+
:tag="args.tag"
|
|
111
|
+
:border-strength="args.borderStrength"
|
|
112
|
+
:shadow-strength="args.shadowStrength"
|
|
113
|
+
:inner-shadow-strength="args.innerShadowStrength"
|
|
114
|
+
:style="blockStyle"
|
|
115
|
+
>
|
|
116
|
+
{{ args.label }}
|
|
117
|
+
</UiBlockDecorated>
|
|
118
|
+
`,
|
|
119
|
+
});
|
|
48
120
|
|
|
49
|
-
const
|
|
121
|
+
export const Default = Template.bind({});
|
|
50
122
|
|
|
51
|
-
export const
|
|
52
|
-
|
|
53
|
-
tag: "div",
|
|
54
|
-
},
|
|
55
|
-
render: (args) => ({
|
|
56
|
-
components: { UiBlockDecorated },
|
|
57
|
-
setup() {
|
|
58
|
-
return { args, blockStyle };
|
|
59
|
-
},
|
|
60
|
-
template: `<UiBlockDecorated v-bind="args" :style="blockStyle">No decoration</UiBlockDecorated>`,
|
|
61
|
-
}),
|
|
62
|
-
};
|
|
123
|
+
export const Border = Template.bind({});
|
|
124
|
+
Border.args = { borderStrength: 3, label: "Border strength 3" };
|
|
63
125
|
|
|
64
|
-
export const
|
|
65
|
-
|
|
66
|
-
tag: "div",
|
|
67
|
-
borderStrength: 3,
|
|
68
|
-
},
|
|
69
|
-
render: (args) => ({
|
|
70
|
-
components: { UiBlockDecorated },
|
|
71
|
-
setup() {
|
|
72
|
-
return { args, blockStyle };
|
|
73
|
-
},
|
|
74
|
-
template: `<UiBlockDecorated v-bind="args" :style="blockStyle">Border strength 3</UiBlockDecorated>`,
|
|
75
|
-
}),
|
|
76
|
-
};
|
|
126
|
+
export const Shadow = Template.bind({});
|
|
127
|
+
Shadow.args = { shadowStrength: 4, label: "Shadow strength 4" };
|
|
77
128
|
|
|
78
|
-
export const
|
|
79
|
-
|
|
80
|
-
tag: "div",
|
|
81
|
-
shadowStrength: 4,
|
|
82
|
-
},
|
|
83
|
-
render: (args) => ({
|
|
84
|
-
components: { UiBlockDecorated },
|
|
85
|
-
setup() {
|
|
86
|
-
return { args, blockStyle };
|
|
87
|
-
},
|
|
88
|
-
template: `<UiBlockDecorated v-bind="args" :style="blockStyle">Shadow strength 4</UiBlockDecorated>`,
|
|
89
|
-
}),
|
|
90
|
-
};
|
|
129
|
+
export const InnerShadow = Template.bind({});
|
|
130
|
+
InnerShadow.args = { innerShadowStrength: 3, label: "Inner shadow strength 3" };
|
|
91
131
|
|
|
92
|
-
export const
|
|
93
|
-
|
|
94
|
-
tag: "div",
|
|
95
|
-
innerShadowStrength: 3,
|
|
96
|
-
},
|
|
97
|
-
render: (args) => ({
|
|
98
|
-
components: { UiBlockDecorated },
|
|
99
|
-
setup() {
|
|
100
|
-
return { args, blockStyle };
|
|
101
|
-
},
|
|
102
|
-
template: `<UiBlockDecorated v-bind="args" :style="blockStyle">Inner shadow strength 3</UiBlockDecorated>`,
|
|
103
|
-
}),
|
|
104
|
-
};
|
|
132
|
+
export const AllCombined = Template.bind({});
|
|
133
|
+
AllCombined.args = { borderStrength: 2, shadowStrength: 3, innerShadowStrength: 2, label: "All three combined" };
|
|
105
134
|
|
|
106
|
-
export const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
135
|
+
export const BrandColours = Template.bind({});
|
|
136
|
+
BrandColours.args = {
|
|
137
|
+
borderStrength: 2,
|
|
138
|
+
shadowStrength: 4,
|
|
139
|
+
innerShadowStrength: 2,
|
|
140
|
+
label: "Brand colours via the colour tokens",
|
|
141
|
+
borderColour: "#7c3aed",
|
|
142
|
+
shadowColour: "#7c3aed",
|
|
143
|
+
background: "#faf5ff",
|
|
144
|
+
textColour: "#3b0764",
|
|
145
|
+
};
|
|
146
|
+
BrandColours.parameters = {
|
|
147
|
+
docs: {
|
|
148
|
+
description: {
|
|
149
|
+
story:
|
|
150
|
+
"`--ui-block-decorated-border-colour` and `--ui-block-decorated-shadow-colour` recolour every strength level; each shadow level keeps its own opacity. Background and text colour are story-only, for checking contrast.",
|
|
117
151
|
},
|
|
118
|
-
|
|
119
|
-
}),
|
|
152
|
+
},
|
|
120
153
|
};
|
|
121
154
|
|
|
122
|
-
export const CustomTokens:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
155
|
+
export const CustomTokens: StoryFn<UiBlockDecoratedStoryArgs> = (args) => ({
|
|
156
|
+
components: { UiBlockDecorated },
|
|
157
|
+
setup() {
|
|
158
|
+
return { args };
|
|
126
159
|
},
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
parameters: {
|
|
142
|
-
docs: {
|
|
143
|
-
description: {
|
|
144
|
-
story: "Overriding a strength level's token via an inline `--ui-block-decorated-*` custom property.",
|
|
145
|
-
},
|
|
160
|
+
template: `
|
|
161
|
+
<UiBlockDecorated
|
|
162
|
+
:shadow-strength="3"
|
|
163
|
+
style="padding: 2rem; --ui-block-decorated-shadow-3: 0 6px 12px rgba(0, 0, 0, 0.25);"
|
|
164
|
+
>
|
|
165
|
+
Custom shadow-3 token
|
|
166
|
+
</UiBlockDecorated>
|
|
167
|
+
`,
|
|
168
|
+
});
|
|
169
|
+
CustomTokens.parameters = {
|
|
170
|
+
controls: { disable: true },
|
|
171
|
+
docs: {
|
|
172
|
+
description: {
|
|
173
|
+
story: "Overriding a whole strength level's token via an inline `--ui-block-decorated-*` custom property.",
|
|
146
174
|
},
|
|
147
175
|
},
|
|
148
176
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="action-menu" :class="[elementClasses]" :style="`--_anchor-name: ${anchorName}`">
|
|
2
|
+
<div ref="rootRef" class="action-menu" :class="[elementClasses]" :style="`--_anchor-name: ${anchorName}`">
|
|
3
3
|
<button
|
|
4
4
|
ref="triggerRef"
|
|
5
5
|
:popovertarget="menuId"
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
class="action-menu-trigger"
|
|
9
9
|
:aria-label="label"
|
|
10
10
|
aria-haspopup="menu"
|
|
11
|
+
:aria-expanded="isOpen"
|
|
12
|
+
@click="handleTriggerClick"
|
|
11
13
|
>
|
|
12
14
|
<Icon :name="triggerIcon" class="action-menu-trigger-icon" aria-hidden="true" />
|
|
13
15
|
</button>
|
|
@@ -17,6 +19,10 @@
|
|
|
17
19
|
ref="popoverRef"
|
|
18
20
|
popover
|
|
19
21
|
class="action-menu-popover"
|
|
22
|
+
:class="{ 'action-menu-popover-open': usesFallbackPopover && isOpen }"
|
|
23
|
+
:style="positionStyle"
|
|
24
|
+
:data-placement="popoverPlacement"
|
|
25
|
+
@beforetoggle="handleBeforeToggle"
|
|
20
26
|
@toggle="handleToggle"
|
|
21
27
|
@keydown="handleKeydown"
|
|
22
28
|
>
|
|
@@ -49,6 +55,7 @@ const id = useId();
|
|
|
49
55
|
const menuId = `action-menu-${id}`;
|
|
50
56
|
const anchorName = `--action-menu-anchor-${id}`;
|
|
51
57
|
|
|
58
|
+
const rootRef = ref<HTMLDivElement | null>(null);
|
|
52
59
|
const triggerRef = ref<HTMLButtonElement | null>(null);
|
|
53
60
|
const popoverRef = ref<HTMLDivElement | null>(null);
|
|
54
61
|
|
|
@@ -56,23 +63,23 @@ const popoverRef = ref<HTMLDivElement | null>(null);
|
|
|
56
63
|
const getMenuItems = (): HTMLElement[] =>
|
|
57
64
|
Array.from(popoverRef.value?.querySelectorAll<HTMLElement>('[role="menuitem"]') ?? []);
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
const {
|
|
67
|
+
isOpen,
|
|
68
|
+
usesFallbackPopover,
|
|
69
|
+
positionStyle,
|
|
70
|
+
popoverPlacement,
|
|
71
|
+
hide,
|
|
72
|
+
handleTriggerClick,
|
|
73
|
+
handleBeforeToggle,
|
|
74
|
+
handleToggle,
|
|
75
|
+
} = useAnchoredPopover({ rootRef, triggerRef, popoverRef, onOpen: () => getMenuItems()[0]?.focus() });
|
|
76
|
+
|
|
77
|
+
/** Close the menu and return focus to the trigger. Called on item click. */
|
|
64
78
|
const closeMenu = () => {
|
|
65
|
-
|
|
79
|
+
hide();
|
|
66
80
|
triggerRef.value?.focus();
|
|
67
81
|
};
|
|
68
82
|
|
|
69
|
-
const handleToggle = (event: Event) => {
|
|
70
|
-
const toggleEvent = event as ToggleEvent;
|
|
71
|
-
if (toggleEvent.newState === "open") {
|
|
72
|
-
getMenuItems()[0]?.focus();
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
|
|
76
83
|
/**
|
|
77
84
|
* Keyboard navigation following the WAI-ARIA menu pattern.
|
|
78
85
|
*
|
|
@@ -81,7 +88,7 @@ const handleToggle = (event: Event) => {
|
|
|
81
88
|
* Tab — close the menu; let the browser Tab naturally
|
|
82
89
|
* (do NOT focus the trigger — Tab should advance
|
|
83
90
|
* to the next element in the page).
|
|
84
|
-
* Escape —
|
|
91
|
+
* Escape — native light dismiss, or the fallback listener without the Popover API.
|
|
85
92
|
*/
|
|
86
93
|
const handleKeydown = (event: KeyboardEvent) => {
|
|
87
94
|
const items = getMenuItems();
|
|
@@ -108,7 +115,7 @@ const handleKeydown = (event: KeyboardEvent) => {
|
|
|
108
115
|
break;
|
|
109
116
|
case "Tab":
|
|
110
117
|
// Close without stealing focus — Tab exits to the next DOM element naturally.
|
|
111
|
-
|
|
118
|
+
hide();
|
|
112
119
|
break;
|
|
113
120
|
}
|
|
114
121
|
};
|
|
@@ -194,6 +201,24 @@ watch(
|
|
|
194
201
|
}
|
|
195
202
|
}
|
|
196
203
|
|
|
204
|
+
/* Kept apart from :popover-open, which would invalidate a shared selector list where unsupported. */
|
|
205
|
+
&.action-menu-popover-open {
|
|
206
|
+
display: block;
|
|
207
|
+
opacity: 1;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@supports not (anchor-name: --a) {
|
|
211
|
+
position: fixed;
|
|
212
|
+
top: calc(var(--_anchor-bottom, 0px) + var(--action-menu-block-distance, 0.4rem));
|
|
213
|
+
right: var(--_anchor-right-inverse, 0px);
|
|
214
|
+
z-index: var(--action-menu-popover-z-index, 999999);
|
|
215
|
+
|
|
216
|
+
&[data-placement="top"] {
|
|
217
|
+
top: auto;
|
|
218
|
+
bottom: calc(var(--_anchor-top-inverse, 0px) + var(--action-menu-block-distance, 0.4rem));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
197
222
|
.action-menu-list {
|
|
198
223
|
display: grid;
|
|
199
224
|
grid-template-columns: auto 1fr auto;
|
|
@@ -31,6 +31,7 @@ All `--action-menu-*` tokens are the stable override surface. Action menus repea
|
|
|
31
31
|
| `--action-menu-popover-min-width` | `20rem` | Minimum menu width |
|
|
32
32
|
| `--action-menu-popover-shadow` | `0 0.4rem 1.6rem rgba(0, 0, 0, 0.1)` | Menu panel drop shadow |
|
|
33
33
|
| `--action-menu-popover-transition-duration` | `200ms` | Open/close fade duration |
|
|
34
|
+
| `--action-menu-popover-z-index` | `999999` | Stacking order in browsers without CSS anchor positioning (see Positioning). Ignored where the popover renders in the top layer |
|
|
34
35
|
| `--action-menu-item-divider` | `0.1rem solid var(--slate-02)` | Divider between list rows |
|
|
35
36
|
|
|
36
37
|
### Menu items (`ActionMenuItem`, resolved on `.action-menu-item`)
|
|
@@ -60,7 +61,10 @@ Private (not public API): `--_trigger-size`, `--_popover-transition-duration`, `
|
|
|
60
61
|
|
|
61
62
|
| Hook | When |
|
|
62
63
|
|---|---|
|
|
63
|
-
| `.action-menu-popover:popover-open` | Menu is open |
|
|
64
|
+
| `.action-menu-popover:popover-open` | Menu is open, Popover API browsers |
|
|
65
|
+
| `.action-menu-popover-open` | Menu is open, browsers without the Popover API (Safari 16 and older). Style it in a **separate rule** from `:popover-open`: a selector list containing `:popover-open` is dropped whole where it's unsupported |
|
|
66
|
+
| `.action-menu-trigger[aria-expanded="true"]` | Menu is open, every browser |
|
|
67
|
+
| `.action-menu-popover[data-placement="top"]` | Flipped above the trigger, browsers without CSS anchor positioning only |
|
|
64
68
|
|
|
65
69
|
Inner classes: `.action-menu-trigger`, `.action-menu-trigger-icon`, `.action-menu-popover`,
|
|
66
70
|
`.action-menu-list`, `.action-menu-list-item`; on each item `.action-menu-item`,
|
|
@@ -76,8 +80,17 @@ Inner classes: `.action-menu-trigger`, `.action-menu-trigger-icon`, `.action-men
|
|
|
76
80
|
|
|
77
81
|
The menu uses the Popover API plus CSS anchor positioning: it opens below the trigger,
|
|
78
82
|
right-aligned, and flips above near the bottom of the viewport (`position-try-fallbacks:
|
|
79
|
-
flip-block`).
|
|
80
|
-
|
|
83
|
+
flip-block`).
|
|
84
|
+
|
|
85
|
+
Fallbacks, via the shared `useAnchoredPopover` composable:
|
|
86
|
+
|
|
87
|
+
- **No anchor positioning** (e.g. Safari 17–18): the popover is placed with `position: fixed` from
|
|
88
|
+
the trigger's measured position, re-measured on scroll and resize, and flipped above when there's
|
|
89
|
+
no room below.
|
|
90
|
+
- **No Popover API** (Safari 16 and older): the menu also opens and closes in JS, with
|
|
91
|
+
outside-click and Escape dismissal. It isn't in the top layer in this mode, so
|
|
92
|
+
`--action-menu-popover-z-index` matters, and an ancestor with `transform`, `filter` or `contain`
|
|
93
|
+
becomes its containing block and can misplace it.
|
|
81
94
|
|
|
82
95
|
---
|
|
83
96
|
|
|
@@ -500,4 +500,32 @@ describe("ActionMenu", () => {
|
|
|
500
500
|
expect(host.findAll(".action-menu-list-item")).toHaveLength(2);
|
|
501
501
|
});
|
|
502
502
|
});
|
|
503
|
+
|
|
504
|
+
// -------------------------
|
|
505
|
+
// Without the Popover API (Safari 16 and older)
|
|
506
|
+
// -------------------------
|
|
507
|
+
describe("Without the Popover API", () => {
|
|
508
|
+
beforeEach(() => {
|
|
509
|
+
Object.defineProperty(HTMLElement.prototype, "showPopover", { value: undefined, writable: true, configurable: true });
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it("opens from the trigger with the fallback open class and aria-expanded", async () => {
|
|
513
|
+
wrapper = await createWrapper({}, { "item-0": '<button role="menuitem">Edit</button>' });
|
|
514
|
+
const popover = wrapper.find(".action-menu-popover");
|
|
515
|
+
expect(popover.classes()).not.toContain("action-menu-popover-open");
|
|
516
|
+
|
|
517
|
+
await wrapper.find(".action-menu-trigger").trigger("click");
|
|
518
|
+
expect(popover.classes()).toContain("action-menu-popover-open");
|
|
519
|
+
expect(wrapper.find(".action-menu-trigger").attributes("aria-expanded")).toBe("true");
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("closes when an item is clicked", async () => {
|
|
523
|
+
wrapper = await createWrapper({}, { "item-0": '<button role="menuitem">Edit</button>' });
|
|
524
|
+
await wrapper.find(".action-menu-trigger").trigger("click");
|
|
525
|
+
await wrapper.find(".action-menu-list-item").trigger("click");
|
|
526
|
+
|
|
527
|
+
expect(wrapper.find(".action-menu-popover").classes()).not.toContain("action-menu-popover-open");
|
|
528
|
+
expect(wrapper.find(".action-menu-trigger").attributes("aria-expanded")).toBe("false");
|
|
529
|
+
});
|
|
530
|
+
});
|
|
503
531
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
3
|
exports[`ActionMenu > Snapshots > default (no items) 1`] = `
|
|
4
|
-
"<div class="action-menu" style="--_anchor-name: --action-menu-anchor-v-0-0;"><button popovertarget="action-menu-v-0-0" popovertargetaction="toggle" type="button" class="action-menu-trigger" aria-label="Open actions menu" aria-haspopup="menu"><span class="iconify i-lucide:ellipsis action-menu-trigger-icon" aria-hidden="true"></span></button>
|
|
4
|
+
"<div class="action-menu" style="--_anchor-name: --action-menu-anchor-v-0-0;"><button popovertarget="action-menu-v-0-0" popovertargetaction="toggle" type="button" class="action-menu-trigger" aria-label="Open actions menu" aria-haspopup="menu" aria-expanded="false"><span class="iconify i-lucide:ellipsis action-menu-trigger-icon" aria-hidden="true"></span></button>
|
|
5
5
|
<div id="action-menu-v-0-0" popover="" class="action-menu-popover">
|
|
6
6
|
<ul class="action-menu-list" role="menu" aria-label="Open actions menu"></ul>
|
|
7
7
|
</div>
|
|
@@ -9,7 +9,7 @@ exports[`ActionMenu > Snapshots > default (no items) 1`] = `
|
|
|
9
9
|
`;
|
|
10
10
|
|
|
11
11
|
exports[`ActionMenu > Snapshots > with custom label 1`] = `
|
|
12
|
-
"<div class="action-menu" style="--_anchor-name: --action-menu-anchor-v-0-0;"><button popovertarget="action-menu-v-0-0" popovertargetaction="toggle" type="button" class="action-menu-trigger" aria-label="Record actions" aria-haspopup="menu"><span class="iconify i-lucide:ellipsis action-menu-trigger-icon" aria-hidden="true"></span></button>
|
|
12
|
+
"<div class="action-menu" style="--_anchor-name: --action-menu-anchor-v-0-0;"><button popovertarget="action-menu-v-0-0" popovertargetaction="toggle" type="button" class="action-menu-trigger" aria-label="Record actions" aria-haspopup="menu" aria-expanded="false"><span class="iconify i-lucide:ellipsis action-menu-trigger-icon" aria-hidden="true"></span></button>
|
|
13
13
|
<div id="action-menu-v-0-0" popover="" class="action-menu-popover">
|
|
14
14
|
<ul class="action-menu-list" role="menu" aria-label="Record actions"></ul>
|
|
15
15
|
</div>
|
|
@@ -17,7 +17,7 @@ exports[`ActionMenu > Snapshots > with custom label 1`] = `
|
|
|
17
17
|
`;
|
|
18
18
|
|
|
19
19
|
exports[`ActionMenu > Snapshots > with slot content 1`] = `
|
|
20
|
-
"<div class="action-menu" style="--_anchor-name: --action-menu-anchor-v-0-0;"><button popovertarget="action-menu-v-0-0" popovertargetaction="toggle" type="button" class="action-menu-trigger" aria-label="Open actions menu" aria-haspopup="menu"><span class="iconify i-lucide:ellipsis action-menu-trigger-icon" aria-hidden="true"></span></button>
|
|
20
|
+
"<div class="action-menu" style="--_anchor-name: --action-menu-anchor-v-0-0;"><button popovertarget="action-menu-v-0-0" popovertargetaction="toggle" type="button" class="action-menu-trigger" aria-label="Open actions menu" aria-haspopup="menu" aria-expanded="false"><span class="iconify i-lucide:ellipsis action-menu-trigger-icon" aria-hidden="true"></span></button>
|
|
21
21
|
<div id="action-menu-v-0-0" popover="" class="action-menu-popover">
|
|
22
22
|
<ul class="action-menu-list" role="menu" aria-label="Open actions menu">
|
|
23
23
|
<li class="action-menu-list-item" role="none"><button role="menuitem">Edit</button></li>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<template v-if="$slots.triggerContent" #triggerContent>
|
|
8
8
|
<slot name="triggerContent"></slot>
|
|
9
9
|
</template>
|
|
10
|
-
<template #tooltipContent>
|
|
10
|
+
<template #tooltipContent="{ close }">
|
|
11
11
|
<div class="popover-content-defined">
|
|
12
12
|
<component
|
|
13
13
|
:is="contentText.tooltipTitle?.tag"
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
popovertargetaction="hide"
|
|
32
32
|
class="display-tooltip-close-button"
|
|
33
33
|
:aria-label="closeButtonAriaLabel"
|
|
34
|
+
@click="close"
|
|
34
35
|
>
|
|
35
36
|
{{ closeButtonText }}
|
|
36
37
|
</button>
|
package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts
CHANGED
|
@@ -67,7 +67,7 @@ export const GuidedTour: Story = {
|
|
|
67
67
|
docs: {
|
|
68
68
|
description: {
|
|
69
69
|
story:
|
|
70
|
-
"Recreates the deleted `app/pages/ui/tooltips.vue` demo: `useTooltipsGuide` walks a container's `[popover]` elements in DOM order, opening each and waiting for its close button before advancing — the autoplay-through-steps pattern seen on some guide intros. Mixes a `DisplayTooltipDefined` step with a plain `DisplayTooltip` step (the
|
|
70
|
+
"Recreates the deleted `app/pages/ui/tooltips.vue` demo: `useTooltipsGuide` walks a container's `[popover]` elements in DOM order, opening each and waiting for its close button before advancing — the autoplay-through-steps pattern seen on some guide intros. Mixes a `DisplayTooltipDefined` step (its close button relabelled \"Next\" via `closeButtonText`) with a plain `DisplayTooltip` step. A hand-written close button must call the `tooltipContent` slot's `close()` as well as carrying `popovertargetaction=\"hide\"`: the attribute alone does nothing on Safari 16, which has no Popover API. Guide auto-starts 800ms after mount in this story; use \"Run guide again\" to replay it.",
|
|
71
71
|
},
|
|
72
72
|
},
|
|
73
73
|
},
|
|
@@ -89,12 +89,14 @@ export const GuidedTour: Story = {
|
|
|
89
89
|
{{ isGuideRunning ? 'Guide running…' : 'Run guide again' }}
|
|
90
90
|
</button>
|
|
91
91
|
<span v-if="isGuideRunning" style="font-size: 1.3rem; color: #666;">
|
|
92
|
-
Step {{ currentTooltipIndex + 1 }} of {{ totalPopovers }} — click
|
|
92
|
+
Step {{ currentTooltipIndex + 1 }} of {{ totalPopovers }} — click Next (Close on the last step) to advance
|
|
93
93
|
</span>
|
|
94
94
|
</div>
|
|
95
95
|
|
|
96
96
|
<div ref="guideContainer" style="display: grid; gap: 2rem;">
|
|
97
97
|
<DisplayTooltipDefined
|
|
98
|
+
close-button-text="Next"
|
|
99
|
+
close-button-aria-label="Next step"
|
|
98
100
|
:content-text="{
|
|
99
101
|
tooltipTitle: { tag: 'h3', text: 'Step 1' },
|
|
100
102
|
tooltipContent: { tag: 'p', text: 'This step uses DisplayTooltipDefined — structured title/body/action content plus a built-in close button.' },
|
|
@@ -106,14 +108,15 @@ export const GuidedTour: Story = {
|
|
|
106
108
|
|
|
107
109
|
<DisplayTooltip tooltip-id="guided-tour-step-2">
|
|
108
110
|
<template #triggerContent>Trigger from plain DisplayTooltip</template>
|
|
109
|
-
<template #tooltipContent>
|
|
110
|
-
<p
|
|
111
|
+
<template #tooltipContent="{ close }">
|
|
112
|
+
<p>This step uses plain DisplayTooltip — you supply the close button yourself via the tooltipContent slot.</p>
|
|
111
113
|
<button
|
|
112
114
|
popovertarget="guided-tour-step-2"
|
|
113
115
|
popovertargetaction="hide"
|
|
114
116
|
class="display-tooltip-close-button"
|
|
115
117
|
style="align-self: flex-end;"
|
|
116
118
|
aria-label="Close tool tip"
|
|
119
|
+
@click="close"
|
|
117
120
|
>
|
|
118
121
|
Close
|
|
119
122
|
</button>
|
package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
3
|
import DisplayTooltipDefined from "../DisplayTooltipDefined.vue";
|
|
4
4
|
import type { TooltipContentText } from "~/types/components";
|
|
@@ -63,4 +63,23 @@ describe("DisplayTooltipDefined", () => {
|
|
|
63
63
|
});
|
|
64
64
|
expect(wrapper.classes()).toContain("custom-class");
|
|
65
65
|
});
|
|
66
|
+
|
|
67
|
+
describe("without the Popover API", () => {
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
Object.defineProperty(HTMLElement.prototype, "showPopover", { value: undefined, writable: true, configurable: true });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
delete (HTMLElement.prototype as unknown as Record<string, unknown>)["showPopover"];
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("closes from the close button", async () => {
|
|
77
|
+
const wrapper = await mountSuspended(DisplayTooltipDefined, { props: { contentText } });
|
|
78
|
+
await wrapper.find(".display-tooltip-trigger-button").trigger("click");
|
|
79
|
+
expect(wrapper.find(".display-tooltip-popover").classes()).toContain("display-tooltip-popover-open");
|
|
80
|
+
|
|
81
|
+
await wrapper.find(".display-tooltip-close-button").trigger("click");
|
|
82
|
+
expect(wrapper.find(".display-tooltip-popover").classes()).not.toContain("display-tooltip-popover-open");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
66
85
|
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
exports[`DisplayTooltipDefined > renders correct HTML structure 1`] = `
|
|
4
4
|
"<div class="display-tooltip-core" style="--_anchor-name: --tooltip-anchor-v-0-0-0;">
|
|
5
5
|
<div class="display-tooltip-trigger-wrapper body-md">
|
|
6
|
-
<!--v-if--><button popovertarget="nuxt-tooltip-fixed" popovertargetaction="toggle" class="display-tooltip-trigger-button" aria-label="Toggle the popover"><span class="iconify i-fa7-solid:circle-question display-tooltip-trigger-icon" aria-hidden="true"></span></button>
|
|
6
|
+
<!--v-if--><button popovertarget="nuxt-tooltip-fixed" popovertargetaction="toggle" class="display-tooltip-trigger-button" aria-label="Toggle the popover" aria-expanded="false"><span class="iconify i-fa7-solid:circle-question display-tooltip-trigger-icon" aria-hidden="true"></span></button>
|
|
7
7
|
</div>
|
|
8
8
|
<div id="nuxt-tooltip-fixed" popover="" class="display-tooltip-popover">
|
|
9
9
|
<div class="display-tooltip-popover-content">
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
| `--deep-expanding-menu-panel-list-gap` | `1.2rem` | Gap between child link grid items |
|
|
21
21
|
| `--deep-expanding-menu-group-link-colour` | `var(--slate-10)` | Child link text colour |
|
|
22
22
|
| `--deep-expanding-menu-group-link-border-colour-hover` | `var(--slate-10)` | Child link bottom border colour on hover/focus |
|
|
23
|
+
| `--deep-expanding-menu-panel-z-index` | `999999` | Panel stacking order in browsers without CSS anchor positioning. Ignored where the panel renders in the top layer |
|
|
23
24
|
|
|
24
25
|
```css
|
|
25
26
|
.my-page {
|
|
@@ -39,14 +40,28 @@ Or scope to a single instance via `styleClassPassthrough`:
|
|
|
39
40
|
|
|
40
41
|
## Not tokenised
|
|
41
42
|
|
|
42
|
-
- The
|
|
43
|
-
these are CSS anchor-positioning fallback rules, not runtime custom properties, so they can't be
|
|
44
|
-
overridden per-instance without a new `@position-try` block.
|
|
43
|
+
- The `1rem` gap between a toggle and its panel.
|
|
45
44
|
- Anchor names and popover target ids are generated per-instance via `useId()` and are not
|
|
46
45
|
consumer-configurable — they only need to be unique, not meaningful.
|
|
47
46
|
|
|
48
47
|
---
|
|
49
48
|
|
|
49
|
+
## Open state and older browsers
|
|
50
|
+
|
|
51
|
+
| Hook | Meaning |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `.navigation-group-toggle[aria-expanded="true"]` | Group open, every browser (flips the caret) |
|
|
54
|
+
| `.navigation-group-panel:popover-open` | Panel open, Popover API browsers |
|
|
55
|
+
| `.deep-expanding-menu-panel-open` | Panel open, browsers without the Popover API (Safari 16 and older). Style it in a **separate rule** from `:popover-open`: a selector list containing `:popover-open` is dropped whole where it's unsupported |
|
|
56
|
+
| `.navigation-group-panel[data-placement="top"]` | Flipped above the toggle, browsers without CSS anchor positioning only |
|
|
57
|
+
|
|
58
|
+
Without CSS anchor positioning (e.g. Safari 17–18) the panel is placed with `position: fixed` from
|
|
59
|
+
the toggle's measured position. Without the Popover API (Safari 16) the panel also opens and closes
|
|
60
|
+
in JS and isn't in the top layer, so `--deep-expanding-menu-panel-z-index` applies, and an ancestor
|
|
61
|
+
with `transform`, `filter` or `contain` becomes its containing block and can misplace it.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
50
65
|
## Local overrides
|
|
51
66
|
|
|
52
67
|
Set the tokens above on an element you own (a page or section class, or a class added with
|
|
@@ -54,4 +69,3 @@ Set the tokens above on an element you own (a page or section class, or a class
|
|
|
54
69
|
`@layer` wrapper) so it beats the library's `@layer components`. If your own file uses
|
|
55
70
|
`<style scoped>`, tokens set on your element still work, but selectors that reach inside the component need
|
|
56
71
|
`:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
|
|
57
|
-
|