srcdev-nuxt-components 9.6.12 → 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.
Files changed (111) hide show
  1. package/.claude/rules/consumer/ids-from-useid.md +2 -1
  2. package/.claude/rules/consumer/story-register-components.md +1 -1
  3. package/.claude/rules/consumer/vfor-keys-from-data.md +14 -0
  4. package/.claude/skills/component-compliance-checklist.md +24 -1
  5. package/.claude/skills/component-naming.md +1 -0
  6. package/.claude/skills/components/banner-video.md +24 -30
  7. package/.claude/skills/components/content-docs.md +18 -5
  8. package/.claude/skills/components/display-dialog.md +16 -6
  9. package/.claude/skills/components/display-qr-code.md +22 -4
  10. package/.claude/skills/components/{display-tooltip-defined.md → display-tooltip-rich.md} +40 -42
  11. package/.claude/skills/components/display-tooltip.md +17 -6
  12. package/.claude/skills/components/eyebrow-text.md +9 -5
  13. package/.claude/skills/components/header-block.md +13 -5
  14. package/.claude/skills/components/hero-text.md +13 -9
  15. package/.claude/skills/components/link-text.md +8 -5
  16. package/.claude/skills/components/pop-over.md +17 -3
  17. package/.claude/skills/components/skip-links.md +16 -0
  18. package/.claude/skills/composable-tooltips-guide.md +4 -4
  19. package/.claude/skills/index.md +6 -6
  20. package/.claude/skills/storybook-add-story.md +5 -2
  21. package/.claude/skills/testing-add-playwright.md +12 -7
  22. package/.claude/skills/testing-add-unit-test.md +8 -0
  23. package/.vscode/srcdev-component-banner-video.code-snippets +38 -25
  24. package/.vscode/srcdev-component-content-docs.code-snippets +69 -60
  25. package/.vscode/srcdev-component-display-dialog.code-snippets +7 -5
  26. package/.vscode/srcdev-component-display-qr-code.code-snippets +12 -1
  27. package/.vscode/srcdev-component-display-tooltip-rich.code-snippets +50 -0
  28. package/.vscode/srcdev-component-display-tooltip.code-snippets +31 -0
  29. package/.vscode/srcdev-component-hero-text.code-snippets +21 -5
  30. package/.vscode/srcdev-component-link-text.code-snippets +3 -3
  31. package/.vscode/srcdev-component-pop-over.code-snippets +28 -0
  32. package/.vscode/srcdev-component-skip-links.code-snippets +1 -1
  33. package/app/components/01.atoms/banner-video/BannerVideo.vue +51 -19
  34. package/app/components/01.atoms/banner-video/CONSUMER-STYLING.md +59 -40
  35. package/app/components/01.atoms/banner-video/stories/BannerVideo.stories.ts +88 -1
  36. package/app/components/01.atoms/banner-video/tests/BannerVideo.spec.ts +127 -78
  37. package/app/components/01.atoms/banner-video/tests/__snapshots__/BannerVideo.spec.ts.snap +12 -4
  38. package/app/components/01.atoms/content-wrappers/docs-pages/CONSUMER-STYLING.md +42 -14
  39. package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +86 -112
  40. package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +134 -8
  41. package/app/components/01.atoms/content-wrappers/docs-pages/tests/ContentDocs.spec.ts +131 -65
  42. package/app/components/01.atoms/display-dialog/CONSUMER-STYLING.md +28 -3
  43. package/app/components/01.atoms/display-dialog/DisplayDialog.vue +39 -31
  44. package/app/components/01.atoms/display-dialog/stories/DisplayDialog.stories.ts +92 -1
  45. package/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts +87 -41
  46. package/app/components/01.atoms/display-dialog/tests/__snapshots__/DisplayDialog.spec.ts.snap +5 -1
  47. package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +70 -24
  48. package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +24 -12
  49. package/app/components/01.atoms/display-tooltip/stories/DisplayTooltip.stories.ts +89 -2
  50. package/app/components/01.atoms/display-tooltip/tests/DisplayTooltip.spec.ts +65 -18
  51. package/app/components/01.atoms/display-tooltip/tests/__snapshots__/DisplayTooltip.spec.ts.snap +3 -3
  52. package/app/components/01.atoms/navigation/skip-links/CONSUMER-STYLING.md +65 -19
  53. package/app/components/01.atoms/navigation/skip-links/SkipLinks.vue +20 -14
  54. package/app/components/01.atoms/navigation/skip-links/stories/SkipLinks.stories.ts +89 -20
  55. package/app/components/01.atoms/navigation/skip-links/tests/SkipLinks.spec.ts +70 -10
  56. package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +42 -9
  57. package/app/components/01.atoms/pop-over/PopOver.vue +28 -12
  58. package/app/components/01.atoms/pop-over/stories/PopOver.stories.ts +114 -2
  59. package/app/components/01.atoms/pop-over/tests/PopOver.spec.ts +35 -0
  60. package/app/components/01.atoms/pop-over/tests/__snapshots__/PopOver.spec.ts.snap +5 -1
  61. package/app/components/01.atoms/qr-code/CONSUMER-STYLING.md +16 -3
  62. package/app/components/01.atoms/qr-code/DisplayQrCode.vue +31 -0
  63. package/app/components/01.atoms/qr-code/stories/DisplayQrCode.stories.ts +190 -157
  64. package/app/components/01.atoms/qr-code/tests/DisplayQrCode.spec.ts +58 -0
  65. package/app/components/01.atoms/text-blocks/eyebrow-text/CONSUMER-STYLING.md +9 -1
  66. package/app/components/01.atoms/text-blocks/eyebrow-text/EyebrowText.vue +5 -4
  67. package/app/components/01.atoms/text-blocks/eyebrow-text/playwright/eyebrow-text.playwright.ts +1 -1
  68. package/app/components/01.atoms/text-blocks/eyebrow-text/stories/EyebrowText.stories.ts +104 -17
  69. package/app/components/01.atoms/text-blocks/eyebrow-text/tests/EyebrowText.spec.ts +68 -118
  70. package/app/components/01.atoms/text-blocks/eyebrow-text/tests/__snapshots__/EyebrowText.spec.ts.snap +8 -8
  71. package/app/components/01.atoms/text-blocks/header-block/CONSUMER-STYLING.md +49 -24
  72. package/app/components/01.atoms/text-blocks/header-block/HeaderBlock.vue +9 -1
  73. package/app/components/01.atoms/text-blocks/header-block/stories/HeaderBlock.stories.ts +96 -26
  74. package/app/components/01.atoms/text-blocks/header-block/tests/HeaderBlock.spec.ts +64 -48
  75. package/app/components/01.atoms/text-blocks/header-block/tests/__snapshots__/HeaderBlock.spec.ts.snap +2 -2
  76. package/app/components/01.atoms/text-blocks/hero-text/CONSUMER-STYLING.md +22 -6
  77. package/app/components/01.atoms/text-blocks/hero-text/HeroText.vue +32 -23
  78. package/app/components/01.atoms/text-blocks/hero-text/playwright/hero-text.playwright.ts +1 -1
  79. package/app/components/01.atoms/text-blocks/hero-text/stories/HeroText.stories.ts +176 -23
  80. package/app/components/01.atoms/text-blocks/hero-text/tests/HeroText.spec.ts +129 -123
  81. package/app/components/01.atoms/text-blocks/hero-text/tests/__snapshots__/HeroText.spec.ts.snap +2 -2
  82. package/app/components/01.atoms/text-blocks/link-text/CONSUMER-STYLING.md +17 -0
  83. package/app/components/01.atoms/text-blocks/link-text/LinkText.vue +9 -3
  84. package/app/components/01.atoms/text-blocks/link-text/stories/LinkText.stories.ts +96 -99
  85. package/app/components/01.atoms/text-blocks/link-text/tests/LinkText.spec.ts +84 -93
  86. package/app/components/02.molecules/display-tooltip-rich/CONSUMER-STYLING.md +110 -0
  87. package/app/components/02.molecules/display-tooltip-rich/DisplayTooltipRich.vue +148 -0
  88. package/app/components/02.molecules/display-tooltip-rich/stories/DisplayTooltipRich.stories.ts +254 -0
  89. package/app/components/02.molecules/display-tooltip-rich/tests/DisplayTooltipRich.spec.ts +156 -0
  90. package/app/components/02.molecules/display-tooltip-rich/tests/__snapshots__/DisplayTooltipRich.spec.ts.snap +22 -0
  91. package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +1 -0
  92. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +1 -1
  93. package/app/components/02.molecules/price-list/tests/__snapshots__/PriceList.spec.ts.snap +8 -8
  94. package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +1 -0
  95. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +15 -15
  96. package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +3 -3
  97. package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +3 -3
  98. package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +3 -3
  99. package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +1 -1
  100. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +9 -9
  101. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +9 -9
  102. package/app/composables/tests/useTooltips.spec.ts +3 -3
  103. package/app/types/components/index.ts +1 -1
  104. package/package.json +1 -1
  105. package/.vscode/srcdev-component-display-tooltip-defined.code-snippets +0 -29
  106. package/app/components/02.molecules/display-tooltip-defined/CONSUMER-STYLING.md +0 -53
  107. package/app/components/02.molecules/display-tooltip-defined/DisplayTooltipDefined.vue +0 -117
  108. package/app/components/02.molecules/display-tooltip-defined/stories/DisplayTooltipDefined.stories.ts +0 -129
  109. package/app/components/02.molecules/display-tooltip-defined/tests/DisplayTooltipDefined.spec.ts +0 -85
  110. package/app/components/02.molecules/display-tooltip-defined/tests/__snapshots__/DisplayTooltipDefined.spec.ts.snap +0 -17
  111. /package/app/types/components/{display-tooltip-defined.d.ts → display-tooltip-rich.d.ts} +0 -0
