@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,476 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite'
2
+ import { expect, fn, userEvent, waitFor, within } from 'storybook/test'
3
+ import { ref } from 'vue'
4
+ import FontPicker, { type NexxtFontPickerFont } from './FontPicker.vue'
5
+
6
+ const WEB_SAFE: NexxtFontPickerFont[] = [
7
+ 'Arial',
8
+ 'Helvetica',
9
+ 'Verdana',
10
+ 'Tahoma',
11
+ 'Trebuchet MS',
12
+ 'Georgia',
13
+ 'Times New Roman',
14
+ 'Palatino',
15
+ 'Courier New',
16
+ ].map((family) => ({ family, source: 'web-safe' }))
17
+
18
+ /** A slice of the Google catalogue, by popularity. Stories load no faces, so rows use the fallback. */
19
+ const GOOGLE: NexxtFontPickerFont[] = [
20
+ 'Roboto',
21
+ 'Open Sans',
22
+ 'Noto Sans JP',
23
+ 'Montserrat',
24
+ 'Inter',
25
+ 'Poppins',
26
+ 'Lato',
27
+ 'Roboto Condensed',
28
+ 'Oswald',
29
+ 'Raleway',
30
+ 'Nunito',
31
+ 'Ubuntu',
32
+ 'Playfair Display',
33
+ 'Rubik',
34
+ 'Merriweather',
35
+ 'Work Sans',
36
+ 'Kanit',
37
+ 'PT Sans',
38
+ 'Lora',
39
+ 'Fira Sans',
40
+ 'Quicksand',
41
+ 'Barlow',
42
+ 'Mulish',
43
+ 'Inconsolata',
44
+ 'IBM Plex Sans',
45
+ 'Titillium Web',
46
+ 'DM Sans',
47
+ 'Heebo',
48
+ 'Libre Baskerville',
49
+ 'Karla',
50
+ 'Josefin Sans',
51
+ 'Dancing Script',
52
+ 'Source Code Pro',
53
+ 'Libre Franklin',
54
+ 'Cabin',
55
+ 'Bebas Neue',
56
+ 'Arimo',
57
+ 'Manrope',
58
+ 'Hind Siliguri',
59
+ 'Space Grotesk',
60
+ ].map((family) => ({ family, source: 'google' }))
61
+
62
+ const FONTS = [...WEB_SAFE, ...GOOGLE]
63
+
64
+ const LONG_FAMILY = 'Noto Serif Display Extra Condensed Semi Expanded Italic'
65
+
66
+ export default {
67
+ title: 'Components/Organisms/FontPicker',
68
+ component: FontPicker,
69
+ // Room for the 360px panel under the trigger, also when Docs renders the story inline
70
+ decorators: [() => ({ template: '<div class="min-h-112 p-2"><story /></div>' })],
71
+ args: {
72
+ fonts: FONTS,
73
+ catalogueStatus: 'ready',
74
+ loadFont: fn(),
75
+ onOpen: fn(),
76
+ onClose: fn(),
77
+ onRetry: fn(),
78
+ },
79
+ argTypes: {
80
+ catalogueStatus: { control: 'inline-radio', options: ['loading', 'ready', 'error'] },
81
+ },
82
+ } satisfies Meta<typeof FontPicker>
83
+
84
+ type Story = StoryObj<typeof FontPicker>
85
+
86
+ /** The settings panel of the editor is 312px of content; the panel is 320 and sticks out 8px. */
87
+ const EDITOR_WIDTH = 'w-78'
88
+
89
+ const renderPicker =
90
+ (initial: string | null, frame = EDITOR_WIDTH) =>
91
+ (args: Story['args']) => ({
92
+ components: { FontPicker },
93
+ setup() {
94
+ const font = ref<string | null>(initial)
95
+ return { args, font, frame }
96
+ },
97
+ template: `
98
+ <div class="flex flex-col gap-3">
99
+ <div :class="frame">
100
+ <FontPicker v-bind="args" v-model="font" />
101
+ </div>
102
+ <p class="extra-small-normal text-gray-700">
103
+ model: <code data-testid="model">{{ font === null ? 'null' : font }}</code>
104
+ </p>
105
+ </div>
106
+ `,
107
+ })
108
+
109
+ const triggerOf = (canvasElement: HTMLElement) =>
110
+ canvasElement.querySelector<HTMLButtonElement>('button[aria-haspopup="listbox"]')!
111
+
112
+ const openPanel = async (canvasElement: HTMLElement) => {
113
+ const canvas = within(canvasElement)
114
+ await userEvent.click(triggerOf(canvasElement))
115
+ const input = await canvas.findByRole('combobox')
116
+ await waitFor(() => expect(input).toHaveFocus())
117
+ return input
118
+ }
119
+
120
+ /** The panel scales in for 200ms; layout sizes are not scaled, rects are. */
121
+ const settled = (element: HTMLElement) =>
122
+ waitFor(() => expect(getComputedStyle(element.closest('.z-50 > div')!).opacity).toBe('1'))
123
+
124
+ const activeOption = (input: HTMLElement) =>
125
+ document.getElementById(input.getAttribute('aria-activedescendant') ?? '')
126
+
127
+ /**
128
+ * The editor: no visible label, `inherit` on top. `null` on the model is "Huisstijl", so the
129
+ * trigger reads "Huisstijl · Montserrat" in the house-style font.
130
+ */
131
+ export const Editor: Story = {
132
+ args: {
133
+ ariaLabel: 'Lettertype',
134
+ inherit: { label: 'Huisstijl', family: 'Georgia' },
135
+ },
136
+ render: renderPicker(null),
137
+ play: async ({ canvasElement, args }) => {
138
+ const canvas = within(canvasElement)
139
+ const trigger = triggerOf(canvasElement)
140
+
141
+ // the visible value is part of the name
142
+ await expect(trigger).toHaveAccessibleName('Lettertype, Huisstijl · Georgia')
143
+ const value = trigger.querySelector<HTMLElement>('[id$="-value"]')!
144
+ await expect(value.style.fontFamily).toContain('Georgia')
145
+
146
+ const input = await openPanel(canvasElement)
147
+ await expect(args.onOpen).toHaveBeenCalledOnce()
148
+
149
+ // Huisstijl first and ungrouped, then the two groups; the current value is highlighted
150
+ const listbox = canvas.getByRole('listbox')
151
+ const first = listbox.firstElementChild as HTMLElement
152
+ await expect(first).toHaveAttribute('role', 'option')
153
+ await expect(activeOption(input)).toBe(first)
154
+ await expect(first).toHaveAccessibleDescription(
155
+ 'Huidige keuze. Volgt de huisstijl, ook als die later verandert',
156
+ )
157
+ await settled(listbox)
158
+ await expect(canvas.getByRole('group', { name: 'Standaard' })).toBeVisible()
159
+ await expect(canvas.getByRole('group', { name: 'Google Fonts' })).toBeInTheDocument()
160
+
161
+ // 320px wide against a 312px trigger, and inside the viewport
162
+ const panel = canvas.getByRole('group', { name: 'Lettertype' })
163
+ await expect(panel.offsetWidth).toBe(320)
164
+ await expect(panel.getBoundingClientRect().right).toBeLessThanOrEqual(
165
+ document.documentElement.clientWidth,
166
+ )
167
+
168
+ // a choice writes the model, closes the panel and puts focus back on the trigger
169
+ await userEvent.click(canvas.getByRole('option', { name: 'Verdana' }))
170
+ await waitFor(() => expect(canvas.queryByRole('listbox')).toBeNull())
171
+ await expect(trigger).toHaveFocus()
172
+ await expect(canvas.getByTestId('model')).toHaveTextContent('Verdana')
173
+ await expect(trigger).toHaveAccessibleName('Lettertype, Verdana')
174
+ },
175
+ }
176
+
177
+ /** The house style has no font of its own: just "Huisstijl", in the sans font. */
178
+ export const InheritWithoutFont: Story = {
179
+ args: {
180
+ ariaLabel: 'Lettertype',
181
+ inherit: { label: 'Huisstijl' },
182
+ },
183
+ render: renderPicker(null),
184
+ play: async ({ canvasElement }) => {
185
+ await expect(triggerOf(canvasElement)).toHaveAccessibleName('Lettertype, Huisstijl')
186
+ },
187
+ }
188
+
189
+ /**
190
+ * A form: visible label, placeholder, no inherit option. The panel follows the trigger's width
191
+ * (at least 320px).
192
+ */
193
+ export const Form: Story = {
194
+ args: {
195
+ label: 'Lettertype voor koppen',
196
+ placeholder: 'Kies een lettertype',
197
+ },
198
+ render: renderPicker(null, 'w-96'),
199
+ play: async ({ canvasElement }) => {
200
+ const canvas = within(canvasElement)
201
+ const trigger = triggerOf(canvasElement)
202
+
203
+ await expect(trigger).toHaveAccessibleName('Lettertype voor koppen Kies een lettertype')
204
+ await openPanel(canvasElement)
205
+
206
+ const panel = canvas.getByRole('group', { name: 'Lettertype voor koppen' })
207
+ await expect(panel.offsetWidth).toBe(trigger.offsetWidth)
208
+ },
209
+ }
210
+
211
+ /** A long name is cut off with an ellipsis; the trigger keeps the width of its container. */
212
+ export const LongName: Story = {
213
+ args: {
214
+ ariaLabel: 'Lettertype',
215
+ inherit: { label: 'Huisstijl', family: LONG_FAMILY },
216
+ },
217
+ render: renderPicker(null),
218
+ play: async ({ canvasElement }) => {
219
+ const trigger = triggerOf(canvasElement)
220
+ const frame = trigger.closest('.w-78')!
221
+ const [label, family] = Array.from(
222
+ trigger.querySelectorAll<HTMLElement>('[id$="-value"] > span'),
223
+ )
224
+
225
+ await expect(trigger.getBoundingClientRect().width).toBe(frame.getBoundingClientRect().width)
226
+ // "Huisstijl" stays whole, the family is the part that truncates
227
+ await expect(label.scrollWidth).toBe(label.clientWidth)
228
+ await expect(family.scrollWidth).toBeGreaterThan(family.clientWidth)
229
+ },
230
+ }
231
+
232
+ /** The current family is no longer in the catalogue; it is still listed, at the top. */
233
+ export const CurrentNotInCatalogue: Story = {
234
+ args: { ariaLabel: 'Lettertype' },
235
+ render: renderPicker('Comic Neue'),
236
+ play: async ({ canvasElement }) => {
237
+ const input = await openPanel(canvasElement)
238
+ const first = within(canvasElement).getByRole('listbox').firstElementChild
239
+
240
+ await expect(activeOption(input)).toBe(first)
241
+ await expect(first).toHaveAccessibleName('Comic Neue')
242
+ await expect(first).toHaveAccessibleDescription('Huidige keuze')
243
+ },
244
+ }
245
+
246
+ /**
247
+ * The catalogue is on its way: the web-safe families can be chosen right away, with skeleton rows
248
+ * under "Google Fonts". A search without results keeps the skeletons; there is no empty state yet.
249
+ */
250
+ export const Loading: Story = {
251
+ args: {
252
+ ariaLabel: 'Lettertype',
253
+ fonts: WEB_SAFE,
254
+ catalogueStatus: 'loading',
255
+ inherit: { label: 'Huisstijl', family: 'Georgia' },
256
+ },
257
+ render: renderPicker(null),
258
+ play: async ({ canvasElement }) => {
259
+ const canvas = within(canvasElement)
260
+ const input = await openPanel(canvasElement)
261
+ const listbox = canvas.getByRole('listbox')
262
+
263
+ await expect(listbox).toHaveAttribute('aria-busy', 'true')
264
+ await settled(listbox)
265
+ await expect(canvas.getByRole('option', { name: 'Arial' })).toBeVisible()
266
+ await expect(canvas.queryByRole('option', { name: 'Roboto' })).toBeNull()
267
+ await expect(listbox.querySelectorAll('[aria-hidden="true"] .animate-pulse')).toHaveLength(6)
268
+
269
+ // grouped rows and the skeletons start 12px in from the group heading; Huisstijl does not
270
+ const heading = canvas.getByText('Standaard')
271
+ const left = (element: Element) => Math.round(element.getBoundingClientRect().left)
272
+ const textOf = (name: string) =>
273
+ canvas
274
+ .getByRole('option', { name: new RegExp(`^${name}`) })
275
+ .querySelector('[data-font-family] > span')!
276
+ const step = Math.round(12 * (heading.getBoundingClientRect().width / heading.offsetWidth))
277
+ await expect(left(textOf('Arial'))).toBe(left(heading) + step)
278
+ await expect(left(textOf('Huisstijl'))).toBe(left(heading))
279
+ await expect(left(listbox.querySelector('.animate-pulse')!)).toBe(left(textOf('Arial')))
280
+
281
+ await userEvent.type(input, 'roboto')
282
+ await expect(canvas.queryByText('Geen lettertypes gevonden')).toBeNull()
283
+ await expect(listbox.querySelectorAll('.animate-pulse')).toHaveLength(6)
284
+ },
285
+ }
286
+
287
+ /**
288
+ * The catalogue failed: only Huisstijl and "Standaard", and a notice under the list. "Opnieuw
289
+ * proberen" really fetches again here: it goes to loading, and after a moment to ready.
290
+ */
291
+ export const CatalogueError: Story = {
292
+ args: {
293
+ ariaLabel: 'Lettertype',
294
+ inherit: { label: 'Huisstijl', family: 'Georgia' },
295
+ },
296
+ render: (args) => ({
297
+ components: { FontPicker },
298
+ setup() {
299
+ const font = ref<string | null>(null)
300
+ const status = ref<'loading' | 'ready' | 'error'>('error')
301
+ const fonts = ref<NexxtFontPickerFont[]>(WEB_SAFE)
302
+ // `args.onRetry` is bound through v-bind as well, so the spy sees the click on its own
303
+ const onRetry = () => {
304
+ status.value = 'loading'
305
+ setTimeout(() => {
306
+ fonts.value = FONTS
307
+ status.value = 'ready'
308
+ }, 600)
309
+ }
310
+ return { args, font, status, fonts, onRetry }
311
+ },
312
+ template: `
313
+ <div class="w-78">
314
+ <FontPicker v-bind="args" v-model="font" :fonts="fonts" :catalogue-status="status" @retry="onRetry" />
315
+ </div>
316
+ `,
317
+ }),
318
+ play: async ({ canvasElement, args }) => {
319
+ const canvas = within(canvasElement)
320
+ const input = await openPanel(canvasElement)
321
+
322
+ await expect(canvas.queryByRole('group', { name: 'Google Fonts' })).toBeNull()
323
+ const retry = canvas.getByRole('button', { name: 'Opnieuw proberen' })
324
+ await expect(retry.closest('[role="status"]')).toHaveTextContent(
325
+ 'Google Fonts konden niet worden geladen.',
326
+ )
327
+
328
+ // search field → listbox → retry: three tab stops, the notice last
329
+ await expect(input).toHaveFocus()
330
+ await userEvent.tab()
331
+ await expect(canvas.getByRole('listbox')).toHaveFocus()
332
+ await userEvent.tab()
333
+ await expect(retry).toHaveFocus()
334
+
335
+ await userEvent.click(retry)
336
+ await expect(args.onRetry).toHaveBeenCalledOnce()
337
+ await waitFor(() =>
338
+ expect(canvas.getByRole('group', { name: 'Google Fonts' })).toBeInTheDocument(),
339
+ )
340
+ },
341
+ }
342
+
343
+ /** Nothing matches: the empty state, with a button that clears the search. */
344
+ export const NoResults: Story = {
345
+ args: { ariaLabel: 'Lettertype' },
346
+ render: renderPicker('Arial'),
347
+ play: async ({ canvasElement }) => {
348
+ const canvas = within(canvasElement)
349
+ const input = await openPanel(canvasElement)
350
+
351
+ await userEvent.type(input, 'zzz')
352
+ await settled(input)
353
+ await expect(await canvas.findByText('Geen lettertypes gevonden')).toBeVisible()
354
+ await expect(canvas.getByText('Niets gevonden voor ‘zzz’.')).toBeVisible()
355
+
356
+ await userEvent.click(canvas.getByRole('button', { name: 'Zoekopdracht wissen' }))
357
+ await expect(input).toHaveValue('')
358
+ await expect(input).toHaveFocus()
359
+ },
360
+ }
361
+
362
+ /**
363
+ * The keyboard from the search field: arrows and PageDown move the highlight, Enter chooses,
364
+ * Escape first clears and then closes, and Tab out of the panel closes it without moving focus.
365
+ */
366
+ export const Keyboard: Story = {
367
+ args: { ariaLabel: 'Lettertype' },
368
+ render: (args) => ({
369
+ components: { FontPicker },
370
+ setup() {
371
+ const font = ref<string | null>('Arial')
372
+ return { args, font }
373
+ },
374
+ template: `
375
+ <div class="flex w-78 flex-col gap-3">
376
+ <FontPicker v-bind="args" v-model="font" />
377
+ <button type="button" class="self-start small-normal text-gray-900 underline">Volgende veld</button>
378
+ <code data-testid="model" class="extra-small-normal text-gray-700">{{ font }}</code>
379
+ </div>
380
+ `,
381
+ }),
382
+ play: async ({ canvasElement }) => {
383
+ const canvas = within(canvasElement)
384
+ const trigger = triggerOf(canvasElement)
385
+
386
+ trigger.focus()
387
+ await userEvent.keyboard('{ArrowDown}')
388
+ const input = await canvas.findByRole('combobox')
389
+ await waitFor(() => expect(input).toHaveFocus())
390
+ await expect(activeOption(input)).toHaveAccessibleName('Arial')
391
+
392
+ await userEvent.keyboard('{ArrowDown}{ArrowDown}')
393
+ await expect(activeOption(input)).toHaveAccessibleName('Verdana')
394
+ // eight down from Verdana: past the end of "Standaard" into "Google Fonts"
395
+ await userEvent.keyboard('{PageDown}')
396
+ await expect(activeOption(input)).toHaveAccessibleName('Open Sans')
397
+ await userEvent.keyboard('{ArrowUp}')
398
+ await expect(activeOption(input)).toHaveAccessibleName('Roboto')
399
+ await expect(canvas.getByTestId('model')).toHaveTextContent('Arial')
400
+
401
+ await userEvent.keyboard('{Enter}')
402
+ await waitFor(() => expect(canvas.queryByRole('listbox')).toBeNull())
403
+ await expect(trigger).toHaveFocus()
404
+ await expect(canvas.getByTestId('model')).toHaveTextContent('Roboto')
405
+
406
+ // Escape: clear, then close
407
+ await userEvent.keyboard('{Enter}')
408
+ const again = await canvas.findByRole('combobox')
409
+ await waitFor(() => expect(again).toHaveFocus())
410
+ await userEvent.type(again, 'ro')
411
+ await userEvent.keyboard('{Escape}')
412
+ await expect(again).toHaveValue('')
413
+ await expect(canvas.getByRole('listbox')).toBeInTheDocument()
414
+ await userEvent.keyboard('{Escape}')
415
+ await waitFor(() => expect(canvas.queryByRole('listbox')).toBeNull())
416
+ await expect(trigger).toHaveFocus()
417
+
418
+ // Tab out of the panel: closed, and focus is where Tab put it
419
+ await userEvent.keyboard('{Enter}')
420
+ await waitFor(() => expect(canvas.getByRole('combobox')).toHaveFocus())
421
+ await userEvent.tab()
422
+ await expect(canvas.getByRole('listbox')).toHaveFocus()
423
+ await userEvent.tab()
424
+ const next = canvas.getByRole('button', { name: 'Volgende veld' })
425
+ await waitFor(() => expect(canvas.queryByRole('listbox')).toBeNull())
426
+ await expect(next).toHaveFocus()
427
+ },
428
+ }
429
+
430
+ /** Dimmed, and it does not open. */
431
+ export const Disabled: Story = {
432
+ args: { ariaLabel: 'Lettertype', disabled: true },
433
+ render: renderPicker('Georgia'),
434
+ play: async ({ canvasElement }) => {
435
+ const trigger = triggerOf(canvasElement)
436
+
437
+ await expect(trigger).toBeDisabled()
438
+ await userEvent.click(trigger, { pointerEventsCheck: 0 })
439
+ await expect(within(canvasElement).queryByRole('listbox')).toBeNull()
440
+ },
441
+ }
442
+
443
+ /**
444
+ * The app's flow: the catalogue is fetched on the first open, which takes a moment; after that it
445
+ * is cached and the panel opens with it in place.
446
+ */
447
+ export const LazyCatalogue: Story = {
448
+ args: {
449
+ ariaLabel: 'Lettertype',
450
+ inherit: { label: 'Huisstijl', family: 'Georgia' },
451
+ },
452
+ render: (args) => ({
453
+ components: { FontPicker },
454
+ setup() {
455
+ const font = ref<string | null>(null)
456
+ const status = ref<'loading' | 'ready' | 'error'>('loading')
457
+ const fonts = ref<NexxtFontPickerFont[]>(WEB_SAFE)
458
+ let fetched = false
459
+ const onOpen = () => {
460
+ args.onOpen?.()
461
+ if (fetched) return
462
+ fetched = true
463
+ setTimeout(() => {
464
+ fonts.value = FONTS
465
+ status.value = 'ready'
466
+ }, 1200)
467
+ }
468
+ return { args, font, status, fonts, onOpen }
469
+ },
470
+ template: `
471
+ <div class="w-78">
472
+ <FontPicker v-bind="args" v-model="font" :fonts="fonts" :catalogue-status="status" @open="onOpen" />
473
+ </div>
474
+ `,
475
+ }),
476
+ }