@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.
- package/dist/components/editor/molecules/EditorBorder/EditorBorder.vue.d.ts +12 -0
- package/dist/components/editor/molecules/EditorColor/_ThemeRoleSwatches.vue.d.ts +16 -0
- package/dist/index.js +1094 -1135
- package/dist/nuxt.js +0 -1
- package/package.json +38 -36
- package/src/attribute-contract.test.ts +151 -0
- package/src/components/atoms/AnimatedNumber/AnimatedNumber.stories.ts +36 -0
- package/src/components/atoms/AnimatedNumber/AnimatedNumber.test.ts +21 -0
- package/src/components/atoms/Avatar/Avatar.stories.ts +122 -0
- package/src/components/atoms/Avatar/Avatar.test.ts +134 -0
- package/src/components/atoms/Button/Button.stories.ts +290 -0
- package/src/components/atoms/Button/Button.test.ts +406 -0
- package/src/components/atoms/Card/Card.stories.ts +45 -0
- package/src/components/atoms/Card/Card.test.ts +46 -0
- package/src/components/atoms/Checkbox/Checkbox.stories.ts +35 -0
- package/src/components/atoms/Checkbox/Checkbox.test.ts +55 -0
- package/src/components/atoms/Chip/Chip.stories.ts +274 -0
- package/src/components/atoms/Chip/Chip.test.ts +230 -0
- package/src/components/atoms/EnergyLabel/EnergyLabel.stories.ts +189 -0
- package/src/components/atoms/EnergyLabel/EnergyLabel.test.ts +100 -0
- package/src/components/atoms/FloatingPanel/FloatingPanel.stories.ts +677 -0
- package/src/components/atoms/FloatingPanel/FloatingPanel.test.ts +864 -0
- package/src/components/atoms/Icon/Icon.stories.ts +63 -0
- package/src/components/atoms/Icon/Icon.test.ts +89 -0
- package/src/components/atoms/Loader/Loader.stories.ts +36 -0
- package/src/components/atoms/Loader/Loader.test.ts +40 -0
- package/src/components/atoms/PickerItem/PickerItem.stories.ts +172 -0
- package/src/components/atoms/PickerItem/PickerItem.test.ts +142 -0
- package/src/components/atoms/PickerPreview/PickerPreview.stories.ts +183 -0
- package/src/components/atoms/PickerPreview/PickerPreview.test.ts +137 -0
- package/src/components/atoms/PresenceIndicator/PresenceIndicator.stories.ts +21 -0
- package/src/components/atoms/PropertyListing/PropertyListing.stories.ts +38 -0
- package/src/components/atoms/PropertyListing/PropertyListing.test.ts +46 -0
- package/src/components/atoms/SelectableOption/SelectableOption.stories.ts +131 -0
- package/src/components/atoms/SelectableOption/SelectableOption.test.ts +96 -0
- package/src/components/atoms/Skeleton/Skeleton.stories.ts +27 -0
- package/src/components/atoms/Skeleton/Skeleton.test.ts +28 -0
- package/src/components/atoms/Slider/Slider.stories.ts +151 -0
- package/src/components/atoms/Slider/Slider.test.ts +191 -0
- package/src/components/atoms/Squircle/Squircle.stories.ts +114 -0
- package/src/components/atoms/Squircle/Squircle.test.ts +70 -0
- package/src/components/atoms/Switch/Switch.stories.ts +91 -0
- package/src/components/atoms/Switch/Switch.test.ts +183 -0
- package/src/components/atoms/Tooltip/Tooltip.stories.ts +158 -0
- package/src/components/atoms/Tooltip/Tooltip.test.ts +134 -0
- package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.stories.ts +124 -0
- package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.test.ts +464 -0
- package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.stories.ts +266 -0
- package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.test.ts +421 -0
- package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.stories.ts +365 -0
- package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.test.ts +484 -0
- package/src/components/editor/composables/useRadioGrid.test.ts +261 -0
- package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.stories.ts +73 -0
- package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.test.ts +189 -0
- package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.stories.ts +309 -0
- package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.test.ts +359 -0
- package/src/components/editor/molecules/EditorAnchor/EditorAnchor.stories.ts +138 -0
- package/src/components/editor/molecules/EditorAnchor/EditorAnchor.test.ts +344 -0
- package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.stories.ts +87 -0
- package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.test.ts +262 -0
- package/src/components/editor/molecules/EditorBlur/EditorBlur.stories.ts +115 -0
- package/src/components/editor/molecules/EditorBlur/EditorBlur.test.ts +243 -0
- package/src/components/editor/molecules/EditorBorder/EditorBorder.stories.ts +187 -0
- package/src/components/editor/molecules/EditorBorder/EditorBorder.test.ts +456 -0
- package/src/components/editor/molecules/EditorBorder/EditorBorder.vue +58 -2
- package/src/components/editor/molecules/EditorColor/EditorColor.stories.ts +563 -0
- package/src/components/editor/molecules/EditorColor/EditorColor.test.ts +689 -0
- package/src/components/editor/molecules/EditorColor/EditorColorGradient.test.ts +793 -0
- package/src/components/editor/molecules/EditorColor/_ColorField.vue +7 -29
- package/src/components/editor/molecules/EditorColor/_ThemeRoleSwatches.vue +57 -0
- package/src/components/editor/molecules/EditorColor/gradientClass.test.ts +303 -0
- package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.stories.ts +72 -0
- package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.test.ts +124 -0
- package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.stories.ts +46 -0
- package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.test.ts +97 -0
- package/src/components/editor/molecules/EditorFontSize/EditorFontSize.stories.ts +248 -0
- package/src/components/editor/molecules/EditorFontSize/EditorFontSize.test.ts +288 -0
- package/src/components/editor/molecules/EditorGap/EditorGap.stories.ts +129 -0
- package/src/components/editor/molecules/EditorGap/EditorGap.test.ts +253 -0
- package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.stories.ts +68 -0
- package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.test.ts +150 -0
- package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.stories.ts +71 -0
- package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.test.ts +161 -0
- package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.stories.ts +204 -0
- package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.test.ts +238 -0
- package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.stories.ts +257 -0
- package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.test.ts +286 -0
- package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.stories.ts +135 -0
- package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.test.ts +230 -0
- package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.stories.ts +154 -0
- package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.test.ts +112 -0
- package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.stories.ts +206 -0
- package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.test.ts +332 -0
- package/src/components/editor/molecules/EditorPadding/EditorPadding.stories.ts +374 -0
- package/src/components/editor/molecules/EditorPadding/EditorPadding.test.ts +712 -0
- package/src/components/editor/molecules/EditorRadius/EditorRadius.stories.ts +64 -0
- package/src/components/editor/molecules/EditorRadius/EditorRadius.test.ts +137 -0
- package/src/components/editor/molecules/EditorShadow/EditorShadow.stories.ts +103 -0
- package/src/components/editor/molecules/EditorShadow/EditorShadow.test.ts +168 -0
- package/src/components/editor/molecules/EditorSize/EditorSize.stories.ts +84 -0
- package/src/components/editor/molecules/EditorSize/EditorSize.test.ts +253 -0
- package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.stories.ts +49 -0
- package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.test.ts +43 -0
- package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.stories.ts +69 -0
- package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.test.ts +155 -0
- package/src/components/molecules/Announcement/Announcement.stories.ts +224 -0
- package/src/components/molecules/Announcement/Announcement.test.ts +161 -0
- package/src/components/molecules/ContactInfoCard/ContactInfoCard.stories.ts +99 -0
- package/src/components/molecules/ContactInfoCard/ContactInfoCard.test.ts +70 -0
- package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.stories.ts +148 -0
- package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.test.ts +271 -0
- package/src/components/molecules/DoughnutChart/DoughnutChart.stories.ts +45 -0
- package/src/components/molecules/DoughnutChart/DoughnutChart.test.ts +205 -0
- package/src/components/molecules/InfoBlock/InfoBlock.stories.ts +84 -0
- package/src/components/molecules/InfoBlock/InfoBlock.test.ts +101 -0
- package/src/components/molecules/InputField/InputField.stories.ts +117 -0
- package/src/components/molecules/InputField/InputField.test.ts +138 -0
- package/src/components/molecules/InputSelect/InputSelect.stories.ts +116 -0
- package/src/components/molecules/InputSelect/InputSelect.test.ts +127 -0
- package/src/components/molecules/ModalFooter/ModalFooter.stories.ts +204 -0
- package/src/components/molecules/ModalFooter/ModalFooter.test.ts +276 -0
- package/src/components/molecules/PageCard/PageCard.stories.ts +100 -0
- package/src/components/molecules/PageCard/PageCard.test.ts +91 -0
- package/src/components/molecules/PageContainer/PageContainer.stories.ts +90 -0
- package/src/components/molecules/PageContainer/PageContainer.test.ts +50 -0
- package/src/components/molecules/PageMockup/PageMockup.stories.ts +38 -0
- package/src/components/molecules/PageMockup/PageMockup.test.ts +20 -0
- package/src/components/molecules/PageTypeTile/PageTypeTile.stories.ts +63 -0
- package/src/components/molecules/PageTypeTile/PageTypeTile.test.ts +41 -0
- package/src/components/molecules/Pagination/Pagination.stories.ts +72 -0
- package/src/components/molecules/Pagination/Pagination.test.ts +130 -0
- package/src/components/molecules/PickerGroup/PickerGroup.stories.ts +90 -0
- package/src/components/molecules/PickerGroup/PickerGroup.test.ts +145 -0
- package/src/components/molecules/PickerList/PickerList.stories.ts +436 -0
- package/src/components/molecules/PickerList/PickerList.test.ts +870 -0
- package/src/components/molecules/PlaceholderInput/PlaceholderInput.stories.ts +48 -0
- package/src/components/molecules/PlaceholderInput/PlaceholderInput.test.ts +107 -0
- package/src/components/molecules/PropertyCard/PropertyCard.stories.ts +46 -0
- package/src/components/molecules/PropertyCard/PropertyCard.test.ts +59 -0
- package/src/components/molecules/Select/Select.stories.ts +264 -0
- package/src/components/molecules/Select/Select.test.ts +315 -0
- package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.stories.ts +84 -0
- package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.test.ts +103 -0
- package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.stories.ts +11 -0
- package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.test.ts +38 -0
- package/src/components/molecules/SocialIcons/SocialIcons.stories.ts +28 -0
- package/src/components/molecules/SocialIcons/SocialIcons.test.ts +58 -0
- package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.stories.ts +11 -0
- package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.test.ts +116 -0
- package/src/components/molecules/SocialMediaType/SocialMediaType.stories.ts +37 -0
- package/src/components/molecules/SocialMediaType/SocialMediaType.test.ts +126 -0
- package/src/components/molecules/Source/Source.stories.ts +35 -0
- package/src/components/molecules/Source/Source.test.ts +45 -0
- package/src/components/molecules/TabbedContent/TabbedContent.stories.ts +96 -0
- package/src/components/molecules/TabbedContent/TabbedContent.test.ts +109 -0
- package/src/components/molecules/Tabs/Tabs.stories.ts +230 -0
- package/src/components/molecules/Tabs/Tabs.test.ts +280 -0
- package/src/components/molecules/TemplateCard/TemplateCard.stories.ts +327 -0
- package/src/components/molecules/TemplateCard/TemplateCard.test.ts +339 -0
- package/src/components/molecules/Textarea/Textarea.stories.ts +216 -0
- package/src/components/molecules/Textarea/Textarea.test.ts +191 -0
- package/src/components/molecules/TimelineEvent/TimelineEvent.stories.ts +319 -0
- package/src/components/molecules/TimelineEvent/TimelineEvent.test.ts +224 -0
- package/src/components/molecules/TimelineLanes/TimelineLanes.stories.ts +29 -0
- package/src/components/molecules/TimelineLanes/TimelineLanes.test.ts +30 -0
- package/src/components/molecules/Toast/Toast.stories.ts +110 -0
- package/src/components/molecules/Toast/Toast.test.ts +103 -0
- package/src/components/molecules/ToggleGroup/ToggleGroup.stories.ts +297 -0
- package/src/components/molecules/ToggleGroup/ToggleGroup.test.ts +578 -0
- package/src/components/molecules/Tree/Tree.stories.ts +884 -0
- package/src/components/molecules/Tree/Tree.test.ts +1247 -0
- package/src/components/molecules/Tree/tree-drop.test.ts +223 -0
- package/src/components/organisms/Calendar/Calendar.stories.ts +108 -0
- package/src/components/organisms/Calendar/Calendar.test.ts +279 -0
- package/src/components/organisms/Calendar/_CalendarDayView.test.ts +104 -0
- package/src/components/organisms/Calendar/_CalendarHeader.test.ts +86 -0
- package/src/components/organisms/Calendar/_CalendarMonthView.test.ts +52 -0
- package/src/components/organisms/Calendar/_CalendarTimePicker.test.ts +89 -0
- package/src/components/organisms/Calendar/_CalendarYearView.test.ts +56 -0
- package/src/components/organisms/ColorPicker/ColorPicker.stories.ts +229 -0
- package/src/components/organisms/ColorPicker/ColorPicker.test.ts +638 -0
- package/src/components/organisms/ColorPicker/gradient.test.ts +67 -0
- package/src/components/organisms/ColorPicker/gradientGeometry.test.ts +206 -0
- package/src/components/organisms/Combobox/Combobox.stories.ts +234 -0
- package/src/components/organisms/Combobox/Combobox.test.ts +512 -0
- package/src/components/organisms/Combobox/Combobox.vue +1 -1
- package/src/components/organisms/ComboboxPanel/ComboboxPanel.stories.ts +158 -0
- package/src/components/organisms/ComboboxPanel/ComboboxPanel.test.ts +524 -0
- package/src/components/organisms/ComboboxPanel/ComboboxPanel.vue +1 -1
- package/src/components/organisms/CountrySelect/CountrySelect.stories.ts +139 -0
- package/src/components/organisms/CountrySelect/CountrySelect.test.ts +149 -0
- package/src/components/organisms/DatePicker/DatePicker.stories.ts +163 -0
- package/src/components/organisms/DatePicker/DatePicker.test.ts +214 -0
- package/src/components/organisms/DropdownButton/DropdownButton.stories.ts +123 -0
- package/src/components/organisms/DropdownButton/DropdownButton.test.ts +379 -0
- package/src/components/organisms/FontPicker/FontPicker.stories.ts +476 -0
- package/src/components/organisms/FontPicker/FontPicker.test.ts +873 -0
- package/src/components/organisms/Header/Header.stories.ts +75 -0
- package/src/components/organisms/Header/Header.test.ts +233 -0
- package/src/components/organisms/IconPicker/IconPicker.stories.ts +734 -0
- package/src/components/organisms/IconPicker/IconPicker.test.ts +669 -0
- package/src/components/organisms/IconPicker/_IconPickerGrid.test.ts +184 -0
- package/src/components/organisms/IconPicker/_IconPickerRail.test.ts +102 -0
- package/src/components/organisms/IconPicker/useIconPickerNavigation.test.ts +215 -0
- package/src/components/organisms/Modal/Modal.stories.ts +350 -0
- package/src/components/organisms/Modal/Modal.test.ts +275 -0
- package/src/components/organisms/PickerPanel/PickerPanel.stories.ts +452 -0
- package/src/components/organisms/PickerPanel/PickerPanel.test.ts +358 -0
- package/src/components/organisms/ProgressBar/ProgressBar.stories.ts +100 -0
- package/src/components/organisms/ProgressBar/ProgressBar.test.ts +417 -0
- package/src/components/organisms/ReferenceCard/ReferenceCard.stories.ts +191 -0
- package/src/components/organisms/ReferenceCard/ReferenceCard.test.ts +165 -0
- package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.stories.ts +65 -0
- package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.test.ts +466 -0
- package/src/components/organisms/StepperHeader/StepperHeader.stories.ts +91 -0
- package/src/components/organisms/StepperHeader/StepperHeader.test.ts +279 -0
- package/src/components/organisms/Table/Table.stories.ts +332 -0
- package/src/components/organisms/Table/Table.test.ts +320 -0
- package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.stories.ts +198 -0
- package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.test.ts +283 -0
- package/src/components/organisms/TimelineTrack/TimelineTrack.stories.ts +120 -0
- package/src/components/organisms/TimelineTrack/TimelineTrack.test.ts +91 -0
- package/src/components/organisms/Toaster/Toaster.stories.ts +51 -0
- package/src/components/organisms/Toaster/Toaster.test.ts +75 -0
- package/src/components/organisms/VideoControls/VideoControls.stories.ts +792 -0
- package/src/components/organisms/VideoControls/VideoControls.test.ts +1056 -0
- package/src/components/organisms/VideoPlayer/VideoPlayer.stories.ts +327 -0
- package/src/components/organisms/VideoPlayer/VideoPlayer.test.ts +439 -0
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
import { afterEach, beforeAll, afterAll, describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
|
4
|
+
import IconPicker, {
|
|
5
|
+
type NexxtIconPickerCategory,
|
|
6
|
+
type NexxtIconPickerIcon,
|
|
7
|
+
type NexxtIconPickerStyle,
|
|
8
|
+
type NexxtIconPickerValue,
|
|
9
|
+
} from './IconPicker.vue'
|
|
10
|
+
import {
|
|
11
|
+
iconLabelFromName,
|
|
12
|
+
iconSearchIndex,
|
|
13
|
+
iconStyleFor,
|
|
14
|
+
searchIcons,
|
|
15
|
+
sortIconsByLabel,
|
|
16
|
+
} from './iconPickerHelpers'
|
|
17
|
+
|
|
18
|
+
// Positioning is FloatingPanel's concern and has its own tests; here the panel is simply placed
|
|
19
|
+
vi.mock('@floating-ui/vue', async (importOriginal) => {
|
|
20
|
+
const actual = await importOriginal<typeof import('@floating-ui/vue')>()
|
|
21
|
+
return {
|
|
22
|
+
...actual,
|
|
23
|
+
useFloating: () => ({
|
|
24
|
+
floatingStyles: ref({}),
|
|
25
|
+
placement: ref('bottom-start'),
|
|
26
|
+
isPositioned: ref(true),
|
|
27
|
+
}),
|
|
28
|
+
autoUpdate: () => () => {},
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// jsdom lays nothing out: the grid gets the height of about five rows of the fallback 63px, so the
|
|
33
|
+
// virtual list renders a window and not the whole catalogue
|
|
34
|
+
const GRID_HEIGHT = 320
|
|
35
|
+
beforeAll(() => {
|
|
36
|
+
Object.defineProperty(HTMLElement.prototype, 'clientHeight', {
|
|
37
|
+
configurable: true,
|
|
38
|
+
get(this: HTMLElement) {
|
|
39
|
+
return this.getAttribute('role') === 'grid' ? GRID_HEIGHT : 0
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
afterAll(() => {
|
|
44
|
+
// back to Element.prototype's own getter
|
|
45
|
+
delete (HTMLElement.prototype as { clientHeight?: number }).clientHeight
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const ALL = ['light', 'regular', 'solid']
|
|
49
|
+
|
|
50
|
+
const STYLES: NexxtIconPickerStyle[] = [
|
|
51
|
+
{ id: 'light', label: 'Light', classes: ['style-light'] },
|
|
52
|
+
{ id: 'regular', label: 'Regular', classes: ['style-regular'] },
|
|
53
|
+
{ id: 'solid', label: 'Solid', classes: ['style-solid'] },
|
|
54
|
+
{ id: 'brands', label: 'Merk', classes: ['style-brands'] },
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
const icon = (name: string, label: string, terms: string[] = [], styles = ALL) => ({
|
|
58
|
+
name,
|
|
59
|
+
label,
|
|
60
|
+
terms,
|
|
61
|
+
styles,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const ICONS: NexxtIconPickerIcon[] = [
|
|
65
|
+
icon('house', 'House', ['home', 'building']),
|
|
66
|
+
icon('house-chimney', 'House chimney', ['home']),
|
|
67
|
+
icon('house-user', 'House user', ['home']),
|
|
68
|
+
icon('warehouse', 'Warehouse'),
|
|
69
|
+
icon('building', 'Building', ['apartment']),
|
|
70
|
+
icon('star', 'Star', ['favorite']),
|
|
71
|
+
icon('heart', 'Heart', ['love']),
|
|
72
|
+
icon('bell', 'Bell'),
|
|
73
|
+
icon('tree', 'Tree'),
|
|
74
|
+
icon('sun', 'Sun'),
|
|
75
|
+
icon('moon', 'Moon'),
|
|
76
|
+
icon('kitchen-set', 'Kitchen set', [], ['solid', 'regular']),
|
|
77
|
+
icon('linkedin', 'LinkedIn', ['social'], ['brands']),
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
const CATEGORIES: NexxtIconPickerCategory[] = [
|
|
81
|
+
{ id: 'nature', label: 'Natuur', icons: ['tree', 'sun', 'moon'] },
|
|
82
|
+
{ id: 'buildings', label: 'Gebouwen', icons: ['house', 'building', 'warehouse', 'unknown'] },
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
const iconClass = (name: string) => `glyph-${name}`
|
|
86
|
+
|
|
87
|
+
type Props = Partial<InstanceType<typeof IconPicker>['$props']>
|
|
88
|
+
|
|
89
|
+
const mounted: VueWrapper[] = []
|
|
90
|
+
|
|
91
|
+
const mountPicker = (props: Props = {}) => {
|
|
92
|
+
const holder: { wrapper?: VueWrapper } = {}
|
|
93
|
+
const wrapper = mount(IconPicker, {
|
|
94
|
+
props: {
|
|
95
|
+
icons: ICONS,
|
|
96
|
+
categories: CATEGORIES,
|
|
97
|
+
styles: STYLES,
|
|
98
|
+
iconClass,
|
|
99
|
+
ariaLabel: 'Symbool',
|
|
100
|
+
'onUpdate:modelValue': (value: NexxtIconPickerValue | null) =>
|
|
101
|
+
holder.wrapper?.setProps({ modelValue: value }),
|
|
102
|
+
...props,
|
|
103
|
+
},
|
|
104
|
+
attachTo: document.body,
|
|
105
|
+
})
|
|
106
|
+
holder.wrapper = wrapper
|
|
107
|
+
mounted.push(wrapper)
|
|
108
|
+
return wrapper
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
mounted.splice(0).forEach((wrapper) => wrapper.unmount())
|
|
113
|
+
document.body.innerHTML = ''
|
|
114
|
+
vi.useRealTimers()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const triggerOf = (wrapper: VueWrapper) =>
|
|
118
|
+
wrapper.find<HTMLButtonElement>('button[aria-haspopup="grid"]').element
|
|
119
|
+
// The panel is teleported to <body>
|
|
120
|
+
const inputOf = () => document.querySelector<HTMLInputElement>('input')!
|
|
121
|
+
const gridOf = () => document.querySelector<HTMLElement>('[role="grid"]')
|
|
122
|
+
const cells = () => Array.from(document.querySelectorAll<HTMLElement>('[role="gridcell"]'))
|
|
123
|
+
const names = () => cells().map((cell) => cell.getAttribute('aria-label'))
|
|
124
|
+
const tabs = () => Array.from(document.querySelectorAll<HTMLElement>('[role="tab"]'))
|
|
125
|
+
const tab = (name: string) => tabs().find((element) => element.textContent?.trim() === name)!
|
|
126
|
+
const activeCell = () => {
|
|
127
|
+
const id = gridOf()?.getAttribute('aria-activedescendant')
|
|
128
|
+
return id ? document.getElementById(id) : null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const open = async (wrapper: VueWrapper) => {
|
|
132
|
+
triggerOf(wrapper).click()
|
|
133
|
+
await flushPromises()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const keydown = async (target: Element, key: string, init: KeyboardEventInit = {}) => {
|
|
137
|
+
target.dispatchEvent(
|
|
138
|
+
new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true, ...init }),
|
|
139
|
+
)
|
|
140
|
+
await flushPromises()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const type = async (value: string) => {
|
|
144
|
+
const input = inputOf()
|
|
145
|
+
input.value = value
|
|
146
|
+
input.dispatchEvent(new Event('input'))
|
|
147
|
+
await flushPromises()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const focusGrid = async () => {
|
|
151
|
+
gridOf()!.focus()
|
|
152
|
+
await flushPromises()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
describe('icon picker helpers', () => {
|
|
156
|
+
it('derives a label from a name', () => {
|
|
157
|
+
expect(iconLabelFromName('house-chimney')).toBe('House chimney')
|
|
158
|
+
expect(iconLabelFromName('star')).toBe('Star')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('ranks the exact name first, then starts-with, then a word that starts, then the rest', () => {
|
|
162
|
+
const index = iconSearchIndex([
|
|
163
|
+
icon('house-user', 'House user'),
|
|
164
|
+
icon('warehouse', 'Warehouse'),
|
|
165
|
+
icon('user-house', 'User house'),
|
|
166
|
+
icon('house', 'House'),
|
|
167
|
+
icon('home-alt', 'Alternative', ['house']),
|
|
168
|
+
])
|
|
169
|
+
expect(searchIcons(index, 'house').map(({ name }) => name)).toEqual([
|
|
170
|
+
'house',
|
|
171
|
+
'house-user',
|
|
172
|
+
'user-house',
|
|
173
|
+
'home-alt',
|
|
174
|
+
'warehouse',
|
|
175
|
+
])
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('matches every word in any case on name, label and terms, and finds nothing for an empty search', () => {
|
|
179
|
+
const index = iconSearchIndex(ICONS)
|
|
180
|
+
expect(searchIcons(index, 'home').map(({ name }) => name)).toEqual([
|
|
181
|
+
'house',
|
|
182
|
+
'house-chimney',
|
|
183
|
+
'house-user',
|
|
184
|
+
])
|
|
185
|
+
expect(searchIcons(index, 'HOUSE chim').map(({ name }) => name)).toEqual(['house-chimney'])
|
|
186
|
+
expect(searchIcons(index, 'house love')).toEqual([])
|
|
187
|
+
expect(searchIcons(index, ' ')).toEqual([])
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('sorts in the given locale', () => {
|
|
191
|
+
const list = [icon('z', 'Zebra'), icon('a', 'Ärger'), icon('b', 'Apen')]
|
|
192
|
+
expect(sortIconsByLabel(list).map(({ label }) => label)).toEqual(['Apen', 'Ärger', 'Zebra'])
|
|
193
|
+
expect(sortIconsByLabel(list, 'sv').map(({ label }) => label)).toEqual([
|
|
194
|
+
'Apen',
|
|
195
|
+
'Zebra',
|
|
196
|
+
'Ärger',
|
|
197
|
+
])
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('keeps the preferred style when the icon has it, the first of the order otherwise', () => {
|
|
201
|
+
const order = ['light', 'regular', 'solid', 'brands']
|
|
202
|
+
expect(iconStyleFor(['light', 'solid'], 'solid', order)).toBe('solid')
|
|
203
|
+
expect(iconStyleFor(['solid', 'regular'], 'light', order)).toBe('regular')
|
|
204
|
+
expect(iconStyleFor(['brands'], 'light', order)).toBe('brands')
|
|
205
|
+
expect(iconStyleFor(['duotone'], 'light', order)).toBeNull()
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
describe('IconPicker', () => {
|
|
210
|
+
describe('trigger', () => {
|
|
211
|
+
it('reads the name, the icon and its style, and promises a grid', () => {
|
|
212
|
+
const wrapper = mountPicker({ modelValue: { icon: 'star', style: 'light' } })
|
|
213
|
+
const trigger = triggerOf(wrapper)
|
|
214
|
+
expect(trigger.getAttribute('aria-label')).toBe('Symbool, Star, Light')
|
|
215
|
+
expect(trigger.getAttribute('aria-haspopup')).toBe('grid')
|
|
216
|
+
expect(trigger.getAttribute('aria-expanded')).toBe('false')
|
|
217
|
+
expect(trigger.querySelector('i')!.className).toContain('style-light glyph-star')
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('shows the placeholder without a value, and names an icon outside the catalogue', () => {
|
|
221
|
+
const empty = mountPicker()
|
|
222
|
+
expect(triggerOf(empty).textContent).toContain('Kies een icoon')
|
|
223
|
+
const unknown = mountPicker({ modelValue: { icon: 'paper-plane', style: 'solid' } })
|
|
224
|
+
expect(triggerOf(unknown).getAttribute('aria-label')).toBe('Symbool, Paper plane, Solid')
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('is labelled by a visible label and the value', () => {
|
|
228
|
+
const wrapper = mountPicker({ label: 'Icoon', ariaLabel: undefined })
|
|
229
|
+
const trigger = triggerOf(wrapper)
|
|
230
|
+
const [labelId, valueId] = trigger.getAttribute('aria-labelledby')!.split(' ')
|
|
231
|
+
expect(document.getElementById(labelId!)!.textContent?.trim()).toBe('Icoon')
|
|
232
|
+
expect(document.getElementById(valueId!)!.textContent?.trim()).toBe('Kies een icoon')
|
|
233
|
+
expect(trigger.hasAttribute('aria-label')).toBe(false)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('warns in development without a label or an accessible name', () => {
|
|
237
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
238
|
+
mountPicker({ ariaLabel: undefined })
|
|
239
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('[NexxtIconPicker]'))
|
|
240
|
+
warn.mockRestore()
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('opens on ArrowDown with focus in the search field', async () => {
|
|
244
|
+
const wrapper = mountPicker()
|
|
245
|
+
await keydown(triggerOf(wrapper), 'ArrowDown')
|
|
246
|
+
expect(wrapper.emitted('open')).toHaveLength(1)
|
|
247
|
+
expect(document.activeElement).toBe(inputOf())
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
describe('panel', () => {
|
|
252
|
+
it('lists "Alle iconen" first, then the categories sorted in Dutch', async () => {
|
|
253
|
+
const wrapper = mountPicker()
|
|
254
|
+
await open(wrapper)
|
|
255
|
+
expect(tabs().map((element) => element.textContent?.trim())).toEqual([
|
|
256
|
+
'Alle iconen',
|
|
257
|
+
'Gebouwen',
|
|
258
|
+
'Natuur',
|
|
259
|
+
])
|
|
260
|
+
expect(tab('Alle iconen').getAttribute('aria-selected')).toBe('true')
|
|
261
|
+
expect(tab('Natuur').getAttribute('title')).toBe('Natuur')
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('leaves out a category without a single icon the catalogue knows', async () => {
|
|
265
|
+
const wrapper = mountPicker({
|
|
266
|
+
categories: [...CATEGORIES, { id: 'ghost', label: 'Spoken', icons: ['nope', 'gone'] }],
|
|
267
|
+
})
|
|
268
|
+
await open(wrapper)
|
|
269
|
+
expect(tabs().map((element) => element.textContent?.trim())).toEqual([
|
|
270
|
+
'Alle iconen',
|
|
271
|
+
'Gebouwen',
|
|
272
|
+
'Natuur',
|
|
273
|
+
])
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('falls back to "Alle iconen" when the chosen category empties', async () => {
|
|
277
|
+
const wrapper = mountPicker()
|
|
278
|
+
await open(wrapper)
|
|
279
|
+
tab('Natuur').click()
|
|
280
|
+
await flushPromises()
|
|
281
|
+
expect(names()).toEqual(['Moon', 'Sun', 'Tree'])
|
|
282
|
+
|
|
283
|
+
// a reloaded catalogue without any icon of the category
|
|
284
|
+
const without = ICONS.filter(({ name }) => !['tree', 'sun', 'moon'].includes(name))
|
|
285
|
+
await wrapper.setProps({ icons: without })
|
|
286
|
+
await flushPromises()
|
|
287
|
+
expect(tabs().map((element) => element.textContent?.trim())).toEqual([
|
|
288
|
+
'Alle iconen',
|
|
289
|
+
'Gebouwen',
|
|
290
|
+
])
|
|
291
|
+
expect(tab('Alle iconen').getAttribute('aria-selected')).toBe('true')
|
|
292
|
+
expect(document.querySelector('[role="tabpanel"]')!.getAttribute('aria-labelledby')).toBe(
|
|
293
|
+
tab('Alle iconen').id,
|
|
294
|
+
)
|
|
295
|
+
expect(names()).toHaveLength(without.length)
|
|
296
|
+
|
|
297
|
+
// and to it again when the category disappears from the props
|
|
298
|
+
tab('Gebouwen').click()
|
|
299
|
+
await flushPromises()
|
|
300
|
+
await wrapper.setProps({ categories: [CATEGORIES[0]!] })
|
|
301
|
+
await flushPromises()
|
|
302
|
+
expect(tab('Alle iconen').getAttribute('aria-selected')).toBe('true')
|
|
303
|
+
expect(names()).toHaveLength(without.length)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('keeps one tab stop and moves over the shown tabs only', async () => {
|
|
307
|
+
const wrapper = mountPicker({
|
|
308
|
+
categories: [{ id: 'ghost', label: 'Aaa leeg', icons: ['nope'] }, ...CATEGORIES],
|
|
309
|
+
})
|
|
310
|
+
await open(wrapper)
|
|
311
|
+
expect(tabs().filter((element) => element.tabIndex === 0)).toHaveLength(1)
|
|
312
|
+
await keydown(tab('Alle iconen'), 'ArrowDown')
|
|
313
|
+
expect(document.activeElement).toBe(tab('Gebouwen'))
|
|
314
|
+
await keydown(tab('Gebouwen'), 'End')
|
|
315
|
+
expect(document.activeElement).toBe(tab('Natuur'))
|
|
316
|
+
expect(tabs().filter((element) => element.tabIndex === 0)).toEqual([tab('Natuur')])
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('sorts rail and grid in `sortLocale`', async () => {
|
|
320
|
+
const list = [icon('z', 'Zebra'), icon('a', 'Ärger'), icon('b', 'Apen')]
|
|
321
|
+
const categories = [
|
|
322
|
+
{ id: 'z', label: 'Zon', icons: ['z'] },
|
|
323
|
+
{ id: 'a', label: 'Äpfel', icons: ['a'] },
|
|
324
|
+
]
|
|
325
|
+
const wrapper = mountPicker({ icons: list, categories, sortLocale: 'sv' })
|
|
326
|
+
await open(wrapper)
|
|
327
|
+
expect(names()).toEqual(['Apen', 'Zebra', 'Ärger'])
|
|
328
|
+
expect(tabs().map((element) => element.textContent?.trim())).toEqual([
|
|
329
|
+
'Alle iconen',
|
|
330
|
+
'Zon',
|
|
331
|
+
'Äpfel',
|
|
332
|
+
])
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('points the search field at the grid, and nowhere when there is no grid', async () => {
|
|
336
|
+
const wrapper = mountPicker()
|
|
337
|
+
await open(wrapper)
|
|
338
|
+
expect(inputOf().getAttribute('aria-controls')).toBe(gridOf()!.id)
|
|
339
|
+
await type('zzz')
|
|
340
|
+
expect(gridOf()).toBeNull()
|
|
341
|
+
expect(inputOf().hasAttribute('aria-controls')).toBe(false)
|
|
342
|
+
expect(document.body.textContent).toContain(
|
|
343
|
+
'Niets voor ‘zzz’. Zoeken werkt alleen met Engelse woorden.',
|
|
344
|
+
)
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it('selects exactly one cell: the first place of the current icon, in the pinned section', async () => {
|
|
348
|
+
const wrapper = mountPicker({
|
|
349
|
+
modelValue: { icon: 'star', style: 'light' },
|
|
350
|
+
pinned: { label: 'Op deze pagina', icons: ['bell', 'star', 'heart'] },
|
|
351
|
+
})
|
|
352
|
+
await open(wrapper)
|
|
353
|
+
const headers = Array.from(document.querySelectorAll('[role="rowheader"]'))
|
|
354
|
+
expect(headers.map((header) => header.textContent)).toEqual(['Op deze pagina', 'Alle iconen'])
|
|
355
|
+
expect(names().slice(0, 3)).toEqual(['Star', 'Bell', 'Heart'])
|
|
356
|
+
const selected = document.querySelectorAll('[aria-selected="true"][role="gridcell"]')
|
|
357
|
+
expect(selected).toHaveLength(1)
|
|
358
|
+
expect(selected[0]!.getAttribute('aria-label')).toBe('Star')
|
|
359
|
+
expect(selected[0]!.closest('[role="row"]')!.getAttribute('aria-rowindex')).toBe('2')
|
|
360
|
+
const description = document.getElementById(selected[0]!.getAttribute('aria-describedby')!)
|
|
361
|
+
expect(description!.textContent).toBe('Huidige keuze')
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it('limits the pinned section to two rows', async () => {
|
|
365
|
+
const wrapper = mountPicker({
|
|
366
|
+
pinned: { label: 'Op deze pagina', icons: ICONS.map(({ name }) => name) },
|
|
367
|
+
})
|
|
368
|
+
await open(wrapper)
|
|
369
|
+
const rows = Array.from(document.querySelectorAll('[role="row"]'))
|
|
370
|
+
const all = rows.findIndex((row) => row.textContent === 'Alle iconen')
|
|
371
|
+
expect(rows.slice(1, all)).toHaveLength(2)
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
it('draws each cell in the current style when the icon has it, its first otherwise', async () => {
|
|
375
|
+
const wrapper = mountPicker({ modelValue: { icon: 'star', style: 'light' } })
|
|
376
|
+
await open(wrapper)
|
|
377
|
+
// the style's classes and the glyph class, whatever else the cell adds for layout
|
|
378
|
+
const glyph = (name: string) =>
|
|
379
|
+
Array.from(
|
|
380
|
+
cells()
|
|
381
|
+
.find((cell) => cell.getAttribute('aria-label') === name)!
|
|
382
|
+
.querySelector('i')!.classList,
|
|
383
|
+
).filter((value) => /^(style|glyph)-/.test(value))
|
|
384
|
+
expect(glyph('Heart')).toEqual(['style-light', 'glyph-heart'])
|
|
385
|
+
expect(glyph('Kitchen set')).toEqual(['style-regular', 'glyph-kitchen-set'])
|
|
386
|
+
expect(glyph('LinkedIn')).toEqual(['style-brands', 'glyph-linkedin'])
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it('merges messages per key with the Dutch defaults', async () => {
|
|
390
|
+
const wrapper = mountPicker({ messages: { searchLabel: 'Search icons' } })
|
|
391
|
+
await open(wrapper)
|
|
392
|
+
expect(inputOf().getAttribute('aria-label')).toBe('Search icons')
|
|
393
|
+
expect(inputOf().getAttribute('placeholder')).toBe('Zoek in het Engels, bijv. house')
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it('announces the first search after reopening on a search of only spaces', async () => {
|
|
397
|
+
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
|
|
398
|
+
const wrapper = mountPicker()
|
|
399
|
+
await open(wrapper)
|
|
400
|
+
await type(' ')
|
|
401
|
+
await keydown(inputOf(), 'Escape')
|
|
402
|
+
expect(gridOf()).toBeNull()
|
|
403
|
+
|
|
404
|
+
await open(wrapper)
|
|
405
|
+
await type('a')
|
|
406
|
+
vi.advanceTimersByTime(500)
|
|
407
|
+
await flushPromises()
|
|
408
|
+
const count = searchIcons(iconSearchIndex(ICONS), 'a').length
|
|
409
|
+
expect(document.querySelector('[aria-live="polite"]')!.textContent?.trim()).toBe(
|
|
410
|
+
`${count} iconen`,
|
|
411
|
+
)
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it('announces the count 500ms after a search or a category change, not on opening', async () => {
|
|
415
|
+
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
|
|
416
|
+
const wrapper = mountPicker()
|
|
417
|
+
await open(wrapper)
|
|
418
|
+
const live = document.querySelector('[aria-live="polite"]')!
|
|
419
|
+
tab('Natuur').click()
|
|
420
|
+
await flushPromises()
|
|
421
|
+
vi.advanceTimersByTime(499)
|
|
422
|
+
await flushPromises()
|
|
423
|
+
expect(live.textContent?.trim()).toBe('')
|
|
424
|
+
vi.advanceTimersByTime(1)
|
|
425
|
+
await flushPromises()
|
|
426
|
+
expect(live.textContent?.trim()).toBe('3 iconen')
|
|
427
|
+
|
|
428
|
+
await type('heart')
|
|
429
|
+
vi.advanceTimersByTime(500)
|
|
430
|
+
await flushPromises()
|
|
431
|
+
expect(live.textContent?.trim()).toBe('1 icoon')
|
|
432
|
+
|
|
433
|
+
// closing and opening again resets search and category; that is no change to announce
|
|
434
|
+
wrapper.findComponent({ name: 'NexxtFloatingPanel' }).vm.close()
|
|
435
|
+
await flushPromises()
|
|
436
|
+
await open(wrapper)
|
|
437
|
+
vi.advanceTimersByTime(1000)
|
|
438
|
+
await flushPromises()
|
|
439
|
+
expect(document.querySelector('[aria-live="polite"]')!.textContent?.trim()).toBe('')
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it('searches the whole catalogue and a category clears the search', async () => {
|
|
443
|
+
const wrapper = mountPicker()
|
|
444
|
+
await open(wrapper)
|
|
445
|
+
tab('Natuur').click()
|
|
446
|
+
await flushPromises()
|
|
447
|
+
expect(names()).toEqual(['Moon', 'Sun', 'Tree'])
|
|
448
|
+
await type('house')
|
|
449
|
+
expect(tab('Alle iconen').getAttribute('aria-selected')).toBe('true')
|
|
450
|
+
expect(names()).toEqual(['House', 'House chimney', 'House user', 'Warehouse'])
|
|
451
|
+
tab('Gebouwen').click()
|
|
452
|
+
await flushPromises()
|
|
453
|
+
expect(inputOf().value).toBe('')
|
|
454
|
+
// a name the catalogue does not know is skipped
|
|
455
|
+
expect(names()).toEqual(['Building', 'House', 'Warehouse'])
|
|
456
|
+
})
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
describe('keyboard', () => {
|
|
460
|
+
it('moves from the field into the grid onto the current icon', async () => {
|
|
461
|
+
const wrapper = mountPicker({ modelValue: { icon: 'heart', style: 'light' } })
|
|
462
|
+
await open(wrapper)
|
|
463
|
+
await keydown(inputOf(), 'ArrowDown')
|
|
464
|
+
expect(document.activeElement).toBe(gridOf())
|
|
465
|
+
expect(activeCell()!.getAttribute('aria-label')).toBe('Heart')
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
it('walks the grid with arrows, Home/End and Ctrl+Home/End', async () => {
|
|
469
|
+
const wrapper = mountPicker()
|
|
470
|
+
await open(wrapper)
|
|
471
|
+
await focusGrid()
|
|
472
|
+
const grid = gridOf()!
|
|
473
|
+
// sorted: Bell, Building, Heart, House, House chimney, House user | Kitchen set, LinkedIn, Moon, ...
|
|
474
|
+
expect(activeCell()!.getAttribute('aria-label')).toBe('Bell')
|
|
475
|
+
await keydown(grid, 'ArrowRight')
|
|
476
|
+
expect(activeCell()!.getAttribute('aria-label')).toBe('Building')
|
|
477
|
+
await keydown(grid, 'End')
|
|
478
|
+
expect(activeCell()!.getAttribute('aria-label')).toBe('House user')
|
|
479
|
+
await keydown(grid, 'ArrowRight')
|
|
480
|
+
expect(activeCell()!.getAttribute('aria-label')).toBe('Kitchen set')
|
|
481
|
+
await keydown(grid, 'ArrowLeft')
|
|
482
|
+
expect(activeCell()!.getAttribute('aria-label')).toBe('House user')
|
|
483
|
+
await keydown(grid, 'Home')
|
|
484
|
+
expect(activeCell()!.getAttribute('aria-label')).toBe('Bell')
|
|
485
|
+
await keydown(grid, 'ArrowDown')
|
|
486
|
+
expect(activeCell()!.getAttribute('aria-label')).toBe('Kitchen set')
|
|
487
|
+
await keydown(grid, 'End', { ctrlKey: true })
|
|
488
|
+
expect(activeCell()!.getAttribute('aria-label')).toBe('Warehouse')
|
|
489
|
+
await keydown(grid, 'Home', { ctrlKey: true })
|
|
490
|
+
expect(activeCell()!.getAttribute('aria-label')).toBe('Bell')
|
|
491
|
+
await keydown(grid, 'ArrowUp')
|
|
492
|
+
expect(document.activeElement).toBe(inputOf())
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
it('pages by the rows in view, less one', async () => {
|
|
496
|
+
const many = Array.from({ length: 120 }, (_, index) =>
|
|
497
|
+
icon(`icon-${index}`, `Icon ${String(index).padStart(3, '0')}`),
|
|
498
|
+
)
|
|
499
|
+
const wrapper = mountPicker({ icons: many })
|
|
500
|
+
await open(wrapper)
|
|
501
|
+
await focusGrid()
|
|
502
|
+
const rowOf = () => activeCell()!.closest('[role="row"]')!.getAttribute('aria-rowindex')
|
|
503
|
+
// 320px over rows of 63px: five rows, so a page is four
|
|
504
|
+
await keydown(gridOf()!, 'PageDown')
|
|
505
|
+
expect(rowOf()).toBe('5')
|
|
506
|
+
await keydown(gridOf()!, 'PageUp')
|
|
507
|
+
expect(rowOf()).toBe('1')
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
it('types a letter from the grid into the search field', async () => {
|
|
511
|
+
const wrapper = mountPicker()
|
|
512
|
+
await open(wrapper)
|
|
513
|
+
await focusGrid()
|
|
514
|
+
await keydown(gridOf()!, 's')
|
|
515
|
+
expect(document.activeElement).toBe(inputOf())
|
|
516
|
+
expect(inputOf().value).toBe('s')
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
it('chooses with Enter, closes and puts focus on the trigger', async () => {
|
|
520
|
+
const wrapper = mountPicker({ modelValue: { icon: 'star', style: 'light' } })
|
|
521
|
+
await open(wrapper)
|
|
522
|
+
await type('kitchen')
|
|
523
|
+
await keydown(inputOf(), 'ArrowDown')
|
|
524
|
+
await keydown(gridOf()!, 'Enter')
|
|
525
|
+
expect(wrapper.emitted('update:modelValue')!.at(-1)).toEqual([
|
|
526
|
+
{ icon: 'kitchen-set', style: 'regular' },
|
|
527
|
+
])
|
|
528
|
+
expect(gridOf()).toBeNull()
|
|
529
|
+
expect(document.activeElement).toBe(triggerOf(wrapper))
|
|
530
|
+
expect(wrapper.emitted('close')).toHaveLength(1)
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
it('chooses the first result with Enter in the field, only with a search', async () => {
|
|
534
|
+
const wrapper = mountPicker({ modelValue: { icon: 'star', style: 'solid' } })
|
|
535
|
+
await open(wrapper)
|
|
536
|
+
await keydown(inputOf(), 'Enter')
|
|
537
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
538
|
+
await type('linked')
|
|
539
|
+
await keydown(inputOf(), 'Enter')
|
|
540
|
+
expect(wrapper.emitted('update:modelValue')!.at(-1)).toEqual([
|
|
541
|
+
{ icon: 'linkedin', style: 'brands' },
|
|
542
|
+
])
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
it('closes with Escape without a change and highlights the current icon on reopening', async () => {
|
|
546
|
+
const wrapper = mountPicker({ modelValue: { icon: 'sun', style: 'light' } })
|
|
547
|
+
await open(wrapper)
|
|
548
|
+
tab('Gebouwen').click()
|
|
549
|
+
await flushPromises()
|
|
550
|
+
await keydown(document.activeElement ?? document.body, 'Escape')
|
|
551
|
+
expect(gridOf()).toBeNull()
|
|
552
|
+
expect(document.activeElement).toBe(triggerOf(wrapper))
|
|
553
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
554
|
+
|
|
555
|
+
await open(wrapper)
|
|
556
|
+
expect(tab('Alle iconen').getAttribute('aria-selected')).toBe('true')
|
|
557
|
+
await focusGrid()
|
|
558
|
+
expect(activeCell()!.getAttribute('aria-label')).toBe('Sun')
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
it('moves and chooses in the rail with the arrow keys', async () => {
|
|
562
|
+
const wrapper = mountPicker()
|
|
563
|
+
await open(wrapper)
|
|
564
|
+
const all = tab('Alle iconen')
|
|
565
|
+
expect(all.tabIndex).toBe(0)
|
|
566
|
+
expect(tab('Gebouwen').tabIndex).toBe(-1)
|
|
567
|
+
await keydown(all, 'ArrowDown')
|
|
568
|
+
expect(tab('Gebouwen').getAttribute('aria-selected')).toBe('true')
|
|
569
|
+
expect(document.activeElement).toBe(tab('Gebouwen'))
|
|
570
|
+
await keydown(tab('Gebouwen'), 'End')
|
|
571
|
+
expect(tab('Natuur').getAttribute('aria-selected')).toBe('true')
|
|
572
|
+
expect(document.querySelector('[role="tabpanel"]')!.getAttribute('aria-labelledby')).toBe(
|
|
573
|
+
tab('Natuur').id,
|
|
574
|
+
)
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
it('closes when focus leaves the panel and the trigger', async () => {
|
|
578
|
+
const outside = document.createElement('button')
|
|
579
|
+
document.body.append(outside)
|
|
580
|
+
const wrapper = mountPicker()
|
|
581
|
+
await open(wrapper)
|
|
582
|
+
gridOf()!.dispatchEvent(new FocusEvent('focusout', { bubbles: true, relatedTarget: outside }))
|
|
583
|
+
await flushPromises()
|
|
584
|
+
expect(gridOf()).toBeNull()
|
|
585
|
+
})
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
describe('virtual grid', () => {
|
|
589
|
+
const LARGE = Array.from({ length: 4317 }, (_, index) =>
|
|
590
|
+
icon(`icon-${index}`, `Icon ${String(index).padStart(4, '0')}`),
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
it('renders a window of rows, never the whole catalogue', async () => {
|
|
594
|
+
const wrapper = mountPicker({ icons: LARGE, categories: [] })
|
|
595
|
+
await open(wrapper)
|
|
596
|
+
expect(gridOf()!.getAttribute('aria-rowcount')).toBe(String(Math.ceil(4317 / 6)))
|
|
597
|
+
expect(cells().length).toBeGreaterThan(0)
|
|
598
|
+
expect(cells().length).toBeLessThanOrEqual(150)
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
it('points aria-activedescendant only at a rendered cell', async () => {
|
|
602
|
+
const wrapper = mountPicker({ icons: LARGE, categories: [] })
|
|
603
|
+
await open(wrapper)
|
|
604
|
+
await focusGrid()
|
|
605
|
+
const grid = gridOf()!
|
|
606
|
+
for (const [key, init] of [
|
|
607
|
+
['End', { ctrlKey: true }],
|
|
608
|
+
['ArrowLeft', {}],
|
|
609
|
+
['PageUp', {}],
|
|
610
|
+
['Home', { ctrlKey: true }],
|
|
611
|
+
['PageDown', {}],
|
|
612
|
+
] as const) {
|
|
613
|
+
await keydown(grid, key, init)
|
|
614
|
+
const id = grid.getAttribute('aria-activedescendant')
|
|
615
|
+
expect(id).toBeTruthy()
|
|
616
|
+
expect(document.getElementById(id!)).not.toBeNull()
|
|
617
|
+
expect(cells().length).toBeLessThanOrEqual(150)
|
|
618
|
+
}
|
|
619
|
+
await keydown(grid, 'End', { ctrlKey: true })
|
|
620
|
+
expect(activeCell()!.getAttribute('aria-label')).toBe('Icon 4316')
|
|
621
|
+
})
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
describe('catalogue status', () => {
|
|
625
|
+
it('shows skeletons while loading, and applies a typed search once the catalogue is in', async () => {
|
|
626
|
+
const wrapper = mountPicker({
|
|
627
|
+
icons: [],
|
|
628
|
+
catalogueStatus: 'loading',
|
|
629
|
+
modelValue: { icon: 'star', style: 'light' },
|
|
630
|
+
})
|
|
631
|
+
await open(wrapper)
|
|
632
|
+
const grid = gridOf()!
|
|
633
|
+
expect(grid.getAttribute('aria-busy')).toBe('true')
|
|
634
|
+
expect(grid.querySelectorAll('.animate-pulse')).toHaveLength(36)
|
|
635
|
+
expect(
|
|
636
|
+
document.querySelector('[role="tablist"]')!.querySelectorAll('.animate-pulse'),
|
|
637
|
+
).toHaveLength(8)
|
|
638
|
+
// no real categories next to the skeletons: without a catalogue they are all empty
|
|
639
|
+
expect(tabs().map((element) => element.textContent?.trim())).toEqual(['Alle iconen'])
|
|
640
|
+
|
|
641
|
+
await type('house')
|
|
642
|
+
await wrapper.setProps({ icons: ICONS, catalogueStatus: 'ready' })
|
|
643
|
+
await flushPromises()
|
|
644
|
+
expect(names()).toEqual(['House', 'House chimney', 'House user', 'Warehouse'])
|
|
645
|
+
})
|
|
646
|
+
|
|
647
|
+
it('keeps the current icon choosable on an error, with a retry', async () => {
|
|
648
|
+
const wrapper = mountPicker({
|
|
649
|
+
icons: [],
|
|
650
|
+
catalogueStatus: 'error',
|
|
651
|
+
modelValue: { icon: 'house-chimney', style: 'solid' },
|
|
652
|
+
})
|
|
653
|
+
await open(wrapper)
|
|
654
|
+
expect(names()).toEqual(['House chimney'])
|
|
655
|
+
expect(cells()[0]!.getAttribute('aria-selected')).toBe('true')
|
|
656
|
+
const status = document.querySelector('[role="status"]')!
|
|
657
|
+
expect(status.textContent).toContain('De iconen konden niet worden geladen.')
|
|
658
|
+
status.querySelector('button')!.click()
|
|
659
|
+
expect(wrapper.emitted('retry')).toHaveLength(1)
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
it('renders no grid and no aria-controls on an error without a current icon', async () => {
|
|
663
|
+
const wrapper = mountPicker({ icons: [], catalogueStatus: 'error' })
|
|
664
|
+
await open(wrapper)
|
|
665
|
+
expect(gridOf()).toBeNull()
|
|
666
|
+
expect(inputOf().hasAttribute('aria-controls')).toBe(false)
|
|
667
|
+
})
|
|
668
|
+
})
|
|
669
|
+
})
|