@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,48 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { ref } from 'vue'
3
+ import PlaceholderInput from './PlaceholderInput.vue'
4
+ import type { NexxtPlaceholderGroup } from './PlaceholderInput.vue'
5
+
6
+ const groups: NexxtPlaceholderGroup[] = [
7
+ { label: 'Contact', items: ['contact voornaam', 'contact achternaam', 'contact woonplaats'] },
8
+ { label: 'Woning', items: ['woning adres', 'woning plaats', 'woning prijs'] },
9
+ ]
10
+
11
+ export default {
12
+ title: 'Components/Molecules/Placeholder Input',
13
+ component: PlaceholderInput,
14
+ decorators: [() => ({ template: '<div class="w-100 mx-auto"><story /></div>' })],
15
+ argTypes: {
16
+ label: { control: 'text' },
17
+ placeholder: { control: 'text' },
18
+ disabled: { control: 'boolean' },
19
+ },
20
+ args: { placeholders: groups, label: 'Titel' },
21
+ } satisfies Meta<typeof PlaceholderInput>
22
+
23
+ type Story = StoryObj<typeof PlaceholderInput>
24
+
25
+ export const Default: Story = {
26
+ render: (args) => ({
27
+ components: { PlaceholderInput },
28
+ setup() {
29
+ const text = ref('Beste {{contact voornaam}},')
30
+ return { args, text }
31
+ },
32
+ // The value is shown as-is: the placeholders are stored as `{{key}}` in the text.
33
+ template: `
34
+ <div class="flex flex-col gap-4">
35
+ <PlaceholderInput v-bind="args" v-model="text" />
36
+ <p class="small-normal text-gray-700">{{ text }}</p>
37
+ </div>
38
+ `,
39
+ }),
40
+ }
41
+
42
+ export const SingleGroup: Story = {
43
+ args: { placeholders: [groups[0]] },
44
+ }
45
+
46
+ export const Disabled: Story = {
47
+ args: { disabled: true },
48
+ }
@@ -0,0 +1,107 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { mount, flushPromises } from '@vue/test-utils'
3
+ import { nextTick, ref } from 'vue'
4
+ import PlaceholderInput from './PlaceholderInput.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-end'),
13
+ isPositioned: ref(true),
14
+ }),
15
+ autoUpdate: () => () => {},
16
+ }
17
+ })
18
+
19
+ const groups = [
20
+ { label: 'Contact', items: ['contact voornaam', 'contact achternaam'] },
21
+ { label: 'Woning', items: ['woning adres'] },
22
+ ]
23
+
24
+ const openMenu = async (wrapper: ReturnType<typeof mount>) => {
25
+ await wrapper.find('button[aria-label="Placeholder invoegen"]').trigger('click')
26
+ await flushPromises()
27
+ await nextTick()
28
+ }
29
+
30
+ describe('NexxtPlaceholderInput', () => {
31
+ it('renders the label and the wand button', () => {
32
+ const wrapper = mount(PlaceholderInput, { props: { placeholders: groups, label: 'Titel' } })
33
+
34
+ expect(wrapper.find('label').text()).toBe('Titel')
35
+ expect(wrapper.find('button[aria-label="Placeholder invoegen"]').exists()).toBe(true)
36
+ })
37
+
38
+ it('lists the placeholders per group when the wand is clicked', async () => {
39
+ const wrapper = mount(PlaceholderInput, { props: { placeholders: groups } })
40
+ await openMenu(wrapper)
41
+
42
+ const options = wrapper.findAll('[role="option"]')
43
+ expect(options.map((option) => option.text())).toEqual([
44
+ 'contact voornaam',
45
+ 'contact achternaam',
46
+ 'woning adres',
47
+ ])
48
+ expect(wrapper.text()).toContain('Contact')
49
+ expect(wrapper.text()).toContain('Woning')
50
+ })
51
+
52
+ it('leaves the group heading out when there is one group', async () => {
53
+ const wrapper = mount(PlaceholderInput, { props: { placeholders: [groups[0]] } })
54
+ await openMenu(wrapper)
55
+
56
+ expect(wrapper.text()).not.toContain('Contact')
57
+ expect(wrapper.findAll('[role="option"]')).toHaveLength(2)
58
+ })
59
+
60
+ it('inserts the placeholder at the caret and closes the menu', async () => {
61
+ const wrapper = mount(PlaceholderInput, {
62
+ props: { placeholders: groups, modelValue: 'Beste , welkom' },
63
+ attachTo: document.body,
64
+ })
65
+ const input = wrapper.find('input').element as HTMLInputElement
66
+ input.focus()
67
+ input.setSelectionRange(6, 6)
68
+
69
+ await openMenu(wrapper)
70
+ await wrapper.findAll('[role="option"]')[0].trigger('click')
71
+ await flushPromises()
72
+
73
+ expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([
74
+ 'Beste {{contact voornaam}}, welkom',
75
+ ])
76
+ expect(wrapper.findAll('[role="option"]')).toHaveLength(0)
77
+
78
+ wrapper.unmount()
79
+ })
80
+
81
+ it('appends when the field was never focused', async () => {
82
+ const wrapper = mount(PlaceholderInput, {
83
+ props: { placeholders: groups, modelValue: 'Hallo ' },
84
+ })
85
+ await openMenu(wrapper)
86
+ await wrapper.findAll('[role="option"]')[2].trigger('click')
87
+
88
+ expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['Hallo {{woning adres}}'])
89
+ })
90
+
91
+ it('uses the given button text', () => {
92
+ const wrapper = mount(PlaceholderInput, {
93
+ props: { placeholders: groups, messages: { insert: 'Veld invoegen' } },
94
+ })
95
+
96
+ expect(wrapper.find('button[aria-label="Veld invoegen"]').exists()).toBe(true)
97
+ })
98
+
99
+ it('disables the wand along with the field', () => {
100
+ const wrapper = mount(PlaceholderInput, { props: { placeholders: groups, disabled: true } })
101
+
102
+ expect(
103
+ wrapper.find('button[aria-label="Placeholder invoegen"]').attributes('disabled'),
104
+ ).toBeDefined()
105
+ expect(wrapper.find('input').attributes('disabled')).toBeDefined()
106
+ })
107
+ })
@@ -0,0 +1,46 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import PropertyCard from './PropertyCard.vue'
3
+
4
+ export default {
5
+ title: 'Components/Molecules/Property card',
6
+ component: PropertyCard,
7
+ } satisfies Meta<typeof PropertyCard>
8
+
9
+ type Story = StoryObj<typeof PropertyCard>
10
+
11
+ export const Default: Story = {
12
+ args: {
13
+ image: 'https://placehold.co/333x160',
14
+ imageAlt: 'Hollandscheveld - Van Echtenstraat 62b',
15
+ price: 520000,
16
+ city: 'Hollandscheveld',
17
+ text: 'Van Echtenstraat 62b',
18
+ },
19
+ }
20
+
21
+ export const WithoutImage: Story = {
22
+ args: {
23
+ price: 520000,
24
+ city: 'Hollandscheveld',
25
+ text: 'Van Echtenstraat 62b',
26
+ },
27
+ }
28
+
29
+ export const LongText: Story = {
30
+ args: {
31
+ image: 'https://placehold.co/333x160',
32
+ imageAlt: 'Hollandscheveld - Van Echtenstraat 62b',
33
+ price: 520000,
34
+ city: 'Hollandscheveld',
35
+ text: 'Van Echtenstraat 62b, 7913 VB, Hollandscheveld, Nederland. Dit is een extra lang adres om te testen of de tekst netjes afbreekt en de kaart langer wordt.',
36
+ },
37
+ }
38
+
39
+ export const OverflowingCity: Story = {
40
+ args: {
41
+ image: 'https://placehold.co/333x160',
42
+ imageAlt: 'Hollandscheveld - Van Echtenstraat 62b',
43
+ price: 1250000,
44
+ city: 'Nieuw-Amsterdam en Veenoord',
45
+ },
46
+ }
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import PropertyCard from './PropertyCard.vue'
4
+
5
+ describe('PropertyCard', () => {
6
+ it('renders with image and property details', () => {
7
+ const wrapper = mount(PropertyCard, {
8
+ props: {
9
+ image: 'https://placehold.co/333x160',
10
+ price: 520000,
11
+ city: 'Hollandscheveld',
12
+ text: 'Van Echtenstraat 62b',
13
+ },
14
+ })
15
+
16
+ expect(wrapper.find('img').exists()).toBe(true)
17
+ expect(wrapper.text()).toContain('€')
18
+ expect(wrapper.text()).toContain('520.000')
19
+ expect(wrapper.text()).toContain('Hollandscheveld')
20
+ expect(wrapper.text()).toContain('Van Echtenstraat 62b')
21
+ })
22
+
23
+ it('renders placeholder with blurred overlay when no image provided', () => {
24
+ const wrapper = mount(PropertyCard, {
25
+ props: { city: 'Hollandscheveld' },
26
+ })
27
+
28
+ expect(wrapper.find('img').exists()).toBe(true)
29
+ expect(wrapper.find('.backdrop-blur-sm').exists()).toBe(true)
30
+ expect(wrapper.find('.backdrop-blur-sm i.fa-house').exists()).toBe(true)
31
+ })
32
+
33
+ it('does not render placeholder when image is provided', () => {
34
+ const wrapper = mount(PropertyCard, {
35
+ props: {
36
+ image: 'https://placehold.co/333x160',
37
+ city: 'Hollandscheveld',
38
+ },
39
+ })
40
+
41
+ expect(wrapper.find('.backdrop-blur-sm').exists()).toBe(false)
42
+ })
43
+
44
+ it('renders with long text', () => {
45
+ const longText =
46
+ 'Van Echtenstraat 62b, 7913 VB, Hollandscheveld, Nederland. Dit is een extra lang adres om te testen of de tekst netjes afbreekt en de kaart langer wordt.'
47
+
48
+ const wrapper = mount(PropertyCard, {
49
+ props: {
50
+ image: 'https://placehold.co/333x160',
51
+ price: 520000,
52
+ city: 'Hollandscheveld',
53
+ text: longText,
54
+ },
55
+ })
56
+
57
+ expect(wrapper.text()).toContain(longText)
58
+ })
59
+ })
@@ -0,0 +1,264 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { expect, screen, userEvent, waitFor, within } from 'storybook/test'
3
+ import NexxtSelect from './Select.vue'
4
+
5
+ export default {
6
+ title: 'Components/Molecules/Select',
7
+ component: NexxtSelect,
8
+ decorators: [
9
+ () => ({
10
+ template:
11
+ '<div style="min-height: 250px; padding: 16px; display: flex; justify-content: center;"><story /></div>',
12
+ }),
13
+ ],
14
+ } satisfies Meta<typeof NexxtSelect>
15
+
16
+ type Story = StoryObj<typeof NexxtSelect>
17
+
18
+ const stringOptions = ['Optie 1', 'Optie 2', 'Optie 3']
19
+
20
+ const objectOptions = [
21
+ { label: 'Amsterdam', value: 'ams' },
22
+ { label: 'Rotterdam', value: 'rot' },
23
+ { label: 'Utrecht', value: 'utr' },
24
+ { label: 'Den Haag', value: 'dhg' },
25
+ ]
26
+
27
+ const render = (args: Story['args']) => ({
28
+ components: { NexxtSelect },
29
+ setup() {
30
+ return { args }
31
+ },
32
+ template: `
33
+ <NexxtSelect
34
+ v-bind="args"
35
+ :modelValue="args.modelValue"
36
+ @update:modelValue="args.modelValue = $event"
37
+ />
38
+ `,
39
+ })
40
+
41
+ export const Default: Story = {
42
+ render,
43
+ args: {
44
+ options: stringOptions,
45
+ placeholder: 'Selecteer...',
46
+ modelValue: null,
47
+ },
48
+ // Without a width of its own the field keeps its 240px floor, and the list
49
+ // opens exactly as wide as the field.
50
+ play: async ({ canvasElement }) => {
51
+ const trigger = within(canvasElement).getByRole('combobox')
52
+ await expect(trigger.offsetWidth).toBe(240)
53
+ await userEvent.click(trigger)
54
+ const listbox = await screen.findByRole('listbox')
55
+ await waitFor(() => expect(listbox.offsetWidth).toBe(trigger.offsetWidth))
56
+ await userEvent.click(trigger)
57
+ },
58
+ }
59
+
60
+ export const WithLabel: Story = {
61
+ render,
62
+ args: {
63
+ ...Default.args,
64
+ label: 'Stad',
65
+ options: objectOptions,
66
+ },
67
+ }
68
+
69
+ export const WithValue: Story = {
70
+ render,
71
+ args: {
72
+ ...Default.args,
73
+ options: objectOptions,
74
+ modelValue: 'rot',
75
+ label: 'Stad',
76
+ },
77
+ }
78
+
79
+ export const WithLeftIcon: Story = {
80
+ render,
81
+ args: {
82
+ ...Default.args,
83
+ label: 'Stad',
84
+ options: objectOptions,
85
+ leftIcon: 'location-dot',
86
+ },
87
+ }
88
+
89
+ /**
90
+ * An option can carry its own `icon`. It is rendered before the label in the
91
+ * list, and in the field once that option is selected — unless `leftIcon` is
92
+ * set, which always wins.
93
+ */
94
+ export const WithIcons: Story = {
95
+ render,
96
+ args: {
97
+ ...Default.args,
98
+ label: 'Type',
99
+ options: [
100
+ { label: 'Heading 1', value: 'h1', icon: 'h1' },
101
+ { label: 'Heading 2', value: 'h2', icon: 'h2' },
102
+ { label: 'Heading 3', value: 'h3', icon: 'h3' },
103
+ ],
104
+ modelValue: 'h1',
105
+ },
106
+ }
107
+
108
+ export const Required: Story = {
109
+ render,
110
+ args: {
111
+ ...Default.args,
112
+ label: 'Verplicht veld',
113
+ options: objectOptions,
114
+ required: true,
115
+ },
116
+ }
117
+
118
+ export const Error: Story = {
119
+ render,
120
+ args: {
121
+ ...Default.args,
122
+ label: 'Stad',
123
+ options: objectOptions,
124
+ error: true,
125
+ errorMessage: 'Dit veld is verplicht',
126
+ },
127
+ }
128
+
129
+ const manyOptions = [
130
+ { label: 'Amsterdam', value: 'ams' },
131
+ { label: 'Rotterdam', value: 'rot' },
132
+ { label: 'Utrecht', value: 'utr' },
133
+ { label: 'Den Haag', value: 'dhg' },
134
+ { label: 'Eindhoven', value: 'ein' },
135
+ { label: 'Groningen', value: 'gro' },
136
+ { label: 'Maastricht', value: 'maa' },
137
+ { label: 'Nijmegen', value: 'nij' },
138
+ ]
139
+
140
+ /**
141
+ * Keyboard navigation. There are no new props for this — the behaviour lives
142
+ * entirely in key handling, so it can only be seen by using it. Tab to the
143
+ * combobox below and try the keys listed beside it.
144
+ *
145
+ * The list is deliberately long: with three options, wrapping past the end and
146
+ * Home/End are invisible, because the first and last option are almost the same
147
+ * place. With eight, every one of these keys does something you can see.
148
+ */
149
+ export const KeyboardNavigation: Story = {
150
+ render: (args) => ({
151
+ components: { NexxtSelect },
152
+ setup() {
153
+ return { args }
154
+ },
155
+ template: `
156
+ <div style="display: flex; gap: 32px; align-items: flex-start; font-family: system-ui, sans-serif;">
157
+ <NexxtSelect
158
+ v-bind="args"
159
+ :modelValue="args.modelValue"
160
+ @update:modelValue="args.modelValue = $event"
161
+ />
162
+ <dl style="margin: 0; font-size: 13px; color: #374151; display: grid; grid-template-columns: auto 1fr; gap: 4px 16px; align-items: baseline;">
163
+ <dt style="font-weight: 600;">Tab</dt><dd style="margin: 0;">move focus to the combobox</dd>
164
+ <dt style="font-weight: 600;">Enter / Space</dt><dd style="margin: 0;">open the list, then select the active option</dd>
165
+ <dt style="font-weight: 600;">Arrow down</dt><dd style="margin: 0;">next option, wrapping from the last back to the first</dd>
166
+ <dt style="font-weight: 600;">Arrow up</dt><dd style="margin: 0;">previous option, wrapping from the first to the last</dd>
167
+ <dt style="font-weight: 600;">Home / End</dt><dd style="margin: 0;">jump to the first / last option</dd>
168
+ <dt style="font-weight: 600;">Escape</dt><dd style="margin: 0;">close the list, leaving the value unchanged</dd>
169
+ </dl>
170
+ </div>
171
+ `,
172
+ }),
173
+ args: {
174
+ options: manyOptions,
175
+ label: 'Stad',
176
+ placeholder: 'Selecteer...',
177
+ modelValue: null,
178
+ },
179
+ }
180
+
181
+ /**
182
+ * The same long list with a value already selected. Opening with the keyboard
183
+ * should start from the selected option rather than the top of the list, so this
184
+ * is where to check that Arrow down from `Maastricht` goes to `Nijmegen` and not
185
+ * back to `Amsterdam`.
186
+ */
187
+ export const KeyboardNavigationWithValue: Story = {
188
+ render,
189
+ args: {
190
+ options: manyOptions,
191
+ label: 'Stad',
192
+ placeholder: 'Selecteer...',
193
+ modelValue: 'maa',
194
+ },
195
+ }
196
+
197
+ const longOptions = [
198
+ { label: 'Een bijzonder lange optie die nooit in het veld past', value: 'long' },
199
+ { label: 'Kort', value: 'short' },
200
+ ]
201
+
202
+ /**
203
+ * Narrow fields. Without a width of its own the select is 240px wide, also
204
+ * inside a shrink-to-fit wrapper (`w-fit`, or `absolute` without a width), but
205
+ * never wider than its container: in a 200px column it is 200px, with or
206
+ * without `w-full`. A width class of your own (`w-48`) just works; no `min-w-0`
207
+ * needed. A label that does not fit is cut off with an ellipsis, and the chevron
208
+ * stays inside the ring.
209
+ */
210
+ export const Narrow: Story = {
211
+ render: (args) => ({
212
+ components: { NexxtSelect },
213
+ setup() {
214
+ return { args }
215
+ },
216
+ template: `
217
+ <div style="position: relative; display: flex; flex-direction: column; gap: 24px; padding-bottom: 64px;">
218
+ <NexxtSelect v-bind="args" class="w-60" aria-label="w-60" />
219
+ <NexxtSelect v-bind="args" class="w-48" aria-label="w-48" />
220
+ <div style="width: 200px;">
221
+ <NexxtSelect v-bind="args" class="w-full" aria-label="w-full" />
222
+ </div>
223
+ <div style="width: 200px;">
224
+ <NexxtSelect v-bind="args" aria-label="column" />
225
+ </div>
226
+ <div class="w-fit">
227
+ <NexxtSelect v-bind="args" aria-label="w-fit" />
228
+ </div>
229
+ <div style="position: absolute; bottom: 0; left: 0;">
230
+ <NexxtSelect v-bind="args" aria-label="absolute" />
231
+ </div>
232
+ </div>
233
+ `,
234
+ }),
235
+ args: {
236
+ options: longOptions,
237
+ modelValue: 'long',
238
+ },
239
+ play: async ({ canvasElement }) => {
240
+ const canvas = within(canvasElement)
241
+ const expected = {
242
+ 'w-60': 240,
243
+ 'w-48': 192,
244
+ 'w-full': 200,
245
+ column: 200,
246
+ 'w-fit': 240,
247
+ absolute: 240,
248
+ }
249
+
250
+ for (const [name, width] of Object.entries(expected)) {
251
+ const trigger = canvas.getByRole('combobox', { name })
252
+ // offsetWidth, not the rect: the test canvas is zoomed.
253
+ await expect(trigger.offsetWidth).toBe(width)
254
+
255
+ const chevron = trigger.lastElementChild as HTMLElement
256
+ const ring = trigger.getBoundingClientRect()
257
+ await expect(chevron.getBoundingClientRect().right).toBeLessThanOrEqual(ring.right)
258
+
259
+ const text = within(trigger).getByText(longOptions[0]!.label)
260
+ await expect(text.scrollWidth).toBeGreaterThan(text.clientWidth)
261
+ await expect(getComputedStyle(text).textOverflow).toBe('ellipsis')
262
+ }
263
+ },
264
+ }