@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,327 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { expect, userEvent, waitFor, within } from 'storybook/test'
3
+ import VideoPlayer from './VideoPlayer.vue'
4
+
5
+ /** A 14-second VP9 tour from .storybook/public; WebM, so the Chromium of CI plays it too. */
6
+ const VIDEO = '/demo-tour.webm'
7
+ /** The first frame of VIDEO. */
8
+ const POSTER = '/demo-tour-poster.jpg'
9
+ /** The same tour cut to 9:16 (540×960), as a phone or TikTok video, and its first frame. */
10
+ const VERTICAL_VIDEO = '/demo-tour-vertical.webm'
11
+ const VERTICAL_POSTER = '/demo-tour-vertical-poster.jpg'
12
+
13
+ const meta = {
14
+ title: 'Components/Organisms/VideoPlayer',
15
+ component: VideoPlayer,
16
+ decorators: [() => ({ template: '<div class="w-120"><story /></div>' })],
17
+ // The rebuilt player is clean: axe runs as a gate here, not in the repo-wide reporting mode
18
+ parameters: { a11y: { test: 'error' } },
19
+ args: {
20
+ src: VIDEO,
21
+ name: 'Rondleiding',
22
+ controls: 'full',
23
+ },
24
+ argTypes: {
25
+ src: { control: 'text' },
26
+ poster: { control: 'text' },
27
+ name: { control: 'text' },
28
+ autoplay: { control: 'boolean' },
29
+ muted: { control: 'boolean' },
30
+ loop: { control: 'boolean' },
31
+ controls: { control: 'inline-radio', options: ['full', 'toggle', 'none'] },
32
+ },
33
+ } satisfies Meta<typeof VideoPlayer>
34
+
35
+ export default meta
36
+
37
+ type Story = StoryObj<typeof meta>
38
+
39
+ /** sRGB of a CSS colour over `backdrop`, via a 1px canvas (handles oklch and alpha). */
40
+ const composite = (color: string, backdrop = 'transparent') => {
41
+ const context = document.createElement('canvas').getContext('2d')!
42
+ context.fillStyle = backdrop
43
+ context.fillRect(0, 0, 1, 1)
44
+ context.fillStyle = color
45
+ context.fillRect(0, 0, 1, 1)
46
+ return [...context.getImageData(0, 0, 1, 1).data.slice(0, 3)]
47
+ }
48
+
49
+ const contrast = (a: number[], b: number[]) => {
50
+ const luminance = (rgb: number[]) => {
51
+ const [r, g, b] = rgb.map((channel) => {
52
+ const c = channel / 255
53
+ return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
54
+ })
55
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b
56
+ }
57
+ const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x)
58
+ return (hi + 0.05) / (lo + 0.05)
59
+ }
60
+
61
+ /**
62
+ * The big play button's focus ring lies on the frame: blue-600 against the white band holds
63
+ * 3:1, and on a white or a black frame at least one band does.
64
+ */
65
+ const expectStartRing = async (button: HTMLElement) => {
66
+ const style = getComputedStyle(button)
67
+ await expect(style.outlineStyle).toBe('solid')
68
+ const ring = style.boxShadow
69
+ .split(/,(?![^(]*\))/)
70
+ .find((shadow) => / 2px$/.test(shadow.trim()))
71
+ ?.match(/(?:rgba?|oklch|oklab|color)\([^)]*\)/)?.[0]
72
+ await expect(ring).toBeDefined()
73
+ const [outline, band] = [composite(style.outlineColor), composite(ring!)]
74
+ await expect(contrast(outline, band)).toBeGreaterThanOrEqual(3)
75
+ for (const frame of ['#ffffff', '#000000']) {
76
+ const backdrop = composite(frame)
77
+ await expect(
78
+ Math.max(contrast(outline, backdrop), contrast(band, backdrop)),
79
+ ).toBeGreaterThanOrEqual(3)
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Before the first play a big play button stands over the video, the one tab stop; the bar
85
+ * waits behind it. Started, the button fades and the focus goes to the bar's play button.
86
+ */
87
+ export const Full: Story = {
88
+ play: async ({ canvasElement }) => {
89
+ const canvas = within(canvasElement)
90
+ const root = canvasElement.querySelector<HTMLElement>('[data-video-player]')!
91
+ const video = canvasElement.querySelector('video')!
92
+
93
+ // The begin state: only the big button is reachable
94
+ const start = canvas.getByRole('button', { name: 'Afspelen: Rondleiding' })
95
+ await expect(canvas.queryByRole('group')).toBeNull()
96
+ await expect(start.getBoundingClientRect().width).toBe(64)
97
+ await userEvent.tab()
98
+ await expect(start).toHaveFocus()
99
+ await expectStartRing(start)
100
+
101
+ // Muted here only: a test has no real gesture, and the browser plays sound only after one
102
+ video.muted = true
103
+
104
+ // Enter starts it; the focus lands on the bar's play button, and as keyboard focus the bar
105
+ // stays while the video plays
106
+ await userEvent.keyboard('{Enter}')
107
+ await waitFor(() => expect(video.paused).toBe(false), { timeout: 5000 })
108
+ const barPlay = canvas.getByRole('button', { name: 'Pauzeren' })
109
+ await waitFor(() => expect(barPlay).toHaveFocus())
110
+ await waitFor(() => expect(canvasElement.querySelector('[data-video-start]')).toBeNull())
111
+ const bar = canvas.getByRole('group', { name: 'Videobediening: Rondleiding' })
112
+ // Past the hide delay on the video's own clock, rather than a fixed sleep
113
+ const playedFrom = video.currentTime
114
+ await waitFor(() => expect(video.currentTime - playedFrom).toBeGreaterThan(2.8), {
115
+ timeout: 10000,
116
+ })
117
+ await expect(getComputedStyle(bar).opacity).toBe('1')
118
+ video.pause()
119
+
120
+ // Paused again, the big button stays away
121
+ await expect(canvasElement.querySelector('[data-video-start]')).toBeNull()
122
+ // Loaded: the bar knows the duration
123
+ await waitFor(
124
+ () =>
125
+ expect(canvas.getByRole('slider', { name: 'Tijdlijn' })).toHaveAttribute(
126
+ 'aria-valuetext',
127
+ expect.stringMatching(/ van 0:13$/),
128
+ ),
129
+ { timeout: 5000 },
130
+ )
131
+ await expect(getComputedStyle(root).borderTopLeftRadius).toBe('8px')
132
+ },
133
+ }
134
+
135
+ /** The round play/pause button, with mute beside it on hover; a click on the video does nothing. */
136
+ export const Toggle: Story = {
137
+ args: { controls: 'toggle' },
138
+ play: async ({ canvasElement }) => {
139
+ const canvas = within(canvasElement)
140
+ const video = canvasElement.querySelector('video')!
141
+
142
+ await waitFor(() => expect(video.readyState).toBeGreaterThanOrEqual(1), { timeout: 5000 })
143
+ await expect(canvas.queryByRole('alert')).toBeNull()
144
+
145
+ // Before the first play only the big button is reachable; a click on it starts the video
146
+ await expect(canvas.getAllByRole('button')).toHaveLength(1)
147
+ // Muted here only: a test has no real gesture, and the browser plays sound only after one
148
+ video.muted = true
149
+ await userEvent.click(canvas.getByRole('button', { name: 'Afspelen: Rondleiding' }))
150
+ await waitFor(() => expect(video.paused).toBe(false), { timeout: 5000 })
151
+ await waitFor(() => expect(canvasElement.querySelector('[data-video-start]')).toBeNull())
152
+
153
+ // After a mouse click the focus moves into the corner, but the corner still hides
154
+ const corner = canvasElement.querySelector<HTMLElement>('[data-video-controls]')!
155
+ await expect(corner.contains(document.activeElement)).toBe(true)
156
+ await userEvent.unhover(video)
157
+ await waitFor(() => expect(getComputedStyle(corner).opacity).toBe('0'), { timeout: 4000 })
158
+ video.pause()
159
+
160
+ await waitFor(() =>
161
+ expect(canvas.getAllByRole('button').map((b) => b.getAttribute('aria-label'))).toEqual([
162
+ 'Geluid aan: Rondleiding',
163
+ 'Afspelen: Rondleiding',
164
+ ]),
165
+ )
166
+ },
167
+ }
168
+
169
+ /**
170
+ * No visible controls, and no click handler: a click on the video reaches the parent (the
171
+ * media library selects with it). Played and paused from outside, as on hover there.
172
+ */
173
+ export const None: Story = {
174
+ args: { controls: 'none', autoplay: true, muted: true, loop: true },
175
+ play: async ({ canvasElement }) => {
176
+ const video = canvasElement.querySelector('video')!
177
+ await expect(within(canvasElement).queryByRole('button')).toBeNull()
178
+ await waitFor(() => expect(video.paused).toBe(false), { timeout: 5000 })
179
+ await expect(video.muted).toBe(true)
180
+ video.pause()
181
+ },
182
+ }
183
+
184
+ /**
185
+ * With a poster nothing is preloaded: the poster stands, with the big play button over it,
186
+ * until the video plays.
187
+ */
188
+ export const Poster: Story = {
189
+ args: { poster: POSTER },
190
+ play: async ({ canvasElement }) => {
191
+ const video = canvasElement.querySelector('video')!
192
+ await expect(
193
+ within(canvasElement).getByRole('button', { name: 'Afspelen: Rondleiding' }),
194
+ ).toBeVisible()
195
+ await expect(video).toHaveAttribute('preload', 'none')
196
+ await expect(video).toHaveAttribute('poster', POSTER)
197
+ },
198
+ }
199
+
200
+ /** A source that does not load: a message, never the URL. */
201
+ export const Error: Story = {
202
+ args: { src: '/missing-video.webm' },
203
+ play: async ({ canvasElement }) => {
204
+ const alert = await within(canvasElement).findByRole('alert', {}, { timeout: 5000 })
205
+ await expect(alert).toHaveTextContent('De video kan niet worden geladen')
206
+ await expect(alert.innerHTML).not.toContain('missing-video')
207
+ await expect(within(canvasElement).queryByRole('group')).toBeNull()
208
+ },
209
+ }
210
+
211
+ /** The radius is a default: a `rounded-*` class from the consumer wins. */
212
+ export const CustomRadius: Story = {
213
+ render: (args) => ({
214
+ components: { VideoPlayer },
215
+ setup: () => ({ args }),
216
+ template: '<VideoPlayer v-bind="args" class="rounded-3xl" />',
217
+ }),
218
+ play: async ({ canvasElement }) => {
219
+ const root = canvasElement.querySelector<HTMLElement>('[data-video-player]')!
220
+ await expect(getComputedStyle(root).borderTopLeftRadius).toBe('24px')
221
+ },
222
+ }
223
+
224
+ /** Autoplay starts without sound; the bar has the sound button to turn it on. */
225
+ export const Autoplay: Story = {
226
+ args: { autoplay: true, muted: true, loop: true },
227
+ play: async ({ canvasElement }) => {
228
+ const canvas = within(canvasElement)
229
+ const video = canvasElement.querySelector('video')!
230
+
231
+ await waitFor(() => expect(video.paused).toBe(false), { timeout: 5000 })
232
+ await expect(video.muted).toBe(true)
233
+
234
+ // Untouched while it plays: the bar fades and the cursor goes with it
235
+ const root = canvasElement.querySelector<HTMLElement>('[data-video-player]')!
236
+ await waitFor(() => expect(getComputedStyle(root).cursor).toBe('none'), { timeout: 4000 })
237
+
238
+ // A hidden bar lets clicks through; moving the mouse brings it back first
239
+ await userEvent.hover(video)
240
+ await expect(getComputedStyle(root).cursor).not.toBe('none')
241
+ await userEvent.click(canvas.getByRole('button', { name: 'Geluid aan' }))
242
+ await expect(video.muted).toBe(false)
243
+ video.pause()
244
+ },
245
+ }
246
+
247
+ /**
248
+ * A 9:16 video in a portrait frame of 360 by 640, filled the way a consumer does it (as the
249
+ * media library): `size-full` on the player and `object-cover` on its video.
250
+ */
251
+ const verticalRender =
252
+ (frameClass = 'w-90'): Story['render'] =>
253
+ (args) => ({
254
+ components: { VideoPlayer },
255
+ setup: () => ({ args, frameClass }),
256
+ template: `
257
+ <div data-frame class="aspect-9/16" :class="frameClass">
258
+ <VideoPlayer v-bind="args" class="size-full [&>video]:h-full [&>video]:object-cover" />
259
+ </div>
260
+ `,
261
+ })
262
+
263
+ /** Portrait with the full bar; the volume opens in a popover above the sound button. */
264
+ export const Vertical: Story = {
265
+ args: { src: VERTICAL_VIDEO, poster: VERTICAL_POSTER, muted: true },
266
+ render: verticalRender(),
267
+ play: async ({ canvasElement }) => {
268
+ const canvas = within(canvasElement)
269
+ const frame = canvasElement.querySelector<HTMLElement>('[data-frame]')!.getBoundingClientRect()
270
+ const video = canvasElement.querySelector('video')!
271
+
272
+ // Plays in portrait (also in the Chromium of CI), started with the big button
273
+ await userEvent.click(canvas.getByRole('button', { name: 'Afspelen: Rondleiding' }))
274
+ await waitFor(() => expect(video.currentTime).toBeGreaterThan(0.3), { timeout: 5000 })
275
+ await expect(video.videoHeight / video.videoWidth).toBeCloseTo(16 / 9, 2)
276
+ video.pause()
277
+
278
+ // Fills the frame, cropped rather than stretched
279
+ const box = video.getBoundingClientRect()
280
+ await expect([box.width, box.height]).toEqual([frame.width, frame.height])
281
+ await expect(getComputedStyle(video).objectFit).toBe('cover')
282
+
283
+ // The bar fits the width; the volume popover fits inside the frame, clear of the corners
284
+ const capsule = canvas.getByRole('group').getBoundingClientRect()
285
+ await expect(capsule.left).toBeGreaterThanOrEqual(frame.left)
286
+ await expect(capsule.right).toBeLessThanOrEqual(frame.right)
287
+ canvas.getByRole('button', { name: /^Geluid/ }).focus()
288
+ const popover = canvasElement.querySelector<HTMLElement>('[data-volume-popover]')!
289
+ await waitFor(() => expect(getComputedStyle(popover).opacity).toBe('1'))
290
+ const volumeBox = popover.firstElementChild!.getBoundingClientRect()
291
+ await expect(volumeBox.top).toBeGreaterThanOrEqual(frame.top + 8)
292
+ await expect(volumeBox.right).toBeLessThanOrEqual(frame.right)
293
+ await expect(canvas.getByRole('slider', { name: 'Tijdlijn' })).toBeVisible()
294
+ },
295
+ }
296
+
297
+ /**
298
+ * A narrow portrait player (216px) with the corner toggle. Below 240px wide the big play button
299
+ * is 48px instead of 64, centred over the poster.
300
+ */
301
+ export const VerticalToggle: Story = {
302
+ args: { src: VERTICAL_VIDEO, poster: VERTICAL_POSTER, controls: 'toggle' },
303
+ render: verticalRender('w-54'),
304
+ play: async ({ canvasElement }) => {
305
+ const canvas = within(canvasElement)
306
+ const frame = canvasElement.querySelector<HTMLElement>('[data-frame]')!.getBoundingClientRect()
307
+ const start = canvas.getByRole('button', { name: 'Afspelen: Rondleiding' })
308
+ const box = start.getBoundingClientRect()
309
+ await expect(box.width).toBe(48)
310
+ await expect(
311
+ Math.abs(box.left + box.width / 2 - (frame.left + frame.width / 2)),
312
+ ).toBeLessThanOrEqual(0.5)
313
+
314
+ // Muted here only: a test has no real gesture, and the browser plays sound only after one
315
+ canvasElement.querySelector('video')!.muted = true
316
+ await userEvent.click(start)
317
+ await waitFor(() => expect(canvasElement.querySelector('[data-video-start]')).toBeNull(), {
318
+ timeout: 5000,
319
+ })
320
+ canvasElement.querySelector('video')!.pause()
321
+ const corner = (
322
+ await canvas.findByRole('button', { name: 'Afspelen: Rondleiding' })
323
+ ).getBoundingClientRect()
324
+ await expect(corner.right).toBeLessThanOrEqual(frame.right)
325
+ await expect(corner.bottom).toBeLessThanOrEqual(frame.bottom)
326
+ },
327
+ }