@nexxtmove/ui 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/dist/index.js +49 -126
  2. package/dist/nuxt.js +0 -1
  3. package/package.json +38 -36
  4. package/src/attribute-contract.test.ts +151 -0
  5. package/src/components/atoms/AnimatedNumber/AnimatedNumber.stories.ts +36 -0
  6. package/src/components/atoms/AnimatedNumber/AnimatedNumber.test.ts +21 -0
  7. package/src/components/atoms/Avatar/Avatar.stories.ts +122 -0
  8. package/src/components/atoms/Avatar/Avatar.test.ts +134 -0
  9. package/src/components/atoms/Button/Button.stories.ts +290 -0
  10. package/src/components/atoms/Button/Button.test.ts +406 -0
  11. package/src/components/atoms/Card/Card.stories.ts +45 -0
  12. package/src/components/atoms/Card/Card.test.ts +46 -0
  13. package/src/components/atoms/Checkbox/Checkbox.stories.ts +35 -0
  14. package/src/components/atoms/Checkbox/Checkbox.test.ts +55 -0
  15. package/src/components/atoms/Chip/Chip.stories.ts +274 -0
  16. package/src/components/atoms/Chip/Chip.test.ts +230 -0
  17. package/src/components/atoms/EnergyLabel/EnergyLabel.stories.ts +189 -0
  18. package/src/components/atoms/EnergyLabel/EnergyLabel.test.ts +100 -0
  19. package/src/components/atoms/FloatingPanel/FloatingPanel.stories.ts +677 -0
  20. package/src/components/atoms/FloatingPanel/FloatingPanel.test.ts +864 -0
  21. package/src/components/atoms/Icon/Icon.stories.ts +63 -0
  22. package/src/components/atoms/Icon/Icon.test.ts +89 -0
  23. package/src/components/atoms/Loader/Loader.stories.ts +36 -0
  24. package/src/components/atoms/Loader/Loader.test.ts +40 -0
  25. package/src/components/atoms/PickerItem/PickerItem.stories.ts +172 -0
  26. package/src/components/atoms/PickerItem/PickerItem.test.ts +142 -0
  27. package/src/components/atoms/PickerPreview/PickerPreview.stories.ts +183 -0
  28. package/src/components/atoms/PickerPreview/PickerPreview.test.ts +137 -0
  29. package/src/components/atoms/PresenceIndicator/PresenceIndicator.stories.ts +21 -0
  30. package/src/components/atoms/PropertyListing/PropertyListing.stories.ts +38 -0
  31. package/src/components/atoms/PropertyListing/PropertyListing.test.ts +46 -0
  32. package/src/components/atoms/SelectableOption/SelectableOption.stories.ts +131 -0
  33. package/src/components/atoms/SelectableOption/SelectableOption.test.ts +96 -0
  34. package/src/components/atoms/Skeleton/Skeleton.stories.ts +27 -0
  35. package/src/components/atoms/Skeleton/Skeleton.test.ts +28 -0
  36. package/src/components/atoms/Slider/Slider.stories.ts +151 -0
  37. package/src/components/atoms/Slider/Slider.test.ts +191 -0
  38. package/src/components/atoms/Squircle/Squircle.stories.ts +114 -0
  39. package/src/components/atoms/Squircle/Squircle.test.ts +70 -0
  40. package/src/components/atoms/Switch/Switch.stories.ts +91 -0
  41. package/src/components/atoms/Switch/Switch.test.ts +183 -0
  42. package/src/components/atoms/Tooltip/Tooltip.stories.ts +158 -0
  43. package/src/components/atoms/Tooltip/Tooltip.test.ts +134 -0
  44. package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.stories.ts +124 -0
  45. package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.test.ts +464 -0
  46. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.stories.ts +266 -0
  47. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.test.ts +421 -0
  48. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.stories.ts +365 -0
  49. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.test.ts +484 -0
  50. package/src/components/editor/composables/useRadioGrid.test.ts +261 -0
  51. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.stories.ts +73 -0
  52. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.test.ts +189 -0
  53. package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.stories.ts +309 -0
  54. package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.test.ts +359 -0
  55. package/src/components/editor/molecules/EditorAnchor/EditorAnchor.stories.ts +138 -0
  56. package/src/components/editor/molecules/EditorAnchor/EditorAnchor.test.ts +344 -0
  57. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.stories.ts +87 -0
  58. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.test.ts +262 -0
  59. package/src/components/editor/molecules/EditorBlur/EditorBlur.stories.ts +115 -0
  60. package/src/components/editor/molecules/EditorBlur/EditorBlur.test.ts +243 -0
  61. package/src/components/editor/molecules/EditorBorder/EditorBorder.stories.ts +187 -0
  62. package/src/components/editor/molecules/EditorBorder/EditorBorder.test.ts +456 -0
  63. package/src/components/editor/molecules/EditorColor/EditorColor.stories.ts +563 -0
  64. package/src/components/editor/molecules/EditorColor/EditorColor.test.ts +689 -0
  65. package/src/components/editor/molecules/EditorColor/EditorColorGradient.test.ts +793 -0
  66. package/src/components/editor/molecules/EditorColor/gradientClass.test.ts +303 -0
  67. package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.stories.ts +72 -0
  68. package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.test.ts +124 -0
  69. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.stories.ts +46 -0
  70. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.test.ts +97 -0
  71. package/src/components/editor/molecules/EditorFontSize/EditorFontSize.stories.ts +248 -0
  72. package/src/components/editor/molecules/EditorFontSize/EditorFontSize.test.ts +288 -0
  73. package/src/components/editor/molecules/EditorGap/EditorGap.stories.ts +129 -0
  74. package/src/components/editor/molecules/EditorGap/EditorGap.test.ts +253 -0
  75. package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.stories.ts +68 -0
  76. package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.test.ts +150 -0
  77. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.stories.ts +71 -0
  78. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.test.ts +161 -0
  79. package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.stories.ts +204 -0
  80. package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.test.ts +238 -0
  81. package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.stories.ts +257 -0
  82. package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.test.ts +286 -0
  83. package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.stories.ts +135 -0
  84. package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.test.ts +230 -0
  85. package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.stories.ts +154 -0
  86. package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.test.ts +112 -0
  87. package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.stories.ts +206 -0
  88. package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.test.ts +332 -0
  89. package/src/components/editor/molecules/EditorPadding/EditorPadding.stories.ts +374 -0
  90. package/src/components/editor/molecules/EditorPadding/EditorPadding.test.ts +712 -0
  91. package/src/components/editor/molecules/EditorRadius/EditorRadius.stories.ts +64 -0
  92. package/src/components/editor/molecules/EditorRadius/EditorRadius.test.ts +137 -0
  93. package/src/components/editor/molecules/EditorShadow/EditorShadow.stories.ts +103 -0
  94. package/src/components/editor/molecules/EditorShadow/EditorShadow.test.ts +168 -0
  95. package/src/components/editor/molecules/EditorSize/EditorSize.stories.ts +84 -0
  96. package/src/components/editor/molecules/EditorSize/EditorSize.test.ts +253 -0
  97. package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.stories.ts +49 -0
  98. package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.test.ts +43 -0
  99. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.stories.ts +69 -0
  100. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.test.ts +155 -0
  101. package/src/components/molecules/Announcement/Announcement.stories.ts +224 -0
  102. package/src/components/molecules/Announcement/Announcement.test.ts +161 -0
  103. package/src/components/molecules/ContactInfoCard/ContactInfoCard.stories.ts +99 -0
  104. package/src/components/molecules/ContactInfoCard/ContactInfoCard.test.ts +70 -0
  105. package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.stories.ts +148 -0
  106. package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.test.ts +271 -0
  107. package/src/components/molecules/DoughnutChart/DoughnutChart.stories.ts +45 -0
  108. package/src/components/molecules/DoughnutChart/DoughnutChart.test.ts +205 -0
  109. package/src/components/molecules/InfoBlock/InfoBlock.stories.ts +84 -0
  110. package/src/components/molecules/InfoBlock/InfoBlock.test.ts +101 -0
  111. package/src/components/molecules/InputField/InputField.stories.ts +117 -0
  112. package/src/components/molecules/InputField/InputField.test.ts +138 -0
  113. package/src/components/molecules/InputSelect/InputSelect.stories.ts +116 -0
  114. package/src/components/molecules/InputSelect/InputSelect.test.ts +127 -0
  115. package/src/components/molecules/ModalFooter/ModalFooter.stories.ts +204 -0
  116. package/src/components/molecules/ModalFooter/ModalFooter.test.ts +276 -0
  117. package/src/components/molecules/PageCard/PageCard.stories.ts +100 -0
  118. package/src/components/molecules/PageCard/PageCard.test.ts +91 -0
  119. package/src/components/molecules/PageContainer/PageContainer.stories.ts +90 -0
  120. package/src/components/molecules/PageContainer/PageContainer.test.ts +50 -0
  121. package/src/components/molecules/PageMockup/PageMockup.stories.ts +38 -0
  122. package/src/components/molecules/PageMockup/PageMockup.test.ts +20 -0
  123. package/src/components/molecules/PageTypeTile/PageTypeTile.stories.ts +63 -0
  124. package/src/components/molecules/PageTypeTile/PageTypeTile.test.ts +41 -0
  125. package/src/components/molecules/Pagination/Pagination.stories.ts +72 -0
  126. package/src/components/molecules/Pagination/Pagination.test.ts +130 -0
  127. package/src/components/molecules/PickerGroup/PickerGroup.stories.ts +90 -0
  128. package/src/components/molecules/PickerGroup/PickerGroup.test.ts +145 -0
  129. package/src/components/molecules/PickerList/PickerList.stories.ts +436 -0
  130. package/src/components/molecules/PickerList/PickerList.test.ts +870 -0
  131. package/src/components/molecules/PlaceholderInput/PlaceholderInput.stories.ts +48 -0
  132. package/src/components/molecules/PlaceholderInput/PlaceholderInput.test.ts +107 -0
  133. package/src/components/molecules/PropertyCard/PropertyCard.stories.ts +46 -0
  134. package/src/components/molecules/PropertyCard/PropertyCard.test.ts +59 -0
  135. package/src/components/molecules/Select/Select.stories.ts +264 -0
  136. package/src/components/molecules/Select/Select.test.ts +315 -0
  137. package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.stories.ts +84 -0
  138. package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.test.ts +103 -0
  139. package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.stories.ts +11 -0
  140. package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.test.ts +38 -0
  141. package/src/components/molecules/SocialIcons/SocialIcons.stories.ts +28 -0
  142. package/src/components/molecules/SocialIcons/SocialIcons.test.ts +58 -0
  143. package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.stories.ts +11 -0
  144. package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.test.ts +116 -0
  145. package/src/components/molecules/SocialMediaType/SocialMediaType.stories.ts +37 -0
  146. package/src/components/molecules/SocialMediaType/SocialMediaType.test.ts +126 -0
  147. package/src/components/molecules/Source/Source.stories.ts +35 -0
  148. package/src/components/molecules/Source/Source.test.ts +45 -0
  149. package/src/components/molecules/TabbedContent/TabbedContent.stories.ts +96 -0
  150. package/src/components/molecules/TabbedContent/TabbedContent.test.ts +109 -0
  151. package/src/components/molecules/Tabs/Tabs.stories.ts +230 -0
  152. package/src/components/molecules/Tabs/Tabs.test.ts +280 -0
  153. package/src/components/molecules/TemplateCard/TemplateCard.stories.ts +327 -0
  154. package/src/components/molecules/TemplateCard/TemplateCard.test.ts +339 -0
  155. package/src/components/molecules/Textarea/Textarea.stories.ts +216 -0
  156. package/src/components/molecules/Textarea/Textarea.test.ts +191 -0
  157. package/src/components/molecules/TimelineEvent/TimelineEvent.stories.ts +319 -0
  158. package/src/components/molecules/TimelineEvent/TimelineEvent.test.ts +224 -0
  159. package/src/components/molecules/TimelineLanes/TimelineLanes.stories.ts +29 -0
  160. package/src/components/molecules/TimelineLanes/TimelineLanes.test.ts +30 -0
  161. package/src/components/molecules/Toast/Toast.stories.ts +110 -0
  162. package/src/components/molecules/Toast/Toast.test.ts +103 -0
  163. package/src/components/molecules/ToggleGroup/ToggleGroup.stories.ts +297 -0
  164. package/src/components/molecules/ToggleGroup/ToggleGroup.test.ts +578 -0
  165. package/src/components/molecules/Tree/Tree.stories.ts +884 -0
  166. package/src/components/molecules/Tree/Tree.test.ts +1247 -0
  167. package/src/components/molecules/Tree/tree-drop.test.ts +223 -0
  168. package/src/components/organisms/Calendar/Calendar.stories.ts +108 -0
  169. package/src/components/organisms/Calendar/Calendar.test.ts +279 -0
  170. package/src/components/organisms/Calendar/_CalendarDayView.test.ts +104 -0
  171. package/src/components/organisms/Calendar/_CalendarHeader.test.ts +86 -0
  172. package/src/components/organisms/Calendar/_CalendarMonthView.test.ts +52 -0
  173. package/src/components/organisms/Calendar/_CalendarTimePicker.test.ts +89 -0
  174. package/src/components/organisms/Calendar/_CalendarYearView.test.ts +56 -0
  175. package/src/components/organisms/ColorPicker/ColorPicker.stories.ts +229 -0
  176. package/src/components/organisms/ColorPicker/ColorPicker.test.ts +638 -0
  177. package/src/components/organisms/ColorPicker/gradient.test.ts +67 -0
  178. package/src/components/organisms/ColorPicker/gradientGeometry.test.ts +206 -0
  179. package/src/components/organisms/Combobox/Combobox.stories.ts +234 -0
  180. package/src/components/organisms/Combobox/Combobox.test.ts +512 -0
  181. package/src/components/organisms/Combobox/Combobox.vue +1 -1
  182. package/src/components/organisms/ComboboxPanel/ComboboxPanel.stories.ts +158 -0
  183. package/src/components/organisms/ComboboxPanel/ComboboxPanel.test.ts +524 -0
  184. package/src/components/organisms/ComboboxPanel/ComboboxPanel.vue +1 -1
  185. package/src/components/organisms/CountrySelect/CountrySelect.stories.ts +139 -0
  186. package/src/components/organisms/CountrySelect/CountrySelect.test.ts +149 -0
  187. package/src/components/organisms/DatePicker/DatePicker.stories.ts +163 -0
  188. package/src/components/organisms/DatePicker/DatePicker.test.ts +214 -0
  189. package/src/components/organisms/DropdownButton/DropdownButton.stories.ts +123 -0
  190. package/src/components/organisms/DropdownButton/DropdownButton.test.ts +379 -0
  191. package/src/components/organisms/FontPicker/FontPicker.stories.ts +476 -0
  192. package/src/components/organisms/FontPicker/FontPicker.test.ts +873 -0
  193. package/src/components/organisms/Header/Header.stories.ts +75 -0
  194. package/src/components/organisms/Header/Header.test.ts +233 -0
  195. package/src/components/organisms/IconPicker/IconPicker.stories.ts +734 -0
  196. package/src/components/organisms/IconPicker/IconPicker.test.ts +669 -0
  197. package/src/components/organisms/IconPicker/_IconPickerGrid.test.ts +184 -0
  198. package/src/components/organisms/IconPicker/_IconPickerRail.test.ts +102 -0
  199. package/src/components/organisms/IconPicker/useIconPickerNavigation.test.ts +215 -0
  200. package/src/components/organisms/Modal/Modal.stories.ts +350 -0
  201. package/src/components/organisms/Modal/Modal.test.ts +275 -0
  202. package/src/components/organisms/PickerPanel/PickerPanel.stories.ts +452 -0
  203. package/src/components/organisms/PickerPanel/PickerPanel.test.ts +358 -0
  204. package/src/components/organisms/ProgressBar/ProgressBar.stories.ts +100 -0
  205. package/src/components/organisms/ProgressBar/ProgressBar.test.ts +417 -0
  206. package/src/components/organisms/ReferenceCard/ReferenceCard.stories.ts +191 -0
  207. package/src/components/organisms/ReferenceCard/ReferenceCard.test.ts +165 -0
  208. package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.stories.ts +65 -0
  209. package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.test.ts +466 -0
  210. package/src/components/organisms/StepperHeader/StepperHeader.stories.ts +91 -0
  211. package/src/components/organisms/StepperHeader/StepperHeader.test.ts +279 -0
  212. package/src/components/organisms/Table/Table.stories.ts +332 -0
  213. package/src/components/organisms/Table/Table.test.ts +320 -0
  214. package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.stories.ts +198 -0
  215. package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.test.ts +283 -0
  216. package/src/components/organisms/TimelineTrack/TimelineTrack.stories.ts +120 -0
  217. package/src/components/organisms/TimelineTrack/TimelineTrack.test.ts +91 -0
  218. package/src/components/organisms/Toaster/Toaster.stories.ts +51 -0
  219. package/src/components/organisms/Toaster/Toaster.test.ts +75 -0
  220. package/src/components/organisms/VideoControls/VideoControls.stories.ts +792 -0
  221. package/src/components/organisms/VideoControls/VideoControls.test.ts +1056 -0
  222. package/src/components/organisms/VideoPlayer/VideoPlayer.stories.ts +327 -0
  223. package/src/components/organisms/VideoPlayer/VideoPlayer.test.ts +439 -0
