srcdev-nuxt-components 9.6.13 → 9.6.14
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/story-register-components.md +1 -1
- package/.claude/skills/component-compliance-checklist.md +5 -0
- package/.claude/skills/components/display-qr-code.md +22 -4
- package/.claude/skills/components/eyebrow-text.md +9 -5
- package/.claude/skills/components/header-block.md +13 -5
- package/.claude/skills/components/hero-text.md +13 -9
- package/.claude/skills/components/link-text.md +8 -5
- package/.claude/skills/components/pop-over.md +17 -3
- package/.claude/skills/storybook-add-story.md +5 -2
- package/.claude/skills/testing-add-playwright.md +12 -7
- package/.vscode/srcdev-component-display-qr-code.code-snippets +12 -1
- package/.vscode/srcdev-component-hero-text.code-snippets +21 -5
- package/.vscode/srcdev-component-link-text.code-snippets +3 -3
- package/.vscode/srcdev-component-pop-over.code-snippets +28 -0
- package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +42 -9
- package/app/components/01.atoms/pop-over/PopOver.vue +28 -12
- package/app/components/01.atoms/pop-over/stories/PopOver.stories.ts +114 -2
- package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +35 -0
- package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +5 -1
- package/app/components/01.atoms/qr-code/CONSUMER-STYLING.md +16 -3
- package/app/components/01.atoms/qr-code/DisplayQrCode.vue +31 -0
- package/app/components/01.atoms/qr-code/stories/DisplayQrCode.stories.ts +190 -157
- package/app/components/01.atoms/qr-code/tests/DisplayQrCode.spec.ts +58 -0
- package/app/components/01.atoms/text-blocks/eyebrow-text/CONSUMER-STYLING.md +9 -1
- package/app/components/01.atoms/text-blocks/eyebrow-text/EyebrowText.vue +5 -4
- package/app/components/01.atoms/text-blocks/eyebrow-text/playwright/eyebrow-text.playwright.ts +1 -1
- package/app/components/01.atoms/text-blocks/eyebrow-text/stories/EyebrowText.stories.ts +104 -17
- package/app/components/01.atoms/text-blocks/eyebrow-text/tests/EyebrowText.spec.ts +68 -118
- package/app/components/01.atoms/text-blocks/eyebrow-text/tests/__snapshots__/EyebrowText.spec.ts.snap +8 -8
- package/app/components/01.atoms/text-blocks/header-block/CONSUMER-STYLING.md +49 -24
- package/app/components/01.atoms/text-blocks/header-block/HeaderBlock.vue +9 -1
- package/app/components/01.atoms/text-blocks/header-block/stories/HeaderBlock.stories.ts +96 -26
- package/app/components/01.atoms/text-blocks/header-block/tests/HeaderBlock.spec.ts +64 -48
- package/app/components/01.atoms/text-blocks/header-block/tests/__snapshots__/HeaderBlock.spec.ts.snap +2 -2
- package/app/components/01.atoms/text-blocks/hero-text/CONSUMER-STYLING.md +22 -6
- package/app/components/01.atoms/text-blocks/hero-text/HeroText.vue +32 -23
- package/app/components/01.atoms/text-blocks/hero-text/playwright/hero-text.playwright.ts +1 -1
- package/app/components/01.atoms/text-blocks/hero-text/stories/HeroText.stories.ts +176 -23
- package/app/components/01.atoms/text-blocks/hero-text/tests/HeroText.spec.ts +129 -123
- package/app/components/01.atoms/text-blocks/hero-text/tests/__snapshots__/HeroText.spec.ts.snap +2 -2
- package/app/components/01.atoms/text-blocks/link-text/CONSUMER-STYLING.md +17 -0
- package/app/components/01.atoms/text-blocks/link-text/LinkText.vue +9 -3
- package/app/components/01.atoms/text-blocks/link-text/stories/LinkText.stories.ts +96 -99
- package/app/components/01.atoms/text-blocks/link-text/tests/LinkText.spec.ts +84 -93
- package/app/components/02.molecules/price-list/tests/__snapshots__/PriceList.spec.ts.snap +8 -8
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +15 -15
- package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +3 -3
- package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +3 -3
- package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +3 -3
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +1 -1
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +9 -9
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +9 -9
- package/package.json +1 -1
|
@@ -28,21 +28,25 @@
|
|
|
28
28
|
:class="{ 'pop-over-popover-open': usesFallbackPopover && isOpen }"
|
|
29
29
|
:style="positionStyle"
|
|
30
30
|
:data-placement="popoverPlacement"
|
|
31
|
+
:role="popoverAriaLabel ? 'dialog' : undefined"
|
|
31
32
|
:aria-label="popoverAriaLabel || undefined"
|
|
32
33
|
@beforetoggle="handleBeforeToggle"
|
|
33
34
|
@toggle="handleToggle"
|
|
34
35
|
>
|
|
35
|
-
<
|
|
36
|
-
|
|
36
|
+
<InputButton
|
|
37
|
+
type="button"
|
|
38
|
+
variant="inline"
|
|
37
39
|
:popovertarget="popoverId"
|
|
38
40
|
popovertargetaction="hide"
|
|
39
|
-
|
|
40
|
-
class="pop-over-close-button"
|
|
41
|
+
:button-text="closeButtonAriaLabel"
|
|
41
42
|
:aria-label="closeButtonAriaLabel"
|
|
43
|
+
style-class-passthrough="pop-over-close-button"
|
|
42
44
|
@click="hide"
|
|
43
45
|
>
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
+
<template #iconOnly>
|
|
47
|
+
<Icon :name="closeIcon" class="pop-over-close-button-icon" aria-hidden="true" />
|
|
48
|
+
</template>
|
|
49
|
+
</InputButton>
|
|
46
50
|
<div class="pop-over-content">
|
|
47
51
|
<slot name="content"></slot>
|
|
48
52
|
</div>
|
|
@@ -56,10 +60,12 @@ interface Props {
|
|
|
56
60
|
placement?: "top" | "right" | "bottom" | "left";
|
|
57
61
|
/** aria-label on the trigger button — set this when the trigger slot is icon-only. */
|
|
58
62
|
triggerAriaLabel?: string;
|
|
59
|
-
/**
|
|
63
|
+
/** Names the panel and gives it role="dialog"; set this when the content slot has no visible heading. */
|
|
60
64
|
popoverAriaLabel?: string;
|
|
61
65
|
/** aria-label on the close button — override for localisation. */
|
|
62
66
|
closeButtonAriaLabel?: string;
|
|
67
|
+
/** Iconify name for the close button icon. */
|
|
68
|
+
closeIcon?: string;
|
|
63
69
|
styleClassPassthrough?: string | string[];
|
|
64
70
|
}
|
|
65
71
|
|
|
@@ -68,6 +74,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
68
74
|
triggerAriaLabel: "",
|
|
69
75
|
popoverAriaLabel: "",
|
|
70
76
|
closeButtonAriaLabel: "Close",
|
|
77
|
+
closeIcon: "lucide:x",
|
|
71
78
|
styleClassPassthrough: () => [],
|
|
72
79
|
});
|
|
73
80
|
|
|
@@ -78,7 +85,6 @@ const anchorName = `--pop-over-anchor-${id}`;
|
|
|
78
85
|
const rootRef = ref<HTMLDivElement | null>(null);
|
|
79
86
|
const triggerRef = ref<HTMLButtonElement | null>(null);
|
|
80
87
|
const popoverRef = ref<HTMLDivElement | null>(null);
|
|
81
|
-
const closeButtonRef = ref<HTMLButtonElement | null>(null);
|
|
82
88
|
|
|
83
89
|
// Focus moves to the close button on open; closing returns it to the trigger in every mode.
|
|
84
90
|
const {
|
|
@@ -95,7 +101,7 @@ const {
|
|
|
95
101
|
triggerRef,
|
|
96
102
|
popoverRef,
|
|
97
103
|
side: () => props.placement,
|
|
98
|
-
onOpen: () =>
|
|
104
|
+
onOpen: () => popoverRef.value?.querySelector<HTMLElement>(".pop-over-close-button")?.focus(),
|
|
99
105
|
});
|
|
100
106
|
|
|
101
107
|
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
@@ -113,11 +119,14 @@ watch(
|
|
|
113
119
|
.pop-over {
|
|
114
120
|
display: inline-block;
|
|
115
121
|
position: relative;
|
|
122
|
+
max-inline-size: 100%;
|
|
116
123
|
|
|
117
124
|
.pop-over-trigger {
|
|
118
125
|
all: unset;
|
|
119
126
|
cursor: pointer;
|
|
120
127
|
anchor-name: var(--_anchor-name);
|
|
128
|
+
max-inline-size: 100%;
|
|
129
|
+
overflow-wrap: anywhere;
|
|
121
130
|
outline: var(--pop-over-trigger-outline-width, 0.1rem) solid transparent;
|
|
122
131
|
|
|
123
132
|
&:focus-visible {
|
|
@@ -141,7 +150,10 @@ watch(
|
|
|
141
150
|
--pop-over-shadow,
|
|
142
151
|
0 0.4rem 1.6rem rgba(0, 0, 0, 0.12)
|
|
143
152
|
);
|
|
144
|
-
width: var(--pop-over-width, 30rem);
|
|
153
|
+
width: min(var(--pop-over-width, 30rem), calc(100vw - 3.2rem));
|
|
154
|
+
max-block-size: var(--pop-over-max-block-size, calc(100dvh - 3.2rem));
|
|
155
|
+
overflow-y: auto;
|
|
156
|
+
overflow-wrap: anywhere;
|
|
145
157
|
|
|
146
158
|
opacity: 0;
|
|
147
159
|
transition:
|
|
@@ -169,8 +181,8 @@ watch(
|
|
|
169
181
|
all: unset;
|
|
170
182
|
cursor: pointer;
|
|
171
183
|
position: absolute;
|
|
172
|
-
|
|
173
|
-
|
|
184
|
+
inset-block-start: var(--pop-over-close-button-offset, 0.8rem);
|
|
185
|
+
inset-inline-end: var(--pop-over-close-button-offset, 0.8rem);
|
|
174
186
|
display: grid;
|
|
175
187
|
place-items: center;
|
|
176
188
|
width: var(--pop-over-close-button-size, 2.4rem);
|
|
@@ -186,6 +198,10 @@ watch(
|
|
|
186
198
|
outline-offset: 0.1rem;
|
|
187
199
|
}
|
|
188
200
|
|
|
201
|
+
.input-button-icon {
|
|
202
|
+
margin: 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
189
205
|
.pop-over-close-button-icon {
|
|
190
206
|
width: var(--pop-over-close-button-icon-size, 1.4rem);
|
|
191
207
|
height: var(--pop-over-close-button-icon-size, 1.4rem);
|
|
@@ -18,7 +18,7 @@ const meta: Meta<typeof PopOver> = {
|
|
|
18
18
|
},
|
|
19
19
|
popoverAriaLabel: {
|
|
20
20
|
control: "text",
|
|
21
|
-
description: "
|
|
21
|
+
description: "Names the popover panel and gives it role=\"dialog\"; set this when the content slot has no visible heading.",
|
|
22
22
|
table: { category: "Accessibility" },
|
|
23
23
|
},
|
|
24
24
|
closeButtonAriaLabel: {
|
|
@@ -26,17 +26,23 @@ const meta: Meta<typeof PopOver> = {
|
|
|
26
26
|
description: "aria-label on the close button — override for localisation.",
|
|
27
27
|
table: { category: "Accessibility" },
|
|
28
28
|
},
|
|
29
|
+
closeIcon: {
|
|
30
|
+
control: "text",
|
|
31
|
+
description: "Iconify name for the close button icon.",
|
|
32
|
+
table: { category: "Basic" },
|
|
33
|
+
},
|
|
29
34
|
styleClassPassthrough: { table: { disable: true } },
|
|
30
35
|
},
|
|
31
36
|
args: {
|
|
32
37
|
placement: "right",
|
|
33
38
|
closeButtonAriaLabel: "Close",
|
|
39
|
+
closeIcon: "lucide:x",
|
|
34
40
|
},
|
|
35
41
|
parameters: {
|
|
36
42
|
docs: {
|
|
37
43
|
description: {
|
|
38
44
|
component:
|
|
39
|
-
"A generic anchor-positioned disclosure panel
|
|
45
|
+
"A generic anchor-positioned disclosure panel built on the native Popover API and CSS anchor positioning, with fallbacks for Safari 16-18. The `trigger` and `content` slots are entirely consumer-supplied; `placement` picks the side the panel opens on (it flips when there is no room), and the close button is labelled by `closeButtonAriaLabel` with its icon set by `closeIcon`. Set `triggerAriaLabel` for an icon-only trigger and `popoverAriaLabel` when the content has no visible heading. See DisplayTooltip for a fixed icon-trigger variant, or ActionMenu for one with menu semantics.",
|
|
40
46
|
},
|
|
41
47
|
},
|
|
42
48
|
},
|
|
@@ -46,6 +52,14 @@ export default meta;
|
|
|
46
52
|
type Story = StoryObj<typeof PopOver>;
|
|
47
53
|
|
|
48
54
|
export const Default: Story = {
|
|
55
|
+
parameters: {
|
|
56
|
+
docs: {
|
|
57
|
+
description: {
|
|
58
|
+
story:
|
|
59
|
+
"Plain text trigger and a single paragraph of content. Use the Controls panel to change the placement and labels.",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
49
63
|
render: (args) => ({
|
|
50
64
|
components: { PopOver },
|
|
51
65
|
setup() {
|
|
@@ -67,6 +81,14 @@ export const Default: Story = {
|
|
|
67
81
|
};
|
|
68
82
|
|
|
69
83
|
export const Placements: Story = {
|
|
84
|
+
parameters: {
|
|
85
|
+
docs: {
|
|
86
|
+
description: {
|
|
87
|
+
story:
|
|
88
|
+
"One instance per `placement` value. Each panel flips to the opposite side when it would overflow the viewport.",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
70
92
|
render: (args) => ({
|
|
71
93
|
components: { PopOver },
|
|
72
94
|
setup() {
|
|
@@ -96,6 +118,14 @@ export const Placements: Story = {
|
|
|
96
118
|
};
|
|
97
119
|
|
|
98
120
|
export const IconTrigger: Story = {
|
|
121
|
+
parameters: {
|
|
122
|
+
docs: {
|
|
123
|
+
description: {
|
|
124
|
+
story:
|
|
125
|
+
"An icon-only trigger: `triggerAriaLabel` gives the button its accessible name, and `popoverAriaLabel` names the panel because the content has no visible heading.",
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
99
129
|
render: (args) => ({
|
|
100
130
|
components: { PopOver },
|
|
101
131
|
setup() {
|
|
@@ -117,6 +147,14 @@ export const IconTrigger: Story = {
|
|
|
117
147
|
};
|
|
118
148
|
|
|
119
149
|
export const CustomColours: Story = {
|
|
150
|
+
parameters: {
|
|
151
|
+
docs: {
|
|
152
|
+
description: {
|
|
153
|
+
story:
|
|
154
|
+
"Overriding the public colour tokens with inline custom properties on the component.",
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
120
158
|
render: (args) => ({
|
|
121
159
|
components: { PopOver },
|
|
122
160
|
setup() {
|
|
@@ -139,3 +177,77 @@ export const CustomColours: Story = {
|
|
|
139
177
|
`,
|
|
140
178
|
}),
|
|
141
179
|
};
|
|
180
|
+
|
|
181
|
+
export const StressTest: Story = {
|
|
182
|
+
name: "Stress Test (Worst-Case Data)",
|
|
183
|
+
args: {
|
|
184
|
+
triggerAriaLabel: "Weitereinformationenzurlieferunganzeigen 😀",
|
|
185
|
+
popoverAriaLabel: "Lieferbedingungenundrückgaberichtlinien",
|
|
186
|
+
closeButtonAriaLabel: "Schließenundzurückzurbestellübersicht",
|
|
187
|
+
},
|
|
188
|
+
render: (args) => ({
|
|
189
|
+
components: { PopOver },
|
|
190
|
+
setup() {
|
|
191
|
+
return { args };
|
|
192
|
+
},
|
|
193
|
+
template: `
|
|
194
|
+
<div style="display: grid; gap: 2.4rem; padding: 2.4rem; font-size: 1.4rem;">
|
|
195
|
+
<p style="margin: 0;">Long unbroken trigger text and worst-case content inside an 18rem column.</p>
|
|
196
|
+
<div style="inline-size: 18rem; outline: 0.1rem dashed currentColor;">
|
|
197
|
+
<PopOver v-bind="args">
|
|
198
|
+
<template #trigger>
|
|
199
|
+
<span>😀 Lieferkostenundversandbedingungenfürallebestellungen</span>
|
|
200
|
+
</template>
|
|
201
|
+
<template #content>
|
|
202
|
+
<h3 style="margin-block-end: 0.8rem;">Kostenloselieferungfürallebestellungenübereinemmindestbestellwert</h3>
|
|
203
|
+
<p>https://example.invalid/a/very/long/unbroken/url/that/never/offers/a/break/opportunity/anywhere/at/all</p>
|
|
204
|
+
<p><b>not bold</b> Bestellungen über fünfzig Pfund qualifizieren sich für die kostenlose Standardlieferung innerhalb des Vereinigten Königreichs, ausgenommen sind Inseln und abgelegene Regionen.</p>
|
|
205
|
+
</template>
|
|
206
|
+
</PopOver>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<p style="margin: 0;">An oversized <code>--pop-over-width</code> (80rem): the panel stays inside the viewport.</p>
|
|
210
|
+
<PopOver placement="bottom" style="--pop-over-width: 80rem;">
|
|
211
|
+
<template #trigger><span>Very wide panel</span></template>
|
|
212
|
+
<template #content><p>The panel is capped at the viewport width minus 3.2rem.</p></template>
|
|
213
|
+
</PopOver>
|
|
214
|
+
|
|
215
|
+
<p style="margin: 0;">Very tall content: the panel caps at the viewport height and scrolls.</p>
|
|
216
|
+
<PopOver placement="bottom">
|
|
217
|
+
<template #trigger><span>Very tall panel</span></template>
|
|
218
|
+
<template #content>
|
|
219
|
+
<p v-for="n in 30" :key="n">Paragraph {{ n }} of thirty.</p>
|
|
220
|
+
</template>
|
|
221
|
+
</PopOver>
|
|
222
|
+
|
|
223
|
+
<p style="margin: 0;">Empty content slot, and single characters in the trigger and content.</p>
|
|
224
|
+
<div style="display: flex; gap: 2.4rem;">
|
|
225
|
+
<PopOver placement="bottom">
|
|
226
|
+
<template #trigger><span>Empty content</span></template>
|
|
227
|
+
</PopOver>
|
|
228
|
+
<PopOver placement="bottom" trigger-aria-label="X">
|
|
229
|
+
<template #trigger><span>X</span></template>
|
|
230
|
+
<template #content><p>Y</p></template>
|
|
231
|
+
</PopOver>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<p style="margin: 0;">Right-to-left content at the right edge: the panel flips left and the close button sits at the inline end.</p>
|
|
235
|
+
<div dir="rtl" style="display: flex; justify-content: flex-start;">
|
|
236
|
+
<PopOver placement="right" close-button-aria-label="إغلاق">
|
|
237
|
+
<template #trigger><span>التوصيل المجاني</span></template>
|
|
238
|
+
<template #content><p>الطلبات التي تزيد عن خمسين جنيهًا مؤهلة للتوصيل المجاني.</p></template>
|
|
239
|
+
</PopOver>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
`,
|
|
243
|
+
}),
|
|
244
|
+
parameters: {
|
|
245
|
+
docs: {
|
|
246
|
+
story: { inline: false, height: "900px" },
|
|
247
|
+
description: {
|
|
248
|
+
story:
|
|
249
|
+
"Worst-case content. Open each popover and check: unbroken trigger text wraps inside the dashed column; unbroken heading, URL and German copy wrap inside the panel; HTML-like text renders as text; an oversized `--pop-over-width` never makes the panel wider than the viewport; very tall content scrolls inside a panel no taller than the viewport; an empty content slot leaves just the close button; right-to-left content puts the close button at the inline end and flips the panel when there is no room.",
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
};
|
|
@@ -104,6 +104,16 @@ describe("PopOver", () => {
|
|
|
104
104
|
expect(wrapper.find(".pop-over-popover").attributes("aria-label")).toBe("Filter options");
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
it("has no role without popoverAriaLabel", async () => {
|
|
108
|
+
wrapper = await createWrapper();
|
|
109
|
+
expect(wrapper.find(".pop-over-popover").attributes("role")).toBeUndefined();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("has role='dialog' when popoverAriaLabel is given", async () => {
|
|
113
|
+
wrapper = await createWrapper({ popoverAriaLabel: "Filter options" });
|
|
114
|
+
expect(wrapper.find(".pop-over-popover").attributes("role")).toBe("dialog");
|
|
115
|
+
});
|
|
116
|
+
|
|
107
117
|
it("sets data-placement on the root element", async () => {
|
|
108
118
|
wrapper = await createWrapper({ placement: "top" });
|
|
109
119
|
expect(wrapper.find(".pop-over").attributes("data-placement")).toBe("top");
|
|
@@ -133,6 +143,31 @@ describe("PopOver", () => {
|
|
|
133
143
|
expect(closeButton.attributes("popovertargetaction")).toBe("hide");
|
|
134
144
|
expect(closeButton.attributes("popovertarget")).toBe(vm.popoverId);
|
|
135
145
|
});
|
|
146
|
+
|
|
147
|
+
it("is an inline InputButton with type='button'", async () => {
|
|
148
|
+
wrapper = await createWrapper();
|
|
149
|
+
const closeButton = wrapper.find(".pop-over-close-button");
|
|
150
|
+
expect(closeButton.classes()).toContain("input-button");
|
|
151
|
+
expect(closeButton.attributes("data-variant")).toBe("inline");
|
|
152
|
+
expect(closeButton.attributes("type")).toBe("button");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("renders the close label as visually hidden button text", async () => {
|
|
156
|
+
wrapper = await createWrapper({ closeButtonAriaLabel: "Dismiss" });
|
|
157
|
+
const text = wrapper.find(".pop-over-close-button .input-button-text");
|
|
158
|
+
expect(text.text()).toBe("Dismiss");
|
|
159
|
+
expect(text.classes()).toContain("sr-only");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("uses lucide:x as the default close icon", async () => {
|
|
163
|
+
wrapper = await createWrapper();
|
|
164
|
+
expect(wrapper.find(".pop-over-close-button-icon").classes().join(" ")).toContain("lucide:x");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("uses a custom closeIcon", async () => {
|
|
168
|
+
wrapper = await createWrapper({ closeIcon: "mdi:close" });
|
|
169
|
+
expect(wrapper.find(".pop-over-close-button-icon").classes().join(" ")).toContain("mdi:close");
|
|
170
|
+
});
|
|
136
171
|
});
|
|
137
172
|
|
|
138
173
|
describe("handleToggle focus management", () => {
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
exports[`PopOver > renders correct HTML structure 1`] = `
|
|
4
4
|
"<div class="pop-over" style="--_anchor-name: --pop-over-anchor-v-0-0;" data-placement="right"><button popovertarget="pop-over-v-0-0" popovertargetaction="toggle" type="button" class="pop-over-trigger" aria-expanded="false"><span>Open</span></button>
|
|
5
|
-
<div id="pop-over-v-0-0" popover="" class="pop-over-popover"><button
|
|
5
|
+
<div id="pop-over-v-0-0" popover="" class="pop-over-popover"><button type="button" data-testid="input-button" data-theme="default" data-variant="inline" data-icon-only="" class="input-button pop-over-close-button" popovertarget="pop-over-v-0-0" popovertargetaction="hide" aria-label="Close">
|
|
6
|
+
<!--v-if-->
|
|
7
|
+
<!--v-if--><span class="input-button-text sr-only">Close</span>
|
|
8
|
+
<!--v-if--><span class="input-button-icon" data-slot="icon-only"><span class="iconify i-lucide:x pop-over-close-button-icon" aria-hidden="true"></span></span>
|
|
9
|
+
</button>
|
|
6
10
|
<div class="pop-over-content">
|
|
7
11
|
<p>Details</p>
|
|
8
12
|
</div>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
| Token | Default | Controls |
|
|
6
6
|
|---|---|---|
|
|
7
|
-
| `--display-qr-code-size` | the `size` prop (`256px`) | Width of the code; height follows via `aspect-ratio: 1 / 1` |
|
|
7
|
+
| `--display-qr-code-size` | the `size` prop (`256px`) | Width of the code; height follows via `aspect-ratio: 1 / 1`. Capped at `max-inline-size: 100%`, so it never overflows its container |
|
|
8
8
|
|
|
9
9
|
Module colours are props, not tokens: `black-color` (default `currentColor`) and `white-color`
|
|
10
10
|
(default `transparent`). They're written into the SVG by `nuxt-qrcode`. Because the dark modules
|
|
@@ -20,7 +20,19 @@ No private `--_` tokens.
|
|
|
20
20
|
## State hooks
|
|
21
21
|
|
|
22
22
|
The root is the `nuxt-qrcode` SVG with class `.display-qr-code`, `role="img"` and an
|
|
23
|
-
`aria-label` (the `aria-label` prop, default `"QR code"`).
|
|
23
|
+
`aria-label` (the `aria-label` prop, default `"QR code"`).
|
|
24
|
+
|
|
25
|
+
When the value is too long to encode, the root is instead a `<span>` with classes
|
|
26
|
+
`.display-qr-code.display-qr-code-fallback` and a `data-error` attribute, holding the `fallback`
|
|
27
|
+
slot. It keeps the same size and passthrough classes, centres its content, and has no role or
|
|
28
|
+
label (the slot content speaks for itself).
|
|
29
|
+
|
|
30
|
+
| Hook | Meaning |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `.display-qr-code-fallback`, `[data-error]` | The value couldn't be encoded; the `fallback` slot is showing |
|
|
33
|
+
|
|
34
|
+
> Changed 2026-10-19: the fallback root is new (an over-capacity value used to throw during
|
|
35
|
+
> render), and the code is now capped at its container's width.
|
|
24
36
|
|
|
25
37
|
---
|
|
26
38
|
|
|
@@ -45,4 +57,5 @@ Set the tokens above on an element you own (a page or section class, or a class
|
|
|
45
57
|
|
|
46
58
|
## Class passthrough
|
|
47
59
|
|
|
48
|
-
`style-class-passthrough` adds classes to the root SVG
|
|
60
|
+
`style-class-passthrough` adds classes to the root SVG, or to the fallback `<span>` when the value
|
|
61
|
+
can't be encoded. Reactive after mount.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<Qrcode
|
|
3
|
+
v-if="!hasEncodeError"
|
|
3
4
|
:value="qrValue"
|
|
4
5
|
class="display-qr-code"
|
|
5
6
|
:variant
|
|
@@ -10,6 +11,9 @@
|
|
|
10
11
|
:aria-label="ariaLabel"
|
|
11
12
|
:class="[elementClasses]"
|
|
12
13
|
/>
|
|
14
|
+
<span v-else class="display-qr-code display-qr-code-fallback" :class="[elementClasses]" data-error>
|
|
15
|
+
<slot name="fallback"></slot>
|
|
16
|
+
</span>
|
|
13
17
|
</template>
|
|
14
18
|
|
|
15
19
|
<script setup lang="ts">
|
|
@@ -36,6 +40,26 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
36
40
|
styleClassPassthrough: () => [],
|
|
37
41
|
});
|
|
38
42
|
|
|
43
|
+
const emit = defineEmits<{
|
|
44
|
+
error: [error: Error];
|
|
45
|
+
}>();
|
|
46
|
+
|
|
47
|
+
// The encoder throws while rendering when a value exceeds QR capacity (about 2.9KB).
|
|
48
|
+
const hasEncodeError = ref(false);
|
|
49
|
+
|
|
50
|
+
onErrorCaptured((error) => {
|
|
51
|
+
hasEncodeError.value = true;
|
|
52
|
+
emit("error", error instanceof Error ? error : new Error(String(error)));
|
|
53
|
+
return false;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
watch(
|
|
57
|
+
() => props.qrValue,
|
|
58
|
+
() => {
|
|
59
|
+
hasEncodeError.value = false;
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
|
|
39
63
|
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
40
64
|
|
|
41
65
|
watch(
|
|
@@ -49,6 +73,13 @@ watch(
|
|
|
49
73
|
.display-qr-code {
|
|
50
74
|
aspect-ratio: 1 / 1;
|
|
51
75
|
width: var(--display-qr-code-size, v-bind(size));
|
|
76
|
+
max-inline-size: 100%;
|
|
77
|
+
|
|
78
|
+
&.display-qr-code-fallback {
|
|
79
|
+
display: inline-grid;
|
|
80
|
+
place-items: center;
|
|
81
|
+
overflow-wrap: anywhere;
|
|
82
|
+
}
|
|
52
83
|
}
|
|
53
84
|
}
|
|
54
85
|
</style>
|