@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,248 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { expect, fn, userEvent, within } from 'storybook/test'
|
|
3
|
+
import { computed, ref } from 'vue'
|
|
4
|
+
import EditorFontSize, { EDITOR_FONT_SIZE_STEPS } from './EditorFontSize.vue'
|
|
5
|
+
import type { EditorFontSizeValue } from './EditorFontSize.vue'
|
|
6
|
+
|
|
7
|
+
const NBSP = ' '
|
|
8
|
+
|
|
9
|
+
const TOKENS = EDITOR_FONT_SIZE_STEPS.map(({ token }) => token)
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
title: 'Components/Editor Components/Molecules/Editor Font Size',
|
|
13
|
+
component: EditorFontSize,
|
|
14
|
+
// 312px: the content width of the editor's settings panel
|
|
15
|
+
decorators: [() => ({ template: '<div class="mx-auto w-78"><story /></div>' })],
|
|
16
|
+
args: {
|
|
17
|
+
onPointerup: fn(),
|
|
18
|
+
onReset: fn(),
|
|
19
|
+
},
|
|
20
|
+
argTypes: {
|
|
21
|
+
label: { control: 'text' },
|
|
22
|
+
// moves the slider while the model is null, and is the sample's size then
|
|
23
|
+
defaultToken: { control: 'select', options: TOKENS },
|
|
24
|
+
},
|
|
25
|
+
} satisfies Meta<typeof EditorFontSize>
|
|
26
|
+
|
|
27
|
+
type Story = StoryObj<typeof EditorFontSize>
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The control with a sample paragraph under it. The sample carries the class the app would put
|
|
31
|
+
* on the element: the model, or the default size while the model is `null`. It is the real
|
|
32
|
+
* class, so the sample only changes size when that class compiles.
|
|
33
|
+
*/
|
|
34
|
+
const renderSize = (initial: EditorFontSizeValue | null) => (args: Story['args']) => ({
|
|
35
|
+
components: { EditorFontSize },
|
|
36
|
+
setup() {
|
|
37
|
+
const size = ref<EditorFontSizeValue | null>(initial)
|
|
38
|
+
const sampleClass = computed(() => size.value ?? args?.defaultToken ?? 'text-sm')
|
|
39
|
+
return { args, size, sampleClass }
|
|
40
|
+
},
|
|
41
|
+
template: `
|
|
42
|
+
<div>
|
|
43
|
+
<EditorFontSize v-bind="args" v-model="size" />
|
|
44
|
+
<p class="extra-small-normal text-gray-700">
|
|
45
|
+
model: <code data-testid="model">{{ size ?? 'null' }}</code>
|
|
46
|
+
</p>
|
|
47
|
+
<!-- \`wrap-break-word\`: at 9xl a single word is wider than the 312px column -->
|
|
48
|
+
<p data-testid="sample" class="mt-4 text-gray-900 wrap-break-word" :class="sampleClass">
|
|
49
|
+
Een licht appartement op loopafstand van het centrum, met een ruim balkon op het zuiden.
|
|
50
|
+
</p>
|
|
51
|
+
</div>
|
|
52
|
+
`,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const thumbOf = (canvasElement: HTMLElement) => within(canvasElement).getByRole('slider')
|
|
56
|
+
|
|
57
|
+
/** The font size the sample renders at, in px, as Chromium computes it. */
|
|
58
|
+
const sampleSize = (canvasElement: HTMLElement) =>
|
|
59
|
+
parseFloat(getComputedStyle(within(canvasElement).getByTestId('sample')).fontSize)
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* No size of its own: the slider sits on the default step, `text-sm`. The ends of the list are
|
|
63
|
+
* 12 and 128px; every step writes its class, the default one too.
|
|
64
|
+
*/
|
|
65
|
+
export const Default: Story = {
|
|
66
|
+
render: renderSize(null),
|
|
67
|
+
play: async ({ canvasElement }) => {
|
|
68
|
+
const canvas = within(canvasElement)
|
|
69
|
+
const thumb = thumbOf(canvasElement)
|
|
70
|
+
|
|
71
|
+
await expect(thumb).toHaveAccessibleName('Grootte')
|
|
72
|
+
await expect(thumb).toHaveAttribute('aria-valuetext', `14${NBSP}px`)
|
|
73
|
+
await expect(sampleSize(canvasElement)).toBe(14)
|
|
74
|
+
// invisible, so out of the accessibility tree and its name: found by its label attribute
|
|
75
|
+
const reset = canvasElement.querySelector<HTMLElement>(
|
|
76
|
+
'button[aria-label="Terug naar standaard"]',
|
|
77
|
+
)!
|
|
78
|
+
await expect(reset).not.toBeVisible()
|
|
79
|
+
|
|
80
|
+
thumb.focus()
|
|
81
|
+
await userEvent.keyboard('{End}')
|
|
82
|
+
await expect(thumb).toHaveAttribute('aria-valuetext', `128${NBSP}px`)
|
|
83
|
+
await expect(canvas.getByTestId('model')).toHaveTextContent('text-9xl')
|
|
84
|
+
await expect(sampleSize(canvasElement)).toBe(128)
|
|
85
|
+
|
|
86
|
+
// the sample wraps inside the column instead of running out of it
|
|
87
|
+
const sample = canvas.getByTestId('sample')
|
|
88
|
+
const column = canvasElement.querySelector('.w-78')!.getBoundingClientRect()
|
|
89
|
+
await expect(sample.scrollWidth).toBeLessThanOrEqual(sample.clientWidth)
|
|
90
|
+
await expect(sample.getBoundingClientRect().right).toBeLessThanOrEqual(column.right)
|
|
91
|
+
|
|
92
|
+
await userEvent.keyboard('{Home}')
|
|
93
|
+
await expect(sampleSize(canvasElement)).toBe(12)
|
|
94
|
+
|
|
95
|
+
// back onto the default step by keyboard: an explicit class, not null
|
|
96
|
+
await userEvent.keyboard('{ArrowRight}')
|
|
97
|
+
await expect(canvas.getByTestId('model')).toHaveTextContent('text-sm')
|
|
98
|
+
await expect(reset).toBeVisible()
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A default other than `text-sm`: the slider and the sample follow it while the model is null. */
|
|
103
|
+
export const OtherDefault: Story = {
|
|
104
|
+
args: { defaultToken: 'text-base' },
|
|
105
|
+
render: renderSize(null),
|
|
106
|
+
play: async ({ canvasElement }) => {
|
|
107
|
+
await expect(thumbOf(canvasElement)).toHaveAttribute('aria-valuetext', `16${NBSP}px`)
|
|
108
|
+
await expect(sampleSize(canvasElement)).toBe(16)
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The reset button is invisible while the model is null, and so out of the tab order, while it
|
|
114
|
+
* keeps its place. Using it sets the model to `null`, emits `reset`, leaves focus on the thumb, and
|
|
115
|
+
* the sample falls back to the default size.
|
|
116
|
+
*/
|
|
117
|
+
export const Reset: Story = {
|
|
118
|
+
render: renderSize('text-xl'),
|
|
119
|
+
play: async ({ canvasElement, args }) => {
|
|
120
|
+
const canvas = within(canvasElement)
|
|
121
|
+
const reset = canvas.getByRole('button', { name: 'Terug naar standaard' })
|
|
122
|
+
const thumb = thumbOf(canvasElement)
|
|
123
|
+
|
|
124
|
+
await expect(sampleSize(canvasElement)).toBe(20)
|
|
125
|
+
await expect(reset).toBeVisible()
|
|
126
|
+
const trackWidth = thumb.parentElement!.getBoundingClientRect().width
|
|
127
|
+
|
|
128
|
+
await userEvent.click(reset)
|
|
129
|
+
await expect(args.onReset).toHaveBeenCalledOnce()
|
|
130
|
+
await expect(canvas.getByTestId('model')).toHaveTextContent('null')
|
|
131
|
+
await expect(thumb).toHaveFocus()
|
|
132
|
+
await expect(sampleSize(canvasElement)).toBe(14)
|
|
133
|
+
|
|
134
|
+
// `visibility: hidden` keeps its room, so the slider keeps its width, and takes it out of the
|
|
135
|
+
// tab order
|
|
136
|
+
await expect(getComputedStyle(reset).visibility).toBe('hidden')
|
|
137
|
+
await expect(thumb.parentElement!.getBoundingClientRect().width).toBe(trackWidth)
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* One drag is one undo step: the app merges the changes while dragging and closes the step on
|
|
143
|
+
* `pointerup`. A reset always gets a step of its own. The log shows what the app would see.
|
|
144
|
+
*/
|
|
145
|
+
export const UndoEvents: Story = {
|
|
146
|
+
render: (args) => ({
|
|
147
|
+
components: { EditorFontSize },
|
|
148
|
+
setup() {
|
|
149
|
+
const size = ref<EditorFontSizeValue | null>(null)
|
|
150
|
+
const log = ref<string[]>([])
|
|
151
|
+
const onUpdate = (value: EditorFontSizeValue | null) => {
|
|
152
|
+
size.value = value
|
|
153
|
+
log.value.push(`change ${value ?? 'null'}`)
|
|
154
|
+
}
|
|
155
|
+
const onPointerup = () => log.value.push('pointerup: end of step')
|
|
156
|
+
const onReset = () => log.value.push('reset: own step')
|
|
157
|
+
return { args, size, log, onUpdate, onPointerup, onReset }
|
|
158
|
+
},
|
|
159
|
+
template: `
|
|
160
|
+
<div>
|
|
161
|
+
<EditorFontSize
|
|
162
|
+
v-bind="args"
|
|
163
|
+
:model-value="size"
|
|
164
|
+
@update:model-value="onUpdate"
|
|
165
|
+
@pointerup="onPointerup"
|
|
166
|
+
@reset="onReset"
|
|
167
|
+
/>
|
|
168
|
+
<ol data-testid="log" class="extra-small-normal text-gray-700">
|
|
169
|
+
<li v-for="(entry, index) in log" :key="index">{{ entry }}</li>
|
|
170
|
+
</ol>
|
|
171
|
+
</div>
|
|
172
|
+
`,
|
|
173
|
+
}),
|
|
174
|
+
// The drag itself is a real pointer gesture and is left to the unit test (pointerup) and to
|
|
175
|
+
// trying it here by hand; the play covers the keyboard step and the reset
|
|
176
|
+
play: async ({ canvasElement }) => {
|
|
177
|
+
const canvas = within(canvasElement)
|
|
178
|
+
const entries = () =>
|
|
179
|
+
Array.from(canvas.getByTestId('log').querySelectorAll('li')).map((li) => li.textContent)
|
|
180
|
+
|
|
181
|
+
thumbOf(canvasElement).focus()
|
|
182
|
+
await userEvent.keyboard('{ArrowRight}')
|
|
183
|
+
await expect(entries()).toEqual(['change text-base'])
|
|
184
|
+
|
|
185
|
+
await userEvent.click(canvas.getByRole('button', { name: 'Terug naar standaard' }))
|
|
186
|
+
await expect(entries()).toEqual(['change text-base', 'change null', 'reset: own step'])
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The readout is as wide at "14 px" as at "128 px", so the slider beside it keeps its width over
|
|
192
|
+
* the whole list.
|
|
193
|
+
*/
|
|
194
|
+
export const ReadoutWidth: Story = {
|
|
195
|
+
render: renderSize(null),
|
|
196
|
+
play: async ({ canvasElement }) => {
|
|
197
|
+
const thumb = thumbOf(canvasElement)
|
|
198
|
+
const readout = within(canvasElement).getByText(`14 px`)
|
|
199
|
+
// the slider root, which the thumb is placed in
|
|
200
|
+
const slider = thumb.parentElement!
|
|
201
|
+
const textWidth = () => {
|
|
202
|
+
const range = document.createRange()
|
|
203
|
+
range.selectNodeContents(readout)
|
|
204
|
+
return range.getBoundingClientRect().width
|
|
205
|
+
}
|
|
206
|
+
const measure = () => ({
|
|
207
|
+
readout: readout.getBoundingClientRect().width,
|
|
208
|
+
text: textWidth(),
|
|
209
|
+
slider: slider.getBoundingClientRect().width,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
const narrow = measure()
|
|
213
|
+
thumb.focus()
|
|
214
|
+
await userEvent.keyboard('{End}')
|
|
215
|
+
await expect(readout).toHaveTextContent('128 px')
|
|
216
|
+
const wide = measure()
|
|
217
|
+
|
|
218
|
+
// the widest label still fits inside the minimum width, so nothing moved
|
|
219
|
+
// `min-w-14`: 56px, with at least 8px to spare around the widest label
|
|
220
|
+
await expect(wide.readout).toBe(56)
|
|
221
|
+
await expect(wide.readout - wide.text).toBeGreaterThanOrEqual(8)
|
|
222
|
+
await expect(wide.readout).toBe(narrow.readout)
|
|
223
|
+
await expect(wide.slider).toBe(narrow.slider)
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Every step, one control each, so the whole readout range is in view. */
|
|
228
|
+
export const AllSteps: Story = {
|
|
229
|
+
render: () => ({
|
|
230
|
+
components: { EditorFontSize },
|
|
231
|
+
setup() {
|
|
232
|
+
return { tokens: TOKENS }
|
|
233
|
+
},
|
|
234
|
+
template: `
|
|
235
|
+
<div>
|
|
236
|
+
<EditorFontSize v-for="token in tokens" :key="token" :label="token" :model-value="token" />
|
|
237
|
+
</div>
|
|
238
|
+
`,
|
|
239
|
+
}),
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export const CustomMessages: Story = {
|
|
243
|
+
args: {
|
|
244
|
+
label: 'Size',
|
|
245
|
+
messages: { unit: 'pt', reset: 'Back to default' },
|
|
246
|
+
},
|
|
247
|
+
render: renderSize('text-lg'),
|
|
248
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { afterEach, describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { mount, type VueWrapper } from '@vue/test-utils'
|
|
3
|
+
import EditorFontSize, { EDITOR_FONT_SIZE_STEPS } from './EditorFontSize.vue'
|
|
4
|
+
import type { EditorFontSizeValue } from './EditorFontSize.vue'
|
|
5
|
+
import Slider from '../../../atoms/Slider/Slider.vue'
|
|
6
|
+
|
|
7
|
+
// jsdom ships no ResizeObserver, and reka-ui's slider observes its track to place the thumbs
|
|
8
|
+
vi.stubGlobal(
|
|
9
|
+
'ResizeObserver',
|
|
10
|
+
class {
|
|
11
|
+
observe() {}
|
|
12
|
+
unobserve() {}
|
|
13
|
+
disconnect() {}
|
|
14
|
+
},
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// jsdom implements no pointer capture either, and reka-ui captures the pointer on thumb pointerdown
|
|
18
|
+
Element.prototype.setPointerCapture ??= () => {}
|
|
19
|
+
Element.prototype.releasePointerCapture ??= () => {}
|
|
20
|
+
Element.prototype.hasPointerCapture ??= () => false
|
|
21
|
+
|
|
22
|
+
const NBSP = ' '
|
|
23
|
+
|
|
24
|
+
const mounted: VueWrapper[] = []
|
|
25
|
+
|
|
26
|
+
const mountSize = (props: Record<string, unknown> = {}) => {
|
|
27
|
+
const wrapper = mount(EditorFontSize, {
|
|
28
|
+
props: { modelValue: null, ...props },
|
|
29
|
+
attachTo: document.body,
|
|
30
|
+
})
|
|
31
|
+
mounted.push(wrapper)
|
|
32
|
+
return wrapper
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The index the slider sits on, read from the model handed to the slider. */
|
|
36
|
+
const sliderIndex = (wrapper: VueWrapper) => wrapper.findComponent(Slider).props('modelValue')
|
|
37
|
+
|
|
38
|
+
const thumbOf = (wrapper: VueWrapper) => wrapper.find('[role="slider"]')
|
|
39
|
+
const resetOf = (wrapper: VueWrapper) => wrapper.find('button[type="button"]')
|
|
40
|
+
|
|
41
|
+
const indexOf = (token: EditorFontSizeValue) =>
|
|
42
|
+
EDITOR_FONT_SIZE_STEPS.findIndex((step) => step.token === token)
|
|
43
|
+
|
|
44
|
+
describe('EditorFontSize', () => {
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
mounted.splice(0).forEach((wrapper) => wrapper.unmount())
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// No assertions on typography utilities: the label and the value are found by
|
|
50
|
+
// text or by structure, so restyling the block or the slider cannot turn
|
|
51
|
+
// these red.
|
|
52
|
+
it('renders the Dutch default label', () => {
|
|
53
|
+
expect(mountSize().find('p').text()).toBe('Grootte')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('renders a custom label, also on the thumb', () => {
|
|
57
|
+
const wrapper = mountSize({ label: 'Lettergrootte' })
|
|
58
|
+
|
|
59
|
+
expect(wrapper.find('p').text()).toBe('Lettergrootte')
|
|
60
|
+
expect(thumbOf(wrapper).attributes('aria-label')).toBe('Lettergrootte')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// The consumer stores these classes and lists them in an `@source inline`
|
|
64
|
+
// rule, so the list is a contract: any change here is a breaking change.
|
|
65
|
+
it('offers exactly the agreed steps', () => {
|
|
66
|
+
expect(EDITOR_FONT_SIZE_STEPS).toEqual([
|
|
67
|
+
{ token: 'text-xs', px: 12 },
|
|
68
|
+
{ token: 'text-sm', px: 14 },
|
|
69
|
+
{ token: 'text-base', px: 16 },
|
|
70
|
+
{ token: 'text-lg', px: 18 },
|
|
71
|
+
{ token: 'text-xl', px: 20 },
|
|
72
|
+
{ token: 'text-2xl', px: 24 },
|
|
73
|
+
{ token: 'text-3xl', px: 30 },
|
|
74
|
+
{ token: 'text-4xl', px: 36 },
|
|
75
|
+
{ token: 'text-5xl', px: 48 },
|
|
76
|
+
{ token: 'text-6xl', px: 60 },
|
|
77
|
+
{ token: 'text-7xl', px: 72 },
|
|
78
|
+
{ token: 'text-8xl', px: 96 },
|
|
79
|
+
{ token: 'text-9xl', px: 128 },
|
|
80
|
+
])
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('spans the whole step list', () => {
|
|
84
|
+
const slider = mountSize().findComponent(Slider)
|
|
85
|
+
|
|
86
|
+
expect(slider.props('min')).toBe(0)
|
|
87
|
+
expect(slider.props('max')).toBe(12)
|
|
88
|
+
expect(slider.props('step')).toBe(1)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('default step', () => {
|
|
92
|
+
it('puts null on text-sm without a defaultToken', () => {
|
|
93
|
+
const wrapper = mountSize()
|
|
94
|
+
|
|
95
|
+
expect(sliderIndex(wrapper)).toEqual([indexOf('text-sm')])
|
|
96
|
+
expect(wrapper.text()).toContain(`14${NBSP}px`)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('treats an unset model as null', () => {
|
|
100
|
+
const wrapper = mount(EditorFontSize)
|
|
101
|
+
mounted.push(wrapper)
|
|
102
|
+
|
|
103
|
+
expect(sliderIndex(wrapper)).toEqual([indexOf('text-sm')])
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('puts null on the step of defaultToken', () => {
|
|
107
|
+
const wrapper = mountSize({ defaultToken: 'text-lg' })
|
|
108
|
+
|
|
109
|
+
expect(sliderIndex(wrapper)).toEqual([indexOf('text-lg')])
|
|
110
|
+
expect(wrapper.text()).toContain(`18${NBSP}px`)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('falls back to text-sm for a defaultToken that is not on the list', () => {
|
|
114
|
+
const wrapper = mountSize({ defaultToken: 'text-10xl' as unknown as EditorFontSizeValue })
|
|
115
|
+
|
|
116
|
+
expect(sliderIndex(wrapper)).toEqual([indexOf('text-sm')])
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it.each([
|
|
120
|
+
['an unknown value', 'text-10xl'],
|
|
121
|
+
['a size from another scale', 'text-[15px]'],
|
|
122
|
+
])('sits on the default step for %s', (_, value) => {
|
|
123
|
+
// The type rules these out, which is exactly why the runtime fallback is
|
|
124
|
+
// worth a test: a stored value gets past the types.
|
|
125
|
+
const wrapper = mountSize({
|
|
126
|
+
defaultToken: 'text-base',
|
|
127
|
+
modelValue: value as unknown as EditorFontSizeValue,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
expect(sliderIndex(wrapper)).toEqual([indexOf('text-base')])
|
|
131
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// The fallback is for faulty or old data; a development build says so, once per value
|
|
136
|
+
describe('development warnings', () => {
|
|
137
|
+
const spyWarn = () => vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
138
|
+
|
|
139
|
+
afterEach(() => {
|
|
140
|
+
vi.restoreAllMocks()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('warns once for an unknown defaultToken and reads out text-sm', async () => {
|
|
144
|
+
const warn = spyWarn()
|
|
145
|
+
const wrapper = mountSize({ defaultToken: 'text-10xl' as unknown as EditorFontSizeValue })
|
|
146
|
+
|
|
147
|
+
expect(wrapper.text()).toContain(`14${NBSP}px`)
|
|
148
|
+
await wrapper.setProps({ label: 'Opnieuw renderen' })
|
|
149
|
+
expect(warn).toHaveBeenCalledOnce()
|
|
150
|
+
expect(warn.mock.calls[0][0]).toContain('text-10xl')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('warns once for a model value off the list, reads out the default and keeps reset', async () => {
|
|
154
|
+
const warn = spyWarn()
|
|
155
|
+
const wrapper = mountSize({ modelValue: 'text-[15px]' as unknown as EditorFontSizeValue })
|
|
156
|
+
|
|
157
|
+
expect(wrapper.text()).toContain(`14${NBSP}px`)
|
|
158
|
+
expect(resetOf(wrapper).classes()).not.toContain('invisible')
|
|
159
|
+
|
|
160
|
+
await wrapper.setProps({ label: 'Opnieuw renderen' })
|
|
161
|
+
await wrapper.setProps({ modelValue: 'text-[15px]' as unknown as EditorFontSizeValue })
|
|
162
|
+
expect(warn).toHaveBeenCalledOnce()
|
|
163
|
+
expect(warn.mock.calls[0][0]).toContain('text-[15px]')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('warns again for another value off the list', async () => {
|
|
167
|
+
const warn = spyWarn()
|
|
168
|
+
const wrapper = mountSize({ modelValue: 'text-[15px]' as unknown as EditorFontSizeValue })
|
|
169
|
+
|
|
170
|
+
await wrapper.setProps({ modelValue: 'text-[16px]' as unknown as EditorFontSizeValue })
|
|
171
|
+
|
|
172
|
+
expect(warn).toHaveBeenCalledTimes(2)
|
|
173
|
+
expect(warn.mock.calls[1][0]).toContain('text-[16px]')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it.each([[{ modelValue: null }], [{ modelValue: 'text-xl', defaultToken: 'text-base' }]])(
|
|
177
|
+
'stays silent for valid values: %o',
|
|
178
|
+
(props) => {
|
|
179
|
+
const warn = spyWarn()
|
|
180
|
+
|
|
181
|
+
mountSize(props)
|
|
182
|
+
|
|
183
|
+
expect(warn).not.toHaveBeenCalled()
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it.each(EDITOR_FONT_SIZE_STEPS.map((step) => [step]))(
|
|
189
|
+
'puts the slider on %o and reads out its px',
|
|
190
|
+
(step) => {
|
|
191
|
+
const wrapper = mountSize({ modelValue: step.token })
|
|
192
|
+
|
|
193
|
+
expect(sliderIndex(wrapper)).toEqual([EDITOR_FONT_SIZE_STEPS.indexOf(step)])
|
|
194
|
+
expect(wrapper.text()).toContain(`${step.px}${NBSP}px`)
|
|
195
|
+
// aria-valuenow is the step index, which is meaningless read aloud
|
|
196
|
+
expect(thumbOf(wrapper).attributes('aria-valuetext')).toBe(`${step.px}${NBSP}px`)
|
|
197
|
+
},
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
it('renders custom copy for the unit and the reset button', () => {
|
|
201
|
+
const wrapper = mountSize({ modelValue: 'text-xl', messages: { unit: 'pt', reset: 'Reset' } })
|
|
202
|
+
|
|
203
|
+
expect(wrapper.text()).toContain(`20${NBSP}pt`)
|
|
204
|
+
expect(resetOf(wrapper).attributes('aria-label')).toBe('Reset')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// Only the reset button writes null: moving onto the default step is a choice of its own
|
|
208
|
+
it('emits the token of every step, the default step included', async () => {
|
|
209
|
+
const wrapper = mountSize({ modelValue: 'text-xl' })
|
|
210
|
+
const slider = wrapper.findComponent(Slider)
|
|
211
|
+
|
|
212
|
+
slider.vm.$emit('update:modelValue', [indexOf('text-sm')])
|
|
213
|
+
slider.vm.$emit('update:modelValue', [indexOf('text-xs')])
|
|
214
|
+
await wrapper.vm.$nextTick()
|
|
215
|
+
|
|
216
|
+
expect(wrapper.emitted('update:modelValue')).toEqual([['text-sm'], ['text-xs']])
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('passes the release of the slider on as pointerup', async () => {
|
|
220
|
+
const wrapper = mountSize({ modelValue: 'text-lg' })
|
|
221
|
+
|
|
222
|
+
await thumbOf(wrapper).trigger('pointerup')
|
|
223
|
+
|
|
224
|
+
expect(wrapper.emitted('pointerup')).toHaveLength(1)
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
describe('reset', () => {
|
|
228
|
+
// `invisible` is `visibility: hidden`, which takes the button out of the tab order
|
|
229
|
+
// while it keeps its width, so the slider does not move
|
|
230
|
+
it('is rendered but invisible while the model is null', () => {
|
|
231
|
+
const reset = resetOf(mountSize({ modelValue: null }))
|
|
232
|
+
|
|
233
|
+
expect(reset.exists()).toBe(true)
|
|
234
|
+
expect(reset.classes()).toContain('invisible')
|
|
235
|
+
expect(reset.attributes('aria-label')).toBe('Terug naar standaard')
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('is visible for any token, the default step included', () => {
|
|
239
|
+
expect(resetOf(mountSize({ modelValue: 'text-sm' })).classes()).not.toContain('invisible')
|
|
240
|
+
expect(resetOf(mountSize({ modelValue: 'text-xl' })).classes()).not.toContain('invisible')
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it.each(['text-sm', 'text-xl'] as const)(
|
|
244
|
+
'from %s: sets the model to null, then emits reset, then focuses the thumb',
|
|
245
|
+
async (token) => {
|
|
246
|
+
const order: string[] = []
|
|
247
|
+
const wrapper = mountSize({
|
|
248
|
+
modelValue: token,
|
|
249
|
+
'onUpdate:modelValue': (value: unknown) => order.push(`model:${value}`),
|
|
250
|
+
onReset: () => order.push('reset'),
|
|
251
|
+
})
|
|
252
|
+
// reka shows the thumb (display: none until then) in the render after mount; a user
|
|
253
|
+
// cannot click before it, and jsdom 30 refuses focus to an element that is not rendered.
|
|
254
|
+
await wrapper.vm.$nextTick()
|
|
255
|
+
|
|
256
|
+
await resetOf(wrapper).trigger('click')
|
|
257
|
+
|
|
258
|
+
expect(order).toEqual(['model:null', 'reset'])
|
|
259
|
+
expect(document.activeElement).toBe(thumbOf(wrapper).element)
|
|
260
|
+
},
|
|
261
|
+
)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
describe('keyboard', () => {
|
|
265
|
+
const press = async (wrapper: VueWrapper, key: string) => {
|
|
266
|
+
await thumbOf(wrapper).trigger('keydown', { key })
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
it('writes text-base on ArrowRight from null', async () => {
|
|
270
|
+
const wrapper = mountSize({ modelValue: null })
|
|
271
|
+
|
|
272
|
+
await press(wrapper, 'ArrowRight')
|
|
273
|
+
|
|
274
|
+
expect(wrapper.emitted('update:modelValue')).toEqual([['text-base']])
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it.each([
|
|
278
|
+
['Home', 'text-xs'],
|
|
279
|
+
['End', 'text-9xl'],
|
|
280
|
+
])('writes the end of the list on %s', async (key, token) => {
|
|
281
|
+
const wrapper = mountSize({ modelValue: 'text-lg' })
|
|
282
|
+
|
|
283
|
+
await press(wrapper, key)
|
|
284
|
+
|
|
285
|
+
expect(wrapper.emitted('update:modelValue')).toEqual([[token]])
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
})
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { expect, userEvent, within } from 'storybook/test'
|
|
3
|
+
import { ref } from 'vue'
|
|
4
|
+
import EditorGap from './EditorGap.vue'
|
|
5
|
+
import type { EditorGapValue } from './EditorGap.vue'
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
title: 'Components/Editor Components/Molecules/Editor Gap',
|
|
9
|
+
component: EditorGap,
|
|
10
|
+
decorators: [() => ({ template: '<div class="w-100 mx-auto"><story /></div>' })],
|
|
11
|
+
argTypes: {
|
|
12
|
+
label: { control: 'text' },
|
|
13
|
+
},
|
|
14
|
+
} satisfies Meta<typeof EditorGap>
|
|
15
|
+
|
|
16
|
+
type Story = StoryObj<typeof EditorGap>
|
|
17
|
+
|
|
18
|
+
export const Default: Story = {
|
|
19
|
+
render: (args) => ({
|
|
20
|
+
components: { EditorGap },
|
|
21
|
+
setup() {
|
|
22
|
+
const gap = ref<EditorGapValue>('gap-4')
|
|
23
|
+
return { args, gap }
|
|
24
|
+
},
|
|
25
|
+
// The value is shown as-is: it is the Tailwind class the editor applies.
|
|
26
|
+
template: `
|
|
27
|
+
<div>
|
|
28
|
+
<EditorGap v-bind="args" v-model="gap" />
|
|
29
|
+
<p class="small-normal text-gray-700">{{ gap }}</p>
|
|
30
|
+
</div>
|
|
31
|
+
`,
|
|
32
|
+
}),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const CustomMessages: Story = {
|
|
36
|
+
args: {
|
|
37
|
+
label: 'Gap',
|
|
38
|
+
messages: { unit: 'px' },
|
|
39
|
+
},
|
|
40
|
+
render: (args) => ({
|
|
41
|
+
components: { EditorGap },
|
|
42
|
+
setup() {
|
|
43
|
+
const gap = ref<EditorGapValue>('gap-4')
|
|
44
|
+
return { args, gap }
|
|
45
|
+
},
|
|
46
|
+
template: `<EditorGap v-bind="args" v-model="gap" />`,
|
|
47
|
+
}),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* axe in error mode on the Auto stories. The toolbar is NexxtToggleGroup, whose
|
|
52
|
+
* chosen label fails color-contrast on its own (cornflower-600 text, 4.31:1 on
|
|
53
|
+
* white); that is ToggleGroup's, not this block's, so it is left out here.
|
|
54
|
+
*/
|
|
55
|
+
const AUTO_A11Y = { a11y: { test: 'error', context: { exclude: ['[role="toolbar"]'] } } }
|
|
56
|
+
|
|
57
|
+
/** The slider and the readout of one EditorGap, by the readout's text. */
|
|
58
|
+
const sliderRow = (block: HTMLElement) => {
|
|
59
|
+
const readout = within(block).getByText(/\d+ px/)
|
|
60
|
+
const slider = readout.parentElement!.firstElementChild!
|
|
61
|
+
const box = (el: Element) => {
|
|
62
|
+
const { left, width } = el.getBoundingClientRect()
|
|
63
|
+
return { left, width }
|
|
64
|
+
}
|
|
65
|
+
return { slider: box(slider), readout: box(readout) }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* With the auto model bound: Vast | Auto above the slider, on Vast. Below it,
|
|
70
|
+
* for comparison, the same block without the model: the slider and the readout
|
|
71
|
+
* keep their width and place in all three.
|
|
72
|
+
*/
|
|
73
|
+
export const WithAuto: Story = {
|
|
74
|
+
parameters: AUTO_A11Y,
|
|
75
|
+
render: (args) => ({
|
|
76
|
+
components: { EditorGap },
|
|
77
|
+
setup() {
|
|
78
|
+
const gap = ref<EditorGapValue>('gap-4')
|
|
79
|
+
const auto = ref(false)
|
|
80
|
+
return { args, gap, auto }
|
|
81
|
+
},
|
|
82
|
+
template: `
|
|
83
|
+
<div>
|
|
84
|
+
<EditorGap v-bind="args" v-model="gap" v-model:auto="auto" data-variant="auto" />
|
|
85
|
+
<EditorGap v-bind="args" v-model="gap" data-variant="plain" />
|
|
86
|
+
<p class="small-normal text-gray-700">{{ gap }}, auto: {{ auto }}</p>
|
|
87
|
+
</div>
|
|
88
|
+
`,
|
|
89
|
+
}),
|
|
90
|
+
play: async ({ canvasElement }) => {
|
|
91
|
+
const canvas = within(canvasElement)
|
|
92
|
+
const block = (variant: string) =>
|
|
93
|
+
canvasElement.querySelector(`[data-variant="${variant}"]`) as HTMLElement
|
|
94
|
+
const settle = () =>
|
|
95
|
+
new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
|
96
|
+
|
|
97
|
+
await document.fonts.ready
|
|
98
|
+
await settle()
|
|
99
|
+
const plain = sliderRow(block('plain'))
|
|
100
|
+
const vast = sliderRow(block('auto'))
|
|
101
|
+
await expect(vast).toEqual(plain)
|
|
102
|
+
await expect(
|
|
103
|
+
within(block('auto')).getByRole('slider', { name: 'Tussenruimte' }),
|
|
104
|
+
).toBeInTheDocument()
|
|
105
|
+
|
|
106
|
+
await userEvent.click(canvas.getByRole('button', { name: 'Auto' }))
|
|
107
|
+
await settle()
|
|
108
|
+
|
|
109
|
+
await expect(sliderRow(block('auto'))).toEqual(plain)
|
|
110
|
+
await expect(
|
|
111
|
+
within(block('auto')).getByRole('slider', { name: 'Tussenruimte, minimaal' }),
|
|
112
|
+
).toBeInTheDocument()
|
|
113
|
+
await expect(canvas.getByText('gap-4, auto: true')).toBeInTheDocument()
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Auto on: the slider sets the minimum gap. */
|
|
118
|
+
export const AutoOn: Story = {
|
|
119
|
+
parameters: AUTO_A11Y,
|
|
120
|
+
render: (args) => ({
|
|
121
|
+
components: { EditorGap },
|
|
122
|
+
setup() {
|
|
123
|
+
const gap = ref<EditorGapValue>('gap-6')
|
|
124
|
+
const auto = ref(true)
|
|
125
|
+
return { args, gap, auto }
|
|
126
|
+
},
|
|
127
|
+
template: `<EditorGap v-bind="args" v-model="gap" v-model:auto="auto" />`,
|
|
128
|
+
}),
|
|
129
|
+
}
|