@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,309 @@
|
|
|
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 EditorAlignmentGrid from './EditorAlignmentGrid.vue'
|
|
5
|
+
import type {
|
|
6
|
+
EditorAlignmentDirection,
|
|
7
|
+
EditorAlignmentMode,
|
|
8
|
+
EditorAlignmentValue,
|
|
9
|
+
} from './EditorAlignmentGrid.vue'
|
|
10
|
+
import NexxtToggleGroup from '../../../molecules/ToggleGroup/ToggleGroup.vue'
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
title: 'Components/Editor Components/Molecules/Editor Alignment Grid',
|
|
14
|
+
component: EditorAlignmentGrid,
|
|
15
|
+
decorators: [() => ({ template: '<div class="w-100 mx-auto"><story /></div>' })],
|
|
16
|
+
parameters: {
|
|
17
|
+
// New and clean, so axe fails the run here instead of reporting.
|
|
18
|
+
a11y: { test: 'error' },
|
|
19
|
+
},
|
|
20
|
+
args: { onCommit: fn() },
|
|
21
|
+
argTypes: {
|
|
22
|
+
label: { control: 'text' },
|
|
23
|
+
direction: { control: 'inline-radio', options: ['row', 'column'] },
|
|
24
|
+
mode: { control: 'inline-radio', options: ['both', 'cross', 'main'] },
|
|
25
|
+
auto: { control: 'boolean' },
|
|
26
|
+
stretched: { control: 'boolean' },
|
|
27
|
+
},
|
|
28
|
+
} satisfies Meta<typeof EditorAlignmentGrid>
|
|
29
|
+
|
|
30
|
+
type Story = StoryObj<typeof EditorAlignmentGrid>
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Any computed CSS colour as sRGB channels. Tailwind v4 colours compute to
|
|
34
|
+
* `oklch()`, so the colour is painted on a 1px canvas and read back.
|
|
35
|
+
*/
|
|
36
|
+
const toRgb = (color: string) => {
|
|
37
|
+
const context = document.createElement('canvas').getContext('2d')!
|
|
38
|
+
context.fillStyle = color
|
|
39
|
+
context.fillRect(0, 0, 1, 1)
|
|
40
|
+
const [r, g, b] = context.getImageData(0, 0, 1, 1).data
|
|
41
|
+
return [r, g, b]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** WCAG 2 contrast ratio of two CSS colours. */
|
|
45
|
+
const contrast = (a: string, b: string) => {
|
|
46
|
+
const luminance = (color: string) => {
|
|
47
|
+
const [r, g, b] = toRgb(color).map((channel) => {
|
|
48
|
+
const c = channel / 255
|
|
49
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
|
|
50
|
+
})
|
|
51
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
52
|
+
}
|
|
53
|
+
const [light, dark] = [luminance(a), luminance(b)].sort((x, y) => y - x)
|
|
54
|
+
return (light + 0.05) / (dark + 0.05)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const WHITE = 'rgb(255, 255, 255)'
|
|
58
|
+
|
|
59
|
+
/** The grey tray around the radiogroup, which every mode shares. */
|
|
60
|
+
const tray = (canvasElement: HTMLElement) =>
|
|
61
|
+
canvasElement.querySelector('[role="radiogroup"]')!.parentElement as HTMLElement
|
|
62
|
+
|
|
63
|
+
/** The grid keeps its 112 px in every mode: no layout jump in the panel. */
|
|
64
|
+
const expectFixedSize = async (canvasElement: HTMLElement) => {
|
|
65
|
+
const { width, height } = tray(canvasElement).getBoundingClientRect()
|
|
66
|
+
await expect([width, height]).toEqual([112, 112])
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Renders the grid with its own model, starting at `initial`, and shows the value. */
|
|
70
|
+
const withModel =
|
|
71
|
+
(initial: EditorAlignmentValue): Story['render'] =>
|
|
72
|
+
(args) => ({
|
|
73
|
+
components: { EditorAlignmentGrid },
|
|
74
|
+
setup() {
|
|
75
|
+
const value = ref<EditorAlignmentValue>(initial)
|
|
76
|
+
return { args, value }
|
|
77
|
+
},
|
|
78
|
+
template: `
|
|
79
|
+
<div class="flex flex-col gap-2">
|
|
80
|
+
<EditorAlignmentGrid v-bind="args" v-model="value" />
|
|
81
|
+
<p class="small-normal text-gray-700">x: {{ value.x ?? 'null' }}, y: {{ value.y ?? 'null' }}</p>
|
|
82
|
+
</div>
|
|
83
|
+
`,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
/** Nine cells in a row. The plays cover the keyboard, the commit, the ring and the contrast. */
|
|
87
|
+
export const Row: Story = {
|
|
88
|
+
args: { direction: 'row' },
|
|
89
|
+
render: withModel({ x: 'start', y: 'start' }),
|
|
90
|
+
play: async ({ canvasElement, args }) => {
|
|
91
|
+
const canvas = within(canvasElement)
|
|
92
|
+
await expectFixedSize(canvasElement)
|
|
93
|
+
|
|
94
|
+
// One tab stop, on the chosen cell.
|
|
95
|
+
await userEvent.tab()
|
|
96
|
+
const first = canvas.getByRole('radio', { name: 'Linksboven' })
|
|
97
|
+
await expect(first).toHaveFocus()
|
|
98
|
+
|
|
99
|
+
// The ring is drawn outside the cell, so it lands on the grey tray, and on a
|
|
100
|
+
// white cell next to it: it needs 3:1 against both.
|
|
101
|
+
await expect(first.matches(':focus-visible')).toBe(true)
|
|
102
|
+
const ring = getComputedStyle(first)
|
|
103
|
+
await expect(ring.outlineStyle).not.toBe('none')
|
|
104
|
+
await expect(contrast(ring.outlineColor, WHITE)).toBeGreaterThanOrEqual(3)
|
|
105
|
+
await expect(
|
|
106
|
+
contrast(ring.outlineColor, getComputedStyle(tray(canvasElement)).backgroundColor),
|
|
107
|
+
).toBeGreaterThanOrEqual(3)
|
|
108
|
+
|
|
109
|
+
// Measured before any arrow: a colour read mid-transition is no colour at all.
|
|
110
|
+
// Not by colour alone: the dot holds 3:1 on the tray, the glyph on white.
|
|
111
|
+
const dot = canvas.getByRole('radio', { name: 'Rechtsonder' }).querySelector('.rounded-full')!
|
|
112
|
+
await expect(
|
|
113
|
+
contrast(
|
|
114
|
+
getComputedStyle(dot).backgroundColor,
|
|
115
|
+
getComputedStyle(tray(canvasElement)).backgroundColor,
|
|
116
|
+
),
|
|
117
|
+
).toBeGreaterThanOrEqual(3)
|
|
118
|
+
// The hover preview of an unchosen cell: 3:1 on the tray, and on white for a
|
|
119
|
+
// cell inside a stretched band.
|
|
120
|
+
const preview = canvas
|
|
121
|
+
.getByRole('radio', { name: 'Rechtsonder' })
|
|
122
|
+
.querySelector('.text-gray-700')!
|
|
123
|
+
const previewColor = getComputedStyle(preview).color
|
|
124
|
+
await expect(
|
|
125
|
+
contrast(previewColor, getComputedStyle(tray(canvasElement)).backgroundColor),
|
|
126
|
+
).toBeGreaterThanOrEqual(3)
|
|
127
|
+
await expect(contrast(previewColor, WHITE)).toBeGreaterThanOrEqual(3)
|
|
128
|
+
const glyph = first.querySelector('.text-cornflower-blue-600')!
|
|
129
|
+
await expect(
|
|
130
|
+
contrast(getComputedStyle(glyph).color, getComputedStyle(first).backgroundColor),
|
|
131
|
+
).toBeGreaterThanOrEqual(3)
|
|
132
|
+
|
|
133
|
+
// Arrows choose at once and stop at the edge.
|
|
134
|
+
await userEvent.keyboard('{ArrowLeft}{ArrowRight}{ArrowDown}')
|
|
135
|
+
const middle = canvas.getByRole('radio', { name: 'Midden' })
|
|
136
|
+
await expect(middle).toHaveFocus()
|
|
137
|
+
await expect(middle).toHaveAttribute('aria-checked', 'true')
|
|
138
|
+
await expect(args.onCommit).not.toHaveBeenCalled()
|
|
139
|
+
|
|
140
|
+
// Leaving the grid ends the keyboard run: one commit.
|
|
141
|
+
await userEvent.tab()
|
|
142
|
+
await expect(args.onCommit).toHaveBeenCalledOnce()
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Nine cells in a column: the glyph lies down. A click commits straight away. */
|
|
147
|
+
export const Column: Story = {
|
|
148
|
+
args: { direction: 'column' },
|
|
149
|
+
render: withModel({ x: 'end', y: 'start' }),
|
|
150
|
+
play: async ({ canvasElement, args }) => {
|
|
151
|
+
await expectFixedSize(canvasElement)
|
|
152
|
+
await userEvent.click(within(canvasElement).getByRole('radio', { name: 'Midden onder' }))
|
|
153
|
+
await expect(args.onCommit).toHaveBeenCalledOnce()
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Auto in a row: three horizontal bands, the bars spread out. */
|
|
158
|
+
export const AutoRow: Story = {
|
|
159
|
+
args: { direction: 'row', mode: 'cross', auto: true },
|
|
160
|
+
render: withModel({ x: null, y: 'center' }),
|
|
161
|
+
play: async ({ canvasElement }) => {
|
|
162
|
+
const canvas = within(canvasElement)
|
|
163
|
+
await expectFixedSize(canvasElement)
|
|
164
|
+
|
|
165
|
+
await userEvent.tab()
|
|
166
|
+
// Only the arrows along the bands do anything.
|
|
167
|
+
await userEvent.keyboard('{ArrowRight}')
|
|
168
|
+
await expect(canvas.getByRole('radio', { name: 'Midden' })).toHaveAttribute(
|
|
169
|
+
'aria-checked',
|
|
170
|
+
'true',
|
|
171
|
+
)
|
|
172
|
+
await userEvent.keyboard('{ArrowDown}')
|
|
173
|
+
await expect(canvas.getByRole('radio', { name: 'Onder' })).toHaveFocus()
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Auto in a column: three vertical bands. */
|
|
178
|
+
export const AutoColumn: Story = {
|
|
179
|
+
args: { direction: 'column', mode: 'cross', auto: true },
|
|
180
|
+
render: withModel({ x: 'end', y: null }),
|
|
181
|
+
play: async ({ canvasElement }) => expectFixedSize(canvasElement),
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The main axis has no effect in a row: horizontal bands, the bars packed. */
|
|
185
|
+
export const CrossOnlyRow: Story = {
|
|
186
|
+
args: { direction: 'row', mode: 'cross' },
|
|
187
|
+
render: withModel({ x: null, y: 'start' }),
|
|
188
|
+
play: async ({ canvasElement }) => expectFixedSize(canvasElement),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The cross axis has no effect in a row: vertical bands for the main-axis place. */
|
|
192
|
+
export const MainOnlyRow: Story = {
|
|
193
|
+
args: { direction: 'row', mode: 'main' },
|
|
194
|
+
render: withModel({ x: 'center', y: null }),
|
|
195
|
+
play: async ({ canvasElement }) => expectFixedSize(canvasElement),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The cross axis has no effect in a column: horizontal bands for the main-axis place. */
|
|
199
|
+
export const MainOnlyColumn: Story = {
|
|
200
|
+
args: { direction: 'column', mode: 'main' },
|
|
201
|
+
render: withModel({ x: null, y: 'end' }),
|
|
202
|
+
play: async ({ canvasElement }) => expectFixedSize(canvasElement),
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Legacy stretch: no radio checked, a band with long bars at the main-axis place. */
|
|
206
|
+
export const Stretched: Story = {
|
|
207
|
+
args: { direction: 'row', stretched: true },
|
|
208
|
+
render: withModel({ x: 'center', y: 'start' }),
|
|
209
|
+
play: async ({ canvasElement }) => {
|
|
210
|
+
const canvas = within(canvasElement)
|
|
211
|
+
await expectFixedSize(canvasElement)
|
|
212
|
+
|
|
213
|
+
for (const radio of canvas.getAllByRole('radio')) {
|
|
214
|
+
await expect(radio).toHaveAttribute('aria-checked', 'false')
|
|
215
|
+
}
|
|
216
|
+
await userEvent.tab()
|
|
217
|
+
await expect(canvas.getByRole('radio', { name: 'Midden' })).toHaveFocus()
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Legacy stretch with Auto: all three bands light up. */
|
|
222
|
+
export const StretchedAuto: Story = {
|
|
223
|
+
args: { direction: 'row', mode: 'cross', auto: true, stretched: true },
|
|
224
|
+
render: withModel({ x: 'start', y: 'center' }),
|
|
225
|
+
play: async ({ canvasElement }) => expectFixedSize(canvasElement),
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Switched from outside while the focus is in the grid, as the app does when Auto flips. */
|
|
229
|
+
const switchedMode = ref<EditorAlignmentMode>('both')
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* A mode switch while the focus is in the grid: the focus moves to the tab stop
|
|
233
|
+
* of the new layer instead of dropping to `body`, and the keyboard run goes on
|
|
234
|
+
* until the focus really leaves.
|
|
235
|
+
*/
|
|
236
|
+
export const SwitchWhileFocused: Story = {
|
|
237
|
+
render: (args) => ({
|
|
238
|
+
components: { EditorAlignmentGrid },
|
|
239
|
+
setup() {
|
|
240
|
+
switchedMode.value = 'both'
|
|
241
|
+
const value = ref<EditorAlignmentValue>({ x: 'start', y: 'start' })
|
|
242
|
+
return { args, value, mode: switchedMode }
|
|
243
|
+
},
|
|
244
|
+
template: `<EditorAlignmentGrid v-bind="args" v-model="value" :mode="mode" />`,
|
|
245
|
+
}),
|
|
246
|
+
play: async ({ canvasElement, args }) => {
|
|
247
|
+
const canvas = within(canvasElement)
|
|
248
|
+
|
|
249
|
+
await userEvent.tab()
|
|
250
|
+
await userEvent.keyboard('{ArrowDown}')
|
|
251
|
+
switchedMode.value = 'cross'
|
|
252
|
+
|
|
253
|
+
await waitFor(() => expect(canvas.getByRole('radio', { name: 'Midden' })).toHaveFocus())
|
|
254
|
+
await expect(document.activeElement).not.toBe(document.body)
|
|
255
|
+
await expect(args.onCommit).not.toHaveBeenCalled()
|
|
256
|
+
|
|
257
|
+
// Past the 150 ms crossfade: the old layer is gone and the focus held.
|
|
258
|
+
await new Promise((resolve) => setTimeout(resolve, 300))
|
|
259
|
+
await expect(canvas.getByRole('radio', { name: 'Midden' })).toHaveFocus()
|
|
260
|
+
await expect(args.onCommit).not.toHaveBeenCalled()
|
|
261
|
+
|
|
262
|
+
await userEvent.tab()
|
|
263
|
+
await expect(args.onCommit).toHaveBeenCalledOnce()
|
|
264
|
+
},
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Switch mode and direction to see the crossfade. The size stays the same
|
|
269
|
+
* through every switch.
|
|
270
|
+
*/
|
|
271
|
+
export const Playground: Story = {
|
|
272
|
+
parameters: {
|
|
273
|
+
// The demo switches are labelled NexxtToggleGroups, whose chosen label fails
|
|
274
|
+
// color-contrast on its own (cornflower-600 text, 4.31:1). Not this grid.
|
|
275
|
+
a11y: { test: 'error', context: { exclude: ['[data-demo-controls]'] } },
|
|
276
|
+
},
|
|
277
|
+
render: (args) => ({
|
|
278
|
+
components: { EditorAlignmentGrid, NexxtToggleGroup },
|
|
279
|
+
setup() {
|
|
280
|
+
const value = ref<EditorAlignmentValue>({ x: 'start', y: 'start' })
|
|
281
|
+
const direction = ref<EditorAlignmentDirection>('row')
|
|
282
|
+
const mode = ref<EditorAlignmentMode>('both')
|
|
283
|
+
const auto = ref('off')
|
|
284
|
+
return { args, value, direction, mode, auto }
|
|
285
|
+
},
|
|
286
|
+
template: `
|
|
287
|
+
<div class="flex flex-col gap-3">
|
|
288
|
+
<div data-demo-controls class="flex flex-col gap-3">
|
|
289
|
+
<NexxtToggleGroup v-model="direction" aria-label="Richting" :options="[{ value: 'row', label: 'Rij' }, { value: 'column', label: 'Kolom' }]" />
|
|
290
|
+
<NexxtToggleGroup v-model="mode" aria-label="Modus" :options="[{ value: 'both', label: '9' }, { value: 'cross', label: 'Dwarsas' }, { value: 'main', label: 'Hoofdas' }]" />
|
|
291
|
+
<NexxtToggleGroup v-model="auto" aria-label="Auto" :options="[{ value: 'off', label: 'Vast' }, { value: 'on', label: 'Auto' }]" />
|
|
292
|
+
</div>
|
|
293
|
+
<EditorAlignmentGrid
|
|
294
|
+
v-bind="args"
|
|
295
|
+
v-model="value"
|
|
296
|
+
:direction="direction"
|
|
297
|
+
:mode="mode"
|
|
298
|
+
:auto="auto === 'on'"
|
|
299
|
+
/>
|
|
300
|
+
</div>
|
|
301
|
+
`,
|
|
302
|
+
}),
|
|
303
|
+
play: async ({ canvasElement }) => {
|
|
304
|
+
const canvas = within(canvasElement)
|
|
305
|
+
await userEvent.click(canvas.getByRole('button', { name: 'Dwarsas' }))
|
|
306
|
+
await userEvent.click(canvas.getByRole('button', { name: 'Kolom' }))
|
|
307
|
+
await expectFixedSize(canvasElement)
|
|
308
|
+
},
|
|
309
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest'
|
|
2
|
+
import { mount, type VueWrapper } from '@vue/test-utils'
|
|
3
|
+
import { nextTick } from 'vue'
|
|
4
|
+
import EditorAlignmentGrid, {
|
|
5
|
+
EDITOR_ALIGNMENT_POSITIONS,
|
|
6
|
+
type EditorAlignmentValue,
|
|
7
|
+
} from './EditorAlignmentGrid.vue'
|
|
8
|
+
|
|
9
|
+
const CELL_NAMES = [
|
|
10
|
+
'Linksboven',
|
|
11
|
+
'Midden boven',
|
|
12
|
+
'Rechtsboven',
|
|
13
|
+
'Links midden',
|
|
14
|
+
'Midden',
|
|
15
|
+
'Rechts midden',
|
|
16
|
+
'Linksonder',
|
|
17
|
+
'Midden onder',
|
|
18
|
+
'Rechtsonder',
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
const mounted: VueWrapper[] = []
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
mounted.splice(0).forEach((wrapper) => wrapper.unmount())
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
/** A parent that owns the model, as `v-model` does. Attached, so focus can be read. */
|
|
28
|
+
const mountGrid = (
|
|
29
|
+
modelValue: EditorAlignmentValue,
|
|
30
|
+
props: Record<string, unknown> = {},
|
|
31
|
+
attrs: Record<string, unknown> = {},
|
|
32
|
+
) => {
|
|
33
|
+
const wrapper = mount(EditorAlignmentGrid, {
|
|
34
|
+
props: {
|
|
35
|
+
modelValue,
|
|
36
|
+
'onUpdate:modelValue': (value: EditorAlignmentValue) =>
|
|
37
|
+
wrapper.setProps({ modelValue: value }),
|
|
38
|
+
...props,
|
|
39
|
+
},
|
|
40
|
+
attrs,
|
|
41
|
+
attachTo: document.body,
|
|
42
|
+
})
|
|
43
|
+
mounted.push(wrapper)
|
|
44
|
+
return wrapper
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const radios = (wrapper: VueWrapper) => wrapper.findAll('[role="radio"]')
|
|
48
|
+
const names = (wrapper: VueWrapper) =>
|
|
49
|
+
radios(wrapper).map((radio) => radio.attributes('aria-label'))
|
|
50
|
+
const checked = (wrapper: VueWrapper) =>
|
|
51
|
+
radios(wrapper)
|
|
52
|
+
.filter((radio) => radio.attributes('aria-checked') === 'true')
|
|
53
|
+
.map((radio) => radio.attributes('aria-label'))
|
|
54
|
+
const tabStops = (wrapper: VueWrapper) =>
|
|
55
|
+
radios(wrapper)
|
|
56
|
+
.filter((radio) => radio.attributes('tabindex') === '0')
|
|
57
|
+
.map((radio) => radio.attributes('aria-label'))
|
|
58
|
+
|
|
59
|
+
/** The readout is the last hidden span: it follows the grid. */
|
|
60
|
+
const readout = (wrapper: VueWrapper) => wrapper.findAll('span[aria-hidden="true"]').at(-1)!.text()
|
|
61
|
+
|
|
62
|
+
const radio = (wrapper: VueWrapper, name: string) => {
|
|
63
|
+
const found = radios(wrapper).find((candidate) => candidate.attributes('aria-label') === name)
|
|
64
|
+
if (!found) throw new Error(`No radio "${name}"`)
|
|
65
|
+
return found
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const press = async (wrapper: VueWrapper, key: string) => {
|
|
69
|
+
const focused = radios(wrapper).find((candidate) => candidate.element === document.activeElement)
|
|
70
|
+
if (!focused) throw new Error('No radio has focus')
|
|
71
|
+
await focused.trigger('keydown', { key })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const focusedName = () => (document.activeElement as HTMLElement).getAttribute('aria-label')
|
|
75
|
+
|
|
76
|
+
const TOP_LEFT = { x: 'start', y: 'start' } as const
|
|
77
|
+
|
|
78
|
+
describe('EditorAlignmentGrid', () => {
|
|
79
|
+
describe('nine cells', () => {
|
|
80
|
+
it('renders a radiogroup named by the block label, with nine named radios', () => {
|
|
81
|
+
const wrapper = mountGrid(TOP_LEFT)
|
|
82
|
+
|
|
83
|
+
expect(wrapper.find('p').text()).toBe('Uitlijning')
|
|
84
|
+
expect(wrapper.get('[role="radiogroup"]').attributes('aria-label')).toBe('Uitlijning')
|
|
85
|
+
expect(names(wrapper)).toEqual(CELL_NAMES)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('puts attrs on the radiogroup', () => {
|
|
89
|
+
const wrapper = mountGrid(TOP_LEFT, {}, { 'aria-describedby': 'hint' })
|
|
90
|
+
|
|
91
|
+
expect(wrapper.get('[role="radiogroup"]').attributes('aria-describedby')).toBe('hint')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('checks the cell of the model and puts the only tab stop there', () => {
|
|
95
|
+
const wrapper = mountGrid({ x: 'end', y: 'center' })
|
|
96
|
+
|
|
97
|
+
expect(checked(wrapper)).toEqual(['Rechts midden'])
|
|
98
|
+
expect(tabStops(wrapper)).toEqual(['Rechts midden'])
|
|
99
|
+
expect(readout(wrapper)).toBe('Rechts midden')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('emits the visual place of every cell on click, and commits', async () => {
|
|
103
|
+
for (const [index, name] of CELL_NAMES.entries()) {
|
|
104
|
+
const wrapper = mountGrid({ x: null, y: null })
|
|
105
|
+
|
|
106
|
+
await radio(wrapper, name).trigger('click')
|
|
107
|
+
|
|
108
|
+
expect(wrapper.emitted('update:modelValue')).toEqual([
|
|
109
|
+
[
|
|
110
|
+
{
|
|
111
|
+
x: EDITOR_ALIGNMENT_POSITIONS[index % 3],
|
|
112
|
+
y: EDITOR_ALIGNMENT_POSITIONS[Math.floor(index / 3)],
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
])
|
|
116
|
+
expect(wrapper.emitted('commit')).toHaveLength(1)
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('emits and commits nothing for the cell that is already chosen', async () => {
|
|
121
|
+
const wrapper = mountGrid({ x: 'center', y: 'center' })
|
|
122
|
+
|
|
123
|
+
await radio(wrapper, 'Midden').trigger('click')
|
|
124
|
+
|
|
125
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
126
|
+
expect(wrapper.emitted('commit')).toBeUndefined()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// The glyph is the drawing of the content: standing bars side by side in a
|
|
130
|
+
// row, lying bars stacked in a column, lined up on the cross-axis place.
|
|
131
|
+
it('draws standing bars on the vertical place in a row', () => {
|
|
132
|
+
const wrapper = mountGrid({ x: 'center', y: 'end' }, { direction: 'row' })
|
|
133
|
+
const glyph = radio(wrapper, 'Midden onder').get('.gap-0\\.5')
|
|
134
|
+
|
|
135
|
+
expect(glyph.classes()).toContain('items-end')
|
|
136
|
+
expect(glyph.findAll('span').every((bar) => bar.classes().includes('w-1'))).toBe(true)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('draws lying bars on the horizontal place in a column', () => {
|
|
140
|
+
const wrapper = mountGrid({ x: 'end', y: 'start' }, { direction: 'column' })
|
|
141
|
+
const glyph = radio(wrapper, 'Rechtsboven').get('.gap-0\\.5')
|
|
142
|
+
|
|
143
|
+
expect(glyph.classes()).toEqual(expect.arrayContaining(['flex-col', 'items-end']))
|
|
144
|
+
expect(glyph.findAll('span').every((bar) => bar.classes().includes('h-1'))).toBe(true)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('replaces only the overridden message keys', () => {
|
|
148
|
+
const wrapper = mountGrid(TOP_LEFT, { messages: { center: 'Centre', topLeft: 'TL' } })
|
|
149
|
+
|
|
150
|
+
expect(names(wrapper)).toEqual([
|
|
151
|
+
'TL',
|
|
152
|
+
...CELL_NAMES.slice(1, 4),
|
|
153
|
+
'Centre',
|
|
154
|
+
...CELL_NAMES.slice(5),
|
|
155
|
+
])
|
|
156
|
+
expect(readout(wrapper)).toBe('TL')
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('bands', () => {
|
|
161
|
+
it.each([
|
|
162
|
+
['row', 'cross', ['Boven', 'Midden', 'Onder'], { x: null, y: 'end' }],
|
|
163
|
+
['row', 'main', ['Links', 'Midden', 'Rechts'], { x: 'end', y: null }],
|
|
164
|
+
['column', 'cross', ['Links', 'Midden', 'Rechts'], { x: 'end', y: null }],
|
|
165
|
+
['column', 'main', ['Boven', 'Midden', 'Onder'], { x: null, y: 'end' }],
|
|
166
|
+
] as const)(
|
|
167
|
+
'%s with %s: three named bands, the other axis null',
|
|
168
|
+
async (direction, mode, bands, emitted) => {
|
|
169
|
+
const wrapper = mountGrid({ x: 'center', y: 'center' }, { direction, mode })
|
|
170
|
+
|
|
171
|
+
expect(names(wrapper)).toEqual(bands)
|
|
172
|
+
expect(checked(wrapper)).toEqual(['Midden'])
|
|
173
|
+
|
|
174
|
+
await radio(wrapper, bands[2]).trigger('click')
|
|
175
|
+
|
|
176
|
+
expect(wrapper.emitted('update:modelValue')).toEqual([[emitted]])
|
|
177
|
+
expect(wrapper.emitted('commit')).toHaveLength(1)
|
|
178
|
+
},
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
it('spans each band over three cells, along the main axis in cross mode', () => {
|
|
182
|
+
const row = mountGrid(TOP_LEFT, { direction: 'row', mode: 'cross' })
|
|
183
|
+
const column = mountGrid(TOP_LEFT, { direction: 'column', mode: 'cross' })
|
|
184
|
+
|
|
185
|
+
radios(row).forEach((band) => expect(band.classes()).toContain('col-span-3'))
|
|
186
|
+
radios(column).forEach((band) => expect(band.classes()).toContain('row-span-3'))
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('reads out the band, with Auto when the main axis distributes', () => {
|
|
190
|
+
const plain = mountGrid({ x: null, y: 'center' }, { mode: 'cross' })
|
|
191
|
+
const auto = mountGrid({ x: null, y: 'center' }, { mode: 'cross', auto: true })
|
|
192
|
+
|
|
193
|
+
expect(readout(plain)).toBe('Midden')
|
|
194
|
+
expect(readout(auto)).toBe('Auto · Midden')
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('spreads the bars of the chosen band with Auto, packs them without', () => {
|
|
198
|
+
const packed = mountGrid({ x: null, y: 'start' }, { mode: 'cross' })
|
|
199
|
+
const spread = mountGrid({ x: null, y: 'start' }, { mode: 'cross', auto: true })
|
|
200
|
+
|
|
201
|
+
expect(radio(packed, 'Boven').get('.gap-0\\.5').classes()).not.toContain('justify-between')
|
|
202
|
+
expect(radio(spread, 'Boven').get('.gap-0\\.5').classes()).toContain('justify-between')
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('draws three spread dots on an unchosen band with Auto, one without', () => {
|
|
206
|
+
const packed = mountGrid({ x: null, y: 'start' }, { mode: 'cross' })
|
|
207
|
+
const spread = mountGrid({ x: null, y: 'start' }, { mode: 'cross', auto: true })
|
|
208
|
+
const dots = (wrapper: VueWrapper) => radio(wrapper, 'Onder').findAll('.bg-gray-700').length
|
|
209
|
+
|
|
210
|
+
expect(dots(packed)).toBe(1)
|
|
211
|
+
expect(dots(spread)).toBe(3)
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
describe('stretched', () => {
|
|
216
|
+
it('checks nothing and reads out the main-axis place', () => {
|
|
217
|
+
const wrapper = mountGrid({ x: 'end', y: 'start' }, { stretched: true })
|
|
218
|
+
|
|
219
|
+
expect(checked(wrapper)).toEqual([])
|
|
220
|
+
expect(readout(wrapper)).toBe('Uitgerekt · Rechts')
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('puts the tab stop on the main-axis place × the middle', () => {
|
|
224
|
+
const row = mountGrid({ x: 'end', y: 'start' }, { stretched: true, direction: 'row' })
|
|
225
|
+
const column = mountGrid({ x: 'start', y: 'end' }, { stretched: true, direction: 'column' })
|
|
226
|
+
|
|
227
|
+
expect(tabStops(row)).toEqual(['Rechts midden'])
|
|
228
|
+
expect(tabStops(column)).toEqual(['Midden onder'])
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('reads out Auto as the place with Auto, and lights all three bands', () => {
|
|
232
|
+
const wrapper = mountGrid(
|
|
233
|
+
{ x: 'start', y: 'center' },
|
|
234
|
+
{ stretched: true, mode: 'cross', auto: true },
|
|
235
|
+
)
|
|
236
|
+
const layer = wrapper.get('[aria-hidden="true"].pointer-events-none')
|
|
237
|
+
|
|
238
|
+
expect(readout(wrapper)).toBe('Uitgerekt · Auto')
|
|
239
|
+
expect(layer.findAll('.bg-white')).toHaveLength(3)
|
|
240
|
+
expect(tabStops(wrapper)).toEqual(['Midden'])
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('draws one hidden band in nine cells, and the cells stay clickable', async () => {
|
|
244
|
+
const wrapper = mountGrid({ x: 'center', y: 'start' }, { stretched: true })
|
|
245
|
+
const layer = wrapper.get('[aria-hidden="true"].pointer-events-none')
|
|
246
|
+
|
|
247
|
+
expect(layer.findAll('.bg-white')).toHaveLength(1)
|
|
248
|
+
|
|
249
|
+
await radio(wrapper, 'Midden boven').trigger('click')
|
|
250
|
+
|
|
251
|
+
expect(wrapper.emitted('update:modelValue')).toEqual([[{ x: 'center', y: 'start' }]])
|
|
252
|
+
expect(wrapper.emitted('commit')).toHaveLength(1)
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
describe('keyboard', () => {
|
|
257
|
+
it('moves and chooses in two dimensions without wrapping', async () => {
|
|
258
|
+
const wrapper = mountGrid(TOP_LEFT)
|
|
259
|
+
;(radio(wrapper, 'Linksboven').element as HTMLElement).focus()
|
|
260
|
+
|
|
261
|
+
await press(wrapper, 'ArrowLeft')
|
|
262
|
+
await press(wrapper, 'ArrowUp')
|
|
263
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
264
|
+
|
|
265
|
+
await press(wrapper, 'ArrowRight')
|
|
266
|
+
await press(wrapper, 'ArrowDown')
|
|
267
|
+
|
|
268
|
+
expect(checked(wrapper)).toEqual(['Midden'])
|
|
269
|
+
expect(focusedName()).toBe('Midden')
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('jumps to the first and last cell of the row on Home and End', async () => {
|
|
273
|
+
const wrapper = mountGrid({ x: 'center', y: 'center' })
|
|
274
|
+
;(radio(wrapper, 'Midden').element as HTMLElement).focus()
|
|
275
|
+
|
|
276
|
+
await press(wrapper, 'End')
|
|
277
|
+
expect(focusedName()).toBe('Rechts midden')
|
|
278
|
+
await press(wrapper, 'Home')
|
|
279
|
+
expect(focusedName()).toBe('Links midden')
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('only moves along the bands in a band mode', async () => {
|
|
283
|
+
const wrapper = mountGrid({ x: null, y: 'start' }, { mode: 'cross', direction: 'row' })
|
|
284
|
+
;(radio(wrapper, 'Boven').element as HTMLElement).focus()
|
|
285
|
+
|
|
286
|
+
await press(wrapper, 'ArrowRight')
|
|
287
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
288
|
+
|
|
289
|
+
await press(wrapper, 'ArrowDown')
|
|
290
|
+
expect(checked(wrapper)).toEqual(['Midden'])
|
|
291
|
+
expect(focusedName()).toBe('Midden')
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('commits once for arrows followed by a click, not again on leaving', async () => {
|
|
295
|
+
const wrapper = mountGrid(TOP_LEFT)
|
|
296
|
+
const outside = document.createElement('button')
|
|
297
|
+
document.body.append(outside)
|
|
298
|
+
;(radio(wrapper, 'Linksboven').element as HTMLElement).focus()
|
|
299
|
+
|
|
300
|
+
await press(wrapper, 'ArrowRight')
|
|
301
|
+
await radio(wrapper, 'Rechtsonder').trigger('click')
|
|
302
|
+
outside.focus()
|
|
303
|
+
|
|
304
|
+
expect(wrapper.emitted('commit')).toHaveLength(1)
|
|
305
|
+
outside.remove()
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('commits nothing when the focus passes through without a key', () => {
|
|
309
|
+
const wrapper = mountGrid(TOP_LEFT)
|
|
310
|
+
const outside = document.createElement('button')
|
|
311
|
+
document.body.append(outside)
|
|
312
|
+
|
|
313
|
+
;(radio(wrapper, 'Linksboven').element as HTMLElement).focus()
|
|
314
|
+
outside.focus()
|
|
315
|
+
|
|
316
|
+
expect(wrapper.emitted('commit')).toBeUndefined()
|
|
317
|
+
outside.remove()
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
// The app switches the mode while the user is in the grid (a direction
|
|
321
|
+
// change elsewhere, Auto through the keyboard in a test): the focus moves to
|
|
322
|
+
// the new layer, and the run only ends when the focus really leaves.
|
|
323
|
+
it('keeps the focus in the grid across a mode switch, and commits only on leaving', async () => {
|
|
324
|
+
const wrapper = mountGrid({ x: 'start', y: 'start' })
|
|
325
|
+
const outside = document.createElement('button')
|
|
326
|
+
document.body.append(outside)
|
|
327
|
+
;(radio(wrapper, 'Linksboven').element as HTMLElement).focus()
|
|
328
|
+
|
|
329
|
+
await press(wrapper, 'ArrowDown')
|
|
330
|
+
await wrapper.setProps({ mode: 'cross' })
|
|
331
|
+
await nextTick()
|
|
332
|
+
|
|
333
|
+
expect(document.activeElement).not.toBe(document.body)
|
|
334
|
+
expect(document.activeElement?.getAttribute('role')).toBe('radio')
|
|
335
|
+
expect(focusedName()).toBe('Midden')
|
|
336
|
+
expect(wrapper.emitted('commit')).toBeUndefined()
|
|
337
|
+
|
|
338
|
+
outside.focus()
|
|
339
|
+
expect(wrapper.emitted('commit')).toHaveLength(1)
|
|
340
|
+
outside.remove()
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('commits once, when the focus leaves after a run of arrows', async () => {
|
|
344
|
+
const wrapper = mountGrid(TOP_LEFT)
|
|
345
|
+
const outside = document.createElement('button')
|
|
346
|
+
document.body.append(outside)
|
|
347
|
+
;(radio(wrapper, 'Linksboven').element as HTMLElement).focus()
|
|
348
|
+
|
|
349
|
+
await press(wrapper, 'ArrowRight')
|
|
350
|
+
await press(wrapper, 'ArrowRight')
|
|
351
|
+
expect(wrapper.emitted('update:modelValue')).toHaveLength(2)
|
|
352
|
+
expect(wrapper.emitted('commit')).toBeUndefined()
|
|
353
|
+
|
|
354
|
+
outside.focus()
|
|
355
|
+
expect(wrapper.emitted('commit')).toHaveLength(1)
|
|
356
|
+
outside.remove()
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
})
|