@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,734 @@
|
|
|
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 IconPicker, {
|
|
5
|
+
type NexxtIconPickerCategory,
|
|
6
|
+
type NexxtIconPickerIcon,
|
|
7
|
+
type NexxtIconPickerStyle,
|
|
8
|
+
type NexxtIconPickerValue,
|
|
9
|
+
} from './IconPicker.vue'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The stories draw with the Font Awesome kit Storybook loads; the component itself knows no icon
|
|
13
|
+
* set. These classes and `iconClass` are what a consumer hands in.
|
|
14
|
+
*/
|
|
15
|
+
const STYLES: NexxtIconPickerStyle[] = [
|
|
16
|
+
{ id: 'light', label: 'Light', classes: ['fa-light'] },
|
|
17
|
+
{ id: 'regular', label: 'Regular', classes: ['fa-regular'] },
|
|
18
|
+
{ id: 'solid', label: 'Solid', classes: ['fa-solid'] },
|
|
19
|
+
{ id: 'thin', label: 'Thin', classes: ['fa-thin'] },
|
|
20
|
+
{ id: 'duotone-light', label: 'Duotone', classes: ['fa-duotone', 'fa-light'] },
|
|
21
|
+
{ id: 'brands', label: 'Merk', classes: ['fa-brands'] },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const ALL_STYLES = ['light', 'regular', 'solid', 'thin', 'duotone-light']
|
|
25
|
+
|
|
26
|
+
const iconClass = (name: string) => `fa-${name}`
|
|
27
|
+
|
|
28
|
+
const icon = (name: string, label: string, terms: string[] = [], styles = ALL_STYLES) => ({
|
|
29
|
+
name,
|
|
30
|
+
label,
|
|
31
|
+
terms,
|
|
32
|
+
styles,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
/** A slice of the kit: about fifty icons, one of them a brand. */
|
|
36
|
+
const ICONS: NexxtIconPickerIcon[] = [
|
|
37
|
+
icon('house', 'House', ['home', 'building', 'residence']),
|
|
38
|
+
icon('house-chimney', 'House chimney', ['home', 'fireplace']),
|
|
39
|
+
icon('house-user', 'House user', ['home', 'resident']),
|
|
40
|
+
icon('building', 'Building', ['apartment', 'office']),
|
|
41
|
+
icon('city', 'City', ['buildings', 'town']),
|
|
42
|
+
icon('store', 'Store', ['shop', 'market']),
|
|
43
|
+
icon('warehouse', 'Warehouse', ['storage']),
|
|
44
|
+
icon('key', 'Key', ['unlock', 'password']),
|
|
45
|
+
icon('door-open', 'Door open', ['entry', 'exit']),
|
|
46
|
+
icon('bed', 'Bed', ['sleep', 'bedroom']),
|
|
47
|
+
icon('couch', 'Couch', ['sofa', 'living room']),
|
|
48
|
+
icon('bath', 'Bath', ['bathroom', 'tub']),
|
|
49
|
+
// Fewer styles: a cell shows it in the first of `styles` it has when the current one is missing
|
|
50
|
+
icon('kitchen-set', 'Kitchen set', ['cooking', 'pan'], ['solid', 'regular']),
|
|
51
|
+
icon('tree', 'Tree', ['forest', 'nature']),
|
|
52
|
+
icon('leaf', 'Leaf', ['eco', 'green']),
|
|
53
|
+
icon('sun', 'Sun', ['weather', 'bright']),
|
|
54
|
+
icon('moon', 'Moon', ['night', 'dark']),
|
|
55
|
+
icon('cloud', 'Cloud', ['weather']),
|
|
56
|
+
icon('snowflake', 'Snowflake', ['winter', 'cold']),
|
|
57
|
+
icon('star', 'Star', ['favorite', 'rating']),
|
|
58
|
+
icon('heart', 'Heart', ['love', 'like']),
|
|
59
|
+
icon('bell', 'Bell', ['notification', 'alert']),
|
|
60
|
+
icon('envelope', 'Envelope', ['email', 'mail', 'letter']),
|
|
61
|
+
icon('phone', 'Phone', ['call', 'telephone']),
|
|
62
|
+
icon('comment', 'Comment', ['chat', 'message']),
|
|
63
|
+
icon('user', 'User', ['person', 'profile']),
|
|
64
|
+
icon('users', 'Users', ['people', 'group']),
|
|
65
|
+
icon('user-tie', 'User tie', ['agent', 'business']),
|
|
66
|
+
icon('handshake', 'Handshake', ['deal', 'agreement']),
|
|
67
|
+
icon('briefcase', 'Briefcase', ['work', 'business']),
|
|
68
|
+
icon('chart-line', 'Chart line', ['graph', 'trend']),
|
|
69
|
+
icon('chart-pie', 'Chart pie', ['graph', 'share']),
|
|
70
|
+
icon('calendar', 'Calendar', ['date', 'event']),
|
|
71
|
+
icon('clock', 'Clock', ['time', 'hour']),
|
|
72
|
+
icon('camera', 'Camera', ['photo', 'picture']),
|
|
73
|
+
icon('image', 'Image', ['photo', 'picture']),
|
|
74
|
+
icon('video', 'Video', ['film', 'movie']),
|
|
75
|
+
icon('music', 'Music', ['song', 'audio']),
|
|
76
|
+
icon('map-location-dot', 'Map location dot', ['map', 'pin']),
|
|
77
|
+
icon('location-dot', 'Location dot', ['map', 'pin', 'marker']),
|
|
78
|
+
icon('car', 'Car', ['vehicle', 'drive']),
|
|
79
|
+
icon('bicycle', 'Bicycle', ['bike', 'cycle']),
|
|
80
|
+
icon('plane', 'Plane', ['airplane', 'flight', 'travel']),
|
|
81
|
+
icon('train', 'Train', ['rail', 'transit']),
|
|
82
|
+
icon('truck', 'Truck', ['delivery', 'moving']),
|
|
83
|
+
icon('magnifying-glass', 'Magnifying glass', ['search', 'find']),
|
|
84
|
+
icon('gear', 'Gear', ['settings', 'cog']),
|
|
85
|
+
icon('lock', 'Lock', ['secure', 'padlock']),
|
|
86
|
+
icon('shield', 'Shield', ['protect', 'security']),
|
|
87
|
+
icon('circle-check', 'Circle check', ['done', 'ok']),
|
|
88
|
+
icon('circle-info', 'Circle info', ['information', 'help']),
|
|
89
|
+
icon('linkedin', 'LinkedIn', ['social', 'network'], ['brands']),
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
/** A few categories. The labels sort in Dutch; the last one shows how a long label truncates. */
|
|
93
|
+
const CATEGORIES: NexxtIconPickerCategory[] = [
|
|
94
|
+
{
|
|
95
|
+
id: 'buildings',
|
|
96
|
+
label: 'Gebouwen',
|
|
97
|
+
icons: ['house', 'house-chimney', 'house-user', 'building', 'city', 'store', 'warehouse'],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'household',
|
|
101
|
+
label: 'Huishouden',
|
|
102
|
+
icons: ['key', 'door-open', 'bed', 'couch', 'bath', 'kitchen-set', 'house'],
|
|
103
|
+
},
|
|
104
|
+
{ id: 'nature', label: 'Natuur', icons: ['tree', 'leaf', 'sun', 'moon', 'cloud', 'snowflake'] },
|
|
105
|
+
{
|
|
106
|
+
id: 'communication',
|
|
107
|
+
label: 'Communicatie',
|
|
108
|
+
icons: ['bell', 'envelope', 'phone', 'comment', 'users'],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'business',
|
|
112
|
+
label: 'Zakelijk',
|
|
113
|
+
icons: ['user-tie', 'handshake', 'briefcase', 'chart-line', 'chart-pie', 'calendar'],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'transportation',
|
|
117
|
+
label: 'Vervoer',
|
|
118
|
+
icons: ['car', 'bicycle', 'plane', 'train', 'truck'],
|
|
119
|
+
},
|
|
120
|
+
{ id: 'brands', label: 'Merken', icons: ['linkedin'] },
|
|
121
|
+
// Only names the catalogue does not know: an empty category, which the rail leaves out
|
|
122
|
+
{ id: 'retired', label: 'Archief', icons: ['retired-icon', 'another-retired-icon'] },
|
|
123
|
+
{
|
|
124
|
+
id: 'security',
|
|
125
|
+
label: 'Beveiliging, sloten en toegangscontrole',
|
|
126
|
+
icons: ['lock', 'key', 'shield'],
|
|
127
|
+
},
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
const PINNED = { label: 'Op deze pagina', icons: ['envelope', 'phone', 'location-dot', 'star'] }
|
|
131
|
+
|
|
132
|
+
export default {
|
|
133
|
+
title: 'Components/Organisms/IconPicker',
|
|
134
|
+
component: IconPicker,
|
|
135
|
+
// Room for the panel under the trigger, also when Docs renders the story inline
|
|
136
|
+
decorators: [() => ({ template: '<div class="min-h-148 p-2"><story /></div>' })],
|
|
137
|
+
parameters: {
|
|
138
|
+
// A new component without known violations: axe gates these stories, against the project's
|
|
139
|
+
// reporting mode. The plays end with the panel open where its content needs checking.
|
|
140
|
+
a11y: { test: 'error' },
|
|
141
|
+
},
|
|
142
|
+
args: {
|
|
143
|
+
icons: ICONS,
|
|
144
|
+
categories: CATEGORIES,
|
|
145
|
+
styles: STYLES,
|
|
146
|
+
iconClass,
|
|
147
|
+
catalogueStatus: 'ready',
|
|
148
|
+
onOpen: fn(),
|
|
149
|
+
onClose: fn(),
|
|
150
|
+
onRetry: fn(),
|
|
151
|
+
},
|
|
152
|
+
argTypes: {
|
|
153
|
+
catalogueStatus: { control: 'inline-radio', options: ['loading', 'ready', 'error'] },
|
|
154
|
+
side: { control: 'inline-radio', options: ['bottom', 'left'] },
|
|
155
|
+
},
|
|
156
|
+
} satisfies Meta<typeof IconPicker>
|
|
157
|
+
|
|
158
|
+
type Story = StoryObj<typeof IconPicker>
|
|
159
|
+
|
|
160
|
+
/** The settings panel of the editor is 312px of content. */
|
|
161
|
+
const EDITOR_WIDTH = 'w-78'
|
|
162
|
+
|
|
163
|
+
const renderPicker =
|
|
164
|
+
(initial: NexxtIconPickerValue | null, frame = EDITOR_WIDTH) =>
|
|
165
|
+
(args: Story['args']) => ({
|
|
166
|
+
components: { IconPicker },
|
|
167
|
+
setup() {
|
|
168
|
+
const value = ref<NexxtIconPickerValue | null>(initial)
|
|
169
|
+
return { args, value, frame }
|
|
170
|
+
},
|
|
171
|
+
template: `
|
|
172
|
+
<div class="flex flex-col gap-3">
|
|
173
|
+
<div :class="frame">
|
|
174
|
+
<IconPicker v-bind="args" v-model="value" />
|
|
175
|
+
</div>
|
|
176
|
+
<button type="button" class="self-start small-normal text-gray-900 underline">Volgende veld</button>
|
|
177
|
+
<p class="extra-small-normal text-gray-700">
|
|
178
|
+
model: <code data-testid="model">{{ value === null ? 'null' : value.icon + ' / ' + value.style }}</code>
|
|
179
|
+
</p>
|
|
180
|
+
</div>
|
|
181
|
+
`,
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// The panel is teleported to <body>, so it is looked up there, not in the canvas
|
|
185
|
+
const page = within(document.body)
|
|
186
|
+
|
|
187
|
+
const triggerOf = (canvasElement: HTMLElement) =>
|
|
188
|
+
canvasElement.querySelector<HTMLButtonElement>('button[aria-haspopup="grid"]')!
|
|
189
|
+
|
|
190
|
+
const gridOf = () => page.getByRole('grid')
|
|
191
|
+
|
|
192
|
+
/** The panel scales in for 200ms; layout sizes are not scaled, rects are. */
|
|
193
|
+
const settled = (element: HTMLElement) =>
|
|
194
|
+
waitFor(() => expect(getComputedStyle(element.closest('.z-50 > div')!).opacity).toBe('1'))
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Opens the panel and waits for its enter transition. The field has focus while the panel still
|
|
198
|
+
* carries the transition's `opacity-0`, so a `toBeVisible` right after would fail on a slow runner.
|
|
199
|
+
*/
|
|
200
|
+
const openPanel = async (canvasElement: HTMLElement) => {
|
|
201
|
+
await userEvent.click(triggerOf(canvasElement))
|
|
202
|
+
const input = await page.findByRole('textbox', { name: 'Icoon zoeken' })
|
|
203
|
+
await waitFor(() => expect(input).toHaveFocus())
|
|
204
|
+
await settled(input)
|
|
205
|
+
return input
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const activeCell = () =>
|
|
209
|
+
document.getElementById(gridOf().getAttribute('aria-activedescendant') ?? '')
|
|
210
|
+
|
|
211
|
+
const closed = () => waitFor(() => expect(page.queryByRole('grid')).toBeNull())
|
|
212
|
+
|
|
213
|
+
const MEASURE_CLASS = 'zz-glyph-measure'
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The box of the glyph itself, not of its `<i>`: Font Awesome draws the glyph in `::before`, which
|
|
217
|
+
* has no rect. For one measurement the pseudo-element is switched off and a span with the same
|
|
218
|
+
* character and font takes its place, laid out exactly as `::before` would be.
|
|
219
|
+
*/
|
|
220
|
+
const glyphBox = (glyph: HTMLElement) => {
|
|
221
|
+
if (!document.getElementById(MEASURE_CLASS)) {
|
|
222
|
+
const style = document.createElement('style')
|
|
223
|
+
style.id = MEASURE_CLASS
|
|
224
|
+
style.textContent = `.${MEASURE_CLASS}::before { content: none !important; }`
|
|
225
|
+
document.head.append(style)
|
|
226
|
+
}
|
|
227
|
+
const before = getComputedStyle(glyph, '::before')
|
|
228
|
+
const character = /^"(.*?)"/u.exec(before.content)?.[1] ?? ''
|
|
229
|
+
const span = document.createElement('span')
|
|
230
|
+
span.textContent = character
|
|
231
|
+
Object.assign(span.style, {
|
|
232
|
+
fontFamily: before.fontFamily,
|
|
233
|
+
fontWeight: before.fontWeight,
|
|
234
|
+
fontStyle: before.fontStyle,
|
|
235
|
+
fontSize: before.fontSize,
|
|
236
|
+
lineHeight: before.lineHeight,
|
|
237
|
+
})
|
|
238
|
+
glyph.classList.add(MEASURE_CLASS)
|
|
239
|
+
glyph.append(span)
|
|
240
|
+
const box = span.getBoundingClientRect()
|
|
241
|
+
span.remove()
|
|
242
|
+
glyph.classList.remove(MEASURE_CLASS)
|
|
243
|
+
return { character, box }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** The glyph's centre lies within 0.5px of its frame's centre, on both axes. */
|
|
247
|
+
const expectCentred = async (glyph: HTMLElement, frame: HTMLElement) => {
|
|
248
|
+
const { character, box } = glyphBox(glyph)
|
|
249
|
+
const outer = frame.getBoundingClientRect()
|
|
250
|
+
await expect(character).not.toBe('')
|
|
251
|
+
await expect(box.width).toBeGreaterThan(0)
|
|
252
|
+
await expect(
|
|
253
|
+
Math.abs(box.left + box.width / 2 - (outer.left + outer.width / 2)),
|
|
254
|
+
).toBeLessThanOrEqual(0.5)
|
|
255
|
+
await expect(
|
|
256
|
+
Math.abs(box.top + box.height / 2 - (outer.top + outer.height / 2)),
|
|
257
|
+
).toBeLessThanOrEqual(0.5)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Waits for the icon font, so a glyph has its real width before it is measured. */
|
|
261
|
+
const iconFontReady = async () => {
|
|
262
|
+
await document.fonts.ready
|
|
263
|
+
await waitFor(() =>
|
|
264
|
+
expect(
|
|
265
|
+
[...document.fonts].some(
|
|
266
|
+
(font) => /Font Awesome/.test(font.family) && font.status === 'loaded',
|
|
267
|
+
),
|
|
268
|
+
).toBe(true),
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The editor: no visible label, the icons already on the page pinned on top, and the panel beside
|
|
274
|
+
* the settings panel. The trigger reads "Symbool, Envelope, Light"; the current icon leads the
|
|
275
|
+
* pinned section and is the one selected cell.
|
|
276
|
+
*/
|
|
277
|
+
export const Editor: Story = {
|
|
278
|
+
args: { ariaLabel: 'Symbool', pinned: PINNED },
|
|
279
|
+
render: renderPicker({ icon: 'envelope', style: 'light' }),
|
|
280
|
+
play: async ({ canvasElement, args }) => {
|
|
281
|
+
const canvas = within(canvasElement)
|
|
282
|
+
const trigger = triggerOf(canvasElement)
|
|
283
|
+
await expect(trigger).toHaveAccessibleName('Symbool, Envelope, Light')
|
|
284
|
+
await expect(trigger).toHaveAttribute('aria-expanded', 'false')
|
|
285
|
+
|
|
286
|
+
const input = await openPanel(canvasElement)
|
|
287
|
+
await expect(args.onOpen).toHaveBeenCalledOnce()
|
|
288
|
+
await expect(trigger).toHaveAttribute('aria-expanded', 'true')
|
|
289
|
+
|
|
290
|
+
const grid = gridOf()
|
|
291
|
+
await settled(grid)
|
|
292
|
+
await expect(input).toHaveAttribute('aria-controls', grid.id)
|
|
293
|
+
await expect(page.getByRole('tab', { name: 'Alle iconen' })).toHaveAttribute(
|
|
294
|
+
'aria-selected',
|
|
295
|
+
'true',
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
// the pinned section first, the current icon leading it; one selected cell
|
|
299
|
+
const headers = page.getAllByRole('rowheader')
|
|
300
|
+
await expect(headers.map((header) => header.textContent)).toEqual([
|
|
301
|
+
'Op deze pagina',
|
|
302
|
+
'Alle iconen',
|
|
303
|
+
])
|
|
304
|
+
const selected = grid.querySelectorAll('[aria-selected="true"]')
|
|
305
|
+
await expect(selected).toHaveLength(1)
|
|
306
|
+
await expect(selected[0]).toHaveAccessibleName('Envelope')
|
|
307
|
+
await expect(selected[0]).toHaveAccessibleDescription('Huidige keuze')
|
|
308
|
+
await expect(selected[0]!.closest('[role="row"]')).toHaveAttribute('aria-rowindex', '2')
|
|
309
|
+
|
|
310
|
+
// the panel: 576 wide (less where the viewport is narrower), the surface's shadow-sm
|
|
311
|
+
const panel = page.getByRole('group', { name: 'Symbool' })
|
|
312
|
+
const surface = panel.parentElement!
|
|
313
|
+
await expect(surface.className).toContain('shadow-sm')
|
|
314
|
+
const viewport = document.documentElement.clientWidth
|
|
315
|
+
await expect(surface.offsetWidth).toBe(Math.min(576, viewport - 16))
|
|
316
|
+
await expect(surface.offsetHeight).toBeLessThanOrEqual(520)
|
|
317
|
+
|
|
318
|
+
// the rail is 192px; six square cells share the grid's width with 12px between them
|
|
319
|
+
const rail = page.getByRole('tablist', { name: 'Categorieën' })
|
|
320
|
+
await expect(rail.offsetWidth).toBe(192)
|
|
321
|
+
const row = grid.querySelectorAll<HTMLElement>('[role="row"]:has([role="gridcell"])')[1]!
|
|
322
|
+
await expect(getComputedStyle(row).gridTemplateColumns.split(' ')).toHaveLength(6)
|
|
323
|
+
await expect(getComputedStyle(row).columnGap).toBe('12px')
|
|
324
|
+
const cells = Array.from(row.querySelectorAll<HTMLElement>('[role="gridcell"]'))
|
|
325
|
+
const content = grid.clientWidth - 16
|
|
326
|
+
const cell = (content - 5 * 12) / 6
|
|
327
|
+
for (const element of cells) {
|
|
328
|
+
await expect(Math.abs(element.offsetWidth - cell)).toBeLessThanOrEqual(1)
|
|
329
|
+
await expect(element.offsetHeight).toBe(element.offsetWidth)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// every glyph sits in the middle of its cell, whatever its width: the selected cell, a plain
|
|
333
|
+
// one, a hovered one; and in the trigger's preview box and the footer
|
|
334
|
+
await iconFontReady()
|
|
335
|
+
for (const name of ['Envelope', 'Bicycle', 'Star', 'Phone']) {
|
|
336
|
+
const target = page.getAllByRole('gridcell', { name })[0]!
|
|
337
|
+
await expectCentred(target.querySelector('i')!, target)
|
|
338
|
+
}
|
|
339
|
+
const camera = page.getByRole('gridcell', { name: 'Camera' })
|
|
340
|
+
await userEvent.hover(camera)
|
|
341
|
+
await expectCentred(camera.querySelector('i')!, camera)
|
|
342
|
+
const preview = trigger.querySelector<HTMLElement>('i')!
|
|
343
|
+
await expectCentred(preview, preview.parentElement!)
|
|
344
|
+
const footerGlyph = panel.querySelector<HTMLElement>('[aria-hidden="true"].h-11 i')!
|
|
345
|
+
await expectCentred(footerGlyph, footerGlyph)
|
|
346
|
+
|
|
347
|
+
// hover moves the highlight and the footer, never the model
|
|
348
|
+
await userEvent.hover(page.getByRole('gridcell', { name: 'Bell' }))
|
|
349
|
+
await expect(panel).toHaveTextContent('bell')
|
|
350
|
+
await expect(canvas.getByTestId('model')).toHaveTextContent('envelope / light')
|
|
351
|
+
|
|
352
|
+
// a choice writes the model, closes the panel and puts focus back on the trigger
|
|
353
|
+
await userEvent.click(page.getByRole('gridcell', { name: 'Kitchen set' }))
|
|
354
|
+
await closed()
|
|
355
|
+
await expect(trigger).toHaveFocus()
|
|
356
|
+
// Kitchen set has no light: it takes the first style it has in the order of `styles`
|
|
357
|
+
await expect(canvas.getByTestId('model')).toHaveTextContent('kitchen-set / regular')
|
|
358
|
+
await expect(trigger).toHaveAccessibleName('Symbool, Kitchen set, Regular')
|
|
359
|
+
|
|
360
|
+
// open again: the panel is left open for axe
|
|
361
|
+
await openPanel(canvasElement)
|
|
362
|
+
},
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** A form: visible label, placeholder, nothing chosen yet. */
|
|
366
|
+
export const Form: Story = {
|
|
367
|
+
args: { label: 'Icoon voor de knop' },
|
|
368
|
+
render: renderPicker(null, 'w-96'),
|
|
369
|
+
play: async ({ canvasElement }) => {
|
|
370
|
+
const trigger = triggerOf(canvasElement)
|
|
371
|
+
await expect(trigger).toHaveAccessibleName('Icoon voor de knop Kies een icoon')
|
|
372
|
+
await openPanel(canvasElement)
|
|
373
|
+
await expect(gridOf().querySelector('[aria-selected="true"]')).toBeNull()
|
|
374
|
+
},
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Search runs over the whole catalogue: name, label and the extra terms, every word has to match.
|
|
379
|
+
* An exact name ranks first, then a name or label that starts with the search, then one where a
|
|
380
|
+
* word does, then the rest; alphabetical within a rank. Searching resets the category, a category
|
|
381
|
+
* clears the search.
|
|
382
|
+
*/
|
|
383
|
+
export const Search: Story = {
|
|
384
|
+
args: { ariaLabel: 'Symbool' },
|
|
385
|
+
render: renderPicker({ icon: 'star', style: 'solid' }),
|
|
386
|
+
play: async ({ canvasElement }) => {
|
|
387
|
+
const input = await openPanel(canvasElement)
|
|
388
|
+
const names = () =>
|
|
389
|
+
page.getAllByRole('gridcell').map((cell) => cell.getAttribute('aria-label') ?? '')
|
|
390
|
+
|
|
391
|
+
await userEvent.click(page.getByRole('tab', { name: 'Natuur' }))
|
|
392
|
+
await expect(names()).toEqual(['Cloud', 'Leaf', 'Moon', 'Snowflake', 'Sun', 'Tree'])
|
|
393
|
+
|
|
394
|
+
// exact name, then names that start with it, then a match inside a word
|
|
395
|
+
await userEvent.type(input, 'house')
|
|
396
|
+
await expect(page.getByRole('tab', { name: 'Alle iconen' })).toHaveAttribute(
|
|
397
|
+
'aria-selected',
|
|
398
|
+
'true',
|
|
399
|
+
)
|
|
400
|
+
await expect(names()).toEqual(['House', 'House chimney', 'House user', 'Warehouse'])
|
|
401
|
+
|
|
402
|
+
// `home` is only a search term
|
|
403
|
+
await userEvent.clear(input)
|
|
404
|
+
await userEvent.type(input, 'home')
|
|
405
|
+
await expect(names()).toEqual(['House', 'House chimney', 'House user'])
|
|
406
|
+
|
|
407
|
+
await userEvent.clear(input)
|
|
408
|
+
await userEvent.type(input, 'HOUSE chim')
|
|
409
|
+
await expect(names()).toEqual(['House chimney'])
|
|
410
|
+
|
|
411
|
+
// the count is announced after 500ms
|
|
412
|
+
const live = document.querySelector('[aria-live="polite"]')!
|
|
413
|
+
await waitFor(() => expect(live).toHaveTextContent('1 icoon'))
|
|
414
|
+
|
|
415
|
+
await userEvent.click(page.getByRole('tab', { name: 'Gebouwen' }))
|
|
416
|
+
await expect(input).toHaveValue('')
|
|
417
|
+
await waitFor(() => expect(live).toHaveTextContent('7 iconen'))
|
|
418
|
+
|
|
419
|
+
// a category without any icon the catalogue knows is not in the rail
|
|
420
|
+
await expect(page.queryByRole('tab', { name: 'Archief' })).toBeNull()
|
|
421
|
+
|
|
422
|
+
// a long label truncates, with the full name as its title
|
|
423
|
+
const long = page.getByRole('tab', { name: 'Beveiliging, sloten en toegangscontrole' })
|
|
424
|
+
await expect(long).toHaveAttribute('title', 'Beveiliging, sloten en toegangscontrole')
|
|
425
|
+
const text = long.querySelector('span')!
|
|
426
|
+
await expect(getComputedStyle(text).textOverflow).toBe('ellipsis')
|
|
427
|
+
},
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* The keyboard: ↓ on the trigger opens with focus in the search field, ↓ goes on into the grid,
|
|
432
|
+
* arrows, Home/End, Ctrl+Home/End and PageUp/PageDown move the highlight, Enter chooses, Escape
|
|
433
|
+
* closes without a change. Tab runs search field → rail → grid → out, which closes the panel.
|
|
434
|
+
*/
|
|
435
|
+
export const Keyboard: Story = {
|
|
436
|
+
args: { ariaLabel: 'Symbool' },
|
|
437
|
+
render: renderPicker({ icon: 'house', style: 'light' }),
|
|
438
|
+
play: async ({ canvasElement }) => {
|
|
439
|
+
const canvas = within(canvasElement)
|
|
440
|
+
const trigger = triggerOf(canvasElement)
|
|
441
|
+
|
|
442
|
+
trigger.focus()
|
|
443
|
+
await userEvent.keyboard('{ArrowDown}')
|
|
444
|
+
const input = await page.findByRole('textbox', { name: 'Icoon zoeken' })
|
|
445
|
+
await waitFor(() => expect(input).toHaveFocus())
|
|
446
|
+
|
|
447
|
+
// into the grid: the current icon, in view
|
|
448
|
+
await userEvent.keyboard('{ArrowDown}')
|
|
449
|
+
const grid = gridOf()
|
|
450
|
+
await expect(grid).toHaveFocus()
|
|
451
|
+
await expect(activeCell()).toHaveAccessibleName('House')
|
|
452
|
+
|
|
453
|
+
await userEvent.keyboard('{ArrowRight}')
|
|
454
|
+
await expect(activeCell()).toHaveAccessibleName('House chimney')
|
|
455
|
+
await userEvent.keyboard('{Home}')
|
|
456
|
+
await expect(activeCell()?.getAttribute('aria-colindex')).toBe('1')
|
|
457
|
+
await userEvent.keyboard('{End}')
|
|
458
|
+
await expect(activeCell()?.getAttribute('aria-colindex')).toBe('6')
|
|
459
|
+
await userEvent.keyboard('{Control>}{End}{/Control}')
|
|
460
|
+
await expect(activeCell()).toHaveAccessibleName('Warehouse')
|
|
461
|
+
await userEvent.keyboard('{Control>}{Home}{/Control}')
|
|
462
|
+
await expect(activeCell()).toHaveAccessibleName('Bath')
|
|
463
|
+
|
|
464
|
+
// a page is the rows in view less one
|
|
465
|
+
const rowOf = () => Number(activeCell()!.closest('[role="row"]')!.getAttribute('aria-rowindex'))
|
|
466
|
+
const step = Math.max(1, Math.floor(grid.clientHeight / (activeCell()!.offsetHeight + 12)) - 1)
|
|
467
|
+
await userEvent.keyboard('{PageDown}')
|
|
468
|
+
await expect(rowOf()).toBe(1 + step)
|
|
469
|
+
await userEvent.keyboard('{PageUp}')
|
|
470
|
+
await expect(rowOf()).toBe(1)
|
|
471
|
+
|
|
472
|
+
// ↑ in the first row goes back to the field; a letter in the grid goes there too
|
|
473
|
+
await userEvent.keyboard('{ArrowUp}')
|
|
474
|
+
await expect(input).toHaveFocus()
|
|
475
|
+
await userEvent.keyboard('{ArrowDown}')
|
|
476
|
+
await userEvent.keyboard('t')
|
|
477
|
+
await expect(input).toHaveFocus()
|
|
478
|
+
await expect(input).toHaveValue('t')
|
|
479
|
+
await userEvent.clear(input)
|
|
480
|
+
|
|
481
|
+
// Enter chooses, closes and returns focus
|
|
482
|
+
await userEvent.keyboard('{ArrowDown}{ArrowRight}{Enter}')
|
|
483
|
+
await closed()
|
|
484
|
+
await expect(trigger).toHaveFocus()
|
|
485
|
+
await expect(canvas.getByTestId('model')).toHaveTextContent('house-chimney / light')
|
|
486
|
+
|
|
487
|
+
// Escape closes without a change; reopening highlights the current icon again
|
|
488
|
+
await userEvent.keyboard('{Enter}')
|
|
489
|
+
await waitFor(() => expect(page.getByRole('textbox', { name: 'Icoon zoeken' })).toHaveFocus())
|
|
490
|
+
await userEvent.click(page.getByRole('tab', { name: 'Natuur' }))
|
|
491
|
+
await userEvent.keyboard('{Escape}')
|
|
492
|
+
await closed()
|
|
493
|
+
await expect(trigger).toHaveFocus()
|
|
494
|
+
await expect(canvas.getByTestId('model')).toHaveTextContent('house-chimney / light')
|
|
495
|
+
await userEvent.keyboard('{Enter}')
|
|
496
|
+
const field = await page.findByRole('textbox', { name: 'Icoon zoeken' })
|
|
497
|
+
await waitFor(() => expect(field).toHaveFocus())
|
|
498
|
+
await expect(page.getByRole('tab', { name: 'Alle iconen' })).toHaveAttribute(
|
|
499
|
+
'aria-selected',
|
|
500
|
+
'true',
|
|
501
|
+
)
|
|
502
|
+
await userEvent.keyboard('{ArrowDown}')
|
|
503
|
+
await expect(activeCell()).toHaveAccessibleName('House chimney')
|
|
504
|
+
|
|
505
|
+
// Tab order: field → rail → grid → out, which closes
|
|
506
|
+
field.focus()
|
|
507
|
+
await userEvent.tab()
|
|
508
|
+
await expect(page.getByRole('tab', { name: 'Alle iconen' })).toHaveFocus()
|
|
509
|
+
// the rail item shows its focus ring: no `outline-style: none` left over from :focus
|
|
510
|
+
const focusedTab = page.getByRole('tab', { name: 'Alle iconen' })
|
|
511
|
+
await expect(focusedTab.matches(':focus-visible')).toBe(true)
|
|
512
|
+
await expect(getComputedStyle(focusedTab).outlineStyle).not.toBe('none')
|
|
513
|
+
await expect(getComputedStyle(focusedTab).outlineWidth).toBe('2px')
|
|
514
|
+
await userEvent.keyboard('{ArrowDown}')
|
|
515
|
+
await expect(document.activeElement).toHaveAttribute('aria-selected', 'true')
|
|
516
|
+
await userEvent.tab()
|
|
517
|
+
await expect(page.getByRole('grid')).toHaveFocus()
|
|
518
|
+
await userEvent.tab()
|
|
519
|
+
await closed()
|
|
520
|
+
await expect(canvas.getByRole('button', { name: 'Volgende veld' })).toHaveFocus()
|
|
521
|
+
},
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* The catalogue is on its way: skeletons in the rail and the grid, no spinner. The search field
|
|
526
|
+
* works already; a term typed now applies once the catalogue is in.
|
|
527
|
+
*/
|
|
528
|
+
export const Loading: Story = {
|
|
529
|
+
args: { ariaLabel: 'Symbool', icons: [], catalogueStatus: 'loading' },
|
|
530
|
+
render: renderPicker({ icon: 'house', style: 'light' }),
|
|
531
|
+
play: async ({ canvasElement }) => {
|
|
532
|
+
await openPanel(canvasElement)
|
|
533
|
+
const grid = gridOf()
|
|
534
|
+
await expect(grid).toHaveAttribute('aria-busy', 'true')
|
|
535
|
+
await expect(grid.querySelectorAll('.animate-pulse')).toHaveLength(36)
|
|
536
|
+
const rail = page.getByRole('tablist')
|
|
537
|
+
await expect(rail.querySelectorAll('.animate-pulse')).toHaveLength(8)
|
|
538
|
+
// only the skeletons stand in for the categories: none of the real ones shows yet
|
|
539
|
+
await expect(page.getAllByRole('tab').map((element) => element.textContent?.trim())).toEqual([
|
|
540
|
+
'Alle iconen',
|
|
541
|
+
])
|
|
542
|
+
await expect(page.queryByRole('progressbar')).toBeNull()
|
|
543
|
+
},
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* The app's flow: the catalogue is fetched on the first open. A term typed while it loads is
|
|
548
|
+
* applied as soon as it is in.
|
|
549
|
+
*/
|
|
550
|
+
export const LazyCatalogue: Story = {
|
|
551
|
+
args: { ariaLabel: 'Symbool' },
|
|
552
|
+
render: (args) => ({
|
|
553
|
+
components: { IconPicker },
|
|
554
|
+
setup() {
|
|
555
|
+
const value = ref<NexxtIconPickerValue | null>({ icon: 'star', style: 'light' })
|
|
556
|
+
const status = ref<'loading' | 'ready' | 'error'>('loading')
|
|
557
|
+
const icons = ref<NexxtIconPickerIcon[]>([])
|
|
558
|
+
let fetched = false
|
|
559
|
+
const onOpen = () => {
|
|
560
|
+
args.onOpen?.()
|
|
561
|
+
if (fetched) return
|
|
562
|
+
fetched = true
|
|
563
|
+
setTimeout(() => {
|
|
564
|
+
icons.value = ICONS
|
|
565
|
+
status.value = 'ready'
|
|
566
|
+
}, 800)
|
|
567
|
+
}
|
|
568
|
+
return { args, value, status, icons, onOpen }
|
|
569
|
+
},
|
|
570
|
+
template: `
|
|
571
|
+
<div class="w-78">
|
|
572
|
+
<IconPicker v-bind="args" v-model="value" :icons="icons" :catalogue-status="status" @open="onOpen" />
|
|
573
|
+
</div>
|
|
574
|
+
`,
|
|
575
|
+
}),
|
|
576
|
+
play: async ({ canvasElement }) => {
|
|
577
|
+
const input = await openPanel(canvasElement)
|
|
578
|
+
await userEvent.type(input, 'truck')
|
|
579
|
+
await waitFor(() => expect(page.getByRole('gridcell', { name: 'Truck' })).toBeInTheDocument())
|
|
580
|
+
await expect(page.getAllByRole('gridcell')).toHaveLength(1)
|
|
581
|
+
},
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* NexxtButton's link variant is cornflower-blue-600 on white, 4.31:1: a known violation of the
|
|
586
|
+
* button itself, not of this picker. Kept out of the gate so the rest of the panel is checked.
|
|
587
|
+
*/
|
|
588
|
+
const LINK_BUTTON_EXEMPT = {
|
|
589
|
+
a11y: { test: 'error', context: { exclude: ['[data-variant="link"]'] } },
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* The catalogue failed: the current icon stays the one choosable cell, with a notice and
|
|
594
|
+
* "Opnieuw proberen" under the grid. The consumer fetches again on `@retry`.
|
|
595
|
+
*/
|
|
596
|
+
export const CatalogueError: Story = {
|
|
597
|
+
args: { ariaLabel: 'Symbool', icons: [], catalogueStatus: 'error' },
|
|
598
|
+
parameters: LINK_BUTTON_EXEMPT,
|
|
599
|
+
render: renderPicker({ icon: 'house-chimney', style: 'solid' }),
|
|
600
|
+
play: async ({ canvasElement, args }) => {
|
|
601
|
+
await openPanel(canvasElement)
|
|
602
|
+
const cells = page.getAllByRole('gridcell')
|
|
603
|
+
await expect(cells).toHaveLength(1)
|
|
604
|
+
await expect(cells[0]).toHaveAccessibleName('House chimney')
|
|
605
|
+
await expect(cells[0]).toHaveAttribute('aria-selected', 'true')
|
|
606
|
+
|
|
607
|
+
const retry = page.getByRole('button', { name: 'Opnieuw proberen' })
|
|
608
|
+
await expect(retry.closest('[role="status"]')).toHaveTextContent(
|
|
609
|
+
'De iconen konden niet worden geladen.',
|
|
610
|
+
)
|
|
611
|
+
// after the grid in the tab order
|
|
612
|
+
gridOf().focus()
|
|
613
|
+
await userEvent.tab()
|
|
614
|
+
await expect(retry).toHaveFocus()
|
|
615
|
+
await userEvent.keyboard('{Enter}')
|
|
616
|
+
await expect(args.onRetry).toHaveBeenCalledOnce()
|
|
617
|
+
|
|
618
|
+
// the current icon can still be chosen
|
|
619
|
+
gridOf().focus()
|
|
620
|
+
await userEvent.keyboard('{Enter}')
|
|
621
|
+
await closed()
|
|
622
|
+
await expect(triggerOf(canvasElement)).toHaveFocus()
|
|
623
|
+
await openPanel(canvasElement)
|
|
624
|
+
},
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** Nothing matches: the empty state, with a button that clears the search. No grid to control. */
|
|
628
|
+
export const NoResults: Story = {
|
|
629
|
+
args: { ariaLabel: 'Symbool' },
|
|
630
|
+
parameters: LINK_BUTTON_EXEMPT,
|
|
631
|
+
render: renderPicker({ icon: 'house', style: 'light' }),
|
|
632
|
+
play: async ({ canvasElement }) => {
|
|
633
|
+
const input = await openPanel(canvasElement)
|
|
634
|
+
await userEvent.type(input, 'huis')
|
|
635
|
+
await waitFor(() => expect(page.getByText('Geen iconen gevonden')).toBeVisible())
|
|
636
|
+
await waitFor(() =>
|
|
637
|
+
expect(
|
|
638
|
+
page.getByText('Niets voor ‘huis’. Zoeken werkt alleen met Engelse woorden.'),
|
|
639
|
+
).toBeVisible(),
|
|
640
|
+
)
|
|
641
|
+
await expect(input).not.toHaveAttribute('aria-controls')
|
|
642
|
+
|
|
643
|
+
await userEvent.click(page.getByRole('button', { name: 'Zoekopdracht wissen' }))
|
|
644
|
+
await expect(input).toHaveValue('')
|
|
645
|
+
await expect(input).toHaveFocus()
|
|
646
|
+
await userEvent.type(input, 'huis')
|
|
647
|
+
},
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Next to a side panel: the panel opens to the left of the trigger, `sideOffset` away. Where the
|
|
652
|
+
* room on the left is less than the panel plus an 8px margin, it opens below, right aligned.
|
|
653
|
+
*/
|
|
654
|
+
export const SideLeft: Story = {
|
|
655
|
+
args: { ariaLabel: 'Symbool', side: 'left', sideOffset: 37, pinned: PINNED },
|
|
656
|
+
render: (args) => ({
|
|
657
|
+
components: { IconPicker },
|
|
658
|
+
setup() {
|
|
659
|
+
const value = ref<NexxtIconPickerValue | null>({ icon: 'star', style: 'light' })
|
|
660
|
+
return { args, value }
|
|
661
|
+
},
|
|
662
|
+
template: `
|
|
663
|
+
<div class="flex justify-end">
|
|
664
|
+
<div class="w-78 border-l border-gray-200 pl-7">
|
|
665
|
+
<IconPicker v-bind="args" v-model="value" />
|
|
666
|
+
</div>
|
|
667
|
+
</div>
|
|
668
|
+
`,
|
|
669
|
+
}),
|
|
670
|
+
play: async ({ canvasElement }) => {
|
|
671
|
+
const trigger = triggerOf(canvasElement)
|
|
672
|
+
await openPanel(canvasElement)
|
|
673
|
+
const grid = gridOf()
|
|
674
|
+
await settled(grid)
|
|
675
|
+
const surface = page.getByRole('group', { name: 'Symbool' }).parentElement!
|
|
676
|
+
const panel = surface.getBoundingClientRect()
|
|
677
|
+
const anchor = trigger.getBoundingClientRect()
|
|
678
|
+
|
|
679
|
+
if (anchor.left - 37 >= 576 + 8) {
|
|
680
|
+
await expect(Math.round(anchor.left - panel.right)).toBe(37)
|
|
681
|
+
await expect(Math.round(panel.top)).toBe(Math.round(anchor.top))
|
|
682
|
+
} else {
|
|
683
|
+
await expect(Math.round(panel.top - anchor.bottom)).toBe(8)
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const SIZE = 4317
|
|
689
|
+
|
|
690
|
+
/** The full kit is about this size: the grid renders only the rows in view, never all of them. */
|
|
691
|
+
const LARGE: NexxtIconPickerIcon[] = Array.from({ length: SIZE }, (_, index) => {
|
|
692
|
+
const base = ICONS[index % (ICONS.length - 1)]!
|
|
693
|
+
return {
|
|
694
|
+
name: `${base.name}-${index}`,
|
|
695
|
+
label: `${base.label} ${String(index).padStart(4, '0')}`,
|
|
696
|
+
terms: base.terms,
|
|
697
|
+
styles: base.styles,
|
|
698
|
+
}
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
const LARGE_LABELS = LARGE.map(({ label }) => label).sort(new Intl.Collator('nl').compare)
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* 4317 icons, as many as the kit. The grid is virtualised per row: whatever the scroll position,
|
|
705
|
+
* only the rows in view and three either side are in the DOM.
|
|
706
|
+
*/
|
|
707
|
+
export const LargeCatalogue: Story = {
|
|
708
|
+
args: {
|
|
709
|
+
ariaLabel: 'Symbool',
|
|
710
|
+
icons: LARGE,
|
|
711
|
+
categories: [],
|
|
712
|
+
iconClass: (name: string) => `fa-${name.replace(/-\d+$/, '')}`,
|
|
713
|
+
},
|
|
714
|
+
render: renderPicker({ icon: 'star-19', style: 'light' }),
|
|
715
|
+
play: async ({ canvasElement }) => {
|
|
716
|
+
await openPanel(canvasElement)
|
|
717
|
+
const grid = gridOf()
|
|
718
|
+
await expect(grid).toHaveAttribute('aria-rowcount', String(Math.ceil(SIZE / 6)))
|
|
719
|
+
await expect(grid.querySelectorAll('[role="gridcell"]').length).toBeLessThanOrEqual(150)
|
|
720
|
+
|
|
721
|
+
grid.scrollTop = grid.scrollHeight
|
|
722
|
+
grid.dispatchEvent(new Event('scroll'))
|
|
723
|
+
await waitFor(() =>
|
|
724
|
+
expect(page.queryByRole('gridcell', { name: LARGE_LABELS.at(-1) })).toBeInTheDocument(),
|
|
725
|
+
)
|
|
726
|
+
await expect(grid.querySelectorAll('[role="gridcell"]').length).toBeLessThanOrEqual(150)
|
|
727
|
+
|
|
728
|
+
// Ctrl+Home back to the first cell: the active descendant is always a rendered cell
|
|
729
|
+
grid.focus()
|
|
730
|
+
await userEvent.keyboard('{Control>}{Home}{/Control}')
|
|
731
|
+
await expect(activeCell()).not.toBeNull()
|
|
732
|
+
await expect(activeCell()).toHaveAccessibleName(LARGE_LABELS[0]!)
|
|
733
|
+
},
|
|
734
|
+
}
|