@nexxtmove/ui 2.6.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/dist/components/editor/molecules/EditorColor/EditorColor.vue.d.ts +7 -5
  2. package/dist/components/editor/molecules/EditorColor/_ColorField.vue.d.ts +4 -1
  3. package/dist/components/organisms/ColorPicker/ColorPicker.vue.d.ts +3 -1
  4. package/dist/index.js +95 -172
  5. package/dist/nuxt.js +0 -1
  6. package/package.json +38 -36
  7. package/src/attribute-contract.test.ts +151 -0
  8. package/src/components/atoms/AnimatedNumber/AnimatedNumber.stories.ts +36 -0
  9. package/src/components/atoms/AnimatedNumber/AnimatedNumber.test.ts +21 -0
  10. package/src/components/atoms/Avatar/Avatar.stories.ts +122 -0
  11. package/src/components/atoms/Avatar/Avatar.test.ts +134 -0
  12. package/src/components/atoms/Button/Button.stories.ts +290 -0
  13. package/src/components/atoms/Button/Button.test.ts +406 -0
  14. package/src/components/atoms/Card/Card.stories.ts +45 -0
  15. package/src/components/atoms/Card/Card.test.ts +46 -0
  16. package/src/components/atoms/Checkbox/Checkbox.stories.ts +35 -0
  17. package/src/components/atoms/Checkbox/Checkbox.test.ts +55 -0
  18. package/src/components/atoms/Chip/Chip.stories.ts +274 -0
  19. package/src/components/atoms/Chip/Chip.test.ts +230 -0
  20. package/src/components/atoms/EnergyLabel/EnergyLabel.stories.ts +189 -0
  21. package/src/components/atoms/EnergyLabel/EnergyLabel.test.ts +100 -0
  22. package/src/components/atoms/FloatingPanel/FloatingPanel.stories.ts +677 -0
  23. package/src/components/atoms/FloatingPanel/FloatingPanel.test.ts +864 -0
  24. package/src/components/atoms/Icon/Icon.stories.ts +63 -0
  25. package/src/components/atoms/Icon/Icon.test.ts +89 -0
  26. package/src/components/atoms/Loader/Loader.stories.ts +36 -0
  27. package/src/components/atoms/Loader/Loader.test.ts +40 -0
  28. package/src/components/atoms/PickerItem/PickerItem.stories.ts +172 -0
  29. package/src/components/atoms/PickerItem/PickerItem.test.ts +142 -0
  30. package/src/components/atoms/PickerPreview/PickerPreview.stories.ts +183 -0
  31. package/src/components/atoms/PickerPreview/PickerPreview.test.ts +137 -0
  32. package/src/components/atoms/PresenceIndicator/PresenceIndicator.stories.ts +21 -0
  33. package/src/components/atoms/PropertyListing/PropertyListing.stories.ts +38 -0
  34. package/src/components/atoms/PropertyListing/PropertyListing.test.ts +46 -0
  35. package/src/components/atoms/SelectableOption/SelectableOption.stories.ts +131 -0
  36. package/src/components/atoms/SelectableOption/SelectableOption.test.ts +96 -0
  37. package/src/components/atoms/Skeleton/Skeleton.stories.ts +27 -0
  38. package/src/components/atoms/Skeleton/Skeleton.test.ts +28 -0
  39. package/src/components/atoms/Slider/Slider.stories.ts +151 -0
  40. package/src/components/atoms/Slider/Slider.test.ts +191 -0
  41. package/src/components/atoms/Squircle/Squircle.stories.ts +114 -0
  42. package/src/components/atoms/Squircle/Squircle.test.ts +70 -0
  43. package/src/components/atoms/Switch/Switch.stories.ts +91 -0
  44. package/src/components/atoms/Switch/Switch.test.ts +183 -0
  45. package/src/components/atoms/Tooltip/Tooltip.stories.ts +158 -0
  46. package/src/components/atoms/Tooltip/Tooltip.test.ts +134 -0
  47. package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.stories.ts +124 -0
  48. package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.test.ts +464 -0
  49. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.stories.ts +266 -0
  50. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.test.ts +421 -0
  51. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.stories.ts +365 -0
  52. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.test.ts +484 -0
  53. package/src/components/editor/composables/useRadioGrid.test.ts +261 -0
  54. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.stories.ts +73 -0
  55. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.test.ts +189 -0
  56. package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.stories.ts +309 -0
  57. package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.test.ts +359 -0
  58. package/src/components/editor/molecules/EditorAnchor/EditorAnchor.stories.ts +138 -0
  59. package/src/components/editor/molecules/EditorAnchor/EditorAnchor.test.ts +344 -0
  60. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.stories.ts +87 -0
  61. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.test.ts +262 -0
  62. package/src/components/editor/molecules/EditorBlur/EditorBlur.stories.ts +115 -0
  63. package/src/components/editor/molecules/EditorBlur/EditorBlur.test.ts +243 -0
  64. package/src/components/editor/molecules/EditorBorder/EditorBorder.stories.ts +187 -0
  65. package/src/components/editor/molecules/EditorBorder/EditorBorder.test.ts +456 -0
  66. package/src/components/editor/molecules/EditorColor/EditorColor.stories.ts +633 -0
  67. package/src/components/editor/molecules/EditorColor/EditorColor.test.ts +776 -0
  68. package/src/components/editor/molecules/EditorColor/EditorColor.vue +7 -5
  69. package/src/components/editor/molecules/EditorColor/EditorColorGradient.test.ts +832 -0
  70. package/src/components/editor/molecules/EditorColor/_ColorField.vue +20 -15
  71. package/src/components/editor/molecules/EditorColor/gradientClass.test.ts +303 -0
  72. package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.stories.ts +72 -0
  73. package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.test.ts +124 -0
  74. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.stories.ts +46 -0
  75. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.test.ts +97 -0
  76. package/src/components/editor/molecules/EditorFontSize/EditorFontSize.stories.ts +248 -0
  77. package/src/components/editor/molecules/EditorFontSize/EditorFontSize.test.ts +288 -0
  78. package/src/components/editor/molecules/EditorGap/EditorGap.stories.ts +129 -0
  79. package/src/components/editor/molecules/EditorGap/EditorGap.test.ts +253 -0
  80. package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.stories.ts +68 -0
  81. package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.test.ts +150 -0
  82. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.stories.ts +71 -0
  83. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.test.ts +161 -0
  84. package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.stories.ts +204 -0
  85. package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.test.ts +238 -0
  86. package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.stories.ts +257 -0
  87. package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.test.ts +286 -0
  88. package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.stories.ts +135 -0
  89. package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.test.ts +230 -0
  90. package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.stories.ts +154 -0
  91. package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.test.ts +112 -0
  92. package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.stories.ts +206 -0
  93. package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.test.ts +332 -0
  94. package/src/components/editor/molecules/EditorPadding/EditorPadding.stories.ts +374 -0
  95. package/src/components/editor/molecules/EditorPadding/EditorPadding.test.ts +712 -0
  96. package/src/components/editor/molecules/EditorRadius/EditorRadius.stories.ts +64 -0
  97. package/src/components/editor/molecules/EditorRadius/EditorRadius.test.ts +137 -0
  98. package/src/components/editor/molecules/EditorShadow/EditorShadow.stories.ts +103 -0
  99. package/src/components/editor/molecules/EditorShadow/EditorShadow.test.ts +168 -0
  100. package/src/components/editor/molecules/EditorSize/EditorSize.stories.ts +84 -0
  101. package/src/components/editor/molecules/EditorSize/EditorSize.test.ts +253 -0
  102. package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.stories.ts +49 -0
  103. package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.test.ts +43 -0
  104. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.stories.ts +69 -0
  105. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.test.ts +155 -0
  106. package/src/components/molecules/Announcement/Announcement.stories.ts +224 -0
  107. package/src/components/molecules/Announcement/Announcement.test.ts +161 -0
  108. package/src/components/molecules/ContactInfoCard/ContactInfoCard.stories.ts +99 -0
  109. package/src/components/molecules/ContactInfoCard/ContactInfoCard.test.ts +70 -0
  110. package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.stories.ts +148 -0
  111. package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.test.ts +271 -0
  112. package/src/components/molecules/DoughnutChart/DoughnutChart.stories.ts +45 -0
  113. package/src/components/molecules/DoughnutChart/DoughnutChart.test.ts +205 -0
  114. package/src/components/molecules/InfoBlock/InfoBlock.stories.ts +84 -0
  115. package/src/components/molecules/InfoBlock/InfoBlock.test.ts +101 -0
  116. package/src/components/molecules/InputField/InputField.stories.ts +117 -0
  117. package/src/components/molecules/InputField/InputField.test.ts +138 -0
  118. package/src/components/molecules/InputSelect/InputSelect.stories.ts +116 -0
  119. package/src/components/molecules/InputSelect/InputSelect.test.ts +127 -0
  120. package/src/components/molecules/ModalFooter/ModalFooter.stories.ts +204 -0
  121. package/src/components/molecules/ModalFooter/ModalFooter.test.ts +276 -0
  122. package/src/components/molecules/PageCard/PageCard.stories.ts +100 -0
  123. package/src/components/molecules/PageCard/PageCard.test.ts +91 -0
  124. package/src/components/molecules/PageContainer/PageContainer.stories.ts +90 -0
  125. package/src/components/molecules/PageContainer/PageContainer.test.ts +50 -0
  126. package/src/components/molecules/PageMockup/PageMockup.stories.ts +38 -0
  127. package/src/components/molecules/PageMockup/PageMockup.test.ts +20 -0
  128. package/src/components/molecules/PageTypeTile/PageTypeTile.stories.ts +63 -0
  129. package/src/components/molecules/PageTypeTile/PageTypeTile.test.ts +41 -0
  130. package/src/components/molecules/Pagination/Pagination.stories.ts +72 -0
  131. package/src/components/molecules/Pagination/Pagination.test.ts +130 -0
  132. package/src/components/molecules/PickerGroup/PickerGroup.stories.ts +90 -0
  133. package/src/components/molecules/PickerGroup/PickerGroup.test.ts +145 -0
  134. package/src/components/molecules/PickerList/PickerList.stories.ts +436 -0
  135. package/src/components/molecules/PickerList/PickerList.test.ts +870 -0
  136. package/src/components/molecules/PlaceholderInput/PlaceholderInput.stories.ts +48 -0
  137. package/src/components/molecules/PlaceholderInput/PlaceholderInput.test.ts +107 -0
  138. package/src/components/molecules/PropertyCard/PropertyCard.stories.ts +46 -0
  139. package/src/components/molecules/PropertyCard/PropertyCard.test.ts +59 -0
  140. package/src/components/molecules/Select/Select.stories.ts +264 -0
  141. package/src/components/molecules/Select/Select.test.ts +315 -0
  142. package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.stories.ts +84 -0
  143. package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.test.ts +103 -0
  144. package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.stories.ts +11 -0
  145. package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.test.ts +38 -0
  146. package/src/components/molecules/SocialIcons/SocialIcons.stories.ts +28 -0
  147. package/src/components/molecules/SocialIcons/SocialIcons.test.ts +58 -0
  148. package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.stories.ts +11 -0
  149. package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.test.ts +116 -0
  150. package/src/components/molecules/SocialMediaType/SocialMediaType.stories.ts +37 -0
  151. package/src/components/molecules/SocialMediaType/SocialMediaType.test.ts +126 -0
  152. package/src/components/molecules/Source/Source.stories.ts +35 -0
  153. package/src/components/molecules/Source/Source.test.ts +45 -0
  154. package/src/components/molecules/TabbedContent/TabbedContent.stories.ts +96 -0
  155. package/src/components/molecules/TabbedContent/TabbedContent.test.ts +109 -0
  156. package/src/components/molecules/Tabs/Tabs.stories.ts +230 -0
  157. package/src/components/molecules/Tabs/Tabs.test.ts +280 -0
  158. package/src/components/molecules/TemplateCard/TemplateCard.stories.ts +327 -0
  159. package/src/components/molecules/TemplateCard/TemplateCard.test.ts +339 -0
  160. package/src/components/molecules/Textarea/Textarea.stories.ts +216 -0
  161. package/src/components/molecules/Textarea/Textarea.test.ts +191 -0
  162. package/src/components/molecules/TimelineEvent/TimelineEvent.stories.ts +319 -0
  163. package/src/components/molecules/TimelineEvent/TimelineEvent.test.ts +224 -0
  164. package/src/components/molecules/TimelineLanes/TimelineLanes.stories.ts +29 -0
  165. package/src/components/molecules/TimelineLanes/TimelineLanes.test.ts +30 -0
  166. package/src/components/molecules/Toast/Toast.stories.ts +110 -0
  167. package/src/components/molecules/Toast/Toast.test.ts +103 -0
  168. package/src/components/molecules/ToggleGroup/ToggleGroup.stories.ts +297 -0
  169. package/src/components/molecules/ToggleGroup/ToggleGroup.test.ts +578 -0
  170. package/src/components/molecules/Tree/Tree.stories.ts +884 -0
  171. package/src/components/molecules/Tree/Tree.test.ts +1247 -0
  172. package/src/components/molecules/Tree/tree-drop.test.ts +223 -0
  173. package/src/components/organisms/Calendar/Calendar.stories.ts +108 -0
  174. package/src/components/organisms/Calendar/Calendar.test.ts +279 -0
  175. package/src/components/organisms/Calendar/_CalendarDayView.test.ts +104 -0
  176. package/src/components/organisms/Calendar/_CalendarHeader.test.ts +86 -0
  177. package/src/components/organisms/Calendar/_CalendarMonthView.test.ts +52 -0
  178. package/src/components/organisms/Calendar/_CalendarTimePicker.test.ts +89 -0
  179. package/src/components/organisms/Calendar/_CalendarYearView.test.ts +56 -0
  180. package/src/components/organisms/ColorPicker/ColorPicker.stories.ts +229 -0
  181. package/src/components/organisms/ColorPicker/ColorPicker.test.ts +696 -0
  182. package/src/components/organisms/ColorPicker/ColorPicker.vue +12 -3
  183. package/src/components/organisms/ColorPicker/gradient.test.ts +67 -0
  184. package/src/components/organisms/ColorPicker/gradientGeometry.test.ts +206 -0
  185. package/src/components/organisms/Combobox/Combobox.stories.ts +234 -0
  186. package/src/components/organisms/Combobox/Combobox.test.ts +512 -0
  187. package/src/components/organisms/Combobox/Combobox.vue +1 -1
  188. package/src/components/organisms/ComboboxPanel/ComboboxPanel.stories.ts +158 -0
  189. package/src/components/organisms/ComboboxPanel/ComboboxPanel.test.ts +524 -0
  190. package/src/components/organisms/ComboboxPanel/ComboboxPanel.vue +1 -1
  191. package/src/components/organisms/CountrySelect/CountrySelect.stories.ts +139 -0
  192. package/src/components/organisms/CountrySelect/CountrySelect.test.ts +149 -0
  193. package/src/components/organisms/DatePicker/DatePicker.stories.ts +163 -0
  194. package/src/components/organisms/DatePicker/DatePicker.test.ts +214 -0
  195. package/src/components/organisms/DropdownButton/DropdownButton.stories.ts +123 -0
  196. package/src/components/organisms/DropdownButton/DropdownButton.test.ts +379 -0
  197. package/src/components/organisms/FontPicker/FontPicker.stories.ts +476 -0
  198. package/src/components/organisms/FontPicker/FontPicker.test.ts +873 -0
  199. package/src/components/organisms/Header/Header.stories.ts +75 -0
  200. package/src/components/organisms/Header/Header.test.ts +233 -0
  201. package/src/components/organisms/IconPicker/IconPicker.stories.ts +734 -0
  202. package/src/components/organisms/IconPicker/IconPicker.test.ts +669 -0
  203. package/src/components/organisms/IconPicker/_IconPickerGrid.test.ts +184 -0
  204. package/src/components/organisms/IconPicker/_IconPickerRail.test.ts +102 -0
  205. package/src/components/organisms/IconPicker/useIconPickerNavigation.test.ts +215 -0
  206. package/src/components/organisms/Modal/Modal.stories.ts +350 -0
  207. package/src/components/organisms/Modal/Modal.test.ts +275 -0
  208. package/src/components/organisms/PickerPanel/PickerPanel.stories.ts +452 -0
  209. package/src/components/organisms/PickerPanel/PickerPanel.test.ts +358 -0
  210. package/src/components/organisms/ProgressBar/ProgressBar.stories.ts +100 -0
  211. package/src/components/organisms/ProgressBar/ProgressBar.test.ts +417 -0
  212. package/src/components/organisms/ReferenceCard/ReferenceCard.stories.ts +191 -0
  213. package/src/components/organisms/ReferenceCard/ReferenceCard.test.ts +165 -0
  214. package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.stories.ts +65 -0
  215. package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.test.ts +466 -0
  216. package/src/components/organisms/StepperHeader/StepperHeader.stories.ts +91 -0
  217. package/src/components/organisms/StepperHeader/StepperHeader.test.ts +279 -0
  218. package/src/components/organisms/Table/Table.stories.ts +332 -0
  219. package/src/components/organisms/Table/Table.test.ts +320 -0
  220. package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.stories.ts +198 -0
  221. package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.test.ts +283 -0
  222. package/src/components/organisms/TimelineTrack/TimelineTrack.stories.ts +120 -0
  223. package/src/components/organisms/TimelineTrack/TimelineTrack.test.ts +91 -0
  224. package/src/components/organisms/Toaster/Toaster.stories.ts +51 -0
  225. package/src/components/organisms/Toaster/Toaster.test.ts +75 -0
  226. package/src/components/organisms/VideoControls/VideoControls.stories.ts +792 -0
  227. package/src/components/organisms/VideoControls/VideoControls.test.ts +1056 -0
  228. package/src/components/organisms/VideoPlayer/VideoPlayer.stories.ts +327 -0
  229. package/src/components/organisms/VideoPlayer/VideoPlayer.test.ts +439 -0
