@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,161 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest'
2
+ import { mount, type VueWrapper } from '@vue/test-utils'
3
+ import EditorJustifyContent, { EDITOR_JUSTIFY_CONTENT_OPTIONS } from './EditorJustifyContent.vue'
4
+
5
+ // ToggleGroup awaits document.fonts.ready on mount, which jsdom does not ship.
6
+ beforeEach(() => {
7
+ Object.defineProperty(document, 'fonts', {
8
+ value: { ready: Promise.resolve() },
9
+ configurable: true,
10
+ })
11
+ })
12
+
13
+ /**
14
+ * The options are icon-only, so their copy lives on the button as the
15
+ * accessible name rather than as visible text.
16
+ */
17
+ const optionNames = (wrapper: VueWrapper) =>
18
+ wrapper.findAll('button').map((button) => button.attributes('aria-label'))
19
+
20
+ describe('EditorJustifyContent', () => {
21
+ // The options sit in a ToggleGroup toolbar; its name is the block label, so
22
+ // a screen reader announces which setting the pressed option belongs to.
23
+ it('names the options toolbar with the block label', async () => {
24
+ const wrapper = mount(EditorJustifyContent, { props: { modelValue: 'justify-start' } })
25
+ expect(wrapper.get('[role="toolbar"]').attributes('aria-label')).toBe('Uitlijning')
26
+
27
+ await wrapper.setProps({ label: 'Custom' })
28
+ expect(wrapper.get('[role="toolbar"]').attributes('aria-label')).toBe('Custom')
29
+ })
30
+
31
+ // No assertions on typography utilities: copy is found by structure or by
32
+ // accessible name, so restyling the block or the toggle group cannot turn
33
+ // these red.
34
+ it('renders the Dutch default label', () => {
35
+ const wrapper = mount(EditorJustifyContent, { props: { modelValue: 'justify-start' } })
36
+
37
+ expect(wrapper.find('p').text()).toBe('Uitlijning')
38
+ })
39
+
40
+ it('renders a button for each of the four options', () => {
41
+ const wrapper = mount(EditorJustifyContent, { props: { modelValue: 'justify-start' } })
42
+
43
+ expect(wrapper.findAll('button')).toHaveLength(4)
44
+ })
45
+
46
+ it('names the options with the Dutch defaults', () => {
47
+ const wrapper = mount(EditorJustifyContent, { props: { modelValue: 'justify-start' } })
48
+
49
+ expect(optionNames(wrapper)).toEqual(['Begin', 'Midden', 'Einde', 'Verdelen'])
50
+ })
51
+
52
+ it('renders a custom label', () => {
53
+ const wrapper = mount(EditorJustifyContent, {
54
+ props: { modelValue: 'justify-start', label: 'Alignment' },
55
+ })
56
+
57
+ expect(wrapper.find('p').text()).toBe('Alignment')
58
+ })
59
+
60
+ it('names the options with custom messages', () => {
61
+ const wrapper = mount(EditorJustifyContent, {
62
+ props: {
63
+ modelValue: 'justify-start',
64
+ messages: { start: 'Start', center: 'Center', end: 'End', between: 'Space between' },
65
+ },
66
+ })
67
+
68
+ expect(optionNames(wrapper)).toEqual(['Start', 'Center', 'End', 'Space between'])
69
+ })
70
+
71
+ it('keeps the untouched defaults when only one message is overridden', () => {
72
+ const wrapper = mount(EditorJustifyContent, {
73
+ props: { modelValue: 'justify-start', messages: { between: 'Space between' } },
74
+ })
75
+
76
+ expect(optionNames(wrapper)).toEqual(['Begin', 'Midden', 'Einde', 'Space between'])
77
+ })
78
+
79
+ it('emits update:modelValue with the Tailwind class of the clicked option', async () => {
80
+ const wrapper = mount(EditorJustifyContent, { props: { modelValue: 'justify-start' } })
81
+
82
+ await wrapper.findAll('button')[2].trigger('click')
83
+
84
+ expect(wrapper.emitted('update:modelValue')).toEqual([['justify-end']])
85
+ })
86
+
87
+ it('renders only icons, no visible option text', () => {
88
+ const wrapper = mount(EditorJustifyContent, { props: { modelValue: 'justify-start' } })
89
+
90
+ // Each icon is rendered twice: once per toggle group layer.
91
+ expect(wrapper.findAll('i')).toHaveLength(8)
92
+ wrapper.findAll('button').forEach((button) => expect(button.text()).toBe(''))
93
+ })
94
+
95
+ it('renders no tooltip: the copy is the accessible name, not a hover overlay', () => {
96
+ const wrapper = mount(EditorJustifyContent, { props: { modelValue: 'justify-start' } })
97
+
98
+ // ToggleGroup only mounts NexxtTooltip -- and with it reka-ui's
99
+ // TooltipProvider and the trigger's data attribute -- for options that
100
+ // carry a `tooltip`.
101
+ expect(wrapper.findAllComponents({ name: 'TooltipProvider' })).toHaveLength(0)
102
+ expect(wrapper.find('[data-grace-area-trigger]').exists()).toBe(false)
103
+ })
104
+
105
+ it('offers all four options when `options` is not given', () => {
106
+ const wrapper = mount(EditorJustifyContent, { props: { modelValue: 'justify-start' } })
107
+
108
+ expect(optionNames(wrapper)).toEqual(['Begin', 'Midden', 'Einde', 'Verdelen'])
109
+ })
110
+
111
+ it('offers only the options passed in `options`', () => {
112
+ const wrapper = mount(EditorJustifyContent, {
113
+ props: {
114
+ modelValue: 'justify-center',
115
+ options: ['justify-start', 'justify-center', 'justify-end'],
116
+ },
117
+ })
118
+
119
+ expect(optionNames(wrapper)).toEqual(['Begin', 'Midden', 'Einde'])
120
+ })
121
+
122
+ it('keeps the canonical order whatever order `options` is in', () => {
123
+ const wrapper = mount(EditorJustifyContent, {
124
+ props: {
125
+ modelValue: 'justify-end',
126
+ options: ['justify-between', 'justify-end', 'justify-start'],
127
+ },
128
+ })
129
+
130
+ expect(optionNames(wrapper)).toEqual(['Begin', 'Einde', 'Verdelen'])
131
+ })
132
+
133
+ it('emits the class of a clicked option from a subset', async () => {
134
+ const wrapper = mount(EditorJustifyContent, {
135
+ props: { modelValue: 'justify-start', options: ['justify-start', 'justify-end'] },
136
+ })
137
+
138
+ await wrapper.findAll('button')[1].trigger('click')
139
+
140
+ expect(wrapper.emitted('update:modelValue')).toEqual([['justify-end']])
141
+ })
142
+
143
+ it('is always visible, so it renders no switch', () => {
144
+ const wrapper = mount(EditorJustifyContent, { props: { modelValue: 'justify-start' } })
145
+
146
+ expect(wrapper.find('input[type="checkbox"]').exists()).toBe(false)
147
+ })
148
+ })
149
+
150
+ // The consumer derives its class matchers from this list, so the list is a
151
+ // contract: any change here changes what the app recognises.
152
+ describe('EDITOR_JUSTIFY_CONTENT_OPTIONS', () => {
153
+ it('offers exactly the agreed options', () => {
154
+ expect(EDITOR_JUSTIFY_CONTENT_OPTIONS).toEqual([
155
+ { token: 'start' },
156
+ { token: 'center' },
157
+ { token: 'end' },
158
+ { token: 'between' },
159
+ ])
160
+ })
161
+ })
@@ -0,0 +1,204 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { expect, fn, userEvent, within } from 'storybook/test'
3
+ import { computed, ref, watch } from 'vue'
4
+ import EditorLetterSpacing, { EDITOR_LETTER_SPACING_STEPS } from './EditorLetterSpacing.vue'
5
+ import type { EditorLetterSpacingValue } from './EditorLetterSpacing.vue'
6
+ import { EDITOR_FONT_SIZE_STEPS } from '../EditorFontSize/EditorFontSize.vue'
7
+ import type { EditorFontSizeValue } from '../EditorFontSize/EditorFontSize.vue'
8
+
9
+ const NO_CLASS = '(geen klasse)'
10
+
11
+ export default {
12
+ title: 'Components/Editor Components/Molecules/Editor Letter Spacing',
13
+ component: EditorLetterSpacing,
14
+ // 312px: the content width of the editor's settings panel
15
+ decorators: [() => ({ template: '<div class="mx-auto w-78"><story /></div>' })],
16
+ args: {
17
+ onPointerup: fn(),
18
+ },
19
+ argTypes: {
20
+ label: { control: 'text' },
21
+ // the base of the readout, and the size of the sample paragraph
22
+ fontSize: { control: 'select', options: EDITOR_FONT_SIZE_STEPS.map(({ token }) => token) },
23
+ // sets the model; the slider writes it back, so the two stay in step
24
+ modelValue: {
25
+ control: 'select',
26
+ options: EDITOR_LETTER_SPACING_STEPS.map(({ token }) => token ?? NO_CLASS),
27
+ mapping: { [NO_CLASS]: null },
28
+ },
29
+ messages: { control: 'object' },
30
+ },
31
+ } satisfies Meta<typeof EditorLetterSpacing>
32
+
33
+ type Story = StoryObj<typeof EditorLetterSpacing>
34
+
35
+ /**
36
+ * The control with a sample paragraph under it. The sample carries the classes the app would put
37
+ * on the element: the `fontSize` token plus the model. They are the real classes, so the sample
38
+ * only changes when they compile.
39
+ */
40
+ const renderLetterSpacing = (args: Story['args']) => ({
41
+ components: { EditorLetterSpacing },
42
+ setup() {
43
+ const letterSpacing = ref<EditorLetterSpacingValue | null>(args?.modelValue ?? null)
44
+ watch(
45
+ () => args?.modelValue,
46
+ (value) => (letterSpacing.value = value ?? null),
47
+ )
48
+ const sampleClass = computed(() => [args?.fontSize ?? 'text-sm', letterSpacing.value])
49
+ return { args, letterSpacing, sampleClass }
50
+ },
51
+ template: `
52
+ <div>
53
+ <EditorLetterSpacing v-bind="args" v-model="letterSpacing" />
54
+ <p class="extra-small-normal text-gray-700">
55
+ model: <code data-testid="model">{{ letterSpacing ?? 'null' }}</code>
56
+ </p>
57
+ <!-- \`wrap-break-word\`: at 9xl a single word is wider than the 312px column -->
58
+ <p data-testid="sample" class="mt-4 text-gray-900 wrap-break-word" :class="sampleClass">
59
+ Een licht appartement op loopafstand van het centrum, met een ruim balkon op het zuiden.
60
+ </p>
61
+ </div>
62
+ `,
63
+ })
64
+
65
+ const thumbOf = (canvasElement: HTMLElement) => within(canvasElement).getByRole('slider')
66
+
67
+ /** The px in the readout, as the thumb announces it: `-0,7 px` is -0.7. */
68
+ const readoutPx = (canvasElement: HTMLElement) => {
69
+ const text = thumbOf(canvasElement).getAttribute('aria-valuetext') ?? ''
70
+ return Number(/^(-?[\d,]+) px$/.exec(text)?.[1].replace(',', '.'))
71
+ }
72
+
73
+ /** The letter spacing the sample renders at, in px, as Chromium computes it; `normal` is 0. */
74
+ const sampleLetterSpacing = (canvasElement: HTMLElement) => {
75
+ const { letterSpacing } = getComputedStyle(within(canvasElement).getByTestId('sample'))
76
+ return letterSpacing === 'normal' ? 0 : parseFloat(letterSpacing)
77
+ }
78
+
79
+ /**
80
+ * Walks every step by keyboard, from the tightest to the widest, and checks the readout against
81
+ * the spacing Chromium renders: the readout is that spacing rounded to one decimal.
82
+ */
83
+ const expectEveryStepMatches = async (canvasElement: HTMLElement) => {
84
+ const thumb = thumbOf(canvasElement)
85
+ thumb.focus()
86
+ await userEvent.keyboard('{Home}')
87
+
88
+ for (const [index, step] of EDITOR_LETTER_SPACING_STEPS.entries()) {
89
+ if (index > 0) await userEvent.keyboard('{ArrowRight}')
90
+ await expect(within(canvasElement).getByTestId('model')).toHaveTextContent(step.token ?? 'null')
91
+ // 0.05 is half a decimal; the margin absorbs the float noise in the computed value
92
+ await expect(
93
+ Math.abs(sampleLetterSpacing(canvasElement) - readoutPx(canvasElement)),
94
+ ).toBeLessThanOrEqual(0.05 + 1e-6)
95
+ }
96
+ }
97
+
98
+ /**
99
+ * The default step: no class, zero spacing, with the thumb in the middle of the track. The play
100
+ * walks every step at text-sm, where they are fractions of a px.
101
+ */
102
+ export const Default: Story = {
103
+ args: { modelValue: null },
104
+ render: renderLetterSpacing,
105
+ play: async ({ canvasElement }) => {
106
+ const thumb = thumbOf(canvasElement)
107
+
108
+ await expect(thumb).toHaveAccessibleName('Letterafstand')
109
+ await expect(thumb).toHaveAttribute('aria-valuetext', '0 px')
110
+ await expect(sampleLetterSpacing(canvasElement)).toBe(0)
111
+
112
+ await expectEveryStepMatches(canvasElement)
113
+ await expect(thumb).toHaveAttribute('aria-valuetext', '1,4 px')
114
+ },
115
+ }
116
+
117
+ /**
118
+ * A heading-sized text, tightened as a large heading usually is: the same em steps are whole px
119
+ * here, which a fixed px scale could never be at both sizes.
120
+ */
121
+ export const LargeText: Story = {
122
+ args: { modelValue: 'tracking-[-0.025em]', fontSize: 'text-6xl' },
123
+ render: renderLetterSpacing,
124
+ play: async ({ canvasElement }) => {
125
+ await expect(thumbOf(canvasElement)).toHaveAttribute('aria-valuetext', '-1,5 px')
126
+ await expect(sampleLetterSpacing(canvasElement)).toBe(-1.5)
127
+
128
+ await expectEveryStepMatches(canvasElement)
129
+ await expect(sampleLetterSpacing(canvasElement)).toBe(6)
130
+ },
131
+ }
132
+
133
+ /**
134
+ * The widest readouts, `-6,4 px` and `12,8 px` at text-9xl, fit the minimum width of the readout,
135
+ * so the slider beside it keeps its width over the whole list.
136
+ */
137
+ export const ReadoutWidth: Story = {
138
+ args: { modelValue: 'tracking-[-0.05em]', fontSize: 'text-9xl' },
139
+ render: renderLetterSpacing,
140
+ play: async ({ canvasElement }) => {
141
+ const thumb = thumbOf(canvasElement)
142
+ const readout = within(canvasElement).getByText('-6,4 px')
143
+ // the slider root, which the thumb is placed in
144
+ const slider = thumb.parentElement!
145
+ const textWidth = () => {
146
+ const range = document.createRange()
147
+ range.selectNodeContents(readout)
148
+ return range.getBoundingClientRect().width
149
+ }
150
+ const measure = () => ({
151
+ readout: readout.getBoundingClientRect().width,
152
+ text: textWidth(),
153
+ slider: slider.getBoundingClientRect().width,
154
+ })
155
+
156
+ const tight = measure()
157
+ thumb.focus()
158
+ await userEvent.keyboard('{End}')
159
+ await expect(readout).toHaveTextContent('12,8 px')
160
+ const wide = measure()
161
+
162
+ // `min-w-16`: 64px, with at least 8px to spare around either label
163
+ await expect(wide.readout).toBe(64)
164
+ await expect(
165
+ Math.min(tight.readout - tight.text, wide.readout - wide.text),
166
+ ).toBeGreaterThanOrEqual(8)
167
+ await expect(wide.readout).toBe(tight.readout)
168
+ await expect(wide.slider).toBe(tight.slider)
169
+ },
170
+ }
171
+
172
+ /** Every step, one control and sample each, so the whole readout range is in view. */
173
+ export const AllSteps: Story = {
174
+ render: (args) => ({
175
+ components: { EditorLetterSpacing },
176
+ setup() {
177
+ const tokens = EDITOR_LETTER_SPACING_STEPS.map(({ token }) => token)
178
+ const fontSize = computed<EditorFontSizeValue>(() => args?.fontSize ?? 'text-sm')
179
+ return { args, tokens, fontSize }
180
+ },
181
+ template: `
182
+ <div class="flex flex-col gap-4">
183
+ <div v-for="token in tokens" :key="token ?? 'default'">
184
+ <EditorLetterSpacing v-bind="args" :label="token ?? '0'" :model-value="token" />
185
+ <p class="text-gray-900 wrap-break-word" :class="[fontSize, token]">
186
+ Een licht appartement op loopafstand van het centrum.
187
+ </p>
188
+ </div>
189
+ </div>
190
+ `,
191
+ }),
192
+ }
193
+
194
+ export const CustomMessages: Story = {
195
+ args: {
196
+ label: 'Letter spacing',
197
+ modelValue: 'tracking-[-0.025em]',
198
+ messages: { unit: 'pt', decimalSeparator: '.' },
199
+ },
200
+ render: renderLetterSpacing,
201
+ play: async ({ canvasElement }) => {
202
+ await expect(thumbOf(canvasElement)).toHaveAttribute('aria-valuetext', '-0.4 pt')
203
+ },
204
+ }
@@ -0,0 +1,238 @@
1
+ import { afterEach, describe, it, expect, vi } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import EditorLetterSpacing, { EDITOR_LETTER_SPACING_STEPS } from './EditorLetterSpacing.vue'
4
+ import type { EditorLetterSpacingValue } from './EditorLetterSpacing.vue'
5
+ import type { EditorFontSizeValue } from '../EditorFontSize/EditorFontSize.vue'
6
+ import Slider from '../../../atoms/Slider/Slider.vue'
7
+
8
+ // jsdom ships no ResizeObserver, and reka-ui's slider observes its track to place the thumbs
9
+ vi.stubGlobal(
10
+ 'ResizeObserver',
11
+ class {
12
+ observe() {}
13
+ unobserve() {}
14
+ disconnect() {}
15
+ },
16
+ )
17
+
18
+ // jsdom implements no pointer capture either, and reka-ui captures the pointer on thumb pointerdown
19
+ Element.prototype.setPointerCapture ??= () => {}
20
+ Element.prototype.releasePointerCapture ??= () => {}
21
+ Element.prototype.hasPointerCapture ??= () => false
22
+
23
+ /** The index the slider sits on, read from the model handed to the slider. */
24
+ const sliderIndex = (wrapper: ReturnType<typeof mount>) =>
25
+ wrapper.findComponent(Slider).props('modelValue')
26
+
27
+ /** Where the no-class step sits: the middle of the list, not the first entry. */
28
+ const DEFAULT_INDEX = 3
29
+
30
+ describe('EditorLetterSpacing', () => {
31
+ // No assertions on typography utilities: the label and the value are found by
32
+ // text or by structure, so restyling the block or the slider cannot turn
33
+ // these red.
34
+ it('renders the Dutch default label', () => {
35
+ const wrapper = mount(EditorLetterSpacing, { props: { modelValue: null } })
36
+
37
+ expect(wrapper.find('p').text()).toBe('Letterafstand')
38
+ })
39
+
40
+ it('renders a custom label', () => {
41
+ const wrapper = mount(EditorLetterSpacing, {
42
+ props: { modelValue: null, label: 'Letter spacing' },
43
+ })
44
+
45
+ expect(wrapper.find('p').text()).toBe('Letter spacing')
46
+ })
47
+
48
+ // The consumer stores these classes and lists them in an `@source inline`
49
+ // rule, and the back end allows exactly these, so the list is a contract:
50
+ // any change here is a breaking change.
51
+ it('offers exactly the agreed steps', () => {
52
+ expect(EDITOR_LETTER_SPACING_STEPS).toEqual([
53
+ { token: 'tracking-[-0.05em]', em: -0.05 },
54
+ { token: 'tracking-[-0.025em]', em: -0.025 },
55
+ { token: 'tracking-[-0.01em]', em: -0.01 },
56
+ { token: null, em: 0 },
57
+ { token: 'tracking-[0.01em]', em: 0.01 },
58
+ { token: 'tracking-[0.025em]', em: 0.025 },
59
+ { token: 'tracking-[0.05em]', em: 0.05 },
60
+ { token: 'tracking-[0.075em]', em: 0.075 },
61
+ { token: 'tracking-[0.1em]', em: 0.1 },
62
+ ])
63
+ })
64
+
65
+ it('spans the whole step list', () => {
66
+ const wrapper = mount(EditorLetterSpacing, { props: { modelValue: null } })
67
+ const slider = wrapper.findComponent(Slider)
68
+
69
+ expect(slider.props('min')).toBe(0)
70
+ expect(slider.props('max')).toBe(8)
71
+ expect(slider.props('step')).toBe(1)
72
+ })
73
+
74
+ it('puts the default step, no class, in the middle of the track', () => {
75
+ const wrapper = mount(EditorLetterSpacing, { props: { modelValue: null } })
76
+
77
+ expect(sliderIndex(wrapper)).toEqual([DEFAULT_INDEX])
78
+ expect(wrapper.text()).toContain('0 px')
79
+ })
80
+
81
+ it('treats an unset model as the default step', () => {
82
+ const wrapper = mount(EditorLetterSpacing)
83
+
84
+ expect(sliderIndex(wrapper)).toEqual([DEFAULT_INDEX])
85
+ })
86
+
87
+ describe('readout', () => {
88
+ // em times the px of `fontSize`, to one decimal, half away from zero; a comma as the decimal
89
+ // separator and a minus sign on the tight side. text-lg has 0.075em at 1.35px exactly, which
90
+ // a float makes 1.3499…: it still reads 1,4.
91
+ it.each([
92
+ ['text-sm', ['-0,7', '-0,4', '-0,1', '0', '0,1', '0,4', '0,7', '1,1', '1,4']],
93
+ ['text-lg', ['-0,9', '-0,5', '-0,2', '0', '0,2', '0,5', '0,9', '1,4', '1,8']],
94
+ ['text-xl', ['-1', '-0,5', '-0,2', '0', '0,2', '0,5', '1', '1,5', '2']],
95
+ ['text-6xl', ['-3', '-1,5', '-0,6', '0', '0,6', '1,5', '3', '4,5', '6']],
96
+ ['text-9xl', ['-6,4', '-3,2', '-1,3', '0', '1,3', '3,2', '6,4', '9,6', '12,8']],
97
+ ] as const)('reads every step in px at %s', (fontSize, readouts) => {
98
+ EDITOR_LETTER_SPACING_STEPS.forEach((step, index) => {
99
+ const wrapper = mount(EditorLetterSpacing, {
100
+ props: { modelValue: step.token, fontSize },
101
+ })
102
+
103
+ expect(sliderIndex(wrapper)).toEqual([index])
104
+ // aria-valuenow is the step index, which is meaningless read aloud
105
+ expect(wrapper.find('[role="slider"]').attributes('aria-valuetext')).toBe(
106
+ `${readouts[index]} px`,
107
+ )
108
+ })
109
+ })
110
+
111
+ it('reads at text-sm without a fontSize', () => {
112
+ const wrapper = mount(EditorLetterSpacing, { props: { modelValue: 'tracking-[0.1em]' } })
113
+
114
+ expect(wrapper.text()).toContain('1,4 px')
115
+ })
116
+
117
+ it('follows a change of fontSize', async () => {
118
+ const wrapper = mount(EditorLetterSpacing, { props: { modelValue: 'tracking-[-0.05em]' } })
119
+
120
+ expect(wrapper.text()).toContain('-0,7 px')
121
+ await wrapper.setProps({ fontSize: 'text-9xl' })
122
+ expect(wrapper.text()).toContain('-6,4 px')
123
+ })
124
+
125
+ it('renders a custom unit and decimal separator', () => {
126
+ const wrapper = mount(EditorLetterSpacing, {
127
+ props: {
128
+ modelValue: 'tracking-[-0.025em]',
129
+ messages: { unit: 'pt', decimalSeparator: '.' },
130
+ },
131
+ })
132
+
133
+ expect(wrapper.text()).toContain('-0.4 pt')
134
+ })
135
+ })
136
+
137
+ it('emits update:modelValue with the Tailwind class of the new step', async () => {
138
+ const wrapper = mount(EditorLetterSpacing, { props: { modelValue: null } })
139
+
140
+ wrapper.findComponent(Slider).vm.$emit('update:modelValue', [1])
141
+ await wrapper.vm.$nextTick()
142
+
143
+ expect(wrapper.emitted('update:modelValue')).toEqual([['tracking-[-0.025em]']])
144
+ })
145
+
146
+ it('emits null when moved back onto the default step', async () => {
147
+ const wrapper = mount(EditorLetterSpacing, { props: { modelValue: 'tracking-[0.05em]' } })
148
+
149
+ wrapper.findComponent(Slider).vm.$emit('update:modelValue', [DEFAULT_INDEX])
150
+ await wrapper.vm.$nextTick()
151
+
152
+ expect(wrapper.emitted('update:modelValue')).toEqual([[null]])
153
+ })
154
+
155
+ // The fallback is for old or faulty data; a development build says so, once per value
156
+ describe('values off the list', () => {
157
+ const spyWarn = () => vi.spyOn(console, 'warn').mockImplementation(() => undefined)
158
+
159
+ afterEach(() => {
160
+ vi.restoreAllMocks()
161
+ })
162
+
163
+ // The type rules these out, which is exactly why the runtime fallback is
164
+ // worth a test: a stored value gets past the types. The default step, not
165
+ // the tightest one: an unknown value is no reason to squeeze the text.
166
+ it.each(['tracking-[1px]', 'tracking-wide'])(
167
+ 'puts %s on the default step, warns once and writes nothing back',
168
+ async (value) => {
169
+ const warn = spyWarn()
170
+ const outOfScale = value as unknown as EditorLetterSpacingValue
171
+ const wrapper = mount(EditorLetterSpacing, { props: { modelValue: outOfScale } })
172
+
173
+ expect(sliderIndex(wrapper)).toEqual([DEFAULT_INDEX])
174
+ expect(wrapper.text()).toContain('0 px')
175
+
176
+ await wrapper.setProps({ label: 'Opnieuw renderen' })
177
+ await wrapper.setProps({ modelValue: outOfScale })
178
+ expect(warn).toHaveBeenCalledOnce()
179
+ expect(warn.mock.calls[0][0]).toContain(value)
180
+ expect(wrapper.emitted('update:modelValue')).toBeUndefined()
181
+ },
182
+ )
183
+
184
+ it('warns again for another value off the list', async () => {
185
+ const warn = spyWarn()
186
+ const wrapper = mount(EditorLetterSpacing, {
187
+ props: { modelValue: 'tracking-[1px]' as unknown as EditorLetterSpacingValue },
188
+ })
189
+
190
+ await wrapper.setProps({
191
+ modelValue: 'tracking-[2px]' as unknown as EditorLetterSpacingValue,
192
+ })
193
+
194
+ expect(warn).toHaveBeenCalledTimes(2)
195
+ expect(warn.mock.calls[1][0]).toContain('tracking-[2px]')
196
+ })
197
+
198
+ it('reads an unknown fontSize at text-sm and warns once', async () => {
199
+ const warn = spyWarn()
200
+ const wrapper = mount(EditorLetterSpacing, {
201
+ props: {
202
+ modelValue: 'tracking-[0.1em]',
203
+ fontSize: 'text-10xl' as unknown as EditorFontSizeValue,
204
+ },
205
+ })
206
+
207
+ expect(wrapper.text()).toContain('1,4 px')
208
+ await wrapper.setProps({ label: 'Opnieuw renderen' })
209
+ expect(warn).toHaveBeenCalledOnce()
210
+ expect(warn.mock.calls[0][0]).toContain('text-10xl')
211
+ })
212
+
213
+ it.each<[{ modelValue: EditorLetterSpacingValue | null; fontSize?: EditorFontSizeValue }]>([
214
+ [{ modelValue: null }],
215
+ [{ modelValue: 'tracking-[0.075em]', fontSize: 'text-6xl' }],
216
+ ])('stays silent for valid values: %o', (props) => {
217
+ const warn = spyWarn()
218
+
219
+ mount(EditorLetterSpacing, { props })
220
+
221
+ expect(warn).not.toHaveBeenCalled()
222
+ })
223
+ })
224
+
225
+ it('passes the release of the slider on as pointerup', async () => {
226
+ const wrapper = mount(EditorLetterSpacing, { props: { modelValue: null } })
227
+
228
+ await wrapper.find('[role="slider"]').trigger('pointerup')
229
+
230
+ expect(wrapper.emitted('pointerup')).toHaveLength(1)
231
+ })
232
+
233
+ it('names the thumb after the label', () => {
234
+ const wrapper = mount(EditorLetterSpacing, { props: { modelValue: null } })
235
+
236
+ expect(wrapper.find('[role="slider"]').attributes('aria-label')).toBe('Letterafstand')
237
+ })
238
+ })