@@ -0,0 +1,206 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { expect, userEvent, waitFor, within } from 'storybook/test'
3
+ import { computed, ref } from 'vue'
4
+ import EditorOverrideStatus from './EditorOverrideStatus.vue'
5
+ import EditorColor from '../EditorColor/EditorColor.vue'
6
+ import type { EditorColorThemeRole, EditorColorValue } from '../EditorColor/EditorColor.vue'
7
+ import EditorRadius from '../EditorRadius/EditorRadius.vue'
8
+ import type { EditorRadiusValue } from '../EditorRadius/EditorRadius.vue'
9
+ import Slider from '../../../atoms/Slider/Slider.vue'
10
+
11
+ export default {
12
+ title: 'Components/Editor Components/Molecules/Editor Override Status',
13
+ component: EditorOverrideStatus,
14
+ decorators: [() => ({ template: '<div class="w-100 mx-auto"><story /></div>' })],
15
+ parameters: {
16
+ // New and clean, so axe fails the run here instead of reporting.
17
+ a11y: { test: 'error' },
18
+ },
19
+ } satisfies Meta<typeof EditorOverrideStatus>
20
+
21
+ type Story = StoryObj<typeof EditorOverrideStatus>
22
+
23
+ const THEME_ROLES: EditorColorThemeRole[] = [
24
+ { role: 'main', label: 'Hoofdkleur', hex: '#1777EA' },
25
+ { role: 'accent', label: 'Accentkleur', hex: '#F59E0B' },
26
+ ]
27
+
28
+ /**
29
+ * Any computed CSS colour as sRGB channels. Tailwind v4 colours compute to
30
+ * `oklch()`, so the colour is painted on a 1px canvas and read back.
31
+ */
32
+ const toRgb = (color: string) => {
33
+ const context = document.createElement('canvas').getContext('2d')!
34
+ context.fillStyle = color
35
+ context.fillRect(0, 0, 1, 1)
36
+ const [r, g, b] = context.getImageData(0, 0, 1, 1).data
37
+ return [r, g, b]
38
+ }
39
+
40
+ /** WCAG 2 contrast ratio of two CSS colours. */
41
+ const contrast = (a: string, b: string) => {
42
+ const luminance = (color: string) => {
43
+ const [r, g, b] = toRgb(color).map((channel) => {
44
+ const c = channel / 255
45
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
46
+ })
47
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b
48
+ }
49
+ const [light, dark] = [luminance(a), luminance(b)].sort((x, y) => y - x)
50
+ return (light + 0.05) / (dark + 0.05)
51
+ }
52
+
53
+ /**
54
+ * The house-style line under a colour: always a status, a reset while the
55
+ * colour is the customer's own. Starts changed, so the reset is there.
56
+ */
57
+ export const Theme: Story = {
58
+ render: () => ({
59
+ components: { EditorOverrideStatus, EditorColor },
60
+ setup() {
61
+ const color = ref<EditorColorValue>('bg-[#E11D48]')
62
+ const overridden = computed(() => color.value !== 'bg-theme-main')
63
+ const reset = () => (color.value = 'bg-theme-main')
64
+ return { color, overridden, reset, THEME_ROLES }
65
+ },
66
+ template: `
67
+ <EditorOverrideStatus
68
+ :overridden="overridden"
69
+ label="Volgt huisstijl"
70
+ reset-label="Terug naar huisstijl"
71
+ @reset="reset"
72
+ >
73
+ <EditorColor v-model="color" label="Achtergrond" :theme-roles="THEME_ROLES" />
74
+ </EditorOverrideStatus>
75
+ `,
76
+ }),
77
+ // The ring is measured on the reset button, reached by keyboard: a ring that
78
+ // never shows, or one too faint on white, passes every other check.
79
+ play: async ({ canvasElement }) => {
80
+ const canvas = within(canvasElement)
81
+ const reset = canvas.getByRole('button', { name: 'Terug naar huisstijl' })
82
+
83
+ await userEvent.tab()
84
+ await userEvent.tab()
85
+
86
+ await expect(reset).toHaveFocus()
87
+ await expect(reset.matches(':focus-visible')).toBe(true)
88
+ const style = getComputedStyle(reset)
89
+ await expect(style.outlineStyle).not.toBe('none')
90
+ await expect(contrast(style.outlineColor, 'rgb(255, 255, 255)')).toBeGreaterThanOrEqual(3)
91
+
92
+ await userEvent.keyboard('{Enter}')
93
+
94
+ await waitFor(() => expect(canvas.getByText('Volgt huisstijl')).toBeInTheDocument())
95
+ await expect(canvas.queryByRole('button', { name: 'Terug naar huisstijl' })).toBeNull()
96
+ await expect(document.activeElement).not.toBe(document.body)
97
+ await expect(document.activeElement).toHaveAttribute('aria-haspopup')
98
+ },
99
+ }
100
+
101
+ /** The same line for a fixed standard of the element itself, under a slider. */
102
+ export const Standard: Story = {
103
+ render: () => ({
104
+ components: { EditorOverrideStatus, EditorRadius },
105
+ setup() {
106
+ const radius = ref<EditorRadiusValue>('rounded-2xl')
107
+ const overridden = computed(() => radius.value !== 'rounded-lg')
108
+ const reset = () => (radius.value = 'rounded-lg')
109
+ return { radius, overridden, reset }
110
+ },
111
+ template: `
112
+ <EditorOverrideStatus
113
+ :overridden="overridden"
114
+ label="Standaard"
115
+ reset-label="Terug naar standaard"
116
+ @reset="reset"
117
+ >
118
+ <EditorRadius v-model="radius" />
119
+ </EditorOverrideStatus>
120
+ `,
121
+ }),
122
+ play: async ({ canvasElement }) => {
123
+ const canvas = within(canvasElement)
124
+
125
+ await userEvent.click(canvas.getByRole('button', { name: 'Terug naar standaard' }))
126
+
127
+ await waitFor(() => expect(canvas.getByText('Standaard')).toBeInTheDocument())
128
+ await expect(canvas.getByRole('slider')).toHaveFocus()
129
+ },
130
+ }
131
+
132
+ /**
133
+ * The mobile marker: nothing while the phone follows the desktop, a line with a
134
+ * reset while it differs. After the reset the line goes, and the focus goes to
135
+ * the slider rather than to `body`.
136
+ */
137
+ export const Mobile: Story = {
138
+ render: () => ({
139
+ components: { EditorOverrideStatus, EditorRadius },
140
+ setup() {
141
+ const desktop: EditorRadiusValue = 'rounded-lg'
142
+ const radius = ref<EditorRadiusValue>('rounded-none')
143
+ const overridden = computed(() => radius.value !== desktop)
144
+ const reset = () => (radius.value = desktop)
145
+ return { radius, overridden, reset }
146
+ },
147
+ template: `
148
+ <EditorOverrideStatus
149
+ :overridden="overridden"
150
+ hide-when-default
151
+ label="Gelijk aan desktop"
152
+ overridden-label="Aangepast voor mobiel"
153
+ reset-label="Terug naar desktop"
154
+ @reset="reset"
155
+ >
156
+ <EditorRadius v-model="radius" />
157
+ </EditorOverrideStatus>
158
+ `,
159
+ }),
160
+ play: async ({ canvasElement }) => {
161
+ const canvas = within(canvasElement)
162
+
163
+ await expect(canvas.getByText('Aangepast voor mobiel')).toBeInTheDocument()
164
+ await userEvent.click(canvas.getByRole('button', { name: 'Terug naar desktop' }))
165
+
166
+ await waitFor(() => expect(canvas.queryByText('Aangepast voor mobiel')).toBeNull())
167
+ await expect(canvas.queryByRole('button', { name: 'Terug naar desktop' })).toBeNull()
168
+ await expect(canvas.getByRole('slider')).toHaveFocus()
169
+ },
170
+ }
171
+
172
+ /**
173
+ * The size line of a background image: the zoom as a share of the image's own
174
+ * size, with the value in the overridden label.
175
+ */
176
+ export const Size: Story = {
177
+ render: () => ({
178
+ components: { EditorOverrideStatus, Slider },
179
+ setup() {
180
+ const zoom = ref([150])
181
+ const overridden = computed(() => zoom.value[0] !== 100)
182
+ const reset = () => (zoom.value = [100])
183
+ return { zoom, overridden, reset }
184
+ },
185
+ template: `
186
+ <EditorOverrideStatus
187
+ :overridden="overridden"
188
+ label="Grootte 100% (origineel)"
189
+ :overridden-label="'Grootte ' + zoom[0] + '%'"
190
+ reset-label="Terug naar 100%"
191
+ @reset="reset"
192
+ >
193
+ <Slider v-model="zoom" aria-label="Grootte" :min="25" :max="300" :step="5" />
194
+ </EditorOverrideStatus>
195
+ `,
196
+ }),
197
+ play: async ({ canvasElement }) => {
198
+ const canvas = within(canvasElement)
199
+
200
+ await expect(canvas.getByText('Grootte 150%')).toBeInTheDocument()
201
+ await userEvent.click(canvas.getByRole('button', { name: 'Terug naar 100%' }))
202
+
203
+ await waitFor(() => expect(canvas.getByText('Grootte 100% (origineel)')).toBeInTheDocument())
204
+ await expect(canvas.getByRole('slider')).toHaveFocus()
205
+ },
206
+ }
@@ -0,0 +1,332 @@
1
+ import { describe, it, expect, afterEach, vi } from 'vitest'
2
+ import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
3
+ import { defineComponent, h, ref, nextTick } from 'vue'
4
+ import EditorOverrideStatus from './EditorOverrideStatus.vue'
5
+
6
+ const mounted: VueWrapper[] = []
7
+
8
+ afterEach(() => {
9
+ mounted.splice(0).forEach((wrapper) => wrapper.unmount())
10
+ vi.useRealTimers()
11
+ })
12
+
13
+ const BASE = { label: 'Volgt huisstijl', resetLabel: 'Terug naar huisstijl' }
14
+
15
+ const mountStatus = (props: Record<string, unknown> = {}, slot?: string) => {
16
+ const wrapper = mount(EditorOverrideStatus, {
17
+ props: { overridden: false, ...BASE, ...props },
18
+ slots: slot ? { default: slot } : {},
19
+ attachTo: document.body,
20
+ })
21
+ mounted.push(wrapper)
22
+ return wrapper
23
+ }
24
+
25
+ /**
26
+ * A parent that owns the override and clears it on reset, as a consumer does:
27
+ * the reset button then leaves the DOM in the same update.
28
+ */
29
+ const mountWithParent = (
30
+ props: Record<string, unknown> = {},
31
+ slot = '<button>Kleur</button>',
32
+ errorHandler = vi.fn(),
33
+ ) => {
34
+ const overridden = ref(true)
35
+ const onReset = vi.fn(() => (overridden.value = false))
36
+ const Parent = defineComponent({
37
+ setup: () => () =>
38
+ h(
39
+ EditorOverrideStatus,
40
+ {
41
+ ...BASE,
42
+ ...props,
43
+ overridden: overridden.value,
44
+ onReset,
45
+ },
46
+ { default: () => h('div', { innerHTML: slot }) },
47
+ ),
48
+ })
49
+ // Vue routes a rejected async handler to the app's errorHandler, so a spy
50
+ // there is where an error thrown during the reset would surface.
51
+ const wrapper = mount(Parent, { attachTo: document.body, global: { config: { errorHandler } } })
52
+ mounted.push(wrapper)
53
+ return { wrapper, overridden, onReset, errorHandler }
54
+ }
55
+
56
+ const resetButton = (wrapper: VueWrapper) =>
57
+ wrapper.findAll('button').find((button) => button.text() === BASE.resetLabel)
58
+
59
+ const pressReset = async (wrapper: VueWrapper) => {
60
+ const button = resetButton(wrapper)
61
+ if (!button) throw new Error('No reset button')
62
+ ;(button.element as HTMLElement).focus()
63
+ await button.trigger('click')
64
+ await nextTick()
65
+ }
66
+
67
+ describe('EditorOverrideStatus', () => {
68
+ describe('not overridden', () => {
69
+ it('shows the label and no reset button', () => {
70
+ const wrapper = mountStatus()
71
+
72
+ expect(wrapper.text()).toContain('Volgt huisstijl')
73
+ expect(wrapper.text()).not.toContain('Aangepast')
74
+ expect(resetButton(wrapper)).toBeUndefined()
75
+ })
76
+
77
+ it('renders the slot above the status line', () => {
78
+ const wrapper = mountStatus({}, '<input aria-label="Waarde" />')
79
+
80
+ expect(wrapper.element.firstElementChild?.tagName).toBe('INPUT')
81
+ })
82
+ })
83
+
84
+ describe('overridden', () => {
85
+ it('shows the Dutch default status and the reset button', () => {
86
+ const wrapper = mountStatus({ overridden: true })
87
+
88
+ expect(wrapper.text()).toContain('Aangepast')
89
+ expect(wrapper.text()).not.toContain('Volgt huisstijl')
90
+ expect(resetButton(wrapper)?.attributes('type')).toBe('button')
91
+ })
92
+
93
+ it('shows a custom overridden label', () => {
94
+ const wrapper = mountStatus({ overridden: true, overriddenLabel: 'Grootte 150%' })
95
+
96
+ expect(wrapper.text()).toContain('Grootte 150%')
97
+ })
98
+
99
+ it('hides the dot from assistive technology', () => {
100
+ const wrapper = mountStatus({ overridden: true })
101
+ const dot = wrapper.find('span[aria-hidden="true"]')
102
+
103
+ expect(dot.exists()).toBe(true)
104
+ expect(dot.text()).toBe('')
105
+ })
106
+
107
+ it('emits reset when the button is pressed', async () => {
108
+ const wrapper = mountStatus({ overridden: true })
109
+
110
+ await resetButton(wrapper)!.trigger('click')
111
+
112
+ expect(wrapper.emitted('reset')).toEqual([[]])
113
+ })
114
+ })
115
+
116
+ describe('hideWhenDefault', () => {
117
+ it('renders no status line while not overridden', () => {
118
+ const wrapper = mountStatus({ hideWhenDefault: true }, '<input aria-label="Waarde" />')
119
+
120
+ expect(wrapper.text()).toBe('')
121
+ expect(wrapper.findAll('button')).toHaveLength(0)
122
+ expect(wrapper.find('input').exists()).toBe(true)
123
+ })
124
+
125
+ it('renders the line while overridden', () => {
126
+ const wrapper = mountStatus({ hideWhenDefault: true, overridden: true })
127
+
128
+ expect(wrapper.text()).toContain('Aangepast')
129
+ expect(resetButton(wrapper)).toBeDefined()
130
+ })
131
+ })
132
+
133
+ describe('focus after a reset', () => {
134
+ it('goes to the first focusable element in the slot', async () => {
135
+ const { wrapper } = mountWithParent(
136
+ {},
137
+ '<span>Kleur</span><button disabled>Uit</button><button id="first">Kleur kiezen</button><button>Tweede</button>',
138
+ )
139
+
140
+ await pressReset(wrapper)
141
+
142
+ expect(resetButton(wrapper)).toBeUndefined()
143
+ expect(document.activeElement?.id).toBe('first')
144
+ })
145
+
146
+ it('skips the cells a roving tabindex takes out of the tab order', async () => {
147
+ const { wrapper } = mountWithParent(
148
+ {},
149
+ '<button tabindex="-1">A</button><button id="stop" tabindex="0">B</button>',
150
+ )
151
+
152
+ await pressReset(wrapper)
153
+
154
+ expect(document.activeElement?.id).toBe('stop')
155
+ })
156
+
157
+ it('goes to the focusTarget when it is given', async () => {
158
+ const { wrapper } = mountWithParent(
159
+ { focusTarget: '[role="slider"]' },
160
+ '<button>Eerst</button><span id="thumb" role="slider" tabindex="0" aria-label="Dikte" aria-valuenow="2"></span>',
161
+ )
162
+
163
+ await pressReset(wrapper)
164
+
165
+ expect(document.activeElement?.id).toBe('thumb')
166
+ })
167
+
168
+ it('falls back to the first focusable element when the focusTarget matches nothing', async () => {
169
+ const { wrapper } = mountWithParent(
170
+ { focusTarget: '[role="slider"]' },
171
+ '<button id="first">Kleur kiezen</button>',
172
+ )
173
+
174
+ await pressReset(wrapper)
175
+
176
+ expect(document.activeElement?.id).toBe('first')
177
+ })
178
+
179
+ it('falls back to the component itself when the slot has nothing focusable', async () => {
180
+ const { wrapper } = mountWithParent({ hideWhenDefault: true }, '<span>Alleen tekst</span>')
181
+ const root = wrapper.findComponent(EditorOverrideStatus).element as HTMLElement
182
+
183
+ await pressReset(wrapper)
184
+
185
+ expect(document.activeElement).toBe(root)
186
+ expect(root.getAttribute('tabindex')).toBe('-1')
187
+
188
+ root.blur()
189
+ expect(root.hasAttribute('tabindex')).toBe(false)
190
+ })
191
+
192
+ // `'[role=slider'` is no test case: CSS closes an open bracket at the end, so
193
+ // both Chromium and jsdom accept it. A dangling colon does throw.
194
+ it('still emits and falls back when the focusTarget is not a valid selector', async () => {
195
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
196
+ const { wrapper, onReset, errorHandler } = mountWithParent(
197
+ { focusTarget: 'button:' },
198
+ '<button id="first">Kleur kiezen</button>',
199
+ )
200
+
201
+ await pressReset(wrapper)
202
+ await flushPromises()
203
+
204
+ expect(onReset).toHaveBeenCalledOnce()
205
+ expect(document.activeElement?.id).toBe('first')
206
+ expect(errorHandler).not.toHaveBeenCalled()
207
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('"button:"'))
208
+ warn.mockRestore()
209
+ })
210
+
211
+ it('takes the temporary tabindex off again when the root cannot take the focus', async () => {
212
+ const { wrapper } = mountWithParent({ hideWhenDefault: true }, '<span>Alleen tekst</span>')
213
+ const root = wrapper.findComponent(EditorOverrideStatus).element as HTMLElement
214
+ // A root that refuses the focus, as a hidden one would.
215
+ root.focus = () => {}
216
+
217
+ await pressReset(wrapper)
218
+
219
+ expect(document.activeElement).not.toBe(root)
220
+ expect(root.hasAttribute('tabindex')).toBe(false)
221
+ })
222
+
223
+ it('never leaves the focus on body, in any variant', async () => {
224
+ const variants = [
225
+ [{}, '<button>Kleur</button>'],
226
+ [{ hideWhenDefault: true }, '<button>Kleur</button>'],
227
+ [{ focusTarget: '.missing' }, '<span>Tekst</span>'],
228
+ [{ hideWhenDefault: true }, ''],
229
+ ] as const
230
+
231
+ for (const [props, slot] of variants) {
232
+ const { wrapper } = mountWithParent(props, slot)
233
+
234
+ await pressReset(wrapper)
235
+
236
+ expect(document.activeElement).not.toBe(document.body)
237
+ expect(wrapper.element.contains(document.activeElement)).toBe(true)
238
+ }
239
+ })
240
+
241
+ // A control that the reset re-renders loses the focus a second time; the
242
+ // check after the update catches that.
243
+ it('refocuses when the reset replaces the control', async () => {
244
+ const overridden = ref(true)
245
+ const Parent = defineComponent({
246
+ setup: () => () =>
247
+ h(
248
+ EditorOverrideStatus,
249
+ {
250
+ ...BASE,
251
+ overridden: overridden.value,
252
+ onReset: () => (overridden.value = false),
253
+ },
254
+ {
255
+ default: () => h('button', { key: String(overridden.value), id: 'control' }, 'Kleur'),
256
+ },
257
+ ),
258
+ })
259
+ const wrapper = mount(Parent, { attachTo: document.body })
260
+ mounted.push(wrapper)
261
+
262
+ await pressReset(wrapper)
263
+
264
+ expect(document.activeElement?.id).toBe('control')
265
+ expect(document.activeElement?.isConnected).toBe(true)
266
+ })
267
+ })
268
+
269
+ describe('announcements', () => {
270
+ const region = (wrapper: VueWrapper) => wrapper.get('[role="status"]')
271
+
272
+ it('announces the new status after a reset, then clears it', async () => {
273
+ vi.useFakeTimers()
274
+ const { wrapper } = mountWithParent()
275
+
276
+ await pressReset(wrapper)
277
+
278
+ expect(region(wrapper).text()).toBe('Volgt huisstijl')
279
+
280
+ vi.advanceTimersByTime(1000)
281
+ await nextTick()
282
+ expect(region(wrapper).text()).toBe('')
283
+ })
284
+
285
+ it('announces a change made in the control', async () => {
286
+ const wrapper = mountStatus({}, '<input aria-label="Waarde" />')
287
+ ;(wrapper.get('input').element as HTMLElement).focus()
288
+
289
+ await wrapper.setProps({ overridden: true })
290
+
291
+ expect(region(wrapper).text()).toBe('Aangepast')
292
+ })
293
+
294
+ // The control announces its own value; "Grootte 110%" on every arrow key
295
+ // would only repeat it.
296
+ it('stays silent when only the overridden label changes', async () => {
297
+ const wrapper = mountStatus(
298
+ { overridden: true, overriddenLabel: 'Grootte 105%' },
299
+ '<input aria-label="Grootte" />',
300
+ )
301
+ ;(wrapper.get('input').element as HTMLElement).focus()
302
+
303
+ await wrapper.setProps({ overriddenLabel: 'Grootte 110%' })
304
+
305
+ expect(region(wrapper).text()).toBe('')
306
+ })
307
+
308
+ it('announces the label that belongs to the new state on a reset', async () => {
309
+ const wrapper = mountStatus(
310
+ {
311
+ overridden: true,
312
+ overriddenLabel: 'Grootte 110%',
313
+ label: 'Grootte 100% (origineel)',
314
+ },
315
+ '<input aria-label="Grootte" />',
316
+ )
317
+ ;(wrapper.get('input').element as HTMLElement).focus()
318
+
319
+ await wrapper.setProps({ overridden: false })
320
+
321
+ expect(region(wrapper).text()).toBe('Grootte 100% (origineel)')
322
+ })
323
+
324
+ it('stays silent when the status changes while the focus is elsewhere', async () => {
325
+ const wrapper = mountStatus({}, '<input aria-label="Waarde" />')
326
+
327
+ await wrapper.setProps({ overridden: true })
328
+
329
+ expect(region(wrapper).text()).toBe('')
330
+ })
331
+ })
332
+ })