@nexxtmove/ui 2.6.0 → 2.8.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.
Files changed (229) hide show
  1. package/dist/components/editor/molecules/EditorColor/EditorColor.vue.d.ts +7 -5
  2. package/dist/components/editor/molecules/EditorColor/_ColorField.vue.d.ts +4 -1
  3. package/dist/components/organisms/ColorPicker/ColorPicker.vue.d.ts +3 -1
  4. package/dist/index.js +95 -172
  5. package/dist/nuxt.js +0 -1
  6. package/package.json +38 -36
  7. package/src/attribute-contract.test.ts +151 -0
  8. package/src/components/atoms/AnimatedNumber/AnimatedNumber.stories.ts +36 -0
  9. package/src/components/atoms/AnimatedNumber/AnimatedNumber.test.ts +21 -0
  10. package/src/components/atoms/Avatar/Avatar.stories.ts +122 -0
  11. package/src/components/atoms/Avatar/Avatar.test.ts +134 -0
  12. package/src/components/atoms/Button/Button.stories.ts +290 -0
  13. package/src/components/atoms/Button/Button.test.ts +406 -0
  14. package/src/components/atoms/Card/Card.stories.ts +45 -0
  15. package/src/components/atoms/Card/Card.test.ts +46 -0
  16. package/src/components/atoms/Checkbox/Checkbox.stories.ts +35 -0
  17. package/src/components/atoms/Checkbox/Checkbox.test.ts +55 -0
  18. package/src/components/atoms/Chip/Chip.stories.ts +274 -0
  19. package/src/components/atoms/Chip/Chip.test.ts +230 -0
  20. package/src/components/atoms/EnergyLabel/EnergyLabel.stories.ts +189 -0
  21. package/src/components/atoms/EnergyLabel/EnergyLabel.test.ts +100 -0
  22. package/src/components/atoms/FloatingPanel/FloatingPanel.stories.ts +677 -0
  23. package/src/components/atoms/FloatingPanel/FloatingPanel.test.ts +864 -0
  24. package/src/components/atoms/Icon/Icon.stories.ts +63 -0
  25. package/src/components/atoms/Icon/Icon.test.ts +89 -0
  26. package/src/components/atoms/Loader/Loader.stories.ts +36 -0
  27. package/src/components/atoms/Loader/Loader.test.ts +40 -0
  28. package/src/components/atoms/PickerItem/PickerItem.stories.ts +172 -0
  29. package/src/components/atoms/PickerItem/PickerItem.test.ts +142 -0
  30. package/src/components/atoms/PickerPreview/PickerPreview.stories.ts +183 -0
  31. package/src/components/atoms/PickerPreview/PickerPreview.test.ts +137 -0
  32. package/src/components/atoms/PresenceIndicator/PresenceIndicator.stories.ts +21 -0
  33. package/src/components/atoms/PropertyListing/PropertyListing.stories.ts +38 -0
  34. package/src/components/atoms/PropertyListing/PropertyListing.test.ts +46 -0
  35. package/src/components/atoms/SelectableOption/SelectableOption.stories.ts +131 -0
  36. package/src/components/atoms/SelectableOption/SelectableOption.test.ts +96 -0
  37. package/src/components/atoms/Skeleton/Skeleton.stories.ts +27 -0
  38. package/src/components/atoms/Skeleton/Skeleton.test.ts +28 -0
  39. package/src/components/atoms/Slider/Slider.stories.ts +151 -0
  40. package/src/components/atoms/Slider/Slider.test.ts +191 -0
  41. package/src/components/atoms/Squircle/Squircle.stories.ts +114 -0
  42. package/src/components/atoms/Squircle/Squircle.test.ts +70 -0
  43. package/src/components/atoms/Switch/Switch.stories.ts +91 -0
  44. package/src/components/atoms/Switch/Switch.test.ts +183 -0
  45. package/src/components/atoms/Tooltip/Tooltip.stories.ts +158 -0
  46. package/src/components/atoms/Tooltip/Tooltip.test.ts +134 -0
  47. package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.stories.ts +124 -0
  48. package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.test.ts +464 -0
  49. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.stories.ts +266 -0
  50. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.test.ts +421 -0
  51. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.stories.ts +365 -0
  52. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.test.ts +484 -0
  53. package/src/components/editor/composables/useRadioGrid.test.ts +261 -0
  54. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.stories.ts +73 -0
  55. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.test.ts +189 -0
  56. package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.stories.ts +309 -0
  57. package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.test.ts +359 -0
  58. package/src/components/editor/molecules/EditorAnchor/EditorAnchor.stories.ts +138 -0
  59. package/src/components/editor/molecules/EditorAnchor/EditorAnchor.test.ts +344 -0
  60. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.stories.ts +87 -0
  61. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.test.ts +262 -0
  62. package/src/components/editor/molecules/EditorBlur/EditorBlur.stories.ts +115 -0
  63. package/src/components/editor/molecules/EditorBlur/EditorBlur.test.ts +243 -0
  64. package/src/components/editor/molecules/EditorBorder/EditorBorder.stories.ts +187 -0
  65. package/src/components/editor/molecules/EditorBorder/EditorBorder.test.ts +456 -0
  66. package/src/components/editor/molecules/EditorColor/EditorColor.stories.ts +633 -0
  67. package/src/components/editor/molecules/EditorColor/EditorColor.test.ts +776 -0
  68. package/src/components/editor/molecules/EditorColor/EditorColor.vue +7 -5
  69. package/src/components/editor/molecules/EditorColor/EditorColorGradient.test.ts +832 -0
  70. package/src/components/editor/molecules/EditorColor/_ColorField.vue +20 -15
  71. package/src/components/editor/molecules/EditorColor/gradientClass.test.ts +303 -0
  72. package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.stories.ts +72 -0
  73. package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.test.ts +124 -0
  74. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.stories.ts +46 -0
  75. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.test.ts +97 -0
  76. package/src/components/editor/molecules/EditorFontSize/EditorFontSize.stories.ts +248 -0
  77. package/src/components/editor/molecules/EditorFontSize/EditorFontSize.test.ts +288 -0
  78. package/src/components/editor/molecules/EditorGap/EditorGap.stories.ts +129 -0
  79. package/src/components/editor/molecules/EditorGap/EditorGap.test.ts +253 -0
  80. package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.stories.ts +68 -0
  81. package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.test.ts +150 -0
  82. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.stories.ts +71 -0
  83. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.test.ts +161 -0
  84. package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.stories.ts +204 -0
  85. package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.test.ts +238 -0
  86. package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.stories.ts +257 -0
  87. package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.test.ts +286 -0
  88. package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.stories.ts +135 -0
  89. package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.test.ts +230 -0
  90. package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.stories.ts +154 -0
  91. package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.test.ts +112 -0
  92. package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.stories.ts +206 -0
  93. package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.test.ts +332 -0
  94. package/src/components/editor/molecules/EditorPadding/EditorPadding.stories.ts +374 -0
  95. package/src/components/editor/molecules/EditorPadding/EditorPadding.test.ts +712 -0
  96. package/src/components/editor/molecules/EditorRadius/EditorRadius.stories.ts +64 -0
  97. package/src/components/editor/molecules/EditorRadius/EditorRadius.test.ts +137 -0
  98. package/src/components/editor/molecules/EditorShadow/EditorShadow.stories.ts +103 -0
  99. package/src/components/editor/molecules/EditorShadow/EditorShadow.test.ts +168 -0
  100. package/src/components/editor/molecules/EditorSize/EditorSize.stories.ts +84 -0
  101. package/src/components/editor/molecules/EditorSize/EditorSize.test.ts +253 -0
  102. package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.stories.ts +49 -0
  103. package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.test.ts +43 -0
  104. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.stories.ts +69 -0
  105. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.test.ts +155 -0
  106. package/src/components/molecules/Announcement/Announcement.stories.ts +224 -0
  107. package/src/components/molecules/Announcement/Announcement.test.ts +161 -0
  108. package/src/components/molecules/ContactInfoCard/ContactInfoCard.stories.ts +99 -0
  109. package/src/components/molecules/ContactInfoCard/ContactInfoCard.test.ts +70 -0
  110. package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.stories.ts +148 -0
  111. package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.test.ts +271 -0
  112. package/src/components/molecules/DoughnutChart/DoughnutChart.stories.ts +45 -0
  113. package/src/components/molecules/DoughnutChart/DoughnutChart.test.ts +205 -0
  114. package/src/components/molecules/InfoBlock/InfoBlock.stories.ts +84 -0
  115. package/src/components/molecules/InfoBlock/InfoBlock.test.ts +101 -0
  116. package/src/components/molecules/InputField/InputField.stories.ts +117 -0
  117. package/src/components/molecules/InputField/InputField.test.ts +138 -0
  118. package/src/components/molecules/InputSelect/InputSelect.stories.ts +116 -0
  119. package/src/components/molecules/InputSelect/InputSelect.test.ts +127 -0
  120. package/src/components/molecules/ModalFooter/ModalFooter.stories.ts +204 -0
  121. package/src/components/molecules/ModalFooter/ModalFooter.test.ts +276 -0
  122. package/src/components/molecules/PageCard/PageCard.stories.ts +100 -0
  123. package/src/components/molecules/PageCard/PageCard.test.ts +91 -0
  124. package/src/components/molecules/PageContainer/PageContainer.stories.ts +90 -0
  125. package/src/components/molecules/PageContainer/PageContainer.test.ts +50 -0
  126. package/src/components/molecules/PageMockup/PageMockup.stories.ts +38 -0
  127. package/src/components/molecules/PageMockup/PageMockup.test.ts +20 -0
  128. package/src/components/molecules/PageTypeTile/PageTypeTile.stories.ts +63 -0
  129. package/src/components/molecules/PageTypeTile/PageTypeTile.test.ts +41 -0
  130. package/src/components/molecules/Pagination/Pagination.stories.ts +72 -0
  131. package/src/components/molecules/Pagination/Pagination.test.ts +130 -0
  132. package/src/components/molecules/PickerGroup/PickerGroup.stories.ts +90 -0
  133. package/src/components/molecules/PickerGroup/PickerGroup.test.ts +145 -0
  134. package/src/components/molecules/PickerList/PickerList.stories.ts +436 -0
  135. package/src/components/molecules/PickerList/PickerList.test.ts +870 -0
  136. package/src/components/molecules/PlaceholderInput/PlaceholderInput.stories.ts +48 -0
  137. package/src/components/molecules/PlaceholderInput/PlaceholderInput.test.ts +107 -0
  138. package/src/components/molecules/PropertyCard/PropertyCard.stories.ts +46 -0
  139. package/src/components/molecules/PropertyCard/PropertyCard.test.ts +59 -0
  140. package/src/components/molecules/Select/Select.stories.ts +264 -0
  141. package/src/components/molecules/Select/Select.test.ts +315 -0
  142. package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.stories.ts +84 -0
  143. package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.test.ts +103 -0
  144. package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.stories.ts +11 -0
  145. package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.test.ts +38 -0
  146. package/src/components/molecules/SocialIcons/SocialIcons.stories.ts +28 -0
  147. package/src/components/molecules/SocialIcons/SocialIcons.test.ts +58 -0
  148. package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.stories.ts +11 -0
  149. package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.test.ts +116 -0
  150. package/src/components/molecules/SocialMediaType/SocialMediaType.stories.ts +37 -0
  151. package/src/components/molecules/SocialMediaType/SocialMediaType.test.ts +126 -0
  152. package/src/components/molecules/Source/Source.stories.ts +35 -0
  153. package/src/components/molecules/Source/Source.test.ts +45 -0
  154. package/src/components/molecules/TabbedContent/TabbedContent.stories.ts +96 -0
  155. package/src/components/molecules/TabbedContent/TabbedContent.test.ts +109 -0
  156. package/src/components/molecules/Tabs/Tabs.stories.ts +230 -0
  157. package/src/components/molecules/Tabs/Tabs.test.ts +280 -0
  158. package/src/components/molecules/TemplateCard/TemplateCard.stories.ts +327 -0
  159. package/src/components/molecules/TemplateCard/TemplateCard.test.ts +339 -0
  160. package/src/components/molecules/Textarea/Textarea.stories.ts +216 -0
  161. package/src/components/molecules/Textarea/Textarea.test.ts +191 -0
  162. package/src/components/molecules/TimelineEvent/TimelineEvent.stories.ts +319 -0
  163. package/src/components/molecules/TimelineEvent/TimelineEvent.test.ts +224 -0
  164. package/src/components/molecules/TimelineLanes/TimelineLanes.stories.ts +29 -0
  165. package/src/components/molecules/TimelineLanes/TimelineLanes.test.ts +30 -0
  166. package/src/components/molecules/Toast/Toast.stories.ts +110 -0
  167. package/src/components/molecules/Toast/Toast.test.ts +103 -0
  168. package/src/components/molecules/ToggleGroup/ToggleGroup.stories.ts +297 -0
  169. package/src/components/molecules/ToggleGroup/ToggleGroup.test.ts +578 -0
  170. package/src/components/molecules/Tree/Tree.stories.ts +884 -0
  171. package/src/components/molecules/Tree/Tree.test.ts +1247 -0
  172. package/src/components/molecules/Tree/tree-drop.test.ts +223 -0
  173. package/src/components/organisms/Calendar/Calendar.stories.ts +108 -0
  174. package/src/components/organisms/Calendar/Calendar.test.ts +279 -0
  175. package/src/components/organisms/Calendar/_CalendarDayView.test.ts +104 -0
  176. package/src/components/organisms/Calendar/_CalendarHeader.test.ts +86 -0
  177. package/src/components/organisms/Calendar/_CalendarMonthView.test.ts +52 -0
  178. package/src/components/organisms/Calendar/_CalendarTimePicker.test.ts +89 -0
  179. package/src/components/organisms/Calendar/_CalendarYearView.test.ts +56 -0
  180. package/src/components/organisms/ColorPicker/ColorPicker.stories.ts +229 -0
  181. package/src/components/organisms/ColorPicker/ColorPicker.test.ts +696 -0
  182. package/src/components/organisms/ColorPicker/ColorPicker.vue +12 -3
  183. package/src/components/organisms/ColorPicker/gradient.test.ts +67 -0
  184. package/src/components/organisms/ColorPicker/gradientGeometry.test.ts +206 -0
  185. package/src/components/organisms/Combobox/Combobox.stories.ts +234 -0
  186. package/src/components/organisms/Combobox/Combobox.test.ts +512 -0
  187. package/src/components/organisms/Combobox/Combobox.vue +1 -1
  188. package/src/components/organisms/ComboboxPanel/ComboboxPanel.stories.ts +158 -0
  189. package/src/components/organisms/ComboboxPanel/ComboboxPanel.test.ts +524 -0
  190. package/src/components/organisms/ComboboxPanel/ComboboxPanel.vue +1 -1
  191. package/src/components/organisms/CountrySelect/CountrySelect.stories.ts +139 -0
  192. package/src/components/organisms/CountrySelect/CountrySelect.test.ts +149 -0
  193. package/src/components/organisms/DatePicker/DatePicker.stories.ts +163 -0
  194. package/src/components/organisms/DatePicker/DatePicker.test.ts +214 -0
  195. package/src/components/organisms/DropdownButton/DropdownButton.stories.ts +123 -0
  196. package/src/components/organisms/DropdownButton/DropdownButton.test.ts +379 -0
  197. package/src/components/organisms/FontPicker/FontPicker.stories.ts +476 -0
  198. package/src/components/organisms/FontPicker/FontPicker.test.ts +873 -0
  199. package/src/components/organisms/Header/Header.stories.ts +75 -0
  200. package/src/components/organisms/Header/Header.test.ts +233 -0
  201. package/src/components/organisms/IconPicker/IconPicker.stories.ts +734 -0
  202. package/src/components/organisms/IconPicker/IconPicker.test.ts +669 -0
  203. package/src/components/organisms/IconPicker/_IconPickerGrid.test.ts +184 -0
  204. package/src/components/organisms/IconPicker/_IconPickerRail.test.ts +102 -0
  205. package/src/components/organisms/IconPicker/useIconPickerNavigation.test.ts +215 -0
  206. package/src/components/organisms/Modal/Modal.stories.ts +350 -0
  207. package/src/components/organisms/Modal/Modal.test.ts +275 -0
  208. package/src/components/organisms/PickerPanel/PickerPanel.stories.ts +452 -0
  209. package/src/components/organisms/PickerPanel/PickerPanel.test.ts +358 -0
  210. package/src/components/organisms/ProgressBar/ProgressBar.stories.ts +100 -0
  211. package/src/components/organisms/ProgressBar/ProgressBar.test.ts +417 -0
  212. package/src/components/organisms/ReferenceCard/ReferenceCard.stories.ts +191 -0
  213. package/src/components/organisms/ReferenceCard/ReferenceCard.test.ts +165 -0
  214. package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.stories.ts +65 -0
  215. package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.test.ts +466 -0
  216. package/src/components/organisms/StepperHeader/StepperHeader.stories.ts +91 -0
  217. package/src/components/organisms/StepperHeader/StepperHeader.test.ts +279 -0
  218. package/src/components/organisms/Table/Table.stories.ts +332 -0
  219. package/src/components/organisms/Table/Table.test.ts +320 -0
  220. package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.stories.ts +198 -0
  221. package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.test.ts +283 -0
  222. package/src/components/organisms/TimelineTrack/TimelineTrack.stories.ts +120 -0
  223. package/src/components/organisms/TimelineTrack/TimelineTrack.test.ts +91 -0
  224. package/src/components/organisms/Toaster/Toaster.stories.ts +51 -0
  225. package/src/components/organisms/Toaster/Toaster.test.ts +75 -0
  226. package/src/components/organisms/VideoControls/VideoControls.stories.ts +792 -0
  227. package/src/components/organisms/VideoControls/VideoControls.test.ts +1056 -0
  228. package/src/components/organisms/VideoPlayer/VideoPlayer.stories.ts +327 -0
  229. package/src/components/organisms/VideoPlayer/VideoPlayer.test.ts +439 -0
