@nexxtmove/ui 2.5.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/dist/components/editor/molecules/EditorBorder/EditorBorder.vue.d.ts +12 -0
  2. package/dist/components/editor/molecules/EditorColor/_ThemeRoleSwatches.vue.d.ts +16 -0
  3. package/dist/index.js +1094 -1135
  4. package/dist/nuxt.js +0 -1
  5. package/package.json +38 -36
  6. package/src/attribute-contract.test.ts +151 -0
  7. package/src/components/atoms/AnimatedNumber/AnimatedNumber.stories.ts +36 -0
  8. package/src/components/atoms/AnimatedNumber/AnimatedNumber.test.ts +21 -0
  9. package/src/components/atoms/Avatar/Avatar.stories.ts +122 -0
  10. package/src/components/atoms/Avatar/Avatar.test.ts +134 -0
  11. package/src/components/atoms/Button/Button.stories.ts +290 -0
  12. package/src/components/atoms/Button/Button.test.ts +406 -0
  13. package/src/components/atoms/Card/Card.stories.ts +45 -0
  14. package/src/components/atoms/Card/Card.test.ts +46 -0
  15. package/src/components/atoms/Checkbox/Checkbox.stories.ts +35 -0
  16. package/src/components/atoms/Checkbox/Checkbox.test.ts +55 -0
  17. package/src/components/atoms/Chip/Chip.stories.ts +274 -0
  18. package/src/components/atoms/Chip/Chip.test.ts +230 -0
  19. package/src/components/atoms/EnergyLabel/EnergyLabel.stories.ts +189 -0
  20. package/src/components/atoms/EnergyLabel/EnergyLabel.test.ts +100 -0
  21. package/src/components/atoms/FloatingPanel/FloatingPanel.stories.ts +677 -0
  22. package/src/components/atoms/FloatingPanel/FloatingPanel.test.ts +864 -0
  23. package/src/components/atoms/Icon/Icon.stories.ts +63 -0
  24. package/src/components/atoms/Icon/Icon.test.ts +89 -0
  25. package/src/components/atoms/Loader/Loader.stories.ts +36 -0
  26. package/src/components/atoms/Loader/Loader.test.ts +40 -0
  27. package/src/components/atoms/PickerItem/PickerItem.stories.ts +172 -0
  28. package/src/components/atoms/PickerItem/PickerItem.test.ts +142 -0
  29. package/src/components/atoms/PickerPreview/PickerPreview.stories.ts +183 -0
  30. package/src/components/atoms/PickerPreview/PickerPreview.test.ts +137 -0
  31. package/src/components/atoms/PresenceIndicator/PresenceIndicator.stories.ts +21 -0
  32. package/src/components/atoms/PropertyListing/PropertyListing.stories.ts +38 -0
  33. package/src/components/atoms/PropertyListing/PropertyListing.test.ts +46 -0
  34. package/src/components/atoms/SelectableOption/SelectableOption.stories.ts +131 -0
  35. package/src/components/atoms/SelectableOption/SelectableOption.test.ts +96 -0
  36. package/src/components/atoms/Skeleton/Skeleton.stories.ts +27 -0
  37. package/src/components/atoms/Skeleton/Skeleton.test.ts +28 -0
  38. package/src/components/atoms/Slider/Slider.stories.ts +151 -0
  39. package/src/components/atoms/Slider/Slider.test.ts +191 -0
  40. package/src/components/atoms/Squircle/Squircle.stories.ts +114 -0
  41. package/src/components/atoms/Squircle/Squircle.test.ts +70 -0
  42. package/src/components/atoms/Switch/Switch.stories.ts +91 -0
  43. package/src/components/atoms/Switch/Switch.test.ts +183 -0
  44. package/src/components/atoms/Tooltip/Tooltip.stories.ts +158 -0
  45. package/src/components/atoms/Tooltip/Tooltip.test.ts +134 -0
  46. package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.stories.ts +124 -0
  47. package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.test.ts +464 -0
  48. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.stories.ts +266 -0
  49. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.test.ts +421 -0
  50. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.stories.ts +365 -0
  51. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.test.ts +484 -0
  52. package/src/components/editor/composables/useRadioGrid.test.ts +261 -0
  53. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.stories.ts +73 -0
  54. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.test.ts +189 -0
  55. package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.stories.ts +309 -0
  56. package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.test.ts +359 -0
  57. package/src/components/editor/molecules/EditorAnchor/EditorAnchor.stories.ts +138 -0
  58. package/src/components/editor/molecules/EditorAnchor/EditorAnchor.test.ts +344 -0
  59. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.stories.ts +87 -0
  60. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.test.ts +262 -0
  61. package/src/components/editor/molecules/EditorBlur/EditorBlur.stories.ts +115 -0
  62. package/src/components/editor/molecules/EditorBlur/EditorBlur.test.ts +243 -0
  63. package/src/components/editor/molecules/EditorBorder/EditorBorder.stories.ts +187 -0
  64. package/src/components/editor/molecules/EditorBorder/EditorBorder.test.ts +456 -0
  65. package/src/components/editor/molecules/EditorBorder/EditorBorder.vue +58 -2
  66. package/src/components/editor/molecules/EditorColor/EditorColor.stories.ts +563 -0
  67. package/src/components/editor/molecules/EditorColor/EditorColor.test.ts +689 -0
  68. package/src/components/editor/molecules/EditorColor/EditorColorGradient.test.ts +793 -0
  69. package/src/components/editor/molecules/EditorColor/_ColorField.vue +7 -29
  70. package/src/components/editor/molecules/EditorColor/_ThemeRoleSwatches.vue +57 -0
  71. package/src/components/editor/molecules/EditorColor/gradientClass.test.ts +303 -0
  72. package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.stories.ts +72 -0
  73. package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.test.ts +124 -0
  74. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.stories.ts +46 -0
  75. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.test.ts +97 -0
  76. package/src/components/editor/molecules/EditorFontSize/EditorFontSize.stories.ts +248 -0
  77. package/src/components/editor/molecules/EditorFontSize/EditorFontSize.test.ts +288 -0
  78. package/src/components/editor/molecules/EditorGap/EditorGap.stories.ts +129 -0
  79. package/src/components/editor/molecules/EditorGap/EditorGap.test.ts +253 -0
  80. package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.stories.ts +68 -0
  81. package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.test.ts +150 -0
  82. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.stories.ts +71 -0
  83. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.test.ts +161 -0
  84. package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.stories.ts +204 -0
  85. package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.test.ts +238 -0
  86. package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.stories.ts +257 -0
  87. package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.test.ts +286 -0
  88. package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.stories.ts +135 -0
  89. package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.test.ts +230 -0
  90. package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.stories.ts +154 -0
  91. package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.test.ts +112 -0
  92. package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.stories.ts +206 -0
  93. package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.test.ts +332 -0
  94. package/src/components/editor/molecules/EditorPadding/EditorPadding.stories.ts +374 -0
  95. package/src/components/editor/molecules/EditorPadding/EditorPadding.test.ts +712 -0
  96. package/src/components/editor/molecules/EditorRadius/EditorRadius.stories.ts +64 -0
  97. package/src/components/editor/molecules/EditorRadius/EditorRadius.test.ts +137 -0
  98. package/src/components/editor/molecules/EditorShadow/EditorShadow.stories.ts +103 -0
  99. package/src/components/editor/molecules/EditorShadow/EditorShadow.test.ts +168 -0
  100. package/src/components/editor/molecules/EditorSize/EditorSize.stories.ts +84 -0
  101. package/src/components/editor/molecules/EditorSize/EditorSize.test.ts +253 -0
  102. package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.stories.ts +49 -0
  103. package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.test.ts +43 -0
  104. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.stories.ts +69 -0
  105. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.test.ts +155 -0
  106. package/src/components/molecules/Announcement/Announcement.stories.ts +224 -0
  107. package/src/components/molecules/Announcement/Announcement.test.ts +161 -0
  108. package/src/components/molecules/ContactInfoCard/ContactInfoCard.stories.ts +99 -0
  109. package/src/components/molecules/ContactInfoCard/ContactInfoCard.test.ts +70 -0
  110. package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.stories.ts +148 -0
  111. package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.test.ts +271 -0
  112. package/src/components/molecules/DoughnutChart/DoughnutChart.stories.ts +45 -0
  113. package/src/components/molecules/DoughnutChart/DoughnutChart.test.ts +205 -0
  114. package/src/components/molecules/InfoBlock/InfoBlock.stories.ts +84 -0
  115. package/src/components/molecules/InfoBlock/InfoBlock.test.ts +101 -0
  116. package/src/components/molecules/InputField/InputField.stories.ts +117 -0
  117. package/src/components/molecules/InputField/InputField.test.ts +138 -0
  118. package/src/components/molecules/InputSelect/InputSelect.stories.ts +116 -0
  119. package/src/components/molecules/InputSelect/InputSelect.test.ts +127 -0
  120. package/src/components/molecules/ModalFooter/ModalFooter.stories.ts +204 -0
  121. package/src/components/molecules/ModalFooter/ModalFooter.test.ts +276 -0
  122. package/src/components/molecules/PageCard/PageCard.stories.ts +100 -0
  123. package/src/components/molecules/PageCard/PageCard.test.ts +91 -0
  124. package/src/components/molecules/PageContainer/PageContainer.stories.ts +90 -0
  125. package/src/components/molecules/PageContainer/PageContainer.test.ts +50 -0
  126. package/src/components/molecules/PageMockup/PageMockup.stories.ts +38 -0
  127. package/src/components/molecules/PageMockup/PageMockup.test.ts +20 -0
  128. package/src/components/molecules/PageTypeTile/PageTypeTile.stories.ts +63 -0
  129. package/src/components/molecules/PageTypeTile/PageTypeTile.test.ts +41 -0
  130. package/src/components/molecules/Pagination/Pagination.stories.ts +72 -0
  131. package/src/components/molecules/Pagination/Pagination.test.ts +130 -0
  132. package/src/components/molecules/PickerGroup/PickerGroup.stories.ts +90 -0
  133. package/src/components/molecules/PickerGroup/PickerGroup.test.ts +145 -0
  134. package/src/components/molecules/PickerList/PickerList.stories.ts +436 -0
  135. package/src/components/molecules/PickerList/PickerList.test.ts +870 -0
  136. package/src/components/molecules/PlaceholderInput/PlaceholderInput.stories.ts +48 -0
  137. package/src/components/molecules/PlaceholderInput/PlaceholderInput.test.ts +107 -0
  138. package/src/components/molecules/PropertyCard/PropertyCard.stories.ts +46 -0
  139. package/src/components/molecules/PropertyCard/PropertyCard.test.ts +59 -0
  140. package/src/components/molecules/Select/Select.stories.ts +264 -0
  141. package/src/components/molecules/Select/Select.test.ts +315 -0
  142. package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.stories.ts +84 -0
  143. package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.test.ts +103 -0
  144. package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.stories.ts +11 -0
  145. package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.test.ts +38 -0
  146. package/src/components/molecules/SocialIcons/SocialIcons.stories.ts +28 -0
  147. package/src/components/molecules/SocialIcons/SocialIcons.test.ts +58 -0
  148. package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.stories.ts +11 -0
  149. package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.test.ts +116 -0
  150. package/src/components/molecules/SocialMediaType/SocialMediaType.stories.ts +37 -0
  151. package/src/components/molecules/SocialMediaType/SocialMediaType.test.ts +126 -0
  152. package/src/components/molecules/Source/Source.stories.ts +35 -0
  153. package/src/components/molecules/Source/Source.test.ts +45 -0
  154. package/src/components/molecules/TabbedContent/TabbedContent.stories.ts +96 -0
  155. package/src/components/molecules/TabbedContent/TabbedContent.test.ts +109 -0
  156. package/src/components/molecules/Tabs/Tabs.stories.ts +230 -0
  157. package/src/components/molecules/Tabs/Tabs.test.ts +280 -0
  158. package/src/components/molecules/TemplateCard/TemplateCard.stories.ts +327 -0
  159. package/src/components/molecules/TemplateCard/TemplateCard.test.ts +339 -0
  160. package/src/components/molecules/Textarea/Textarea.stories.ts +216 -0
  161. package/src/components/molecules/Textarea/Textarea.test.ts +191 -0
  162. package/src/components/molecules/TimelineEvent/TimelineEvent.stories.ts +319 -0
  163. package/src/components/molecules/TimelineEvent/TimelineEvent.test.ts +224 -0
  164. package/src/components/molecules/TimelineLanes/TimelineLanes.stories.ts +29 -0
  165. package/src/components/molecules/TimelineLanes/TimelineLanes.test.ts +30 -0
  166. package/src/components/molecules/Toast/Toast.stories.ts +110 -0
  167. package/src/components/molecules/Toast/Toast.test.ts +103 -0
  168. package/src/components/molecules/ToggleGroup/ToggleGroup.stories.ts +297 -0
  169. package/src/components/molecules/ToggleGroup/ToggleGroup.test.ts +578 -0
  170. package/src/components/molecules/Tree/Tree.stories.ts +884 -0
  171. package/src/components/molecules/Tree/Tree.test.ts +1247 -0
  172. package/src/components/molecules/Tree/tree-drop.test.ts +223 -0
  173. package/src/components/organisms/Calendar/Calendar.stories.ts +108 -0
  174. package/src/components/organisms/Calendar/Calendar.test.ts +279 -0
  175. package/src/components/organisms/Calendar/_CalendarDayView.test.ts +104 -0
  176. package/src/components/organisms/Calendar/_CalendarHeader.test.ts +86 -0
  177. package/src/components/organisms/Calendar/_CalendarMonthView.test.ts +52 -0
  178. package/src/components/organisms/Calendar/_CalendarTimePicker.test.ts +89 -0
  179. package/src/components/organisms/Calendar/_CalendarYearView.test.ts +56 -0
  180. package/src/components/organisms/ColorPicker/ColorPicker.stories.ts +229 -0
  181. package/src/components/organisms/ColorPicker/ColorPicker.test.ts +638 -0
  182. package/src/components/organisms/ColorPicker/gradient.test.ts +67 -0
  183. package/src/components/organisms/ColorPicker/gradientGeometry.test.ts +206 -0
  184. package/src/components/organisms/Combobox/Combobox.stories.ts +234 -0
  185. package/src/components/organisms/Combobox/Combobox.test.ts +512 -0
  186. package/src/components/organisms/Combobox/Combobox.vue +1 -1
  187. package/src/components/organisms/ComboboxPanel/ComboboxPanel.stories.ts +158 -0
  188. package/src/components/organisms/ComboboxPanel/ComboboxPanel.test.ts +524 -0
  189. package/src/components/organisms/ComboboxPanel/ComboboxPanel.vue +1 -1
  190. package/src/components/organisms/CountrySelect/CountrySelect.stories.ts +139 -0
  191. package/src/components/organisms/CountrySelect/CountrySelect.test.ts +149 -0
  192. package/src/components/organisms/DatePicker/DatePicker.stories.ts +163 -0
  193. package/src/components/organisms/DatePicker/DatePicker.test.ts +214 -0
  194. package/src/components/organisms/DropdownButton/DropdownButton.stories.ts +123 -0
  195. package/src/components/organisms/DropdownButton/DropdownButton.test.ts +379 -0
  196. package/src/components/organisms/FontPicker/FontPicker.stories.ts +476 -0
  197. package/src/components/organisms/FontPicker/FontPicker.test.ts +873 -0
  198. package/src/components/organisms/Header/Header.stories.ts +75 -0
  199. package/src/components/organisms/Header/Header.test.ts +233 -0
  200. package/src/components/organisms/IconPicker/IconPicker.stories.ts +734 -0
  201. package/src/components/organisms/IconPicker/IconPicker.test.ts +669 -0
  202. package/src/components/organisms/IconPicker/_IconPickerGrid.test.ts +184 -0
  203. package/src/components/organisms/IconPicker/_IconPickerRail.test.ts +102 -0
  204. package/src/components/organisms/IconPicker/useIconPickerNavigation.test.ts +215 -0
  205. package/src/components/organisms/Modal/Modal.stories.ts +350 -0
  206. package/src/components/organisms/Modal/Modal.test.ts +275 -0
  207. package/src/components/organisms/PickerPanel/PickerPanel.stories.ts +452 -0
  208. package/src/components/organisms/PickerPanel/PickerPanel.test.ts +358 -0
  209. package/src/components/organisms/ProgressBar/ProgressBar.stories.ts +100 -0
  210. package/src/components/organisms/ProgressBar/ProgressBar.test.ts +417 -0
  211. package/src/components/organisms/ReferenceCard/ReferenceCard.stories.ts +191 -0
  212. package/src/components/organisms/ReferenceCard/ReferenceCard.test.ts +165 -0
  213. package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.stories.ts +65 -0
  214. package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.test.ts +466 -0
  215. package/src/components/organisms/StepperHeader/StepperHeader.stories.ts +91 -0
  216. package/src/components/organisms/StepperHeader/StepperHeader.test.ts +279 -0
  217. package/src/components/organisms/Table/Table.stories.ts +332 -0
  218. package/src/components/organisms/Table/Table.test.ts +320 -0
  219. package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.stories.ts +198 -0
  220. package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.test.ts +283 -0
  221. package/src/components/organisms/TimelineTrack/TimelineTrack.stories.ts +120 -0
  222. package/src/components/organisms/TimelineTrack/TimelineTrack.test.ts +91 -0
  223. package/src/components/organisms/Toaster/Toaster.stories.ts +51 -0
  224. package/src/components/organisms/Toaster/Toaster.test.ts +75 -0
  225. package/src/components/organisms/VideoControls/VideoControls.stories.ts +792 -0
  226. package/src/components/organisms/VideoControls/VideoControls.test.ts +1056 -0
  227. package/src/components/organisms/VideoPlayer/VideoPlayer.stories.ts +327 -0
  228. package/src/components/organisms/VideoPlayer/VideoPlayer.test.ts +439 -0
