@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.
Files changed (223) hide show
  1. package/dist/index.js +49 -126
  2. package/dist/nuxt.js +0 -1
  3. package/package.json +38 -36
  4. package/src/attribute-contract.test.ts +151 -0
  5. package/src/components/atoms/AnimatedNumber/AnimatedNumber.stories.ts +36 -0
  6. package/src/components/atoms/AnimatedNumber/AnimatedNumber.test.ts +21 -0
  7. package/src/components/atoms/Avatar/Avatar.stories.ts +122 -0
  8. package/src/components/atoms/Avatar/Avatar.test.ts +134 -0
  9. package/src/components/atoms/Button/Button.stories.ts +290 -0
  10. package/src/components/atoms/Button/Button.test.ts +406 -0
  11. package/src/components/atoms/Card/Card.stories.ts +45 -0
  12. package/src/components/atoms/Card/Card.test.ts +46 -0
  13. package/src/components/atoms/Checkbox/Checkbox.stories.ts +35 -0
  14. package/src/components/atoms/Checkbox/Checkbox.test.ts +55 -0
  15. package/src/components/atoms/Chip/Chip.stories.ts +274 -0
  16. package/src/components/atoms/Chip/Chip.test.ts +230 -0
  17. package/src/components/atoms/EnergyLabel/EnergyLabel.stories.ts +189 -0
  18. package/src/components/atoms/EnergyLabel/EnergyLabel.test.ts +100 -0
  19. package/src/components/atoms/FloatingPanel/FloatingPanel.stories.ts +677 -0
  20. package/src/components/atoms/FloatingPanel/FloatingPanel.test.ts +864 -0
  21. package/src/components/atoms/Icon/Icon.stories.ts +63 -0
  22. package/src/components/atoms/Icon/Icon.test.ts +89 -0
  23. package/src/components/atoms/Loader/Loader.stories.ts +36 -0
  24. package/src/components/atoms/Loader/Loader.test.ts +40 -0
  25. package/src/components/atoms/PickerItem/PickerItem.stories.ts +172 -0
  26. package/src/components/atoms/PickerItem/PickerItem.test.ts +142 -0
  27. package/src/components/atoms/PickerPreview/PickerPreview.stories.ts +183 -0
  28. package/src/components/atoms/PickerPreview/PickerPreview.test.ts +137 -0
  29. package/src/components/atoms/PresenceIndicator/PresenceIndicator.stories.ts +21 -0
  30. package/src/components/atoms/PropertyListing/PropertyListing.stories.ts +38 -0
  31. package/src/components/atoms/PropertyListing/PropertyListing.test.ts +46 -0
  32. package/src/components/atoms/SelectableOption/SelectableOption.stories.ts +131 -0
  33. package/src/components/atoms/SelectableOption/SelectableOption.test.ts +96 -0
  34. package/src/components/atoms/Skeleton/Skeleton.stories.ts +27 -0
  35. package/src/components/atoms/Skeleton/Skeleton.test.ts +28 -0
  36. package/src/components/atoms/Slider/Slider.stories.ts +151 -0
  37. package/src/components/atoms/Slider/Slider.test.ts +191 -0
  38. package/src/components/atoms/Squircle/Squircle.stories.ts +114 -0
  39. package/src/components/atoms/Squircle/Squircle.test.ts +70 -0
  40. package/src/components/atoms/Switch/Switch.stories.ts +91 -0
  41. package/src/components/atoms/Switch/Switch.test.ts +183 -0
  42. package/src/components/atoms/Tooltip/Tooltip.stories.ts +158 -0
  43. package/src/components/atoms/Tooltip/Tooltip.test.ts +134 -0
  44. package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.stories.ts +124 -0
  45. package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.test.ts +464 -0
  46. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.stories.ts +266 -0
  47. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.test.ts +421 -0
  48. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.stories.ts +365 -0
  49. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.test.ts +484 -0
  50. package/src/components/editor/composables/useRadioGrid.test.ts +261 -0
  51. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.stories.ts +73 -0
  52. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.test.ts +189 -0
  53. package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.stories.ts +309 -0
  54. package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.test.ts +359 -0
  55. package/src/components/editor/molecules/EditorAnchor/EditorAnchor.stories.ts +138 -0
  56. package/src/components/editor/molecules/EditorAnchor/EditorAnchor.test.ts +344 -0
  57. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.stories.ts +87 -0
  58. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.test.ts +262 -0
  59. package/src/components/editor/molecules/EditorBlur/EditorBlur.stories.ts +115 -0
  60. package/src/components/editor/molecules/EditorBlur/EditorBlur.test.ts +243 -0
  61. package/src/components/editor/molecules/EditorBorder/EditorBorder.stories.ts +187 -0
  62. package/src/components/editor/molecules/EditorBorder/EditorBorder.test.ts +456 -0
  63. package/src/components/editor/molecules/EditorColor/EditorColor.stories.ts +563 -0
  64. package/src/components/editor/molecules/EditorColor/EditorColor.test.ts +689 -0
  65. package/src/components/editor/molecules/EditorColor/EditorColorGradient.test.ts +793 -0
  66. package/src/components/editor/molecules/EditorColor/gradientClass.test.ts +303 -0
  67. package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.stories.ts +72 -0
  68. package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.test.ts +124 -0
  69. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.stories.ts +46 -0
  70. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.test.ts +97 -0
  71. package/src/components/editor/molecules/EditorFontSize/EditorFontSize.stories.ts +248 -0
  72. package/src/components/editor/molecules/EditorFontSize/EditorFontSize.test.ts +288 -0
  73. package/src/components/editor/molecules/EditorGap/EditorGap.stories.ts +129 -0
  74. package/src/components/editor/molecules/EditorGap/EditorGap.test.ts +253 -0
  75. package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.stories.ts +68 -0
  76. package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.test.ts +150 -0
  77. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.stories.ts +71 -0
  78. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.test.ts +161 -0
  79. package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.stories.ts +204 -0
  80. package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.test.ts +238 -0
  81. package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.stories.ts +257 -0
  82. package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.test.ts +286 -0
  83. package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.stories.ts +135 -0
  84. package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.test.ts +230 -0
  85. package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.stories.ts +154 -0
  86. package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.test.ts +112 -0
  87. package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.stories.ts +206 -0
  88. package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.test.ts +332 -0
  89. package/src/components/editor/molecules/EditorPadding/EditorPadding.stories.ts +374 -0
  90. package/src/components/editor/molecules/EditorPadding/EditorPadding.test.ts +712 -0
  91. package/src/components/editor/molecules/EditorRadius/EditorRadius.stories.ts +64 -0
  92. package/src/components/editor/molecules/EditorRadius/EditorRadius.test.ts +137 -0
  93. package/src/components/editor/molecules/EditorShadow/EditorShadow.stories.ts +103 -0
  94. package/src/components/editor/molecules/EditorShadow/EditorShadow.test.ts +168 -0
  95. package/src/components/editor/molecules/EditorSize/EditorSize.stories.ts +84 -0
  96. package/src/components/editor/molecules/EditorSize/EditorSize.test.ts +253 -0
  97. package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.stories.ts +49 -0
  98. package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.test.ts +43 -0
  99. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.stories.ts +69 -0
  100. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.test.ts +155 -0
  101. package/src/components/molecules/Announcement/Announcement.stories.ts +224 -0
  102. package/src/components/molecules/Announcement/Announcement.test.ts +161 -0
  103. package/src/components/molecules/ContactInfoCard/ContactInfoCard.stories.ts +99 -0
  104. package/src/components/molecules/ContactInfoCard/ContactInfoCard.test.ts +70 -0
  105. package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.stories.ts +148 -0
  106. package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.test.ts +271 -0
  107. package/src/components/molecules/DoughnutChart/DoughnutChart.stories.ts +45 -0
  108. package/src/components/molecules/DoughnutChart/DoughnutChart.test.ts +205 -0
  109. package/src/components/molecules/InfoBlock/InfoBlock.stories.ts +84 -0
  110. package/src/components/molecules/InfoBlock/InfoBlock.test.ts +101 -0
  111. package/src/components/molecules/InputField/InputField.stories.ts +117 -0
  112. package/src/components/molecules/InputField/InputField.test.ts +138 -0
  113. package/src/components/molecules/InputSelect/InputSelect.stories.ts +116 -0
  114. package/src/components/molecules/InputSelect/InputSelect.test.ts +127 -0
  115. package/src/components/molecules/ModalFooter/ModalFooter.stories.ts +204 -0
  116. package/src/components/molecules/ModalFooter/ModalFooter.test.ts +276 -0
  117. package/src/components/molecules/PageCard/PageCard.stories.ts +100 -0
  118. package/src/components/molecules/PageCard/PageCard.test.ts +91 -0
  119. package/src/components/molecules/PageContainer/PageContainer.stories.ts +90 -0
  120. package/src/components/molecules/PageContainer/PageContainer.test.ts +50 -0
  121. package/src/components/molecules/PageMockup/PageMockup.stories.ts +38 -0
  122. package/src/components/molecules/PageMockup/PageMockup.test.ts +20 -0
  123. package/src/components/molecules/PageTypeTile/PageTypeTile.stories.ts +63 -0
  124. package/src/components/molecules/PageTypeTile/PageTypeTile.test.ts +41 -0
  125. package/src/components/molecules/Pagination/Pagination.stories.ts +72 -0
  126. package/src/components/molecules/Pagination/Pagination.test.ts +130 -0
  127. package/src/components/molecules/PickerGroup/PickerGroup.stories.ts +90 -0
  128. package/src/components/molecules/PickerGroup/PickerGroup.test.ts +145 -0
  129. package/src/components/molecules/PickerList/PickerList.stories.ts +436 -0
  130. package/src/components/molecules/PickerList/PickerList.test.ts +870 -0
  131. package/src/components/molecules/PlaceholderInput/PlaceholderInput.stories.ts +48 -0
  132. package/src/components/molecules/PlaceholderInput/PlaceholderInput.test.ts +107 -0
  133. package/src/components/molecules/PropertyCard/PropertyCard.stories.ts +46 -0
  134. package/src/components/molecules/PropertyCard/PropertyCard.test.ts +59 -0
  135. package/src/components/molecules/Select/Select.stories.ts +264 -0
  136. package/src/components/molecules/Select/Select.test.ts +315 -0
  137. package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.stories.ts +84 -0
  138. package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.test.ts +103 -0
  139. package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.stories.ts +11 -0
  140. package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.test.ts +38 -0
  141. package/src/components/molecules/SocialIcons/SocialIcons.stories.ts +28 -0
  142. package/src/components/molecules/SocialIcons/SocialIcons.test.ts +58 -0
  143. package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.stories.ts +11 -0
  144. package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.test.ts +116 -0
  145. package/src/components/molecules/SocialMediaType/SocialMediaType.stories.ts +37 -0
  146. package/src/components/molecules/SocialMediaType/SocialMediaType.test.ts +126 -0
  147. package/src/components/molecules/Source/Source.stories.ts +35 -0
  148. package/src/components/molecules/Source/Source.test.ts +45 -0
  149. package/src/components/molecules/TabbedContent/TabbedContent.stories.ts +96 -0
  150. package/src/components/molecules/TabbedContent/TabbedContent.test.ts +109 -0
  151. package/src/components/molecules/Tabs/Tabs.stories.ts +230 -0
  152. package/src/components/molecules/Tabs/Tabs.test.ts +280 -0
  153. package/src/components/molecules/TemplateCard/TemplateCard.stories.ts +327 -0
  154. package/src/components/molecules/TemplateCard/TemplateCard.test.ts +339 -0
  155. package/src/components/molecules/Textarea/Textarea.stories.ts +216 -0
  156. package/src/components/molecules/Textarea/Textarea.test.ts +191 -0
  157. package/src/components/molecules/TimelineEvent/TimelineEvent.stories.ts +319 -0
  158. package/src/components/molecules/TimelineEvent/TimelineEvent.test.ts +224 -0
  159. package/src/components/molecules/TimelineLanes/TimelineLanes.stories.ts +29 -0
  160. package/src/components/molecules/TimelineLanes/TimelineLanes.test.ts +30 -0
  161. package/src/components/molecules/Toast/Toast.stories.ts +110 -0
  162. package/src/components/molecules/Toast/Toast.test.ts +103 -0
  163. package/src/components/molecules/ToggleGroup/ToggleGroup.stories.ts +297 -0
  164. package/src/components/molecules/ToggleGroup/ToggleGroup.test.ts +578 -0
  165. package/src/components/molecules/Tree/Tree.stories.ts +884 -0
  166. package/src/components/molecules/Tree/Tree.test.ts +1247 -0
  167. package/src/components/molecules/Tree/tree-drop.test.ts +223 -0
  168. package/src/components/organisms/Calendar/Calendar.stories.ts +108 -0
  169. package/src/components/organisms/Calendar/Calendar.test.ts +279 -0
  170. package/src/components/organisms/Calendar/_CalendarDayView.test.ts +104 -0
  171. package/src/components/organisms/Calendar/_CalendarHeader.test.ts +86 -0
  172. package/src/components/organisms/Calendar/_CalendarMonthView.test.ts +52 -0
  173. package/src/components/organisms/Calendar/_CalendarTimePicker.test.ts +89 -0
  174. package/src/components/organisms/Calendar/_CalendarYearView.test.ts +56 -0
  175. package/src/components/organisms/ColorPicker/ColorPicker.stories.ts +229 -0
  176. package/src/components/organisms/ColorPicker/ColorPicker.test.ts +638 -0
  177. package/src/components/organisms/ColorPicker/gradient.test.ts +67 -0
  178. package/src/components/organisms/ColorPicker/gradientGeometry.test.ts +206 -0
  179. package/src/components/organisms/Combobox/Combobox.stories.ts +234 -0
  180. package/src/components/organisms/Combobox/Combobox.test.ts +512 -0
  181. package/src/components/organisms/Combobox/Combobox.vue +1 -1
  182. package/src/components/organisms/ComboboxPanel/ComboboxPanel.stories.ts +158 -0
  183. package/src/components/organisms/ComboboxPanel/ComboboxPanel.test.ts +524 -0
  184. package/src/components/organisms/ComboboxPanel/ComboboxPanel.vue +1 -1
  185. package/src/components/organisms/CountrySelect/CountrySelect.stories.ts +139 -0
  186. package/src/components/organisms/CountrySelect/CountrySelect.test.ts +149 -0
  187. package/src/components/organisms/DatePicker/DatePicker.stories.ts +163 -0
  188. package/src/components/organisms/DatePicker/DatePicker.test.ts +214 -0
  189. package/src/components/organisms/DropdownButton/DropdownButton.stories.ts +123 -0
  190. package/src/components/organisms/DropdownButton/DropdownButton.test.ts +379 -0
  191. package/src/components/organisms/FontPicker/FontPicker.stories.ts +476 -0
  192. package/src/components/organisms/FontPicker/FontPicker.test.ts +873 -0
  193. package/src/components/organisms/Header/Header.stories.ts +75 -0
  194. package/src/components/organisms/Header/Header.test.ts +233 -0
  195. package/src/components/organisms/IconPicker/IconPicker.stories.ts +734 -0
  196. package/src/components/organisms/IconPicker/IconPicker.test.ts +669 -0
  197. package/src/components/organisms/IconPicker/_IconPickerGrid.test.ts +184 -0
  198. package/src/components/organisms/IconPicker/_IconPickerRail.test.ts +102 -0
  199. package/src/components/organisms/IconPicker/useIconPickerNavigation.test.ts +215 -0
  200. package/src/components/organisms/Modal/Modal.stories.ts +350 -0
  201. package/src/components/organisms/Modal/Modal.test.ts +275 -0
  202. package/src/components/organisms/PickerPanel/PickerPanel.stories.ts +452 -0
  203. package/src/components/organisms/PickerPanel/PickerPanel.test.ts +358 -0
  204. package/src/components/organisms/ProgressBar/ProgressBar.stories.ts +100 -0
  205. package/src/components/organisms/ProgressBar/ProgressBar.test.ts +417 -0
  206. package/src/components/organisms/ReferenceCard/ReferenceCard.stories.ts +191 -0
  207. package/src/components/organisms/ReferenceCard/ReferenceCard.test.ts +165 -0
  208. package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.stories.ts +65 -0
  209. package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.test.ts +466 -0
  210. package/src/components/organisms/StepperHeader/StepperHeader.stories.ts +91 -0
  211. package/src/components/organisms/StepperHeader/StepperHeader.test.ts +279 -0
  212. package/src/components/organisms/Table/Table.stories.ts +332 -0
  213. package/src/components/organisms/Table/Table.test.ts +320 -0
  214. package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.stories.ts +198 -0
  215. package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.test.ts +283 -0
  216. package/src/components/organisms/TimelineTrack/TimelineTrack.stories.ts +120 -0
  217. package/src/components/organisms/TimelineTrack/TimelineTrack.test.ts +91 -0
  218. package/src/components/organisms/Toaster/Toaster.stories.ts +51 -0
  219. package/src/components/organisms/Toaster/Toaster.test.ts +75 -0
  220. package/src/components/organisms/VideoControls/VideoControls.stories.ts +792 -0
  221. package/src/components/organisms/VideoControls/VideoControls.test.ts +1056 -0
  222. package/src/components/organisms/VideoPlayer/VideoPlayer.stories.ts +327 -0
  223. package/src/components/organisms/VideoPlayer/VideoPlayer.test.ts +439 -0
