@nexxtmove/ui 2.5.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/components/editor/molecules/EditorBorder/EditorBorder.vue.d.ts +12 -0
- package/dist/components/editor/molecules/EditorColor/_ThemeRoleSwatches.vue.d.ts +16 -0
- package/dist/index.js +1094 -1135
- 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/EditorBorder/EditorBorder.vue +58 -2
- 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/_ColorField.vue +7 -29
- package/src/components/editor/molecules/EditorColor/_ThemeRoleSwatches.vue +57 -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,204 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import ModalFooter from './ModalFooter.vue'
|
|
4
|
+
import NexxtModal from '../../organisms/Modal/Modal.vue'
|
|
5
|
+
import NexxtButton from '../../atoms/Button/Button.vue'
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
title: 'Components/Molecules/Modal Footer',
|
|
9
|
+
component: ModalFooter,
|
|
10
|
+
} satisfies Meta<typeof ModalFooter>
|
|
11
|
+
|
|
12
|
+
type Story = StoryObj<typeof ModalFooter>
|
|
13
|
+
|
|
14
|
+
// The footer only reads correctly in the place it is used, so every story renders
|
|
15
|
+
// it in the real Modal's `footer` slot rather than on a blank canvas.
|
|
16
|
+
//
|
|
17
|
+
// They start CLOSED, and must stay that way. The docs page renders every story on
|
|
18
|
+
// one screen, so a modal that opens on mount stacks itself on top of the others --
|
|
19
|
+
// eight overlapping dialogs, with only the last one reachable. Opening via the
|
|
20
|
+
// trigger costs one click and keeps the docs page readable. `Modal.stories.ts`
|
|
21
|
+
// follows the same convention throughout.
|
|
22
|
+
const inModal = (
|
|
23
|
+
title: string,
|
|
24
|
+
body: string,
|
|
25
|
+
footerAttrs: string,
|
|
26
|
+
handlers = '@cancel="open = false" @confirm="open = false"',
|
|
27
|
+
) => ({
|
|
28
|
+
components: { ModalFooter, NexxtModal, NexxtButton },
|
|
29
|
+
setup() {
|
|
30
|
+
const open = ref(false)
|
|
31
|
+
return { open }
|
|
32
|
+
},
|
|
33
|
+
template: `
|
|
34
|
+
<div>
|
|
35
|
+
<NexxtButton v-if="!open" @click="open = true">Open modal</NexxtButton>
|
|
36
|
+
<NexxtModal v-model="open" title="${title}">
|
|
37
|
+
<p>${body}</p>
|
|
38
|
+
<template #footer>
|
|
39
|
+
<ModalFooter cancel-label="Annuleren" ${footerAttrs} ${handlers} />
|
|
40
|
+
</template>
|
|
41
|
+
</NexxtModal>
|
|
42
|
+
</div>
|
|
43
|
+
`,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The two-button shape. Both labels are required props: the library has no i18n
|
|
48
|
+
* layer, so copy comes from the consumer.
|
|
49
|
+
*/
|
|
50
|
+
export const Default: Story = {
|
|
51
|
+
render: () =>
|
|
52
|
+
inModal('Wijzigingen opslaan', 'Weet je zeker dat je verder wilt?', 'confirm-label="Opslaan"'),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* `confirmVariant="danger"` — the delete-confirmation shape. The destructive
|
|
57
|
+
* action has to be the one that stands out, with cancel staying quiet next to it.
|
|
58
|
+
*/
|
|
59
|
+
export const DangerConfirm: Story = {
|
|
60
|
+
render: () =>
|
|
61
|
+
inModal(
|
|
62
|
+
'Contact verwijderen',
|
|
63
|
+
'Dit contact wordt permanent verwijderd. Deze actie kan niet ongedaan worden gemaakt.',
|
|
64
|
+
'confirm-label="Verwijderen" confirm-variant="danger"',
|
|
65
|
+
),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* `confirmLoading` on the action button — the state between submitting and the modal
|
|
70
|
+
* closing. Cancel stays reachable so a slow request is not a trap.
|
|
71
|
+
*/
|
|
72
|
+
export const Loading: Story = {
|
|
73
|
+
render: () =>
|
|
74
|
+
inModal('Wijzigingen opslaan', 'Bezig met opslaan…', 'confirm-label="Opslaan" confirm-loading'),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* `confirmLoading` on a `danger` confirm, which is where a spinner most needs to be
|
|
79
|
+
* legible: a light spinner has to hold up on the red button, not only on the
|
|
80
|
+
* default one.
|
|
81
|
+
*/
|
|
82
|
+
export const LoadingDanger: Story = {
|
|
83
|
+
render: () =>
|
|
84
|
+
inModal(
|
|
85
|
+
'Contact verwijderen',
|
|
86
|
+
'Bezig met verwijderen…',
|
|
87
|
+
'confirm-label="Verwijderen" confirm-variant="danger" confirm-loading',
|
|
88
|
+
),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* `confirmDisabled` on the action button — the form is incomplete, so confirming is not
|
|
93
|
+
* offered yet. Visually distinct from `confirmLoading`: nothing is happening here.
|
|
94
|
+
*/
|
|
95
|
+
export const Disabled: Story = {
|
|
96
|
+
render: () =>
|
|
97
|
+
inModal(
|
|
98
|
+
'Wijzigingen opslaan',
|
|
99
|
+
'Vul eerst alle verplichte velden in.',
|
|
100
|
+
'confirm-label="Opslaan" confirm-disabled',
|
|
101
|
+
),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* `confirmIcon` on the action button. The prop exists because a scripted
|
|
106
|
+
* conversion dropped `icon` from two real modals without anything failing —
|
|
107
|
+
* it type-checked and ran, it just rendered no icon.
|
|
108
|
+
*/
|
|
109
|
+
export const ConfirmIcon: Story = {
|
|
110
|
+
render: () =>
|
|
111
|
+
inModal(
|
|
112
|
+
'Wijzigingen opslaan',
|
|
113
|
+
'De bevestigknop draagt een icoon naast zijn label.',
|
|
114
|
+
'confirm-label="Opslaan" confirm-icon="floppy-disk"',
|
|
115
|
+
),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** A cancel label that says what leaving actually does. */
|
|
119
|
+
export const CustomCancelLabel: Story = {
|
|
120
|
+
render: () =>
|
|
121
|
+
inModal(
|
|
122
|
+
'Concept verlaten',
|
|
123
|
+
'Je wijzigingen gaan verloren.',
|
|
124
|
+
'cancel-label="Terug naar bewerken" confirm-label="Verlaten" confirm-variant="danger"',
|
|
125
|
+
),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Every state next to each other, outside a modal, so the set can be compared in
|
|
130
|
+
* one glance — and so the footer's own alignment is visible without the modal's
|
|
131
|
+
* own `justify-end` footer slot doing part of the work.
|
|
132
|
+
*/
|
|
133
|
+
export const AllStates: Story = {
|
|
134
|
+
render: () => ({
|
|
135
|
+
components: { ModalFooter },
|
|
136
|
+
setup: () => ({
|
|
137
|
+
rows: [
|
|
138
|
+
{ label: 'default', props: { confirmLabel: 'Opslaan' } },
|
|
139
|
+
{
|
|
140
|
+
label: 'confirmVariant="danger"',
|
|
141
|
+
props: { confirmLabel: 'Verwijderen', confirmVariant: 'danger' },
|
|
142
|
+
},
|
|
143
|
+
{ label: 'confirmLoading', props: { confirmLabel: 'Opslaan', confirmLoading: true } },
|
|
144
|
+
{ label: 'confirmDisabled', props: { confirmLabel: 'Opslaan', confirmDisabled: true } },
|
|
145
|
+
{
|
|
146
|
+
label: 'custom cancelLabel',
|
|
147
|
+
props: { cancelLabel: 'Terug naar bewerken', confirmLabel: 'Verlaten' },
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
}),
|
|
151
|
+
template: `
|
|
152
|
+
<div style="display: flex; flex-direction: column; gap: 8px; max-width: 560px; font-family: system-ui, sans-serif;">
|
|
153
|
+
<div v-for="row in rows" :key="row.label">
|
|
154
|
+
<p style="margin: 0 0 2px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: #374151;">
|
|
155
|
+
{{ row.label }}
|
|
156
|
+
</p>
|
|
157
|
+
<!-- Modal supplies this padding via its own #footer wrapper; outside a
|
|
158
|
+
Modal the component deliberately adds none, so the story does. -->
|
|
159
|
+
<div style="border: 1px solid #e5e7eb; border-radius: 8px; padding: 20px 24px;">
|
|
160
|
+
<ModalFooter v-bind="row.props" />
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
`,
|
|
165
|
+
}),
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Both events wired up, with a log, so it is visible that `cancel` and `confirm`
|
|
170
|
+
* are emitted rather than the footer closing the modal itself. Closing stays the
|
|
171
|
+
* caller's decision — which is what lets `confirm` await a request first.
|
|
172
|
+
*/
|
|
173
|
+
export const Events: Story = {
|
|
174
|
+
render: () => ({
|
|
175
|
+
components: { ModalFooter, NexxtModal, NexxtButton },
|
|
176
|
+
setup() {
|
|
177
|
+
const open = ref(false)
|
|
178
|
+
const log = ref<string[]>([])
|
|
179
|
+
const record = (event: string) => {
|
|
180
|
+
log.value.push(event)
|
|
181
|
+
open.value = false
|
|
182
|
+
}
|
|
183
|
+
return { open, log, record }
|
|
184
|
+
},
|
|
185
|
+
template: `
|
|
186
|
+
<div style="display: flex; flex-direction: column; gap: 12px; align-items: flex-start;">
|
|
187
|
+
<NexxtButton v-if="!open" @click="open = true">Open modal</NexxtButton>
|
|
188
|
+
<NexxtModal v-model="open" title="Wijzigingen opslaan">
|
|
189
|
+
<p>Bevestig of annuleer om het event te zien.</p>
|
|
190
|
+
<template #footer>
|
|
191
|
+
<ModalFooter
|
|
192
|
+
confirm-label="Opslaan"
|
|
193
|
+
@cancel="record('cancel')"
|
|
194
|
+
@confirm="record('confirm')"
|
|
195
|
+
/>
|
|
196
|
+
</template>
|
|
197
|
+
</NexxtModal>
|
|
198
|
+
<ul style="margin: 0; padding: 0; list-style: none; font-size: 12px; color: #374151;">
|
|
199
|
+
<li v-for="(entry, i) in log" :key="i">emitted: {{ entry }}</li>
|
|
200
|
+
</ul>
|
|
201
|
+
</div>
|
|
202
|
+
`,
|
|
203
|
+
}),
|
|
204
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import ModalFooter from './ModalFooter.vue'
|
|
4
|
+
|
|
5
|
+
const CANCEL_LABEL = 'Annuleren'
|
|
6
|
+
|
|
7
|
+
const mountFooter = (props: Record<string, unknown> = {}) =>
|
|
8
|
+
mount(ModalFooter, { props: { confirmLabel: 'Opslaan', cancelLabel: CANCEL_LABEL, ...props } })
|
|
9
|
+
|
|
10
|
+
// Buttons are located by their visible label, never by index, so a footer that
|
|
11
|
+
// swapped the two buttons around cannot make these assertions pass by accident.
|
|
12
|
+
const byLabel = (wrapper: ReturnType<typeof mountFooter>, label: string) => {
|
|
13
|
+
const found = wrapper.findAll('button').filter((b) => b.text().includes(label))
|
|
14
|
+
expect(found).toHaveLength(1)
|
|
15
|
+
return found[0]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const action = (wrapper: ReturnType<typeof mountFooter>) => byLabel(wrapper, 'Opslaan')
|
|
19
|
+
const cancel = (wrapper: ReturnType<typeof mountFooter>, label = CANCEL_LABEL) =>
|
|
20
|
+
byLabel(wrapper, label)
|
|
21
|
+
|
|
22
|
+
describe('ModalFooter labels', () => {
|
|
23
|
+
it('renders the required confirm label', () => {
|
|
24
|
+
expect(mountFooter().text()).toContain('Opslaan')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('renders the supplied cancel label', () => {
|
|
28
|
+
const wrapper = mountFooter({ cancelLabel: 'Sluiten' })
|
|
29
|
+
|
|
30
|
+
expect(cancel(wrapper, 'Sluiten').exists()).toBe(true)
|
|
31
|
+
expect(wrapper.text()).not.toContain(CANCEL_LABEL)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('renders exactly two buttons', () => {
|
|
35
|
+
expect(mountFooter().findAll('button')).toHaveLength(2)
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('ModalFooter events', () => {
|
|
40
|
+
it('emits confirm, and only confirm, when the action button is clicked', async () => {
|
|
41
|
+
const wrapper = mountFooter()
|
|
42
|
+
await action(wrapper).trigger('click')
|
|
43
|
+
|
|
44
|
+
expect(wrapper.emitted('confirm')).toHaveLength(1)
|
|
45
|
+
expect(wrapper.emitted('cancel')).toBeUndefined()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('emits cancel, and only cancel, when the cancel button is clicked', async () => {
|
|
49
|
+
const wrapper = mountFooter()
|
|
50
|
+
await cancel(wrapper).trigger('click')
|
|
51
|
+
|
|
52
|
+
expect(wrapper.emitted('cancel')).toHaveLength(1)
|
|
53
|
+
expect(wrapper.emitted('confirm')).toBeUndefined()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('emits cancel from a renamed cancel button too', async () => {
|
|
57
|
+
const wrapper = mountFooter({ cancelLabel: 'Sluiten' })
|
|
58
|
+
await cancel(wrapper, 'Sluiten').trigger('click')
|
|
59
|
+
|
|
60
|
+
expect(wrapper.emitted('cancel')).toHaveLength(1)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('ModalFooter confirmDisabled', () => {
|
|
65
|
+
it('disables the action button and leaves cancel usable', async () => {
|
|
66
|
+
const wrapper = mountFooter({ confirmDisabled: true })
|
|
67
|
+
|
|
68
|
+
expect(action(wrapper).attributes('disabled')).toBeDefined()
|
|
69
|
+
expect(cancel(wrapper).attributes('disabled')).toBeUndefined()
|
|
70
|
+
|
|
71
|
+
await cancel(wrapper).trigger('click')
|
|
72
|
+
expect(wrapper.emitted('cancel')).toHaveLength(1)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('leaves both buttons enabled when confirmDisabled is not set', () => {
|
|
76
|
+
const wrapper = mountFooter()
|
|
77
|
+
|
|
78
|
+
expect(action(wrapper).attributes('disabled')).toBeUndefined()
|
|
79
|
+
expect(cancel(wrapper).attributes('disabled')).toBeUndefined()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('does not emit confirm while confirmDisabled', async () => {
|
|
83
|
+
const wrapper = mountFooter({ confirmDisabled: true })
|
|
84
|
+
await action(wrapper).trigger('click')
|
|
85
|
+
|
|
86
|
+
expect(wrapper.emitted('confirm')).toBeUndefined()
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('ModalFooter cancelDisabled', () => {
|
|
91
|
+
it('disables the cancel button and leaves the action usable', () => {
|
|
92
|
+
const wrapper = mountFooter({ cancelDisabled: true })
|
|
93
|
+
|
|
94
|
+
// Which button, not merely that one is disabled: wiring this to the wrong
|
|
95
|
+
// button is the plausible slip and it would still look like it works.
|
|
96
|
+
expect(cancel(wrapper).attributes('disabled')).toBeDefined()
|
|
97
|
+
expect(action(wrapper).attributes('disabled')).toBeUndefined()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('does not emit cancel while cancelDisabled', async () => {
|
|
101
|
+
const wrapper = mountFooter({ cancelDisabled: true })
|
|
102
|
+
|
|
103
|
+
await cancel(wrapper).trigger('click')
|
|
104
|
+
|
|
105
|
+
expect(wrapper.emitted('cancel')).toBeUndefined()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('still emits confirm while cancel is disabled', async () => {
|
|
109
|
+
const wrapper = mountFooter({ cancelDisabled: true })
|
|
110
|
+
|
|
111
|
+
await action(wrapper).trigger('click')
|
|
112
|
+
|
|
113
|
+
expect(wrapper.emitted('confirm')).toHaveLength(1)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('leaves cancel enabled when cancelDisabled is unset', () => {
|
|
117
|
+
expect(cancel(mountFooter()).attributes('disabled')).toBeUndefined()
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('ModalFooter confirmIcon', () => {
|
|
122
|
+
// The icon exists because a scripted conversion silently dropped `icon` from
|
|
123
|
+
// two action buttons: it type-checked, linted and ran, it just rendered no
|
|
124
|
+
// icon. So these assert which button carries it, not that one does.
|
|
125
|
+
it('renders the icon on the action button and not on cancel', () => {
|
|
126
|
+
const wrapper = mountFooter({ confirmIcon: 'check' })
|
|
127
|
+
|
|
128
|
+
expect(action(wrapper).find('i.fa-check').exists()).toBe(true)
|
|
129
|
+
expect(cancel(wrapper).find('i').exists()).toBe(false)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('renders no icon on either button when confirmIcon is unset', () => {
|
|
133
|
+
const wrapper = mountFooter()
|
|
134
|
+
|
|
135
|
+
expect(action(wrapper).find('i').exists()).toBe(false)
|
|
136
|
+
expect(cancel(wrapper).find('i').exists()).toBe(false)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('keeps the label alongside the icon rather than replacing it', () => {
|
|
140
|
+
const wrapper = mountFooter({ confirmIcon: 'check' })
|
|
141
|
+
|
|
142
|
+
expect(action(wrapper).text()).toContain('Opslaan')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('accepts an icon name bound at runtime', async () => {
|
|
146
|
+
const wrapper = mountFooter({ confirmIcon: 'check' })
|
|
147
|
+
await wrapper.setProps({ confirmIcon: 'floppy-disk' })
|
|
148
|
+
|
|
149
|
+
expect(action(wrapper).find('i.fa-floppy-disk').exists()).toBe(true)
|
|
150
|
+
expect(action(wrapper).find('i.fa-check').exists()).toBe(false)
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('ModalFooter confirmLoading', () => {
|
|
155
|
+
// The spinner is Font Awesome's, not a Tailwind utility, so matching it here is
|
|
156
|
+
// safe from the @source generation trap.
|
|
157
|
+
const spinnerIn = (button: ReturnType<typeof action>) =>
|
|
158
|
+
button.find('i.fa-spinner-third').exists()
|
|
159
|
+
|
|
160
|
+
it('shows the busy indicator on the action button only', () => {
|
|
161
|
+
const wrapper = mountFooter({ confirmLoading: true })
|
|
162
|
+
|
|
163
|
+
expect(spinnerIn(action(wrapper))).toBe(true)
|
|
164
|
+
expect(spinnerIn(cancel(wrapper))).toBe(false)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('shows no busy indicator on either button when confirmLoading is unset', () => {
|
|
168
|
+
const wrapper = mountFooter()
|
|
169
|
+
|
|
170
|
+
expect(spinnerIn(action(wrapper))).toBe(false)
|
|
171
|
+
expect(spinnerIn(cancel(wrapper))).toBe(false)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('keeps cancel clickable while the action is loading', async () => {
|
|
175
|
+
const wrapper = mountFooter({ confirmLoading: true })
|
|
176
|
+
await cancel(wrapper).trigger('click')
|
|
177
|
+
|
|
178
|
+
expect(wrapper.emitted('cancel')).toHaveLength(1)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('moves the busy indicator when confirmLoading is toggled at runtime', async () => {
|
|
182
|
+
const wrapper = mountFooter()
|
|
183
|
+
expect(spinnerIn(action(wrapper))).toBe(false)
|
|
184
|
+
|
|
185
|
+
await wrapper.setProps({ confirmLoading: true })
|
|
186
|
+
expect(spinnerIn(action(wrapper))).toBe(true)
|
|
187
|
+
expect(spinnerIn(cancel(wrapper))).toBe(false)
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
describe('ModalFooter confirmVariant', () => {
|
|
192
|
+
// The cancel button has a fixed variant of its own; confirmVariant must not
|
|
193
|
+
// reach it. Compared against a baseline mount rather than a hardcoded value, so
|
|
194
|
+
// this stays true if the cancel styling is ever changed deliberately.
|
|
195
|
+
it.each(['primary', 'secondary', 'success', 'danger', 'warning'])(
|
|
196
|
+
'passes the variant "%s" through to the action button, leaving cancel alone',
|
|
197
|
+
(variant) => {
|
|
198
|
+
const baseline = cancel(mountFooter()).attributes('data-variant')
|
|
199
|
+
const wrapper = mountFooter({ confirmVariant: variant })
|
|
200
|
+
|
|
201
|
+
expect(action(wrapper).attributes('data-variant')).toBe(variant)
|
|
202
|
+
expect(cancel(wrapper).attributes('data-variant')).toBe(baseline)
|
|
203
|
+
},
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
it('falls through to the button default when no variant is supplied', () => {
|
|
207
|
+
expect(action(mountFooter()).attributes('data-variant')).toBe('primary')
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('accepts a variant computed at runtime rather than a literal', async () => {
|
|
211
|
+
// mirrors the real call sites: `options.variant ?? 'primary'` and
|
|
212
|
+
// `isLast ? 'success' : 'primary'`
|
|
213
|
+
const options: { variant?: string } = {}
|
|
214
|
+
const wrapper = mountFooter({ confirmVariant: options.variant ?? 'primary' })
|
|
215
|
+
expect(action(wrapper).attributes('data-variant')).toBe('primary')
|
|
216
|
+
|
|
217
|
+
const isLast = true
|
|
218
|
+
await wrapper.setProps({ confirmVariant: isLast ? 'success' : 'primary' })
|
|
219
|
+
expect(action(wrapper).attributes('data-variant')).toBe('success')
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('re-renders the action button when the variant changes', async () => {
|
|
223
|
+
const wrapper = mountFooter({ confirmVariant: 'primary' })
|
|
224
|
+
await wrapper.setProps({ confirmVariant: 'danger' })
|
|
225
|
+
|
|
226
|
+
expect(action(wrapper).attributes('data-variant')).toBe('danger')
|
|
227
|
+
expect(wrapper.emitted('confirm')).toBeUndefined()
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
describe('ModalFooter layout', () => {
|
|
232
|
+
it('is a single-root component, so fallthrough attributes have one home', () => {
|
|
233
|
+
// A comment node before the root makes this multi-root, and the compiler
|
|
234
|
+
// strips comments in production only -- so a consumer's class would land
|
|
235
|
+
// somewhere different in dev than in prod. Guarding that here.
|
|
236
|
+
const wrapper = mountFooter()
|
|
237
|
+
const row = wrapper.find('button').element.parentElement
|
|
238
|
+
|
|
239
|
+
expect(wrapper.element).toBe(row)
|
|
240
|
+
expect(wrapper.classes().length).toBeGreaterThan(0)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('puts a consumer class, id and data attribute on the row itself', () => {
|
|
244
|
+
const wrapper = mount(ModalFooter, {
|
|
245
|
+
props: { confirmLabel: 'Opslaan', cancelLabel: CANCEL_LABEL },
|
|
246
|
+
attrs: { class: 'consumer-class', id: 'consumer-id', 'data-test': 'footer' },
|
|
247
|
+
})
|
|
248
|
+
const row = wrapper.find('button').element.parentElement as HTMLElement
|
|
249
|
+
|
|
250
|
+
expect(row.classList.contains('consumer-class')).toBe(true)
|
|
251
|
+
expect(row.getAttribute('id')).toBe('consumer-id')
|
|
252
|
+
expect(row.getAttribute('data-test')).toBe('footer')
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('adds no padding of its own, because the modal footer slot supplies it', () => {
|
|
256
|
+
const classes = mountFooter().classes()
|
|
257
|
+
|
|
258
|
+
expect(classes.length).toBeGreaterThan(0)
|
|
259
|
+
expect(classes.filter((c) => /^-?p[xytblrse]?-/.test(c))).toEqual([])
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('renders both buttons as siblings in one row element', () => {
|
|
263
|
+
const wrapper = mountFooter()
|
|
264
|
+
const parents = wrapper.findAll('button').map((b) => b.element.parentElement)
|
|
265
|
+
|
|
266
|
+
expect(parents[0]?.tagName).toBe('DIV')
|
|
267
|
+
expect(parents[0]).toBe(parents[1])
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('renders cancel before the action button in document order', () => {
|
|
271
|
+
const wrapper = mountFooter()
|
|
272
|
+
const labels = wrapper.findAll('button').map((b) => b.text())
|
|
273
|
+
|
|
274
|
+
expect(labels).toEqual([CANCEL_LABEL, 'Opslaan'])
|
|
275
|
+
})
|
|
276
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import NexxtDropdownButton from '../../organisms/DropdownButton/DropdownButton.vue'
|
|
3
|
+
import PageCard from './PageCard.vue'
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title: 'Components/Molecules/Page Card',
|
|
7
|
+
component: PageCard,
|
|
8
|
+
} satisfies Meta<typeof PageCard>
|
|
9
|
+
|
|
10
|
+
type Story = StoryObj<typeof PageCard>
|
|
11
|
+
|
|
12
|
+
const ACTIONS_SLOT = `
|
|
13
|
+
<template #actions>
|
|
14
|
+
<NexxtDropdownButton
|
|
15
|
+
icon-only
|
|
16
|
+
variant="secondary"
|
|
17
|
+
trigger-icon="ellipsis"
|
|
18
|
+
aria-label="Pagina-menu"
|
|
19
|
+
:options="[{ label: 'Bekijken', action: () => {} }, { label: 'Verwijderen', action: () => {} }]"
|
|
20
|
+
/>
|
|
21
|
+
</template>
|
|
22
|
+
`
|
|
23
|
+
|
|
24
|
+
export const Default: Story = {
|
|
25
|
+
args: {
|
|
26
|
+
title: 'Welkomstpagina',
|
|
27
|
+
mockup: 'introduction',
|
|
28
|
+
icon: 'hand-wave',
|
|
29
|
+
},
|
|
30
|
+
render: (args) => ({
|
|
31
|
+
components: { PageCard },
|
|
32
|
+
setup() {
|
|
33
|
+
return { args }
|
|
34
|
+
},
|
|
35
|
+
template: '<PageCard v-bind="args" class="max-w-sm" />',
|
|
36
|
+
}),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const WithMenu: Story = {
|
|
40
|
+
args: {
|
|
41
|
+
title: 'Over ons',
|
|
42
|
+
mockup: 'about',
|
|
43
|
+
icon: 'users',
|
|
44
|
+
selectable: true,
|
|
45
|
+
},
|
|
46
|
+
render: (args) => ({
|
|
47
|
+
components: { PageCard, NexxtDropdownButton },
|
|
48
|
+
setup() {
|
|
49
|
+
return { args }
|
|
50
|
+
},
|
|
51
|
+
template: `<PageCard v-bind="args" class="max-w-sm">${ACTIONS_SLOT}</PageCard>`,
|
|
52
|
+
}),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const Selected: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
title: 'Waarde-indicatie',
|
|
58
|
+
mockup: 'valuation',
|
|
59
|
+
icon: 'circle-euro',
|
|
60
|
+
selectable: true,
|
|
61
|
+
selected: true,
|
|
62
|
+
},
|
|
63
|
+
render: (args) => ({
|
|
64
|
+
components: { PageCard },
|
|
65
|
+
setup() {
|
|
66
|
+
return { args }
|
|
67
|
+
},
|
|
68
|
+
template: '<PageCard v-bind="args" class="max-w-sm" />',
|
|
69
|
+
}),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const Dragging: Story = {
|
|
73
|
+
args: {
|
|
74
|
+
title: 'Courtage',
|
|
75
|
+
mockup: 'packages',
|
|
76
|
+
icon: 'piggy-bank',
|
|
77
|
+
dragging: true,
|
|
78
|
+
},
|
|
79
|
+
render: (args) => ({
|
|
80
|
+
components: { PageCard },
|
|
81
|
+
setup() {
|
|
82
|
+
return { args }
|
|
83
|
+
},
|
|
84
|
+
template: '<PageCard v-bind="args" class="max-w-sm" />',
|
|
85
|
+
}),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const EmptyPreview: Story = {
|
|
89
|
+
args: {
|
|
90
|
+
title: 'Overige pagina',
|
|
91
|
+
icon: 'file-lines',
|
|
92
|
+
},
|
|
93
|
+
render: (args) => ({
|
|
94
|
+
components: { PageCard },
|
|
95
|
+
setup() {
|
|
96
|
+
return { args }
|
|
97
|
+
},
|
|
98
|
+
template: '<PageCard v-bind="args" class="max-w-sm" />',
|
|
99
|
+
}),
|
|
100
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import PageCard from './PageCard.vue'
|
|
4
|
+
|
|
5
|
+
describe('PageCard', () => {
|
|
6
|
+
it('renders the title and icon in the footer', () => {
|
|
7
|
+
const wrapper = mount(PageCard, { props: { title: 'Welkomstpagina', icon: 'hand-wave' } })
|
|
8
|
+
|
|
9
|
+
expect(wrapper.find('h3').text()).toBe('Welkomstpagina')
|
|
10
|
+
expect(wrapper.find('.fa-hand-wave').exists()).toBe(true)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('renders the preview and actions slots', () => {
|
|
14
|
+
const wrapper = mount(PageCard, {
|
|
15
|
+
props: { title: 'Welkomstpagina' },
|
|
16
|
+
slots: {
|
|
17
|
+
preview: '<svg data-test="mockup" />',
|
|
18
|
+
actions: '<button data-test="menu">…</button>',
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
expect(wrapper.find('[data-test="mockup"]').exists()).toBe(true)
|
|
23
|
+
expect(wrapper.find('[data-test="menu"]').exists()).toBe(true)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('draws the mockup for the given type in the preview area', () => {
|
|
27
|
+
const wrapper = mount(PageCard, { props: { title: 'Over ons', mockup: 'about' } })
|
|
28
|
+
|
|
29
|
+
expect(wrapper.find('svg').exists()).toBe(true)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('lets the preview slot win over the mockup', () => {
|
|
33
|
+
const wrapper = mount(PageCard, {
|
|
34
|
+
props: { title: 'Over ons', mockup: 'about' },
|
|
35
|
+
slots: { preview: '<img data-test="thumb" />' },
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
expect(wrapper.find('[data-test="thumb"]').exists()).toBe(true)
|
|
39
|
+
expect(wrapper.find('svg').exists()).toBe(false)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('renders a div and no select button by default', () => {
|
|
43
|
+
const wrapper = mount(PageCard, { props: { title: 'Welkomstpagina' } })
|
|
44
|
+
|
|
45
|
+
expect(wrapper.element.tagName).toBe('DIV')
|
|
46
|
+
expect(wrapper.find('button').exists()).toBe(false)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('renders a stretched button with the title as name when selectable', () => {
|
|
50
|
+
const wrapper = mount(PageCard, { props: { title: 'Welkomstpagina', selectable: true } })
|
|
51
|
+
|
|
52
|
+
const button = wrapper.find('button')
|
|
53
|
+
expect(button.attributes('type')).toBe('button')
|
|
54
|
+
expect(button.attributes('aria-label')).toBe('Welkomstpagina')
|
|
55
|
+
expect(button.classes()).toContain('inset-0')
|
|
56
|
+
// The heading must stay real document structure next to the button, not inside it.
|
|
57
|
+
expect(wrapper.find('h3').element.closest('button')).toBeNull()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('emits select from the button', async () => {
|
|
61
|
+
const wrapper = mount(PageCard, { props: { title: 'Welkomstpagina', selectable: true } })
|
|
62
|
+
|
|
63
|
+
await wrapper.find('button').trigger('click')
|
|
64
|
+
|
|
65
|
+
expect(wrapper.emitted('select')).toHaveLength(1)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('draws the blue border when selected or dragging', () => {
|
|
69
|
+
const selected = mount(PageCard, {
|
|
70
|
+
props: { title: 'Welkomstpagina', selectable: true, selected: true },
|
|
71
|
+
})
|
|
72
|
+
const dragging = mount(PageCard, { props: { title: 'Welkomstpagina', dragging: true } })
|
|
73
|
+
const plain = mount(PageCard, { props: { title: 'Welkomstpagina' } })
|
|
74
|
+
|
|
75
|
+
expect(selected.classes()).toContain('border-cornflower-blue-500')
|
|
76
|
+
expect(selected.find('button').attributes('aria-current')).toBe('true')
|
|
77
|
+
expect(dragging.classes()).toContain('border-cornflower-blue-500')
|
|
78
|
+
expect(dragging.classes()).toContain('opacity-60')
|
|
79
|
+
expect(plain.classes()).toContain('border-gray-200')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('passes class and attributes through to the card', () => {
|
|
83
|
+
const wrapper = mount(PageCard, {
|
|
84
|
+
props: { title: 'Welkomstpagina' },
|
|
85
|
+
attrs: { class: 'max-w-md', 'data-key': '7' },
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
expect(wrapper.classes()).toContain('max-w-md')
|
|
89
|
+
expect(wrapper.attributes('data-key')).toBe('7')
|
|
90
|
+
})
|
|
91
|
+
})
|