@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,452 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { expect, fn, userEvent, waitFor, within } from 'storybook/test'
3
+ import { computed, defineComponent, ref } from 'vue'
4
+ import NexxtButton from '../../atoms/Button/Button.vue'
5
+ import NexxtFloatingPanel from '../../atoms/FloatingPanel/FloatingPanel.vue'
6
+ import NexxtIcon, { type NexxtIconName } from '../../atoms/Icon/Icon.vue'
7
+ import NexxtTabs from '../../molecules/Tabs/Tabs.vue'
8
+ import type {
9
+ NexxtPickerGroupData,
10
+ NexxtPickerItemData,
11
+ } from '../../molecules/PickerList/PickerList.vue'
12
+ import PickerPanel from './PickerPanel.vue'
13
+
14
+ const GROUPS: NexxtPickerGroupData[] = [
15
+ { id: 'basis', label: 'Basis', defaultOpen: true },
16
+ { id: 'media', label: 'Media', defaultOpen: true },
17
+ ]
18
+
19
+ const ITEMS: NexxtPickerItemData[] = [
20
+ {
21
+ id: 'intro',
22
+ label: 'Introductie',
23
+ icon: 'sparkles',
24
+ keywords: ['welkom', 'opening'],
25
+ description: 'Een korte opening bovenaan de pagina.',
26
+ },
27
+ {
28
+ id: 'title',
29
+ label: 'Titel',
30
+ icon: 'heading',
31
+ group: 'basis',
32
+ keywords: ['kop', 'heading'],
33
+ description: 'Een kop die een nieuw onderdeel inleidt.',
34
+ },
35
+ {
36
+ id: 'text',
37
+ label: 'Tekst',
38
+ icon: 'font',
39
+ group: 'basis',
40
+ keywords: ['alinea', 'paragraaf'],
41
+ description: 'Een of meer alinea’s lopende tekst.',
42
+ },
43
+ {
44
+ id: 'table',
45
+ label: 'Tabel',
46
+ icon: 'table',
47
+ group: 'basis',
48
+ keywords: ['rijen', 'kolommen'],
49
+ disabled: true,
50
+ description: 'Nog niet beschikbaar.',
51
+ },
52
+ {
53
+ id: 'quote',
54
+ label: 'Citaat',
55
+ icon: 'quote-left',
56
+ group: 'basis',
57
+ keywords: ['quote', 'uitspraak'],
58
+ description: 'Een uitgelichte uitspraak met bron.',
59
+ },
60
+ {
61
+ id: 'image',
62
+ label: 'Afbeelding',
63
+ icon: 'image',
64
+ group: 'media',
65
+ keywords: ['foto', 'plaatje'],
66
+ description: 'Een foto of illustratie over de volle breedte.',
67
+ },
68
+ {
69
+ id: 'video',
70
+ label: 'Video',
71
+ icon: 'video',
72
+ group: 'media',
73
+ keywords: ['film', 'youtube'],
74
+ description: 'Een ingesloten video met afspeelknop.',
75
+ },
76
+ {
77
+ id: 'gallery',
78
+ label: 'Galerij',
79
+ icon: 'images',
80
+ group: 'media',
81
+ keywords: ['album', 'fotos'],
82
+ description: 'Meerdere afbeeldingen in een raster.',
83
+ },
84
+ ]
85
+
86
+ const COMBINED_ITEMS: NexxtPickerItemData[] = [
87
+ {
88
+ id: 'hero',
89
+ label: 'Openingsbeeld',
90
+ icon: 'panorama',
91
+ description: 'Groot beeld met titel eroverheen.',
92
+ },
93
+ {
94
+ id: 'features',
95
+ label: 'Kenmerken',
96
+ icon: 'list-check',
97
+ description: 'Drie kolommen met icoon en tekst.',
98
+ },
99
+ {
100
+ id: 'contact',
101
+ label: 'Contact',
102
+ icon: 'address-card',
103
+ description: 'Gegevens en een knop om te bellen.',
104
+ },
105
+ ]
106
+
107
+ const COPY = {
108
+ ariaLabel: 'Onderdeel kiezen',
109
+ listLabel: 'Onderdelen',
110
+ searchLabel: 'Zoek een onderdeel',
111
+ searchPlaceholder: 'Zoeken',
112
+ emptyTitle: 'Geen onderdelen gevonden',
113
+ emptyDescription: (search: string) => `Geen resultaat voor '${search}'.`,
114
+ clearLabel: 'Wis zoekopdracht',
115
+ previewEmptyText: 'Kies een onderdeel om een voorbeeld te zien.',
116
+ resultsAnnouncement: (count: number) =>
117
+ count === 1 ? '1 onderdeel gevonden' : `${count} onderdelen gevonden`,
118
+ }
119
+
120
+ /** A neutral stand-in for real preview art: one plane, one outline, the item's icon. */
121
+ const DummyPreview = defineComponent({
122
+ components: { NexxtIcon },
123
+ props: { icon: { type: String as () => NexxtIconName, required: true } },
124
+ template: `
125
+ <div class="relative size-full">
126
+ <svg viewBox="0 0 320 200" width="100%" height="100%" preserveAspectRatio="xMidYMid meet" aria-hidden="true" focusable="false" class="absolute inset-0">
127
+ <rect x="16" y="16" width="288" height="168" rx="6" fill="var(--color-gray-100)" stroke="var(--color-gray-300)" stroke-width="2" vector-effect="non-scaling-stroke" />
128
+ </svg>
129
+ <div class="absolute inset-0 grid place-items-center text-5xl text-cornflower-blue-500">
130
+ <NexxtIcon :name="icon" aria-hidden="true" />
131
+ </div>
132
+ </div>
133
+ `,
134
+ })
135
+
136
+ /** Stands in for the surface a FloatingPanel or a sidebar would give the panel. */
137
+ const SURFACE_CLASSES = 'inline-block overflow-hidden rounded-xl border border-gray-200 bg-white'
138
+
139
+ export default {
140
+ title: 'Components/Organisms/PickerPanel',
141
+ component: PickerPanel,
142
+ args: {
143
+ items: ITEMS,
144
+ groups: GROUPS,
145
+ ...COPY,
146
+ onSelect: fn(),
147
+ onClose: fn(),
148
+ },
149
+ } satisfies Meta<typeof PickerPanel>
150
+
151
+ type Story = StoryObj<typeof PickerPanel>
152
+
153
+ const renderPanel =
154
+ (initialSearch = '') =>
155
+ (args: Story['args']) => ({
156
+ components: { PickerPanel, DummyPreview },
157
+ setup() {
158
+ const highlighted = ref<string | null>(null)
159
+ const search = ref(initialSearch)
160
+ const closed = ref(0)
161
+ const onClose = () => {
162
+ closed.value++
163
+ args?.onClose?.()
164
+ }
165
+ return { args, highlighted, search, closed, onClose, surface: SURFACE_CLASSES }
166
+ },
167
+ template: `
168
+ <div class="flex flex-col items-start gap-3">
169
+ <div :class="surface">
170
+ <PickerPanel
171
+ v-bind="args"
172
+ v-model:highlighted="highlighted"
173
+ v-model:search="search"
174
+ @close="onClose"
175
+ >
176
+ <template #preview="{ item }">
177
+ <DummyPreview :icon="item.icon" />
178
+ </template>
179
+ </PickerPanel>
180
+ </div>
181
+ <dl class="grid grid-cols-[auto_1fr] gap-x-3 extra-small-normal text-gray-700">
182
+ <dt>highlighted</dt><dd><code data-testid="highlighted">{{ highlighted }}</code></dd>
183
+ <dt>search</dt><dd><code data-testid="search">{{ search }}</code></dd>
184
+ <dt>close</dt><dd><code data-testid="closed">{{ closed }}</code></dd>
185
+ </dl>
186
+ </div>
187
+ `,
188
+ })
189
+
190
+ const inputOf = (canvasElement: HTMLElement) => within(canvasElement).getByRole('combobox')
191
+ const listboxOf = (canvasElement: HTMLElement) => within(canvasElement).getByRole('listbox')
192
+
193
+ const activeLabel = (canvasElement: HTMLElement) => {
194
+ const id = inputOf(canvasElement).getAttribute('aria-activedescendant')
195
+ const option = id ? document.getElementById(id) : null
196
+ return (
197
+ Array.from(option?.querySelectorAll(':scope > span:not([hidden])') ?? [])
198
+ .at(-1)
199
+ ?.textContent?.trim() ?? null
200
+ )
201
+ }
202
+
203
+ /**
204
+ * The demo: two groups, one ungrouped item, one disabled item with its reason, and a dummy
205
+ * preview. Both models are logged under the panel.
206
+ */
207
+ export const Demo: Story = {
208
+ render: renderPanel(),
209
+ play: async ({ args, canvasElement }) => {
210
+ const canvas = within(canvasElement)
211
+ const input = inputOf(canvasElement)
212
+ const listbox = listboxOf(canvasElement)
213
+ const panel = canvas.getByRole('group', { name: 'Onderdeel kiezen' })
214
+
215
+ // focus lands in the field, with the first enabled item highlighted
216
+ await waitFor(() => expect(input).toHaveFocus())
217
+ await expect(activeLabel(canvasElement)).toBe('Introductie')
218
+ await expect(canvas.getByTestId('highlighted')).toHaveTextContent('intro')
219
+ await expect(input).toHaveAttribute('aria-controls', listbox.id)
220
+
221
+ // 640 wide; the height is 420, or the viewport minus 5rem when that is less
222
+ const rect = panel.getBoundingClientRect()
223
+ await expect(rect.width).toBe(640)
224
+ await expect(rect.height).toBeCloseTo(Math.min(420, window.innerHeight - 80), 0)
225
+
226
+ // the input stays inside the padded search block of the 264px column, so its own
227
+ // `min-w-60` no longer pushes it under the clipping edge
228
+ const column = listbox.parentElement!.parentElement!
229
+ const searchBlock = column.firstElementChild!
230
+ const padding = parseFloat(getComputedStyle(searchBlock).paddingRight)
231
+ await expect(column.getBoundingClientRect().width).toBe(264)
232
+ await expect(input.getBoundingClientRect().right).toBeLessThanOrEqual(
233
+ searchBlock.getBoundingClientRect().right - padding,
234
+ )
235
+
236
+ // a group after an ungrouped item gets its 8px on top of the 2px row gap
237
+ const intro = document.getElementById(listbox.getAttribute('aria-activedescendant')!)!
238
+ // the list overflows, and the ungrouped option still keeps its full 36px
239
+ await expect(listbox.scrollHeight).toBeGreaterThan(listbox.clientHeight)
240
+ await expect(intro.getBoundingClientRect().height).toBe(36)
241
+ const firstGroup = within(listbox).getAllByRole('group')[0]
242
+ await expect(
243
+ Math.round(firstGroup.getBoundingClientRect().top - intro.getBoundingClientRect().bottom),
244
+ ).toBe(10)
245
+
246
+ // the arrows, PageDown and Enter drive the list while focus stays in the field
247
+ await userEvent.keyboard('{ArrowDown}')
248
+ await expect(activeLabel(canvasElement)).toBe('Titel')
249
+ await userEvent.keyboard('{PageDown}')
250
+ await expect(activeLabel(canvasElement)).toBe('Galerij')
251
+ await userEvent.keyboard('{ArrowUp}{Enter}')
252
+ await expect(args.onSelect).toHaveBeenLastCalledWith(ITEMS[6])
253
+ await expect(input).toHaveFocus()
254
+
255
+ // the preview follows the highlight
256
+ const preview = panel.querySelector<HTMLElement>('[aria-hidden="true"].flex-col')!
257
+ await expect(preview).toHaveTextContent('Een ingesloten video met afspeelknop.')
258
+
259
+ // Escape twice: first the term goes, then the panel asks to close
260
+ await userEvent.type(input, 'foto')
261
+ await expect(activeLabel(canvasElement)).toBe('Afbeelding')
262
+ await userEvent.keyboard('{Escape}')
263
+ await expect(canvas.getByTestId('search')).toHaveTextContent('')
264
+ await expect(canvas.getByTestId('closed')).toHaveTextContent('0')
265
+ await userEvent.keyboard('{Escape}')
266
+ await expect(canvas.getByTestId('closed')).toHaveTextContent('1')
267
+
268
+ // tab order: field, listbox; the preview has no stops
269
+ await userEvent.tab()
270
+ await expect(listbox).toHaveFocus()
271
+ await userEvent.tab()
272
+ await expect(panel.contains(document.activeElement)).toBe(false)
273
+ },
274
+ }
275
+
276
+ /**
277
+ * How the app uses it: in a `NexxtFloatingPanel` with `surface="raised"` and `fit-height`. The
278
+ * picker takes its preferred 420px, or less when the roomiest side of the trigger has less room:
279
+ * `:height="Math.min(420, availableHeight ?? 420)"`. Escape first clears the term, then closes;
280
+ * both `@close`s fire and put focus back on the trigger.
281
+ */
282
+ export const InFloatingPanel: Story = {
283
+ render: (args) => ({
284
+ components: { PickerPanel, NexxtFloatingPanel, NexxtButton, DummyPreview },
285
+ setup() {
286
+ const trigger = ref<InstanceType<typeof NexxtButton> | null>(null)
287
+ const search = ref('')
288
+ // idempotent: the panel and the FloatingPanel both report the same Escape
289
+ const onClosed = () => (trigger.value?.$el as HTMLElement | undefined)?.focus()
290
+ return { args, trigger, search, onClosed }
291
+ },
292
+ template: `
293
+ <div class="flex min-h-132 items-start justify-center p-6">
294
+ <NexxtFloatingPanel surface="raised" fit-height placement="bottom" @close="onClosed">
295
+ <template #trigger="{ isOpen, toggle }">
296
+ <NexxtButton ref="trigger" variant="secondary" icon="plus" :aria-expanded="isOpen" @click="toggle">
297
+ Onderdeel kiezen
298
+ </NexxtButton>
299
+ </template>
300
+ <template #content="{ close, availableHeight }">
301
+ <PickerPanel
302
+ v-bind="args"
303
+ :height="Math.min(420, availableHeight ?? 420)"
304
+ v-model:search="search"
305
+ @select="(item) => { args.onSelect(item); close() }"
306
+ @close="close"
307
+ >
308
+ <template #preview="{ item }">
309
+ <DummyPreview :icon="item.icon" />
310
+ </template>
311
+ </PickerPanel>
312
+ </template>
313
+ </NexxtFloatingPanel>
314
+ </div>
315
+ `,
316
+ }),
317
+ play: async ({ canvasElement }) => {
318
+ const canvas = within(canvasElement)
319
+ const trigger = canvas.getByRole('button', { name: 'Onderdeel kiezen' })
320
+
321
+ await userEvent.click(trigger)
322
+ const input = await canvas.findByRole('combobox')
323
+ await waitFor(() => expect(input).toHaveFocus())
324
+
325
+ // positioning element > transition wrapper > surface
326
+ const floating = input.closest('.z-50')!
327
+ const surface = floating.firstElementChild!.firstElementChild!
328
+ await expect(surface.scrollHeight).toBe(surface.clientHeight)
329
+ const rect = floating.getBoundingClientRect()
330
+ await expect(rect.top).toBeGreaterThanOrEqual(8)
331
+ await expect(rect.bottom).toBeLessThanOrEqual(document.documentElement.clientHeight - 8)
332
+
333
+ await userEvent.type(input, 'video')
334
+ await userEvent.keyboard('{Escape}')
335
+ await expect(input).toHaveValue('')
336
+ await expect(canvas.queryByRole('combobox')).toBe(input)
337
+
338
+ await userEvent.keyboard('{Escape}')
339
+ await waitFor(() => expect(canvas.queryByRole('combobox')).toBeNull())
340
+ await expect(trigger).toHaveFocus()
341
+ },
342
+ }
343
+
344
+ const TABS = ['Onderdelen', 'Combinaties']
345
+
346
+ /** `NexxtTabs` in the header swaps the items; the panel keeps its size. */
347
+ export const WithHeader: Story = {
348
+ render: (args) => ({
349
+ components: { PickerPanel, NexxtTabs, DummyPreview },
350
+ setup() {
351
+ const tab = ref(TABS[0])
352
+ const items = computed(() => (tab.value === TABS[0] ? ITEMS : COMBINED_ITEMS))
353
+ const groups = computed(() => (tab.value === TABS[0] ? GROUPS : []))
354
+ return { args, tab, tabs: TABS, items, groups, surface: SURFACE_CLASSES }
355
+ },
356
+ template: `
357
+ <div :class="surface">
358
+ <PickerPanel v-bind="args" :items="items" :groups="groups">
359
+ <template #header>
360
+ <NexxtTabs v-model="tab" :tabs="tabs" tablist-label="Soort inhoud" />
361
+ </template>
362
+ <template #preview="{ item }">
363
+ <DummyPreview :icon="item.icon" />
364
+ </template>
365
+ </PickerPanel>
366
+ </div>
367
+ `,
368
+ }),
369
+ play: async ({ canvasElement }) => {
370
+ const canvas = within(canvasElement)
371
+
372
+ await userEvent.click(canvas.getByRole('tab', { name: 'Combinaties' }))
373
+ await waitFor(() => expect(canvas.getAllByRole('option')).toHaveLength(COMBINED_ITEMS.length))
374
+ await expect(activeLabel(canvasElement)).toBe('Openingsbeeld')
375
+ },
376
+ }
377
+
378
+ /** 320 high: the column row drops under 340px, so the preview goes and the list takes the width. */
379
+ export const Low: Story = {
380
+ args: { height: 320 },
381
+ render: renderPanel(),
382
+ play: async ({ canvasElement }) => {
383
+ const panel = within(canvasElement).getByRole('group', { name: 'Onderdeel kiezen' })
384
+ // listbox → list root → list column
385
+ const column = listboxOf(canvasElement).parentElement!.parentElement!
386
+
387
+ await expect(panel.querySelector('[class*="aspect-"]')).toBeNull()
388
+ await expect(column.getBoundingClientRect().width).toBe(panel.getBoundingClientRect().width)
389
+ },
390
+ }
391
+
392
+ /** No results: the clear button empties the term and puts focus back in the field. */
393
+ export const SearchEmpty: Story = {
394
+ render: renderPanel('zwembad'),
395
+ play: async ({ canvasElement }) => {
396
+ const canvas = within(canvasElement)
397
+ const input = inputOf(canvasElement)
398
+
399
+ await waitFor(() => expect(input).toHaveFocus())
400
+ await expect(canvas.getByText("Geen resultaat voor 'zwembad'.")).toBeVisible()
401
+ await expect(canvas.getByText('Kies een onderdeel om een voorbeeld te zien.')).toBeVisible()
402
+
403
+ // field, then straight to the clear button: an empty listbox is no tab stop
404
+ await userEvent.tab()
405
+ const clear = canvas.getByRole('button', { name: 'Wis zoekopdracht' })
406
+ await expect(clear).toHaveFocus()
407
+
408
+ await userEvent.keyboard('{Enter}')
409
+ await expect(input).toHaveFocus()
410
+ await expect(input).toHaveValue('')
411
+ await waitFor(() => expect(canvas.getAllByRole('option')).toHaveLength(ITEMS.length))
412
+ },
413
+ }
414
+
415
+ /** Escape clears the search, and the highlighted item stays fully in view in the whole list. */
416
+ export const ClearSearch: Story = {
417
+ render: renderPanel(),
418
+ play: async ({ canvasElement }) => {
419
+ const input = inputOf(canvasElement)
420
+ const listbox = listboxOf(canvasElement)
421
+
422
+ await waitFor(() => expect(input).toHaveFocus())
423
+ await userEvent.type(input, 'gal')
424
+ await expect(activeLabel(canvasElement)).toBe('Galerij')
425
+
426
+ await userEvent.keyboard('{Escape}')
427
+ await expect(input).toHaveValue('')
428
+ await expect(activeLabel(canvasElement)).toBe('Galerij')
429
+ await waitFor(() => {
430
+ const option = document.getElementById(input.getAttribute('aria-activedescendant')!)!
431
+ const box = listbox.getBoundingClientRect()
432
+ const rect = option.getBoundingClientRect()
433
+ expect(rect.top).toBeGreaterThanOrEqual(box.top - 0.5)
434
+ expect(rect.bottom).toBeLessThanOrEqual(box.bottom + 0.5)
435
+ })
436
+ },
437
+ }
438
+
439
+ /** Without a search field the listbox takes focus on mount. */
440
+ export const NotSearchable: Story = {
441
+ args: { searchable: false },
442
+ render: renderPanel(),
443
+ play: async ({ canvasElement }) => {
444
+ const listbox = listboxOf(canvasElement)
445
+
446
+ await waitFor(() => expect(listbox).toHaveFocus())
447
+ await expect(within(canvasElement).queryByRole('combobox')).toBeNull()
448
+
449
+ await userEvent.keyboard('g')
450
+ await expect(listbox.getAttribute('aria-activedescendant')).toMatch(/-opt-gallery$/)
451
+ },
452
+ }