@@ -0,0 +1,230 @@
1
+ import { ref } from 'vue'
2
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
3
+ import { expect, userEvent, waitFor, within } from 'storybook/test'
4
+ import Tabs, { type NexxtTabItem } from './Tabs.vue'
5
+
6
+ export default {
7
+ title: 'Components/Molecules/Tabs',
8
+ component: Tabs,
9
+ decorators: [
10
+ () => ({
11
+ template:
12
+ '<div style="min-height: 250px; padding: 16px; display: flex; align-items: center; justify-content: center; background-color: #F5F5F5;"><story /></div>',
13
+ }),
14
+ ],
15
+ } satisfies Meta<typeof Tabs>
16
+
17
+ type Story = StoryObj<typeof Tabs>
18
+
19
+ export const Default: Story = {
20
+ render: (args) => ({
21
+ components: { Tabs },
22
+ setup() {
23
+ const selected = ref('')
24
+ return { args, selected }
25
+ },
26
+ template: `<Tabs v-bind="args" v-model="selected" />`,
27
+ }),
28
+ args: {
29
+ tabs: ['Overzicht', 'Activiteiten & Communicatie', 'Tijdlijn'],
30
+ },
31
+ }
32
+
33
+ export const TwoTabs: Story = {
34
+ render: (args) => ({
35
+ components: { Tabs },
36
+ setup() {
37
+ const selected = ref(args.tabs[0])
38
+ return { args, selected }
39
+ },
40
+ template: `<Tabs v-bind="args" v-model="selected" />`,
41
+ }),
42
+ args: {
43
+ tabs: ['Active', 'Archived'],
44
+ },
45
+ }
46
+
47
+ export const ManyTabs: Story = {
48
+ render: (args) => ({
49
+ components: { Tabs },
50
+ setup() {
51
+ const selected = ref(args.tabs[0])
52
+ return { args, selected }
53
+ },
54
+ template: `<Tabs v-bind="args" v-model="selected" />`,
55
+ }),
56
+ args: {
57
+ tabs: ['Dashboard', 'Analytics', 'Reports', 'Users', 'Settings'],
58
+ },
59
+ }
60
+
61
+ export const WithIcons: Story = {
62
+ render: (args) => ({
63
+ components: { Tabs },
64
+ setup() {
65
+ const selected = ref('E-mail')
66
+ return { args, selected }
67
+ },
68
+ template: `<Tabs v-bind="args" v-model="selected" />`,
69
+ }),
70
+ args: {
71
+ tabs: [
72
+ { label: 'E-mail', icon: 'envelope' },
73
+ { label: 'WhatsApp', icon: 'whatsapp' },
74
+ ],
75
+ },
76
+ }
77
+
78
+ export const MixedStringAndIconTabs: Story = {
79
+ render: (args) => ({
80
+ components: { Tabs },
81
+ setup() {
82
+ const selected = ref('Overzicht')
83
+ return { args, selected }
84
+ },
85
+ template: `<Tabs v-bind="args" v-model="selected" />`,
86
+ }),
87
+ args: {
88
+ tabs: ['Overzicht', { label: 'E-mail', icon: 'envelope' }, { label: 'Funda', icon: 'funda' }],
89
+ },
90
+ }
91
+
92
+ const DEVICES: NexxtTabItem[] = [
93
+ { label: 'Desktop', icon: 'desktop' },
94
+ { label: 'Tablet', icon: 'tablet-screen-button' },
95
+ { label: 'Mobiel', icon: 'mobile-screen' },
96
+ ]
97
+
98
+ /**
99
+ * `iconOnly` shows every tab as its icon alone. The label stays the `v-model` value and, in a
100
+ * visually hidden span, the tab's accessible name.
101
+ */
102
+ export const IconOnly: Story = {
103
+ render: (args) => ({
104
+ components: { Tabs },
105
+ setup() {
106
+ const selected = ref('Desktop')
107
+ return { args, selected }
108
+ },
109
+ template: `<Tabs v-bind="args" v-model="selected" />`,
110
+ }),
111
+ args: {
112
+ tabs: DEVICES,
113
+ tablistLabel: 'Apparaat',
114
+ iconOnly: true,
115
+ },
116
+ }
117
+
118
+ /**
119
+ * A single tab can opt in or out with its own `iconOnly`, which wins over the tabs-wide prop.
120
+ */
121
+ export const IconOnlyPerTab: Story = {
122
+ render: (args) => ({
123
+ components: { Tabs },
124
+ setup() {
125
+ const selected = ref('Overzicht')
126
+ return { args, selected }
127
+ },
128
+ template: `<Tabs v-bind="args" v-model="selected" />`,
129
+ }),
130
+ args: {
131
+ tabs: [
132
+ 'Overzicht',
133
+ { label: 'E-mail', icon: 'envelope', iconOnly: true },
134
+ { label: 'WhatsApp', icon: 'whatsapp', iconOnly: true },
135
+ ],
136
+ tablistLabel: 'Kanaal',
137
+ },
138
+ }
139
+
140
+ const rect = (el: Element) => el.getBoundingClientRect()
141
+
142
+ /** The pill and the white copy both sit exactly on the selected tab; every copy matches its tab. */
143
+ const expectAligned = async (tablist: HTMLElement) => {
144
+ const tabs = within(tablist).getAllByRole('tab')
145
+ const pill = tablist.firstElementChild!
146
+ const copies = [...tablist.lastElementChild!.children]
147
+ const selectedTab = tabs.find((tab) => tab.getAttribute('aria-selected') === 'true')!
148
+
149
+ await waitFor(
150
+ () => {
151
+ expect(rect(pill).left).toBeCloseTo(rect(selectedTab).left, 1)
152
+ expect(rect(pill).width).toBeCloseTo(rect(selectedTab).width, 1)
153
+ },
154
+ { timeout: 2000 },
155
+ )
156
+ copies.forEach((copy, index) => {
157
+ expect(rect(copy).left).toBeCloseTo(rect(tabs[index]).left, 1)
158
+ expect(rect(copy).width).toBeCloseTo(rect(tabs[index]).width, 1)
159
+ })
160
+ }
161
+
162
+ /**
163
+ * Switching `iconOnly` re-measures the pill, so it lands on the selected tab in either mode. In
164
+ * the app this follows a media query; here a checkbox stands in for it.
165
+ */
166
+ export const IconOnlyToggle: Story = {
167
+ render: (args) => ({
168
+ components: { Tabs },
169
+ setup() {
170
+ const selected = ref('Tablet')
171
+ const iconOnly = ref(true)
172
+ return { args, selected, iconOnly }
173
+ },
174
+ template: `
175
+ <div class="flex flex-col items-center gap-4">
176
+ <label class="flex items-center gap-2 small-normal text-gray-950">
177
+ <input v-model="iconOnly" type="checkbox" />
178
+ Alleen iconen
179
+ </label>
180
+ <Tabs v-bind="args" v-model="selected" :icon-only="iconOnly" />
181
+ </div>
182
+ `,
183
+ }),
184
+ args: {
185
+ tabs: DEVICES,
186
+ tablistLabel: 'Apparaat',
187
+ },
188
+ play: async ({ canvasElement }) => {
189
+ const canvas = within(canvasElement)
190
+ const tablist = canvas.getByRole('tablist', { name: 'Apparaat' })
191
+ const toggle = canvas.getByRole('checkbox', { name: 'Alleen iconen' })
192
+
193
+ await document.fonts.ready
194
+
195
+ // icon-only: a near-square button per tab, still named after its label
196
+ for (const name of ['Desktop', 'Tablet', 'Mobiel']) {
197
+ const tab = canvas.getByRole('tab', { name })
198
+ expect(rect(tab).height).toBe(36)
199
+ expect(rect(tab).width).toBeLessThanOrEqual(48)
200
+ }
201
+ await expectAligned(tablist)
202
+ const compactWidth = rect(canvas.getByRole('tab', { name: 'Tablet' })).width
203
+
204
+ await userEvent.click(toggle)
205
+ await waitFor(() =>
206
+ expect(rect(canvas.getByRole('tab', { name: 'Tablet' })).width).toBeGreaterThan(compactWidth),
207
+ )
208
+ await expectAligned(tablist)
209
+
210
+ await userEvent.click(toggle)
211
+ await expectAligned(tablist)
212
+
213
+ // the keyboard and v-model work on the labels, as before
214
+ canvas.getByRole('tab', { name: 'Tablet' }).focus()
215
+ await userEvent.keyboard('{ArrowRight}')
216
+ await expect(canvas.getByRole('tab', { name: 'Mobiel' })).toHaveAttribute(
217
+ 'aria-selected',
218
+ 'true',
219
+ )
220
+ await userEvent.keyboard('{Home}')
221
+ await expect(canvas.getByRole('tab', { name: 'Desktop' })).toHaveFocus()
222
+ await expectAligned(tablist)
223
+ await userEvent.keyboard('{End}')
224
+ await expect(canvas.getByRole('tab', { name: 'Mobiel' })).toHaveAttribute(
225
+ 'aria-selected',
226
+ 'true',
227
+ )
228
+ await expectAligned(tablist)
229
+ },
230
+ }
@@ -0,0 +1,280 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
2
+ import { flushPromises, mount } from '@vue/test-utils'
3
+ import Tabs, { type NexxtTabItem } from './Tabs.vue'
4
+
5
+ beforeEach(() => {
6
+ Object.defineProperty(document, 'fonts', {
7
+ value: { ready: Promise.resolve() },
8
+ configurable: true,
9
+ })
10
+ })
11
+
12
+ describe('Tabs', () => {
13
+ it('renders all tabs', () => {
14
+ const wrapper = mount(Tabs, { props: { tabs: ['One', 'Two', 'Three'] } })
15
+
16
+ const buttons = wrapper.findAll('button')
17
+
18
+ expect(buttons).toHaveLength(3)
19
+ expect(buttons[0].text()).toBe('One')
20
+ expect(buttons[1].text()).toBe('Two')
21
+ expect(buttons[2].text()).toBe('Three')
22
+ })
23
+
24
+ it('marks the selected tab with aria-selected', () => {
25
+ const wrapper = mount(Tabs, {
26
+ props: { tabs: ['One', 'Two'], modelValue: 'Two' },
27
+ })
28
+
29
+ expect(wrapper.findAll('button')[1].attributes('aria-selected')).toBe('true')
30
+ expect(wrapper.findAll('button')[0].attributes('aria-selected')).toBe('false')
31
+ })
32
+
33
+ it('keeps every tab below the white overlay so the pill wipe stays smooth', () => {
34
+ const wrapper = mount(Tabs, {
35
+ props: { tabs: ['One', 'Two'], modelValue: 'One' },
36
+ })
37
+
38
+ // a tab painted above the z-20 overlay would flip to dark text instantly mid-transition
39
+ for (const button of wrapper.findAll('button')) {
40
+ expect(button.classes()).toContain('z-10')
41
+ }
42
+ })
43
+
44
+ it('emits update:modelValue when a tab is clicked', async () => {
45
+ const wrapper = mount(Tabs, {
46
+ props: { tabs: ['One', 'Two'], modelValue: 'One' },
47
+ })
48
+
49
+ await wrapper.findAll('button')[1].trigger('click')
50
+
51
+ expect(wrapper.emitted('update:modelValue')).toBeTruthy()
52
+ expect(wrapper.emitted('update:modelValue')![0]).toEqual(['Two'])
53
+ })
54
+
55
+ it('updates the active tab after clicking', async () => {
56
+ const wrapper = mount(Tabs, {
57
+ props: {
58
+ tabs: ['One', 'Two'],
59
+ modelValue: 'One',
60
+ 'onUpdate:modelValue': (val: string) => wrapper.setProps({ modelValue: val }),
61
+ },
62
+ })
63
+
64
+ await wrapper.findAll('button')[1].trigger('click')
65
+
66
+ expect(wrapper.findAll('button')[1].attributes('aria-selected')).toBe('true')
67
+ expect(wrapper.findAll('button')[0].attributes('aria-selected')).toBe('false')
68
+ })
69
+
70
+ it('accepts object tabs with icons alongside plain strings', () => {
71
+ const wrapper = mount(Tabs, {
72
+ props: {
73
+ tabs: [{ label: 'E-mail', icon: 'envelope' as const }, { label: 'WhatsApp' }, 'Overig'],
74
+ modelValue: 'E-mail',
75
+ },
76
+ })
77
+
78
+ const buttons = wrapper.findAll('button')
79
+
80
+ expect(buttons).toHaveLength(3)
81
+ expect(buttons[0].text()).toBe('E-mail')
82
+ expect(buttons[1].text()).toBe('WhatsApp')
83
+ expect(buttons[2].text()).toBe('Overig')
84
+ expect(buttons[0].find('.fa-envelope').exists()).toBe(true)
85
+ expect(buttons[1].find('i').exists()).toBe(false)
86
+ })
87
+
88
+ it('renders the icon in the clipped overlay too, so the pill stays aligned', () => {
89
+ const wrapper = mount(Tabs, {
90
+ props: { tabs: [{ label: 'E-mail', icon: 'envelope' as const }], modelValue: 'E-mail' },
91
+ })
92
+
93
+ const overlay = wrapper.find('[aria-hidden="true"].absolute.inset-0')
94
+
95
+ expect(overlay.find('.fa-envelope').exists()).toBe(true)
96
+ expect(overlay.text()).toBe('E-mail')
97
+ })
98
+
99
+ it('emits the label when an object tab is clicked', async () => {
100
+ const wrapper = mount(Tabs, {
101
+ props: {
102
+ tabs: [{ label: 'E-mail', icon: 'envelope' as const }, { label: 'WhatsApp' }],
103
+ modelValue: 'E-mail',
104
+ },
105
+ })
106
+
107
+ await wrapper.findAll('button')[1].trigger('click')
108
+
109
+ expect(wrapper.emitted('update:modelValue')![0]).toEqual(['WhatsApp'])
110
+ })
111
+
112
+ it('moves selection with arrow keys across object tabs', async () => {
113
+ const wrapper = mount(Tabs, {
114
+ props: {
115
+ tabs: ['One', { label: 'Two', icon: 'envelope' as const }],
116
+ modelValue: 'One',
117
+ },
118
+ })
119
+
120
+ await wrapper.findAll('button')[0].trigger('keydown', { key: 'ArrowRight' })
121
+
122
+ expect(wrapper.emitted('update:modelValue')![0]).toEqual(['Two'])
123
+ })
124
+
125
+ it('renders the slider element', () => {
126
+ const wrapper = mount(Tabs, { props: { tabs: ['One', 'Two'] } })
127
+
128
+ expect(wrapper.find('.bg-cornflower-blue-500').exists()).toBe(true)
129
+ })
130
+
131
+ describe('iconOnly', () => {
132
+ const devices: NexxtTabItem[] = [
133
+ { label: 'Desktop', icon: 'desktop' },
134
+ { label: 'Tablet', icon: 'tablet-screen-button' },
135
+ { label: 'Mobiel', icon: 'mobile-screen' },
136
+ ]
137
+
138
+ const overlayOf = (wrapper: ReturnType<typeof mount>) =>
139
+ wrapper.find('[aria-hidden="true"].absolute.inset-0')
140
+
141
+ let warn: ReturnType<typeof vi.spyOn>
142
+
143
+ beforeEach(() => {
144
+ warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
145
+ })
146
+
147
+ afterEach(() => {
148
+ warn.mockRestore()
149
+ })
150
+
151
+ it('keeps the label only as visually hidden text, the name of the tab', () => {
152
+ const wrapper = mount(Tabs, {
153
+ props: { tabs: devices, modelValue: 'Desktop', iconOnly: true },
154
+ })
155
+
156
+ for (const [index, button] of wrapper.findAll('button').entries()) {
157
+ const hidden = button.find('.sr-only')
158
+ expect(hidden.text()).toBe(devices[index].label)
159
+ expect(button.text()).toBe(hidden.text())
160
+ expect(button.find('i').attributes('aria-hidden')).toBe('true')
161
+ expect(button.attributes('aria-label')).toBeUndefined()
162
+ }
163
+ })
164
+
165
+ it('shows only the icons in the white overlay, so it matches the buttons', () => {
166
+ const wrapper = mount(Tabs, {
167
+ props: { tabs: devices, modelValue: 'Desktop', iconOnly: true },
168
+ })
169
+
170
+ const overlay = overlayOf(wrapper)
171
+
172
+ expect(overlay.text()).toBe('')
173
+ expect(overlay.findAll('i')).toHaveLength(3)
174
+ // a copy is as wide as its button only when it carries the same spacing
175
+ wrapper.findAll('button').forEach((button, index) => {
176
+ const copy = overlay.findAll('span')[index]
177
+ const spacing = ['gap-2', 'px-3', 'px-4']
178
+ expect(copy.classes().filter((name) => spacing.includes(name))).toEqual(
179
+ button.classes().filter((name) => spacing.includes(name)),
180
+ )
181
+ })
182
+ })
183
+
184
+ it('renders the same markup with iconOnly false as without the prop', () => {
185
+ const without = mount(Tabs, { props: { tabs: devices, modelValue: 'Tablet' } })
186
+ const off = mount(Tabs, { props: { tabs: devices, modelValue: 'Tablet', iconOnly: false } })
187
+
188
+ expect(off.element.outerHTML).toBe(without.element.outerHTML)
189
+ expect(without.find('.sr-only').exists()).toBe(false)
190
+ })
191
+
192
+ it('still emits the label on click and on arrow, Home and End keys', async () => {
193
+ const wrapper = mount(Tabs, {
194
+ props: {
195
+ tabs: devices,
196
+ modelValue: 'Tablet',
197
+ iconOnly: true,
198
+ 'onUpdate:modelValue': (value: string) => wrapper.setProps({ modelValue: value }),
199
+ },
200
+ })
201
+ const buttons = wrapper.findAll('button')
202
+
203
+ await buttons[0].trigger('click')
204
+ await buttons[0].trigger('keydown', { key: 'ArrowLeft' })
205
+ await buttons[2].trigger('keydown', { key: 'Home' })
206
+ await buttons[0].trigger('keydown', { key: 'End' })
207
+
208
+ expect(wrapper.emitted('update:modelValue')).toEqual([
209
+ ['Desktop'],
210
+ ['Mobiel'],
211
+ ['Desktop'],
212
+ ['Mobiel'],
213
+ ])
214
+ expect(buttons[2].attributes('aria-selected')).toBe('true')
215
+ })
216
+
217
+ it('lets a tab opt in or out on its own', () => {
218
+ const wrapper = mount(Tabs, {
219
+ props: {
220
+ tabs: [
221
+ { label: 'Desktop', icon: 'desktop' as const, iconOnly: false },
222
+ { label: 'Tablet', icon: 'tablet-screen-button' as const },
223
+ ],
224
+ iconOnly: true,
225
+ },
226
+ })
227
+ const [desktop, tablet] = wrapper.findAll('button')
228
+
229
+ expect(desktop.find('.sr-only').exists()).toBe(false)
230
+ expect(tablet.find('.sr-only').exists()).toBe(true)
231
+
232
+ const single = mount(Tabs, {
233
+ props: { tabs: [{ label: 'E-mail', icon: 'envelope' as const, iconOnly: true }, 'Overig'] },
234
+ })
235
+ expect(single.findAll('button')[0].find('.sr-only').text()).toBe('E-mail')
236
+ expect(single.findAll('button')[1].find('.sr-only').exists()).toBe(false)
237
+ })
238
+
239
+ it('shows the label of a tab without an icon, and warns about it', () => {
240
+ const wrapper = mount(Tabs, {
241
+ props: { tabs: [{ label: 'Desktop', icon: 'desktop' as const }, 'Overig'], iconOnly: true },
242
+ })
243
+ const plain = wrapper.findAll('button')[1]
244
+
245
+ expect(plain.find('.sr-only').exists()).toBe(false)
246
+ expect(plain.text()).toBe('Overig')
247
+ expect(overlayOf(wrapper).text()).toBe('Overig')
248
+ expect(warn).toHaveBeenCalledTimes(1)
249
+ expect(warn.mock.calls[0][0]).toContain('"Overig"')
250
+ })
251
+
252
+ it('does not warn when every icon-only tab has an icon', () => {
253
+ mount(Tabs, { props: { tabs: devices, iconOnly: true } })
254
+
255
+ expect(warn).not.toHaveBeenCalled()
256
+ })
257
+
258
+ it('re-measures the pill when iconOnly switches', async () => {
259
+ // jsdom has no layout: give each tab a width that follows its mode
260
+ const rect = vi
261
+ .spyOn(HTMLElement.prototype, 'getBoundingClientRect')
262
+ .mockImplementation(function (this: HTMLElement) {
263
+ if (this.getAttribute('role') !== 'tab') return new DOMRect(0, 0, 0, 36)
264
+ const width = this.querySelector('.sr-only') ? 44 : 100
265
+ return new DOMRect(0, 0, width, 36)
266
+ })
267
+ const wrapper = mount(Tabs, { props: { tabs: devices, modelValue: 'Desktop' } })
268
+ const pill = () => wrapper.find('.bg-cornflower-blue-500').attributes('style')
269
+
270
+ await flushPromises()
271
+ expect(pill()).toContain('width: 100px')
272
+
273
+ await wrapper.setProps({ iconOnly: true })
274
+ await flushPromises()
275
+ expect(pill()).toContain('width: 44px')
276
+
277
+ rect.mockRestore()
278
+ })
279
+ })
280
+ })