@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.
- package/dist/index.js +49 -126
- 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/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/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,870 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { nextTick } from 'vue'
|
|
3
|
+
import { mount, type ComponentMountingOptions, type VueWrapper } from '@vue/test-utils'
|
|
4
|
+
import PickerList, { type NexxtPickerGroupData, type NexxtPickerItemData } from './PickerList.vue'
|
|
5
|
+
|
|
6
|
+
const GROUPS: NexxtPickerGroupData[] = [
|
|
7
|
+
{ id: 'basis', label: 'Basis' },
|
|
8
|
+
{ id: 'media', label: 'Media' },
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
const ITEMS: NexxtPickerItemData[] = [
|
|
12
|
+
{ id: 'intro', label: 'Introductie', icon: 'sparkles' },
|
|
13
|
+
{ id: 'title', label: 'Titel', icon: 'heading', group: 'basis', keywords: ['kop'] },
|
|
14
|
+
{ id: 'text', label: 'Tekst', icon: 'font', group: 'basis', keywords: ['alinea'] },
|
|
15
|
+
{
|
|
16
|
+
id: 'table',
|
|
17
|
+
label: 'Tabel',
|
|
18
|
+
icon: 'table',
|
|
19
|
+
group: 'basis',
|
|
20
|
+
disabled: true,
|
|
21
|
+
description: 'Nog niet beschikbaar',
|
|
22
|
+
},
|
|
23
|
+
{ id: 'image', label: 'Afbeelding', icon: 'image', group: 'media', keywords: ['foto'] },
|
|
24
|
+
{ id: 'video', label: 'Video', icon: 'video', group: 'media' },
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
/** 3 groups × 11 items, for paging and the ends of the list. */
|
|
28
|
+
const MANY_GROUPS: NexxtPickerGroupData[] = ['a', 'b', 'c'].map((id) => ({
|
|
29
|
+
id,
|
|
30
|
+
label: `Groep ${id}`,
|
|
31
|
+
}))
|
|
32
|
+
const MANY_ITEMS: NexxtPickerItemData[] = MANY_GROUPS.flatMap((group) =>
|
|
33
|
+
Array.from({ length: 11 }, (_, index) => ({
|
|
34
|
+
id: `${group.id}${index}`,
|
|
35
|
+
label: `Item ${group.id}${index}`,
|
|
36
|
+
icon: 'square' as const,
|
|
37
|
+
group: group.id,
|
|
38
|
+
})),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
type Props = Partial<InstanceType<typeof PickerList>['$props']>
|
|
42
|
+
/** Everything but the props, which `mountList` builds from its first argument. */
|
|
43
|
+
type Options = Omit<ComponentMountingOptions<typeof PickerList>, 'props' | 'propsData'>
|
|
44
|
+
|
|
45
|
+
const mountList = (props: Props = {}, options: Options = {}) =>
|
|
46
|
+
mount(PickerList, {
|
|
47
|
+
props: {
|
|
48
|
+
items: ITEMS,
|
|
49
|
+
groups: GROUPS,
|
|
50
|
+
ariaLabel: 'Onderdelen',
|
|
51
|
+
// keeps the model in step with the component, as `v-model:highlighted` would
|
|
52
|
+
'onUpdate:highlighted': (value: string | null) => {
|
|
53
|
+
void wrapperRef.current?.setProps({ highlighted: value })
|
|
54
|
+
},
|
|
55
|
+
...props,
|
|
56
|
+
},
|
|
57
|
+
...options,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const wrapperRef: { current: VueWrapper | null } = { current: null }
|
|
61
|
+
|
|
62
|
+
const mountBound = (props: Props = {}, options: Options = {}) => {
|
|
63
|
+
const wrapper = mountList(props, options)
|
|
64
|
+
wrapperRef.current = wrapper
|
|
65
|
+
return wrapper
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const listboxOf = (wrapper: VueWrapper) => wrapper.find('[role="listbox"]')
|
|
69
|
+
|
|
70
|
+
const optionLabels = (wrapper: VueWrapper) =>
|
|
71
|
+
wrapper
|
|
72
|
+
.findAll('[role="option"]')
|
|
73
|
+
.map((option) => option.findAll('span:not([hidden])').at(-1)!.text())
|
|
74
|
+
|
|
75
|
+
const activeId = (wrapper: VueWrapper) => {
|
|
76
|
+
const id = listboxOf(wrapper).attributes('aria-activedescendant')
|
|
77
|
+
return id ? decodeURIComponent(id.split('-opt-')[1]) : null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const press = async (wrapper: VueWrapper, key: string, init: KeyboardEventInit = {}) => {
|
|
81
|
+
const event = new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true, ...init })
|
|
82
|
+
listboxOf(wrapper).element.dispatchEvent(event)
|
|
83
|
+
await nextTick()
|
|
84
|
+
await nextTick()
|
|
85
|
+
return event
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const pointerMove = async (element: Element, x: number, y: number) => {
|
|
89
|
+
const event = new MouseEvent('pointermove', { bubbles: true, clientX: x, clientY: y })
|
|
90
|
+
element.dispatchEvent(event)
|
|
91
|
+
await nextTick()
|
|
92
|
+
await nextTick()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe('PickerList', () => {
|
|
96
|
+
afterEach(() => {
|
|
97
|
+
wrapperRef.current = null
|
|
98
|
+
vi.useRealTimers()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('structure', () => {
|
|
102
|
+
it('is a named listbox and the only tab stop', () => {
|
|
103
|
+
const wrapper = mountBound()
|
|
104
|
+
const listbox = listboxOf(wrapper)
|
|
105
|
+
|
|
106
|
+
expect(listbox.attributes('aria-label')).toBe('Onderdelen')
|
|
107
|
+
expect(listbox.attributes('tabindex')).toBe('0')
|
|
108
|
+
expect(wrapper.findAll('[tabindex]')).toHaveLength(1)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('renders ungrouped items first, then the groups in order', () => {
|
|
112
|
+
const wrapper = mountBound()
|
|
113
|
+
|
|
114
|
+
expect(optionLabels(wrapper)).toEqual([
|
|
115
|
+
'Introductie',
|
|
116
|
+
'Titel',
|
|
117
|
+
'Tekst',
|
|
118
|
+
'Tabel',
|
|
119
|
+
'Afbeelding',
|
|
120
|
+
'Video',
|
|
121
|
+
])
|
|
122
|
+
expect(wrapper.findAll('[role="group"]')).toHaveLength(2)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('treats an item whose group is unknown as ungrouped', () => {
|
|
126
|
+
const wrapper = mountBound({
|
|
127
|
+
items: [...ITEMS, { id: 'lost', label: 'Verdwaald', icon: 'ghost', group: 'nope' }],
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
expect(optionLabels(wrapper).slice(0, 2)).toEqual(['Introductie', 'Verdwaald'])
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('gives options ids that are stable across filtering', async () => {
|
|
134
|
+
const wrapper = mountBound()
|
|
135
|
+
const before = wrapper.find('[role="option"][id$="-opt-video"]').attributes('id')
|
|
136
|
+
|
|
137
|
+
await wrapper.setProps({ search: 'vid' })
|
|
138
|
+
|
|
139
|
+
expect(wrapper.find('[role="option"]').attributes('id')).toBe(before)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('encodes item ids into the option id', () => {
|
|
143
|
+
const wrapper = mountBound({
|
|
144
|
+
items: [{ id: 'met spatie', label: 'Met spatie', icon: 'square' }],
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
expect(wrapper.find('[role="option"]').attributes('id')).toMatch(/-opt-met%20spatie$/)
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe('highlight model', () => {
|
|
152
|
+
it('highlights the first enabled visible item and points aria-activedescendant at it', () => {
|
|
153
|
+
const wrapper = mountBound()
|
|
154
|
+
const id = listboxOf(wrapper).attributes('aria-activedescendant')!
|
|
155
|
+
|
|
156
|
+
expect(wrapper.emitted('update:highlighted')![0]).toEqual(['intro'])
|
|
157
|
+
expect(wrapper.find(`[id="${id}"]`).attributes('aria-selected')).toBe('true')
|
|
158
|
+
expect(wrapper.findAll('[aria-selected="true"]')).toHaveLength(1)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('skips a disabled first item for the fallback', () => {
|
|
162
|
+
const wrapper = mountBound({
|
|
163
|
+
items: ITEMS.filter((item) => item.group === 'basis').reverse(),
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
expect(activeId(wrapper)).toBe('text')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('emits highlight with the item', () => {
|
|
170
|
+
const wrapper = mountBound()
|
|
171
|
+
|
|
172
|
+
expect(wrapper.emitted('highlight')![0]).toEqual([ITEMS[0]])
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('replaces null with the fallback on a non-empty list', async () => {
|
|
176
|
+
const wrapper = mountBound({ highlighted: 'video' })
|
|
177
|
+
|
|
178
|
+
await wrapper.setProps({ highlighted: null })
|
|
179
|
+
|
|
180
|
+
expect(wrapper.emitted('update:highlighted')!.at(-1)).toEqual(['intro'])
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('falls back when the highlighted item is filtered away', async () => {
|
|
184
|
+
const wrapper = mountBound({ highlighted: 'video' })
|
|
185
|
+
|
|
186
|
+
await wrapper.setProps({ search: 'foto' })
|
|
187
|
+
await nextTick()
|
|
188
|
+
|
|
189
|
+
expect(activeId(wrapper)).toBe('image')
|
|
190
|
+
expect(wrapper.emitted('update:highlighted')!.at(-1)).toEqual(['image'])
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('is null with every group folded and no ungrouped items', () => {
|
|
194
|
+
const wrapper = mountBound({
|
|
195
|
+
items: ITEMS.filter((item) => item.group),
|
|
196
|
+
groups: GROUPS.map((group) => ({ ...group, defaultOpen: false })),
|
|
197
|
+
highlighted: 'title',
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
expect(activeId(wrapper)).toBeNull()
|
|
201
|
+
expect(wrapper.emitted('update:highlighted')!.at(-1)).toEqual([null])
|
|
202
|
+
expect(wrapper.emitted('highlight')!.at(-1)).toEqual([null])
|
|
203
|
+
expect(listboxOf(wrapper).attributes('tabindex')).toBe('0')
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
describe('keyboard', () => {
|
|
208
|
+
it('moves across group boundaries without wrapping', async () => {
|
|
209
|
+
const wrapper = mountBound()
|
|
210
|
+
|
|
211
|
+
await press(wrapper, 'ArrowUp')
|
|
212
|
+
expect(activeId(wrapper)).toBe('intro')
|
|
213
|
+
|
|
214
|
+
for (let step = 0; step < 4; step++) await press(wrapper, 'ArrowDown')
|
|
215
|
+
expect(activeId(wrapper)).toBe('image')
|
|
216
|
+
|
|
217
|
+
await press(wrapper, 'ArrowDown')
|
|
218
|
+
await press(wrapper, 'ArrowDown')
|
|
219
|
+
expect(activeId(wrapper)).toBe('video')
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('reaches disabled items with the arrows', async () => {
|
|
223
|
+
const wrapper = mountBound({ highlighted: 'text' })
|
|
224
|
+
|
|
225
|
+
await press(wrapper, 'ArrowDown')
|
|
226
|
+
|
|
227
|
+
expect(activeId(wrapper)).toBe('table')
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('jumps to the ends with Home and End', async () => {
|
|
231
|
+
const wrapper = mountBound()
|
|
232
|
+
|
|
233
|
+
await press(wrapper, 'End')
|
|
234
|
+
expect(activeId(wrapper)).toBe('video')
|
|
235
|
+
|
|
236
|
+
await press(wrapper, 'Home')
|
|
237
|
+
expect(activeId(wrapper)).toBe('intro')
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('pages by 8 and clamps at the ends', async () => {
|
|
241
|
+
const wrapper = mountBound({ items: MANY_ITEMS, groups: MANY_GROUPS })
|
|
242
|
+
|
|
243
|
+
await press(wrapper, 'PageDown')
|
|
244
|
+
expect(activeId(wrapper)).toBe('a8')
|
|
245
|
+
|
|
246
|
+
await press(wrapper, 'PageDown')
|
|
247
|
+
expect(activeId(wrapper)).toBe('b5')
|
|
248
|
+
|
|
249
|
+
await press(wrapper, 'PageUp')
|
|
250
|
+
await press(wrapper, 'PageUp')
|
|
251
|
+
expect(activeId(wrapper)).toBe('a0')
|
|
252
|
+
|
|
253
|
+
await press(wrapper, 'End')
|
|
254
|
+
await press(wrapper, 'PageDown')
|
|
255
|
+
expect(activeId(wrapper)).toBe('c10')
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('prevents the default of every key it handles, and not of others', async () => {
|
|
259
|
+
const wrapper = mountBound()
|
|
260
|
+
|
|
261
|
+
for (const key of ['ArrowDown', 'PageDown', 'Home', 'End', 'Enter', ' ', 'Escape', 'v']) {
|
|
262
|
+
expect((await press(wrapper, key)).defaultPrevented, key).toBe(true)
|
|
263
|
+
}
|
|
264
|
+
expect((await press(wrapper, 'Tab')).defaultPrevented).toBe(false)
|
|
265
|
+
expect((await press(wrapper, 'c', { ctrlKey: true })).defaultPrevented).toBe(false)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('ignores keys while composing', async () => {
|
|
269
|
+
const wrapper = mountBound()
|
|
270
|
+
|
|
271
|
+
const event = await press(wrapper, 'ArrowDown', { isComposing: true })
|
|
272
|
+
|
|
273
|
+
expect(event.defaultPrevented).toBe(false)
|
|
274
|
+
expect(activeId(wrapper)).toBe('intro')
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('selects the highlighted item with Enter and Space', async () => {
|
|
278
|
+
const wrapper = mountBound({ highlighted: 'image' })
|
|
279
|
+
|
|
280
|
+
await press(wrapper, 'Enter')
|
|
281
|
+
await press(wrapper, ' ')
|
|
282
|
+
|
|
283
|
+
expect(wrapper.emitted('select')).toEqual([[ITEMS[4]], [ITEMS[4]]])
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('never selects a disabled item', async () => {
|
|
287
|
+
const wrapper = mountBound({ highlighted: 'table' })
|
|
288
|
+
|
|
289
|
+
await press(wrapper, 'Enter')
|
|
290
|
+
await press(wrapper, ' ')
|
|
291
|
+
await wrapper.find('[id$="-opt-table"]').trigger('click')
|
|
292
|
+
|
|
293
|
+
expect(wrapper.emitted('select')).toBeUndefined()
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('selects on click', async () => {
|
|
297
|
+
const wrapper = mountBound()
|
|
298
|
+
|
|
299
|
+
await wrapper.find('[id$="-opt-video"]').trigger('click')
|
|
300
|
+
|
|
301
|
+
expect(wrapper.emitted('select')).toEqual([[ITEMS[5]]])
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('emits close on Escape', async () => {
|
|
305
|
+
const wrapper = mountBound()
|
|
306
|
+
|
|
307
|
+
await press(wrapper, 'Escape')
|
|
308
|
+
|
|
309
|
+
expect(wrapper.emitted('close')).toEqual([[]])
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
describe('type-ahead', () => {
|
|
313
|
+
beforeEach(() => {
|
|
314
|
+
vi.useFakeTimers()
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
it('jumps to the first visible label starting with the buffer', async () => {
|
|
318
|
+
const wrapper = mountBound()
|
|
319
|
+
|
|
320
|
+
await press(wrapper, 't')
|
|
321
|
+
expect(activeId(wrapper)).toBe('title')
|
|
322
|
+
|
|
323
|
+
await press(wrapper, 'e')
|
|
324
|
+
expect(activeId(wrapper)).toBe('text')
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('includes disabled items and matches without diacritics', async () => {
|
|
328
|
+
const wrapper = mountBound()
|
|
329
|
+
|
|
330
|
+
await press(wrapper, 't')
|
|
331
|
+
await press(wrapper, 'á')
|
|
332
|
+
|
|
333
|
+
expect(activeId(wrapper)).toBe('table')
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('resets the buffer after 800ms', async () => {
|
|
337
|
+
const wrapper = mountBound()
|
|
338
|
+
|
|
339
|
+
await press(wrapper, 'v')
|
|
340
|
+
expect(activeId(wrapper)).toBe('video')
|
|
341
|
+
|
|
342
|
+
vi.advanceTimersByTime(799)
|
|
343
|
+
await press(wrapper, 'i')
|
|
344
|
+
// "vi" still matches Video
|
|
345
|
+
expect(activeId(wrapper)).toBe('video')
|
|
346
|
+
|
|
347
|
+
vi.advanceTimersByTime(800)
|
|
348
|
+
await press(wrapper, 'i')
|
|
349
|
+
expect(activeId(wrapper)).toBe('intro')
|
|
350
|
+
})
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
describe('folding', () => {
|
|
354
|
+
it('folds the group of the highlighted item with ← and moves the highlight on', async () => {
|
|
355
|
+
const wrapper = mountBound({ highlighted: 'text' })
|
|
356
|
+
|
|
357
|
+
await press(wrapper, 'ArrowLeft')
|
|
358
|
+
|
|
359
|
+
expect(optionLabels(wrapper)).not.toContain('Tekst')
|
|
360
|
+
expect(activeId(wrapper)).toBe('image')
|
|
361
|
+
expect(wrapper.findAll('[role="group"]')[0].attributes('aria-expanded')).toBe('false')
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it('moves the highlight to the last item before when nothing follows', async () => {
|
|
365
|
+
const wrapper = mountBound({ highlighted: 'video' })
|
|
366
|
+
|
|
367
|
+
await press(wrapper, 'ArrowLeft')
|
|
368
|
+
|
|
369
|
+
expect(activeId(wrapper)).toBe('table')
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it('does nothing with ← on an ungrouped item', async () => {
|
|
373
|
+
const wrapper = mountBound()
|
|
374
|
+
|
|
375
|
+
const event = await press(wrapper, 'ArrowLeft')
|
|
376
|
+
|
|
377
|
+
expect(event.defaultPrevented).toBe(false)
|
|
378
|
+
expect(wrapper.findAll('[aria-expanded="false"]')).toHaveLength(0)
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
it('opens the next folded group with → and highlights its first item', async () => {
|
|
382
|
+
const wrapper = mountBound({
|
|
383
|
+
groups: [GROUPS[0], { ...GROUPS[1], defaultOpen: false }],
|
|
384
|
+
highlighted: 'title',
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
await press(wrapper, 'ArrowRight')
|
|
388
|
+
|
|
389
|
+
expect(optionLabels(wrapper)).toContain('Video')
|
|
390
|
+
expect(activeId(wrapper)).toBe('image')
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
it('opens a folded group before the highlight when none follows', async () => {
|
|
394
|
+
const wrapper = mountBound({
|
|
395
|
+
groups: [{ ...GROUPS[0], defaultOpen: false }, GROUPS[1]],
|
|
396
|
+
highlighted: 'video',
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
await press(wrapper, 'ArrowRight')
|
|
400
|
+
|
|
401
|
+
expect(activeId(wrapper)).toBe('title')
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it('opens the first group with ↓ or → when every group is folded', async () => {
|
|
405
|
+
for (const key of ['ArrowDown', 'ArrowRight']) {
|
|
406
|
+
const wrapper = mountBound({
|
|
407
|
+
items: ITEMS.filter((item) => item.group),
|
|
408
|
+
groups: GROUPS.map((group) => ({ ...group, defaultOpen: false })),
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
await press(wrapper, key)
|
|
412
|
+
|
|
413
|
+
expect(activeId(wrapper), key).toBe('title')
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it('folds from a click on the heading without jumping the highlight to the top', async () => {
|
|
418
|
+
const wrapper = mountBound({ highlighted: 'text' })
|
|
419
|
+
|
|
420
|
+
await wrapper.findAll('[role="presentation"]')[0].trigger('click')
|
|
421
|
+
|
|
422
|
+
expect(activeId(wrapper)).toBe('image')
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it('keeps the folded state of other groups when groups are added', async () => {
|
|
426
|
+
const wrapper = mountBound()
|
|
427
|
+
|
|
428
|
+
await wrapper.findAll('[role="presentation"]')[1].trigger('click')
|
|
429
|
+
await wrapper.setProps({
|
|
430
|
+
groups: [...GROUPS, { id: 'extra', label: 'Extra', defaultOpen: false }],
|
|
431
|
+
items: [...ITEMS, { id: 'more', label: 'Meer', icon: 'plus', group: 'extra' }],
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
const expanded = wrapper.findAll('[role="group"]').map((g) => g.attributes('aria-expanded'))
|
|
435
|
+
expect(expanded).toEqual(['true', 'false', 'false'])
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
it('falls back to the last ungrouped item when no other group is open', async () => {
|
|
439
|
+
const wrapper = mountBound({
|
|
440
|
+
items: [ITEMS[0], ITEMS[1]],
|
|
441
|
+
groups: [GROUPS[0]],
|
|
442
|
+
highlighted: 'title',
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
await press(wrapper, 'ArrowLeft')
|
|
446
|
+
|
|
447
|
+
expect(activeId(wrapper)).toBe('intro')
|
|
448
|
+
expect(wrapper.emitted('update:highlighted')!.at(-1)).toEqual(['intro'])
|
|
449
|
+
expect(optionLabels(wrapper)).toEqual(['Introductie'])
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('does not fold while not collapsible', async () => {
|
|
453
|
+
const wrapper = mountBound({ collapsible: false, highlighted: 'text' })
|
|
454
|
+
|
|
455
|
+
await press(wrapper, 'ArrowLeft')
|
|
456
|
+
|
|
457
|
+
expect(optionLabels(wrapper)).toContain('Tekst')
|
|
458
|
+
expect(wrapper.find('[aria-expanded]').exists()).toBe(false)
|
|
459
|
+
})
|
|
460
|
+
})
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
describe('pointer', () => {
|
|
464
|
+
it('highlights on a real pointer movement', async () => {
|
|
465
|
+
const wrapper = mountBound()
|
|
466
|
+
const video = wrapper.find('[id$="-opt-video"]').element
|
|
467
|
+
|
|
468
|
+
await pointerMove(video, 10, 10)
|
|
469
|
+
await pointerMove(video, 10, 14)
|
|
470
|
+
|
|
471
|
+
expect(activeId(wrapper)).toBe('video')
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
it('ignores a pointermove that did not move, like the one after a scroll', async () => {
|
|
475
|
+
const wrapper = mountBound()
|
|
476
|
+
const image = wrapper.find('[id$="-opt-image"]').element
|
|
477
|
+
const video = wrapper.find('[id$="-opt-video"]').element
|
|
478
|
+
|
|
479
|
+
await pointerMove(image, 10, 10)
|
|
480
|
+
await press(wrapper, 'End')
|
|
481
|
+
await pointerMove(video, 10, 10)
|
|
482
|
+
|
|
483
|
+
expect(activeId(wrapper)).toBe('video')
|
|
484
|
+
|
|
485
|
+
await press(wrapper, 'Home')
|
|
486
|
+
await pointerMove(video, 10, 10)
|
|
487
|
+
|
|
488
|
+
expect(activeId(wrapper)).toBe('intro')
|
|
489
|
+
})
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
describe('filtering', () => {
|
|
493
|
+
it('matches labels and keywords, case- and diacritic-insensitive', async () => {
|
|
494
|
+
const wrapper = mountBound()
|
|
495
|
+
|
|
496
|
+
await wrapper.setProps({ search: ' FOTO ' })
|
|
497
|
+
expect(optionLabels(wrapper)).toEqual(['Afbeelding'])
|
|
498
|
+
|
|
499
|
+
await wrapper.setProps({ search: 'afbeeldíng' })
|
|
500
|
+
expect(optionLabels(wrapper)).toEqual(['Afbeelding'])
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
it('hides groups without matches and forces the others open', async () => {
|
|
504
|
+
const wrapper = mountBound({
|
|
505
|
+
groups: GROUPS.map((group) => ({ ...group, defaultOpen: false })),
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
await wrapper.setProps({ search: 'e' })
|
|
509
|
+
|
|
510
|
+
const groups = wrapper.findAll('[role="group"]')
|
|
511
|
+
expect(groups).toHaveLength(2)
|
|
512
|
+
expect(groups.every((group) => group.attributes('aria-expanded') === undefined)).toBe(true)
|
|
513
|
+
expect(optionLabels(wrapper)).toContain('Video')
|
|
514
|
+
|
|
515
|
+
await wrapper.setProps({ search: 'vid' })
|
|
516
|
+
expect(wrapper.findAll('[role="group"]')).toHaveLength(1)
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
it('opens and closes groups for a search without animating them', async () => {
|
|
520
|
+
vi.stubGlobal('matchMedia', (query: string) => ({
|
|
521
|
+
matches: query.length < 0,
|
|
522
|
+
addEventListener: () => {},
|
|
523
|
+
removeEventListener: () => {},
|
|
524
|
+
}))
|
|
525
|
+
const wrapper = mountBound(
|
|
526
|
+
{ groups: [GROUPS[0], { ...GROUPS[1], defaultOpen: false }] },
|
|
527
|
+
{ global: { stubs: { transition: false } } },
|
|
528
|
+
)
|
|
529
|
+
const wrappers = () => wrapper.findAll('[role="group"] > .grid')
|
|
530
|
+
|
|
531
|
+
await wrapper.setProps({ search: 'vid' })
|
|
532
|
+
// the folded group is open at once, without enter classes
|
|
533
|
+
expect(optionLabels(wrapper)).toEqual(['Video'])
|
|
534
|
+
expect(
|
|
535
|
+
wrappers()
|
|
536
|
+
.map((el) => el.classes().join(' '))
|
|
537
|
+
.join(' '),
|
|
538
|
+
).not.toMatch(/duration-|0fr/)
|
|
539
|
+
|
|
540
|
+
await wrapper.setProps({ search: '' })
|
|
541
|
+
// and folded again at once: no leaving, inert wrapper lingers
|
|
542
|
+
expect(optionLabels(wrapper)).not.toContain('Video')
|
|
543
|
+
expect(wrapper.find('[role="group"] > [inert]').exists()).toBe(false)
|
|
544
|
+
vi.unstubAllGlobals()
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
it('still animates a fold from the keyboard', async () => {
|
|
548
|
+
vi.stubGlobal('matchMedia', () => ({
|
|
549
|
+
matches: false,
|
|
550
|
+
addEventListener: () => {},
|
|
551
|
+
removeEventListener: () => {},
|
|
552
|
+
}))
|
|
553
|
+
const wrapper = mountBound(
|
|
554
|
+
{ highlighted: 'image' },
|
|
555
|
+
{ global: { stubs: { transition: false } } },
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
await press(wrapper, 'ArrowLeft')
|
|
559
|
+
|
|
560
|
+
expect(wrapper.find('[role="group"] > [inert]').classes()).toContain('duration-120')
|
|
561
|
+
vi.unstubAllGlobals()
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
it('restores the folded state once the search is cleared', async () => {
|
|
565
|
+
const wrapper = mountBound()
|
|
566
|
+
|
|
567
|
+
await wrapper.findAll('[role="presentation"]')[1].trigger('click')
|
|
568
|
+
await wrapper.setProps({ search: 'vid' })
|
|
569
|
+
await wrapper.setProps({ search: '' })
|
|
570
|
+
|
|
571
|
+
expect(wrapper.findAll('[role="group"]')[1].attributes('aria-expanded')).toBe('false')
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
it('treats a blank search as no search', async () => {
|
|
575
|
+
const wrapper = mountBound()
|
|
576
|
+
|
|
577
|
+
await wrapper.setProps({ search: ' ' })
|
|
578
|
+
|
|
579
|
+
expect(optionLabels(wrapper)).toHaveLength(6)
|
|
580
|
+
expect(wrapper.text()).not.toContain('Niets')
|
|
581
|
+
})
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
describe('empty state', () => {
|
|
585
|
+
const copy = {
|
|
586
|
+
emptyTitle: 'Niets gevonden',
|
|
587
|
+
emptyDescription: (search: string) => `Geen resultaat voor '${search}'.`,
|
|
588
|
+
clearLabel: 'Wis zoekopdracht',
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
it('shows the copy with the trimmed term and drops the listbox from the tab order', () => {
|
|
592
|
+
const wrapper = mountBound({ ...copy, search: ' xyz ' })
|
|
593
|
+
|
|
594
|
+
expect(wrapper.text()).toContain('Niets gevonden')
|
|
595
|
+
expect(wrapper.text()).toContain("Geen resultaat voor 'xyz'.")
|
|
596
|
+
expect(listboxOf(wrapper).attributes('tabindex')).toBe('-1')
|
|
597
|
+
expect(listboxOf(wrapper).attributes('aria-activedescendant')).toBeUndefined()
|
|
598
|
+
expect(wrapper.find('i[aria-hidden="true"]').exists()).toBe(true)
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
it('renders a string description as is', () => {
|
|
602
|
+
const wrapper = mountBound({ ...copy, emptyDescription: 'Probeer iets anders', search: 'x1' })
|
|
603
|
+
|
|
604
|
+
expect(wrapper.text()).toContain('Probeer iets anders')
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
it('emits clear from the button', async () => {
|
|
608
|
+
const wrapper = mountBound({ ...copy, search: 'xyz' })
|
|
609
|
+
|
|
610
|
+
await wrapper.find('button').trigger('click')
|
|
611
|
+
|
|
612
|
+
expect(wrapper.emitted('clear')).toEqual([[]])
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
it('renders no button without a clear label, and no missing copy', () => {
|
|
616
|
+
const wrapper = mountBound({ search: 'xyz' })
|
|
617
|
+
|
|
618
|
+
expect(wrapper.find('button').exists()).toBe(false)
|
|
619
|
+
expect(wrapper.findAll('p')).toHaveLength(0)
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
it('lets the empty slot replace the content, with the term and a clear function', async () => {
|
|
623
|
+
const wrapper = mountBound(
|
|
624
|
+
{ ...copy, search: ' xyz ' },
|
|
625
|
+
{
|
|
626
|
+
slots: {
|
|
627
|
+
empty: `<template #empty="{ search, clear }">
|
|
628
|
+
<button type="button" @click="clear">Leeg {{ search }}</button>
|
|
629
|
+
</template>`,
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
expect(wrapper.text()).not.toContain('Niets gevonden')
|
|
635
|
+
await wrapper.find('button').trigger('click')
|
|
636
|
+
|
|
637
|
+
expect(wrapper.find('button').text()).toBe('Leeg xyz')
|
|
638
|
+
expect(wrapper.emitted('clear')).toEqual([[]])
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
it('shows no empty state without a search', () => {
|
|
642
|
+
const wrapper = mountBound({ ...copy, items: [] })
|
|
643
|
+
|
|
644
|
+
expect(wrapper.text()).not.toContain('Niets gevonden')
|
|
645
|
+
})
|
|
646
|
+
})
|
|
647
|
+
|
|
648
|
+
describe('announcement', () => {
|
|
649
|
+
beforeEach(() => {
|
|
650
|
+
vi.useFakeTimers()
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
it('has exactly one live region, empty on mount', () => {
|
|
654
|
+
const wrapper = mountBound({ resultsAnnouncement: (n) => `${n} resultaten` })
|
|
655
|
+
|
|
656
|
+
const regions = wrapper.findAll('[aria-live]')
|
|
657
|
+
expect(regions).toHaveLength(1)
|
|
658
|
+
expect(regions[0].attributes('role')).toBe('status')
|
|
659
|
+
|
|
660
|
+
vi.advanceTimersByTime(1000)
|
|
661
|
+
expect(regions[0].text()).toBe('')
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
it('announces the count 500ms after the last search change', async () => {
|
|
665
|
+
const wrapper = mountBound({ resultsAnnouncement: (n) => `${n} resultaten` })
|
|
666
|
+
const region = () => wrapper.find('[aria-live]').text()
|
|
667
|
+
|
|
668
|
+
await wrapper.setProps({ search: 'v' })
|
|
669
|
+
vi.advanceTimersByTime(300)
|
|
670
|
+
await wrapper.setProps({ search: 'vi' })
|
|
671
|
+
vi.advanceTimersByTime(499)
|
|
672
|
+
await nextTick()
|
|
673
|
+
expect(region()).toBe('')
|
|
674
|
+
|
|
675
|
+
vi.advanceTimersByTime(1)
|
|
676
|
+
await nextTick()
|
|
677
|
+
expect(region()).toBe('1 resultaten')
|
|
678
|
+
|
|
679
|
+
await wrapper.setProps({ search: 'xyz' })
|
|
680
|
+
vi.advanceTimersByTime(500)
|
|
681
|
+
await nextTick()
|
|
682
|
+
expect(region()).toBe('0 resultaten')
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
describe('while busy', () => {
|
|
686
|
+
const announce = (n: number) => `${n} resultaten`
|
|
687
|
+
|
|
688
|
+
it('holds the count back, also past the delay', async () => {
|
|
689
|
+
const wrapper = mountBound({ busy: true, resultsAnnouncement: announce })
|
|
690
|
+
|
|
691
|
+
await wrapper.setProps({ search: 'xyz' })
|
|
692
|
+
vi.advanceTimersByTime(2000)
|
|
693
|
+
await nextTick()
|
|
694
|
+
|
|
695
|
+
expect(wrapper.find('[aria-live]').text()).toBe('')
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
it('announces the final count 500ms after busy ends, without a new search', async () => {
|
|
699
|
+
const wrapper = mountBound({ busy: true, items: [], resultsAnnouncement: announce })
|
|
700
|
+
const region = () => wrapper.find('[aria-live]').text()
|
|
701
|
+
|
|
702
|
+
await wrapper.setProps({ search: 'vid' })
|
|
703
|
+
vi.advanceTimersByTime(1000)
|
|
704
|
+
// the items arrive with the end of busy, as a catalogue fetch would deliver them
|
|
705
|
+
await wrapper.setProps({ busy: false, items: ITEMS })
|
|
706
|
+
vi.advanceTimersByTime(499)
|
|
707
|
+
await nextTick()
|
|
708
|
+
expect(region()).toBe('')
|
|
709
|
+
|
|
710
|
+
vi.advanceTimersByTime(1)
|
|
711
|
+
await nextTick()
|
|
712
|
+
expect(region()).toBe('1 resultaten')
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
it('holds back an announcement that was already on its way when busy starts', async () => {
|
|
716
|
+
const wrapper = mountBound({ resultsAnnouncement: announce })
|
|
717
|
+
const region = () => wrapper.find('[aria-live]').text()
|
|
718
|
+
|
|
719
|
+
await wrapper.setProps({ search: 'xyz' })
|
|
720
|
+
vi.advanceTimersByTime(300)
|
|
721
|
+
await wrapper.setProps({ busy: true })
|
|
722
|
+
vi.advanceTimersByTime(1000)
|
|
723
|
+
await nextTick()
|
|
724
|
+
expect(region()).toBe('')
|
|
725
|
+
|
|
726
|
+
await wrapper.setProps({ busy: false })
|
|
727
|
+
vi.advanceTimersByTime(500)
|
|
728
|
+
await nextTick()
|
|
729
|
+
expect(region()).toBe('0 resultaten')
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
// Nothing was searched, so there is nothing owed: the end of busy alone stays silent
|
|
733
|
+
it('stays silent when busy ends without a search', async () => {
|
|
734
|
+
const wrapper = mountBound({ busy: true, resultsAnnouncement: announce })
|
|
735
|
+
|
|
736
|
+
await wrapper.setProps({ busy: false })
|
|
737
|
+
vi.advanceTimersByTime(1000)
|
|
738
|
+
await nextTick()
|
|
739
|
+
|
|
740
|
+
expect(wrapper.find('[aria-live]').text()).toBe('')
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
it('announces once per search, not again on a later busy cycle', async () => {
|
|
744
|
+
const wrapper = mountBound({ resultsAnnouncement: announce })
|
|
745
|
+
const region = () => wrapper.find('[aria-live]').text()
|
|
746
|
+
|
|
747
|
+
await wrapper.setProps({ search: 'vi' })
|
|
748
|
+
vi.advanceTimersByTime(500)
|
|
749
|
+
await nextTick()
|
|
750
|
+
expect(region()).toBe('1 resultaten')
|
|
751
|
+
|
|
752
|
+
await wrapper.setProps({ busy: true, items: [] })
|
|
753
|
+
await wrapper.setProps({ busy: false, items: ITEMS })
|
|
754
|
+
vi.advanceTimersByTime(1000)
|
|
755
|
+
await nextTick()
|
|
756
|
+
expect(region()).toBe('1 resultaten')
|
|
757
|
+
})
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
it('does not announce on folding', async () => {
|
|
761
|
+
const wrapper = mountBound({ resultsAnnouncement: (n) => `${n} resultaten` })
|
|
762
|
+
|
|
763
|
+
await wrapper.findAll('[role="presentation"]')[1].trigger('click')
|
|
764
|
+
vi.advanceTimersByTime(1000)
|
|
765
|
+
await nextTick()
|
|
766
|
+
|
|
767
|
+
expect(wrapper.find('[aria-live]').text()).toBe('')
|
|
768
|
+
})
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
describe('expose', () => {
|
|
772
|
+
it('drives the list from outside', async () => {
|
|
773
|
+
const wrapper = mountBound()
|
|
774
|
+
const vm = wrapper.vm
|
|
775
|
+
|
|
776
|
+
vm.move(2)
|
|
777
|
+
await nextTick()
|
|
778
|
+
expect(activeId(wrapper)).toBe('text')
|
|
779
|
+
|
|
780
|
+
vm.highlightLast()
|
|
781
|
+
await nextTick()
|
|
782
|
+
expect(activeId(wrapper)).toBe('video')
|
|
783
|
+
|
|
784
|
+
vm.selectHighlighted()
|
|
785
|
+
expect(wrapper.emitted('select')).toEqual([[ITEMS[5]]])
|
|
786
|
+
|
|
787
|
+
vm.highlightFirst()
|
|
788
|
+
await nextTick()
|
|
789
|
+
expect(activeId(wrapper)).toBe('intro')
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
it('exposes the listbox id and the active option id', async () => {
|
|
793
|
+
const wrapper = mountBound()
|
|
794
|
+
|
|
795
|
+
expect(wrapper.vm.listboxId).toBe(listboxOf(wrapper).attributes('id'))
|
|
796
|
+
expect(wrapper.vm.activeDescendantId).toBe(
|
|
797
|
+
listboxOf(wrapper).attributes('aria-activedescendant'),
|
|
798
|
+
)
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
it('focuses the listbox', () => {
|
|
802
|
+
const wrapper = mountBound({}, { attachTo: document.body })
|
|
803
|
+
|
|
804
|
+
wrapper.vm.focus()
|
|
805
|
+
|
|
806
|
+
expect(document.activeElement).toBe(listboxOf(wrapper).element)
|
|
807
|
+
wrapper.unmount()
|
|
808
|
+
})
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
it('renders a row without an icon when the item has none', () => {
|
|
812
|
+
const wrapper = mountBound({ items: [{ id: 'plain', label: 'Zonder icoon' }], groups: [] })
|
|
813
|
+
const option = wrapper.find('[role="option"]')
|
|
814
|
+
|
|
815
|
+
expect(option.find('i').exists()).toBe(false)
|
|
816
|
+
expect(option.text()).toBe('Zonder icoon')
|
|
817
|
+
})
|
|
818
|
+
|
|
819
|
+
describe('busy', () => {
|
|
820
|
+
it('is not busy by default', () => {
|
|
821
|
+
expect(listboxOf(mountBound()).attributes('aria-busy')).toBeUndefined()
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
it('marks the listbox busy', () => {
|
|
825
|
+
expect(listboxOf(mountBound({ busy: true })).attributes('aria-busy')).toBe('true')
|
|
826
|
+
})
|
|
827
|
+
|
|
828
|
+
// More items may still arrive, so "nothing found" would be a claim the list cannot make yet
|
|
829
|
+
it('holds back the empty state while busy', async () => {
|
|
830
|
+
const wrapper = mountBound({ busy: true, search: 'zwembad', emptyTitle: 'Niets' })
|
|
831
|
+
await nextTick()
|
|
832
|
+
|
|
833
|
+
expect(wrapper.text()).not.toContain('Niets')
|
|
834
|
+
|
|
835
|
+
await wrapper.setProps({ busy: false })
|
|
836
|
+
expect(wrapper.text()).toContain('Niets')
|
|
837
|
+
})
|
|
838
|
+
|
|
839
|
+
it('renders the append slot inside the listbox, after the last group', () => {
|
|
840
|
+
const wrapper = mountBound(
|
|
841
|
+
{ busy: true, search: 'zwembad' },
|
|
842
|
+
{ slots: { append: '<div aria-hidden="true" data-test="placeholder">…</div>' } },
|
|
843
|
+
)
|
|
844
|
+
const listbox = listboxOf(wrapper)
|
|
845
|
+
|
|
846
|
+
expect(listbox.element.lastElementChild?.getAttribute('data-test')).toBe('placeholder')
|
|
847
|
+
// still laid out as a filled list, although no option matches
|
|
848
|
+
expect(listbox.classes()).toContain('p-2')
|
|
849
|
+
// no option to walk through, so no tab stop
|
|
850
|
+
expect(listbox.attributes('tabindex')).toBe('-1')
|
|
851
|
+
})
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
it('renders the item slot inside the option', () => {
|
|
855
|
+
const wrapper = mountBound(
|
|
856
|
+
{},
|
|
857
|
+
{
|
|
858
|
+
slots: {
|
|
859
|
+
item: `<template #item="{ item, highlighted }">
|
|
860
|
+
<b>{{ item.label }}{{ highlighted ? '*' : '' }}</b>
|
|
861
|
+
</template>`,
|
|
862
|
+
},
|
|
863
|
+
},
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
const first = wrapper.find('[role="option"]')
|
|
867
|
+
expect(first.find('b').text()).toBe('Introductie*')
|
|
868
|
+
expect(first.attributes('aria-selected')).toBe('true')
|
|
869
|
+
})
|
|
870
|
+
})
|