@@ -0,0 +1,358 @@
1
+ import { afterEach, describe, it, expect, vi } from 'vitest'
2
+ import { defineComponent, nextTick, ref } from 'vue'
3
+ import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
4
+ import PickerPanel from './PickerPanel.vue'
5
+ import type {
6
+ NexxtPickerGroupData,
7
+ NexxtPickerItemData,
8
+ } from '../../molecules/PickerList/PickerList.vue'
9
+
10
+ const GROUPS: NexxtPickerGroupData[] = [
11
+ { id: 'basis', label: 'Basis' },
12
+ { id: 'media', label: 'Media' },
13
+ ]
14
+
15
+ const ITEMS: NexxtPickerItemData[] = [
16
+ { id: 'table', label: 'Tabel', icon: 'table', group: 'basis', disabled: true },
17
+ { id: 'title', label: 'Titel', icon: 'heading', group: 'basis', description: 'Een kop' },
18
+ { id: 'text', label: 'Tekst', icon: 'font', group: 'basis' },
19
+ { id: 'image', label: 'Afbeelding', icon: 'image', group: 'media', keywords: ['foto'] },
20
+ ]
21
+
22
+ const COPY = {
23
+ ariaLabel: 'Onderdeel kiezen',
24
+ listLabel: 'Onderdelen',
25
+ searchLabel: 'Zoek een onderdeel',
26
+ searchPlaceholder: 'Zoeken',
27
+ emptyTitle: 'Geen onderdelen gevonden',
28
+ clearLabel: 'Wis zoekopdracht',
29
+ previewEmptyText: 'Kies een onderdeel',
30
+ }
31
+
32
+ type Props = Partial<InstanceType<typeof PickerPanel>['$props']>
33
+
34
+ const mounted: VueWrapper[] = []
35
+
36
+ const mountPanel = async (props: Props = {}, slots: Record<string, string> = {}) => {
37
+ // The list settles its highlight during setup, before `mount` has returned a wrapper; until
38
+ // then the panel's own local model holds it
39
+ const holder: { wrapper?: VueWrapper } = {}
40
+ const wrapper = mount(PickerPanel, {
41
+ props: {
42
+ items: ITEMS,
43
+ groups: GROUPS,
44
+ ...COPY,
45
+ 'onUpdate:search': (value: string) => holder.wrapper?.setProps({ search: value }),
46
+ 'onUpdate:highlighted': (value: string | null) =>
47
+ holder.wrapper?.setProps({ highlighted: value }),
48
+ ...props,
49
+ },
50
+ slots,
51
+ attachTo: document.body,
52
+ })
53
+ holder.wrapper = wrapper
54
+ mounted.push(wrapper)
55
+ await flushPromises()
56
+ return wrapper
57
+ }
58
+
59
+ const inputOf = (wrapper: VueWrapper) => wrapper.find('input')
60
+ const previewOf = (wrapper: VueWrapper) => wrapper.findComponent({ name: 'NexxtPickerPreview' })
61
+ const listboxOf = (wrapper: VueWrapper) => wrapper.find('[role="listbox"]')
62
+ const activeOption = (wrapper: VueWrapper) =>
63
+ document.getElementById(inputOf(wrapper).attributes('aria-activedescendant')!)
64
+
65
+ const keydown = async (target: Element, key: string) => {
66
+ const event = new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true })
67
+ target.dispatchEvent(event)
68
+ await flushPromises()
69
+ return event
70
+ }
71
+
72
+ /** Focusable elements in tab order (no positive tabindex in use here). */
73
+ const tabOrder = (wrapper: VueWrapper) => {
74
+ // An untyped VueWrapper types its element as `any`; pin it to Element for the query.
75
+ const root: Element = wrapper.element
76
+ return Array.from(
77
+ root.querySelectorAll<HTMLElement>('input, button, a[href], [tabindex]'),
78
+ ).filter((element) => element.tabIndex >= 0 && !element.closest('[inert]'))
79
+ }
80
+
81
+ describe('PickerPanel', () => {
82
+ afterEach(() => {
83
+ mounted.splice(0).forEach((wrapper) => wrapper.unmount())
84
+ vi.restoreAllMocks()
85
+ })
86
+
87
+ it('is a named group with the list and the preview', async () => {
88
+ const wrapper = await mountPanel()
89
+
90
+ expect(wrapper.attributes('role')).toBe('group')
91
+ expect(wrapper.attributes('aria-label')).toBe('Onderdeel kiezen')
92
+ expect(listboxOf(wrapper).attributes('aria-label')).toBe('Onderdelen')
93
+ expect(previewOf(wrapper).find('[class*="aspect-"]').exists()).toBe(true)
94
+ })
95
+
96
+ it('focuses the search field on mount and highlights the first enabled item', async () => {
97
+ const wrapper = await mountPanel()
98
+
99
+ expect(document.activeElement).toBe(inputOf(wrapper).element)
100
+ expect(activeOption(wrapper)?.textContent).toContain('Titel')
101
+ expect(wrapper.emitted('update:highlighted')![0]).toEqual(['title'])
102
+ })
103
+
104
+ it('wires the search field as a combobox that controls the listbox', async () => {
105
+ const wrapper = await mountPanel()
106
+ const input = inputOf(wrapper)
107
+
108
+ expect(input.attributes()).toMatchObject({
109
+ type: 'text',
110
+ role: 'combobox',
111
+ 'aria-expanded': 'true',
112
+ 'aria-autocomplete': 'list',
113
+ 'aria-label': 'Zoek een onderdeel',
114
+ autocomplete: 'off',
115
+ spellcheck: 'false',
116
+ placeholder: 'Zoeken',
117
+ })
118
+ expect(input.attributes('aria-controls')).toBe(listboxOf(wrapper).attributes('id'))
119
+ expect(input.attributes('aria-activedescendant')).toBe(
120
+ listboxOf(wrapper).attributes('aria-activedescendant'),
121
+ )
122
+ })
123
+
124
+ it('lifts the minimum width of the input inside the panel', async () => {
125
+ const wrapper = await mountPanel()
126
+
127
+ expect(wrapper.findComponent({ name: 'NexxtInputField' }).classes()).toContain(
128
+ '[&_input]:min-w-0',
129
+ )
130
+ })
131
+
132
+ it('drives the list from the field while focus stays in it', async () => {
133
+ const wrapper = await mountPanel()
134
+ const input = inputOf(wrapper).element
135
+
136
+ expect((await keydown(input, 'ArrowDown')).defaultPrevented).toBe(true)
137
+ expect(activeOption(wrapper)?.textContent).toContain('Tekst')
138
+
139
+ await keydown(input, 'ArrowUp')
140
+ expect(activeOption(wrapper)?.textContent).toContain('Titel')
141
+
142
+ await keydown(input, 'PageDown')
143
+ expect(activeOption(wrapper)?.textContent).toContain('Afbeelding')
144
+
145
+ await keydown(input, 'PageUp')
146
+ expect(activeOption(wrapper)?.textContent).toContain('Tabel')
147
+
148
+ await keydown(input, 'ArrowDown')
149
+ expect((await keydown(input, 'Enter')).defaultPrevented).toBe(true)
150
+ expect(wrapper.emitted('select')).toEqual([[ITEMS[1]]])
151
+
152
+ expect(document.activeElement).toBe(input)
153
+ })
154
+
155
+ it('leaves Home, End and the horizontal arrows to the text cursor', async () => {
156
+ const wrapper = await mountPanel()
157
+ const input = inputOf(wrapper).element
158
+
159
+ for (const key of ['Home', 'End', 'ArrowLeft', 'ArrowRight']) {
160
+ expect((await keydown(input, key)).defaultPrevented, key).toBe(false)
161
+ }
162
+ expect(activeOption(wrapper)?.textContent).toContain('Titel')
163
+ })
164
+
165
+ it('filters on the search model', async () => {
166
+ const wrapper = await mountPanel()
167
+
168
+ await inputOf(wrapper).setValue('foto')
169
+ await flushPromises()
170
+
171
+ expect(wrapper.emitted('update:search')!.at(-1)).toEqual(['foto'])
172
+ expect(wrapper.findAll('[role="option"]')).toHaveLength(1)
173
+ expect(activeOption(wrapper)?.textContent).toContain('Afbeelding')
174
+ })
175
+
176
+ it('clears the search on Escape without letting a surrounding panel see it', async () => {
177
+ const wrapper = await mountPanel({ search: 'foto' })
178
+ const onDocument = vi.fn()
179
+ document.addEventListener('keydown', onDocument)
180
+
181
+ const event = await keydown(inputOf(wrapper).element, 'Escape')
182
+ document.removeEventListener('keydown', onDocument)
183
+
184
+ expect(event.defaultPrevented).toBe(true)
185
+ expect(wrapper.emitted('update:search')!.at(-1)).toEqual([''])
186
+ expect(wrapper.emitted('close')).toBeUndefined()
187
+ expect(onDocument).not.toHaveBeenCalled()
188
+ })
189
+
190
+ it('emits close on Escape in an empty field and lets the event through', async () => {
191
+ const wrapper = await mountPanel()
192
+ const onDocument = vi.fn()
193
+ document.addEventListener('keydown', onDocument)
194
+
195
+ await keydown(inputOf(wrapper).element, 'Escape')
196
+ document.removeEventListener('keydown', onDocument)
197
+
198
+ expect(wrapper.emitted('close')).toEqual([[]])
199
+ expect(onDocument).toHaveBeenCalledOnce()
200
+ })
201
+
202
+ it('passes close, select and highlight from the list through', async () => {
203
+ const wrapper = await mountPanel()
204
+
205
+ await keydown(listboxOf(wrapper).element, 'ArrowDown')
206
+ await keydown(listboxOf(wrapper).element, 'Enter')
207
+ await keydown(listboxOf(wrapper).element, 'Escape')
208
+
209
+ expect(wrapper.emitted('highlight')!.at(-1)).toEqual([ITEMS[2]])
210
+ expect(wrapper.emitted('select')).toEqual([[ITEMS[2]]])
211
+ expect(wrapper.emitted('close')).toEqual([[]])
212
+ })
213
+
214
+ it('tabs from the field to the listbox, and to the clear button when nothing matches', async () => {
215
+ const wrapper = await mountPanel({ previewEmptyText: 'Kies' })
216
+
217
+ expect(tabOrder(wrapper)).toEqual([inputOf(wrapper).element, listboxOf(wrapper).element])
218
+
219
+ await wrapper.setProps({ search: 'xyz' })
220
+ await flushPromises()
221
+
222
+ expect(tabOrder(wrapper)).toEqual([inputOf(wrapper).element, wrapper.find('button').element])
223
+ })
224
+
225
+ it('clears the search from the empty state and puts focus back in the field', async () => {
226
+ const wrapper = await mountPanel({ search: 'xyz' })
227
+ const button = wrapper.find('button').element as HTMLButtonElement
228
+
229
+ button.focus()
230
+ button.click()
231
+ await flushPromises()
232
+
233
+ expect(wrapper.emitted('update:search')!.at(-1)).toEqual([''])
234
+ expect(document.activeElement).toBe(inputOf(wrapper).element)
235
+ expect(wrapper.findAll('[role="option"]')).toHaveLength(4)
236
+ })
237
+
238
+ it('has exactly one live region', async () => {
239
+ const wrapper = await mountPanel({ resultsAnnouncement: (n) => `${n} onderdelen` })
240
+
241
+ expect(wrapper.findAll('[aria-live]')).toHaveLength(1)
242
+ })
243
+
244
+ // jsdom's CSSOM drops `min(…, calc(100dvh - 5rem))`, so the height clamp is asserted in the
245
+ // Demo story in Chromium; only the width can be read here
246
+ it('sets its width, a number in pixels and a string as is', async () => {
247
+ const wrapper = await mountPanel()
248
+ expect((wrapper.element as HTMLElement).style.width).toBe('640px')
249
+
250
+ await wrapper.setProps({ width: '100%' })
251
+ expect((wrapper.element as HTMLElement).style.width).toBe('100%')
252
+ })
253
+
254
+ it('shows the preview while the row height is unknown', async () => {
255
+ const wrapper = await mountPanel()
256
+
257
+ expect(wrapper.find('[class*="aspect-"]').exists()).toBe(true)
258
+ expect(wrapper.find('.w-66').exists()).toBe(true)
259
+ })
260
+
261
+ it('drops the preview and widens the list below the threshold', async () => {
262
+ vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(339)
263
+ const wrapper = await mountPanel()
264
+
265
+ expect(wrapper.find('[class*="aspect-"]').exists()).toBe(false)
266
+ expect(listboxOf(wrapper).element.parentElement!.parentElement!.className).toContain('w-full')
267
+ })
268
+
269
+ it('keeps the preview at the threshold', async () => {
270
+ vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(340)
271
+ const wrapper = await mountPanel()
272
+
273
+ expect(wrapper.find('[class*="aspect-"]').exists()).toBe(true)
274
+ })
275
+
276
+ it('leaves the preview out at any height when preview is false', async () => {
277
+ vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(800)
278
+ const wrapper = await mountPanel({ preview: false })
279
+
280
+ expect(wrapper.find('[class*="aspect-"]').exists()).toBe(false)
281
+ expect(listboxOf(wrapper).element.parentElement!.parentElement!.className).toContain('w-full')
282
+ })
283
+
284
+ it('passes busy and the append slot to the list', async () => {
285
+ const wrapper = await mountPanel(
286
+ { busy: true, search: 'zwembad' },
287
+ { append: '<div aria-hidden="true" data-test="placeholder" />' },
288
+ )
289
+
290
+ expect(listboxOf(wrapper).attributes('aria-busy')).toBe('true')
291
+ expect(listboxOf(wrapper).find('[data-test="placeholder"]').exists()).toBe(true)
292
+ expect(wrapper.text()).not.toContain(COPY.emptyTitle)
293
+ })
294
+
295
+ it('previews the highlighted item through the preview slot', async () => {
296
+ const wrapper = await mountPanel(
297
+ {},
298
+ { preview: '<template #preview="{ item }"><b>{{ item.id }}</b></template>' },
299
+ )
300
+ const preview = previewOf(wrapper)
301
+
302
+ expect(preview.find('b').text()).toBe('title')
303
+ expect(preview.text()).toContain('Een kop')
304
+
305
+ await keydown(inputOf(wrapper).element, 'ArrowDown')
306
+
307
+ expect(preview.find('b').text()).toBe('text')
308
+ })
309
+
310
+ it('shows the empty preview when nothing is highlighted', async () => {
311
+ const wrapper = await mountPanel(
312
+ { search: 'xyz' },
313
+ { preview: '<template #preview="{ item }"><b>{{ item.id }}</b></template>' },
314
+ )
315
+
316
+ expect(previewOf(wrapper).text()).toBe('Kies een onderdeel')
317
+
318
+ await wrapper.setProps({ search: 'zzz' })
319
+ const withSlot = await mountPanel({ search: 'xyz' }, { 'preview-empty': '<em>Leeg</em>' })
320
+
321
+ expect(previewOf(withSlot).find('em').text()).toBe('Leeg')
322
+ })
323
+
324
+ it('renders the header slot above the columns', async () => {
325
+ const wrapper = await mountPanel({}, { header: '<nav>Tabs</nav>' })
326
+
327
+ expect(wrapper.element.firstElementChild!.textContent).toBe('Tabs')
328
+ })
329
+
330
+ it('focuses the listbox on mount when not searchable', async () => {
331
+ const wrapper = await mountPanel({ searchable: false })
332
+
333
+ expect(inputOf(wrapper).exists()).toBe(false)
334
+ expect(document.activeElement).toBe(listboxOf(wrapper).element)
335
+ })
336
+
337
+ // Through a parent's template ref, as a consumer gets it: `wrapper.vm` would also reach
338
+ // setup state that was never exposed
339
+ it('exposes focus to a template ref', async () => {
340
+ const Parent = defineComponent({
341
+ components: { PickerPanel },
342
+ setup() {
343
+ const panel = ref<{ focus: () => void } | null>(null)
344
+ return { panel, items: ITEMS, groups: GROUPS, copy: COPY }
345
+ },
346
+ template: '<PickerPanel ref="panel" :items="items" :groups="groups" v-bind="copy" />',
347
+ })
348
+ const parent = mount(Parent, { attachTo: document.body })
349
+ await flushPromises()
350
+
351
+ parent.find<HTMLElement>('[role="listbox"]').element.focus()
352
+ parent.vm.panel!.focus()
353
+ await nextTick()
354
+
355
+ expect(document.activeElement).toBe(parent.find('input').element)
356
+ parent.unmount()
357
+ })
358
+ })
@@ -0,0 +1,100 @@
1
+ import { ref, watch } from 'vue'
2
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
3
+ import ProgressBar from './ProgressBar.vue'
4
+
5
+ export default {
6
+ title: 'Components/Organisms/Progress Bar',
7
+ component: ProgressBar,
8
+ } satisfies Meta<typeof ProgressBar>
9
+
10
+ type Story = StoryObj<typeof ProgressBar>
11
+
12
+ // Keeps the stepper interactive in Storybook: clicking a completed step (or
13
+ // picking one from the compact dropdown) actually navigates there, and the
14
+ // button walks forward again.
15
+ const interactiveRender: Story['render'] = (args) => ({
16
+ components: { ProgressBar },
17
+ setup() {
18
+ const currentStep = ref(args.currentStep)
19
+ watch(
20
+ () => args.currentStep,
21
+ (value) => (currentStep.value = value),
22
+ )
23
+ const next = () => {
24
+ currentStep.value = Math.min(currentStep.value + 1, args.steps - 1)
25
+ }
26
+ return { args, currentStep, next }
27
+ },
28
+ template: `
29
+ <ProgressBar v-bind="args" v-model:current-step="currentStep" />
30
+ <button
31
+ style="margin-top: 24px; cursor: pointer; border: 1px solid #d1d5db; border-radius: 8px; padding: 4px 12px; font-size: 12px;"
32
+ :disabled="currentStep >= args.steps - 1"
33
+ @click="next"
34
+ >
35
+ Volgende stap
36
+ </button>
37
+ `,
38
+ })
39
+
40
+ export const Default: Story = {
41
+ args: {
42
+ steps: 5,
43
+ currentStep: 0,
44
+ },
45
+ }
46
+
47
+ export const WithTitles: Story = {
48
+ render: interactiveRender,
49
+ args: {
50
+ steps: 5,
51
+ currentStep: 0,
52
+ stepTitles: ['Step 1', 'Step 2', 'Step 3', 'Step 4', 'Step 5'],
53
+ },
54
+ }
55
+
56
+ export const ManySteps: Story = {
57
+ render: interactiveRender,
58
+ args: {
59
+ steps: 12,
60
+ currentStep: 4,
61
+ stepTitles: [
62
+ 'Soort',
63
+ 'Type',
64
+ 'Inhoud',
65
+ 'Template',
66
+ 'Bewerk',
67
+ 'Inplannen',
68
+ 'Doelgroep',
69
+ 'Kanalen',
70
+ 'Budget',
71
+ 'Planning',
72
+ 'Controle',
73
+ 'Verzenden',
74
+ ],
75
+ },
76
+ }
77
+
78
+ export const Percentage: Story = {
79
+ args: {
80
+ variant: 'percentage',
81
+ steps: 101,
82
+ currentStep: 90,
83
+ },
84
+ }
85
+
86
+ export const PercentageLow: Story = {
87
+ args: {
88
+ variant: 'percentage',
89
+ steps: 101,
90
+ currentStep: 5,
91
+ },
92
+ }
93
+
94
+ export const PercentageHalf: Story = {
95
+ args: {
96
+ variant: 'percentage',
97
+ steps: 101,
98
+ currentStep: 50,
99
+ },
100
+ }