srcdev-nuxt-components 9.6.11 → 9.6.13
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/rules/consumer/ids-from-useid.md +2 -1
- package/.claude/rules/consumer/vfor-keys-from-data.md +14 -0
- package/.claude/skills/component-compliance-checklist.md +24 -1
- package/.claude/skills/component-naming.md +1 -0
- package/.claude/skills/components/banner-video.md +24 -30
- package/.claude/skills/components/content-docs.md +18 -5
- package/.claude/skills/components/display-dialog.md +16 -6
- package/.claude/skills/components/{display-tooltip-defined.md → display-tooltip-rich.md} +40 -42
- package/.claude/skills/components/display-tooltip.md +17 -6
- package/.claude/skills/components/form-field.md +15 -0
- package/.claude/skills/components/form-fieldset.md +18 -2
- package/.claude/skills/components/marquee-scroller.md +51 -14
- package/.claude/skills/components/skip-links.md +16 -0
- package/.claude/skills/composable-tooltips-guide.md +4 -4
- package/.claude/skills/index.md +6 -6
- package/.claude/skills/testing-add-unit-test.md +8 -0
- package/.vscode/srcdev-component-banner-video.code-snippets +38 -25
- package/.vscode/srcdev-component-content-docs.code-snippets +69 -60
- package/.vscode/srcdev-component-display-dialog.code-snippets +7 -5
- package/.vscode/srcdev-component-display-tooltip-rich.code-snippets +50 -0
- package/.vscode/srcdev-component-display-tooltip.code-snippets +31 -0
- package/.vscode/srcdev-component-form-field.code-snippets +7 -6
- package/.vscode/srcdev-component-form-fieldset.code-snippets +12 -12
- package/.vscode/srcdev-component-marquee-scroller.code-snippets +31 -28
- package/.vscode/srcdev-component-skip-links.code-snippets +1 -1
- package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +77 -54
- package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +103 -91
- package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +132 -3
- package/app/components/01.atoms/animations/marquee-scroller/tests/MarqueeScroller.spec.ts +190 -102
- package/app/components/01.atoms/animations/marquee-scroller/tests/__snapshots__/MarqueeScroller.spec.ts.snap +17 -13
- package/app/components/01.atoms/banner-video/BannerVideo.vue +51 -19
- package/app/components/01.atoms/banner-video/CONSUMER-STYLING.md +59 -40
- package/app/components/01.atoms/banner-video/stories/BannerVideo.stories.ts +88 -1
- package/app/components/01.atoms/banner-video/tests/BannerVideo.spec.ts +127 -78
- package/app/components/01.atoms/banner-video/tests/__snapshots__/BannerVideo.spec.ts.snap +12 -4
- package/app/components/01.atoms/content-wrappers/docs-pages/CONSUMER-STYLING.md +42 -14
- package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +86 -112
- package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +134 -8
- package/app/components/01.atoms/content-wrappers/docs-pages/tests/ContentDocs.spec.ts +131 -65
- package/app/components/01.atoms/display-dialog/CONSUMER-STYLING.md +28 -3
- package/app/components/01.atoms/display-dialog/DisplayDialog.vue +39 -31
- package/app/components/01.atoms/display-dialog/stories/DisplayDialog.stories.ts +92 -1
- package/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts +87 -41
- package/app/components/01.atoms/display-dialog/tests/__snapshots__/DisplayDialog.spec.ts.snap +5 -1
- package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +70 -24
- package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +24 -12
- package/app/components/01.atoms/display-tooltip/stories/DisplayTooltip.stories.ts +89 -2
- package/app/components/01.atoms/display-tooltip/tests/DisplayTooltip.spec.ts +65 -18
- package/app/components/01.atoms/display-tooltip/tests/__snapshots__/DisplayTooltip.spec.ts.snap +3 -3
- package/app/components/01.atoms/navigation/skip-links/CONSUMER-STYLING.md +65 -19
- package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +20 -14
- package/app/components/01.atoms/navigation/skip-links/stories/SkipLinks.stories.ts +89 -20
- package/app/components/01.atoms/navigation/skip-links/tests/SkipLinks.spec.ts +70 -10
- package/app/components/02.molecules/display-tooltip-rich/CONSUMER-STYLING.md +110 -0
- package/app/components/02.molecules/display-tooltip-rich/DisplayTooltipRich.vue +148 -0
- package/app/components/02.molecules/display-tooltip-rich/stories/DisplayTooltipRich.stories.ts +254 -0
- package/app/components/02.molecules/display-tooltip-rich/tests/DisplayTooltipRich.spec.ts +156 -0
- package/app/components/02.molecules/display-tooltip-rich/tests/__snapshots__/DisplayTooltipRich.spec.ts.snap +22 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +1 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +1 -1
- package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +1 -0
- package/app/components/05.forms/form-field/CONSUMER-STYLING.md +7 -0
- package/app/components/05.forms/form-field/stories/FormField.stories.ts +116 -2
- package/app/components/05.forms/form-fieldset/CONSUMER-STYLING.md +11 -1
- package/app/components/05.forms/form-fieldset/FormFieldset.vue +4 -2
- package/app/components/05.forms/form-fieldset/stories/FormFieldset.stories.ts +68 -4
- package/app/components/05.forms/form-fieldset/tests/FormFieldset.spec.ts +10 -0
- package/app/composables/tests/useTooltips.spec.ts +3 -3
- package/app/types/components/index.ts +1 -1
- package/package.json +1 -1
- package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +0 -29
- package/app/components/02.molecules/display-tooltip-defined/CONSUMER-STYLING.md +0 -53
- package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +0 -117
- package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts +0 -129
- package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts +0 -85
- package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +0 -17
- /package/app/types/components/{display-tooltip-defined.d.ts → display-tooltip-rich.d.ts} +0 -0
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
:id="id"
|
|
4
4
|
:role="groupRole === 'radiogroup' ? 'radiogroup' : undefined"
|
|
5
5
|
:aria-required="groupRole === 'radiogroup' && required ? 'true' : undefined"
|
|
6
|
-
:aria-invalid="fieldHasError"
|
|
6
|
+
:aria-invalid="fieldHasError ? 'true' : undefined"
|
|
7
7
|
:name="name"
|
|
8
8
|
class="form-fieldset"
|
|
9
9
|
:class="elementClasses"
|
|
10
|
-
:data-testid="dataTestid"
|
|
10
|
+
:data-testid="dataTestid || undefined"
|
|
11
11
|
>
|
|
12
12
|
<legend v-if="legend || slots.legend" class="form-fieldset-legend">
|
|
13
13
|
<slot name="legend">{{ legend }}</slot>
|
|
@@ -54,6 +54,7 @@ watch(
|
|
|
54
54
|
<style lang="css">
|
|
55
55
|
@layer components {
|
|
56
56
|
.form-fieldset {
|
|
57
|
+
min-inline-size: 0;
|
|
57
58
|
margin-block: var(--form-fieldset-margin-block, 0);
|
|
58
59
|
margin-inline: var(--form-fieldset-margin-inline, 0);
|
|
59
60
|
padding-block: var(--form-fieldset-padding-block, 0);
|
|
@@ -65,6 +66,7 @@ watch(
|
|
|
65
66
|
|
|
66
67
|
.form-fieldset-legend {
|
|
67
68
|
color: var(--form-fieldset-legend-color, inherit);
|
|
69
|
+
overflow-wrap: anywhere;
|
|
68
70
|
margin-block: var(--form-fieldset-legend-margin-block, 0.8rem);
|
|
69
71
|
margin-inline: var(--form-fieldset-legend-margin-inline, 0);
|
|
70
72
|
font-size: var(--form-fieldset-legend-font-size, var(--step-5));
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { ref } from "vue";
|
|
1
2
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
3
|
import StorybookComponent from "../FormFieldset.vue";
|
|
4
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
5
|
+
import type { MediaCanvas } from "~/types/components";
|
|
3
6
|
|
|
4
7
|
interface FormFieldsetStoryArgs {
|
|
5
8
|
id: string;
|
|
@@ -41,15 +44,53 @@ export default {
|
|
|
41
44
|
fieldHasError: false,
|
|
42
45
|
styleClassPassthrough: "",
|
|
43
46
|
},
|
|
47
|
+
decorators: [
|
|
48
|
+
(story, context) => ({
|
|
49
|
+
components: { story, CanvasSwitcher },
|
|
50
|
+
setup() {
|
|
51
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
52
|
+
return { canvasName };
|
|
53
|
+
},
|
|
54
|
+
template: `
|
|
55
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
56
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
57
|
+
</div>
|
|
58
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
59
|
+
<story />
|
|
60
|
+
</div>
|
|
61
|
+
`,
|
|
62
|
+
}),
|
|
63
|
+
],
|
|
64
|
+
parameters: {
|
|
65
|
+
layout: "fullscreen",
|
|
66
|
+
docs: {
|
|
67
|
+
description: {
|
|
68
|
+
component:
|
|
69
|
+
"Shared `<fieldset>` + `<legend>` wrapper for a group of controls. `legend` (or the `legend` slot) names the group, the `content` slot holds the controls, `group-role` switches between the native `group` and `radiogroup`, and `required`/`field-has-error` set `aria-required` (radiogroup only) and `aria-invalid`. Mostly a building block: `SingleCheckbox`, `MultipleCheckboxes` and `MultipleRadiobuttons` render it.",
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
44
73
|
} as Meta<FormFieldsetStoryArgs>;
|
|
45
74
|
|
|
75
|
+
// Links in the canvas open in the Storybook manager (target="_top"), not inside the preview iframe.
|
|
76
|
+
const parentNote = `
|
|
77
|
+
<p style="font-size: 1.3rem; margin: 0 0 1.6rem;">
|
|
78
|
+
Rendered by
|
|
79
|
+
<a href="/?path=/story/components-forms-input-checkbox-singlecheckbox--default" target="_top">SingleCheckbox</a>,
|
|
80
|
+
<a href="/?path=/story/components-forms-input-checkbox-multiplecheckboxes--default" target="_top">MultipleCheckboxes</a> and
|
|
81
|
+
<a href="/?path=/story/components-forms-input-radio-multipleradiobuttons--default" target="_top">MultipleRadiobuttons</a>,
|
|
82
|
+
which add styled options, a description and an error message. Inspect the fieldset to see role and aria-* output.
|
|
83
|
+
</p>
|
|
84
|
+
`;
|
|
85
|
+
|
|
46
86
|
const Template: StoryFn<FormFieldsetStoryArgs> = (args) => ({
|
|
47
87
|
components: { StorybookComponent },
|
|
48
88
|
setup() {
|
|
49
89
|
return { args };
|
|
50
90
|
},
|
|
51
91
|
template: `
|
|
52
|
-
<div
|
|
92
|
+
<div>
|
|
93
|
+
${parentNote}
|
|
53
94
|
<StorybookComponent
|
|
54
95
|
:id="args.id"
|
|
55
96
|
:name="args.name"
|
|
@@ -68,17 +109,40 @@ const Template: StoryFn<FormFieldsetStoryArgs> = (args) => ({
|
|
|
68
109
|
</label>
|
|
69
110
|
</template>
|
|
70
111
|
</StorybookComponent>
|
|
71
|
-
<p style="font-size: 1.3rem; margin-top: 1.6rem;">
|
|
72
|
-
Rendered by SingleCheckbox, MultipleCheckboxes and MultipleRadiobuttons. Inspect the fieldset to see role and aria-* output.
|
|
73
|
-
</p>
|
|
74
112
|
</div>
|
|
75
113
|
`,
|
|
76
114
|
});
|
|
77
115
|
|
|
78
116
|
export const Default = Template.bind({});
|
|
117
|
+
Default.parameters = {
|
|
118
|
+
docs: { description: { story: "A required radio group: `role=\"radiogroup\"` and `aria-required=\"true\"` on the fieldset." } },
|
|
119
|
+
};
|
|
79
120
|
|
|
80
121
|
export const CheckboxGroup = Template.bind({});
|
|
81
122
|
CheckboxGroup.args = { groupRole: "group", legend: "Interests", required: false };
|
|
123
|
+
CheckboxGroup.parameters = {
|
|
124
|
+
docs: { description: { story: "`group-role=\"group\"`: no `role` attribute (a fieldset is already a group) and never `aria-required`." } },
|
|
125
|
+
};
|
|
82
126
|
|
|
83
127
|
export const WithError = Template.bind({});
|
|
84
128
|
WithError.args = { fieldHasError: true };
|
|
129
|
+
WithError.parameters = {
|
|
130
|
+
docs: { description: { story: "`field-has-error` sets `aria-invalid=\"true\"`. With it off, the attribute is left out entirely." } },
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const StressTest = Template.bind({});
|
|
134
|
+
StressTest.storyName = "Stress Test (Worst-Case Data)";
|
|
135
|
+
StressTest.args = {
|
|
136
|
+
legend:
|
|
137
|
+
"📞 Bevorzugtekontaktmethodefürrückfragenzuihrerbestellung https://example.com/a/very/long/unbroken/url <b>not bold</b> طريقة الاتصال",
|
|
138
|
+
fieldHasError: true,
|
|
139
|
+
};
|
|
140
|
+
StressTest.parameters = {
|
|
141
|
+
initialCanvas: "mobileCanvas",
|
|
142
|
+
docs: {
|
|
143
|
+
description: {
|
|
144
|
+
story:
|
|
145
|
+
"Long unbroken German, a URL, emoji, HTML-like and right-to-left copy in the legend, with the error state on. Check at every canvas width: the legend wraps inside the dashed outline, the fieldset never grows past its column (it sets `min-inline-size: 0`, so a native fieldset's min-content width can't push it out), and markup renders as text. Wrapping the slotted controls is the slot content's job.",
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
};
|
|
@@ -22,6 +22,11 @@ describe("FormFieldset", () => {
|
|
|
22
22
|
expect(fieldset.attributes("data-testid")).toBe("fieldset-test");
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
+
it("omits data-testid when none is given", async () => {
|
|
26
|
+
const wrapper = await mountSuspended(FormFieldset, { props: baseProps });
|
|
27
|
+
expect(wrapper.find("fieldset").attributes("data-testid")).toBeUndefined();
|
|
28
|
+
});
|
|
29
|
+
|
|
25
30
|
it("renders the content slot", async () => {
|
|
26
31
|
const wrapper = await mountSuspended(FormFieldset, {
|
|
27
32
|
props: baseProps,
|
|
@@ -79,6 +84,11 @@ describe("FormFieldset", () => {
|
|
|
79
84
|
expect(wrapper.find("fieldset").attributes("aria-invalid")).toBe("true");
|
|
80
85
|
});
|
|
81
86
|
|
|
87
|
+
it("omits aria-invalid when fieldHasError is false", async () => {
|
|
88
|
+
const wrapper = await mountSuspended(FormFieldset, { props: baseProps });
|
|
89
|
+
expect(wrapper.find("fieldset").attributes("aria-invalid")).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
|
|
82
92
|
it("does not add a bare error class", async () => {
|
|
83
93
|
const wrapper = await mountSuspended(FormFieldset, { props: { ...baseProps, fieldHasError: true } });
|
|
84
94
|
expect(wrapper.find("fieldset").classes()).not.toContain("error");
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
|
2
2
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
3
|
import { defineComponent, h, nextTick, ref } from "vue";
|
|
4
4
|
import { useTooltipsGuide } from "../useTooltips";
|
|
5
|
-
import
|
|
5
|
+
import DisplayTooltipRich from "~/components/02.molecules/display-tooltip-rich/DisplayTooltipRich.vue";
|
|
6
6
|
|
|
7
7
|
const flush = async () => {
|
|
8
8
|
for (let i = 0; i < 5; i++) await nextTick();
|
|
@@ -18,8 +18,8 @@ const mountGuide = () =>
|
|
|
18
18
|
},
|
|
19
19
|
render() {
|
|
20
20
|
return h("div", { ref: "containerRef" }, [
|
|
21
|
-
h(
|
|
22
|
-
h(
|
|
21
|
+
h(DisplayTooltipRich, { tooltipId: "one", contentText: { tooltipTitle: { tag: "h4", text: "One" } } }),
|
|
22
|
+
h(DisplayTooltipRich, { tooltipId: "two", contentText: { tooltipTitle: { tag: "h4", text: "Two" } } }),
|
|
23
23
|
]);
|
|
24
24
|
},
|
|
25
25
|
}),
|
|
@@ -15,7 +15,7 @@ export * from "./social-icons-list.d"
|
|
|
15
15
|
export * from "./content-docs.d"
|
|
16
16
|
export * from "./breadcrumb"
|
|
17
17
|
export * from "./cookie-consent-banner.d"
|
|
18
|
-
export * from "./display-tooltip-
|
|
18
|
+
export * from "./display-tooltip-rich.d"
|
|
19
19
|
export * from "./marquee-scroller.d"
|
|
20
20
|
export * from "./rotating-carousel-image.d"
|
|
21
21
|
export * from "./skip-links.d"
|
package/package.json
CHANGED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"SRCDEV DisplayTooltipDefined Basic": {
|
|
3
|
-
"description": "DisplayTooltipDefined — structured title/body/action tooltip",
|
|
4
|
-
"scope": "vue,html",
|
|
5
|
-
"body": [
|
|
6
|
-
"<DisplayTooltipDefined",
|
|
7
|
-
" :content-text=\"{",
|
|
8
|
-
" tooltipTitle: { tag: 'h4', text: '$1' },",
|
|
9
|
-
" tooltipContent: { tag: 'p', text: '$0' },",
|
|
10
|
-
" }\"",
|
|
11
|
-
"/>"
|
|
12
|
-
]
|
|
13
|
-
},
|
|
14
|
-
"SRCDEV DisplayTooltipDefined Localised Copy": {
|
|
15
|
-
"description": "DisplayTooltipDefined with localised trigger/close-button copy",
|
|
16
|
-
"scope": "vue,html",
|
|
17
|
-
"body": [
|
|
18
|
-
"<DisplayTooltipDefined",
|
|
19
|
-
" :content-text=\"{",
|
|
20
|
-
" tooltipTitle: { tag: 'h4', text: '$1' },",
|
|
21
|
-
" tooltipContent: { tag: 'p', text: '$2' },",
|
|
22
|
-
" }\"",
|
|
23
|
-
" trigger-aria-label=\"${3:Toggle the popover}\"",
|
|
24
|
-
" close-button-text=\"${4:Close}\"",
|
|
25
|
-
" close-button-aria-label=\"${5:Close tool tip}\"",
|
|
26
|
-
"/>"
|
|
27
|
-
]
|
|
28
|
-
}
|
|
29
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# DisplayTooltipDefined — Consumer Styling Guide
|
|
2
|
-
|
|
3
|
-
## Public token API
|
|
4
|
-
|
|
5
|
-
| Token | Default | Controls |
|
|
6
|
-
|---|---|---|
|
|
7
|
-
| `--display-tooltip-defined-content-gap` | `1.2rem` | Gap between title/body/action/close-button |
|
|
8
|
-
| `--display-tooltip-defined-title-colour` | `inherit` | `tooltipTitle` text colour |
|
|
9
|
-
| `--display-tooltip-defined-title-margin-block` | `0 0` | `tooltipTitle` block margin |
|
|
10
|
-
| `--display-tooltip-defined-title-font-size` | `1.6rem` | `tooltipTitle` font size |
|
|
11
|
-
| `--display-tooltip-defined-title-font-weight` | `700` | `tooltipTitle` font weight |
|
|
12
|
-
| `--display-tooltip-defined-title-line-height` | `120%` | `tooltipTitle` line height |
|
|
13
|
-
| `--display-tooltip-defined-title-letter-spacing` | `normal` | `tooltipTitle` letter spacing |
|
|
14
|
-
| `--display-tooltip-defined-body-colour` | `inherit` | `tooltipContent` text colour |
|
|
15
|
-
| `--display-tooltip-defined-body-margin-block` | `0 0` | `tooltipContent` block margin |
|
|
16
|
-
| `--display-tooltip-defined-body-font-size` | `1.3rem` | `tooltipContent` font size |
|
|
17
|
-
| `--display-tooltip-defined-body-font-weight` | `inherit` | `tooltipContent` font weight |
|
|
18
|
-
| `--display-tooltip-defined-body-line-height` | `140%` | `tooltipContent` line height |
|
|
19
|
-
| `--display-tooltip-defined-body-letter-spacing` | `normal` | `tooltipContent` letter spacing |
|
|
20
|
-
| `--display-tooltip-defined-action-colour` | `inherit` | `tooltipAction` text colour |
|
|
21
|
-
| `--display-tooltip-defined-action-margin-block` | `0 0` | `tooltipAction` block margin |
|
|
22
|
-
| `--display-tooltip-defined-action-font-size` | `1.4rem` | `tooltipAction` font size |
|
|
23
|
-
| `--display-tooltip-defined-action-font-weight` | `700` | `tooltipAction` font weight |
|
|
24
|
-
| `--display-tooltip-defined-action-line-height` | `130%` | `tooltipAction` line height |
|
|
25
|
-
| `--display-tooltip-defined-action-letter-spacing` | `normal` | `tooltipAction` letter spacing |
|
|
26
|
-
| `--display-tooltip-defined-close-button-margin-block` | `1.2rem 0` | Space above the close button |
|
|
27
|
-
|
|
28
|
-
The three `*-colour` tokens default to `inherit`, which resolves to the underlying
|
|
29
|
-
`DisplayTooltip` popover's own `--display-tooltip-popover-text-colour` (see its
|
|
30
|
-
CONSUMER-STYLING.md) — override there for a uniform colour change, or override one of these three
|
|
31
|
-
for a single field.
|
|
32
|
-
|
|
33
|
-
```css
|
|
34
|
-
.my-page {
|
|
35
|
-
--display-tooltip-defined-title-colour: var(--brand-accent);
|
|
36
|
-
--display-tooltip-defined-title-font-size: 1.8rem;
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
This component also renders (and can override) every `--display-tooltip-*` token from the
|
|
41
|
-
underlying `DisplayTooltip` — see its CONSUMER-STYLING.md for the full popover/trigger/close-button
|
|
42
|
-
token API.
|
|
43
|
-
|
|
44
|
-
---
|
|
45
|
-
|
|
46
|
-
## Local overrides
|
|
47
|
-
|
|
48
|
-
Set the tokens above on an element you own (a page or section class, or a class added with
|
|
49
|
-
`:style-class-passthrough`): they inherit down into the component. Keep the block **unlayered** (no
|
|
50
|
-
`@layer` wrapper) so it beats the library's `@layer components`. If your own file uses
|
|
51
|
-
`<style scoped>`, tokens set on your element still work, but selectors that reach inside the component need
|
|
52
|
-
`:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
|
|
53
|
-
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<DisplayTooltip
|
|
3
|
-
:tooltip-id="tooltipId"
|
|
4
|
-
:trigger-aria-label="triggerAriaLabel"
|
|
5
|
-
:style-class-passthrough="styleClassPassthrough"
|
|
6
|
-
>
|
|
7
|
-
<template v-if="$slots.triggerContent" #triggerContent>
|
|
8
|
-
<slot name="triggerContent"></slot>
|
|
9
|
-
</template>
|
|
10
|
-
<template #tooltipContent="{ close }">
|
|
11
|
-
<div class="popover-content-defined">
|
|
12
|
-
<component
|
|
13
|
-
:is="contentText.tooltipTitle?.tag"
|
|
14
|
-
v-if="contentText.tooltipTitle"
|
|
15
|
-
class="tooltip-title subtitle-sm"
|
|
16
|
-
>
|
|
17
|
-
{{ contentText.tooltipTitle.text }}
|
|
18
|
-
</component>
|
|
19
|
-
<component :is="contentText.tooltipContent?.tag" v-if="contentText.tooltipContent" class="tooltip-body body-sm">
|
|
20
|
-
{{ contentText.tooltipContent.text }}
|
|
21
|
-
</component>
|
|
22
|
-
<component
|
|
23
|
-
:is="contentText.tooltipAction?.tag"
|
|
24
|
-
v-if="contentText.tooltipAction"
|
|
25
|
-
class="tooltip-action input-value"
|
|
26
|
-
>
|
|
27
|
-
{{ contentText.tooltipAction.text }}
|
|
28
|
-
</component>
|
|
29
|
-
<button
|
|
30
|
-
:popovertarget="tooltipId"
|
|
31
|
-
popovertargetaction="hide"
|
|
32
|
-
class="display-tooltip-close-button"
|
|
33
|
-
:aria-label="closeButtonAriaLabel"
|
|
34
|
-
@click="close"
|
|
35
|
-
>
|
|
36
|
-
{{ closeButtonText }}
|
|
37
|
-
</button>
|
|
38
|
-
</div>
|
|
39
|
-
</template>
|
|
40
|
-
</DisplayTooltip>
|
|
41
|
-
</template>
|
|
42
|
-
|
|
43
|
-
<script setup lang="ts">
|
|
44
|
-
import type { TooltipContentText } from "~/types/components";
|
|
45
|
-
|
|
46
|
-
interface Props {
|
|
47
|
-
tooltipId?: string;
|
|
48
|
-
contentText?: TooltipContentText;
|
|
49
|
-
/** aria-label on the trigger button — override for localisation. */
|
|
50
|
-
triggerAriaLabel?: string;
|
|
51
|
-
/** Visible text on the close button — override for localisation. */
|
|
52
|
-
closeButtonText?: string;
|
|
53
|
-
/** aria-label on the close button — override for localisation. */
|
|
54
|
-
closeButtonAriaLabel?: string;
|
|
55
|
-
styleClassPassthrough?: string | string[];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const props = withDefaults(defineProps<Props>(), {
|
|
59
|
-
tooltipId: "",
|
|
60
|
-
contentText: () => ({}),
|
|
61
|
-
triggerAriaLabel: "Toggle the popover",
|
|
62
|
-
closeButtonText: "Close",
|
|
63
|
-
closeButtonAriaLabel: "Close tool tip",
|
|
64
|
-
styleClassPassthrough: () => [],
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const tooltipId = computed(() =>
|
|
68
|
-
props.tooltipId.length ? `nuxt-tooltip-${props.tooltipId}` : `nuxt-tooltip-${useId()}`
|
|
69
|
-
);
|
|
70
|
-
</script>
|
|
71
|
-
|
|
72
|
-
<style lang="css">
|
|
73
|
-
@layer components {
|
|
74
|
-
.display-tooltip-core {
|
|
75
|
-
.popover-content-defined {
|
|
76
|
-
display: flex;
|
|
77
|
-
flex-direction: column;
|
|
78
|
-
gap: var(--display-tooltip-defined-content-gap, 1.2rem);
|
|
79
|
-
|
|
80
|
-
/* margin-block resets below neutralise the UA default margin of whatever tag the consumer
|
|
81
|
-
picks via contentText.*.tag (e.g. h3/h4 carry their own margin-block) — gap is the only
|
|
82
|
-
intended spacing mechanism between these three. */
|
|
83
|
-
.tooltip-title {
|
|
84
|
-
color: var(--display-tooltip-defined-title-colour, inherit);
|
|
85
|
-
margin-block: var(--display-tooltip-defined-title-margin-block, 0 0);
|
|
86
|
-
font-size: var(--display-tooltip-defined-title-font-size, 1.6rem);
|
|
87
|
-
font-weight: var(--display-tooltip-defined-title-font-weight, 700);
|
|
88
|
-
line-height: var(--display-tooltip-defined-title-line-height, 120%);
|
|
89
|
-
letter-spacing: var(--display-tooltip-defined-title-letter-spacing, normal);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.tooltip-body {
|
|
93
|
-
color: var(--display-tooltip-defined-body-colour, inherit);
|
|
94
|
-
margin-block: var(--display-tooltip-defined-body-margin-block, 0 0);
|
|
95
|
-
font-size: var(--display-tooltip-defined-body-font-size, 1.3rem);
|
|
96
|
-
font-weight: var(--display-tooltip-defined-body-font-weight, inherit);
|
|
97
|
-
line-height: var(--display-tooltip-defined-body-line-height, 140%);
|
|
98
|
-
letter-spacing: var(--display-tooltip-defined-body-letter-spacing, normal);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
.tooltip-action {
|
|
102
|
-
color: var(--display-tooltip-defined-action-colour, inherit);
|
|
103
|
-
margin-block: var(--display-tooltip-defined-action-margin-block, 0 0);
|
|
104
|
-
font-size: var(--display-tooltip-defined-action-font-size, 1.4rem);
|
|
105
|
-
font-weight: var(--display-tooltip-defined-action-font-weight, 700);
|
|
106
|
-
line-height: var(--display-tooltip-defined-action-line-height, 130%);
|
|
107
|
-
letter-spacing: var(--display-tooltip-defined-action-letter-spacing, normal);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
.display-tooltip-close-button {
|
|
111
|
-
align-self: flex-end;
|
|
112
|
-
margin-block: var(--display-tooltip-defined-close-button-margin-block, 1.2rem 0);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
</style>
|
package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { ref } from "vue";
|
|
2
|
-
import DisplayTooltipDefined from "../DisplayTooltipDefined.vue";
|
|
3
|
-
import DisplayTooltip from "../../../01.atoms/display-tooltip/DisplayTooltip.vue";
|
|
4
|
-
import { useTooltipsGuide } from "~/composables/useTooltips";
|
|
5
|
-
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
6
|
-
import type { TooltipContentText } from "~/types/components";
|
|
7
|
-
|
|
8
|
-
const contentText: TooltipContentText = {
|
|
9
|
-
tooltipTitle: { tag: "h4", text: "Free delivery" },
|
|
10
|
-
tooltipContent: { tag: "p", text: "Orders over £50 qualify for free standard delivery." },
|
|
11
|
-
tooltipAction: { tag: "span", text: "See delivery policy" },
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const meta: Meta<typeof DisplayTooltipDefined> = {
|
|
15
|
-
title: "Molecules/DisplayTooltipDefined",
|
|
16
|
-
component: DisplayTooltipDefined,
|
|
17
|
-
argTypes: {
|
|
18
|
-
tooltipId: {
|
|
19
|
-
control: "text",
|
|
20
|
-
description: "Id used to link the trigger button to the popover. Auto-generated when omitted.",
|
|
21
|
-
table: { category: "Basic" },
|
|
22
|
-
},
|
|
23
|
-
contentText: { table: { disable: true } },
|
|
24
|
-
styleClassPassthrough: { table: { disable: true } },
|
|
25
|
-
},
|
|
26
|
-
args: {
|
|
27
|
-
contentText,
|
|
28
|
-
},
|
|
29
|
-
parameters: {
|
|
30
|
-
docs: {
|
|
31
|
-
description: {
|
|
32
|
-
component:
|
|
33
|
-
"Structured title/body/action content variant of DisplayTooltip, plus a built-in close button wired to the popover.",
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export default meta;
|
|
40
|
-
type Story = StoryObj<typeof DisplayTooltipDefined>;
|
|
41
|
-
|
|
42
|
-
export const Default: Story = {
|
|
43
|
-
render: (args) => ({
|
|
44
|
-
components: { DisplayTooltipDefined },
|
|
45
|
-
setup() {
|
|
46
|
-
return { args };
|
|
47
|
-
},
|
|
48
|
-
template: `<div style="padding: 8rem;"><DisplayTooltipDefined v-bind="args" /></div>`,
|
|
49
|
-
}),
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export const TitleOnly: Story = {
|
|
53
|
-
args: {
|
|
54
|
-
contentText: { tooltipTitle: { tag: "h4", text: "Just a title" } },
|
|
55
|
-
},
|
|
56
|
-
render: (args) => ({
|
|
57
|
-
components: { DisplayTooltipDefined },
|
|
58
|
-
setup() {
|
|
59
|
-
return { args };
|
|
60
|
-
},
|
|
61
|
-
template: `<div style="padding: 8rem;"><DisplayTooltipDefined v-bind="args" /></div>`,
|
|
62
|
-
}),
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export const GuidedTour: Story = {
|
|
66
|
-
parameters: {
|
|
67
|
-
docs: {
|
|
68
|
-
description: {
|
|
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 (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
|
-
},
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
render: () => ({
|
|
75
|
-
components: { DisplayTooltipDefined, DisplayTooltip },
|
|
76
|
-
setup() {
|
|
77
|
-
const guideContainer = ref<HTMLElement | null>(null);
|
|
78
|
-
const { isGuideRunning, restartGuide, currentTooltipIndex, totalPopovers } = useTooltipsGuide(guideContainer, {
|
|
79
|
-
autoStart: true,
|
|
80
|
-
startDelay: 800,
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
return { guideContainer, isGuideRunning, restartGuide, currentTooltipIndex, totalPopovers };
|
|
84
|
-
},
|
|
85
|
-
template: `
|
|
86
|
-
<div style="padding: 4rem; display: grid; gap: 2rem; max-width: 48rem;">
|
|
87
|
-
<div style="display: flex; align-items: center; gap: 1.2rem;">
|
|
88
|
-
<button @click="restartGuide" :disabled="isGuideRunning" style="padding: 0.6rem 1.2rem;">
|
|
89
|
-
{{ isGuideRunning ? 'Guide running…' : 'Run guide again' }}
|
|
90
|
-
</button>
|
|
91
|
-
<span v-if="isGuideRunning" style="font-size: 1.3rem; color: #666;">
|
|
92
|
-
Step {{ currentTooltipIndex + 1 }} of {{ totalPopovers }} — click Next (Close on the last step) to advance
|
|
93
|
-
</span>
|
|
94
|
-
</div>
|
|
95
|
-
|
|
96
|
-
<div ref="guideContainer" style="display: grid; gap: 2rem;">
|
|
97
|
-
<DisplayTooltipDefined
|
|
98
|
-
close-button-text="Next"
|
|
99
|
-
close-button-aria-label="Next step"
|
|
100
|
-
:content-text="{
|
|
101
|
-
tooltipTitle: { tag: 'h3', text: 'Step 1' },
|
|
102
|
-
tooltipContent: { tag: 'p', text: 'This step uses DisplayTooltipDefined — structured title/body/action content plus a built-in close button.' },
|
|
103
|
-
tooltipAction: { tag: 'p', text: 'Step 1 of 2' },
|
|
104
|
-
}"
|
|
105
|
-
>
|
|
106
|
-
<template #triggerContent>Trigger from DisplayTooltipDefined</template>
|
|
107
|
-
</DisplayTooltipDefined>
|
|
108
|
-
|
|
109
|
-
<DisplayTooltip tooltip-id="guided-tour-step-2">
|
|
110
|
-
<template #triggerContent>Trigger from plain DisplayTooltip</template>
|
|
111
|
-
<template #tooltipContent="{ close }">
|
|
112
|
-
<p>This step uses plain DisplayTooltip — you supply the close button yourself via the tooltipContent slot.</p>
|
|
113
|
-
<button
|
|
114
|
-
popovertarget="guided-tour-step-2"
|
|
115
|
-
popovertargetaction="hide"
|
|
116
|
-
class="display-tooltip-close-button"
|
|
117
|
-
style="align-self: flex-end;"
|
|
118
|
-
aria-label="Close tool tip"
|
|
119
|
-
@click="close"
|
|
120
|
-
>
|
|
121
|
-
Close
|
|
122
|
-
</button>
|
|
123
|
-
</template>
|
|
124
|
-
</DisplayTooltip>
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
127
|
-
`,
|
|
128
|
-
}),
|
|
129
|
-
};
|
package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
-
import DisplayTooltipDefined from "../DisplayTooltipDefined.vue";
|
|
4
|
-
import type { TooltipContentText } from "~/types/components";
|
|
5
|
-
|
|
6
|
-
const contentText: TooltipContentText = {
|
|
7
|
-
tooltipTitle: { tag: "h4", text: "Title" },
|
|
8
|
-
tooltipContent: { tag: "p", text: "Body copy" },
|
|
9
|
-
tooltipAction: { tag: "span", text: "Learn more" },
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
describe("DisplayTooltipDefined", () => {
|
|
13
|
-
it("mounts without error", async () => {
|
|
14
|
-
const wrapper = await mountSuspended(DisplayTooltipDefined);
|
|
15
|
-
expect(wrapper.vm).toBeTruthy();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("renders correct HTML structure", async () => {
|
|
19
|
-
const wrapper = await mountSuspended(DisplayTooltipDefined, { props: { contentText, tooltipId: "fixed" } });
|
|
20
|
-
expect(wrapper.html()).toMatchSnapshot();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("renders the title, body, and action from contentText", async () => {
|
|
24
|
-
const wrapper = await mountSuspended(DisplayTooltipDefined, { props: { contentText } });
|
|
25
|
-
expect(wrapper.find(".tooltip-title").text()).toBe("Title");
|
|
26
|
-
expect(wrapper.find(".tooltip-body").text()).toBe("Body copy");
|
|
27
|
-
expect(wrapper.find(".tooltip-action").text()).toBe("Learn more");
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("renders each contentText field with its given tag", async () => {
|
|
31
|
-
const wrapper = await mountSuspended(DisplayTooltipDefined, { props: { contentText } });
|
|
32
|
-
expect(wrapper.find(".tooltip-title").element.tagName.toLowerCase()).toBe("h4");
|
|
33
|
-
expect(wrapper.find(".tooltip-body").element.tagName.toLowerCase()).toBe("p");
|
|
34
|
-
expect(wrapper.find(".tooltip-action").element.tagName.toLowerCase()).toBe("span");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("omits a field when not present in contentText", async () => {
|
|
38
|
-
const wrapper = await mountSuspended(DisplayTooltipDefined, {
|
|
39
|
-
props: { contentText: { tooltipTitle: { tag: "h4", text: "Only a title" } } },
|
|
40
|
-
});
|
|
41
|
-
expect(wrapper.find(".tooltip-title").exists()).toBe(true);
|
|
42
|
-
expect(wrapper.find(".tooltip-body").exists()).toBe(false);
|
|
43
|
-
expect(wrapper.find(".tooltip-action").exists()).toBe(false);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("renders a close button wired to the tooltip id", async () => {
|
|
47
|
-
const wrapper = await mountSuspended(DisplayTooltipDefined, { props: { tooltipId: "my-tip" } });
|
|
48
|
-
const closeButton = wrapper.find(".display-tooltip-close-button");
|
|
49
|
-
expect(closeButton.attributes("popovertarget")).toBe("nuxt-tooltip-my-tip");
|
|
50
|
-
expect(closeButton.attributes("popovertargetaction")).toBe("hide");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("renders the triggerContent slot", async () => {
|
|
54
|
-
const wrapper = await mountSuspended(DisplayTooltipDefined, {
|
|
55
|
-
slots: { triggerContent: "<span class='slot-trigger'>?</span>" },
|
|
56
|
-
});
|
|
57
|
-
expect(wrapper.find(".slot-trigger").text()).toBe("?");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("applies styleClassPassthrough classes to the underlying DisplayTooltip", async () => {
|
|
61
|
-
const wrapper = await mountSuspended(DisplayTooltipDefined, {
|
|
62
|
-
props: { styleClassPassthrough: ["custom-class"] },
|
|
63
|
-
});
|
|
64
|
-
expect(wrapper.classes()).toContain("custom-class");
|
|
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
|
-
});
|
|
85
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
-
|
|
3
|
-
exports[`DisplayTooltipDefined > renders correct HTML structure 1`] = `
|
|
4
|
-
"<div class="display-tooltip-core" style="--_anchor-name: --tooltip-anchor-v-0-0-0;">
|
|
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" aria-expanded="false"><span class="iconify i-fa7-solid:circle-question display-tooltip-trigger-icon" aria-hidden="true"></span></button>
|
|
7
|
-
</div>
|
|
8
|
-
<div id="nuxt-tooltip-fixed" popover="" class="display-tooltip-popover">
|
|
9
|
-
<div class="display-tooltip-popover-content">
|
|
10
|
-
<div class="popover-content-defined">
|
|
11
|
-
<h4 class="tooltip-title subtitle-sm">Title</h4>
|
|
12
|
-
<p class="tooltip-body body-sm">Body copy</p><span class="tooltip-action input-value">Learn more</span><button popovertarget="nuxt-tooltip-fixed" popovertargetaction="hide" class="display-tooltip-close-button" aria-label="Close tool tip">Close</button>
|
|
13
|
-
</div>
|
|
14
|
-
</div>
|
|
15
|
-
</div>
|
|
16
|
-
</div>"
|
|
17
|
-
`;
|
|
File without changes
|