@@ -0,0 +1,149 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { mount, flushPromises } from '@vue/test-utils'
3
+ import { ref, nextTick } from 'vue'
4
+ import NexxtCountrySelect from './CountrySelect.vue'
5
+
6
+ vi.mock('@floating-ui/vue', async (importOriginal) => {
7
+ const actual = await importOriginal<typeof import('@floating-ui/vue')>()
8
+ return {
9
+ ...actual,
10
+ useFloating: () => ({
11
+ floatingStyles: ref({}),
12
+ placement: ref('bottom-start'),
13
+ isPositioned: ref(true),
14
+ }),
15
+ autoUpdate: () => () => {},
16
+ }
17
+ })
18
+
19
+ const open = async (wrapper: ReturnType<typeof mount>) => {
20
+ await wrapper.find('[role="combobox"]').trigger('click')
21
+ await flushPromises()
22
+ await nextTick()
23
+ }
24
+
25
+ describe('NexxtCountrySelect', () => {
26
+ // The width itself is only measurable in a browser (see the Narrow story);
27
+ // jsdom can check the structure the ellipsis depends on.
28
+ it('renders the country name in an element of its own, so it can be truncated', () => {
29
+ const wrapper = mount(NexxtCountrySelect, { props: { modelValue: 'Nederland' } })
30
+ const text = wrapper.find('[role="combobox"] .truncate')
31
+ expect(text.text()).toBe('Nederland')
32
+ expect(text.element.children).toHaveLength(0)
33
+ })
34
+
35
+ it('renders placeholder when no value is set', () => {
36
+ const wrapper = mount(NexxtCountrySelect)
37
+ expect(wrapper.text()).toContain('Selecteer een land...')
38
+ })
39
+
40
+ it('renders label', () => {
41
+ const wrapper = mount(NexxtCountrySelect, { props: { label: 'Land' } })
42
+ expect(wrapper.text()).toContain('Land')
43
+ })
44
+
45
+ it('renders required asterisk when required is true', () => {
46
+ const wrapper = mount(NexxtCountrySelect, { props: { label: 'Land', required: true } })
47
+ const asterisk = wrapper.find('.text-brick-500')
48
+ expect(asterisk.exists()).toBe(true)
49
+ expect(asterisk.text()).toBe('*')
50
+ })
51
+
52
+ it('shows error ring and error message', () => {
53
+ const wrapper = mount(NexxtCountrySelect, {
54
+ props: { error: true, errorMessage: 'Selecteer een land' },
55
+ })
56
+ expect(wrapper.find('.ring-brick-400').exists()).toBe(true)
57
+ expect(wrapper.text()).toContain('Selecteer een land')
58
+ })
59
+
60
+ it('opens dropdown and shows country options', async () => {
61
+ const wrapper = mount(NexxtCountrySelect)
62
+ await open(wrapper)
63
+ const options = wrapper.findAll('[role="option"]')
64
+ expect(options.length).toBeGreaterThan(0)
65
+ })
66
+
67
+ it('displays the selected country name in the trigger', () => {
68
+ const wrapper = mount(NexxtCountrySelect, { props: { modelValue: 'Nederland' } })
69
+ expect(wrapper.find('[role="combobox"]').text()).toContain('Nederland')
70
+ })
71
+
72
+ it('emits update:modelValue with the country name on selection', async () => {
73
+ const wrapper = mount(NexxtCountrySelect)
74
+ await open(wrapper)
75
+ await wrapper.find('[role="option"]').trigger('click')
76
+ const emitted = wrapper.emitted('update:modelValue')
77
+ expect(emitted).toBeTruthy()
78
+ expect(typeof emitted?.[0]?.[0]).toBe('string')
79
+ })
80
+
81
+ it('filters options based on search input', async () => {
82
+ const wrapper = mount(NexxtCountrySelect)
83
+ await open(wrapper)
84
+ const input = wrapper.find('input')
85
+ await input.setValue('neder')
86
+ await nextTick()
87
+ const options = wrapper.findAll('[role="option"]')
88
+ expect(options).toHaveLength(1)
89
+ expect(options[0].text()).toContain('Nederland')
90
+ })
91
+
92
+ it('shows empty state when search has no matches', async () => {
93
+ const wrapper = mount(NexxtCountrySelect)
94
+ await open(wrapper)
95
+ await wrapper.find('input').setValue('xyznonexistent')
96
+ await nextTick()
97
+ expect(wrapper.text()).toContain('Geen landen gevonden')
98
+ expect(wrapper.findAll('[role="option"]')).toHaveLength(0)
99
+ })
100
+
101
+ it('places pinned countries at the top of the list', async () => {
102
+ const wrapper = mount(NexxtCountrySelect, {
103
+ props: { pinnedCountries: ['de', 'nl'] },
104
+ })
105
+ await open(wrapper)
106
+ const options = wrapper.findAll('[role="option"]')
107
+ expect(options[0].text()).toContain('Duitsland')
108
+ expect(options[1].text()).toContain('Nederland')
109
+ })
110
+
111
+ it('sorts remaining countries alphabetically after pinned ones', async () => {
112
+ const wrapper = mount(NexxtCountrySelect, {
113
+ props: { pinnedCountries: ['nl'] },
114
+ })
115
+ await open(wrapper)
116
+ const options = wrapper.findAll('[role="option"]')
117
+ // First option is pinned (Nederland), rest should be alphabetical
118
+ const unpinnedLabels = options.slice(1).map((o) => o.text().trim())
119
+ const sorted = [...unpinnedLabels].sort((a, b) => a.localeCompare(b, 'nl'))
120
+ expect(unpinnedLabels).toEqual(sorted)
121
+ })
122
+
123
+ it('marks the selected option with aria-selected', async () => {
124
+ const wrapper = mount(NexxtCountrySelect, { props: { modelValue: 'België' } })
125
+ await open(wrapper)
126
+ const selected = wrapper.find('[aria-selected="true"]')
127
+ expect(selected.exists()).toBe(true)
128
+ expect(selected.text()).toContain('België')
129
+ })
130
+
131
+ it('closes the dropdown after selecting a country', async () => {
132
+ const wrapper = mount(NexxtCountrySelect)
133
+ await open(wrapper)
134
+ expect(wrapper.find('[role="listbox"]').exists()).toBe(true)
135
+ await wrapper.find('[role="option"]').trigger('click')
136
+ await nextTick()
137
+ expect(wrapper.find('[role="listbox"]').exists()).toBe(false)
138
+ })
139
+
140
+ it('clears the search after selecting a country', async () => {
141
+ const wrapper = mount(NexxtCountrySelect)
142
+ await open(wrapper)
143
+ const input = wrapper.find('input')
144
+ await input.setValue('neder')
145
+ await wrapper.find('[role="option"]').trigger('click')
146
+ await open(wrapper)
147
+ expect(wrapper.find('input').element.value).toBe('')
148
+ })
149
+ })
@@ -0,0 +1,163 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { ref } from 'vue'
3
+ import DatePicker from './DatePicker.vue'
4
+
5
+ export default {
6
+ title: 'Components/Organisms/DatePicker',
7
+ component: DatePicker,
8
+ decorators: [
9
+ () => ({ template: '<div style="min-height: 380px; padding: 16px;"><story /></div>' }),
10
+ ],
11
+ argTypes: {
12
+ anchor: {
13
+ control: 'select',
14
+ options: ['bottom-right', 'bottom-left', 'top-right', 'top-left'],
15
+ },
16
+ monthYearPicker: { control: 'boolean' },
17
+ markedDates: { control: 'object' },
18
+ disabledDates: { control: 'object' },
19
+ minDate: { control: 'date' },
20
+ maxDate: { control: 'date' },
21
+ locale: { control: false },
22
+ },
23
+ } satisfies Meta<typeof DatePicker>
24
+
25
+ type Story = StoryObj<typeof DatePicker>
26
+
27
+ export const Default: Story = {
28
+ args: {
29
+ placeholder: 'Selecteer datum',
30
+ },
31
+ }
32
+
33
+ export const AnchorBottomRight: Story = {
34
+ render: (args) => ({
35
+ components: { DatePicker },
36
+ setup() {
37
+ return { args }
38
+ },
39
+ template: `
40
+ <div style="min-height: 380px; display: flex; justify-content: flex-end; align-items: flex-start; padding: 16px;">
41
+ <DatePicker v-bind="args" />
42
+ </div>
43
+ `,
44
+ }),
45
+ args: {
46
+ placeholder: 'Selecteer datum',
47
+ anchor: 'bottom-right',
48
+ },
49
+ }
50
+
51
+ export const AnchorTopLeft: Story = {
52
+ render: (args) => ({
53
+ components: { DatePicker },
54
+ setup() {
55
+ return { args }
56
+ },
57
+ template: `
58
+ <div style="min-height: 380px; display: flex; align-items: flex-end; padding: 16px;">
59
+ <DatePicker v-bind="args" />
60
+ </div>
61
+ `,
62
+ }),
63
+ args: {
64
+ placeholder: 'Selecteer datum',
65
+ anchor: 'top-left',
66
+ },
67
+ }
68
+
69
+ export const AnchorTopRight: Story = {
70
+ render: (args) => ({
71
+ components: { DatePicker },
72
+ setup() {
73
+ return { args }
74
+ },
75
+ template: `
76
+ <div style="min-height: 380px; display: flex; justify-content: flex-end; align-items: flex-end; padding: 16px;">
77
+ <DatePicker v-bind="args" />
78
+ </div>
79
+ `,
80
+ }),
81
+ args: {
82
+ placeholder: 'Selecteer datum',
83
+ anchor: 'top-right',
84
+ },
85
+ }
86
+
87
+ export const WithoutAutoClose: Story = {
88
+ args: {
89
+ placeholder: 'Selecteer datum',
90
+ autoClose: false,
91
+ },
92
+ }
93
+
94
+ export const DateTime: Story = {
95
+ render: (args) => ({
96
+ components: { DatePicker },
97
+ setup() {
98
+ const selected = ref<Date | null>(new Date())
99
+ return { args, selected }
100
+ },
101
+ template: `
102
+ <div style="min-height: 480px; padding: 16px;">
103
+ <p style="font-size: 13px; color: #555; margin-bottom: 12px;">Geselecteerd: {{ selected ? selected.toLocaleString('nl-NL', { dateStyle: 'short', timeStyle: 'short' }) : 'Geen' }}</p>
104
+ <DatePicker v-bind="args" v-model="selected" datetime />
105
+ </div>
106
+ `,
107
+ }),
108
+ args: {
109
+ placeholder: 'Selecteer datum & tijd',
110
+ datetime: true,
111
+ },
112
+ }
113
+
114
+ export const CustomTrigger: Story = {
115
+ render: (args) => ({
116
+ components: { DatePicker },
117
+ setup() {
118
+ const selected = ref(args.modelValue)
119
+ return { args, selected }
120
+ },
121
+ template: `
122
+ <div style="min-height: 380px; padding: 16px;">
123
+ <p style="font-size: 13px; color: #555; margin-bottom: 12px;">Selected: {{ selected ? selected.toLocaleDateString() : 'None' }}</p>
124
+ <DatePicker v-bind="args" v-model="selected">
125
+ <template #trigger="{ toggle, isOpen }">
126
+ <button
127
+ type="button"
128
+ @click="toggle"
129
+ class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors shadow-sm font-medium"
130
+ >
131
+ {{ isOpen ? 'Sluiten' : 'Kies een datum uit de lijst' }}
132
+ </button>
133
+ </template>
134
+ </DatePicker>
135
+ </div>
136
+ `,
137
+ }),
138
+ args: {
139
+ modelValue: null,
140
+ },
141
+ }
142
+
143
+ export const Playground: Story = {
144
+ render: (args) => ({
145
+ components: { DatePicker },
146
+ setup() {
147
+ const selected = ref<Date | null>(null)
148
+ return { args, selected }
149
+ },
150
+ template: `
151
+ <div style="min-height: 380px; padding: 16px;">
152
+ <DatePicker v-bind="args" v-model="selected" />
153
+ <p v-if="selected" style="margin-top: 12px; font-size: 13px; color: #555;">
154
+ Geselecteerd: {{ selected.toLocaleDateString('nl-NL') }}
155
+ </p>
156
+ </div>
157
+ `,
158
+ }),
159
+ args: {
160
+ placeholder: 'Selecteer datum',
161
+ monthYearPicker: true,
162
+ },
163
+ }
@@ -0,0 +1,214 @@
1
+ import { describe, it, expect, afterEach, vi } from 'vitest'
2
+ import { mount, type VueWrapper } from '@vue/test-utils'
3
+ import { ref, nextTick } from 'vue'
4
+ import DatePicker from './DatePicker.vue'
5
+
6
+ vi.mock('@floating-ui/vue', async (importOriginal) => {
7
+ const actual = await importOriginal<typeof import('@floating-ui/vue')>()
8
+ return {
9
+ ...actual,
10
+ useFloating: () => ({
11
+ floatingStyles: ref({}),
12
+ placement: ref('bottom-start'),
13
+ isPositioned: ref(true),
14
+ }),
15
+ autoUpdate: () => () => {},
16
+ }
17
+ })
18
+
19
+ const YEAR = 2026
20
+ const MONTH = 2 // March (0-indexed)
21
+ const TODAY = new Date(YEAR, MONTH, 20)
22
+
23
+ const teleportStub = { template: '<div><slot /></div>' }
24
+
25
+ const mountDatePicker = (props = {}) =>
26
+ mount(DatePicker, {
27
+ props,
28
+ attachTo: document.body,
29
+ global: { stubs: { Teleport: teleportStub } },
30
+ })
31
+
32
+ const openPicker = async (wrapper: VueWrapper) => {
33
+ await wrapper.find('button').trigger('click')
34
+ await nextTick()
35
+ }
36
+
37
+ describe('DatePicker', () => {
38
+ let wrapper: VueWrapper
39
+
40
+ afterEach(() => {
41
+ wrapper?.unmount()
42
+ })
43
+
44
+ // ── Trigger button ───────────────────────────────────────────────────────
45
+
46
+ it('renders trigger button with placeholder', () => {
47
+ wrapper = mountDatePicker({ placeholder: 'Kies een datum' })
48
+ expect(wrapper.text()).toContain('Kies een datum')
49
+ })
50
+
51
+ it('uses default placeholder when none provided', () => {
52
+ wrapper = mountDatePicker()
53
+ expect(wrapper.text()).toContain('Selecteer datum')
54
+ })
55
+
56
+ // ── Open / close ─────────────────────────────────────────────────────────
57
+
58
+ it('calendar is closed by default', () => {
59
+ wrapper = mountDatePicker()
60
+ expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(false)
61
+ })
62
+
63
+ it('opens when trigger button is clicked', async () => {
64
+ wrapper = mountDatePicker()
65
+ await openPicker(wrapper)
66
+ expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(true)
67
+ })
68
+
69
+ it('closes when trigger button is clicked again', async () => {
70
+ wrapper = mountDatePicker()
71
+ await openPicker(wrapper)
72
+ await wrapper.find('button').trigger('click')
73
+ await nextTick()
74
+ expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(false)
75
+ })
76
+
77
+ it('closes when clicking outside', async () => {
78
+ wrapper = mountDatePicker()
79
+ await openPicker(wrapper)
80
+ document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
81
+ await nextTick()
82
+ expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(false)
83
+ })
84
+
85
+ it('closes on Escape key', async () => {
86
+ wrapper = mountDatePicker()
87
+ await openPicker(wrapper)
88
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
89
+ await nextTick()
90
+ expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(false)
91
+ })
92
+
93
+ // ── Day selection + autoClose ─────────────────────────────────────────────
94
+
95
+ it('closes after selecting a day (autoClose default)', async () => {
96
+ wrapper = mountDatePicker({ modelValue: TODAY })
97
+ await openPicker(wrapper)
98
+
99
+ const dayButtons = wrapper
100
+ .findAll('button[type="button"]')
101
+ .filter((b) => /^\d{1,2}$/.test(b.text().trim()))
102
+ await dayButtons[10]!.trigger('click')
103
+ await nextTick()
104
+
105
+ expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(false)
106
+ })
107
+
108
+ it('stays open after selecting a day if autoClose is false', async () => {
109
+ wrapper = mountDatePicker({ modelValue: TODAY, autoClose: false })
110
+ await openPicker(wrapper)
111
+
112
+ const dayButtons = wrapper
113
+ .findAll('button[type="button"]')
114
+ .filter((b) => /^\d{1,2}$/.test(b.text().trim()))
115
+ await dayButtons[10]!.trigger('click')
116
+ await nextTick()
117
+
118
+ expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(true)
119
+ })
120
+
121
+ it('emits update:modelValue with the clicked date', async () => {
122
+ wrapper = mountDatePicker({ modelValue: TODAY })
123
+ await openPicker(wrapper)
124
+
125
+ const dayButtons = wrapper
126
+ .findAll('button[type="button"]')
127
+ .filter((b) => /^\d{1,2}$/.test(b.text().trim()))
128
+ await dayButtons[10]!.trigger('click')
129
+ await nextTick()
130
+
131
+ expect(wrapper.emitted('update:modelValue')).toBeTruthy()
132
+ expect(wrapper.emitted('update:modelValue')![0]![0]).toBeInstanceOf(Date)
133
+ })
134
+
135
+ // ── Resets to days view on reopen ─────────────────────────────────────────
136
+
137
+ it('resets to days view when closed and reopened', async () => {
138
+ wrapper = mountDatePicker({ modelValue: TODAY, monthYearPicker: true })
139
+ await openPicker(wrapper)
140
+
141
+ // open month picker
142
+ const header = wrapper.find('[class*="cornflower-blue-600"]')
143
+ await header.find('button').trigger('click')
144
+ await nextTick()
145
+ expect(wrapper.find('[class*="grid-cols-3"]').exists()).toBe(true)
146
+
147
+ // close and reopen (v-if remounts Calendar with fresh pickerView = 'days')
148
+ await wrapper.find('button').trigger('click')
149
+ await nextTick()
150
+ await openPicker(wrapper)
151
+ expect(wrapper.find('[class*="grid-cols-7"]').exists()).toBe(true)
152
+ expect(wrapper.find('[class*="grid-cols-3"]').exists()).toBe(false)
153
+ })
154
+
155
+ // ── Custom trigger slot ───────────────────────────────────────────────────
156
+
157
+ it('renders custom trigger through slot', () => {
158
+ wrapper = mount(DatePicker, {
159
+ slots: {
160
+ trigger: `
161
+ <template #trigger="{ toggle }">
162
+ <button class="custom-trigger" @click="toggle">Custom Button</button>
163
+ </template>
164
+ `,
165
+ },
166
+ global: { stubs: { Teleport: teleportStub } },
167
+ })
168
+
169
+ expect(wrapper.find('.custom-trigger').exists()).toBe(true)
170
+ expect(wrapper.text()).toContain('Custom Button')
171
+ })
172
+
173
+ it('toggles picker via custom trigger toggle function', async () => {
174
+ wrapper = mount(DatePicker, {
175
+ slots: {
176
+ trigger: `
177
+ <template #trigger="{ toggle }">
178
+ <button class="custom-trigger" @click="toggle">Toggle</button>
179
+ </template>
180
+ `,
181
+ },
182
+ global: { stubs: { Teleport: teleportStub } },
183
+ })
184
+
185
+ expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(false)
186
+
187
+ await wrapper.find('.custom-trigger').trigger('click')
188
+ await nextTick()
189
+ expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(true)
190
+
191
+ await wrapper.find('.custom-trigger').trigger('click')
192
+ await nextTick()
193
+ expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(false)
194
+ })
195
+
196
+ it('provides isOpen state to custom trigger', async () => {
197
+ wrapper = mount(DatePicker, {
198
+ slots: {
199
+ trigger: `
200
+ <template #trigger="{ isOpen, toggle }">
201
+ <button class="custom-trigger" @click="toggle">{{ isOpen ? 'OPEN' : 'CLOSED' }}</button>
202
+ </template>
203
+ `,
204
+ },
205
+ global: { stubs: { Teleport: teleportStub } },
206
+ })
207
+
208
+ expect(wrapper.find('.custom-trigger').text()).toBe('CLOSED')
209
+
210
+ await wrapper.find('.custom-trigger').trigger('click')
211
+ await nextTick()
212
+ expect(wrapper.find('.custom-trigger').text()).toBe('OPEN')
213
+ })
214
+ })
@@ -0,0 +1,123 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import DropdowndButton from './DropdownButton.vue'
3
+
4
+ export default {
5
+ title: 'Components/Organisms/Dropdown Button',
6
+ component: DropdowndButton,
7
+ decorators: [
8
+ () => ({
9
+ template:
10
+ '<div style="min-height: 250px; padding: 16px; display: flex; align-items: center; justify-content: center;"><story /></div>',
11
+ }),
12
+ ],
13
+ } satisfies Meta<typeof DropdowndButton>
14
+
15
+ type Story = StoryObj<typeof DropdowndButton>
16
+
17
+ export const Default: Story = {
18
+ args: {
19
+ label: 'Actions',
20
+ action: () => alert('Primary action clicked'),
21
+ options: [
22
+ { label: 'Edit', action: () => alert('Edit clicked'), icon: 'pencil', selected: true },
23
+ { label: 'Delete', action: () => alert('Delete clicked'), icon: 'trash-can' },
24
+ ],
25
+ },
26
+ }
27
+
28
+ export const Secondary: Story = {
29
+ args: {
30
+ label: 'More info',
31
+ variant: 'secondary',
32
+ action: () => alert('Secondary main action'),
33
+ options: [
34
+ { label: 'View Profile', action: '/profile', icon: 'user' },
35
+ { label: 'Settings', action: '/settings', icon: 'gear' },
36
+ ],
37
+ },
38
+ }
39
+
40
+ export const LeftPlacement: Story = {
41
+ args: {
42
+ label: 'Menu',
43
+ placement: 'left',
44
+ action: () => console.log('Main action'),
45
+ options: [
46
+ { label: 'Option 1', action: () => console.log('1') },
47
+ { label: 'Option 2', action: () => console.log('2') },
48
+ ],
49
+ },
50
+ }
51
+
52
+ export const WithIcon: Story = {
53
+ args: {
54
+ label: 'Download',
55
+ icon: 'download',
56
+ action: '/download/all',
57
+ options: [
58
+ { label: 'As PDF', action: '/download/pdf', icon: 'file-pdf' },
59
+ { label: 'As CSV', action: '/download/csv', icon: 'file-csv' },
60
+ ],
61
+ },
62
+ }
63
+
64
+ export const IconOnly: Story = {
65
+ args: {
66
+ iconOnly: true,
67
+ triggerIcon: 'ellipsis-vertical',
68
+ variant: 'secondary',
69
+ options: [
70
+ { label: 'Edit', action: () => alert('Edit clicked'), icon: 'pencil' },
71
+ { label: 'Delete', action: () => alert('Delete clicked'), icon: 'trash-can' },
72
+ ],
73
+ },
74
+ }
75
+
76
+ export const Grouped: Story = {
77
+ decorators: [
78
+ () => ({
79
+ template:
80
+ '<div style="min-height: 450px; padding: 16px; display: flex; align-items: start; justify-content: center;"><story /></div>',
81
+ }),
82
+ ],
83
+ args: {
84
+ label: 'Actions',
85
+ action: () => alert('Primary action clicked'),
86
+ options: [
87
+ {
88
+ title: 'File',
89
+ items: [
90
+ { label: 'Download PDF', action: () => alert('PDF'), icon: 'file-pdf' },
91
+ { label: 'Download CSV', action: () => alert('CSV'), icon: 'file-csv' },
92
+ ],
93
+ },
94
+ {
95
+ title: 'Manage',
96
+ items: [
97
+ { label: 'Edit', action: () => alert('Edit'), icon: 'pencil' },
98
+ { label: 'Delete', action: () => alert('Delete'), icon: 'trash-can' },
99
+ ],
100
+ },
101
+ {
102
+ title: 'Columns',
103
+ items: [
104
+ { label: 'Name', checkbox: true, selected: true, action: () => {} },
105
+ { label: 'Email', checkbox: true, action: () => {} },
106
+ { label: 'Status', checkbox: true, action: () => {} },
107
+ ],
108
+ },
109
+ ],
110
+ },
111
+ }
112
+
113
+ export const WithCheckboxes: Story = {
114
+ args: {
115
+ label: 'Columns',
116
+ action: () => {},
117
+ options: [
118
+ { label: 'Name', checkbox: true, selected: true, action: () => {} },
119
+ { label: 'Email', checkbox: true, action: () => {} },
120
+ { label: 'Status', checkbox: true, selected: true, action: () => {} },
121
+ ],
122
+ },
123
+ }