@nexxtmove/ui 2.6.0 → 2.8.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/EditorColor/EditorColor.vue.d.ts +7 -5
- package/dist/components/editor/molecules/EditorColor/_ColorField.vue.d.ts +4 -1
- package/dist/components/organisms/ColorPicker/ColorPicker.vue.d.ts +3 -1
- package/dist/index.js +95 -172
- 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 +633 -0
- package/src/components/editor/molecules/EditorColor/EditorColor.test.ts +776 -0
- package/src/components/editor/molecules/EditorColor/EditorColor.vue +7 -5
- package/src/components/editor/molecules/EditorColor/EditorColorGradient.test.ts +832 -0
- package/src/components/editor/molecules/EditorColor/_ColorField.vue +20 -15
- 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 +696 -0
- package/src/components/organisms/ColorPicker/ColorPicker.vue +12 -3
- 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,677 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { expect, screen, userEvent, waitFor, within } from 'storybook/test'
|
|
3
|
+
import { ref, onMounted } from 'vue'
|
|
4
|
+
import FloatingPanel from './FloatingPanel.vue'
|
|
5
|
+
import NexxtButton from '../Button/Button.vue'
|
|
6
|
+
import NexxtModal from '../../organisms/Modal/Modal.vue'
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
title: 'Components/Atoms/Floating Panel',
|
|
10
|
+
component: FloatingPanel,
|
|
11
|
+
decorators: [
|
|
12
|
+
() => ({
|
|
13
|
+
// Room for an open panel above or below a centred trigger, also inline on the Docs page.
|
|
14
|
+
template:
|
|
15
|
+
'<div style="min-height: 400px; padding: 48px; display: flex; align-items: center; justify-content: center;"><story /></div>',
|
|
16
|
+
}),
|
|
17
|
+
],
|
|
18
|
+
argTypes: {
|
|
19
|
+
placement: {
|
|
20
|
+
control: 'select',
|
|
21
|
+
options: [
|
|
22
|
+
'bottom-start',
|
|
23
|
+
'bottom',
|
|
24
|
+
'bottom-end',
|
|
25
|
+
'top-start',
|
|
26
|
+
'top',
|
|
27
|
+
'top-end',
|
|
28
|
+
'left-start',
|
|
29
|
+
'left',
|
|
30
|
+
'left-end',
|
|
31
|
+
'right-start',
|
|
32
|
+
'right',
|
|
33
|
+
'right-end',
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
offsetDistance: { control: 'number' },
|
|
37
|
+
},
|
|
38
|
+
} satisfies Meta<typeof FloatingPanel>
|
|
39
|
+
|
|
40
|
+
type Story = StoryObj<typeof FloatingPanel>
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The panel scales and fades in from its trigger. Under `prefers-reduced-motion: reduce` it
|
|
44
|
+
* appears and disappears without animation; to see that here, use DevTools → Rendering →
|
|
45
|
+
* "Emulate CSS media feature prefers-reduced-motion".
|
|
46
|
+
*/
|
|
47
|
+
export const Default: Story = {
|
|
48
|
+
render: (args) => ({
|
|
49
|
+
components: { FloatingPanel, NexxtButton },
|
|
50
|
+
setup() {
|
|
51
|
+
return { args }
|
|
52
|
+
},
|
|
53
|
+
template: `
|
|
54
|
+
<FloatingPanel v-bind="args">
|
|
55
|
+
<template #trigger="{ isOpen, toggle }">
|
|
56
|
+
<NexxtButton variant="secondary" :aria-expanded="isOpen" @click="toggle">
|
|
57
|
+
{{ isOpen ? 'Close' : 'Open panel' }}
|
|
58
|
+
</NexxtButton>
|
|
59
|
+
</template>
|
|
60
|
+
<template #content>
|
|
61
|
+
<div style="background: white; border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); min-width: 200px;">
|
|
62
|
+
<p style="margin: 0; font-size: 14px; color: #374151;">Floating panel content</p>
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
65
|
+
</FloatingPanel>
|
|
66
|
+
`,
|
|
67
|
+
}),
|
|
68
|
+
args: {
|
|
69
|
+
placement: 'bottom-start',
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const AllPlacements: Story = {
|
|
74
|
+
render: () => ({
|
|
75
|
+
components: { FloatingPanel, NexxtButton },
|
|
76
|
+
setup() {
|
|
77
|
+
const placements = [
|
|
78
|
+
'bottom-start',
|
|
79
|
+
'bottom',
|
|
80
|
+
'bottom-end',
|
|
81
|
+
'top-start',
|
|
82
|
+
'top',
|
|
83
|
+
'top-end',
|
|
84
|
+
'right-start',
|
|
85
|
+
'right',
|
|
86
|
+
'left-start',
|
|
87
|
+
'left',
|
|
88
|
+
] as const
|
|
89
|
+
return { placements }
|
|
90
|
+
},
|
|
91
|
+
template: `
|
|
92
|
+
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; max-width: 560px;">
|
|
93
|
+
<FloatingPanel v-for="p in placements" :key="p" :placement="p">
|
|
94
|
+
<template #trigger="{ toggle, isOpen }">
|
|
95
|
+
<NexxtButton variant="secondary" :aria-expanded="isOpen" @click="toggle">
|
|
96
|
+
{{ p }}
|
|
97
|
+
</NexxtButton>
|
|
98
|
+
</template>
|
|
99
|
+
<template #content>
|
|
100
|
+
<div style="background: white; border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); font-size: 13px; color: #374151;">
|
|
101
|
+
Placed: {{ p }}
|
|
102
|
+
</div>
|
|
103
|
+
</template>
|
|
104
|
+
</FloatingPanel>
|
|
105
|
+
</div>
|
|
106
|
+
`,
|
|
107
|
+
}),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const InsideOverflowHidden: Story = {
|
|
111
|
+
decorators: [
|
|
112
|
+
() => ({
|
|
113
|
+
// The panel is meant to stick out of the dashed box; the padding keeps it inside the story.
|
|
114
|
+
template: `
|
|
115
|
+
<div style="padding-bottom: 160px;">
|
|
116
|
+
<div style="overflow: hidden; border: 2px dashed #f87171; border-radius: 8px; padding: 32px; display: flex; align-items: center; justify-content: center; min-height: 200px;">
|
|
117
|
+
<p style="position: absolute; top: 8px; left: 12px; font-size: 11px; color: #f87171; margin: 0;">overflow: hidden container: panel escapes via position: fixed</p>
|
|
118
|
+
<story />
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
`,
|
|
122
|
+
}),
|
|
123
|
+
],
|
|
124
|
+
render: (args) => ({
|
|
125
|
+
components: { FloatingPanel, NexxtButton },
|
|
126
|
+
setup() {
|
|
127
|
+
return { args }
|
|
128
|
+
},
|
|
129
|
+
template: `
|
|
130
|
+
<FloatingPanel v-bind="args">
|
|
131
|
+
<template #trigger="{ toggle, isOpen }">
|
|
132
|
+
<NexxtButton variant="secondary" :aria-expanded="isOpen" @click="toggle">
|
|
133
|
+
Open (escapes overflow)
|
|
134
|
+
</NexxtButton>
|
|
135
|
+
</template>
|
|
136
|
+
<template #content>
|
|
137
|
+
<div style="background: white; border: 1px solid #e5e7eb; border-radius: 8px; padding: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); max-width: 280px;">
|
|
138
|
+
<p style="margin: 0 0 8px; font-size: 14px; font-weight: 600; color: #111827;">Escaped via position: fixed</p>
|
|
139
|
+
<p style="margin: 0 0 12px; font-size: 13px; color: #6b7280;">This panel is rendered next to its trigger, but <code>position: fixed</code> takes it out of the container's clipping.</p>
|
|
140
|
+
<p style="margin: 0; font-size: 13px; color: #6b7280;">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt.</p>
|
|
141
|
+
</div>
|
|
142
|
+
</template>
|
|
143
|
+
</FloatingPanel>
|
|
144
|
+
`,
|
|
145
|
+
}),
|
|
146
|
+
args: {
|
|
147
|
+
placement: 'bottom-start',
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const WithInputFocus: Story = {
|
|
152
|
+
render: (args) => ({
|
|
153
|
+
components: { FloatingPanel },
|
|
154
|
+
setup() {
|
|
155
|
+
return { args }
|
|
156
|
+
},
|
|
157
|
+
template: `
|
|
158
|
+
<FloatingPanel v-bind="args">
|
|
159
|
+
<template #trigger="{ isOpen, open }">
|
|
160
|
+
<input
|
|
161
|
+
type="text"
|
|
162
|
+
placeholder="Focus me to open the panel"
|
|
163
|
+
:aria-expanded="isOpen"
|
|
164
|
+
@focus="open"
|
|
165
|
+
style="border: 1px solid #d1d5db; border-radius: 6px; padding: 8px 12px; font-size: 14px; outline: none; width: 240px; box-sizing: border-box;"
|
|
166
|
+
/>
|
|
167
|
+
</template>
|
|
168
|
+
<template #content>
|
|
169
|
+
<div style="background: white; border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); min-width: 240px;">
|
|
170
|
+
<p style="margin: 0 0 8px; font-size: 14px; font-weight: 600; color: #111827;">Suggestions</p>
|
|
171
|
+
<ul style="margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 4px;">
|
|
172
|
+
<li style="font-size: 14px; color: #374151; padding: 6px 8px; border-radius: 4px; cursor: pointer; background: #f9fafb;">Option A</li>
|
|
173
|
+
<li style="font-size: 14px; color: #374151; padding: 6px 8px; border-radius: 4px; cursor: pointer;">Option B</li>
|
|
174
|
+
<li style="font-size: 14px; color: #374151; padding: 6px 8px; border-radius: 4px; cursor: pointer;">Option C</li>
|
|
175
|
+
</ul>
|
|
176
|
+
</div>
|
|
177
|
+
</template>
|
|
178
|
+
</FloatingPanel>
|
|
179
|
+
`,
|
|
180
|
+
}),
|
|
181
|
+
args: {
|
|
182
|
+
placement: 'bottom-start',
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export const WithCloseSlotProp: Story = {
|
|
187
|
+
render: (args) => ({
|
|
188
|
+
components: { FloatingPanel, NexxtButton },
|
|
189
|
+
setup() {
|
|
190
|
+
const log = ref<string[]>([])
|
|
191
|
+
return { args, log }
|
|
192
|
+
},
|
|
193
|
+
template: `
|
|
194
|
+
<div style="display: flex; flex-direction: column; align-items: center; gap: 12px;">
|
|
195
|
+
<FloatingPanel v-bind="args" @open="log.push('opened')" @close="log.push('closed')">
|
|
196
|
+
<template #trigger="{ toggle, isOpen }">
|
|
197
|
+
<NexxtButton variant="secondary" :aria-expanded="isOpen" @click="toggle">
|
|
198
|
+
Toggle
|
|
199
|
+
</NexxtButton>
|
|
200
|
+
</template>
|
|
201
|
+
<template #content="{ close }">
|
|
202
|
+
<div style="background: white; border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); display: flex; flex-direction: column; gap: 8px; min-width: 180px;">
|
|
203
|
+
<p style="margin: 0; font-size: 14px; color: #374151;">Panel is open</p>
|
|
204
|
+
<NexxtButton variant="danger" @click="close">Close from inside</NexxtButton>
|
|
205
|
+
</div>
|
|
206
|
+
</template>
|
|
207
|
+
</FloatingPanel>
|
|
208
|
+
<ul style="font-size: 12px; color: #6b7280; list-style: none; padding: 0; margin: 0;">
|
|
209
|
+
<li v-for="(entry, i) in log" :key="i">{{ entry }}</li>
|
|
210
|
+
</ul>
|
|
211
|
+
</div>
|
|
212
|
+
`,
|
|
213
|
+
}),
|
|
214
|
+
args: {
|
|
215
|
+
placement: 'bottom-start',
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* `surface` (default `false`) wraps the content slot in the standard panel surface
|
|
221
|
+
* — rounded corners, a light border, a white background and a small shadow — so a
|
|
222
|
+
* consumer no longer has to hand-roll one per call site.
|
|
223
|
+
*
|
|
224
|
+
* Both panels below render the exact same unstyled content slot. The left one is
|
|
225
|
+
* the bare default: with nothing behind it, the content sits directly on the page.
|
|
226
|
+
* The right one has `surface` on. Both open on mount so the difference is visible
|
|
227
|
+
* without interacting.
|
|
228
|
+
*/
|
|
229
|
+
export const Surface: Story = {
|
|
230
|
+
render: () => ({
|
|
231
|
+
components: { FloatingPanel, NexxtButton },
|
|
232
|
+
setup() {
|
|
233
|
+
const bare = ref<InstanceType<typeof FloatingPanel> | null>(null)
|
|
234
|
+
const surfaced = ref<InstanceType<typeof FloatingPanel> | null>(null)
|
|
235
|
+
onMounted(() => {
|
|
236
|
+
bare.value?.open()
|
|
237
|
+
surfaced.value?.open()
|
|
238
|
+
})
|
|
239
|
+
return { bare, surfaced }
|
|
240
|
+
},
|
|
241
|
+
template: `
|
|
242
|
+
<div style="display: flex; gap: 160px; align-items: flex-start;">
|
|
243
|
+
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
|
|
244
|
+
<span style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: #374151;">surface: false (default)</span>
|
|
245
|
+
<FloatingPanel ref="bare" placement="bottom">
|
|
246
|
+
<template #trigger="{ toggle, isOpen }">
|
|
247
|
+
<NexxtButton variant="secondary" :aria-expanded="isOpen" @click="toggle">Bare</NexxtButton>
|
|
248
|
+
</template>
|
|
249
|
+
<template #content>
|
|
250
|
+
<div style="width: 180px;">
|
|
251
|
+
<p style="margin: 0 0 4px; font-weight: 600;">Unstyled content</p>
|
|
252
|
+
<p style="margin: 0;">No wrapper — the slot is placed as-is.</p>
|
|
253
|
+
</div>
|
|
254
|
+
</template>
|
|
255
|
+
</FloatingPanel>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<div style="display: flex; flex-direction: column; align-items: center; gap: 8px;">
|
|
259
|
+
<span style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: #374151;">surface: true</span>
|
|
260
|
+
<FloatingPanel ref="surfaced" placement="bottom" surface>
|
|
261
|
+
<template #trigger="{ toggle, isOpen }">
|
|
262
|
+
<NexxtButton variant="secondary" :aria-expanded="isOpen" @click="toggle">Surface</NexxtButton>
|
|
263
|
+
</template>
|
|
264
|
+
<template #content>
|
|
265
|
+
<div style="width: 180px;">
|
|
266
|
+
<p style="margin: 0 0 4px; font-weight: 600;">Unstyled content</p>
|
|
267
|
+
<p style="margin: 0;">Same slot, wrapped by the component.</p>
|
|
268
|
+
</div>
|
|
269
|
+
</template>
|
|
270
|
+
</FloatingPanel>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
`,
|
|
274
|
+
}),
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* `surface` on its own, opened by hand — the everyday case a consumer writes now
|
|
279
|
+
* that the panel supplies its own background.
|
|
280
|
+
*/
|
|
281
|
+
export const SurfaceInteractive: Story = {
|
|
282
|
+
render: (args) => ({
|
|
283
|
+
components: { FloatingPanel, NexxtButton },
|
|
284
|
+
setup() {
|
|
285
|
+
return { args }
|
|
286
|
+
},
|
|
287
|
+
template: `
|
|
288
|
+
<FloatingPanel v-bind="args" surface>
|
|
289
|
+
<template #trigger="{ isOpen, toggle }">
|
|
290
|
+
<NexxtButton variant="secondary" :aria-expanded="isOpen" @click="toggle">
|
|
291
|
+
{{ isOpen ? 'Close' : 'Open panel' }}
|
|
292
|
+
</NexxtButton>
|
|
293
|
+
</template>
|
|
294
|
+
<template #content>
|
|
295
|
+
<div style="padding: 16px; min-width: 220px; max-width: 280px;">
|
|
296
|
+
<p style="margin: 0 0 8px; font-weight: 600;">Surface panel</p>
|
|
297
|
+
<p style="margin: 0;">The border, radius, white background and shadow come from the component.</p>
|
|
298
|
+
</div>
|
|
299
|
+
</template>
|
|
300
|
+
</FloatingPanel>
|
|
301
|
+
`,
|
|
302
|
+
}),
|
|
303
|
+
args: {
|
|
304
|
+
placement: 'bottom-start',
|
|
305
|
+
},
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* `surface` over busy page content, which is the case it exists for: the panel has
|
|
310
|
+
* to stay legible when something is scrolling underneath it. The bare default is
|
|
311
|
+
* shown over the same content for comparison.
|
|
312
|
+
*/
|
|
313
|
+
export const SurfaceOverContent: Story = {
|
|
314
|
+
render: () => ({
|
|
315
|
+
components: { FloatingPanel, NexxtButton },
|
|
316
|
+
setup() {
|
|
317
|
+
const bare = ref<InstanceType<typeof FloatingPanel> | null>(null)
|
|
318
|
+
const surfaced = ref<InstanceType<typeof FloatingPanel> | null>(null)
|
|
319
|
+
onMounted(() => {
|
|
320
|
+
bare.value?.open()
|
|
321
|
+
surfaced.value?.open()
|
|
322
|
+
})
|
|
323
|
+
return { bare, surfaced }
|
|
324
|
+
},
|
|
325
|
+
template: `
|
|
326
|
+
<div style="position: relative; width: 620px;">
|
|
327
|
+
<p style="position: absolute; inset: 60px 0 auto 0; margin: 0; font-size: 13px; line-height: 22px; color: #4b5563;">
|
|
328
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
|
|
329
|
+
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
|
|
330
|
+
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
|
331
|
+
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore.
|
|
332
|
+
</p>
|
|
333
|
+
<div style="display: flex; gap: 200px; position: relative;">
|
|
334
|
+
<FloatingPanel ref="bare" placement="bottom">
|
|
335
|
+
<template #trigger="{ toggle, isOpen }">
|
|
336
|
+
<NexxtButton variant="secondary" :aria-expanded="isOpen" @click="toggle">Bare</NexxtButton>
|
|
337
|
+
</template>
|
|
338
|
+
<template #content>
|
|
339
|
+
<div style="width: 170px;">Content with nothing behind it.</div>
|
|
340
|
+
</template>
|
|
341
|
+
</FloatingPanel>
|
|
342
|
+
|
|
343
|
+
<FloatingPanel ref="surfaced" placement="bottom" surface>
|
|
344
|
+
<template #trigger="{ toggle, isOpen }">
|
|
345
|
+
<NexxtButton variant="secondary" :aria-expanded="isOpen" @click="toggle">Surface</NexxtButton>
|
|
346
|
+
</template>
|
|
347
|
+
<template #content>
|
|
348
|
+
<div style="width: 170px; padding: 12px;">Content on its own surface.</div>
|
|
349
|
+
</template>
|
|
350
|
+
</FloatingPanel>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
`,
|
|
354
|
+
}),
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* `surface="raised"` is the surface for large panels that cover a stretch of white page or
|
|
359
|
+
* canvas, such as a picker: a gray-300 border and `shadow-lg`, so the panel reads as a layer
|
|
360
|
+
* above the page. `surface` (`true`) stays the surface for dropdowns, selects, menus and
|
|
361
|
+
* suggestions. Rule of thumb: if the panel covers content the user was just looking at, raise it.
|
|
362
|
+
*/
|
|
363
|
+
export const Raised: Story = {
|
|
364
|
+
render: () => ({
|
|
365
|
+
components: { FloatingPanel, NexxtButton },
|
|
366
|
+
setup() {
|
|
367
|
+
const standard = ref<InstanceType<typeof FloatingPanel> | null>(null)
|
|
368
|
+
const raised = ref<InstanceType<typeof FloatingPanel> | null>(null)
|
|
369
|
+
onMounted(() => {
|
|
370
|
+
standard.value?.open()
|
|
371
|
+
raised.value?.open()
|
|
372
|
+
})
|
|
373
|
+
return { standard, raised }
|
|
374
|
+
},
|
|
375
|
+
template: `
|
|
376
|
+
<div style="position: relative; width: 100%; background: white; display: grid; grid-template-columns: repeat(auto-fit, 482px); justify-content: center; column-gap: 48px;">
|
|
377
|
+
<div aria-hidden="true" style="position: absolute; inset: 48px 0 0 0; overflow: hidden; font-size: 13px; line-height: 22px; color: #4b5563;">
|
|
378
|
+
<p v-for="n in 8" :key="n" style="margin: 0 0 8px;">
|
|
379
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
|
|
380
|
+
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
|
|
381
|
+
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure
|
|
382
|
+
dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
|
|
383
|
+
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt.
|
|
384
|
+
</p>
|
|
385
|
+
</div>
|
|
386
|
+
<div style="min-height: 340px;">
|
|
387
|
+
<FloatingPanel ref="standard" placement="bottom-start" surface>
|
|
388
|
+
<template #trigger="{ toggle, isOpen }">
|
|
389
|
+
<NexxtButton variant="secondary" :aria-expanded="isOpen" @click="toggle">surface</NexxtButton>
|
|
390
|
+
</template>
|
|
391
|
+
<template #content>
|
|
392
|
+
<div data-testid="content-surface" style="width: 480px; height: 280px; padding: 16px;">
|
|
393
|
+
Small-panel surface over a large area.
|
|
394
|
+
</div>
|
|
395
|
+
</template>
|
|
396
|
+
</FloatingPanel>
|
|
397
|
+
</div>
|
|
398
|
+
<div style="min-height: 340px;">
|
|
399
|
+
<FloatingPanel ref="raised" placement="bottom-start" surface="raised">
|
|
400
|
+
<template #trigger="{ toggle, isOpen }">
|
|
401
|
+
<NexxtButton variant="secondary" :aria-expanded="isOpen" @click="toggle">surface="raised"</NexxtButton>
|
|
402
|
+
</template>
|
|
403
|
+
<template #content>
|
|
404
|
+
<div data-testid="content-raised" style="width: 480px; height: 280px; padding: 16px;">
|
|
405
|
+
Raised surface: reads as a layer above the page.
|
|
406
|
+
</div>
|
|
407
|
+
</template>
|
|
408
|
+
</FloatingPanel>
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
`,
|
|
412
|
+
}),
|
|
413
|
+
play: async ({ canvasElement }) => {
|
|
414
|
+
const canvas = within(canvasElement)
|
|
415
|
+
const standard = (await canvas.findByTestId('content-surface')).parentElement!
|
|
416
|
+
const raised = (await canvas.findByTestId('content-raised')).parentElement!
|
|
417
|
+
|
|
418
|
+
// Resolve the token through an inline style, so the expected value does not depend on the
|
|
419
|
+
// same Tailwind scan that produced the classes under test.
|
|
420
|
+
const probe = document.createElement('div')
|
|
421
|
+
probe.style.color = 'var(--color-gray-300)'
|
|
422
|
+
document.body.append(probe)
|
|
423
|
+
const gray300 = getComputedStyle(probe).color
|
|
424
|
+
probe.remove()
|
|
425
|
+
await expect(
|
|
426
|
+
getComputedStyle(document.documentElement).getPropertyValue('--color-gray-300').trim(),
|
|
427
|
+
).not.toBe('')
|
|
428
|
+
|
|
429
|
+
await expect(getComputedStyle(raised).borderTopColor).toBe(gray300)
|
|
430
|
+
await expect(getComputedStyle(raised).boxShadow).not.toBe('none')
|
|
431
|
+
await expect(getComputedStyle(raised).boxShadow).not.toBe(getComputedStyle(standard).boxShadow)
|
|
432
|
+
await expect(getComputedStyle(standard).borderTopColor).not.toBe(gray300)
|
|
433
|
+
},
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* `fitHeight` hands the content the room on the roomiest side of the trigger, as the
|
|
438
|
+
* `availableHeight` slot prop and as `--floating-panel-available-height`. The panel sets no
|
|
439
|
+
* height itself; the content decides. Here the trigger sits at the bottom of the viewport, so the
|
|
440
|
+
* list is capped to the room above it and the panel flips up.
|
|
441
|
+
*/
|
|
442
|
+
export const FitHeight: Story = {
|
|
443
|
+
parameters: { layout: 'fullscreen' },
|
|
444
|
+
decorators: [
|
|
445
|
+
() => ({
|
|
446
|
+
// The meta decorator adds 48px padding on each side; together they fill the viewport.
|
|
447
|
+
template:
|
|
448
|
+
'<div style="height: calc(100dvh - 96px); display: flex; align-items: end;"><story /></div>',
|
|
449
|
+
}),
|
|
450
|
+
],
|
|
451
|
+
render: () => ({
|
|
452
|
+
components: { FloatingPanel, NexxtButton },
|
|
453
|
+
setup() {
|
|
454
|
+
const rows = Array.from({ length: 60 }, (_, i) => `Row ${i + 1}`)
|
|
455
|
+
return { rows }
|
|
456
|
+
},
|
|
457
|
+
template: `
|
|
458
|
+
<FloatingPanel placement="bottom-start" surface fit-height>
|
|
459
|
+
<template #trigger="{ toggle, isOpen }">
|
|
460
|
+
<NexxtButton variant="secondary" :aria-expanded="isOpen" @click="toggle">Open long list</NexxtButton>
|
|
461
|
+
</template>
|
|
462
|
+
<template #content="{ availableHeight }">
|
|
463
|
+
<ul
|
|
464
|
+
data-testid="fit-list"
|
|
465
|
+
:data-available-height="availableHeight"
|
|
466
|
+
:style="{ maxHeight: availableHeight + 'px' }"
|
|
467
|
+
class="overflow-y-auto"
|
|
468
|
+
style="margin: 0; padding: 8px 16px; list-style: none; width: 220px;"
|
|
469
|
+
>
|
|
470
|
+
<li v-for="row in rows" :key="row" style="padding: 4px 0;">{{ row }}</li>
|
|
471
|
+
</ul>
|
|
472
|
+
</template>
|
|
473
|
+
</FloatingPanel>
|
|
474
|
+
`,
|
|
475
|
+
}),
|
|
476
|
+
play: async ({ canvasElement }) => {
|
|
477
|
+
const canvas = within(canvasElement)
|
|
478
|
+
const trigger = canvas.getByRole('button', { name: 'Open long list' })
|
|
479
|
+
await userEvent.click(trigger)
|
|
480
|
+
const list = await canvas.findByTestId('fit-list')
|
|
481
|
+
|
|
482
|
+
const triggerRect = trigger.getBoundingClientRect()
|
|
483
|
+
const viewport = document.documentElement.clientHeight
|
|
484
|
+
const expected =
|
|
485
|
+
Math.floor(Math.max(triggerRect.top, viewport - triggerRect.bottom)) - 8 - 8 - 2
|
|
486
|
+
await expect(Number(list.dataset.availableHeight)).toBe(expected)
|
|
487
|
+
|
|
488
|
+
// Measure the positioning element, which is never transformed by the enter transition.
|
|
489
|
+
const panel = list.closest('.z-50')!
|
|
490
|
+
await waitFor(() => {
|
|
491
|
+
const rect = panel.getBoundingClientRect()
|
|
492
|
+
// the list uses all the room it gets: 8px from the edge, less than a pixel of rounding
|
|
493
|
+
expect(rect.top).toBeGreaterThanOrEqual(8)
|
|
494
|
+
expect(rect.top).toBeLessThan(9)
|
|
495
|
+
expect(rect.bottom).toBeLessThanOrEqual(triggerRect.top)
|
|
496
|
+
})
|
|
497
|
+
},
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Two sibling wrappers with the same `z-index`: the first holds the trigger, the second overlaps
|
|
502
|
+
* the area where the panel opens. Left, without `teleport`, the panel lives in the first
|
|
503
|
+
* wrapper's stacking context, so the later sibling paints over it. Right, with `teleport`, the
|
|
504
|
+
* panel renders at the end of `<body>` and stays on top.
|
|
505
|
+
*/
|
|
506
|
+
export const StackingContext: Story = {
|
|
507
|
+
render: () => ({
|
|
508
|
+
components: { FloatingPanel, NexxtButton },
|
|
509
|
+
setup() {
|
|
510
|
+
// `ref` inside v-for collects every instance into this array
|
|
511
|
+
const panels = ref<InstanceType<typeof FloatingPanel>[]>([])
|
|
512
|
+
onMounted(() => panels.value.forEach((panel) => panel.open()))
|
|
513
|
+
const columns = [
|
|
514
|
+
{ key: 'in-place', label: 'teleport: false', teleport: false },
|
|
515
|
+
{ key: 'teleported', label: 'teleport: true', teleport: true },
|
|
516
|
+
]
|
|
517
|
+
return { panels, columns }
|
|
518
|
+
},
|
|
519
|
+
template: `
|
|
520
|
+
<div style="display: flex; gap: 32px; align-items: flex-start;">
|
|
521
|
+
<div v-for="column in columns" :key="column.key" :data-column="column.key" style="width: 180px;">
|
|
522
|
+
<p style="margin: 0 0 8px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: #374151;">{{ column.label }}</p>
|
|
523
|
+
<div class="relative z-10">
|
|
524
|
+
<FloatingPanel ref="panels" :teleport="column.teleport" surface>
|
|
525
|
+
<template #trigger="{ toggle, isOpen }">
|
|
526
|
+
<NexxtButton variant="secondary" :aria-expanded="isOpen" @click="toggle">Open</NexxtButton>
|
|
527
|
+
</template>
|
|
528
|
+
<template #content>
|
|
529
|
+
<div :data-testid="'stack-panel-' + column.key" style="width: 160px; height: 96px; padding: 12px;">Panel</div>
|
|
530
|
+
</template>
|
|
531
|
+
</FloatingPanel>
|
|
532
|
+
</div>
|
|
533
|
+
<div data-sibling class="relative z-10" style="margin-top: 24px; height: 120px; background: #fde68a; border-radius: 8px; padding: 8px; font-size: 12px; color: #1f2937;">
|
|
534
|
+
Sibling, same z-index
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
`,
|
|
539
|
+
}),
|
|
540
|
+
play: async ({ canvasElement }) => {
|
|
541
|
+
const topAt = async (key: string) => {
|
|
542
|
+
const column = canvasElement.querySelector(`[data-column="${key}"]`)!
|
|
543
|
+
const sibling = column.querySelector('[data-sibling]')!
|
|
544
|
+
// Teleported content is outside canvasElement, so search the whole document.
|
|
545
|
+
const panel = await within(document.body).findByTestId(`stack-panel-${key}`)
|
|
546
|
+
// Wait for the enter transition: during the scale-in the rect is still growing.
|
|
547
|
+
await new Promise((resolve) => setTimeout(resolve, 300))
|
|
548
|
+
const a = panel.getBoundingClientRect()
|
|
549
|
+
const b = sibling.getBoundingClientRect()
|
|
550
|
+
const x = (Math.max(a.left, b.left) + Math.min(a.right, b.right)) / 2
|
|
551
|
+
const y = (Math.max(a.top, b.top) + Math.min(a.bottom, b.bottom)) / 2
|
|
552
|
+
await expect(Math.min(a.bottom, b.bottom)).toBeGreaterThan(Math.max(a.top, b.top))
|
|
553
|
+
return { hit: document.elementFromPoint(x, y), panel }
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const left = await topAt('in-place')
|
|
557
|
+
await expect(left.hit?.closest('[data-sibling]')).not.toBeNull()
|
|
558
|
+
|
|
559
|
+
const right = await topAt('teleported')
|
|
560
|
+
await expect(right.panel.contains(right.hit)).toBe(true)
|
|
561
|
+
},
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* With `teleport`, the panel sits at the end of `<body>`, yet Tab and Shift+Tab walk the page as
|
|
566
|
+
* if it still followed the trigger: three invisible focus guards hand focus on. Tab from the
|
|
567
|
+
* trigger enters the panel, Tab from its last control continues after the trigger, and Shift+Tab
|
|
568
|
+
* walks the same way back. Tab from the last tab stop of the page skips the panel and leaves the
|
|
569
|
+
* document, as it would without teleport. Closing with focus inside puts focus back on the
|
|
570
|
+
* trigger.
|
|
571
|
+
*/
|
|
572
|
+
export const TeleportKeyboard: Story = {
|
|
573
|
+
render: () => ({
|
|
574
|
+
components: { FloatingPanel, NexxtButton },
|
|
575
|
+
template: `
|
|
576
|
+
<div style="display: flex; gap: 12px; align-items: center;">
|
|
577
|
+
<NexxtButton variant="secondary">Voor</NexxtButton>
|
|
578
|
+
<FloatingPanel teleport surface>
|
|
579
|
+
<template #trigger="{ toggle, isOpen }">
|
|
580
|
+
<NexxtButton variant="secondary" :aria-expanded="isOpen" @click="toggle">Trigger</NexxtButton>
|
|
581
|
+
</template>
|
|
582
|
+
<template #content>
|
|
583
|
+
<div style="display: flex; gap: 8px; padding: 12px;">
|
|
584
|
+
<NexxtButton variant="secondary">Knop 1</NexxtButton>
|
|
585
|
+
<NexxtButton variant="secondary">Knop 2</NexxtButton>
|
|
586
|
+
</div>
|
|
587
|
+
</template>
|
|
588
|
+
</FloatingPanel>
|
|
589
|
+
<NexxtButton variant="secondary">Na</NexxtButton>
|
|
590
|
+
</div>
|
|
591
|
+
`,
|
|
592
|
+
}),
|
|
593
|
+
play: async ({ canvasElement }) => {
|
|
594
|
+
const canvas = within(canvasElement)
|
|
595
|
+
const trigger = canvas.getByRole('button', { name: 'Trigger' })
|
|
596
|
+
await userEvent.click(trigger)
|
|
597
|
+
const first = await screen.findByRole('button', { name: 'Knop 1' })
|
|
598
|
+
const second = screen.getByRole('button', { name: 'Knop 2' })
|
|
599
|
+
const after = canvas.getByRole('button', { name: 'Na' })
|
|
600
|
+
await expect(canvasElement.contains(first)).toBe(false)
|
|
601
|
+
await expect(trigger).toHaveFocus()
|
|
602
|
+
|
|
603
|
+
for (const next of [first, second, after]) {
|
|
604
|
+
await userEvent.tab()
|
|
605
|
+
await expect(next).toHaveFocus()
|
|
606
|
+
}
|
|
607
|
+
for (const previous of [second, first, trigger]) {
|
|
608
|
+
await userEvent.tab({ shift: true })
|
|
609
|
+
await expect(previous).toHaveFocus()
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Escape from inside: focus goes back to the trigger instead of dropping to <body>
|
|
613
|
+
await userEvent.tab()
|
|
614
|
+
await expect(first).toHaveFocus()
|
|
615
|
+
await userEvent.keyboard('{Escape}')
|
|
616
|
+
await waitFor(() => expect(first).not.toBeInTheDocument())
|
|
617
|
+
await expect(trigger).toHaveFocus()
|
|
618
|
+
},
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Inside a `role="dialog"` such as `NexxtModal`, `teleport` is ignored. Reka makes everything
|
|
623
|
+
* outside the dialog `pointer-events: none` and traps focus inside it, so a panel in `<body>`
|
|
624
|
+
* could not be clicked, a click on it would land on the overlay and close the modal, and focus
|
|
625
|
+
* would be pulled back into the dialog. The panel stays next to its trigger instead;
|
|
626
|
+
* `position: fixed` already escapes the dialog's overflow.
|
|
627
|
+
*/
|
|
628
|
+
export const TeleportInModal: Story = {
|
|
629
|
+
render: () => ({
|
|
630
|
+
components: { FloatingPanel, NexxtButton, NexxtModal },
|
|
631
|
+
setup() {
|
|
632
|
+
const open = ref(false)
|
|
633
|
+
const query = ref('')
|
|
634
|
+
return { open, query }
|
|
635
|
+
},
|
|
636
|
+
template: `
|
|
637
|
+
<NexxtButton @click="open = true">Open modal</NexxtButton>
|
|
638
|
+
<NexxtModal v-model="open" title="Modal with a floating panel">
|
|
639
|
+
<p style="margin: 0 0 12px; font-size: 13px; color: #4b5563;">
|
|
640
|
+
This panel sets <code>teleport</code>, but inside a dialog it is ignored: the panel stays
|
|
641
|
+
next to its trigger.
|
|
642
|
+
</p>
|
|
643
|
+
<FloatingPanel teleport surface>
|
|
644
|
+
<template #trigger="{ toggle, isOpen }">
|
|
645
|
+
<NexxtButton variant="secondary" :aria-expanded="isOpen" @click="toggle">Open panel</NexxtButton>
|
|
646
|
+
</template>
|
|
647
|
+
<template #content>
|
|
648
|
+
<div style="padding: 12px; width: 240px;">
|
|
649
|
+
<label style="display: flex; flex-direction: column; gap: 4px; font-size: 13px;">
|
|
650
|
+
Search
|
|
651
|
+
<input v-model="query" type="text" style="border: 1px solid #d1d5db; border-radius: 6px; padding: 6px 8px;" />
|
|
652
|
+
</label>
|
|
653
|
+
</div>
|
|
654
|
+
</template>
|
|
655
|
+
</FloatingPanel>
|
|
656
|
+
</NexxtModal>
|
|
657
|
+
`,
|
|
658
|
+
}),
|
|
659
|
+
play: async ({ canvasElement }) => {
|
|
660
|
+
// Closed on mount, so the Docs page is not covered by the scrim; the play opens it.
|
|
661
|
+
await userEvent.click(within(canvasElement).getByRole('button', { name: 'Open modal' }))
|
|
662
|
+
const dialog = await screen.findByRole('dialog')
|
|
663
|
+
const modal = within(dialog)
|
|
664
|
+
await userEvent.click(modal.getByRole('button', { name: 'Open panel' }))
|
|
665
|
+
const input = await modal.findByRole('textbox', { name: 'Search' })
|
|
666
|
+
await expect(dialog.contains(input)).toBe(true)
|
|
667
|
+
|
|
668
|
+
await userEvent.click(input)
|
|
669
|
+
await userEvent.type(input, 'abc')
|
|
670
|
+
await expect(input).toHaveFocus()
|
|
671
|
+
await expect(input).toHaveValue('abc')
|
|
672
|
+
|
|
673
|
+
await userEvent.click(input.closest('div')!)
|
|
674
|
+
await expect(screen.getByRole('dialog')).toBe(dialog)
|
|
675
|
+
await expect(input).toBeInTheDocument()
|
|
676
|
+
},
|
|
677
|
+
}
|