@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,253 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
2
+ import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
3
+ import { defineComponent, h, ref } from 'vue'
4
+ import EditorSize from './EditorSize.vue'
5
+ import type { EditorSizeValue } from './EditorSize.vue'
6
+
7
+ // ToggleGroup awaits document.fonts.ready on mount, which jsdom does not ship.
8
+ beforeEach(() => {
9
+ Object.defineProperty(document, 'fonts', {
10
+ value: { ready: Promise.resolve() },
11
+ configurable: true,
12
+ })
13
+ })
14
+
15
+ let wrapper: VueWrapper
16
+
17
+ const mountSize = (
18
+ modelValue: EditorSizeValue,
19
+ props: Record<string, unknown> = {},
20
+ // Focus only lands on an element that is in the document.
21
+ attachTo?: HTMLElement,
22
+ ) => {
23
+ wrapper = mount(EditorSize, {
24
+ attachTo,
25
+ props: {
26
+ modelValue,
27
+ 'onUpdate:modelValue': (value: EditorSizeValue) => wrapper.setProps({ modelValue: value }),
28
+ ...props,
29
+ },
30
+ })
31
+ return wrapper
32
+ }
33
+
34
+ const option = (text: string) => wrapper.findAll('button').find((button) => button.text() === text)!
35
+
36
+ const fixedRow = () => wrapper.get('[data-testid="editor-size-fixed-row"]')
37
+
38
+ const focusedInput = () => document.activeElement === wrapper.get('input').element
39
+
40
+ afterEach(() => wrapper?.unmount())
41
+
42
+ /**
43
+ * Mounts behind a consumer that applies each write itself, the way a debounced
44
+ * or store-backed model does: after `delay` ms, or never when `delay` is null.
45
+ */
46
+ const mountBehindConsumer = (modelValue: EditorSizeValue, delay: number | null) => {
47
+ const model = ref(modelValue)
48
+ const Consumer = defineComponent(
49
+ () => () =>
50
+ h(EditorSize, {
51
+ modelValue: model.value,
52
+ 'onUpdate:modelValue': (value: EditorSizeValue) => {
53
+ if (delay !== null) setTimeout(() => (model.value = value), delay)
54
+ },
55
+ }),
56
+ )
57
+ wrapper = mount(Consumer, { attachTo: document.body })
58
+ return model
59
+ }
60
+
61
+ const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
62
+
63
+ const writes = () => wrapper.emitted('update:modelValue')?.map(([value]) => value) ?? []
64
+
65
+ describe('EditorSize', () => {
66
+ // The mode options sit in a ToggleGroup toolbar; its name is the block label.
67
+ it('names the mode toolbar with the block label', () => {
68
+ mountSize({ mode: 'auto' }, { label: 'Breedte' })
69
+
70
+ expect(wrapper.get('[role="toolbar"]').attributes('aria-label')).toBe('Breedte')
71
+ })
72
+
73
+ it('renders the Dutch defaults', () => {
74
+ mountSize({ mode: 'auto' })
75
+
76
+ expect(wrapper.find('p').text()).toBe('Formaat')
77
+ expect(wrapper.findAll('button').map((button) => button.text())).toEqual([
78
+ 'Automatisch',
79
+ 'Vullen',
80
+ 'Vast',
81
+ ])
82
+ })
83
+
84
+ it('names each option after the label, so a width and a height block differ', () => {
85
+ mountSize({ mode: 'auto' }, { label: 'Breedte' })
86
+
87
+ expect(wrapper.findAll('button').map((button) => button.attributes('aria-label'))).toEqual([
88
+ 'Breedte automatisch',
89
+ 'Breedte vullen',
90
+ 'Breedte vast',
91
+ ])
92
+ })
93
+
94
+ it('takes the accessible names from messages when given, per key', () => {
95
+ mountSize(
96
+ { mode: 'auto' },
97
+ { label: 'Breite', messages: { fixed: 'Fest', fixedAriaLabel: 'Feste Breite' } },
98
+ )
99
+
100
+ expect(wrapper.findAll('button').map((button) => button.attributes('aria-label'))).toEqual([
101
+ 'Breite automatisch',
102
+ 'Breite vullen',
103
+ 'Feste Breite',
104
+ ])
105
+ })
106
+
107
+ it('offers only the modes passed, in the library order', () => {
108
+ mountSize({ mode: 'auto' }, { modes: ['fixed', 'auto'] })
109
+
110
+ expect(wrapper.findAll('button').map((button) => button.text())).toEqual([
111
+ 'Automatisch',
112
+ 'Vast',
113
+ ])
114
+ })
115
+
116
+ it('keeps the field out of reach unless the mode is fixed', () => {
117
+ mountSize({ mode: 'fill' })
118
+
119
+ expect(fixedRow().attributes('inert')).toBeDefined()
120
+ })
121
+
122
+ it('shows the fixed dimension, named after the label', () => {
123
+ mountSize({ mode: 'fixed', fixed: { value: 320, unit: 'px' } }, { label: 'Breedte' })
124
+
125
+ expect(fixedRow().attributes('inert')).toBeUndefined()
126
+ expect((wrapper.get('input').element as HTMLInputElement).value).toBe('320')
127
+ expect(wrapper.get('input').attributes('aria-label')).toBe('Breedte (px)')
128
+ })
129
+
130
+ it('writes a picked mode and keeps the fixed dimension for later', async () => {
131
+ mountSize({ mode: 'fixed', fixed: { value: 320, unit: 'px' } })
132
+
133
+ await option('Vullen').trigger('click')
134
+ await option('Vast').trigger('click')
135
+
136
+ expect(writes()).toEqual([
137
+ { mode: 'fill', fixed: { value: 320, unit: 'px' } },
138
+ { mode: 'fixed', fixed: { value: 320, unit: 'px' } },
139
+ ])
140
+ expect(wrapper.emitted('commit')).toHaveLength(2)
141
+ })
142
+
143
+ it('writes nothing for the mode that is already picked', async () => {
144
+ mountSize({ mode: 'auto' })
145
+
146
+ await option('Automatisch').trigger('click')
147
+
148
+ expect(writes()).toEqual([])
149
+ expect(wrapper.emitted('commit')).toBeUndefined()
150
+ })
151
+
152
+ describe('focus', () => {
153
+ it('moves into the field when fixed is picked, with a kept value selected', async () => {
154
+ mountSize({ mode: 'auto', fixed: { value: 320, unit: 'px' } }, {}, document.body)
155
+ // jsdom keeps no selection range on a number input, so the call is checked.
156
+ const select = vi.spyOn(wrapper.get('input').element as HTMLInputElement, 'select')
157
+
158
+ await option('Vast').trigger('click')
159
+ await flushPromises()
160
+
161
+ expect(focusedInput()).toBe(true)
162
+ expect((wrapper.get('input').element as HTMLInputElement).value).toBe('320')
163
+ expect(select).toHaveBeenCalled()
164
+ })
165
+
166
+ it('stays where it is when mounted in fixed', async () => {
167
+ mountSize({ mode: 'fixed', fixed: { value: 320, unit: 'px' } }, {}, document.body)
168
+ await flushPromises()
169
+
170
+ expect(focusedInput()).toBe(false)
171
+ })
172
+
173
+ it('stays where it is when the model turns fixed from outside', async () => {
174
+ mountSize({ mode: 'auto' }, {}, document.body)
175
+
176
+ await wrapper.setProps({ modelValue: { mode: 'fixed' } })
177
+ await flushPromises()
178
+
179
+ expect(focusedInput()).toBe(false)
180
+ })
181
+
182
+ it('waits for a consumer that applies the pick later', async () => {
183
+ mountBehindConsumer({ mode: 'auto', fixed: { value: 320, unit: 'px' } }, 20)
184
+
185
+ await option('Vast').trigger('click')
186
+ await flushPromises()
187
+ expect(focusedInput()).toBe(false)
188
+
189
+ await wait(30)
190
+ await flushPromises()
191
+ expect(focusedInput()).toBe(true)
192
+ })
193
+
194
+ it('drops a pick the consumer never applied once the user moves on', async () => {
195
+ const model = mountBehindConsumer({ mode: 'auto' }, null)
196
+
197
+ await option('Vast').trigger('click')
198
+ document.body.dispatchEvent(new Event('pointerdown', { bubbles: true }))
199
+
200
+ model.value = { mode: 'fixed' }
201
+ await flushPromises()
202
+ expect(focusedInput()).toBe(false)
203
+ })
204
+
205
+ it('drops a pick the consumer never applied at the next mode change', async () => {
206
+ const model = mountBehindConsumer({ mode: 'auto' }, null)
207
+
208
+ await option('Vast').trigger('click')
209
+ model.value = { mode: 'fill' }
210
+ await flushPromises()
211
+
212
+ model.value = { mode: 'fixed' }
213
+ await flushPromises()
214
+ expect(focusedInput()).toBe(false)
215
+ })
216
+
217
+ it('stays where it is for another mode', async () => {
218
+ mountSize({ mode: 'auto' }, {}, document.body)
219
+
220
+ await option('Vullen').trigger('click')
221
+ await flushPromises()
222
+
223
+ expect(focusedInput()).toBe(false)
224
+ })
225
+ })
226
+
227
+ it('writes a typed fixed value and passes its commit on', async () => {
228
+ mountSize({ mode: 'fixed' })
229
+
230
+ await wrapper.get('input').setValue('2000')
231
+ await wrapper.get('input').trigger('blur')
232
+ await flushPromises()
233
+
234
+ expect(writes()).toEqual([{ mode: 'fixed', fixed: { value: 1600, unit: 'px' } }])
235
+ expect(wrapper.emitted('commit')).toHaveLength(1)
236
+ })
237
+
238
+ it('passes the units, limits and messages through to the field', async () => {
239
+ mountSize(
240
+ { mode: 'fixed', fixed: { value: 80, unit: '%' } },
241
+ { units: ['px', '%'], max: { '%': 50 }, messages: { unit: 'Unit', fixed: 'Fixed' } },
242
+ )
243
+
244
+ expect(option('Fixed')).toBeDefined()
245
+ expect(wrapper.get('select').attributes('aria-label')).toBe('Unit')
246
+
247
+ await wrapper.get('input').setValue('80')
248
+ await wrapper.get('input').trigger('blur')
249
+ await flushPromises()
250
+
251
+ expect(writes()).toEqual([{ mode: 'fixed', fixed: { value: 50, unit: '%' } }])
252
+ })
253
+ })
@@ -0,0 +1,49 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { ref } from 'vue'
3
+ import EditorSizeLimit from './EditorSizeLimit.vue'
4
+ import NexxtEditorSettingsBlock from '../../atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
5
+ import type { EditorDimensionValue } from '../../atoms/EditorDimensionField/EditorDimensionField.vue'
6
+
7
+ export default {
8
+ title: 'Components/Editor Components/Molecules/Editor Size Limit',
9
+ component: EditorSizeLimit,
10
+ decorators: [() => ({ template: '<div class="w-100 mx-auto"><story /></div>' })],
11
+ args: { label: 'Maximale breedte' },
12
+ argTypes: {
13
+ label: { control: 'text' },
14
+ },
15
+ } satisfies Meta<typeof EditorSizeLimit>
16
+
17
+ type Story = StoryObj<typeof EditorSizeLimit>
18
+
19
+ /** No limit: the field is empty and shows the placeholder. */
20
+ export const Default: Story = {
21
+ render: (args) => ({
22
+ components: { EditorSizeLimit },
23
+ setup() {
24
+ const limit = ref<EditorDimensionValue | null>(null)
25
+ return { args, limit }
26
+ },
27
+ template: `<EditorSizeLimit v-bind="args" v-model="limit" />`,
28
+ }),
29
+ }
30
+
31
+ /** The two limits side by side inside the block of the sizes they limit, as an editor places them. */
32
+ export const SideBySide: Story = {
33
+ render: () => ({
34
+ components: { EditorSizeLimit, NexxtEditorSettingsBlock },
35
+ setup() {
36
+ const maxWidth = ref<EditorDimensionValue | null>({ value: 960, unit: 'px' })
37
+ const minHeight = ref<EditorDimensionValue | null>(null)
38
+ return { maxWidth, minHeight }
39
+ },
40
+ template: `
41
+ <NexxtEditorSettingsBlock label="Formaat">
42
+ <div class="grid grid-cols-2 gap-2">
43
+ <EditorSizeLimit v-model="maxWidth" label="Maximale breedte" :units="['px', '%']" />
44
+ <EditorSizeLimit v-model="minHeight" label="Minimale hoogte" />
45
+ </div>
46
+ </NexxtEditorSettingsBlock>
47
+ `,
48
+ }),
49
+ }
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { flushPromises, mount } from '@vue/test-utils'
3
+ import EditorSizeLimit from './EditorSizeLimit.vue'
4
+
5
+ describe('EditorSizeLimit', () => {
6
+ it('shows its label above the field and names the field with it', () => {
7
+ const wrapper = mount(EditorSizeLimit, { props: { label: 'Maximale breedte' } })
8
+
9
+ expect(wrapper.get('label > span').text()).toBe('Maximale breedte')
10
+ expect(wrapper.get('input').attributes('aria-label')).toBe('Maximale breedte (px)')
11
+ })
12
+
13
+ it('is empty with the placeholder while there is no limit', () => {
14
+ const wrapper = mount(EditorSizeLimit, {
15
+ props: { label: 'Maximale breedte', modelValue: null },
16
+ })
17
+
18
+ expect((wrapper.get('input').element as HTMLInputElement).value).toBe('')
19
+ expect(wrapper.get('input').attributes('placeholder')).toBe('Geen')
20
+ })
21
+
22
+ it('removes the limit when the field is emptied', async () => {
23
+ const wrapper = mount(EditorSizeLimit, {
24
+ props: { label: 'Minimale hoogte', modelValue: { value: 400, unit: 'px' } },
25
+ })
26
+
27
+ await wrapper.get('input').setValue('')
28
+ await wrapper.get('input').trigger('blur')
29
+ await flushPromises()
30
+
31
+ expect(wrapper.emitted('update:modelValue')).toEqual([[null]])
32
+ expect(wrapper.emitted('commit')).toHaveLength(1)
33
+ })
34
+
35
+ it('passes units and messages through to the field', () => {
36
+ const wrapper = mount(EditorSizeLimit, {
37
+ props: { label: 'Max width', units: ['px', '%'], messages: { placeholder: 'None' } },
38
+ })
39
+
40
+ expect(wrapper.get('input').attributes('placeholder')).toBe('None')
41
+ expect(wrapper.find('select').exists()).toBe(true)
42
+ })
43
+ })
@@ -0,0 +1,69 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { ref } from 'vue'
3
+ import EditorTextAlign from './EditorTextAlign.vue'
4
+ import type { EditorTextAlignValue } from './EditorTextAlign.vue'
5
+
6
+ export default {
7
+ title: 'Components/Editor Components/Molecules/Editor Text Align',
8
+ component: EditorTextAlign,
9
+ decorators: [() => ({ template: '<div class="w-100 mx-auto"><story /></div>' })],
10
+ argTypes: {
11
+ label: { control: 'text' },
12
+ },
13
+ } satisfies Meta<typeof EditorTextAlign>
14
+
15
+ type Story = StoryObj<typeof EditorTextAlign>
16
+
17
+ export const Default: Story = {
18
+ render: (args) => ({
19
+ components: { EditorTextAlign },
20
+ setup() {
21
+ const align = ref<EditorTextAlignValue>('text-left')
22
+ return { args, align }
23
+ },
24
+ // The value is shown as-is: it is the Tailwind class the editor applies.
25
+ template: `
26
+ <div>
27
+ <EditorTextAlign v-bind="args" v-model="align" />
28
+ <p class="small-normal text-gray-700">{{ align }}</p>
29
+ </div>
30
+ `,
31
+ }),
32
+ }
33
+
34
+ export const CustomMessages: Story = {
35
+ args: {
36
+ label: 'Text alignment',
37
+ messages: { left: 'Left', center: 'Center', right: 'Right', justify: 'Justify' },
38
+ },
39
+ render: (args) => ({
40
+ components: { EditorTextAlign },
41
+ setup() {
42
+ const align = ref<EditorTextAlignValue>('text-left')
43
+ return { args, align }
44
+ },
45
+ template: `<EditorTextAlign v-bind="args" v-model="align" />`,
46
+ }),
47
+ }
48
+
49
+ /**
50
+ * A subset of the options, for an element whose alignment the consumer writes
51
+ * as something other than `text-*` and so cannot justify. Always shown in the
52
+ * canonical order, whatever order `options` is in.
53
+ */
54
+ export const OptionsSubset: Story = {
55
+ args: { label: 'Uitlijning', options: ['text-left', 'text-center', 'text-right'] },
56
+ render: (args) => ({
57
+ components: { EditorTextAlign },
58
+ setup() {
59
+ const align = ref<EditorTextAlignValue>('text-center')
60
+ return { args, align }
61
+ },
62
+ template: `
63
+ <div>
64
+ <EditorTextAlign v-bind="args" v-model="align" />
65
+ <p class="small-normal text-gray-700">{{ align }}</p>
66
+ </div>
67
+ `,
68
+ }),
69
+ }
@@ -0,0 +1,155 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest'
2
+ import { mount, type VueWrapper } from '@vue/test-utils'
3
+ import EditorTextAlign, { EDITOR_TEXT_ALIGN_OPTIONS } from './EditorTextAlign.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('EditorTextAlign', () => {
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(EditorTextAlign, { props: { modelValue: 'text-left' } })
25
+ expect(wrapper.get('[role="toolbar"]').attributes('aria-label')).toBe('Tekst 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(EditorTextAlign, { props: { modelValue: 'text-left' } })
36
+
37
+ expect(wrapper.find('p').text()).toBe('Tekst uitlijning')
38
+ })
39
+
40
+ it('renders a button for each of the four options', () => {
41
+ const wrapper = mount(EditorTextAlign, { props: { modelValue: 'text-left' } })
42
+
43
+ expect(wrapper.findAll('button')).toHaveLength(4)
44
+ })
45
+
46
+ it('names the options with the Dutch defaults', () => {
47
+ const wrapper = mount(EditorTextAlign, { props: { modelValue: 'text-left' } })
48
+
49
+ expect(optionNames(wrapper)).toEqual(['Links', 'Midden', 'Rechts', 'Uitvullen'])
50
+ })
51
+
52
+ it('renders a custom label', () => {
53
+ const wrapper = mount(EditorTextAlign, {
54
+ props: { modelValue: 'text-left', label: 'Text alignment' },
55
+ })
56
+
57
+ expect(wrapper.find('p').text()).toBe('Text alignment')
58
+ })
59
+
60
+ it('names the options with custom messages', () => {
61
+ const wrapper = mount(EditorTextAlign, {
62
+ props: {
63
+ modelValue: 'text-left',
64
+ messages: { left: 'Left', center: 'Center', right: 'Right', justify: 'Justify' },
65
+ },
66
+ })
67
+
68
+ expect(optionNames(wrapper)).toEqual(['Left', 'Center', 'Right', 'Justify'])
69
+ })
70
+
71
+ it('keeps the untouched defaults when only one message is overridden', () => {
72
+ const wrapper = mount(EditorTextAlign, {
73
+ props: { modelValue: 'text-left', messages: { justify: 'Justify' } },
74
+ })
75
+
76
+ expect(optionNames(wrapper)).toEqual(['Links', 'Midden', 'Rechts', 'Justify'])
77
+ })
78
+
79
+ it('emits update:modelValue with the Tailwind class of the clicked option', async () => {
80
+ const wrapper = mount(EditorTextAlign, { props: { modelValue: 'text-left' } })
81
+
82
+ await wrapper.findAll('button')[3].trigger('click')
83
+
84
+ expect(wrapper.emitted('update:modelValue')).toEqual([['text-justify']])
85
+ })
86
+
87
+ it('renders only icons, no visible option text', () => {
88
+ const wrapper = mount(EditorTextAlign, { props: { modelValue: 'text-left' } })
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(EditorTextAlign, { props: { modelValue: 'text-left' } })
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(EditorTextAlign, { props: { modelValue: 'text-left' } })
107
+
108
+ expect(optionNames(wrapper)).toEqual(['Links', 'Midden', 'Rechts', 'Uitvullen'])
109
+ })
110
+
111
+ it('offers only the options passed in `options`', () => {
112
+ const wrapper = mount(EditorTextAlign, {
113
+ props: { modelValue: 'text-left', options: ['text-left', 'text-center', 'text-right'] },
114
+ })
115
+
116
+ expect(optionNames(wrapper)).toEqual(['Links', 'Midden', 'Rechts'])
117
+ })
118
+
119
+ it('keeps the canonical order whatever order `options` is in', () => {
120
+ const wrapper = mount(EditorTextAlign, {
121
+ props: { modelValue: 'text-left', options: ['text-right', 'text-justify', 'text-left'] },
122
+ })
123
+
124
+ expect(optionNames(wrapper)).toEqual(['Links', 'Rechts', 'Uitvullen'])
125
+ })
126
+
127
+ it('emits the class of a clicked option from a subset', async () => {
128
+ const wrapper = mount(EditorTextAlign, {
129
+ props: { modelValue: 'text-left', options: ['text-left', 'text-right'] },
130
+ })
131
+
132
+ await wrapper.findAll('button')[1].trigger('click')
133
+
134
+ expect(wrapper.emitted('update:modelValue')).toEqual([['text-right']])
135
+ })
136
+
137
+ it('is always visible, so it renders no switch', () => {
138
+ const wrapper = mount(EditorTextAlign, { props: { modelValue: 'text-left' } })
139
+
140
+ expect(wrapper.find('input[type="checkbox"]').exists()).toBe(false)
141
+ })
142
+ })
143
+
144
+ // The consumer derives its class matchers from this list, so the list is a
145
+ // contract: any change here changes what the app recognises.
146
+ describe('EDITOR_TEXT_ALIGN_OPTIONS', () => {
147
+ it('offers exactly the agreed options', () => {
148
+ expect(EDITOR_TEXT_ALIGN_OPTIONS).toEqual([
149
+ { token: 'left' },
150
+ { token: 'center' },
151
+ { token: 'right' },
152
+ { token: 'justify' },
153
+ ])
154
+ })
155
+ })