@@ -23,14 +23,18 @@
23
23
  </div>
24
24
 
25
25
  <div class="display-dialog-col-right">
26
- <button
26
+ <InputButton
27
+ type="button"
28
+ variant="inline"
27
29
  data-test-id="display-dialog-header-close"
28
- class="display-dialog-close"
29
- @click.prevent="closeDialog()"
30
+ :button-text="resolved.closeLabel"
31
+ style-class-passthrough="display-dialog-close"
32
+ @click="closeDialog()"
30
33
  >
31
- <Icon :name="resolved.closeIcon" class="icon" />
32
- <span class="sr-only">{{ resolved.closeLabel }}</span>
33
- </button>
34
+ <template #iconOnly>
35
+ <Icon :name="resolved.closeIcon" class="icon" aria-hidden="true" />
36
+ </template>
37
+ </InputButton>
34
38
  </div>
35
39
  </div>
36
40
  <div
@@ -139,19 +143,6 @@ onUnmounted(() => {
139
143
  .display-dialog {
140
144
  --_transition-duration: var(--display-dialog-transition-duration, 200ms);
141
145
 
142
- --_header-button-border-hover: var(
143
- --display-dialog-header-button-border-hover,
144
- 0.1rem solid var(--colour-text-default)
145
- );
146
- --_header-button-outline-hover: var(
147
- --display-dialog-header-button-outline-hover,
148
- 0.1rem solid var(--colour-text-default)
149
- );
150
- --_header-button-icon-color: var(
151
- --display-dialog-header-button-icon-color,
152
- var(--colour-text-default)
153
- );
154
-
155
146
  position: fixed;
156
147
  left: 0;
157
148
  top: 0;
@@ -211,13 +202,14 @@ onUnmounted(() => {
211
202
  --_inner-outline: var(--display-dialog-inner-outline, 0.1rem solid var(--colour-text-default));
212
203
 
213
204
  display: grid;
214
- grid-template-rows: auto 1fr auto;
205
+ grid-template-rows: auto minmax(0, 1fr) auto;
215
206
  border-radius: var(--_inner-border-radius);
216
207
  border: var(--_inner-border);
217
208
  outline: var(--_inner-outline);
218
209
  background-color: var(--display-dialog-inner-background, var(--page-bg));
219
210
  height: initial;
220
- width: 100vw;
211
+ max-inline-size: 100%;
212
+ max-block-size: 100%;
221
213
  overflow: hidden;
222
214
 
223
215
  &.alert {
@@ -250,30 +242,33 @@ onUnmounted(() => {
250
242
  }
251
243
 
252
244
  .display-dialog-header {
245
+ grid-row: 1;
253
246
  display: flex;
254
247
  align-items: center;
248
+ gap: var(--display-dialog-header-gap, 1.2rem);
255
249
  padding: var(--display-dialog-header-padding, 1.2rem);
256
250
 
257
251
  &[data-theme] {
258
- border-block-end: 0.2rem solid var(--theme-accent);
259
-
260
- .display-dialog-close .icon {
261
- color: var(--theme-accent);
262
- }
252
+ border-block-end: 0.2rem solid var(--display-dialog-header-accent-color, var(--theme-accent));
263
253
  }
264
254
 
265
255
  .display-dialog-col-left {
266
256
  flex: 1;
257
+ min-inline-size: 0;
258
+ overflow-wrap: anywhere;
267
259
  }
268
260
 
269
261
  .display-dialog-col-right {
262
+ flex: none;
270
263
  margin-inline-start: auto;
271
264
 
272
- .display-dialog-close {
273
- background-color: transparent;
265
+ .display-dialog-close.input-button {
266
+ box-sizing: border-box;
274
267
  display: block flex;
275
268
  align-items: center;
276
269
  justify-content: center;
270
+ min-height: auto;
271
+ background-color: transparent;
277
272
  margin: var(--display-dialog-header-button-margin, 0);
278
273
  padding: var(--display-dialog-header-button-padding, 0.4rem);
279
274
  border: var(--display-dialog-header-button-border, 0.1rem solid transparent);
@@ -287,21 +282,31 @@ onUnmounted(() => {
287
282
  &:hover,
288
283
  &:focus-visible {
289
284
  cursor: pointer;
290
- border: var(--_header-button-border-hover);
291
- outline: var(--_header-button-outline-hover);
285
+ border: var(--display-dialog-header-button-border-hover, 0.1rem solid var(--colour-text-default));
286
+ outline: var(--display-dialog-header-button-outline-hover, 0.1rem solid var(--colour-text-default));
287
+ }
288
+
289
+ .input-button-icon {
290
+ margin: 0;
292
291
  }
293
292
 
294
293
  .icon {
295
- color: var(--_header-button-icon-color);
294
+ color: var(--display-dialog-header-button-icon-color, var(--colour-text-default));
296
295
  display: block;
297
296
  font-size: var(--display-dialog-header-button-icon-size, 2.4rem);
297
+
298
+ .display-dialog-header[data-theme] & {
299
+ color: var(--display-dialog-header-accent-color, var(--theme-accent));
300
+ }
298
301
  }
299
302
  }
300
303
  }
301
304
  }
302
305
 
303
306
  .display-dialog-content {
307
+ grid-row: 2;
304
308
  overflow: hidden;
309
+ overflow-wrap: anywhere;
305
310
  padding: var(--display-dialog-content-padding, 1.2rem);
306
311
 
307
312
  &.allow-content-scroll {
@@ -313,7 +318,10 @@ onUnmounted(() => {
313
318
  }
314
319
 
315
320
  .display-dialog-footer {
321
+ grid-row: 3;
316
322
  display: flex;
323
+ flex-wrap: wrap;
324
+ overflow-wrap: anywhere;
317
325
  gap: var(--display-dialog-footer-gap, 1.2rem);
318
326
  justify-content: flex-end;
319
327
  padding: var(--display-dialog-footer-padding, 1.2rem);
@@ -10,6 +10,9 @@ type StoryArgs = {
10
10
  theme: "" | "info" | "success" | "warning" | "error";
11
11
  closeIcon: string;
12
12
  closeLabel: string;
13
+ stressShowTitle?: boolean;
14
+ stressShowContent?: boolean;
15
+ stressFooterButtons?: number;
13
16
  };
14
17
 
15
18
  export default {
@@ -72,8 +75,9 @@ export default {
72
75
  docs: {
73
76
  description: {
74
77
  component:
75
- "A dialog overlay with header, scrollable content, and footer action slots. Supports `dialog`, `confirm`, `alert`, `modal`, and `fullscreen` variants. The `alert` variant sets `role=alertdialog` and disables dismiss-on-outside-click and Escape.",
78
+ "A dialog overlay with header, scrollable content, and footer action slots. Supports `dialog`, `confirm`, `alert`, `modal`, and `fullscreen` variants. The `alert` variant sets `role=alertdialog` and disables dismiss-on-outside-click and Escape. Slots: `dialogTitle` (also names the dialog), `dialogContent`, `actionButtonLeft` and `actionButtonRight`. Open state is `v-model`; `closeLabel` and `closeIcon` set the header close button. With no `dialogTitle`, pass an `aria-label` (it falls through to the `<dialog>`). Every story renders in its own frame because the overlay is `position: fixed`.",
76
79
  },
80
+ story: { inline: false, height: "600px" },
77
81
  },
78
82
  },
79
83
  } as Meta<StoryArgs>;
@@ -200,3 +204,90 @@ ScrollableContent.parameters = {
200
204
  description: { story: "Content area scrolls independently when it overflows the fixed-height dialog panel." },
201
205
  },
202
206
  };
207
+
208
+ const stressLabels = [
209
+ "Abbrechenundzurückzurübersicht",
210
+ "😀 Bestätigen",
211
+ "<b>not bold</b>",
212
+ "إلغاء العملية",
213
+ "OK",
214
+ "Änderungenspeichernundfortfahrenohnezuwarten",
215
+ ];
216
+
217
+ export const StressTest: StoryFn<StoryArgs> = (args) => ({
218
+ components: { StorybookComponent },
219
+ setup() {
220
+ const open = ref(true);
221
+ return { args, open, stressLabels };
222
+ },
223
+ template: `
224
+ <div style="padding: 4rem; display: flex; justify-content: center;">
225
+ <button :style="'${triggerButtonStyles}'" @click="open = true">Open dialog</button>
226
+ <StorybookComponent
227
+ v-model="open"
228
+ :variant="args.variant"
229
+ :allow-content-scroll="args.allowContentScroll"
230
+ :justify-dialog="args.justifyDialog"
231
+ :align-dialog="args.alignDialog"
232
+ :theme="args.theme || undefined"
233
+ :close-icon="args.closeIcon"
234
+ :close-label="args.closeLabel"
235
+ :lock-viewport="false"
236
+ aria-label="Stress test dialog"
237
+ data-dialog-id="storybook-stress"
238
+ >
239
+ <template v-if="args.stressShowTitle" #dialogTitle>
240
+ <p style="margin: 0; font-weight: 700; font-size: 1.6rem;">😀 Bevorzugtekontaktmethodefürrückfragenundterminvereinbarungenbestätigen &lt;b&gt;not bold&lt;/b&gt; طريقة الاتصال المفضلة</p>
241
+ </template>
242
+ <template v-if="args.stressShowContent" #dialogContent>
243
+ <p style="margin: 0 0 1.2rem; line-height: 1.6;">https://example.invalid/a/very/long/unbroken/url/that/never/offers/a/break/opportunity/anywhere/in/its/path?query=withevenmoreunbrokentextandparameters</p>
244
+ <p style="margin: 0 0 1.2rem; line-height: 1.6;">Sind Sie sicher, dass Sie alle nicht gespeicherten Änderungen an Ihrem Profil, Ihren Benachrichtigungseinstellungen und Ihren gespeicherten Zahlungsmethoden unwiderruflich verwerfen möchten? Dieser Vorgang kann nicht rückgängig gemacht werden.</p>
245
+ <p dir="rtl" style="margin: 0 0 1.2rem; line-height: 1.6;">هل أنت متأكد أنك تريد تجاهل جميع التغييرات غير المحفوظة؟ لا يمكن التراجع عن هذا الإجراء.</p>
246
+ <p v-for="n in 12" :key="n" style="margin: 0 0 1.2rem; line-height: 1.6;">Paragraph {{ n }}: <b>&lt;script&gt;alert("x")&lt;/script&gt;</b> renders as text. Lorem ipsum odor amet, consectetuer adipiscing elit.</p>
247
+ </template>
248
+ <template v-if="args.stressFooterButtons" #actionButtonRight>
249
+ <button
250
+ v-for="label in stressLabels.slice(0, args.stressFooterButtons)"
251
+ :key="label"
252
+ :style="'${actionButtonStyles}'"
253
+ @click="open = false"
254
+ >{{ label }}</button>
255
+ </template>
256
+ </StorybookComponent>
257
+ </div>
258
+ `,
259
+ });
260
+ StressTest.storyName = "Stress Test (Worst-Case Data)";
261
+ StressTest.args = {
262
+ variant: "modal",
263
+ closeLabel: "Dialogfensterschließenundzurückkehren 😀",
264
+ stressShowTitle: true,
265
+ stressShowContent: true,
266
+ stressFooterButtons: 6,
267
+ };
268
+ StressTest.argTypes = {
269
+ stressShowTitle: {
270
+ control: { type: "boolean" },
271
+ description: "Story-only: render the `dialogTitle` slot (off shows the close button alone in the header)",
272
+ table: { category: "Stress test (story-only)" },
273
+ },
274
+ stressShowContent: {
275
+ control: { type: "boolean" },
276
+ description: "Story-only: render the `dialogContent` slot",
277
+ table: { category: "Stress test (story-only)" },
278
+ },
279
+ stressFooterButtons: {
280
+ control: { type: "range", min: 0, max: 6, step: 1 },
281
+ description: "Story-only: number of footer buttons in `actionButtonRight` (0 omits the footer)",
282
+ table: { category: "Stress test (story-only)" },
283
+ },
284
+ };
285
+ StressTest.parameters = {
286
+ docs: {
287
+ story: { inline: false, height: "700px" },
288
+ description: {
289
+ story:
290
+ "Worst-case content, opened on load. A long unbroken title with emoji, HTML-like and right-to-left text, an unbroken URL, long German and Arabic copy, escaped markup that must render as text, and six footer buttons with unbroken labels. Switch `variant` through all five: the panel never exceeds the viewport, the close button stays in the header's top-right corner, the title and content wrap instead of widening the panel, the footer buttons wrap onto further rows and stay visible (content is clipped, or scrolls with `allowContentScroll`, rather than pushing the footer off-screen). Toggle the slots off one at a time: with no content the footer keeps its own row (at the bottom of the fixed-height `dialog` panel) instead of stretching into the middle one, and with nothing but the header the panel is just the close button.",
291
+ },
292
+ },
293
+ };
@@ -10,6 +10,19 @@ const { useAppConfigMock } = vi.hoisted(() => ({
10
10
 
11
11
  mockNuxtImport("useAppConfig", () => useAppConfigMock);
12
12
 
13
+ const mounted: { unmount: () => void }[] = [];
14
+
15
+ const mountDialog = async (options: Parameters<typeof mountSuspended<typeof DisplayDialog>>[1] = {}) => {
16
+ const wrapper = await mountSuspended(DisplayDialog, options);
17
+ mounted.push(wrapper);
18
+ return wrapper;
19
+ };
20
+
21
+ const unmountDialog = (wrapper: { unmount: () => void }) => {
22
+ mounted.splice(mounted.indexOf(wrapper), 1);
23
+ wrapper.unmount();
24
+ };
25
+
13
26
  vi.mock("focus-trap-vue", () => ({
14
27
  FocusTrap: {
15
28
  name: "FocusTrap",
@@ -21,6 +34,8 @@ vi.mock("focus-trap-vue", () => ({
21
34
 
22
35
  describe("DisplayDialog", () => {
23
36
  afterEach(() => {
37
+ mounted.splice(0).forEach((wrapper) => wrapper.unmount());
38
+ vi.restoreAllMocks();
24
39
  document.body.classList.remove("lock");
25
40
  delete document.body.dataset.lockCount;
26
41
  });
@@ -28,7 +43,7 @@ describe("DisplayDialog", () => {
28
43
  // ─── Mount ────────────────────────────────────────────────────────────────
29
44
 
30
45
  it("mounts without error", async () => {
31
- const wrapper = await mountSuspended(DisplayDialog, {
46
+ const wrapper = await mountDialog({
32
47
  props: { dataDialogId: "test" },
33
48
  });
34
49
  expect(wrapper.vm).toBeTruthy();
@@ -37,7 +52,7 @@ describe("DisplayDialog", () => {
37
52
  // ─── Snapshot ─────────────────────────────────────────────────────────────
38
53
 
39
54
  it("renders correct HTML structure with all slots", async () => {
40
- const wrapper = await mountSuspended(DisplayDialog, {
55
+ const wrapper = await mountDialog({
41
56
  props: { dataDialogId: "test", modelValue: true },
42
57
  slots: {
43
58
  dialogTitle: "<p>Title</p>",
@@ -52,14 +67,14 @@ describe("DisplayDialog", () => {
52
67
  // ─── Root element ─────────────────────────────────────────────────────────
53
68
 
54
69
  it("renders as <dialog>", async () => {
55
- const wrapper = await mountSuspended(DisplayDialog, {
70
+ const wrapper = await mountDialog({
56
71
  props: { dataDialogId: "test" },
57
72
  });
58
73
  expect(wrapper.element.tagName).toBe("DIALOG");
59
74
  });
60
75
 
61
76
  it("always has the display-dialog class", async () => {
62
- const wrapper = await mountSuspended(DisplayDialog, {
77
+ const wrapper = await mountDialog({
63
78
  props: { dataDialogId: "test" },
64
79
  });
65
80
  expect(wrapper.classes()).toContain("display-dialog");
@@ -68,7 +83,7 @@ describe("DisplayDialog", () => {
68
83
  // ─── data-dialog-id ───────────────────────────────────────────────────────
69
84
 
70
85
  it("sets data-dialog-id attribute from prop", async () => {
71
- const wrapper = await mountSuspended(DisplayDialog, {
86
+ const wrapper = await mountDialog({
72
87
  props: { dataDialogId: "my-dialog" },
73
88
  });
74
89
  expect(wrapper.attributes("data-dialog-id")).toBe("my-dialog");
@@ -77,14 +92,14 @@ describe("DisplayDialog", () => {
77
92
  // ─── open attribute ───────────────────────────────────────────────────────
78
93
 
79
94
  it("sets [open] attribute when modelValue is true", async () => {
80
- const wrapper = await mountSuspended(DisplayDialog, {
95
+ const wrapper = await mountDialog({
81
96
  props: { dataDialogId: "test", modelValue: true },
82
97
  });
83
98
  expect(wrapper.attributes("open")).toBeDefined();
84
99
  });
85
100
 
86
101
  it("omits [open] attribute when modelValue is false", async () => {
87
- const wrapper = await mountSuspended(DisplayDialog, {
102
+ const wrapper = await mountDialog({
88
103
  props: { dataDialogId: "test", modelValue: false },
89
104
  });
90
105
  expect(wrapper.attributes("open")).toBeUndefined();
@@ -93,7 +108,7 @@ describe("DisplayDialog", () => {
93
108
  // ─── role ─────────────────────────────────────────────────────────────────
94
109
 
95
110
  it("has no explicit role by default — implicit from <dialog> element", async () => {
96
- const wrapper = await mountSuspended(DisplayDialog, {
111
+ const wrapper = await mountDialog({
97
112
  props: { dataDialogId: "test" },
98
113
  });
99
114
  expect(wrapper.attributes("role")).toBeUndefined();
@@ -102,7 +117,7 @@ describe("DisplayDialog", () => {
102
117
  it.each(["dialog", "modal", "confirm", "fullscreen"] as const)(
103
118
  "has no explicit role for variant='%s' — implicit from <dialog> element",
104
119
  async (variant) => {
105
- const wrapper = await mountSuspended(DisplayDialog, {
120
+ const wrapper = await mountDialog({
106
121
  props: { dataDialogId: "test", variant },
107
122
  });
108
123
  expect(wrapper.attributes("role")).toBeUndefined();
@@ -110,7 +125,7 @@ describe("DisplayDialog", () => {
110
125
  );
111
126
 
112
127
  it("has role='alertdialog' for variant='alert'", async () => {
113
- const wrapper = await mountSuspended(DisplayDialog, {
128
+ const wrapper = await mountDialog({
114
129
  props: { dataDialogId: "test", variant: "alert" },
115
130
  });
116
131
  expect(wrapper.attributes("role")).toBe("alertdialog");
@@ -119,7 +134,7 @@ describe("DisplayDialog", () => {
119
134
  // ─── Variant class on .display-dialog-inner ──────────────────────────────────────────────
120
135
 
121
136
  it("applies 'dialog' class to .display-dialog-inner by default", async () => {
122
- const wrapper = await mountSuspended(DisplayDialog, {
137
+ const wrapper = await mountDialog({
123
138
  props: { dataDialogId: "test" },
124
139
  });
125
140
  expect(wrapper.find(".display-dialog-inner").classes()).toContain("dialog");
@@ -128,7 +143,7 @@ describe("DisplayDialog", () => {
128
143
  it.each(["dialog", "modal", "confirm", "alert", "fullscreen"] as const)(
129
144
  "applies '%s' class to .display-dialog-inner",
130
145
  async (variant) => {
131
- const wrapper = await mountSuspended(DisplayDialog, {
146
+ const wrapper = await mountDialog({
132
147
  props: { dataDialogId: "test", variant },
133
148
  });
134
149
  expect(wrapper.find(".display-dialog-inner").classes()).toContain(variant);
@@ -138,7 +153,7 @@ describe("DisplayDialog", () => {
138
153
  // ─── Slots ────────────────────────────────────────────────────────────────
139
154
 
140
155
  it("renders dialogTitle slot in .display-dialog-col-left", async () => {
141
- const wrapper = await mountSuspended(DisplayDialog, {
156
+ const wrapper = await mountDialog({
142
157
  props: { dataDialogId: "test" },
143
158
  slots: { dialogTitle: "<p>My Title</p>" },
144
159
  });
@@ -146,14 +161,14 @@ describe("DisplayDialog", () => {
146
161
  });
147
162
 
148
163
  it("omits .display-dialog-col-left when dialogTitle slot is not provided", async () => {
149
- const wrapper = await mountSuspended(DisplayDialog, {
164
+ const wrapper = await mountDialog({
150
165
  props: { dataDialogId: "test" },
151
166
  });
152
167
  expect(wrapper.find(".display-dialog-col-left").exists()).toBe(false);
153
168
  });
154
169
 
155
170
  it("renders dialogContent slot in .display-dialog-content", async () => {
156
- const wrapper = await mountSuspended(DisplayDialog, {
171
+ const wrapper = await mountDialog({
157
172
  props: { dataDialogId: "test" },
158
173
  slots: { dialogContent: "<p>Content</p>" },
159
174
  });
@@ -161,14 +176,14 @@ describe("DisplayDialog", () => {
161
176
  });
162
177
 
163
178
  it("omits .display-dialog-content when dialogContent slot is not provided", async () => {
164
- const wrapper = await mountSuspended(DisplayDialog, {
179
+ const wrapper = await mountDialog({
165
180
  props: { dataDialogId: "test" },
166
181
  });
167
182
  expect(wrapper.find(".display-dialog-content").exists()).toBe(false);
168
183
  });
169
184
 
170
185
  it("renders .display-dialog-footer when actionButtonLeft slot is provided", async () => {
171
- const wrapper = await mountSuspended(DisplayDialog, {
186
+ const wrapper = await mountDialog({
172
187
  props: { dataDialogId: "test" },
173
188
  slots: { actionButtonLeft: "<button>Cancel</button>" },
174
189
  });
@@ -176,7 +191,7 @@ describe("DisplayDialog", () => {
176
191
  });
177
192
 
178
193
  it("renders .display-dialog-footer when actionButtonRight slot is provided", async () => {
179
- const wrapper = await mountSuspended(DisplayDialog, {
194
+ const wrapper = await mountDialog({
180
195
  props: { dataDialogId: "test" },
181
196
  slots: { actionButtonRight: "<button>Confirm</button>" },
182
197
  });
@@ -184,14 +199,14 @@ describe("DisplayDialog", () => {
184
199
  });
185
200
 
186
201
  it("omits .display-dialog-footer when neither action button slot is provided", async () => {
187
- const wrapper = await mountSuspended(DisplayDialog, {
202
+ const wrapper = await mountDialog({
188
203
  props: { dataDialogId: "test" },
189
204
  });
190
205
  expect(wrapper.find(".display-dialog-footer").exists()).toBe(false);
191
206
  });
192
207
 
193
208
  it("renders both action button slots inside .display-dialog-footer", async () => {
194
- const wrapper = await mountSuspended(DisplayDialog, {
209
+ const wrapper = await mountDialog({
195
210
  props: { dataDialogId: "test" },
196
211
  slots: {
197
212
  actionButtonLeft: "<button>Cancel</button>",
@@ -205,7 +220,7 @@ describe("DisplayDialog", () => {
205
220
  // ─── allowContentScroll ───────────────────────────────────────────────────
206
221
 
207
222
  it("does not apply allow-content-scroll class by default", async () => {
208
- const wrapper = await mountSuspended(DisplayDialog, {
223
+ const wrapper = await mountDialog({
209
224
  props: { dataDialogId: "test" },
210
225
  slots: { dialogContent: "<p>Content</p>" },
211
226
  });
@@ -213,7 +228,7 @@ describe("DisplayDialog", () => {
213
228
  });
214
229
 
215
230
  it("applies allow-content-scroll class when allowContentScroll=true", async () => {
216
- const wrapper = await mountSuspended(DisplayDialog, {
231
+ const wrapper = await mountDialog({
217
232
  props: { dataDialogId: "test", allowContentScroll: true },
218
233
  slots: { dialogContent: "<p>Content</p>" },
219
234
  });
@@ -223,7 +238,7 @@ describe("DisplayDialog", () => {
223
238
  // ─── Close button ─────────────────────────────────────────────────────────
224
239
 
225
240
  it("close button emits update:modelValue=false", async () => {
226
- const wrapper = await mountSuspended(DisplayDialog, {
241
+ const wrapper = await mountDialog({
227
242
  props: { dataDialogId: "test", modelValue: true },
228
243
  });
229
244
  await wrapper.find('[data-test-id="display-dialog-header-close"]').trigger("click");
@@ -231,26 +246,57 @@ describe("DisplayDialog", () => {
231
246
  });
232
247
 
233
248
  it("uses \"Close\" as the close button's screen-reader label by default", async () => {
234
- const wrapper = await mountSuspended(DisplayDialog, { props: { dataDialogId: "test" } });
249
+ const wrapper = await mountDialog({ props: { dataDialogId: "test" } });
235
250
  expect(wrapper.find(".display-dialog-close .sr-only").text()).toBe("Close");
236
251
  });
237
252
 
238
253
  it("uses the closeLabel prop for the close button's screen-reader label", async () => {
239
- const wrapper = await mountSuspended(DisplayDialog, { props: { dataDialogId: "test", closeLabel: "Fermer" } });
254
+ const wrapper = await mountDialog({ props: { dataDialogId: "test", closeLabel: "Fermer" } });
240
255
  expect(wrapper.find(".display-dialog-close .sr-only").text()).toBe("Fermer");
241
256
  });
242
257
 
258
+ it("renders the close button as an icon-only InputButton of type button", async () => {
259
+ const wrapper = await mountDialog({ props: { dataDialogId: "test" } });
260
+ const close = wrapper.find('[data-test-id="display-dialog-header-close"]');
261
+ expect(close.element.tagName).toBe("BUTTON");
262
+ expect(close.classes()).toEqual(expect.arrayContaining(["input-button", "display-dialog-close"]));
263
+ expect(close.attributes("type")).toBe("button");
264
+ expect(close.attributes("data-icon-only")).toBeDefined();
265
+ expect(close.find(".icon").attributes("aria-hidden")).toBe("true");
266
+ });
267
+
268
+ // ─── Accessible name ──────────────────────────────────────────────────────
269
+
270
+ it("labels the dialog by its title when dialogTitle is provided", async () => {
271
+ const wrapper = await mountDialog({
272
+ props: { dataDialogId: "test" },
273
+ slots: { dialogTitle: "<p>My Title</p>" },
274
+ });
275
+ const titleId = wrapper.find(".display-dialog-col-left").attributes("id");
276
+ expect(titleId).toBeTruthy();
277
+ expect(wrapper.attributes("aria-labelledby")).toBe(titleId);
278
+ });
279
+
280
+ it("passes a consumer aria-label through to the <dialog> when there is no title", async () => {
281
+ const wrapper = await mountDialog({
282
+ props: { dataDialogId: "test" },
283
+ attrs: { "aria-label": "Settings" },
284
+ });
285
+ expect(wrapper.attributes("aria-labelledby")).toBeUndefined();
286
+ expect(wrapper.attributes("aria-label")).toBe("Settings");
287
+ });
288
+
243
289
  // ─── styleClassPassthrough ────────────────────────────────────────────────
244
290
 
245
291
  it("applies a styleClassPassthrough string", async () => {
246
- const wrapper = await mountSuspended(DisplayDialog, {
292
+ const wrapper = await mountDialog({
247
293
  props: { dataDialogId: "test", styleClassPassthrough: "my-class" },
248
294
  });
249
295
  expect(wrapper.classes()).toContain("my-class");
250
296
  });
251
297
 
252
298
  it("updates classes when styleClassPassthrough changes", async () => {
253
- const wrapper = await mountSuspended(DisplayDialog, {
299
+ const wrapper = await mountDialog({
254
300
  props: { dataDialogId: "test", styleClassPassthrough: ["original"] },
255
301
  });
256
302
  await wrapper.setProps({ styleClassPassthrough: ["updated"] });
@@ -259,7 +305,7 @@ describe("DisplayDialog", () => {
259
305
  });
260
306
 
261
307
  it("applies multiple styleClassPassthrough classes from an array", async () => {
262
- const wrapper = await mountSuspended(DisplayDialog, {
308
+ const wrapper = await mountDialog({
263
309
  props: { dataDialogId: "test", styleClassPassthrough: ["class-a", "class-b"] },
264
310
  });
265
311
  expect(wrapper.classes()).toContain("class-a");
@@ -269,40 +315,40 @@ describe("DisplayDialog", () => {
269
315
  // ─── Body viewport lock ───────────────────────────────────────────────────
270
316
 
271
317
  it("adds lock class to body on mount when lockViewport=true", async () => {
272
- const wrapper = await mountSuspended(DisplayDialog, {
318
+ const wrapper = await mountDialog({
273
319
  props: { dataDialogId: "test", lockViewport: true },
274
320
  });
275
321
  expect(document.body.classList.contains("lock")).toBe(true);
276
- wrapper.unmount();
322
+ unmountDialog(wrapper);
277
323
  });
278
324
 
279
325
  it("does not add lock class to body when lockViewport=false", async () => {
280
- await mountSuspended(DisplayDialog, {
326
+ await mountDialog({
281
327
  props: { dataDialogId: "test", lockViewport: false },
282
328
  });
283
329
  expect(document.body.classList.contains("lock")).toBe(false);
284
330
  });
285
331
 
286
332
  it("removes lock class from body on unmount when lockViewport=true", async () => {
287
- const wrapper = await mountSuspended(DisplayDialog, {
333
+ const wrapper = await mountDialog({
288
334
  props: { dataDialogId: "test", lockViewport: true },
289
335
  });
290
336
  expect(document.body.classList.contains("lock")).toBe(true);
291
- wrapper.unmount();
337
+ unmountDialog(wrapper);
292
338
  expect(document.body.classList.contains("lock")).toBe(false);
293
339
  });
294
340
 
295
341
  it("does not remove lock when a second locking dialog is still open", async () => {
296
- const wrapper1 = await mountSuspended(DisplayDialog, {
342
+ const wrapper1 = await mountDialog({
297
343
  props: { dataDialogId: "a", lockViewport: true },
298
344
  });
299
- const wrapper2 = await mountSuspended(DisplayDialog, {
345
+ const wrapper2 = await mountDialog({
300
346
  props: { dataDialogId: "b", lockViewport: true },
301
347
  });
302
348
  expect(document.body.classList.contains("lock")).toBe(true);
303
- wrapper2.unmount();
349
+ unmountDialog(wrapper2);
304
350
  expect(document.body.classList.contains("lock")).toBe(true);
305
- wrapper1.unmount();
351
+ unmountDialog(wrapper1);
306
352
  expect(document.body.classList.contains("lock")).toBe(false);
307
353
  });
308
354
 
@@ -327,7 +373,7 @@ describe("DisplayDialog", () => {
327
373
  .find((c: string) => c !== "iconify" && c !== "icon") ?? "";
328
374
 
329
375
  it("uses hardcoded fallbacks when app.config has no displayDialog key", async () => {
330
- const wrapper = await mountSuspended(DisplayDialog, {
376
+ const wrapper = await mountDialog({
331
377
  props: { dataDialogId: "test" },
332
378
  slots: { dialogContent: "<p>Content</p>" },
333
379
  });
@@ -356,7 +402,7 @@ describe("DisplayDialog", () => {
356
402
  },
357
403
  },
358
404
  });
359
- const wrapper = await mountSuspended(DisplayDialog, {
405
+ const wrapper = await mountDialog({
360
406
  props: { dataDialogId: "test" },
361
407
  slots: { dialogContent: "<p>Content</p>" },
362
408
  });
@@ -385,7 +431,7 @@ describe("DisplayDialog", () => {
385
431
  },
386
432
  },
387
433
  });
388
- const wrapper = await mountSuspended(DisplayDialog, {
434
+ const wrapper = await mountDialog({
389
435
  props: {
390
436
  dataDialogId: "test",
391
437
  variant: "fullscreen",
@@ -412,7 +458,7 @@ describe("DisplayDialog", () => {
412
458
  icon: {},
413
459
  srcdev: { displayDialog: { lockViewport: false } },
414
460
  });
415
- await mountSuspended(DisplayDialog, { props: { dataDialogId: "test" } });
461
+ await mountDialog({ props: { dataDialogId: "test" } });
416
462
  expect(document.body.classList.contains("lock")).toBe(false);
417
463
  });
418
464
  });
@@ -7,7 +7,11 @@ exports[`DisplayDialog > renders correct HTML structure with all slots 1`] = `
7
7
  <div id="v-0-0" class="display-dialog-col-left">
8
8
  <p>Title</p>
9
9
  </div>
10
- <div class="display-dialog-col-right"><button data-test-id="display-dialog-header-close" class="display-dialog-close"><span class="iconify undefinedbitcoin-icons:cross-filled icon"></span><span class="sr-only">Close</span></button></div>
10
+ <div class="display-dialog-col-right"><button type="button" data-testid="input-button" data-theme="default" data-variant="inline" data-icon-only="" class="input-button display-dialog-close" data-test-id="display-dialog-header-close">
11
+ <!--v-if-->
12
+ <!--v-if--><span class="input-button-text sr-only">Close</span>
13
+ <!--v-if--><span class="input-button-icon" data-slot="icon-only"><span class="iconify undefinedbitcoin-icons:cross-filled icon" aria-hidden="true"></span></span>
14
+ </button></div>
11
15
  </div>
12
16
  <div class="display-dialog-content">
13
17
  <p>Content</p>