@@ -0,0 +1,339 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import TemplateCard from './TemplateCard.vue'
4
+
5
+ describe('TemplateCard', () => {
6
+ it('renders the title and subtitle', () => {
7
+ const wrapper = mount(TemplateCard, {
8
+ props: {
9
+ image: 'https://example.com/image.webp',
10
+ title: 'Basis template',
11
+ subtitle: '3 dagen geleden aangepast',
12
+ },
13
+ })
14
+
15
+ expect(wrapper.text()).toContain('Basis template')
16
+ expect(wrapper.text()).toContain('3 dagen geleden aangepast')
17
+ })
18
+
19
+ it('renders the image when provided', () => {
20
+ const wrapper = mount(TemplateCard, {
21
+ props: {
22
+ image: 'https://example.com/image.webp',
23
+ title: 'Basis template',
24
+ },
25
+ })
26
+
27
+ const img = wrapper.find('img')
28
+ expect(img.exists()).toBe(true)
29
+ expect(img.attributes('src')).toBe('https://example.com/image.webp')
30
+ // Decorative: the title is right below it as text, so a duplicate alt only adds noise.
31
+ expect(img.attributes('alt')).toBe('')
32
+ expect(img.classes()).toContain('h-38')
33
+ expect(img.classes()).toContain('object-cover')
34
+ })
35
+
36
+ it('renders a placeholder when image is undefined', () => {
37
+ const wrapper = mount(TemplateCard, {
38
+ props: {
39
+ title: 'Basis template',
40
+ },
41
+ })
42
+
43
+ expect(wrapper.find('img').exists()).toBe(false)
44
+ expect(wrapper.find('.bg-gray-100 .fa-image').exists()).toBe(true)
45
+ // The placeholder and a real image must be the same height, or cards in a grid end up unequal.
46
+ expect(wrapper.find('.bg-gray-100').classes()).toContain('h-38')
47
+ })
48
+
49
+ it('does not render a subtitle when omitted', () => {
50
+ const wrapper = mount(TemplateCard, {
51
+ props: {
52
+ title: 'Basis template',
53
+ },
54
+ })
55
+
56
+ expect(wrapper.find('p').exists()).toBe(false)
57
+ })
58
+
59
+ it('does not render the feature list when contentFeatures is empty', () => {
60
+ const wrapper = mount(TemplateCard, {
61
+ props: {
62
+ title: 'Basis template',
63
+ },
64
+ })
65
+
66
+ expect(wrapper.find('ul').exists()).toBe(false)
67
+ })
68
+
69
+ it('renders an icon for each content feature', () => {
70
+ const wrapper = mount(TemplateCard, {
71
+ props: {
72
+ title: 'Basis template',
73
+ contentFeatures: ['references', 'valuation', 'reviews', 'packages'],
74
+ },
75
+ })
76
+
77
+ expect(wrapper.findAll('li')).toHaveLength(4)
78
+ expect(wrapper.find('.fa-house-circle-check').exists()).toBe(true)
79
+ expect(wrapper.find('.fa-circle-euro').exists()).toBe(true)
80
+ expect(wrapper.find('.fa-star').exists()).toBe(true)
81
+ expect(wrapper.find('.fa-piggy-bank').exists()).toBe(true)
82
+ })
83
+
84
+ it('applies the feature colors', () => {
85
+ const wrapper = mount(TemplateCard, {
86
+ props: {
87
+ title: 'Basis template',
88
+ contentFeatures: ['references', 'packages'],
89
+ },
90
+ })
91
+
92
+ expect(wrapper.find('.bg-cornflower-blue-50.text-cornflower-blue-500').exists()).toBe(true)
93
+ expect(wrapper.find('.bg-green-50.text-green-500').exists()).toBe(true)
94
+ })
95
+
96
+ it('renders a div and no button by default', () => {
97
+ const wrapper = mount(TemplateCard, { props: { title: 'Basis template' } })
98
+
99
+ expect(wrapper.element.tagName).toBe('DIV')
100
+ expect(wrapper.find('button').exists()).toBe(false)
101
+ })
102
+
103
+ it('renders a stretched button that never submits a form when selectable', () => {
104
+ const wrapper = mount(TemplateCard, { props: { title: 'Basis template', selectable: true } })
105
+
106
+ const button = wrapper.find('button')
107
+ expect(button.exists()).toBe(true)
108
+ expect(button.attributes('type')).toBe('button')
109
+ // The card content is its own layer next to the button, so the button needs a name of its own.
110
+ expect(button.attributes('aria-label')).toBe('Basis template')
111
+ expect(button.attributes('aria-pressed')).toBe('false')
112
+ expect(button.classes()).toContain('absolute')
113
+ expect(button.classes()).toContain('inset-0')
114
+ })
115
+
116
+ it('keeps the heading and the tooltip triggers outside the select button', () => {
117
+ const wrapper = mount(TemplateCard, {
118
+ props: { title: 'Basis template', selectable: true, contentFeatures: ['references'] },
119
+ })
120
+
121
+ // Flow content inside a <button> is invalid and the heading role would be lost, so the card
122
+ // body must stay a sibling of the button.
123
+ const button = wrapper.find('button')
124
+ expect(button.find('h3').exists()).toBe(false)
125
+ expect(wrapper.find('h3').exists()).toBe(true)
126
+ expect(wrapper.find('h3').element.closest('button')).toBeNull()
127
+ expect(wrapper.find('.bg-cornflower-blue-50').element.closest('button')).toBeNull()
128
+ })
129
+
130
+ it('emits select when the selectable card is clicked', async () => {
131
+ const wrapper = mount(TemplateCard, { props: { title: 'Basis template', selectable: true } })
132
+
133
+ await wrapper.find('button').trigger('click')
134
+
135
+ expect(wrapper.emitted('select')).toHaveLength(1)
136
+ })
137
+
138
+ it('does not emit select while disabled', async () => {
139
+ const wrapper = mount(TemplateCard, {
140
+ props: { title: 'Basis template', selectable: true, disabled: true },
141
+ })
142
+
143
+ const button = wrapper.find('button')
144
+ expect((button.element as HTMLButtonElement).disabled).toBe(true)
145
+
146
+ await button.trigger('click')
147
+
148
+ expect(wrapper.emitted('select')).toBeUndefined()
149
+ })
150
+
151
+ it('does not emit select when it is not selectable', async () => {
152
+ const wrapper = mount(TemplateCard, { props: { title: 'Basis template' } })
153
+
154
+ await wrapper.trigger('click')
155
+
156
+ expect(wrapper.emitted('select')).toBeUndefined()
157
+ })
158
+
159
+ it('selects the card when a feature icon is clicked', async () => {
160
+ const wrapper = mount(TemplateCard, {
161
+ props: { title: 'Basis template', selectable: true, contentFeatures: ['references'] },
162
+ })
163
+
164
+ // The icon claims its own pointer events for the tooltip, so without forwarding the click it
165
+ // would be a dead zone on top of the stretched button.
166
+ await wrapper.find('.bg-cornflower-blue-50').trigger('click')
167
+
168
+ expect(wrapper.emitted('select')).toHaveLength(1)
169
+ })
170
+
171
+ it('does not select on a feature icon of a read-only card', async () => {
172
+ const wrapper = mount(TemplateCard, {
173
+ props: { title: 'Basis template', contentFeatures: ['references'] },
174
+ })
175
+
176
+ await wrapper.find('.bg-cornflower-blue-50').trigger('click')
177
+
178
+ expect(wrapper.emitted('select')).toBeUndefined()
179
+ })
180
+
181
+ it('names the feature icons for screen readers', () => {
182
+ const wrapper = mount(TemplateCard, {
183
+ props: { title: 'Basis template', contentFeatures: ['references'] },
184
+ })
185
+
186
+ // The icon itself is aria-hidden and the trigger is not focusable, so without this label the
187
+ // feature is invisible to a screen reader.
188
+ expect(wrapper.find('.bg-cornflower-blue-50 .sr-only').text()).toBe('Referenties')
189
+ })
190
+
191
+ it('exposes the selected state through aria-pressed', async () => {
192
+ const wrapper = mount(TemplateCard, { props: { title: 'Basis template', selectable: true } })
193
+
194
+ expect(wrapper.find('button').attributes('aria-pressed')).toBe('false')
195
+
196
+ await wrapper.setProps({ selected: true })
197
+
198
+ expect(wrapper.find('button').attributes('aria-pressed')).toBe('true')
199
+ })
200
+
201
+ it('omits aria-pressed on the non-selectable card', () => {
202
+ const wrapper = mount(TemplateCard, { props: { title: 'Basis template', selected: true } })
203
+
204
+ expect(wrapper.attributes('aria-pressed')).toBeUndefined()
205
+ expect(wrapper.find('[aria-pressed]').exists()).toBe(false)
206
+ })
207
+
208
+ it('draws the selected border only when selectable', async () => {
209
+ const wrapper = mount(TemplateCard, {
210
+ props: { title: 'Basis template', selected: true, checkmark: true },
211
+ })
212
+
213
+ // selected, disabled and checkmark only mean something together with selectable, as their
214
+ // JSDoc says: a read-only card must not look chosen.
215
+ expect(wrapper.classes()).not.toContain('border-cornflower-blue-500')
216
+ expect(wrapper.find('i.fa-check').exists()).toBe(false)
217
+
218
+ await wrapper.setProps({ selectable: true })
219
+
220
+ expect(wrapper.classes()).toContain('border-cornflower-blue-500')
221
+ })
222
+
223
+ it('shows the check badge only with checkmark and selected', async () => {
224
+ const wrapper = mount(TemplateCard, {
225
+ props: { title: 'Basis template', selectable: true, selected: true },
226
+ })
227
+
228
+ expect(wrapper.find('i.fa-check').exists()).toBe(false)
229
+
230
+ await wrapper.setProps({ checkmark: true })
231
+
232
+ expect(wrapper.find('i.fa-check').exists()).toBe(true)
233
+
234
+ await wrapper.setProps({ selected: false })
235
+
236
+ expect(wrapper.find('i.fa-check').exists()).toBe(false)
237
+ })
238
+
239
+ it('renders overlay and actions content outside the select button', () => {
240
+ const wrapper = mount(TemplateCard, {
241
+ props: { title: 'Basis template', selectable: true },
242
+ slots: {
243
+ overlay: '<button type="button" data-test="overlay-button">Bekijk</button>',
244
+ actions: '<button type="button" data-test="actions-button">Menu</button>',
245
+ },
246
+ })
247
+
248
+ expect(wrapper.find('[data-test="overlay-button"]').exists()).toBe(true)
249
+ expect(wrapper.find('[data-test="actions-button"]').exists()).toBe(true)
250
+
251
+ // Nested buttons are invalid markup and swallow each other's clicks, so both slots must stay
252
+ // siblings of the select button.
253
+ const button = wrapper.find('button[aria-pressed]')
254
+ expect(button.find('[data-test="overlay-button"]').exists()).toBe(false)
255
+ expect(button.find('[data-test="actions-button"]').exists()).toBe(false)
256
+ })
257
+
258
+ it('renders no actions container when the slot is empty', () => {
259
+ const bare = mount(TemplateCard, { props: { title: 'Basis template', selectable: true } })
260
+ const withActions = mount(TemplateCard, {
261
+ props: { title: 'Basis template', selectable: true },
262
+ slots: { actions: '<button type="button" data-test="actions-button">Menu</button>' },
263
+ })
264
+
265
+ // An empty container would still sit over the top-right corner of the card, so it is dropped
266
+ // with its slot: the root then holds only the card body and the select button.
267
+ expect(bare.element.children).toHaveLength(2)
268
+ expect(withActions.element.children).toHaveLength(3)
269
+ expect(withActions.find('[data-test="actions-button"]').exists()).toBe(true)
270
+ })
271
+
272
+ it('renders no overlay layer when the slot is empty', () => {
273
+ const wrapper = mount(TemplateCard, { props: { title: 'Basis template', selectable: true } })
274
+
275
+ expect(wrapper.find('.backdrop-blur-xs').exists()).toBe(false)
276
+ })
277
+
278
+ it('drops the overlay while selectable and disabled', () => {
279
+ const wrapper = mount(TemplateCard, {
280
+ props: { title: 'Basis template', selectable: true, disabled: true },
281
+ slots: { overlay: '<button type="button" data-test="overlay-button">Bekijk</button>' },
282
+ })
283
+
284
+ // An invisible layer would still hold a tabbable button, and a card that cannot be chosen
285
+ // reveals nothing. #actions is the place for a preview button on a disabled card.
286
+ expect(wrapper.find('.backdrop-blur-xs').exists()).toBe(false)
287
+ expect(wrapper.find('[data-test="overlay-button"]').exists()).toBe(false)
288
+ })
289
+
290
+ it('keeps the overlay on a read-only card, disabled or not', () => {
291
+ const wrapper = mount(TemplateCard, {
292
+ props: { title: 'Basis template', disabled: true },
293
+ slots: { overlay: '<button type="button" data-test="overlay-button">Bekijk</button>' },
294
+ })
295
+
296
+ expect(wrapper.find('.backdrop-blur-xs').exists()).toBe(true)
297
+ expect(wrapper.find('[data-test="overlay-button"]').exists()).toBe(true)
298
+ })
299
+
300
+ it('lets the slot buttons keep their own clicks', async () => {
301
+ const wrapper = mount(TemplateCard, {
302
+ props: { title: 'Basis template', selectable: true },
303
+ slots: {
304
+ overlay: '<button type="button" data-test="overlay-button">Bekijk</button>',
305
+ actions: '<button type="button" data-test="actions-button">Menu</button>',
306
+ },
307
+ })
308
+
309
+ const overlayButton = wrapper.find('[data-test="overlay-button"]')
310
+ const actionsButton = wrapper.find('[data-test="actions-button"]')
311
+
312
+ await overlayButton.trigger('click')
313
+ await actionsButton.trigger('click')
314
+
315
+ expect(wrapper.emitted('select')).toBeUndefined()
316
+
317
+ // Both layers come after the select button and carry a z-index, so they win the hit test
318
+ // instead of the stretched button swallowing the click.
319
+ const order = [...wrapper.element.querySelectorAll('button')]
320
+ expect(order.indexOf(wrapper.find('button[aria-pressed]').element)).toBe(0)
321
+ expect(order.indexOf(actionsButton.element)).toBeGreaterThan(0)
322
+ expect(order.indexOf(overlayButton.element)).toBeGreaterThan(0)
323
+ expect(actionsButton.element.closest('.z-20')).not.toBeNull()
324
+ expect(overlayButton.element.closest('.z-10')).not.toBeNull()
325
+ })
326
+
327
+ it('hands selected to the overlay and actions slots', () => {
328
+ const wrapper = mount(TemplateCard, {
329
+ props: { title: 'Basis template', selectable: true, selected: true },
330
+ slots: {
331
+ overlay: `<template #overlay="{ selected }"><span data-test="overlay">{{ selected ? 'aan' : 'uit' }}</span></template>`,
332
+ actions: `<template #actions="{ selected }"><span data-test="actions">{{ selected ? 'aan' : 'uit' }}</span></template>`,
333
+ },
334
+ })
335
+
336
+ expect(wrapper.find('[data-test="overlay"]').text()).toBe('aan')
337
+ expect(wrapper.find('[data-test="actions"]').text()).toBe('aan')
338
+ })
339
+ })
@@ -0,0 +1,216 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { expect, within } from 'storybook/test'
3
+ import { ref } from 'vue'
4
+ import { NexxtInputField, NexxtTextarea } from '../../../index'
5
+
6
+ export default {
7
+ title: 'Components/Molecules/Textarea',
8
+ component: NexxtTextarea,
9
+ } satisfies Meta<typeof NexxtTextarea>
10
+
11
+ type Story = StoryObj<typeof NexxtTextarea>
12
+
13
+ const withModel = (attrs: string, initial = '') => ({
14
+ components: { NexxtTextarea },
15
+ setup() {
16
+ const value = ref(initial)
17
+ return { value }
18
+ },
19
+ template: `<div style="max-width: 480px"><NexxtTextarea v-model="value" ${attrs} /></div>`,
20
+ })
21
+
22
+ /**
23
+ * The default: a fixed row count, so the field keeps a predictable height.
24
+ */
25
+ export const Default: Story = {
26
+ render: () => withModel('label="Toelichting" placeholder="Typ hier je toelichting"'),
27
+ }
28
+
29
+ /**
30
+ * The field is `inline-flex` by default, so it takes the width of its content.
31
+ * A consumer that wants it to fill its container passes `class="w-full"`, which
32
+ * lands on the wrapper -- the control inside is already full-width.
33
+ */
34
+ export const FullWidth: Story = {
35
+ render: () => ({
36
+ components: { NexxtTextarea },
37
+ setup() {
38
+ const value = ref('')
39
+ return { value }
40
+ },
41
+ template: `
42
+ <div style="width: 100%; border: 1px dashed #d1d5db; padding: 12px">
43
+ <NexxtTextarea v-model="value" class="w-full" label="Vult de volle breedte"
44
+ placeholder="Deze groeit mee met de container" />
45
+ </div>
46
+ `,
47
+ }),
48
+ play: async ({ canvasElement }) => {
49
+ const field = within(canvasElement).getByRole('textbox')
50
+ const container = canvasElement.querySelector('div[style]') as HTMLElement
51
+
52
+ // Within a pixel of the container's inner width: proof that `w-full`
53
+ // reached the wrapper, since the control alone cannot widen a shrink-wrapped
54
+ // parent.
55
+ const inner = container.clientWidth - 24
56
+ await expect(Math.abs(field.getBoundingClientRect().width - inner)).toBeLessThanOrEqual(1)
57
+ },
58
+ }
59
+
60
+ /** Without `w-full` the field stays at its natural width, for comparison. */
61
+ export const NaturalWidth: Story = {
62
+ render: () => ({
63
+ components: { NexxtTextarea },
64
+ setup() {
65
+ const value = ref('')
66
+ return { value }
67
+ },
68
+ template: `
69
+ <div style="width: 100%; border: 1px dashed #d1d5db; padding: 12px">
70
+ <NexxtTextarea v-model="value" label="Neemt zijn eigen breedte" />
71
+ </div>
72
+ `,
73
+ }),
74
+ }
75
+
76
+ export const WithoutLabel: Story = {
77
+ render: () => withModel('placeholder="Zonder label"'),
78
+ }
79
+
80
+ export const Required: Story = {
81
+ render: () => withModel('label="Toelichting" required placeholder="Verplicht veld"'),
82
+ }
83
+
84
+ export const WithError: Story = {
85
+ render: () =>
86
+ withModel('label="Toelichting" error error-message="Dit veld is verplicht"', 'Te kort'),
87
+ }
88
+
89
+ /** More rows for a field that is expected to hold a few paragraphs. */
90
+ export const MoreRows: Story = {
91
+ render: () => withModel('label="Bericht" :rows="8" placeholder="Acht regels hoog"'),
92
+ }
93
+
94
+ /**
95
+ * `autosize` grows the field with its content. Opt-in on purpose: a field that
96
+ * changes height while you type moves whatever sits below it, which is rarely
97
+ * what a form wants by default.
98
+ */
99
+ export const Autosize: Story = {
100
+ render: () =>
101
+ withModel(
102
+ 'label="Groeit mee" autosize placeholder="Typ meerdere regels en kijk wat er gebeurt"',
103
+ 'Eén regel.\nTwee regels.\nDrie regels.',
104
+ ),
105
+ // jsdom has no layout engine, so the unit tests stub scrollHeight and
106
+ // line-height. Only a real browser proves the field actually grows.
107
+ play: async ({ canvasElement }) => {
108
+ const field = within(canvasElement).getByRole('textbox') as HTMLTextAreaElement
109
+ const threeLines = field.getBoundingClientRect().height
110
+
111
+ field.value = Array.from({ length: 10 }, (_, i) => `Regel ${i + 1}`).join('\n')
112
+ field.dispatchEvent(new Event('input', { bubbles: true }))
113
+ await new Promise((r) => requestAnimationFrame(r))
114
+
115
+ await expect(field.getBoundingClientRect().height).toBeGreaterThan(threeLines)
116
+ },
117
+ }
118
+
119
+ /** With `maxRows` the field stops growing and starts scrolling instead. */
120
+ export const AutosizeWithMax: Story = {
121
+ render: () =>
122
+ withModel(
123
+ 'label="Groeit tot 5 regels" autosize :max-rows="5"',
124
+ 'Regel een\nRegel twee\nRegel drie\nRegel vier\nRegel vijf\nRegel zes\nRegel zeven',
125
+ ),
126
+ // Seven lines of content against a five-row cap: the field must stop at the
127
+ // cap and scroll, not keep growing.
128
+ play: async ({ canvasElement }) => {
129
+ const field = within(canvasElement).getByRole('textbox') as HTMLTextAreaElement
130
+ await new Promise((r) => requestAnimationFrame(r))
131
+
132
+ const style = getComputedStyle(field)
133
+ const lineHeight = parseFloat(style.lineHeight)
134
+ const padding = parseFloat(style.paddingTop) + parseFloat(style.paddingBottom)
135
+ const border = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth)
136
+
137
+ // The cap is five rows of text plus the field's own padding and border,
138
+ // because `height` is border-box here. Measuring against rows alone would
139
+ // expect a field that is padding-and-border too short.
140
+ const capped = 5 * lineHeight + padding + border
141
+
142
+ await expect(field.scrollHeight).toBeGreaterThan(capped - padding - border)
143
+ await expect(field.getBoundingClientRect().height).toBeLessThanOrEqual(capped + 1)
144
+ await expect(getComputedStyle(field).overflowY).toBe('auto')
145
+ },
146
+ }
147
+
148
+ /**
149
+ * A one-row field lines up with a NexxtInputField. Without a floor it would sit
150
+ * a few pixels lower, which is visible the moment the two share a form row.
151
+ */
152
+ export const MatchesInputFieldHeight: Story = {
153
+ render: () => ({
154
+ components: { NexxtTextarea, NexxtInputField },
155
+ setup() {
156
+ const text = ref('')
157
+ const area = ref('')
158
+ return { text, area }
159
+ },
160
+ template: `
161
+ <div style="display: flex; gap: 12px; align-items: flex-start; width: 520px">
162
+ <NexxtInputField v-model="text" class="w-full" placeholder="NexxtInputField" />
163
+ <NexxtTextarea v-model="area" class="w-full" :rows="1" placeholder="NexxtTextarea, 1 rij" />
164
+ </div>
165
+ `,
166
+ }),
167
+ play: async ({ canvasElement }) => {
168
+ const input = canvasElement.querySelector('input') as HTMLElement
169
+ const area = canvasElement.querySelector('textarea') as HTMLElement
170
+
171
+ await expect(area.getBoundingClientRect().height).toBe(input.getBoundingClientRect().height)
172
+ },
173
+ }
174
+
175
+ /**
176
+ * `resize` controls the drag grip in the corner. `vertical` is the default;
177
+ * `none` pins the height, which is what a form wants when the field sits in a
178
+ * fixed layout.
179
+ */
180
+ export const Resize: Story = {
181
+ render: () => ({
182
+ components: { NexxtTextarea },
183
+ setup() {
184
+ const a = ref('')
185
+ const b = ref('')
186
+ return { a, b }
187
+ },
188
+ template: `
189
+ <div style="display: flex; gap: 12px; width: 620px">
190
+ <NexxtTextarea v-model="a" class="w-full" label="Sleepbaar (standaard)" />
191
+ <NexxtTextarea v-model="b" class="w-full" label="Vaste hoogte" resize="none" />
192
+ </div>
193
+ `,
194
+ }),
195
+ // A class assertion would pass whether or not Tailwind generated any CSS for
196
+ // it. Only the computed value says the grip is actually gone.
197
+ play: async ({ canvasElement }) => {
198
+ const [draggable, pinned] = canvasElement.querySelectorAll('textarea')
199
+
200
+ await expect(getComputedStyle(draggable).resize).toBe('vertical')
201
+ await expect(getComputedStyle(pinned).resize).toBe('none')
202
+ },
203
+ }
204
+
205
+ /** `autosize` owns the height, so it drops the grip whatever `resize` says. */
206
+ export const AutosizeIgnoresResize: Story = {
207
+ render: () => withModel('label="Groeit mee" autosize resize="vertical"', 'Eén regel.'),
208
+ play: async ({ canvasElement }) => {
209
+ const field = within(canvasElement).getByRole('textbox')
210
+ await expect(getComputedStyle(field).resize).toBe('none')
211
+ },
212
+ }
213
+
214
+ export const Disabled: Story = {
215
+ render: () => withModel('label="Niet te bewerken" disabled', 'Vaste inhoud'),
216
+ }