@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,145 @@
|
|
|
1
|
+
import { afterEach, describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { nextTick } from 'vue'
|
|
3
|
+
import { mount } from '@vue/test-utils'
|
|
4
|
+
import PickerGroup from './PickerGroup.vue'
|
|
5
|
+
|
|
6
|
+
const slots = {
|
|
7
|
+
default: '<div role="option" id="a">Afbeelding</div><div role="option" id="b">Video</div>',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const headingOf = (wrapper: ReturnType<typeof mount>) => wrapper.find('[role="presentation"]')
|
|
11
|
+
|
|
12
|
+
const stubReducedMotion = (reduce: boolean) =>
|
|
13
|
+
vi.stubGlobal('matchMedia', (query: string) => ({
|
|
14
|
+
matches: reduce && query.includes('reduce'),
|
|
15
|
+
addEventListener: () => {},
|
|
16
|
+
removeEventListener: () => {},
|
|
17
|
+
}))
|
|
18
|
+
|
|
19
|
+
describe('PickerGroup', () => {
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.unstubAllGlobals()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('is a group labelled by its heading, expanded by default', () => {
|
|
25
|
+
const wrapper = mount(PickerGroup, { props: { label: 'Media' }, slots })
|
|
26
|
+
const labelledBy = wrapper.attributes('aria-labelledby')!
|
|
27
|
+
|
|
28
|
+
expect(wrapper.attributes('role')).toBe('group')
|
|
29
|
+
expect(wrapper.find(`[id="${labelledBy}"]`).text()).toBe('Media')
|
|
30
|
+
expect(wrapper.attributes('aria-expanded')).toBe('true')
|
|
31
|
+
expect(wrapper.findAll('[role="option"]')).toHaveLength(2)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('renders the heading as a presentation element without a tab stop or a control', () => {
|
|
35
|
+
const wrapper = mount(PickerGroup, { props: { label: 'Media' }, slots })
|
|
36
|
+
const heading = headingOf(wrapper)
|
|
37
|
+
|
|
38
|
+
expect(heading.attributes('tabindex')).toBeUndefined()
|
|
39
|
+
expect(heading.find('button, a, [tabindex]').exists()).toBe(false)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('toggles the model from the heading', async () => {
|
|
43
|
+
const wrapper = mount(PickerGroup, { props: { label: 'Media' }, slots })
|
|
44
|
+
|
|
45
|
+
await headingOf(wrapper).trigger('click')
|
|
46
|
+
|
|
47
|
+
expect(wrapper.emitted('update:open')).toEqual([[false]])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('keeps the options out of the DOM while closed', () => {
|
|
51
|
+
const wrapper = mount(PickerGroup, { props: { label: 'Media', open: false }, slots })
|
|
52
|
+
|
|
53
|
+
expect(wrapper.findAll('[role="option"]')).toHaveLength(0)
|
|
54
|
+
expect(wrapper.attributes('aria-expanded')).toBe('false')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('turns the heading into a plain label when not collapsible', async () => {
|
|
58
|
+
const wrapper = mount(PickerGroup, {
|
|
59
|
+
props: { label: 'Media', collapsible: false, open: true },
|
|
60
|
+
slots,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
await headingOf(wrapper).trigger('click')
|
|
64
|
+
|
|
65
|
+
expect(wrapper.emitted('update:open')).toBeUndefined()
|
|
66
|
+
expect(wrapper.attributes('aria-expanded')).toBeUndefined()
|
|
67
|
+
expect(wrapper.attributes('aria-labelledby')).toBeDefined()
|
|
68
|
+
expect(wrapper.find('i').exists()).toBe(false)
|
|
69
|
+
expect(
|
|
70
|
+
headingOf(wrapper)
|
|
71
|
+
.classes()
|
|
72
|
+
.some((name) => name.startsWith('hover:')),
|
|
73
|
+
).toBe(false)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('shows a chevron only when collapsible, turned while closed', async () => {
|
|
77
|
+
const wrapper = mount(PickerGroup, { props: { label: 'Media' }, slots })
|
|
78
|
+
const chevron = () => wrapper.find('i')
|
|
79
|
+
|
|
80
|
+
expect(chevron().attributes('aria-hidden')).toBe('true')
|
|
81
|
+
expect(chevron().classes()).not.toContain('-rotate-90')
|
|
82
|
+
|
|
83
|
+
await wrapper.setProps({ open: false })
|
|
84
|
+
|
|
85
|
+
expect(chevron().classes()).toContain('-rotate-90')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('prevents the mousedown default on the heading', () => {
|
|
89
|
+
const wrapper = mount(PickerGroup, { props: { label: 'Media' }, slots })
|
|
90
|
+
const event = new MouseEvent('mousedown', { bubbles: true, cancelable: true })
|
|
91
|
+
|
|
92
|
+
headingOf(wrapper).element.dispatchEvent(event)
|
|
93
|
+
|
|
94
|
+
expect(event.defaultPrevented).toBe(true)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('unfolds over 160ms with a reduced-motion fallback', async () => {
|
|
98
|
+
stubReducedMotion(false)
|
|
99
|
+
const wrapper = mount(PickerGroup, {
|
|
100
|
+
props: { label: 'Media', open: false },
|
|
101
|
+
slots,
|
|
102
|
+
global: { stubs: { transition: false } },
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
await wrapper.setProps({ open: true })
|
|
106
|
+
|
|
107
|
+
const classes = wrapper.find('.grid').classes()
|
|
108
|
+
expect(classes).toEqual(
|
|
109
|
+
expect.arrayContaining([
|
|
110
|
+
'transition-[grid-template-rows,opacity]',
|
|
111
|
+
'duration-160',
|
|
112
|
+
'motion-reduce:transition-none',
|
|
113
|
+
'grid-rows-[0fr]',
|
|
114
|
+
]),
|
|
115
|
+
)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('keeps leaving options inert while they fold away', async () => {
|
|
119
|
+
stubReducedMotion(false)
|
|
120
|
+
const wrapper = mount(PickerGroup, {
|
|
121
|
+
props: { label: 'Media', open: true },
|
|
122
|
+
slots,
|
|
123
|
+
global: { stubs: { transition: false } },
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
await wrapper.setProps({ open: false })
|
|
127
|
+
|
|
128
|
+
// still in the DOM for the 120ms leave, but no longer reachable
|
|
129
|
+
expect(wrapper.find('.grid').attributes('inert')).toBeDefined()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('removes the options synchronously under reduced motion', async () => {
|
|
133
|
+
stubReducedMotion(true)
|
|
134
|
+
const wrapper = mount(PickerGroup, {
|
|
135
|
+
props: { label: 'Media', open: true },
|
|
136
|
+
slots,
|
|
137
|
+
global: { stubs: { transition: false } },
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
await wrapper.setProps({ open: false })
|
|
141
|
+
await nextTick()
|
|
142
|
+
|
|
143
|
+
expect(wrapper.findAll('[role="option"]')).toHaveLength(0)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { expect, fn, userEvent, waitFor, within } from 'storybook/test'
|
|
3
|
+
import { ref } from 'vue'
|
|
4
|
+
import PickerList, { type NexxtPickerGroupData, type NexxtPickerItemData } from './PickerList.vue'
|
|
5
|
+
|
|
6
|
+
const GROUPS: NexxtPickerGroupData[] = [
|
|
7
|
+
{ id: 'basis', label: 'Basis', defaultOpen: true },
|
|
8
|
+
{ id: 'media', label: 'Media', defaultOpen: true },
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
const ITEMS: NexxtPickerItemData[] = [
|
|
12
|
+
{ id: 'intro', label: 'Introductie', icon: 'sparkles', keywords: ['welkom'] },
|
|
13
|
+
{ id: 'title', label: 'Titel', icon: 'heading', group: 'basis', keywords: ['kop', 'heading'] },
|
|
14
|
+
{ id: 'text', label: 'Tekst', icon: 'font', group: 'basis', keywords: ['alinea', 'paragraaf'] },
|
|
15
|
+
{
|
|
16
|
+
id: 'table',
|
|
17
|
+
label: 'Tabel',
|
|
18
|
+
icon: 'table',
|
|
19
|
+
group: 'basis',
|
|
20
|
+
disabled: true,
|
|
21
|
+
description: 'Nog niet beschikbaar.',
|
|
22
|
+
},
|
|
23
|
+
{ id: 'quote', label: 'Citaat', icon: 'quote-left', group: 'basis', keywords: ['quote'] },
|
|
24
|
+
{ id: 'image', label: 'Afbeelding', icon: 'image', group: 'media', keywords: ['foto'] },
|
|
25
|
+
{ id: 'video', label: 'Video', icon: 'video', group: 'media', keywords: ['film'] },
|
|
26
|
+
{ id: 'gallery', label: 'Galerij', icon: 'images', group: 'media', keywords: ['album'] },
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
const MANY_GROUPS: NexxtPickerGroupData[] = [
|
|
30
|
+
{ id: 'a', label: 'Tekst' },
|
|
31
|
+
{ id: 'b', label: 'Media' },
|
|
32
|
+
{ id: 'c', label: 'Indeling' },
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
const MANY_ITEMS: NexxtPickerItemData[] = MANY_GROUPS.flatMap((group) =>
|
|
36
|
+
Array.from({ length: 11 }, (_, index) => ({
|
|
37
|
+
id: `${group.id}-${index + 1}`,
|
|
38
|
+
label: `${group.label} ${index + 1}`,
|
|
39
|
+
icon: 'square' as const,
|
|
40
|
+
group: group.id,
|
|
41
|
+
})),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const COPY = {
|
|
45
|
+
ariaLabel: 'Onderdelen',
|
|
46
|
+
emptyTitle: 'Geen onderdelen gevonden',
|
|
47
|
+
emptyDescription: (search: string) => `Geen resultaat voor '${search}'.`,
|
|
48
|
+
clearLabel: 'Wis zoekopdracht',
|
|
49
|
+
resultsAnnouncement: (count: number) =>
|
|
50
|
+
count === 1 ? '1 onderdeel gevonden' : `${count} onderdelen gevonden`,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default {
|
|
54
|
+
title: 'Components/Molecules/PickerList',
|
|
55
|
+
component: PickerList,
|
|
56
|
+
args: {
|
|
57
|
+
items: ITEMS,
|
|
58
|
+
groups: GROUPS,
|
|
59
|
+
...COPY,
|
|
60
|
+
onSelect: fn(),
|
|
61
|
+
onClear: fn(),
|
|
62
|
+
onClose: fn(),
|
|
63
|
+
},
|
|
64
|
+
} satisfies Meta<typeof PickerList>
|
|
65
|
+
|
|
66
|
+
type Story = StoryObj<typeof PickerList>
|
|
67
|
+
|
|
68
|
+
const FRAME_CLASSES =
|
|
69
|
+
'flex h-80 w-66 flex-col overflow-hidden rounded-xl border border-gray-200 bg-white'
|
|
70
|
+
|
|
71
|
+
/** The list in a 264×320 frame, with a field of the consumer's own bound to `search`. */
|
|
72
|
+
const renderStandalone =
|
|
73
|
+
(initialSearch = '') =>
|
|
74
|
+
(args: Story['args']) => ({
|
|
75
|
+
components: { PickerList },
|
|
76
|
+
setup() {
|
|
77
|
+
const search = ref(initialSearch)
|
|
78
|
+
const highlighted = ref<string | null>(null)
|
|
79
|
+
// `args.onClear` rides along through v-bind; this only resets the story's own term
|
|
80
|
+
const clear = () => (search.value = '')
|
|
81
|
+
return { args, search, highlighted, clear, frame: FRAME_CLASSES }
|
|
82
|
+
},
|
|
83
|
+
template: `
|
|
84
|
+
<div class="flex flex-col gap-3">
|
|
85
|
+
<label class="flex w-66 flex-col gap-1 small-normal text-gray-700">
|
|
86
|
+
Zoeken
|
|
87
|
+
<input v-model="search" type="text" class="h-10 rounded-lg px-3 ring ring-gray-200 focus:ring-cornflower-blue-500 focus:outline-none" />
|
|
88
|
+
</label>
|
|
89
|
+
<div :class="frame">
|
|
90
|
+
<PickerList v-bind="args" v-model:highlighted="highlighted" :search="search" class="h-full" @clear="clear" />
|
|
91
|
+
</div>
|
|
92
|
+
<p class="extra-small-normal text-gray-700">highlighted: <code data-testid="highlighted">{{ highlighted }}</code></p>
|
|
93
|
+
</div>
|
|
94
|
+
`,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const listboxOf = (canvasElement: HTMLElement) => within(canvasElement).getByRole('listbox')
|
|
98
|
+
|
|
99
|
+
/** The visible label of an option, without its hidden description. */
|
|
100
|
+
const labelOf = (option: Element | null) =>
|
|
101
|
+
Array.from(option?.querySelectorAll(':scope > span:not([hidden])') ?? [])
|
|
102
|
+
.at(-1)
|
|
103
|
+
?.textContent?.trim() ?? null
|
|
104
|
+
|
|
105
|
+
const activeLabel = (canvasElement: HTMLElement) => {
|
|
106
|
+
const id = listboxOf(canvasElement).getAttribute('aria-activedescendant')
|
|
107
|
+
return id ? labelOf(document.getElementById(id)) : null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Tab from the field into the list: the listbox is the only tab stop in it. Every key in the
|
|
112
|
+
* list works from there: arrows, Home/End, PageUp/PageDown, ←/→, Enter, Space, Escape and
|
|
113
|
+
* type-ahead.
|
|
114
|
+
*/
|
|
115
|
+
export const Standalone: Story = {
|
|
116
|
+
render: renderStandalone(),
|
|
117
|
+
play: async ({ args, canvasElement }) => {
|
|
118
|
+
const listbox = listboxOf(canvasElement)
|
|
119
|
+
|
|
120
|
+
await userEvent.click(within(canvasElement).getByRole('textbox'))
|
|
121
|
+
await userEvent.tab()
|
|
122
|
+
await expect(listbox).toHaveFocus()
|
|
123
|
+
await expect(activeLabel(canvasElement)).toBe('Introductie')
|
|
124
|
+
|
|
125
|
+
// the list overflows its 320px frame, and the ungrouped option still keeps its full 36px
|
|
126
|
+
await expect(listbox.scrollHeight).toBeGreaterThan(listbox.clientHeight)
|
|
127
|
+
const intro = document.getElementById(listbox.getAttribute('aria-activedescendant')!)!
|
|
128
|
+
await expect(intro.getBoundingClientRect().height).toBe(36)
|
|
129
|
+
|
|
130
|
+
// across the group boundary, never wrapping
|
|
131
|
+
await userEvent.keyboard('{ArrowUp}')
|
|
132
|
+
await expect(activeLabel(canvasElement)).toBe('Introductie')
|
|
133
|
+
await userEvent.keyboard('{ArrowDown}{ArrowDown}')
|
|
134
|
+
await expect(activeLabel(canvasElement)).toBe('Tekst')
|
|
135
|
+
await userEvent.keyboard('{End}')
|
|
136
|
+
await expect(activeLabel(canvasElement)).toBe('Galerij')
|
|
137
|
+
await userEvent.keyboard('{ArrowDown}')
|
|
138
|
+
await expect(activeLabel(canvasElement)).toBe('Galerij')
|
|
139
|
+
await userEvent.keyboard('{Home}')
|
|
140
|
+
await expect(activeLabel(canvasElement)).toBe('Introductie')
|
|
141
|
+
|
|
142
|
+
// Enter and Space select; a disabled item never does
|
|
143
|
+
await userEvent.keyboard('{ArrowDown}{Enter}')
|
|
144
|
+
await userEvent.keyboard(' ')
|
|
145
|
+
await expect(args.onSelect).toHaveBeenCalledTimes(2)
|
|
146
|
+
await expect(args.onSelect).toHaveBeenLastCalledWith(ITEMS[1])
|
|
147
|
+
await userEvent.keyboard('{ArrowDown}{ArrowDown}{Enter} ')
|
|
148
|
+
await expect(activeLabel(canvasElement)).toBe('Tabel')
|
|
149
|
+
await expect(args.onSelect).toHaveBeenCalledTimes(2)
|
|
150
|
+
|
|
151
|
+
// a click selects as well, and keeps focus where it was
|
|
152
|
+
await userEvent.click(within(canvasElement).getByText('Video'))
|
|
153
|
+
await expect(args.onSelect).toHaveBeenLastCalledWith(ITEMS[6])
|
|
154
|
+
await expect(listbox).toHaveFocus()
|
|
155
|
+
|
|
156
|
+
// ← folds the group of the highlight, → opens it again
|
|
157
|
+
await userEvent.keyboard('{End}{ArrowLeft}')
|
|
158
|
+
// out of the DOM once the 120ms fold has played; inert until then
|
|
159
|
+
await waitFor(() => expect(within(canvasElement).queryByText('Video')).toBeNull())
|
|
160
|
+
await expect(activeLabel(canvasElement)).toBe('Citaat')
|
|
161
|
+
await userEvent.keyboard('{ArrowRight}')
|
|
162
|
+
await expect(activeLabel(canvasElement)).toBe('Afbeelding')
|
|
163
|
+
|
|
164
|
+
await userEvent.keyboard('{Escape}')
|
|
165
|
+
await expect(args.onClose).toHaveBeenCalledOnce()
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Type a letter while the list has focus; the buffer resets after 800ms. */
|
|
170
|
+
export const TypeAhead: Story = {
|
|
171
|
+
render: renderStandalone(),
|
|
172
|
+
play: async ({ canvasElement }) => {
|
|
173
|
+
listboxOf(canvasElement).focus()
|
|
174
|
+
|
|
175
|
+
await userEvent.keyboard('t')
|
|
176
|
+
await expect(activeLabel(canvasElement)).toBe('Titel')
|
|
177
|
+
await userEvent.keyboard('a')
|
|
178
|
+
// disabled items are type-ahead targets too
|
|
179
|
+
await expect(activeLabel(canvasElement)).toBe('Tabel')
|
|
180
|
+
|
|
181
|
+
await new Promise((resolve) => setTimeout(resolve, 900))
|
|
182
|
+
await userEvent.keyboard('v')
|
|
183
|
+
await expect(activeLabel(canvasElement)).toBe('Video')
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Filtering ignores case and diacritics, matches keywords, and never leaves the list without a highlight. */
|
|
188
|
+
export const Filtering: Story = {
|
|
189
|
+
render: renderStandalone(),
|
|
190
|
+
play: async ({ canvasElement }) => {
|
|
191
|
+
const canvas = within(canvasElement)
|
|
192
|
+
const field = canvas.getByRole('textbox')
|
|
193
|
+
const labels = () => canvas.queryAllByRole('option').map(labelOf)
|
|
194
|
+
|
|
195
|
+
// highlight something the next search removes
|
|
196
|
+
listboxOf(canvasElement).focus()
|
|
197
|
+
await userEvent.keyboard('{End}')
|
|
198
|
+
await expect(activeLabel(canvasElement)).toBe('Galerij')
|
|
199
|
+
|
|
200
|
+
await userEvent.type(field, 'afbeeldíng')
|
|
201
|
+
await expect(labels()).toEqual(['Afbeelding'])
|
|
202
|
+
await expect(activeLabel(canvasElement)).toBe('Afbeelding')
|
|
203
|
+
await expect(canvas.getByTestId('highlighted')).toHaveTextContent('image')
|
|
204
|
+
|
|
205
|
+
await userEvent.clear(field)
|
|
206
|
+
await userEvent.type(field, 'KOP')
|
|
207
|
+
await expect(labels()).toEqual(['Titel'])
|
|
208
|
+
// groups without results are gone
|
|
209
|
+
await expect(canvas.getAllByRole('group')).toHaveLength(1)
|
|
210
|
+
|
|
211
|
+
// one live region, filled 500ms after the last change
|
|
212
|
+
const regions = canvasElement.querySelectorAll('[aria-live]')
|
|
213
|
+
await expect(regions).toHaveLength(1)
|
|
214
|
+
await expect(regions[0]).toHaveTextContent('')
|
|
215
|
+
await waitFor(() => expect(regions[0]).toHaveTextContent('1 onderdeel gevonden'), {
|
|
216
|
+
timeout: 1500,
|
|
217
|
+
})
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** 33 items in three groups: the keyboard scrolls the highlight into view, the pointer does not. */
|
|
222
|
+
export const ManyItems: Story = {
|
|
223
|
+
args: { items: MANY_ITEMS, groups: MANY_GROUPS },
|
|
224
|
+
render: renderStandalone(),
|
|
225
|
+
play: async ({ canvasElement }) => {
|
|
226
|
+
const listbox = listboxOf(canvasElement)
|
|
227
|
+
const inView = (label: string) => {
|
|
228
|
+
const option = within(listbox).getByText(label).closest<HTMLElement>('[role="option"]')!
|
|
229
|
+
const box = listbox.getBoundingClientRect()
|
|
230
|
+
const rect = option.getBoundingClientRect()
|
|
231
|
+
return rect.top >= box.top - 0.5 && rect.bottom <= box.bottom + 0.5
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
listbox.focus()
|
|
235
|
+
await userEvent.keyboard('{PageDown}')
|
|
236
|
+
await expect(activeLabel(canvasElement)).toBe('Tekst 9')
|
|
237
|
+
await expect(inView('Tekst 9')).toBe(true)
|
|
238
|
+
|
|
239
|
+
await userEvent.keyboard('{End}')
|
|
240
|
+
await expect(activeLabel(canvasElement)).toBe('Indeling 11')
|
|
241
|
+
await waitFor(() => expect(inView('Indeling 11')).toBe(true))
|
|
242
|
+
await userEvent.keyboard('{PageDown}')
|
|
243
|
+
await expect(activeLabel(canvasElement)).toBe('Indeling 11')
|
|
244
|
+
|
|
245
|
+
await userEvent.keyboard('{PageUp}')
|
|
246
|
+
await expect(activeLabel(canvasElement)).toBe('Indeling 3')
|
|
247
|
+
await waitFor(() => expect(inView('Indeling 3')).toBe(true))
|
|
248
|
+
|
|
249
|
+
await userEvent.keyboard('{Home}')
|
|
250
|
+
await waitFor(() => expect(listbox.scrollTop).toBe(0))
|
|
251
|
+
|
|
252
|
+
// a pointer highlight on a half-visible row leaves the scroll position alone
|
|
253
|
+
const rows = within(listbox).getAllByRole('option')
|
|
254
|
+
const partial = rows.find((row) => {
|
|
255
|
+
const box = listbox.getBoundingClientRect()
|
|
256
|
+
const rect = row.getBoundingClientRect()
|
|
257
|
+
return rect.top < box.bottom && rect.bottom > box.bottom
|
|
258
|
+
})!
|
|
259
|
+
const { left, top } = partial.getBoundingClientRect()
|
|
260
|
+
partial.dispatchEvent(
|
|
261
|
+
new PointerEvent('pointermove', { bubbles: true, clientX: left + 5, clientY: top + 2 }),
|
|
262
|
+
)
|
|
263
|
+
partial.dispatchEvent(
|
|
264
|
+
new PointerEvent('pointermove', { bubbles: true, clientX: left + 6, clientY: top + 3 }),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
await waitFor(() => expect(partial).toHaveAttribute('aria-selected', 'true'))
|
|
268
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
269
|
+
await expect(listbox.scrollTop).toBe(0)
|
|
270
|
+
},
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Folding with ← deep in a scrolled list: the heading of the folded group comes into view, the
|
|
275
|
+
* rows fold up under it, and the next group's first item ends up in view once they settle.
|
|
276
|
+
*/
|
|
277
|
+
export const FoldWhileScrolled: Story = {
|
|
278
|
+
args: { items: MANY_ITEMS, groups: MANY_GROUPS },
|
|
279
|
+
render: renderStandalone(),
|
|
280
|
+
play: async ({ canvasElement }) => {
|
|
281
|
+
const listbox = listboxOf(canvasElement)
|
|
282
|
+
const inView = (element: Element) => {
|
|
283
|
+
const box = listbox.getBoundingClientRect()
|
|
284
|
+
const rect = element.getBoundingClientRect()
|
|
285
|
+
return rect.top >= box.top - 0.5 && rect.bottom <= box.bottom + 0.5
|
|
286
|
+
}
|
|
287
|
+
const option = (label: string) =>
|
|
288
|
+
within(listbox).getByText(label).closest<HTMLElement>('[role="option"]')!
|
|
289
|
+
const settled = () => new Promise((resolve) => setTimeout(resolve, 300))
|
|
290
|
+
|
|
291
|
+
listbox.focus()
|
|
292
|
+
await userEvent.keyboard('{Home}{PageDown}{ArrowDown}')
|
|
293
|
+
await expect(activeLabel(canvasElement)).toBe('Tekst 10')
|
|
294
|
+
await expect(listbox.scrollTop).toBeGreaterThan(0)
|
|
295
|
+
|
|
296
|
+
await userEvent.keyboard('{ArrowLeft}')
|
|
297
|
+
await expect(activeLabel(canvasElement)).toBe('Media 1')
|
|
298
|
+
await settled()
|
|
299
|
+
await expect(inView(option('Media 1'))).toBe(true)
|
|
300
|
+
await expect(inView(within(listbox).getAllByRole('group')[0].firstElementChild!)).toBe(true)
|
|
301
|
+
|
|
302
|
+
// → opens the folded group again, and its first item is in view once it has unfolded
|
|
303
|
+
await userEvent.keyboard('{ArrowRight}')
|
|
304
|
+
await expect(activeLabel(canvasElement)).toBe('Tekst 1')
|
|
305
|
+
await settled()
|
|
306
|
+
await expect(inView(option('Tekst 1'))).toBe(true)
|
|
307
|
+
},
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Clearing a search puts every row back at its full-list position. The highlighted item stays
|
|
312
|
+
* in view, also when the pointer highlighted it.
|
|
313
|
+
*/
|
|
314
|
+
export const ClearSearchKeepsHighlightInView: Story = {
|
|
315
|
+
args: { items: MANY_ITEMS, groups: MANY_GROUPS },
|
|
316
|
+
render: renderStandalone(),
|
|
317
|
+
play: async ({ canvasElement }) => {
|
|
318
|
+
const canvas = within(canvasElement)
|
|
319
|
+
const field = canvas.getByRole('textbox')
|
|
320
|
+
const listbox = listboxOf(canvasElement)
|
|
321
|
+
const active = () => document.getElementById(listbox.getAttribute('aria-activedescendant')!)!
|
|
322
|
+
const inView = (element: Element) => {
|
|
323
|
+
const box = listbox.getBoundingClientRect()
|
|
324
|
+
const rect = element.getBoundingClientRect()
|
|
325
|
+
return rect.top >= box.top - 0.5 && rect.bottom <= box.bottom + 0.5
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// highlighted by the search itself
|
|
329
|
+
await userEvent.type(field, 'Indeling 7')
|
|
330
|
+
await expect(activeLabel(canvasElement)).toBe('Indeling 7')
|
|
331
|
+
await userEvent.clear(field)
|
|
332
|
+
await expect(canvas.getAllByRole('option')).toHaveLength(MANY_ITEMS.length)
|
|
333
|
+
await expect(activeLabel(canvasElement)).toBe('Indeling 7')
|
|
334
|
+
await waitFor(() => expect(inView(active())).toBe(true))
|
|
335
|
+
|
|
336
|
+
// highlighted by the pointer: the scroll follows the search, so it still happens
|
|
337
|
+
listbox.scrollTop = 0
|
|
338
|
+
await userEvent.type(field, 'Indeling')
|
|
339
|
+
const target = canvas.getByText('Indeling 9').closest('[role="option"]')!
|
|
340
|
+
const { left, top } = target.getBoundingClientRect()
|
|
341
|
+
target.dispatchEvent(
|
|
342
|
+
new PointerEvent('pointermove', { bubbles: true, clientX: left + 5, clientY: top + 2 }),
|
|
343
|
+
)
|
|
344
|
+
target.dispatchEvent(
|
|
345
|
+
new PointerEvent('pointermove', { bubbles: true, clientX: left + 6, clientY: top + 3 }),
|
|
346
|
+
)
|
|
347
|
+
await waitFor(() => expect(activeLabel(canvasElement)).toBe('Indeling 9'))
|
|
348
|
+
await userEvent.clear(field)
|
|
349
|
+
await expect(activeLabel(canvasElement)).toBe('Indeling 9')
|
|
350
|
+
await waitFor(() => expect(inView(active())).toBe(true))
|
|
351
|
+
},
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** No results: the list leaves the tab order and the clear button takes over. */
|
|
355
|
+
export const SearchEmpty: Story = {
|
|
356
|
+
render: renderStandalone('zwembad'),
|
|
357
|
+
play: async ({ args, canvasElement }) => {
|
|
358
|
+
const canvas = within(canvasElement)
|
|
359
|
+
const listbox = listboxOf(canvasElement)
|
|
360
|
+
|
|
361
|
+
await expect(canvas.getByText('Geen onderdelen gevonden')).toBeVisible()
|
|
362
|
+
await expect(canvas.getByText("Geen resultaat voor 'zwembad'.")).toBeVisible()
|
|
363
|
+
await expect(listbox).toHaveAttribute('tabindex', '-1')
|
|
364
|
+
await expect(listbox).not.toHaveAttribute('aria-activedescendant')
|
|
365
|
+
|
|
366
|
+
await userEvent.click(canvas.getByRole('textbox'))
|
|
367
|
+
await userEvent.tab()
|
|
368
|
+
await expect(canvas.getByRole('button', { name: 'Wis zoekopdracht' })).toHaveFocus()
|
|
369
|
+
|
|
370
|
+
await userEvent.keyboard('{Enter}')
|
|
371
|
+
await expect(args.onClear).toHaveBeenCalledOnce()
|
|
372
|
+
await waitFor(() => expect(canvas.getAllByRole('option')).toHaveLength(ITEMS.length))
|
|
373
|
+
},
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export const CustomEmptySlot: Story = {
|
|
377
|
+
render: (args) => ({
|
|
378
|
+
components: { PickerList },
|
|
379
|
+
setup: () => ({ args, search: ref('zwembad'), frame: FRAME_CLASSES }),
|
|
380
|
+
template: `
|
|
381
|
+
<div :class="frame">
|
|
382
|
+
<PickerList v-bind="args" :search="search" class="h-full" @clear="search = ''">
|
|
383
|
+
<template #empty="{ search: term, clear }">
|
|
384
|
+
<div class="flex flex-1 flex-col items-center justify-center gap-2 p-4 text-center">
|
|
385
|
+
<p class="small-normal text-gray-900">Niets voor „{{ term }}".</p>
|
|
386
|
+
<button type="button" class="small-semibold text-cornflower-blue-700 underline" @click="clear">
|
|
387
|
+
Opnieuw zoeken
|
|
388
|
+
</button>
|
|
389
|
+
</div>
|
|
390
|
+
</template>
|
|
391
|
+
</PickerList>
|
|
392
|
+
</div>
|
|
393
|
+
`,
|
|
394
|
+
}),
|
|
395
|
+
play: async ({ canvasElement }) => {
|
|
396
|
+
const canvas = within(canvasElement)
|
|
397
|
+
|
|
398
|
+
await expect(canvas.getByText('Niets voor „zwembad".')).toBeVisible()
|
|
399
|
+
await userEvent.click(canvas.getByRole('button', { name: 'Opnieuw zoeken' }))
|
|
400
|
+
await waitFor(() => expect(canvas.getAllByRole('option')).toHaveLength(ITEMS.length))
|
|
401
|
+
},
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Every group folded: nothing is highlighted until → or ↓ opens the first group. */
|
|
405
|
+
export const AllGroupsClosed: Story = {
|
|
406
|
+
args: {
|
|
407
|
+
items: ITEMS.filter((item) => item.group),
|
|
408
|
+
groups: GROUPS.map((group) => ({ ...group, defaultOpen: false })),
|
|
409
|
+
},
|
|
410
|
+
render: renderStandalone(),
|
|
411
|
+
play: async ({ canvasElement }) => {
|
|
412
|
+
const listbox = listboxOf(canvasElement)
|
|
413
|
+
|
|
414
|
+
await expect(listbox).not.toHaveAttribute('aria-activedescendant')
|
|
415
|
+
await expect(listbox).toHaveAttribute('tabindex', '0')
|
|
416
|
+
|
|
417
|
+
listbox.focus()
|
|
418
|
+
await userEvent.keyboard('{ArrowRight}')
|
|
419
|
+
await expect(activeLabel(canvasElement)).toBe('Titel')
|
|
420
|
+
},
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Headings are labels only: no chevrons, and every group stays open. */
|
|
424
|
+
export const NotCollapsible: Story = {
|
|
425
|
+
args: {
|
|
426
|
+
collapsible: false,
|
|
427
|
+
groups: GROUPS.map((group) => ({ ...group, defaultOpen: false })),
|
|
428
|
+
},
|
|
429
|
+
render: renderStandalone(),
|
|
430
|
+
play: async ({ canvasElement }) => {
|
|
431
|
+
const groups = within(canvasElement).getAllByRole('group')
|
|
432
|
+
|
|
433
|
+
await expect(groups.every((group) => !group.hasAttribute('aria-expanded'))).toBe(true)
|
|
434
|
+
await expect(within(canvasElement).getAllByRole('option')).toHaveLength(ITEMS.length)
|
|
435
|
+
},
|
|
436
|
+
}
|