@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,257 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { expect, fn, userEvent, within } from 'storybook/test'
|
|
3
|
+
import { computed, ref, watch } from 'vue'
|
|
4
|
+
import EditorLineHeight, { EDITOR_LINE_HEIGHT_STEPS } from './EditorLineHeight.vue'
|
|
5
|
+
import type { EditorLineHeightValue } from './EditorLineHeight.vue'
|
|
6
|
+
import { EDITOR_FONT_SIZE_STEPS } from '../EditorFontSize/EditorFontSize.vue'
|
|
7
|
+
import type { EditorFontSizeValue } from '../EditorFontSize/EditorFontSize.vue'
|
|
8
|
+
|
|
9
|
+
const NO_CLASS = '(geen klasse)'
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
title: 'Components/Editor Components/Molecules/Editor Line Height',
|
|
13
|
+
component: EditorLineHeight,
|
|
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
|
+
},
|
|
19
|
+
argTypes: {
|
|
20
|
+
label: { control: 'text' },
|
|
21
|
+
// the base of the readout, and the size of the sample paragraph
|
|
22
|
+
fontSize: { control: 'select', options: EDITOR_FONT_SIZE_STEPS.map(({ token }) => token) },
|
|
23
|
+
// sets the model; the slider writes it back, so the two stay in step
|
|
24
|
+
modelValue: {
|
|
25
|
+
control: 'select',
|
|
26
|
+
options: EDITOR_LINE_HEIGHT_STEPS.map(({ token }) => token ?? NO_CLASS),
|
|
27
|
+
mapping: { [NO_CLASS]: null },
|
|
28
|
+
},
|
|
29
|
+
messages: { control: 'object' },
|
|
30
|
+
},
|
|
31
|
+
} satisfies Meta<typeof EditorLineHeight>
|
|
32
|
+
|
|
33
|
+
type Story = StoryObj<typeof EditorLineHeight>
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The control with a sample paragraph under it. The sample carries the classes the app would put
|
|
37
|
+
* on the element: the `fontSize` token plus the model. They are the real classes, so the sample
|
|
38
|
+
* only changes when they compile.
|
|
39
|
+
*/
|
|
40
|
+
const renderLineHeight = (args: Story['args']) => ({
|
|
41
|
+
components: { EditorLineHeight },
|
|
42
|
+
setup() {
|
|
43
|
+
const lineHeight = ref<EditorLineHeightValue | null>(args?.modelValue ?? null)
|
|
44
|
+
watch(
|
|
45
|
+
() => args?.modelValue,
|
|
46
|
+
(value) => (lineHeight.value = value ?? null),
|
|
47
|
+
)
|
|
48
|
+
const sampleClass = computed(() => [args?.fontSize ?? 'text-sm', lineHeight.value])
|
|
49
|
+
return { args, lineHeight, sampleClass }
|
|
50
|
+
},
|
|
51
|
+
template: `
|
|
52
|
+
<div>
|
|
53
|
+
<EditorLineHeight v-bind="args" v-model="lineHeight" />
|
|
54
|
+
<p class="extra-small-normal text-gray-700">
|
|
55
|
+
model: <code data-testid="model">{{ lineHeight ?? 'null' }}</code>
|
|
56
|
+
</p>
|
|
57
|
+
<!--
|
|
58
|
+
\`wrap-break-word\`: at 9xl a single word is wider than the 312px column. \`pt-[0.25em]\`:
|
|
59
|
+
below a factor of 1 the glyphs stick out of the first line box, by 23px at 9xl and 0.8,
|
|
60
|
+
which \`mt-4\` alone does not cover; in em, so the room grows with the text
|
|
61
|
+
-->
|
|
62
|
+
<p
|
|
63
|
+
data-testid="sample"
|
|
64
|
+
class="mt-4 pt-[0.25em] text-gray-900 wrap-break-word"
|
|
65
|
+
:class="sampleClass"
|
|
66
|
+
>
|
|
67
|
+
Een licht appartement op loopafstand van het centrum, met een ruim balkon op het zuiden.
|
|
68
|
+
</p>
|
|
69
|
+
</div>
|
|
70
|
+
`,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const thumbOf = (canvasElement: HTMLElement) => within(canvasElement).getByRole('slider')
|
|
74
|
+
|
|
75
|
+
/** The px in the readout, as the thumb announces it: `Standaard · 20 px` is 20. */
|
|
76
|
+
const readoutPx = (canvasElement: HTMLElement) => {
|
|
77
|
+
const text = thumbOf(canvasElement).getAttribute('aria-valuetext') ?? ''
|
|
78
|
+
return Number(/(\d+) px$/.exec(text)?.[1])
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The line height the sample renders at, in px, as Chromium computes it. */
|
|
82
|
+
const sampleLineHeight = (canvasElement: HTMLElement) =>
|
|
83
|
+
parseFloat(getComputedStyle(within(canvasElement).getByTestId('sample')).lineHeight)
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The text of the sample stays below the controls. Below a factor of 1 the glyphs stick out of
|
|
87
|
+
* their line box, so the glyph box is measured (a Range over the text), not the paragraph. Inside
|
|
88
|
+
* the paragraph the lines may overlap; that is what such a tight line height does.
|
|
89
|
+
*/
|
|
90
|
+
const expectSampleClearsControls = async (canvasElement: HTMLElement) => {
|
|
91
|
+
const range = document.createRange()
|
|
92
|
+
range.selectNodeContents(within(canvasElement).getByTestId('sample'))
|
|
93
|
+
const controls = within(canvasElement).getByTestId('model').closest('p')!
|
|
94
|
+
|
|
95
|
+
await expect(range.getBoundingClientRect().top).toBeGreaterThanOrEqual(
|
|
96
|
+
controls.getBoundingClientRect().bottom,
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Walks every step by keyboard, from the first to the last, and checks the readout against the
|
|
102
|
+
* line height Chromium renders: the readout is that line height rounded to whole px.
|
|
103
|
+
*/
|
|
104
|
+
const expectEveryStepMatches = async (canvasElement: HTMLElement) => {
|
|
105
|
+
const thumb = thumbOf(canvasElement)
|
|
106
|
+
thumb.focus()
|
|
107
|
+
await userEvent.keyboard('{Home}')
|
|
108
|
+
|
|
109
|
+
for (const step of EDITOR_LINE_HEIGHT_STEPS) {
|
|
110
|
+
if (step.token !== null) await userEvent.keyboard('{ArrowRight}')
|
|
111
|
+
await expect(within(canvasElement).getByTestId('model')).toHaveTextContent(step.token ?? 'null')
|
|
112
|
+
await expect(
|
|
113
|
+
Math.abs(sampleLineHeight(canvasElement) - readoutPx(canvasElement)),
|
|
114
|
+
).toBeLessThanOrEqual(0.5)
|
|
115
|
+
await expectSampleClearsControls(canvasElement)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The default step: no class, so the text keeps the line height of its `text-*` token, and the
|
|
121
|
+
* readout names it with that height in px.
|
|
122
|
+
*/
|
|
123
|
+
export const Default: Story = {
|
|
124
|
+
args: { modelValue: null },
|
|
125
|
+
render: renderLineHeight,
|
|
126
|
+
play: async ({ canvasElement }) => {
|
|
127
|
+
const thumb = thumbOf(canvasElement)
|
|
128
|
+
|
|
129
|
+
await expect(thumb).toHaveAccessibleName('Regelafstand')
|
|
130
|
+
await expect(thumb).toHaveAttribute('aria-valuetext', 'Standaard · 20 px')
|
|
131
|
+
await expect(sampleLineHeight(canvasElement)).toBe(20)
|
|
132
|
+
|
|
133
|
+
await expectEveryStepMatches(canvasElement)
|
|
134
|
+
// the last step is twice the size: 28px at text-sm
|
|
135
|
+
await expect(sampleLineHeight(canvasElement)).toBe(28)
|
|
136
|
+
|
|
137
|
+
// the first step after the default is the tightest: 0.8 of 14px is 11.2px, read as 11
|
|
138
|
+
await userEvent.keyboard('{Home}{ArrowRight}')
|
|
139
|
+
await expect(thumb).toHaveAttribute('aria-valuetext', '11 px')
|
|
140
|
+
await expect(sampleLineHeight(canvasElement)).toBeCloseTo(11.2, 1)
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A heading-sized text: the same factors, so the line height grows with the size and the lines
|
|
146
|
+
* never overlap the way a fixed `leading-9` would at 60px.
|
|
147
|
+
*/
|
|
148
|
+
export const LargeText: Story = {
|
|
149
|
+
args: { modelValue: null, fontSize: 'text-6xl' },
|
|
150
|
+
render: renderLineHeight,
|
|
151
|
+
play: async ({ canvasElement }) => {
|
|
152
|
+
await expect(thumbOf(canvasElement)).toHaveAttribute('aria-valuetext', 'Standaard · 60 px')
|
|
153
|
+
await expect(sampleLineHeight(canvasElement)).toBe(60)
|
|
154
|
+
|
|
155
|
+
await expectEveryStepMatches(canvasElement)
|
|
156
|
+
await expect(sampleLineHeight(canvasElement)).toBe(120)
|
|
157
|
+
|
|
158
|
+
await userEvent.keyboard('{Home}{ArrowRight}')
|
|
159
|
+
await expect(thumbOf(canvasElement)).toHaveAttribute('aria-valuetext', '48 px')
|
|
160
|
+
await expect(sampleLineHeight(canvasElement)).toBe(48)
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The tightest step at the largest size, the worst case for overlap: the glyphs stick out of
|
|
166
|
+
* their 102px lines, and still stay clear of the controls above the text.
|
|
167
|
+
*/
|
|
168
|
+
export const BelowOne: Story = {
|
|
169
|
+
args: { modelValue: 'leading-[0.8]', fontSize: 'text-9xl' },
|
|
170
|
+
render: renderLineHeight,
|
|
171
|
+
play: async ({ canvasElement }) => {
|
|
172
|
+
await expect(thumbOf(canvasElement)).toHaveAttribute('aria-valuetext', '102 px')
|
|
173
|
+
await expect(sampleLineHeight(canvasElement)).toBeCloseTo(102.4, 1)
|
|
174
|
+
await expectSampleClearsControls(canvasElement)
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** A stored factor: the slider sits on its step, the readout is the factor at this size. */
|
|
179
|
+
export const WithValue: Story = {
|
|
180
|
+
args: { modelValue: 'leading-[1.5]', fontSize: 'text-lg' },
|
|
181
|
+
render: renderLineHeight,
|
|
182
|
+
play: async ({ canvasElement }) => {
|
|
183
|
+
await expect(thumbOf(canvasElement)).toHaveAttribute('aria-valuetext', '27 px')
|
|
184
|
+
await expect(sampleLineHeight(canvasElement)).toBe(27)
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The widest readout, `Standaard · 128 px` at text-9xl, fits the minimum width of the readout, so
|
|
190
|
+
* the slider beside it keeps its width over the whole list.
|
|
191
|
+
*/
|
|
192
|
+
export const ReadoutWidth: Story = {
|
|
193
|
+
args: { modelValue: null, fontSize: 'text-9xl' },
|
|
194
|
+
render: renderLineHeight,
|
|
195
|
+
play: async ({ canvasElement }) => {
|
|
196
|
+
const thumb = thumbOf(canvasElement)
|
|
197
|
+
const readout = within(canvasElement).getByText('Standaard · 128 px')
|
|
198
|
+
// the slider root, which the thumb is placed in
|
|
199
|
+
const slider = thumb.parentElement!
|
|
200
|
+
const textWidth = () => {
|
|
201
|
+
const range = document.createRange()
|
|
202
|
+
range.selectNodeContents(readout)
|
|
203
|
+
return range.getBoundingClientRect().width
|
|
204
|
+
}
|
|
205
|
+
const measure = () => ({
|
|
206
|
+
readout: readout.getBoundingClientRect().width,
|
|
207
|
+
text: textWidth(),
|
|
208
|
+
slider: slider.getBoundingClientRect().width,
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
const widest = measure()
|
|
212
|
+
thumb.focus()
|
|
213
|
+
await userEvent.keyboard('{ArrowRight}')
|
|
214
|
+
await expect(readout).toHaveTextContent('102 px')
|
|
215
|
+
const narrow = measure()
|
|
216
|
+
|
|
217
|
+
// `min-w-36`: 144px, with at least 8px to spare around the widest label
|
|
218
|
+
await expect(widest.readout).toBe(144)
|
|
219
|
+
await expect(widest.readout - widest.text).toBeGreaterThanOrEqual(8)
|
|
220
|
+
await expect(narrow.readout).toBe(widest.readout)
|
|
221
|
+
await expect(narrow.slider).toBe(widest.slider)
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Every step, one control and sample each, so the whole readout range is in view. */
|
|
226
|
+
export const AllSteps: Story = {
|
|
227
|
+
render: (args) => ({
|
|
228
|
+
components: { EditorLineHeight },
|
|
229
|
+
setup() {
|
|
230
|
+
const tokens = EDITOR_LINE_HEIGHT_STEPS.map(({ token }) => token)
|
|
231
|
+
const fontSize = computed<EditorFontSizeValue>(() => args?.fontSize ?? 'text-sm')
|
|
232
|
+
return { args, tokens, fontSize }
|
|
233
|
+
},
|
|
234
|
+
template: `
|
|
235
|
+
<div class="flex flex-col gap-4">
|
|
236
|
+
<div v-for="token in tokens" :key="token ?? 'default'">
|
|
237
|
+
<EditorLineHeight v-bind="args" :label="token ?? 'Standaard'" :model-value="token" />
|
|
238
|
+
<p class="pt-[0.25em] text-gray-900 wrap-break-word" :class="[fontSize, token]">
|
|
239
|
+
Een licht appartement op loopafstand van het centrum.
|
|
240
|
+
</p>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
`,
|
|
244
|
+
}),
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export const CustomMessages: Story = {
|
|
248
|
+
args: {
|
|
249
|
+
label: 'Line height',
|
|
250
|
+
modelValue: null,
|
|
251
|
+
messages: { unit: 'pt', default: 'Auto' },
|
|
252
|
+
},
|
|
253
|
+
render: renderLineHeight,
|
|
254
|
+
play: async ({ canvasElement }) => {
|
|
255
|
+
await expect(thumbOf(canvasElement)).toHaveAttribute('aria-valuetext', 'Auto · 20 pt')
|
|
256
|
+
},
|
|
257
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { afterEach, describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import EditorLineHeight, {
|
|
4
|
+
EDITOR_LINE_HEIGHT_STEPS,
|
|
5
|
+
EDITOR_TEXT_LINE_HEIGHT_RATIOS,
|
|
6
|
+
} from './EditorLineHeight.vue'
|
|
7
|
+
import type { EditorFontSizeValue } from '../EditorFontSize/EditorFontSize.vue'
|
|
8
|
+
import type { EditorLineHeightValue } from './EditorLineHeight.vue'
|
|
9
|
+
import Slider from '../../../atoms/Slider/Slider.vue'
|
|
10
|
+
|
|
11
|
+
// jsdom ships no ResizeObserver, and reka-ui's slider observes its track to place the thumbs
|
|
12
|
+
vi.stubGlobal(
|
|
13
|
+
'ResizeObserver',
|
|
14
|
+
class {
|
|
15
|
+
observe() {}
|
|
16
|
+
unobserve() {}
|
|
17
|
+
disconnect() {}
|
|
18
|
+
},
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// jsdom implements no pointer capture either, and reka-ui captures the pointer on thumb pointerdown
|
|
22
|
+
Element.prototype.setPointerCapture ??= () => {}
|
|
23
|
+
Element.prototype.releasePointerCapture ??= () => {}
|
|
24
|
+
Element.prototype.hasPointerCapture ??= () => false
|
|
25
|
+
|
|
26
|
+
/** The index the slider sits on, read from the model handed to the slider. */
|
|
27
|
+
const sliderIndex = (wrapper: ReturnType<typeof mount>) =>
|
|
28
|
+
wrapper.findComponent(Slider).props('modelValue')
|
|
29
|
+
|
|
30
|
+
describe('EditorLineHeight', () => {
|
|
31
|
+
// No assertions on typography utilities: the label and the value are found by
|
|
32
|
+
// text or by structure, so restyling the block or the slider cannot turn
|
|
33
|
+
// these red.
|
|
34
|
+
it('renders the Dutch default label', () => {
|
|
35
|
+
const wrapper = mount(EditorLineHeight, { props: { modelValue: null } })
|
|
36
|
+
|
|
37
|
+
expect(wrapper.find('p').text()).toBe('Regelafstand')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('renders a custom label', () => {
|
|
41
|
+
const wrapper = mount(EditorLineHeight, {
|
|
42
|
+
props: { modelValue: null, label: 'Line height' },
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
expect(wrapper.find('p').text()).toBe('Line height')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// The consumer stores these classes and lists them in an `@source inline`
|
|
49
|
+
// rule, and the back end allows exactly these, so the list is a contract:
|
|
50
|
+
// any change here is a breaking change.
|
|
51
|
+
it('offers exactly the agreed steps', () => {
|
|
52
|
+
expect(EDITOR_LINE_HEIGHT_STEPS).toEqual([
|
|
53
|
+
{ token: null, factor: null },
|
|
54
|
+
{ token: 'leading-[0.8]', factor: 0.8 },
|
|
55
|
+
{ token: 'leading-[0.85]', factor: 0.85 },
|
|
56
|
+
{ token: 'leading-[0.9]', factor: 0.9 },
|
|
57
|
+
{ token: 'leading-[0.95]', factor: 0.95 },
|
|
58
|
+
{ token: 'leading-[1]', factor: 1 },
|
|
59
|
+
{ token: 'leading-[1.1]', factor: 1.1 },
|
|
60
|
+
{ token: 'leading-[1.2]', factor: 1.2 },
|
|
61
|
+
{ token: 'leading-[1.3]', factor: 1.3 },
|
|
62
|
+
{ token: 'leading-[1.4]', factor: 1.4 },
|
|
63
|
+
{ token: 'leading-[1.5]', factor: 1.5 },
|
|
64
|
+
{ token: 'leading-[1.6]', factor: 1.6 },
|
|
65
|
+
{ token: 'leading-[1.75]', factor: 1.75 },
|
|
66
|
+
{ token: 'leading-[2]', factor: 2 },
|
|
67
|
+
])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// Tailwind v4's default `--text-*--line-height`; the theme does not override them
|
|
71
|
+
it('knows the line height every text token carries by itself', () => {
|
|
72
|
+
expect(EDITOR_TEXT_LINE_HEIGHT_RATIOS).toEqual({
|
|
73
|
+
'text-xs': 1 / 0.75,
|
|
74
|
+
'text-sm': 1.25 / 0.875,
|
|
75
|
+
'text-base': 1.5,
|
|
76
|
+
'text-lg': 1.75 / 1.125,
|
|
77
|
+
'text-xl': 1.75 / 1.25,
|
|
78
|
+
'text-2xl': 2 / 1.5,
|
|
79
|
+
'text-3xl': 2.25 / 1.875,
|
|
80
|
+
'text-4xl': 2.5 / 2.25,
|
|
81
|
+
'text-5xl': 1,
|
|
82
|
+
'text-6xl': 1,
|
|
83
|
+
'text-7xl': 1,
|
|
84
|
+
'text-8xl': 1,
|
|
85
|
+
'text-9xl': 1,
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('spans the whole step list', () => {
|
|
90
|
+
const wrapper = mount(EditorLineHeight, { props: { modelValue: null } })
|
|
91
|
+
const slider = wrapper.findComponent(Slider)
|
|
92
|
+
|
|
93
|
+
expect(slider.props('min')).toBe(0)
|
|
94
|
+
expect(slider.props('max')).toBe(13)
|
|
95
|
+
expect(slider.props('step')).toBe(1)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('readout', () => {
|
|
99
|
+
// The readout is the factor times the px of `fontSize`, in whole px
|
|
100
|
+
it.each([
|
|
101
|
+
['text-sm', [11, 12, 13, 13, 14, 15, 17, 18, 20, 21, 22, 25, 28]],
|
|
102
|
+
['text-lg', [14, 15, 16, 17, 18, 20, 22, 23, 25, 27, 29, 32, 36]],
|
|
103
|
+
// 0.85 and 0.95 land on a half: 25.5 and 28.5 round up
|
|
104
|
+
['text-3xl', [24, 26, 27, 29, 30, 33, 36, 39, 42, 45, 48, 53, 60]],
|
|
105
|
+
['text-6xl', [48, 51, 54, 57, 60, 66, 72, 78, 84, 90, 96, 105, 120]],
|
|
106
|
+
['text-9xl', [102, 109, 115, 122, 128, 141, 154, 166, 179, 192, 205, 224, 256]],
|
|
107
|
+
] as const)('reads every step in px at %s', (fontSize, readouts) => {
|
|
108
|
+
EDITOR_LINE_HEIGHT_STEPS.slice(1).forEach((step, index) => {
|
|
109
|
+
const wrapper = mount(EditorLineHeight, {
|
|
110
|
+
props: { modelValue: step.token, fontSize },
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
expect(sliderIndex(wrapper)).toEqual([index + 1])
|
|
114
|
+
// aria-valuenow is the step index, which is meaningless read aloud
|
|
115
|
+
expect(wrapper.find('[role="slider"]').attributes('aria-valuetext')).toBe(
|
|
116
|
+
`${readouts[index]} px`,
|
|
117
|
+
)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// No class: the token's own line height, named
|
|
122
|
+
it.each([
|
|
123
|
+
['text-xs', 16],
|
|
124
|
+
['text-sm', 20],
|
|
125
|
+
['text-base', 24],
|
|
126
|
+
['text-lg', 28],
|
|
127
|
+
['text-xl', 28],
|
|
128
|
+
['text-2xl', 32],
|
|
129
|
+
['text-3xl', 36],
|
|
130
|
+
['text-4xl', 40],
|
|
131
|
+
['text-5xl', 48],
|
|
132
|
+
['text-6xl', 60],
|
|
133
|
+
['text-9xl', 128],
|
|
134
|
+
] as const)('reads the default step at %s as Standaard · %i px', (fontSize, px) => {
|
|
135
|
+
const wrapper = mount(EditorLineHeight, { props: { modelValue: null, fontSize } })
|
|
136
|
+
|
|
137
|
+
expect(sliderIndex(wrapper)).toEqual([0])
|
|
138
|
+
expect(wrapper.find('[role="slider"]').attributes('aria-valuetext')).toBe(
|
|
139
|
+
`Standaard · ${px} px`,
|
|
140
|
+
)
|
|
141
|
+
expect(wrapper.text()).toContain(`Standaard · ${px} px`)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('reads at text-sm without a fontSize, and for an unset model', () => {
|
|
145
|
+
const wrapper = mount(EditorLineHeight)
|
|
146
|
+
|
|
147
|
+
expect(sliderIndex(wrapper)).toEqual([0])
|
|
148
|
+
expect(wrapper.text()).toContain('Standaard · 20 px')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('follows a change of fontSize', async () => {
|
|
152
|
+
const wrapper = mount(EditorLineHeight, { props: { modelValue: 'leading-[1.5]' } })
|
|
153
|
+
|
|
154
|
+
expect(wrapper.text()).toContain('21 px')
|
|
155
|
+
await wrapper.setProps({ fontSize: 'text-6xl' })
|
|
156
|
+
expect(wrapper.text()).toContain('90 px')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('renders custom copy for the default step and the unit', () => {
|
|
160
|
+
const wrapper = mount(EditorLineHeight, {
|
|
161
|
+
props: { modelValue: null, messages: { default: 'Auto', unit: 'pt' } },
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
expect(wrapper.text()).toContain('Auto · 20 pt')
|
|
165
|
+
|
|
166
|
+
const withValue = mount(EditorLineHeight, {
|
|
167
|
+
props: { modelValue: 'leading-[2]', messages: { unit: 'pt' } },
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
expect(withValue.text()).toContain('28 pt')
|
|
171
|
+
expect(withValue.text()).not.toContain('Standaard')
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('emits update:modelValue with the Tailwind class of the new step', async () => {
|
|
176
|
+
const wrapper = mount(EditorLineHeight, { props: { modelValue: null } })
|
|
177
|
+
|
|
178
|
+
// Index 10 on the list is `leading-[1.5]`.
|
|
179
|
+
wrapper.findComponent(Slider).vm.$emit('update:modelValue', [10])
|
|
180
|
+
await wrapper.vm.$nextTick()
|
|
181
|
+
|
|
182
|
+
expect(wrapper.emitted('update:modelValue')).toEqual([['leading-[1.5]']])
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('emits the tightest step, below 1, from the default step', async () => {
|
|
186
|
+
const wrapper = mount(EditorLineHeight, { props: { modelValue: null } })
|
|
187
|
+
|
|
188
|
+
wrapper.findComponent(Slider).vm.$emit('update:modelValue', [1])
|
|
189
|
+
await wrapper.vm.$nextTick()
|
|
190
|
+
|
|
191
|
+
expect(wrapper.emitted('update:modelValue')).toEqual([['leading-[0.8]']])
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('emits null when moved back onto the default step', async () => {
|
|
195
|
+
const wrapper = mount(EditorLineHeight, { props: { modelValue: 'leading-[1.5]' } })
|
|
196
|
+
|
|
197
|
+
wrapper.findComponent(Slider).vm.$emit('update:modelValue', [0])
|
|
198
|
+
await wrapper.vm.$nextTick()
|
|
199
|
+
|
|
200
|
+
expect(wrapper.emitted('update:modelValue')).toEqual([[null]])
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// The fallback is for old or faulty data; a development build says so, once per value
|
|
204
|
+
describe('values off the list', () => {
|
|
205
|
+
const spyWarn = () => vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
206
|
+
|
|
207
|
+
afterEach(() => {
|
|
208
|
+
vi.restoreAllMocks()
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
// The type rules these out, which is exactly why the runtime fallback is
|
|
212
|
+
// worth a test: a stored value gets past the types.
|
|
213
|
+
it.each(['leading-6', 'leading-[1.25]'])(
|
|
214
|
+
'puts %s on the default step, warns once and writes nothing back',
|
|
215
|
+
async (value) => {
|
|
216
|
+
const warn = spyWarn()
|
|
217
|
+
const outOfScale = value as unknown as EditorLineHeightValue
|
|
218
|
+
const wrapper = mount(EditorLineHeight, { props: { modelValue: outOfScale } })
|
|
219
|
+
|
|
220
|
+
expect(sliderIndex(wrapper)).toEqual([0])
|
|
221
|
+
expect(wrapper.text()).toContain('Standaard · 20 px')
|
|
222
|
+
|
|
223
|
+
await wrapper.setProps({ label: 'Opnieuw renderen' })
|
|
224
|
+
await wrapper.setProps({ modelValue: outOfScale })
|
|
225
|
+
expect(warn).toHaveBeenCalledOnce()
|
|
226
|
+
expect(warn.mock.calls[0][0]).toContain(value)
|
|
227
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
228
|
+
},
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
it('warns again for another value off the list', async () => {
|
|
232
|
+
const warn = spyWarn()
|
|
233
|
+
const wrapper = mount(EditorLineHeight, {
|
|
234
|
+
props: { modelValue: 'leading-6' as unknown as EditorLineHeightValue },
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
await wrapper.setProps({ modelValue: 'leading-8' as unknown as EditorLineHeightValue })
|
|
238
|
+
|
|
239
|
+
expect(warn).toHaveBeenCalledTimes(2)
|
|
240
|
+
expect(warn.mock.calls[1][0]).toContain('leading-8')
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('reads an unknown fontSize at text-sm and warns once', async () => {
|
|
244
|
+
const warn = spyWarn()
|
|
245
|
+
const wrapper = mount(EditorLineHeight, {
|
|
246
|
+
props: { modelValue: null, fontSize: 'text-10xl' as unknown as EditorFontSizeValue },
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
expect(wrapper.text()).toContain('Standaard · 20 px')
|
|
250
|
+
await wrapper.setProps({ label: 'Opnieuw renderen' })
|
|
251
|
+
expect(warn).toHaveBeenCalledOnce()
|
|
252
|
+
expect(warn.mock.calls[0][0]).toContain('text-10xl')
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it.each<[{ modelValue: EditorLineHeightValue | null; fontSize?: EditorFontSizeValue }]>([
|
|
256
|
+
[{ modelValue: null }],
|
|
257
|
+
[{ modelValue: 'leading-[1.75]', fontSize: 'text-6xl' }],
|
|
258
|
+
])('stays silent for valid values: %o', (props) => {
|
|
259
|
+
const warn = spyWarn()
|
|
260
|
+
|
|
261
|
+
mount(EditorLineHeight, { props })
|
|
262
|
+
|
|
263
|
+
expect(warn).not.toHaveBeenCalled()
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('passes the release of the slider on as pointerup', async () => {
|
|
268
|
+
const wrapper = mount(EditorLineHeight, { props: { modelValue: 'leading-[1.5]' } })
|
|
269
|
+
|
|
270
|
+
await wrapper.find('[role="slider"]').trigger('pointerup')
|
|
271
|
+
|
|
272
|
+
expect(wrapper.emitted('pointerup')).toHaveLength(1)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('names the thumb after the label', () => {
|
|
276
|
+
const wrapper = mount(EditorLineHeight, { props: { modelValue: null } })
|
|
277
|
+
|
|
278
|
+
expect(wrapper.find('[role="slider"]').attributes('aria-label')).toBe('Regelafstand')
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('is always visible, so it renders no switch', () => {
|
|
282
|
+
const wrapper = mount(EditorLineHeight, { props: { modelValue: null } })
|
|
283
|
+
|
|
284
|
+
expect(wrapper.find('input[type="checkbox"]').exists()).toBe(false)
|
|
285
|
+
})
|
|
286
|
+
})
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { expect, userEvent, waitFor, within } from 'storybook/test'
|
|
4
|
+
import EditorLineStyle from './EditorLineStyle.vue'
|
|
5
|
+
import type { EditorLineStyleValue, EditorLineThickness } from './EditorLineStyle.vue'
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
title: 'Components/Editor Components/Molecules/Editor Line Style',
|
|
9
|
+
component: EditorLineStyle,
|
|
10
|
+
decorators: [() => ({ template: '<div class="w-100 mx-auto"><story /></div>' })],
|
|
11
|
+
argTypes: {
|
|
12
|
+
label: { control: 'text' },
|
|
13
|
+
},
|
|
14
|
+
} satisfies Meta<typeof EditorLineStyle>
|
|
15
|
+
|
|
16
|
+
type Story = StoryObj<typeof EditorLineStyle>
|
|
17
|
+
|
|
18
|
+
/** Shows both models as-is: a style token and a thickness in px. */
|
|
19
|
+
const render = (args: Story['args'], style: EditorLineStyleValue, px: EditorLineThickness) => ({
|
|
20
|
+
components: { EditorLineStyle },
|
|
21
|
+
setup() {
|
|
22
|
+
const lineStyle = ref(style)
|
|
23
|
+
const thickness = ref(px)
|
|
24
|
+
return { args, lineStyle, thickness }
|
|
25
|
+
},
|
|
26
|
+
template: `
|
|
27
|
+
<div>
|
|
28
|
+
<EditorLineStyle v-bind="args" v-model="lineStyle" v-model:thickness="thickness" />
|
|
29
|
+
<p data-testid="line-style-value" class="small-normal text-gray-700">
|
|
30
|
+
{{ lineStyle }} / {{ thickness }}
|
|
31
|
+
</p>
|
|
32
|
+
</div>
|
|
33
|
+
`,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const readout = (canvasElement: HTMLElement) =>
|
|
37
|
+
canvasElement.querySelector('[data-testid="line-style-value"]')!.textContent!.trim()
|
|
38
|
+
|
|
39
|
+
const thicknessRow = (canvasElement: HTMLElement) =>
|
|
40
|
+
canvasElement.querySelector('[data-testid="editor-line-style-thickness"]')!
|
|
41
|
+
|
|
42
|
+
export const Default: Story = {
|
|
43
|
+
render: (args) => render(args, 'solid', 2),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** No line: the thickness row is collapsed and inert, so Tab skips the slider. */
|
|
47
|
+
export const None: Story = {
|
|
48
|
+
render: (args) => render(args, 'none', 2),
|
|
49
|
+
play: async ({ canvasElement }) => {
|
|
50
|
+
const canvas = within(canvasElement)
|
|
51
|
+
const row = thicknessRow(canvasElement)
|
|
52
|
+
|
|
53
|
+
expect(row.className).toContain('grid-rows-[0fr]')
|
|
54
|
+
expect(row.getAttribute('inert')).not.toBeNull()
|
|
55
|
+
|
|
56
|
+
await userEvent.click(canvas.getByRole('button', { name: 'Gestreept' }))
|
|
57
|
+
|
|
58
|
+
await waitFor(() => expect(row.className).toContain('grid-rows-[1fr]'))
|
|
59
|
+
expect(row.getAttribute('inert')).toBeNull()
|
|
60
|
+
expect(readout(canvasElement)).toBe('dashed / 2')
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Dotted starts at 2 px. An incoming 1 shows as 2 on the slider but stays 1 in
|
|
66
|
+
* the model: the consumer bumps it in the same patch as the style change, so
|
|
67
|
+
* the component does not write it on its own.
|
|
68
|
+
*/
|
|
69
|
+
export const DottedAtOnePx: Story = {
|
|
70
|
+
render: (args) => render(args, 'dotted', 1),
|
|
71
|
+
play: async ({ canvasElement }) => {
|
|
72
|
+
const canvas = within(canvasElement)
|
|
73
|
+
const thumb = canvas.getByRole('slider', { name: 'Dikte' })
|
|
74
|
+
|
|
75
|
+
expect(thumb).toHaveAttribute('aria-valuetext', '2 px')
|
|
76
|
+
expect(readout(canvasElement)).toBe('dotted / 1')
|
|
77
|
+
|
|
78
|
+
thumb.focus()
|
|
79
|
+
await userEvent.keyboard('{ArrowRight}')
|
|
80
|
+
await waitFor(() => expect(readout(canvasElement)).toBe('dotted / 3'))
|
|
81
|
+
expect(thumb).toHaveAttribute('aria-valuetext', '3 px')
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Every option is a drawing without a tooltip, like the other editor toggle
|
|
87
|
+
* groups; its copy is the accessible name, so each one is still named.
|
|
88
|
+
*/
|
|
89
|
+
export const OptionNames: Story = {
|
|
90
|
+
render: (args) => render(args, 'solid', 3),
|
|
91
|
+
play: async ({ canvasElement }) => {
|
|
92
|
+
const canvas = within(canvasElement)
|
|
93
|
+
|
|
94
|
+
for (const name of ['Geen', 'Doorgetrokken', 'Gestreept', 'Gestippeld']) {
|
|
95
|
+
expect(canvas.getByRole('button', { name })).toBeInTheDocument()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await userEvent.hover(canvas.getByRole('button', { name: 'Gestippeld' }))
|
|
99
|
+
expect(document.querySelector('[data-reka-popper-content-wrapper]')).toBeNull()
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const CustomMessages: Story = {
|
|
104
|
+
args: {
|
|
105
|
+
label: 'Line',
|
|
106
|
+
messages: {
|
|
107
|
+
none: 'None',
|
|
108
|
+
solid: 'Solid',
|
|
109
|
+
dashed: 'Dashed',
|
|
110
|
+
dotted: 'Dotted',
|
|
111
|
+
thickness: 'Thickness',
|
|
112
|
+
unit: 'px',
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
render: (args) => render(args, 'dashed', 4),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The style options are one tab stop: arrows move between them, and Tab goes
|
|
120
|
+
* straight on to the thickness slider.
|
|
121
|
+
*/
|
|
122
|
+
export const KeyboardOrder: Story = {
|
|
123
|
+
render: (args) => render(args, 'dashed', 3),
|
|
124
|
+
play: async ({ canvasElement }) => {
|
|
125
|
+
const canvas = within(canvasElement)
|
|
126
|
+
|
|
127
|
+
canvas.getByRole('button', { name: 'Gestreept' }).focus()
|
|
128
|
+
await userEvent.keyboard('{ArrowRight}')
|
|
129
|
+
expect(canvas.getByRole('button', { name: 'Gestippeld' })).toHaveFocus()
|
|
130
|
+
expect(readout(canvasElement)).toBe('dashed / 3')
|
|
131
|
+
|
|
132
|
+
await userEvent.tab()
|
|
133
|
+
expect(canvas.getByRole('slider', { name: 'Dikte' })).toHaveFocus()
|
|
134
|
+
},
|
|
135
|
+
}
|