@nexxtmove/ui 2.6.0 → 2.7.0
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/dist/index.js +49 -126
- package/dist/nuxt.js +0 -1
- package/package.json +38 -36
- package/src/attribute-contract.test.ts +151 -0
- package/src/components/atoms/AnimatedNumber/AnimatedNumber.stories.ts +36 -0
- package/src/components/atoms/AnimatedNumber/AnimatedNumber.test.ts +21 -0
- package/src/components/atoms/Avatar/Avatar.stories.ts +122 -0
- package/src/components/atoms/Avatar/Avatar.test.ts +134 -0
- package/src/components/atoms/Button/Button.stories.ts +290 -0
- package/src/components/atoms/Button/Button.test.ts +406 -0
- package/src/components/atoms/Card/Card.stories.ts +45 -0
- package/src/components/atoms/Card/Card.test.ts +46 -0
- package/src/components/atoms/Checkbox/Checkbox.stories.ts +35 -0
- package/src/components/atoms/Checkbox/Checkbox.test.ts +55 -0
- package/src/components/atoms/Chip/Chip.stories.ts +274 -0
- package/src/components/atoms/Chip/Chip.test.ts +230 -0
- package/src/components/atoms/EnergyLabel/EnergyLabel.stories.ts +189 -0
- package/src/components/atoms/EnergyLabel/EnergyLabel.test.ts +100 -0
- package/src/components/atoms/FloatingPanel/FloatingPanel.stories.ts +677 -0
- package/src/components/atoms/FloatingPanel/FloatingPanel.test.ts +864 -0
- package/src/components/atoms/Icon/Icon.stories.ts +63 -0
- package/src/components/atoms/Icon/Icon.test.ts +89 -0
- package/src/components/atoms/Loader/Loader.stories.ts +36 -0
- package/src/components/atoms/Loader/Loader.test.ts +40 -0
- package/src/components/atoms/PickerItem/PickerItem.stories.ts +172 -0
- package/src/components/atoms/PickerItem/PickerItem.test.ts +142 -0
- package/src/components/atoms/PickerPreview/PickerPreview.stories.ts +183 -0
- package/src/components/atoms/PickerPreview/PickerPreview.test.ts +137 -0
- package/src/components/atoms/PresenceIndicator/PresenceIndicator.stories.ts +21 -0
- package/src/components/atoms/PropertyListing/PropertyListing.stories.ts +38 -0
- package/src/components/atoms/PropertyListing/PropertyListing.test.ts +46 -0
- package/src/components/atoms/SelectableOption/SelectableOption.stories.ts +131 -0
- package/src/components/atoms/SelectableOption/SelectableOption.test.ts +96 -0
- package/src/components/atoms/Skeleton/Skeleton.stories.ts +27 -0
- package/src/components/atoms/Skeleton/Skeleton.test.ts +28 -0
- package/src/components/atoms/Slider/Slider.stories.ts +151 -0
- package/src/components/atoms/Slider/Slider.test.ts +191 -0
- package/src/components/atoms/Squircle/Squircle.stories.ts +114 -0
- package/src/components/atoms/Squircle/Squircle.test.ts +70 -0
- package/src/components/atoms/Switch/Switch.stories.ts +91 -0
- package/src/components/atoms/Switch/Switch.test.ts +183 -0
- package/src/components/atoms/Tooltip/Tooltip.stories.ts +158 -0
- package/src/components/atoms/Tooltip/Tooltip.test.ts +134 -0
- package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.stories.ts +124 -0
- package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.test.ts +464 -0
- package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.stories.ts +266 -0
- package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.test.ts +421 -0
- package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.stories.ts +365 -0
- package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.test.ts +484 -0
- package/src/components/editor/composables/useRadioGrid.test.ts +261 -0
- package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.stories.ts +73 -0
- package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.test.ts +189 -0
- package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.stories.ts +309 -0
- package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.test.ts +359 -0
- package/src/components/editor/molecules/EditorAnchor/EditorAnchor.stories.ts +138 -0
- package/src/components/editor/molecules/EditorAnchor/EditorAnchor.test.ts +344 -0
- package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.stories.ts +87 -0
- package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.test.ts +262 -0
- package/src/components/editor/molecules/EditorBlur/EditorBlur.stories.ts +115 -0
- package/src/components/editor/molecules/EditorBlur/EditorBlur.test.ts +243 -0
- package/src/components/editor/molecules/EditorBorder/EditorBorder.stories.ts +187 -0
- package/src/components/editor/molecules/EditorBorder/EditorBorder.test.ts +456 -0
- package/src/components/editor/molecules/EditorColor/EditorColor.stories.ts +563 -0
- package/src/components/editor/molecules/EditorColor/EditorColor.test.ts +689 -0
- package/src/components/editor/molecules/EditorColor/EditorColorGradient.test.ts +793 -0
- package/src/components/editor/molecules/EditorColor/gradientClass.test.ts +303 -0
- package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.stories.ts +72 -0
- package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.test.ts +124 -0
- package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.stories.ts +46 -0
- package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.test.ts +97 -0
- package/src/components/editor/molecules/EditorFontSize/EditorFontSize.stories.ts +248 -0
- package/src/components/editor/molecules/EditorFontSize/EditorFontSize.test.ts +288 -0
- package/src/components/editor/molecules/EditorGap/EditorGap.stories.ts +129 -0
- package/src/components/editor/molecules/EditorGap/EditorGap.test.ts +253 -0
- package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.stories.ts +68 -0
- package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.test.ts +150 -0
- package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.stories.ts +71 -0
- package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.test.ts +161 -0
- package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.stories.ts +204 -0
- package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.test.ts +238 -0
- package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.stories.ts +257 -0
- package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.test.ts +286 -0
- package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.stories.ts +135 -0
- package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.test.ts +230 -0
- package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.stories.ts +154 -0
- package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.test.ts +112 -0
- package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.stories.ts +206 -0
- package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.test.ts +332 -0
- package/src/components/editor/molecules/EditorPadding/EditorPadding.stories.ts +374 -0
- package/src/components/editor/molecules/EditorPadding/EditorPadding.test.ts +712 -0
- package/src/components/editor/molecules/EditorRadius/EditorRadius.stories.ts +64 -0
- package/src/components/editor/molecules/EditorRadius/EditorRadius.test.ts +137 -0
- package/src/components/editor/molecules/EditorShadow/EditorShadow.stories.ts +103 -0
- package/src/components/editor/molecules/EditorShadow/EditorShadow.test.ts +168 -0
- package/src/components/editor/molecules/EditorSize/EditorSize.stories.ts +84 -0
- package/src/components/editor/molecules/EditorSize/EditorSize.test.ts +253 -0
- package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.stories.ts +49 -0
- package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.test.ts +43 -0
- package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.stories.ts +69 -0
- package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.test.ts +155 -0
- package/src/components/molecules/Announcement/Announcement.stories.ts +224 -0
- package/src/components/molecules/Announcement/Announcement.test.ts +161 -0
- package/src/components/molecules/ContactInfoCard/ContactInfoCard.stories.ts +99 -0
- package/src/components/molecules/ContactInfoCard/ContactInfoCard.test.ts +70 -0
- package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.stories.ts +148 -0
- package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.test.ts +271 -0
- package/src/components/molecules/DoughnutChart/DoughnutChart.stories.ts +45 -0
- package/src/components/molecules/DoughnutChart/DoughnutChart.test.ts +205 -0
- package/src/components/molecules/InfoBlock/InfoBlock.stories.ts +84 -0
- package/src/components/molecules/InfoBlock/InfoBlock.test.ts +101 -0
- package/src/components/molecules/InputField/InputField.stories.ts +117 -0
- package/src/components/molecules/InputField/InputField.test.ts +138 -0
- package/src/components/molecules/InputSelect/InputSelect.stories.ts +116 -0
- package/src/components/molecules/InputSelect/InputSelect.test.ts +127 -0
- package/src/components/molecules/ModalFooter/ModalFooter.stories.ts +204 -0
- package/src/components/molecules/ModalFooter/ModalFooter.test.ts +276 -0
- package/src/components/molecules/PageCard/PageCard.stories.ts +100 -0
- package/src/components/molecules/PageCard/PageCard.test.ts +91 -0
- package/src/components/molecules/PageContainer/PageContainer.stories.ts +90 -0
- package/src/components/molecules/PageContainer/PageContainer.test.ts +50 -0
- package/src/components/molecules/PageMockup/PageMockup.stories.ts +38 -0
- package/src/components/molecules/PageMockup/PageMockup.test.ts +20 -0
- package/src/components/molecules/PageTypeTile/PageTypeTile.stories.ts +63 -0
- package/src/components/molecules/PageTypeTile/PageTypeTile.test.ts +41 -0
- package/src/components/molecules/Pagination/Pagination.stories.ts +72 -0
- package/src/components/molecules/Pagination/Pagination.test.ts +130 -0
- package/src/components/molecules/PickerGroup/PickerGroup.stories.ts +90 -0
- package/src/components/molecules/PickerGroup/PickerGroup.test.ts +145 -0
- package/src/components/molecules/PickerList/PickerList.stories.ts +436 -0
- package/src/components/molecules/PickerList/PickerList.test.ts +870 -0
- package/src/components/molecules/PlaceholderInput/PlaceholderInput.stories.ts +48 -0
- package/src/components/molecules/PlaceholderInput/PlaceholderInput.test.ts +107 -0
- package/src/components/molecules/PropertyCard/PropertyCard.stories.ts +46 -0
- package/src/components/molecules/PropertyCard/PropertyCard.test.ts +59 -0
- package/src/components/molecules/Select/Select.stories.ts +264 -0
- package/src/components/molecules/Select/Select.test.ts +315 -0
- package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.stories.ts +84 -0
- package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.test.ts +103 -0
- package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.stories.ts +11 -0
- package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.test.ts +38 -0
- package/src/components/molecules/SocialIcons/SocialIcons.stories.ts +28 -0
- package/src/components/molecules/SocialIcons/SocialIcons.test.ts +58 -0
- package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.stories.ts +11 -0
- package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.test.ts +116 -0
- package/src/components/molecules/SocialMediaType/SocialMediaType.stories.ts +37 -0
- package/src/components/molecules/SocialMediaType/SocialMediaType.test.ts +126 -0
- package/src/components/molecules/Source/Source.stories.ts +35 -0
- package/src/components/molecules/Source/Source.test.ts +45 -0
- package/src/components/molecules/TabbedContent/TabbedContent.stories.ts +96 -0
- package/src/components/molecules/TabbedContent/TabbedContent.test.ts +109 -0
- package/src/components/molecules/Tabs/Tabs.stories.ts +230 -0
- package/src/components/molecules/Tabs/Tabs.test.ts +280 -0
- package/src/components/molecules/TemplateCard/TemplateCard.stories.ts +327 -0
- package/src/components/molecules/TemplateCard/TemplateCard.test.ts +339 -0
- package/src/components/molecules/Textarea/Textarea.stories.ts +216 -0
- package/src/components/molecules/Textarea/Textarea.test.ts +191 -0
- package/src/components/molecules/TimelineEvent/TimelineEvent.stories.ts +319 -0
- package/src/components/molecules/TimelineEvent/TimelineEvent.test.ts +224 -0
- package/src/components/molecules/TimelineLanes/TimelineLanes.stories.ts +29 -0
- package/src/components/molecules/TimelineLanes/TimelineLanes.test.ts +30 -0
- package/src/components/molecules/Toast/Toast.stories.ts +110 -0
- package/src/components/molecules/Toast/Toast.test.ts +103 -0
- package/src/components/molecules/ToggleGroup/ToggleGroup.stories.ts +297 -0
- package/src/components/molecules/ToggleGroup/ToggleGroup.test.ts +578 -0
- package/src/components/molecules/Tree/Tree.stories.ts +884 -0
- package/src/components/molecules/Tree/Tree.test.ts +1247 -0
- package/src/components/molecules/Tree/tree-drop.test.ts +223 -0
- package/src/components/organisms/Calendar/Calendar.stories.ts +108 -0
- package/src/components/organisms/Calendar/Calendar.test.ts +279 -0
- package/src/components/organisms/Calendar/_CalendarDayView.test.ts +104 -0
- package/src/components/organisms/Calendar/_CalendarHeader.test.ts +86 -0
- package/src/components/organisms/Calendar/_CalendarMonthView.test.ts +52 -0
- package/src/components/organisms/Calendar/_CalendarTimePicker.test.ts +89 -0
- package/src/components/organisms/Calendar/_CalendarYearView.test.ts +56 -0
- package/src/components/organisms/ColorPicker/ColorPicker.stories.ts +229 -0
- package/src/components/organisms/ColorPicker/ColorPicker.test.ts +638 -0
- package/src/components/organisms/ColorPicker/gradient.test.ts +67 -0
- package/src/components/organisms/ColorPicker/gradientGeometry.test.ts +206 -0
- package/src/components/organisms/Combobox/Combobox.stories.ts +234 -0
- package/src/components/organisms/Combobox/Combobox.test.ts +512 -0
- package/src/components/organisms/Combobox/Combobox.vue +1 -1
- package/src/components/organisms/ComboboxPanel/ComboboxPanel.stories.ts +158 -0
- package/src/components/organisms/ComboboxPanel/ComboboxPanel.test.ts +524 -0
- package/src/components/organisms/ComboboxPanel/ComboboxPanel.vue +1 -1
- package/src/components/organisms/CountrySelect/CountrySelect.stories.ts +139 -0
- package/src/components/organisms/CountrySelect/CountrySelect.test.ts +149 -0
- package/src/components/organisms/DatePicker/DatePicker.stories.ts +163 -0
- package/src/components/organisms/DatePicker/DatePicker.test.ts +214 -0
- package/src/components/organisms/DropdownButton/DropdownButton.stories.ts +123 -0
- package/src/components/organisms/DropdownButton/DropdownButton.test.ts +379 -0
- package/src/components/organisms/FontPicker/FontPicker.stories.ts +476 -0
- package/src/components/organisms/FontPicker/FontPicker.test.ts +873 -0
- package/src/components/organisms/Header/Header.stories.ts +75 -0
- package/src/components/organisms/Header/Header.test.ts +233 -0
- package/src/components/organisms/IconPicker/IconPicker.stories.ts +734 -0
- package/src/components/organisms/IconPicker/IconPicker.test.ts +669 -0
- package/src/components/organisms/IconPicker/_IconPickerGrid.test.ts +184 -0
- package/src/components/organisms/IconPicker/_IconPickerRail.test.ts +102 -0
- package/src/components/organisms/IconPicker/useIconPickerNavigation.test.ts +215 -0
- package/src/components/organisms/Modal/Modal.stories.ts +350 -0
- package/src/components/organisms/Modal/Modal.test.ts +275 -0
- package/src/components/organisms/PickerPanel/PickerPanel.stories.ts +452 -0
- package/src/components/organisms/PickerPanel/PickerPanel.test.ts +358 -0
- package/src/components/organisms/ProgressBar/ProgressBar.stories.ts +100 -0
- package/src/components/organisms/ProgressBar/ProgressBar.test.ts +417 -0
- package/src/components/organisms/ReferenceCard/ReferenceCard.stories.ts +191 -0
- package/src/components/organisms/ReferenceCard/ReferenceCard.test.ts +165 -0
- package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.stories.ts +65 -0
- package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.test.ts +466 -0
- package/src/components/organisms/StepperHeader/StepperHeader.stories.ts +91 -0
- package/src/components/organisms/StepperHeader/StepperHeader.test.ts +279 -0
- package/src/components/organisms/Table/Table.stories.ts +332 -0
- package/src/components/organisms/Table/Table.test.ts +320 -0
- package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.stories.ts +198 -0
- package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.test.ts +283 -0
- package/src/components/organisms/TimelineTrack/TimelineTrack.stories.ts +120 -0
- package/src/components/organisms/TimelineTrack/TimelineTrack.test.ts +91 -0
- package/src/components/organisms/Toaster/Toaster.stories.ts +51 -0
- package/src/components/organisms/Toaster/Toaster.test.ts +75 -0
- package/src/components/organisms/VideoControls/VideoControls.stories.ts +792 -0
- package/src/components/organisms/VideoControls/VideoControls.test.ts +1056 -0
- package/src/components/organisms/VideoPlayer/VideoPlayer.stories.ts +327 -0
- package/src/components/organisms/VideoPlayer/VideoPlayer.test.ts +439 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { expect, screen, userEvent, within } from 'storybook/test'
|
|
3
|
+
import { defineComponent, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
4
|
+
import { NexxtButton, NexxtModal } from '../../../index'
|
|
5
|
+
|
|
6
|
+
const WidthDisplay = defineComponent({
|
|
7
|
+
setup() {
|
|
8
|
+
const el = ref<HTMLDivElement | null>(null)
|
|
9
|
+
const width = ref(0)
|
|
10
|
+
let observer: ResizeObserver
|
|
11
|
+
|
|
12
|
+
onMounted(() => {
|
|
13
|
+
if (!el.value) return
|
|
14
|
+
observer = new ResizeObserver(([entry]) => {
|
|
15
|
+
width.value = Math.round(entry.contentRect.width)
|
|
16
|
+
})
|
|
17
|
+
observer.observe(el.value)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
onBeforeUnmount(() => observer?.disconnect())
|
|
21
|
+
|
|
22
|
+
return { el, width }
|
|
23
|
+
},
|
|
24
|
+
template: `<div ref="el" class="mt-4 flex h-24 w-full items-center justify-center rounded border border-dashed border-gray-300 bg-gray-100 text-xs text-gray-500">{{ width }}px</div>`,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export default {
|
|
28
|
+
title: 'Components/Organisms/Modal',
|
|
29
|
+
component: NexxtModal,
|
|
30
|
+
} satisfies Meta<typeof NexxtModal>
|
|
31
|
+
|
|
32
|
+
type Story = StoryObj<typeof NexxtModal>
|
|
33
|
+
|
|
34
|
+
const trigger = `<NexxtButton @click="open = true">Open modal</NexxtButton>`
|
|
35
|
+
|
|
36
|
+
export const Default: Story = {
|
|
37
|
+
render: () => ({
|
|
38
|
+
components: { NexxtModal, NexxtButton },
|
|
39
|
+
setup() {
|
|
40
|
+
const open = ref(false)
|
|
41
|
+
return { open }
|
|
42
|
+
},
|
|
43
|
+
template: `
|
|
44
|
+
${trigger}
|
|
45
|
+
<NexxtModal v-model="open" title="Modal title">
|
|
46
|
+
<p class="text-sm text-gray-600">This is the modal body content.</p>
|
|
47
|
+
</NexxtModal>
|
|
48
|
+
`,
|
|
49
|
+
}),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const WithHeaderSlot: Story = {
|
|
53
|
+
render: () => ({
|
|
54
|
+
components: { NexxtModal, NexxtButton },
|
|
55
|
+
setup() {
|
|
56
|
+
const open = ref(false)
|
|
57
|
+
return { open }
|
|
58
|
+
},
|
|
59
|
+
template: `
|
|
60
|
+
${trigger}
|
|
61
|
+
<NexxtModal v-model="open" title="Modal title" description="A short description.">
|
|
62
|
+
<template #header>
|
|
63
|
+
<div class="mt-2 flex gap-2">
|
|
64
|
+
<span class="rounded-full bg-blue-100 px-2 py-0.5 text-xs text-blue-700">Tag 1</span>
|
|
65
|
+
<span class="rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-700">Tag 2</span>
|
|
66
|
+
</div>
|
|
67
|
+
</template>
|
|
68
|
+
<p class="text-sm text-gray-600">The header slot renders below the description.</p>
|
|
69
|
+
</NexxtModal>
|
|
70
|
+
`,
|
|
71
|
+
}),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const WithDescription: Story = {
|
|
75
|
+
render: () => ({
|
|
76
|
+
components: { NexxtModal, NexxtButton },
|
|
77
|
+
setup() {
|
|
78
|
+
const open = ref(false)
|
|
79
|
+
return { open }
|
|
80
|
+
},
|
|
81
|
+
template: `
|
|
82
|
+
${trigger}
|
|
83
|
+
<NexxtModal v-model="open" title="Modal title" description="A short description to provide context.">
|
|
84
|
+
<p class="text-sm text-gray-600">This is the modal body content.</p>
|
|
85
|
+
</NexxtModal>
|
|
86
|
+
`,
|
|
87
|
+
}),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const WithFooter: Story = {
|
|
91
|
+
render: () => ({
|
|
92
|
+
components: { NexxtModal, NexxtButton },
|
|
93
|
+
setup() {
|
|
94
|
+
const open = ref(false)
|
|
95
|
+
return { open }
|
|
96
|
+
},
|
|
97
|
+
template: `
|
|
98
|
+
${trigger}
|
|
99
|
+
<NexxtModal v-model="open" title="Confirm action" description="This action cannot be undone.">
|
|
100
|
+
<p class="text-sm text-gray-600">Are you sure you want to continue?</p>
|
|
101
|
+
<template #footer>
|
|
102
|
+
<NexxtButton variant="secondary" @click="open = false">Cancel</NexxtButton>
|
|
103
|
+
<NexxtButton variant="danger">Delete</NexxtButton>
|
|
104
|
+
</template>
|
|
105
|
+
</NexxtModal>
|
|
106
|
+
`,
|
|
107
|
+
}),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const SizeSm: Story = {
|
|
111
|
+
render: () => ({
|
|
112
|
+
components: { NexxtModal, NexxtButton, WidthDisplay },
|
|
113
|
+
setup() {
|
|
114
|
+
const open = ref(false)
|
|
115
|
+
return { open }
|
|
116
|
+
},
|
|
117
|
+
template: `
|
|
118
|
+
${trigger}
|
|
119
|
+
<NexxtModal v-model="open" title="Small modal" size="sm">
|
|
120
|
+
<p class="text-sm text-gray-600">A compact modal for simple messages.</p>
|
|
121
|
+
<WidthDisplay />
|
|
122
|
+
<template #footer>
|
|
123
|
+
<NexxtButton @click="open = false">Got it</NexxtButton>
|
|
124
|
+
</template>
|
|
125
|
+
</NexxtModal>
|
|
126
|
+
`,
|
|
127
|
+
}),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const SizeMd: Story = {
|
|
131
|
+
render: () => ({
|
|
132
|
+
components: { NexxtModal, NexxtButton, WidthDisplay },
|
|
133
|
+
setup() {
|
|
134
|
+
const open = ref(false)
|
|
135
|
+
return { open }
|
|
136
|
+
},
|
|
137
|
+
template: `
|
|
138
|
+
${trigger}
|
|
139
|
+
<NexxtModal v-model="open" title="Medium modal" size="md">
|
|
140
|
+
<p class="text-sm text-gray-600">A medium modal for more content.</p>
|
|
141
|
+
<WidthDisplay />
|
|
142
|
+
<template #footer>
|
|
143
|
+
<NexxtButton @click="open = false">Got it</NexxtButton>
|
|
144
|
+
</template>
|
|
145
|
+
</NexxtModal>
|
|
146
|
+
`,
|
|
147
|
+
}),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const SizeLg: Story = {
|
|
151
|
+
render: () => ({
|
|
152
|
+
components: { NexxtModal, NexxtButton, WidthDisplay },
|
|
153
|
+
setup() {
|
|
154
|
+
const open = ref(false)
|
|
155
|
+
return { open }
|
|
156
|
+
},
|
|
157
|
+
template: `
|
|
158
|
+
${trigger}
|
|
159
|
+
<NexxtModal v-model="open" title="Large modal" size="lg">
|
|
160
|
+
<p class="text-sm text-gray-600">A wider modal for richer content like forms or tables.</p>
|
|
161
|
+
<WidthDisplay />
|
|
162
|
+
</NexxtModal>
|
|
163
|
+
`,
|
|
164
|
+
}),
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const SizeXl: Story = {
|
|
168
|
+
render: () => ({
|
|
169
|
+
components: { NexxtModal, NexxtButton, WidthDisplay },
|
|
170
|
+
setup() {
|
|
171
|
+
const open = ref(false)
|
|
172
|
+
return { open }
|
|
173
|
+
},
|
|
174
|
+
template: `
|
|
175
|
+
${trigger}
|
|
176
|
+
<NexxtModal v-model="open" title="Extra-large modal" size="xl">
|
|
177
|
+
<p class="text-sm text-gray-600">An extra-large modal for complex layouts or data-heavy content.</p>
|
|
178
|
+
<WidthDisplay />
|
|
179
|
+
</NexxtModal>
|
|
180
|
+
`,
|
|
181
|
+
}),
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// The two customWidth stories assert a *resolved* max-width in a real browser.
|
|
185
|
+
// The prop used to render an arbitrary-value max-width utility built from the
|
|
186
|
+
// runtime value, which Tailwind never generates, so the modal had no max-width
|
|
187
|
+
// at all -- and nothing caught it, because asserting a class is present says
|
|
188
|
+
// nothing about whether a width was applied. Only a computed value proves it.
|
|
189
|
+
const openModal = async (canvasElement: HTMLElement) => {
|
|
190
|
+
await userEvent.click(within(canvasElement).getByRole('button', { name: /open modal/i }))
|
|
191
|
+
// Rendered through DialogPortal, so it lands on document.body, not the canvas.
|
|
192
|
+
return screen.findByRole('dialog')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export const SizeCustom: Story = {
|
|
196
|
+
render: () => ({
|
|
197
|
+
components: { NexxtModal, NexxtButton, WidthDisplay },
|
|
198
|
+
setup() {
|
|
199
|
+
const open = ref(false)
|
|
200
|
+
return { open }
|
|
201
|
+
},
|
|
202
|
+
template: `
|
|
203
|
+
${trigger}
|
|
204
|
+
<NexxtModal v-model="open" title="Custom width modal" custom-width="420px">
|
|
205
|
+
<p class="text-sm text-gray-600">This modal has a fixed custom width of 420px via the <code>customWidth</code> prop.</p>
|
|
206
|
+
<WidthDisplay />
|
|
207
|
+
</NexxtModal>
|
|
208
|
+
`,
|
|
209
|
+
}),
|
|
210
|
+
play: async ({ canvasElement }) => {
|
|
211
|
+
const dialog = await openModal(canvasElement)
|
|
212
|
+
await expect(getComputedStyle(dialog).maxWidth).toBe('420px')
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 90vw is what the app actually passes, in EditTemplateDialog and
|
|
217
|
+
// EditVideoTemplateDialog. A viewport unit only resolves in a real browser,
|
|
218
|
+
// which is why this assertion lives here rather than in the jsdom suite.
|
|
219
|
+
export const SizeCustomViewportUnit: Story = {
|
|
220
|
+
render: () => ({
|
|
221
|
+
components: { NexxtModal, NexxtButton, WidthDisplay },
|
|
222
|
+
setup() {
|
|
223
|
+
const open = ref(false)
|
|
224
|
+
return { open }
|
|
225
|
+
},
|
|
226
|
+
template: `
|
|
227
|
+
${trigger}
|
|
228
|
+
<NexxtModal v-model="open" title="Viewport-width modal" custom-width="90vw">
|
|
229
|
+
<p class="text-sm text-gray-600">Width is <code>90vw</code>, matching the app's template dialogs.</p>
|
|
230
|
+
<WidthDisplay />
|
|
231
|
+
</NexxtModal>
|
|
232
|
+
`,
|
|
233
|
+
}),
|
|
234
|
+
play: async ({ canvasElement }) => {
|
|
235
|
+
const dialog = await openModal(canvasElement)
|
|
236
|
+
const resolved = parseFloat(getComputedStyle(dialog).maxWidth)
|
|
237
|
+
|
|
238
|
+
// Resolved to real pixels rather than left as a literal, and actually 90%
|
|
239
|
+
// of the viewport.
|
|
240
|
+
await expect(Number.isNaN(resolved)).toBe(false)
|
|
241
|
+
await expect(Math.abs(resolved - window.innerWidth * 0.9)).toBeLessThanOrEqual(1)
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export const OverflowVertical: Story = {
|
|
246
|
+
render: () => ({
|
|
247
|
+
components: { NexxtModal, NexxtButton },
|
|
248
|
+
setup() {
|
|
249
|
+
const open = ref(false)
|
|
250
|
+
return { open }
|
|
251
|
+
},
|
|
252
|
+
template: `
|
|
253
|
+
${trigger}
|
|
254
|
+
<NexxtModal v-model="open" title="Tall content" description="The body scrolls, the header and footer stay fixed.">
|
|
255
|
+
<div class="flex flex-col gap-3">
|
|
256
|
+
<div v-for="i in 40" :key="i" class="flex h-10 items-center rounded border border-dashed border-gray-300 bg-gray-100 px-3 text-xs text-gray-500">Row {{ i }}</div>
|
|
257
|
+
</div>
|
|
258
|
+
<template #footer>
|
|
259
|
+
<NexxtButton variant="secondary" @click="open = false">Cancel</NexxtButton>
|
|
260
|
+
<NexxtButton @click="open = false">Confirm</NexxtButton>
|
|
261
|
+
</template>
|
|
262
|
+
</NexxtModal>
|
|
263
|
+
`,
|
|
264
|
+
}),
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export const OverflowHorizontal: Story = {
|
|
268
|
+
render: () => ({
|
|
269
|
+
components: { NexxtModal, NexxtButton },
|
|
270
|
+
setup() {
|
|
271
|
+
const open = ref(false)
|
|
272
|
+
return { open }
|
|
273
|
+
},
|
|
274
|
+
template: `
|
|
275
|
+
${trigger}
|
|
276
|
+
<NexxtModal v-model="open" title="Wide content" description="Content wider than the modal scrolls horizontally.">
|
|
277
|
+
<div class="w-200 rounded border border-dashed border-gray-300 bg-gray-100 p-4 text-xs text-gray-500">
|
|
278
|
+
This div is 800px wide — wider than the sm/md modal.
|
|
279
|
+
</div>
|
|
280
|
+
</NexxtModal>
|
|
281
|
+
`,
|
|
282
|
+
}),
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export const NonDismissible: Story = {
|
|
286
|
+
render: () => ({
|
|
287
|
+
components: { NexxtModal, NexxtButton },
|
|
288
|
+
setup() {
|
|
289
|
+
const open = ref(false)
|
|
290
|
+
return { open }
|
|
291
|
+
},
|
|
292
|
+
template: `
|
|
293
|
+
${trigger}
|
|
294
|
+
<NexxtModal v-model="open" title="Required action" description="You must complete this before continuing." :dismissible="false" :show-close="false">
|
|
295
|
+
<p class="text-sm text-gray-600">This modal cannot be closed by clicking the overlay or pressing Escape.</p>
|
|
296
|
+
<template #footer>
|
|
297
|
+
<NexxtButton @click="open = false">Acknowledge</NexxtButton>
|
|
298
|
+
</template>
|
|
299
|
+
</NexxtModal>
|
|
300
|
+
`,
|
|
301
|
+
}),
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export const WithBeforeClose: Story = {
|
|
305
|
+
render: () => ({
|
|
306
|
+
components: { NexxtModal, NexxtButton },
|
|
307
|
+
setup() {
|
|
308
|
+
const open = ref(false)
|
|
309
|
+
const confirming = ref(false)
|
|
310
|
+
|
|
311
|
+
const beforeClose = () =>
|
|
312
|
+
new Promise<boolean>((resolve) => {
|
|
313
|
+
confirming.value = true
|
|
314
|
+
setTimeout(() => {
|
|
315
|
+
confirming.value = false
|
|
316
|
+
resolve(true)
|
|
317
|
+
}, 2000)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
return { open, confirming, beforeClose }
|
|
321
|
+
},
|
|
322
|
+
template: `
|
|
323
|
+
${trigger}
|
|
324
|
+
<NexxtModal v-model="open" title="Edit form" description="Try closing — it will wait 2 seconds before allowing it." :before-close="beforeClose">
|
|
325
|
+
<p class="text-sm text-gray-600">Simulates an async guard (e.g. saving a form before close).</p>
|
|
326
|
+
<p v-if="confirming" class="mt-3 text-sm text-orange-500">Saving changes, please wait…</p>
|
|
327
|
+
<template #footer>
|
|
328
|
+
<NexxtButton variant="secondary" @click="open = false">Close</NexxtButton>
|
|
329
|
+
<NexxtButton>Save</NexxtButton>
|
|
330
|
+
</template>
|
|
331
|
+
</NexxtModal>
|
|
332
|
+
`,
|
|
333
|
+
}),
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export const ContentOnly: Story = {
|
|
337
|
+
render: () => ({
|
|
338
|
+
components: { NexxtModal, NexxtButton },
|
|
339
|
+
setup() {
|
|
340
|
+
const open = ref(false)
|
|
341
|
+
return { open }
|
|
342
|
+
},
|
|
343
|
+
template: `
|
|
344
|
+
${trigger}
|
|
345
|
+
<NexxtModal v-model="open">
|
|
346
|
+
<p class="text-sm text-gray-600">No title or description — just content.</p>
|
|
347
|
+
</NexxtModal>
|
|
348
|
+
`,
|
|
349
|
+
}),
|
|
350
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import { defineComponent, nextTick, ref } from 'vue'
|
|
4
|
+
import Modal from './Modal.vue'
|
|
5
|
+
|
|
6
|
+
const DialogRootStub = defineComponent({
|
|
7
|
+
props: ['open'],
|
|
8
|
+
emits: ['update:open'],
|
|
9
|
+
template: '<div><slot /></div>',
|
|
10
|
+
})
|
|
11
|
+
const DialogPortalStub = defineComponent({ template: '<div><slot /></div>' })
|
|
12
|
+
const DialogOverlayStub = defineComponent({ template: '<div class="dialog-overlay" />' })
|
|
13
|
+
const DialogContentStub = defineComponent({
|
|
14
|
+
props: ['ariaDescribedby'],
|
|
15
|
+
emits: ['pointerDownOutside', 'interactOutside', 'escapeKeyDown'],
|
|
16
|
+
template: '<div class="dialog-content" :aria-describedby="ariaDescribedby"><slot /></div>',
|
|
17
|
+
})
|
|
18
|
+
const DialogTitleStub = defineComponent({ template: '<div class="dialog-title"><slot /></div>' })
|
|
19
|
+
const DialogDescriptionStub = defineComponent({
|
|
20
|
+
template: '<div class="dialog-description"><slot /></div>',
|
|
21
|
+
})
|
|
22
|
+
const DialogCloseStub = defineComponent({
|
|
23
|
+
emits: ['click'],
|
|
24
|
+
template: '<button class="dialog-close" @click="$emit(\'click\')"><slot /></button>',
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const stubs = {
|
|
28
|
+
DialogRoot: DialogRootStub,
|
|
29
|
+
DialogPortal: DialogPortalStub,
|
|
30
|
+
DialogOverlay: DialogOverlayStub,
|
|
31
|
+
DialogContent: DialogContentStub,
|
|
32
|
+
DialogTitle: DialogTitleStub,
|
|
33
|
+
DialogDescription: DialogDescriptionStub,
|
|
34
|
+
DialogClose: DialogCloseStub,
|
|
35
|
+
Transition: { template: '<slot />' },
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const mountModal = (props = {}, slots = {}) =>
|
|
39
|
+
mount(Modal, {
|
|
40
|
+
props: { modelValue: true, ...props },
|
|
41
|
+
slots,
|
|
42
|
+
global: { stubs },
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('Modal', () => {
|
|
46
|
+
describe('header', () => {
|
|
47
|
+
it('renders title when provided', () => {
|
|
48
|
+
const wrapper = mountModal({ title: 'My title' })
|
|
49
|
+
expect(wrapper.find('.dialog-title').text()).toBe('My title')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('renders description when provided', () => {
|
|
53
|
+
const wrapper = mountModal({ description: 'My description' })
|
|
54
|
+
expect(wrapper.find('.dialog-description').text()).toBe('My description')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('renders header div when title is provided', () => {
|
|
58
|
+
const wrapper = mountModal({ title: 'Title' })
|
|
59
|
+
expect(wrapper.find('.dialog-title').exists()).toBe(true)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('renders header div when description is provided', () => {
|
|
63
|
+
const wrapper = mountModal({ description: 'Desc' })
|
|
64
|
+
expect(wrapper.find('.dialog-description').exists()).toBe(true)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('does not render title or description when neither is provided', () => {
|
|
68
|
+
const wrapper = mountModal()
|
|
69
|
+
expect(wrapper.find('.dialog-title').exists()).toBe(false)
|
|
70
|
+
expect(wrapper.find('.dialog-description').exists()).toBe(false)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('renders header slot below description', () => {
|
|
74
|
+
const wrapper = mountModal(
|
|
75
|
+
{ title: 'Title', description: 'Desc' },
|
|
76
|
+
{ header: '<span class="header-extra">Extra</span>' },
|
|
77
|
+
)
|
|
78
|
+
const description = wrapper.find('.dialog-description')
|
|
79
|
+
const extra = wrapper.find('.header-extra')
|
|
80
|
+
|
|
81
|
+
expect(extra.exists()).toBe(true)
|
|
82
|
+
expect(
|
|
83
|
+
description.element.compareDocumentPosition(extra.element) &
|
|
84
|
+
Node.DOCUMENT_POSITION_FOLLOWING,
|
|
85
|
+
).toBeTruthy()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('shows header bar when only header slot is provided', () => {
|
|
89
|
+
const wrapper = mountModal({}, { header: '<span class="header-extra">Extra</span>' })
|
|
90
|
+
|
|
91
|
+
expect(wrapper.find('.header-extra').exists()).toBe(true)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('trims whitespace-only description to nothing', () => {
|
|
95
|
+
const wrapper = mountModal({ description: ' ' })
|
|
96
|
+
expect(wrapper.find('.dialog-description').exists()).toBe(false)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('close button', () => {
|
|
101
|
+
it('renders close button in header when title is present', () => {
|
|
102
|
+
const wrapper = mountModal({ title: 'Title' })
|
|
103
|
+
expect(wrapper.find('.dialog-close').exists()).toBe(true)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('renders absolute close button when no title or description', () => {
|
|
107
|
+
const wrapper = mountModal()
|
|
108
|
+
expect(wrapper.find('.dialog-close').exists()).toBe(true)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('does not render close button when showClose is false', () => {
|
|
112
|
+
const wrapper = mountModal({ showClose: false })
|
|
113
|
+
expect(wrapper.find('.dialog-close').exists()).toBe(false)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('does not render close button when showClose is false even with title', () => {
|
|
117
|
+
const wrapper = mountModal({ title: 'Title', showClose: false })
|
|
118
|
+
expect(wrapper.find('.dialog-close').exists()).toBe(false)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('slots', () => {
|
|
123
|
+
it('renders default slot content', () => {
|
|
124
|
+
const wrapper = mountModal({}, { default: '<p class="body">Body text</p>' })
|
|
125
|
+
expect(wrapper.find('.body').exists()).toBe(true)
|
|
126
|
+
expect(wrapper.find('.body').text()).toBe('Body text')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('renders footer slot when provided', () => {
|
|
130
|
+
const wrapper = mountModal({}, { footer: '<button class="footer-btn">Save</button>' })
|
|
131
|
+
expect(wrapper.find('.footer-btn').exists()).toBe(true)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('does not render footer when footer slot is absent', () => {
|
|
135
|
+
const wrapper = mountModal()
|
|
136
|
+
expect(wrapper.find('.footer-btn').exists()).toBe(false)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('size prop', () => {
|
|
141
|
+
it('applies max-w-xl for sm', () => {
|
|
142
|
+
const wrapper = mountModal({ size: 'sm' })
|
|
143
|
+
expect(wrapper.find('.dialog-content').classes()).toContain('max-w-xl')
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('applies max-w-3xl for md (default)', () => {
|
|
147
|
+
const wrapper = mountModal()
|
|
148
|
+
expect(wrapper.find('.dialog-content').classes()).toContain('max-w-3xl')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('applies max-w-5xl for lg', () => {
|
|
152
|
+
const wrapper = mountModal({ size: 'lg' })
|
|
153
|
+
expect(wrapper.find('.dialog-content').classes()).toContain('max-w-5xl')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('applies max-w-7xl for xl', () => {
|
|
157
|
+
const wrapper = mountModal({ size: 'xl' })
|
|
158
|
+
expect(wrapper.find('.dialog-content').classes()).toContain('max-w-7xl')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('leaves the inline max-width unset when customWidth is not given', () => {
|
|
162
|
+
const el = mountModal({ size: 'sm' }).find('.dialog-content').element as HTMLElement
|
|
163
|
+
expect(el.style.maxWidth).toBe('')
|
|
164
|
+
expect(el.classList).toContain('max-w-xl')
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// These assert the inline style rather than a class on purpose. customWidth
|
|
169
|
+
// used to render an arbitrary-value max-width utility built from the prop,
|
|
170
|
+
// which Tailwind never generates from a runtime value, so the modal silently
|
|
171
|
+
// had no max-width at all -- and the previous tests passed throughout,
|
|
172
|
+
// because they asserted the class was present rather than that a width was
|
|
173
|
+
// applied.
|
|
174
|
+
//
|
|
175
|
+
// Do not write a complete arbitrary-value class anywhere in this file. The
|
|
176
|
+
// Tailwind @source glob scans these .ts files, so the literal strings in the
|
|
177
|
+
// old tests were themselves generating the CSS that made the broken code
|
|
178
|
+
// look like it worked in Storybook. A comment counts.
|
|
179
|
+
describe('customWidth', () => {
|
|
180
|
+
it('applies a value that carries its own unit untouched', () => {
|
|
181
|
+
const el = mountModal({ customWidth: '420px' }).find('.dialog-content').element as HTMLElement
|
|
182
|
+
expect(el.style.maxWidth).toBe('420px')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('treats a bare number as pixels', () => {
|
|
186
|
+
const el = mountModal({ customWidth: '640' }).find('.dialog-content').element as HTMLElement
|
|
187
|
+
expect(el.style.maxWidth).toBe('640px')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it.each(['90vw', '80vw', '42rem', '100%'])('passes %s through unchanged', (value) => {
|
|
191
|
+
const el = mountModal({ customWidth: value }).find('.dialog-content').element as HTMLElement
|
|
192
|
+
expect(el.style.maxWidth).toBe(value)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('never emits an arbitrary-value Tailwind class, which would not exist', () => {
|
|
196
|
+
const classes = mountModal({ customWidth: '420px' }).find('.dialog-content').classes()
|
|
197
|
+
expect(classes.some((c) => c.startsWith('max-w-['))).toBe(false)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('replaces the size class rather than sitting alongside it', () => {
|
|
201
|
+
const found = mountModal({ size: 'sm', customWidth: '300px' }).find('.dialog-content')
|
|
202
|
+
expect(found.classes()).not.toContain('max-w-xl')
|
|
203
|
+
expect((found.element as HTMLElement).style.maxWidth).toBe('300px')
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
describe('aria-describedby', () => {
|
|
208
|
+
it('sets aria-describedby to "undefined" when no description', () => {
|
|
209
|
+
const wrapper = mountModal()
|
|
210
|
+
expect(wrapper.find('.dialog-content').attributes('aria-describedby')).toBe('undefined')
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('does not set aria-describedby when description is provided', () => {
|
|
214
|
+
const wrapper = mountModal({ description: 'Some description' })
|
|
215
|
+
expect(wrapper.find('.dialog-content').attributes('aria-describedby')).toBeUndefined()
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
describe('beforeClose guard', () => {
|
|
220
|
+
it('closes when beforeClose returns true', async () => {
|
|
221
|
+
const beforeClose = vi.fn().mockResolvedValue(true)
|
|
222
|
+
const modelValue = ref(true)
|
|
223
|
+
const wrapper = mount(Modal, {
|
|
224
|
+
props: {
|
|
225
|
+
modelValue: modelValue.value,
|
|
226
|
+
beforeClose,
|
|
227
|
+
'onUpdate:modelValue': (val: boolean) => {
|
|
228
|
+
modelValue.value = val
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
global: { stubs },
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
wrapper.findComponent(DialogRootStub).vm.$emit('update:open', false)
|
|
235
|
+
await nextTick()
|
|
236
|
+
|
|
237
|
+
expect(beforeClose).toHaveBeenCalled()
|
|
238
|
+
expect(modelValue.value).toBe(false)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('keeps modal open when beforeClose returns false', async () => {
|
|
242
|
+
const beforeClose = vi.fn().mockResolvedValue(false)
|
|
243
|
+
const modelValue = ref(true)
|
|
244
|
+
const wrapper = mount(Modal, {
|
|
245
|
+
props: {
|
|
246
|
+
modelValue: modelValue.value,
|
|
247
|
+
beforeClose,
|
|
248
|
+
'onUpdate:modelValue': (val: boolean) => {
|
|
249
|
+
modelValue.value = val
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
global: { stubs },
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
wrapper.findComponent(DialogRootStub).vm.$emit('update:open', false)
|
|
256
|
+
await nextTick()
|
|
257
|
+
|
|
258
|
+
expect(beforeClose).toHaveBeenCalled()
|
|
259
|
+
expect(modelValue.value).toBe(true)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('does not call beforeClose when opening', async () => {
|
|
263
|
+
const beforeClose = vi.fn().mockResolvedValue(true)
|
|
264
|
+
const wrapper = mount(Modal, {
|
|
265
|
+
props: { modelValue: false, beforeClose },
|
|
266
|
+
global: { stubs },
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
wrapper.findComponent(DialogRootStub).vm.$emit('update:open', true)
|
|
270
|
+
await nextTick()
|
|
271
|
+
|
|
272
|
+
expect(beforeClose).not.toHaveBeenCalled()
|
|
273
|
+
})
|
|
274
|
+
})
|
|
275
|
+
})
|