@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,184 @@
1
+ import { afterAll, afterEach, beforeAll, describe, it, expect } from 'vitest'
2
+ import { defineComponent, h, nextTick, ref } from 'vue'
3
+ import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
4
+ import IconPickerGrid from './_IconPickerGrid.vue'
5
+ import type { NexxtIconPickerIcon } from './IconPicker.vue'
6
+ import type { GridPosition, GridRow } from './useIconPickerCatalogue'
7
+ import type { IconPickerGridHandle } from './useIconPickerNavigation'
8
+
9
+ // jsdom lays nothing out: the grid gets the height of about five rows of the fallback 63px
10
+ beforeAll(() => {
11
+ Object.defineProperty(HTMLElement.prototype, 'clientHeight', {
12
+ configurable: true,
13
+ get(this: HTMLElement) {
14
+ return this.getAttribute('role') === 'grid' ? 320 : 0
15
+ },
16
+ })
17
+ })
18
+ afterAll(() => {
19
+ delete (HTMLElement.prototype as { clientHeight?: number }).clientHeight
20
+ })
21
+
22
+ const icon = (index: number): NexxtIconPickerIcon => ({
23
+ name: `icon-${index}`,
24
+ label: `Icon ${index}`,
25
+ terms: [],
26
+ styles: ['light'],
27
+ })
28
+
29
+ /** A heading, then rows of six icons. */
30
+ const makeRows = (count: number): GridRow[] => [
31
+ { kind: 'heading', key: 'heading', label: 'Alle iconen' },
32
+ ...Array.from({ length: count }, (_, row) => ({
33
+ kind: 'icons' as const,
34
+ key: `row-${row}`,
35
+ icons: Array.from({ length: 6 }, (_, col) => icon(row * 6 + col)),
36
+ })),
37
+ ]
38
+
39
+ const BASE = {
40
+ gridId: 'p-grid',
41
+ labelledby: 'p-tab-0',
42
+ cellIdPrefix: 'p-cell',
43
+ currentDescriptionId: 'p-current',
44
+ highlight: null as GridPosition | null,
45
+ focused: false,
46
+ selected: null as GridPosition | null,
47
+ glyphClass: (item: NexxtIconPickerIcon) => ['style-light', `glyph-${item.name}`],
48
+ }
49
+
50
+ type Props = Partial<InstanceType<typeof IconPickerGrid>['$props']>
51
+
52
+ const mounted: VueWrapper[] = []
53
+
54
+ /** The grid in a host that holds its ref, so the exposed handle is what a parent really gets. */
55
+ const mountGrid = async (props: Props = {}) => {
56
+ const handle = ref<IconPickerGridHandle | null>(null)
57
+ const state = ref({ rows: makeRows(3), ...BASE, ...props })
58
+ const Host = defineComponent({
59
+ setup: () => () => h(IconPickerGrid, { ...state.value, ref: handle }),
60
+ })
61
+ const wrapper = mount(Host, { attachTo: document.body })
62
+ mounted.push(wrapper)
63
+ const grid = wrapper.findComponent(IconPickerGrid)
64
+ // the virtual list measures its container after mounting
65
+ await flushPromises()
66
+ const update = async (next: Props) => {
67
+ state.value = { ...state.value, ...next }
68
+ await flushPromises()
69
+ }
70
+ return { wrapper, grid, handle, update }
71
+ }
72
+
73
+ afterEach(() => {
74
+ mounted.splice(0).forEach((wrapper) => wrapper.unmount())
75
+ })
76
+
77
+ const gridOf = (wrapper: VueWrapper) => wrapper.find('[role="grid"]')
78
+ const cells = (wrapper: VueWrapper) => wrapper.findAll('[role="gridcell"]')
79
+
80
+ describe('IconPickerGrid', () => {
81
+ it('shows a busy grid of 6×6 hidden skeleton cells', async () => {
82
+ const { wrapper } = await mountGrid({ skeleton: true })
83
+ const grid = gridOf(wrapper)
84
+ expect(grid.attributes('aria-busy')).toBe('true')
85
+ expect(grid.attributes('id')).toBe('p-grid')
86
+ expect(grid.attributes('aria-labelledby')).toBe('p-tab-0')
87
+ expect(grid.findAll('.animate-pulse')).toHaveLength(36)
88
+ expect(cells(wrapper)).toHaveLength(0)
89
+ })
90
+
91
+ it('renders headings as rowheaders and icons as named cells with ids', async () => {
92
+ const { wrapper } = await mountGrid()
93
+ const grid = gridOf(wrapper)
94
+ expect(grid.attributes('tabindex')).toBe('0')
95
+ expect(grid.attributes('aria-rowcount')).toBe('4')
96
+ expect(grid.attributes('aria-colcount')).toBe('6')
97
+ expect(wrapper.find('[role="rowheader"]').text()).toBe('Alle iconen')
98
+ const first = cells(wrapper)[0]!
99
+ expect(first.attributes('id')).toBe('p-cell-1-0')
100
+ expect(first.attributes('aria-label')).toBe('Icon 0')
101
+ expect(first.attributes('aria-colindex')).toBe('1')
102
+ expect(first.find('i').classes()).toEqual(['style-light', 'glyph-icon-0'])
103
+ expect(first.element.closest('[role="row"]')!.getAttribute('aria-rowindex')).toBe('2')
104
+ })
105
+
106
+ it('renders only a window of a long list', async () => {
107
+ const { wrapper } = await mountGrid({ rows: makeRows(700) })
108
+ expect(gridOf(wrapper).attributes('aria-rowcount')).toBe('701')
109
+ expect(cells(wrapper).length).toBeGreaterThan(0)
110
+ expect(cells(wrapper).length).toBeLessThanOrEqual(150)
111
+ })
112
+
113
+ it('marks the one selected cell, described as the current choice', async () => {
114
+ const { wrapper } = await mountGrid({ selected: { row: 2, col: 3 } })
115
+ const selected = wrapper.findAll('[aria-selected="true"]')
116
+ expect(selected).toHaveLength(1)
117
+ expect(selected[0]!.attributes('id')).toBe('p-cell-2-3')
118
+ expect(selected[0]!.attributes('aria-describedby')).toBe('p-current')
119
+ expect(selected[0]!.classes()).toContain('ring-2')
120
+ })
121
+
122
+ it('draws the highlight and points aria-activedescendant at it only while focused', async () => {
123
+ const { wrapper, update } = await mountGrid({ highlight: { row: 1, col: 2 } })
124
+ expect(gridOf(wrapper).attributes('aria-activedescendant')).toBeUndefined()
125
+ expect(wrapper.find('#p-cell-1-2').classes()).not.toContain('-outline-offset-2')
126
+
127
+ await update({ focused: true })
128
+ expect(gridOf(wrapper).attributes('aria-activedescendant')).toBe('p-cell-1-2')
129
+ expect(wrapper.find('#p-cell-1-2').classes()).toContain('-outline-offset-2')
130
+
131
+ // a highlight on a heading, or outside the rows, points nowhere
132
+ await update({ highlight: { row: 0, col: 0 } })
133
+ expect(gridOf(wrapper).attributes('aria-activedescendant')).toBeUndefined()
134
+ })
135
+
136
+ it('moves the highlight outline in on a selected cell', async () => {
137
+ const { wrapper } = await mountGrid({
138
+ highlight: { row: 1, col: 0 },
139
+ selected: { row: 1, col: 0 },
140
+ focused: true,
141
+ })
142
+ await nextTick()
143
+ expect(wrapper.find('#p-cell-1-0').classes()).toContain('-outline-offset-4')
144
+ })
145
+
146
+ it('emits focus, blur, keydown, hover and choose', async () => {
147
+ const { wrapper, grid } = await mountGrid()
148
+ await gridOf(wrapper).trigger('focus')
149
+ await gridOf(wrapper).trigger('blur')
150
+ await gridOf(wrapper).trigger('keydown', { key: 'ArrowRight' })
151
+ await cells(wrapper)[7]!.trigger('mouseenter')
152
+ await cells(wrapper)[7]!.trigger('click')
153
+ expect(grid.emitted('focus')).toHaveLength(1)
154
+ expect(grid.emitted('blur')).toHaveLength(1)
155
+ expect((grid.emitted('keydown')![0]![0] as KeyboardEvent).key).toBe('ArrowRight')
156
+ expect(grid.emitted('hover')).toEqual([[{ row: 2, col: 1 }]])
157
+ expect(grid.emitted('choose')).toEqual([[icon(7)]])
158
+ })
159
+
160
+ it('exposes focus, reveal, inView, pageRows and scrollToTop to its parent', async () => {
161
+ const { wrapper, handle, update } = await mountGrid({ rows: makeRows(700) })
162
+ const element = gridOf(wrapper).element as HTMLElement
163
+ expect(handle.value).not.toBeNull()
164
+
165
+ handle.value!.focus()
166
+ expect(document.activeElement).toBe(element)
167
+
168
+ // 320px over rows of 63px: five rows fit, a page is four
169
+ expect(handle.value!.pageRows()).toBe(4)
170
+ expect(handle.value!.inView(1)).toBe(true)
171
+ expect(handle.value!.inView(500)).toBe(false)
172
+
173
+ // reveal renders the far row at once, so the active descendant exists
174
+ handle.value!.reveal(500)
175
+ await update({ highlight: { row: 500, col: 0 }, focused: true })
176
+ expect(element.scrollTop).toBeGreaterThan(0)
177
+ const id = element.getAttribute('aria-activedescendant')
178
+ expect(id).toBe('p-cell-500-0')
179
+ expect(document.getElementById(id!)).not.toBeNull()
180
+
181
+ handle.value!.scrollToTop()
182
+ expect(element.scrollTop).toBe(0)
183
+ })
184
+ })
@@ -0,0 +1,102 @@
1
+ import { afterEach, describe, it, expect } from 'vitest'
2
+ import { mount, type VueWrapper } from '@vue/test-utils'
3
+ import IconPickerRail from './_IconPickerRail.vue'
4
+
5
+ const TABS = [
6
+ { id: null, label: 'Alle iconen' },
7
+ { id: 'buildings', label: 'Gebouwen' },
8
+ { id: 'nature', label: 'Natuur' },
9
+ ]
10
+
11
+ const mounted: VueWrapper[] = []
12
+
13
+ const mountRail = (props: Partial<InstanceType<typeof IconPickerRail>['$props']> = {}) => {
14
+ const wrapper = mount(IconPickerRail, {
15
+ props: {
16
+ tabs: TABS,
17
+ activeIndex: 0,
18
+ label: 'Categorieën',
19
+ panelId: 'p-panel',
20
+ tabId: (index: number) => `p-tab-${index}`,
21
+ // play the parent: a selection moves the active tab
22
+ onSelect: (id: string | null) =>
23
+ wrapper.setProps({ activeIndex: TABS.findIndex((tab) => tab.id === id) }),
24
+ ...props,
25
+ },
26
+ attachTo: document.body,
27
+ })
28
+ mounted.push(wrapper)
29
+ return wrapper
30
+ }
31
+
32
+ afterEach(() => {
33
+ mounted.splice(0).forEach((wrapper) => wrapper.unmount())
34
+ })
35
+
36
+ const tabsOf = (wrapper: VueWrapper) => wrapper.findAll('[role="tab"]')
37
+
38
+ describe('IconPickerRail', () => {
39
+ it('renders a vertical, named tablist of tabs that control the panel', () => {
40
+ const wrapper = mountRail()
41
+ const list = wrapper.find('[role="tablist"]')
42
+ expect(list.attributes('aria-orientation')).toBe('vertical')
43
+ expect(list.attributes('aria-label')).toBe('Categorieën')
44
+ expect(tabsOf(wrapper).map((tab) => tab.text())).toEqual(['Alle iconen', 'Gebouwen', 'Natuur'])
45
+ expect(tabsOf(wrapper).map((tab) => tab.attributes('id'))).toEqual([
46
+ 'p-tab-0',
47
+ 'p-tab-1',
48
+ 'p-tab-2',
49
+ ])
50
+ expect(tabsOf(wrapper).every((tab) => tab.attributes('aria-controls') === 'p-panel')).toBe(true)
51
+ })
52
+
53
+ it('has one tab stop: the selected tab', () => {
54
+ const wrapper = mountRail({ activeIndex: 1 })
55
+ expect(tabsOf(wrapper).map((tab) => tab.attributes('tabindex'))).toEqual(['-1', '0', '-1'])
56
+ expect(tabsOf(wrapper).map((tab) => tab.attributes('aria-selected'))).toEqual([
57
+ 'false',
58
+ 'true',
59
+ 'false',
60
+ ])
61
+ })
62
+
63
+ it('gives every label the full name as title, in a truncating span', () => {
64
+ const wrapper = mountRail()
65
+ const tab = tabsOf(wrapper)[1]!
66
+ expect(tab.attributes('title')).toBe('Gebouwen')
67
+ expect(tab.find('span').classes()).toContain('truncate')
68
+ })
69
+
70
+ it('emits the id of a clicked tab', async () => {
71
+ const wrapper = mountRail()
72
+ await tabsOf(wrapper)[2]!.trigger('click')
73
+ expect(wrapper.emitted('select')).toEqual([['nature']])
74
+ })
75
+
76
+ it('moves and chooses with the arrow keys, Home and End, and focuses the tab', async () => {
77
+ const wrapper = mountRail()
78
+ const list = wrapper.find('[role="tablist"]')
79
+ await list.trigger('keydown', { key: 'ArrowDown' })
80
+ expect(wrapper.emitted('select')!.at(-1)).toEqual(['buildings'])
81
+ expect(document.activeElement?.id).toBe('p-tab-1')
82
+ await list.trigger('keydown', { key: 'End' })
83
+ expect(wrapper.emitted('select')!.at(-1)).toEqual(['nature'])
84
+ expect(document.activeElement?.id).toBe('p-tab-2')
85
+ await list.trigger('keydown', { key: 'ArrowDown' })
86
+ expect(wrapper.emitted('select')!.at(-1)).toEqual(['nature'])
87
+ await list.trigger('keydown', { key: 'Home' })
88
+ expect(wrapper.emitted('select')!.at(-1)).toEqual([null])
89
+ await list.trigger('keydown', { key: 'ArrowUp' })
90
+ expect(wrapper.emitted('select')!.at(-1)).toEqual([null])
91
+ await list.trigger('keydown', { key: 'ArrowRight' })
92
+ expect(wrapper.emitted('select')).toHaveLength(5)
93
+ })
94
+
95
+ it('shows eight hidden skeleton bars after the tabs while loading', () => {
96
+ const wrapper = mountRail({ tabs: [TABS[0]!], loading: true })
97
+ expect(tabsOf(wrapper)).toHaveLength(1)
98
+ const bars = wrapper.findAll('[aria-hidden="true"]')
99
+ expect(bars).toHaveLength(8)
100
+ expect(wrapper.findAll('.animate-pulse')).toHaveLength(8)
101
+ })
102
+ })
@@ -0,0 +1,215 @@
1
+ import { afterEach, describe, it, expect, vi } from 'vitest'
2
+ import { computed, effectScope, nextTick, ref, type EffectScope } from 'vue'
3
+ import type { NexxtIconPickerIcon } from './IconPicker.vue'
4
+ import type { GridPosition, GridRow } from './useIconPickerCatalogue'
5
+ import { useIconPickerNavigation, type IconPickerGridHandle } from './useIconPickerNavigation'
6
+
7
+ const icon = (index: number): NexxtIconPickerIcon => ({
8
+ name: `icon-${index}`,
9
+ label: `Icon ${index}`,
10
+ terms: [],
11
+ styles: ['light'],
12
+ })
13
+
14
+ /** A heading, three full rows, and a last row of two. */
15
+ const ROWS: GridRow[] = [
16
+ { kind: 'heading', key: 'h', label: 'Alle iconen' },
17
+ ...[0, 1, 2].map((row) => ({
18
+ kind: 'icons' as const,
19
+ key: `r${row}`,
20
+ icons: Array.from({ length: 6 }, (_, col) => icon(row * 6 + col)),
21
+ })),
22
+ { kind: 'icons', key: 'r3', icons: [icon(18), icon(19)] },
23
+ ]
24
+
25
+ const scopes: EffectScope[] = []
26
+
27
+ afterEach(() => {
28
+ scopes.splice(0).forEach((scope) => scope.stop())
29
+ vi.useRealTimers()
30
+ })
31
+
32
+ const setup = (
33
+ options: { current?: GridPosition | null; inView?: boolean; page?: number } = {},
34
+ ) => {
35
+ const grid: IconPickerGridHandle = {
36
+ focus: vi.fn(),
37
+ reveal: vi.fn(),
38
+ inView: vi.fn(() => options.inView ?? true),
39
+ pageRows: vi.fn(() => options.page ?? 2),
40
+ scrollToTop: vi.fn(),
41
+ }
42
+ const search = ref('')
43
+ const category = ref<string | null>(null)
44
+ const isOpen = ref(true)
45
+ const empty = ref(false)
46
+ const focusSearch = vi.fn()
47
+ const choose = vi.fn()
48
+ const iconAt = (position: GridPosition | null) => {
49
+ const row = position ? ROWS[position.row] : undefined
50
+ return row?.kind === 'icons' ? (row.icons[position!.col] ?? null) : null
51
+ }
52
+ const rowLength = (row: number | undefined) => {
53
+ const entry = row === undefined ? undefined : ROWS[row]
54
+ return entry?.kind === 'icons' ? entry.icons.length : 0
55
+ }
56
+ const scope = effectScope()
57
+ scopes.push(scope)
58
+ const navigation = scope.run(() =>
59
+ useIconPickerNavigation({
60
+ iconRows: computed(() => [1, 2, 3, 4]),
61
+ currentPosition: computed(() => options.current ?? null),
62
+ iconCount: computed(() => 20),
63
+ catalogueEmpty: computed(() => empty.value),
64
+ iconAt,
65
+ rowLength,
66
+ search,
67
+ term: computed(() => search.value.trim()),
68
+ category,
69
+ isOpen,
70
+ grid: () => grid,
71
+ focusSearch,
72
+ choose,
73
+ results: () => (count: number) => `${count} iconen`,
74
+ }),
75
+ )!
76
+ const key = (name: string, init: KeyboardEventInit = {}) => {
77
+ const event = new KeyboardEvent('keydown', { key: name, cancelable: true, ...init })
78
+ navigation.onGridKeydown(event)
79
+ return event
80
+ }
81
+ return { navigation, grid, search, category, isOpen, empty, focusSearch, choose, key }
82
+ }
83
+
84
+ describe('useIconPickerNavigation', () => {
85
+ it('highlights the current icon on opening, after the reset', () => {
86
+ const { navigation, search } = setup({ current: { row: 2, col: 3 } })
87
+ search.value = 'house'
88
+ navigation.open(() => {
89
+ search.value = ''
90
+ })
91
+ expect(navigation.highlight.value).toEqual({ row: 2, col: 3 })
92
+ expect(navigation.highlightedIcon.value).toEqual(icon(9))
93
+ })
94
+
95
+ it('enters the grid on the current icon when it is in view, the first cell otherwise', () => {
96
+ const inView = setup({ current: { row: 3, col: 1 } })
97
+ inView.navigation.onSearchKeydown(new KeyboardEvent('keydown', { key: 'ArrowDown' }))
98
+ expect(inView.navigation.highlight.value).toEqual({ row: 3, col: 1 })
99
+ expect(inView.grid.focus).toHaveBeenCalled()
100
+
101
+ const outOfView = setup({ current: { row: 3, col: 1 }, inView: false })
102
+ outOfView.navigation.onSearchKeydown(new KeyboardEvent('keydown', { key: 'ArrowDown' }))
103
+ expect(outOfView.navigation.highlight.value).toEqual({ row: 1, col: 0 })
104
+ expect(outOfView.grid.reveal).toHaveBeenCalledWith(1)
105
+ })
106
+
107
+ it('highlights the first cell when the grid takes focus without one', () => {
108
+ const { navigation } = setup()
109
+ navigation.onGridFocus()
110
+ expect(navigation.gridFocused.value).toBe(true)
111
+ expect(navigation.highlight.value).toEqual({ row: 1, col: 0 })
112
+ navigation.onGridBlur()
113
+ expect(navigation.gridFocused.value).toBe(false)
114
+ })
115
+
116
+ it('walks the rows, skipping headings, and reveals every row it moves to', () => {
117
+ const { navigation, grid, key, focusSearch } = setup()
118
+ navigation.onGridFocus()
119
+ key('End')
120
+ expect(navigation.highlight.value).toEqual({ row: 1, col: 5 })
121
+ key('ArrowRight')
122
+ expect(navigation.highlight.value).toEqual({ row: 2, col: 0 })
123
+ key('ArrowLeft')
124
+ expect(navigation.highlight.value).toEqual({ row: 1, col: 5 })
125
+ key('ArrowDown')
126
+ key('ArrowDown')
127
+ key('ArrowDown')
128
+ // the last row is short: the column clamps
129
+ expect(navigation.highlight.value).toEqual({ row: 4, col: 1 })
130
+ key('Home', { ctrlKey: true })
131
+ expect(navigation.highlight.value).toEqual({ row: 1, col: 0 })
132
+ key('End', { metaKey: true })
133
+ expect(navigation.highlight.value).toEqual({ row: 4, col: 1 })
134
+ expect(grid.reveal).toHaveBeenLastCalledWith(4)
135
+ key('PageUp')
136
+ expect(navigation.highlight.value).toEqual({ row: 2, col: 1 })
137
+ key('PageDown')
138
+ expect(navigation.highlight.value).toEqual({ row: 4, col: 1 })
139
+ key('Home', { ctrlKey: true })
140
+ key('ArrowUp')
141
+ expect(focusSearch).toHaveBeenCalledOnce()
142
+ })
143
+
144
+ it('chooses with Enter and Space, and leaves other keys alone', () => {
145
+ const { navigation, key, choose } = setup()
146
+ navigation.onGridFocus()
147
+ expect(key('Enter').defaultPrevented).toBe(true)
148
+ key(' ')
149
+ expect(choose).toHaveBeenCalledTimes(2)
150
+ expect(choose).toHaveBeenCalledWith(icon(0))
151
+ expect(key('Tab').defaultPrevented).toBe(false)
152
+ })
153
+
154
+ it('sends a letter typed in the grid to the search field', () => {
155
+ const { navigation, key, search, focusSearch } = setup()
156
+ navigation.onGridFocus()
157
+ key('h')
158
+ key('o')
159
+ expect(search.value).toBe('ho')
160
+ expect(focusSearch).toHaveBeenCalledTimes(2)
161
+ key('c', { ctrlKey: true })
162
+ expect(search.value).toBe('ho')
163
+ })
164
+
165
+ it('chooses the first result with Enter in the field, only with a search', () => {
166
+ const { navigation, search, choose } = setup()
167
+ navigation.onSearchKeydown(new KeyboardEvent('keydown', { key: 'Enter' }))
168
+ expect(choose).not.toHaveBeenCalled()
169
+ search.value = 'icon'
170
+ navigation.onSearchKeydown(new KeyboardEvent('keydown', { key: 'Enter' }))
171
+ expect(choose).toHaveBeenCalledWith(icon(0))
172
+ })
173
+
174
+ it('resets highlight and scroll on a new list, and announces it after 500ms', async () => {
175
+ vi.useFakeTimers()
176
+ const { navigation, grid, category } = setup({ current: { row: 2, col: 0 } })
177
+ navigation.onGridFocus()
178
+ category.value = 'nature'
179
+ await nextTick()
180
+ expect(navigation.highlight.value).toBeNull()
181
+ expect(grid.scrollToTop).toHaveBeenCalledOnce()
182
+ vi.advanceTimersByTime(500)
183
+ expect(navigation.announcement.value).toBe('20 iconen')
184
+
185
+ category.value = null
186
+ await nextTick()
187
+ expect(navigation.highlight.value).toEqual({ row: 2, col: 0 })
188
+ })
189
+
190
+ it('does not announce the reset on opening, and announces the next change', async () => {
191
+ vi.useFakeTimers()
192
+ const { navigation, search, category } = setup()
193
+ category.value = 'nature'
194
+ await nextTick()
195
+ vi.advanceTimersByTime(500)
196
+ navigation.open(() => {
197
+ category.value = null
198
+ })
199
+ await nextTick()
200
+ vi.advanceTimersByTime(500)
201
+ expect(navigation.announcement.value).toBe('')
202
+
203
+ search.value = 'a'
204
+ await nextTick()
205
+ vi.advanceTimersByTime(500)
206
+ expect(navigation.announcement.value).toBe('20 iconen')
207
+ })
208
+
209
+ it('forgets grid focus on closing', () => {
210
+ const { navigation } = setup()
211
+ navigation.onGridFocus()
212
+ navigation.close()
213
+ expect(navigation.gridFocused.value).toBe(false)
214
+ })
215
+ })