@nexxtmove/ui 0.1.31 → 0.1.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +124 -17
- package/dist/index.js +1079 -980
- package/dist/nuxt.js +8 -7
- package/package.json +1 -1
- package/src/components/Calendar/_CalendarDayView.vue +1 -1
- package/src/components/Chip/Chip.vue +9 -4
- package/src/components/Pagination/Pagination.vue +93 -0
- package/src/components/Table/Table.vue +2 -2
- package/src/components.json +1 -0
- package/src/index.ts +1 -0
- package/src/components/AnimatedNumber/AnimatedNumber.stories.ts +0 -15
- package/src/components/AnimatedNumber/AnimatedNumber.test.ts +0 -56
- package/src/components/Avatar/Avatar.stories.ts +0 -28
- package/src/components/Avatar/Avatar.test.ts +0 -55
- package/src/components/Button/Button.stories.ts +0 -212
- package/src/components/Button/Button.test.ts +0 -318
- package/src/components/Calendar/Calendar.stories.ts +0 -91
- package/src/components/Calendar/Calendar.test.ts +0 -279
- package/src/components/Calendar/_CalendarDayView.test.ts +0 -104
- package/src/components/Calendar/_CalendarHeader.test.ts +0 -86
- package/src/components/Calendar/_CalendarMonthView.test.ts +0 -52
- package/src/components/Calendar/_CalendarYearView.test.ts +0 -56
- package/src/components/Checkbox/Checkbox.stories.ts +0 -35
- package/src/components/Checkbox/Checkbox.test.ts +0 -55
- package/src/components/Chip/Chip.stories.ts +0 -42
- package/src/components/Chip/Chip.test.ts +0 -51
- package/src/components/CustomerJourneyTile/CustomerJourneyTile.stories.ts +0 -86
- package/src/components/CustomerJourneyTile/CustomerJourneyTile.test.ts +0 -178
- package/src/components/DatePicker/DatePicker.stories.ts +0 -149
- package/src/components/DatePicker/DatePicker.test.ts +0 -191
- package/src/components/DropdownButton/DropdownButton.stories.ts +0 -68
- package/src/components/DropdownButton/DropdownButton.test.ts +0 -124
- package/src/components/Header/Header.stories.ts +0 -54
- package/src/components/Header/Header.test.ts +0 -183
- package/src/components/Icon/Icon.stories.ts +0 -50
- package/src/components/Icon/Icon.test.ts +0 -73
- package/src/components/InfoBlock/InfoBlock.stories.ts +0 -90
- package/src/components/InfoBlock/InfoBlock.test.ts +0 -101
- package/src/components/ProgressBar/ProgressBar.stories.ts +0 -30
- package/src/components/ProgressBar/ProgressBar.test.ts +0 -314
- package/src/components/SocialIcons/SocialIcons.stories.ts +0 -34
- package/src/components/SocialIcons/SocialIcons.test.ts +0 -58
- package/src/components/SocialMediaCustomTemplate/SocialMediaCustomTemplate.stories.ts +0 -11
- package/src/components/SocialMediaCustomTemplate/SocialMediaCustomTemplate.test.ts +0 -131
- package/src/components/SocialMediaTemplate/SocialMediaTemplate.stories.ts +0 -71
- package/src/components/SocialMediaTemplate/SocialMediaTemplate.test.ts +0 -466
- package/src/components/SocialMediaType/SocialMediaType.stories.ts +0 -43
- package/src/components/SocialMediaType/SocialMediaType.test.ts +0 -126
- package/src/components/StepperHeader/StepperHeader.stories.ts +0 -47
- package/src/components/StepperHeader/StepperHeader.test.ts +0 -244
- package/src/components/Table/Table.stories.ts +0 -283
- package/src/components/Table/Table.test.ts +0 -220
- package/src/components/TimelineEvent/TimelineEvent.stories.ts +0 -291
- package/src/components/TimelineEvent/TimelineEvent.test.ts +0 -166
- package/src/components/TimelinePhaseblock/TimelinePhaseblock.stories.ts +0 -198
- package/src/components/TimelinePhaseblock/TimelinePhaseblock.test.ts +0 -283
- package/src/components/Tooltip/Tooltip.stories.ts +0 -146
- package/src/components/Tooltip/Tooltip.test.ts +0 -122
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { mount } from '@vue/test-utils'
|
|
3
|
-
import CustomerJourneyTile from './CustomerJourneyTile.vue'
|
|
4
|
-
|
|
5
|
-
const stubs = { NexxtIcon: { template: '<span :data-name="name" />', props: ['name'] } }
|
|
6
|
-
|
|
7
|
-
const mountTile = (props = {}) =>
|
|
8
|
-
mount(CustomerJourneyTile, {
|
|
9
|
-
props: { ...props },
|
|
10
|
-
global: { stubs },
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
describe('CustomerJourneyTile', () => {
|
|
14
|
-
describe('rendering', () => {
|
|
15
|
-
it('renders a button element', () => {
|
|
16
|
-
const wrapper = mountTile({ title: 'Latent' })
|
|
17
|
-
expect(wrapper.element.tagName).toBe('BUTTON')
|
|
18
|
-
expect(wrapper.attributes('type')).toBe('button')
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
it('renders the title label', () => {
|
|
22
|
-
const wrapper = mountTile({ title: 'Wonen' })
|
|
23
|
-
expect(wrapper.text()).toContain('Wonen')
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('does not render title label when title is omitted', () => {
|
|
27
|
-
const wrapper = mountTile({ amount: 100 })
|
|
28
|
-
expect(wrapper.text()).not.toContain('Latent')
|
|
29
|
-
expect(wrapper.find('span:not(.heading-1)').exists()).toBe(false)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('does not render the icon div when title is omitted', () => {
|
|
33
|
-
const wrapper = mountTile({ amount: 100 })
|
|
34
|
-
expect(wrapper.find('.bg-purple-100').exists()).toBe(false)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('does not render the icon div when title does not match a phase', () => {
|
|
38
|
-
const wrapper = mountTile({ title: 'Custom Title' })
|
|
39
|
-
expect(wrapper.find('.bg-purple-100').exists()).toBe(false)
|
|
40
|
-
expect(wrapper.text()).toContain('Custom Title')
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('renders the correct icon for each phase-matching title', () => {
|
|
44
|
-
const cases: Array<[string, string]> = [
|
|
45
|
-
['Latent', 'eye-closed'],
|
|
46
|
-
['Oriëntatie', 'building-magnifying-glass'],
|
|
47
|
-
['Actief zoeken', 'house-person-return'],
|
|
48
|
-
['Selectie', 'handshake'],
|
|
49
|
-
['Transactie', 'pencil-mechanical'],
|
|
50
|
-
['Wonen', 'house-heart'],
|
|
51
|
-
]
|
|
52
|
-
for (const [title, expectedIcon] of cases) {
|
|
53
|
-
const wrapper = mountTile({ title })
|
|
54
|
-
expect(wrapper.find(`[data-name="${expectedIcon}"]`).exists()).toBe(true)
|
|
55
|
-
}
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('marks the icon as aria-hidden', () => {
|
|
59
|
-
const wrapper = mountTile({ title: 'Latent' })
|
|
60
|
-
expect(wrapper.find('[aria-hidden="true"]').exists()).toBe(true)
|
|
61
|
-
})
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
describe('accessibility', () => {
|
|
65
|
-
it('has an aria-label with the title', () => {
|
|
66
|
-
const wrapper = mountTile({ title: 'Wonen' })
|
|
67
|
-
expect(wrapper.attributes('aria-label')).toBe('Wonen')
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('includes amount in the aria-label', () => {
|
|
71
|
-
const wrapper = mountTile({ title: 'Latent', amount: 270 })
|
|
72
|
-
expect(wrapper.attributes('aria-label')).toBe('Latent, 270 contacten')
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('includes positive percentage in the aria-label', () => {
|
|
76
|
-
const wrapper = mountTile({
|
|
77
|
-
title: 'Latent',
|
|
78
|
-
percentage: 14,
|
|
79
|
-
percentageExplanaition: 'vorige maand',
|
|
80
|
-
})
|
|
81
|
-
expect(wrapper.attributes('aria-label')).toBe('Latent, +14% vorige maand')
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('includes negative percentage in the aria-label', () => {
|
|
85
|
-
const wrapper = mountTile({ title: 'Latent', percentage: -8 })
|
|
86
|
-
expect(wrapper.attributes('aria-label')).toBe('Latent, -8%')
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('has a visible focus ring via focus-visible classes', () => {
|
|
90
|
-
const wrapper = mountTile({ title: 'Latent' })
|
|
91
|
-
expect(wrapper.classes()).toContain('focus-visible:outline-2')
|
|
92
|
-
expect(wrapper.classes()).toContain('focus-visible:outline-offset-2')
|
|
93
|
-
expect(wrapper.classes()).toContain('focus-visible:outline-cornflower-blue-500')
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('applies standard padding when no icon is present', () => {
|
|
97
|
-
const wrapper = mountTile({ amount: 100 })
|
|
98
|
-
expect(wrapper.classes()).toContain('p-4')
|
|
99
|
-
expect(wrapper.classes()).not.toContain('gap-6')
|
|
100
|
-
expect(wrapper.classes()).not.toContain('p-1.5')
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('applies layout gap and small padding when icon is present', () => {
|
|
104
|
-
const wrapper = mountTile({ title: 'Latent', amount: 100 })
|
|
105
|
-
expect(wrapper.classes()).toContain('gap-6')
|
|
106
|
-
expect(wrapper.classes()).toContain('p-1.5')
|
|
107
|
-
expect(wrapper.classes()).not.toContain('p-4')
|
|
108
|
-
})
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
describe('amount', () => {
|
|
112
|
-
it('renders the amount when provided', () => {
|
|
113
|
-
const wrapper = mountTile({ title: 'Latent', amount: 270 })
|
|
114
|
-
expect(wrapper.text()).toContain('270')
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('renders the amount with purple color when a phase-matching title is provided', () => {
|
|
118
|
-
const wrapper = mountTile({ title: 'Latent', amount: 270 })
|
|
119
|
-
expect(wrapper.find('.heading-1').classes()).toContain('text-purple-500')
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('renders the amount with standard text color when title is not a known phase', () => {
|
|
123
|
-
const wrapper = mountTile({ title: 'Custom Title', amount: 270 })
|
|
124
|
-
expect(wrapper.find('.heading-1').classes()).toContain('text-sapphire-900')
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('renders the amount with standard text color when title is omitted', () => {
|
|
128
|
-
const wrapper = mountTile({ amount: 270 })
|
|
129
|
-
expect(wrapper.find('.heading-1').classes()).toContain('text-sapphire-900')
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('does not render an amount element when omitted', () => {
|
|
133
|
-
const wrapper = mountTile({ title: 'Latent' })
|
|
134
|
-
expect(wrapper.find('.heading-1').exists()).toBe(false)
|
|
135
|
-
})
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
describe('percentage', () => {
|
|
139
|
-
it('renders a positive percentage with a + prefix', () => {
|
|
140
|
-
const wrapper = mountTile({ percentage: 14 })
|
|
141
|
-
expect(wrapper.text()).toContain('+14%')
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
it('renders a negative percentage without a + prefix', () => {
|
|
145
|
-
const wrapper = mountTile({ percentage: -8 })
|
|
146
|
-
expect(wrapper.text()).toContain('-8%')
|
|
147
|
-
expect(wrapper.text()).not.toContain('+-8%')
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
it('applies green color class for a positive percentage', () => {
|
|
151
|
-
const wrapper = mountTile({ percentage: 14 })
|
|
152
|
-
expect(wrapper.find('.text-green-500').exists()).toBe(true)
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
it('applies red color class for a negative percentage', () => {
|
|
156
|
-
const wrapper = mountTile({ percentage: -8 })
|
|
157
|
-
expect(wrapper.find('.text-brick-500').exists()).toBe(true)
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
it('does not render the percentage block when omitted', () => {
|
|
161
|
-
const wrapper = mountTile()
|
|
162
|
-
expect(wrapper.find('.text-green-500').exists()).toBe(false)
|
|
163
|
-
expect(wrapper.find('.text-brick-500').exists()).toBe(false)
|
|
164
|
-
})
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
describe('percentageExplanaition', () => {
|
|
168
|
-
it('renders the explanation when provided alongside percentage', () => {
|
|
169
|
-
const wrapper = mountTile({ percentage: 14, percentageExplanaition: 'vorige maand' })
|
|
170
|
-
expect(wrapper.text()).toContain('vorige maand')
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
it('does not render the explanation when omitted', () => {
|
|
174
|
-
const wrapper = mountTile({ percentage: 14 })
|
|
175
|
-
expect(wrapper.text()).not.toContain('vorige maand')
|
|
176
|
-
})
|
|
177
|
-
})
|
|
178
|
-
})
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
-
import { ref } from 'vue'
|
|
3
|
-
import DatePicker from './DatePicker.vue'
|
|
4
|
-
|
|
5
|
-
export default {
|
|
6
|
-
title: 'Components/Molecules/DatePicker',
|
|
7
|
-
component: DatePicker,
|
|
8
|
-
decorators: [
|
|
9
|
-
() => ({ template: '<div style="min-height: 380px; padding: 16px;"><story /></div>' }),
|
|
10
|
-
],
|
|
11
|
-
argTypes: {
|
|
12
|
-
anchor: {
|
|
13
|
-
control: 'select',
|
|
14
|
-
options: ['bottom-right', 'bottom-left', 'top-right', 'top-left'],
|
|
15
|
-
},
|
|
16
|
-
monthYearPicker: { control: 'boolean' },
|
|
17
|
-
markedDates: { control: 'object' },
|
|
18
|
-
disabledDates: { control: 'object' },
|
|
19
|
-
minDate: { control: 'date' },
|
|
20
|
-
maxDate: { control: 'date' },
|
|
21
|
-
locale: { control: false },
|
|
22
|
-
},
|
|
23
|
-
parameters: {
|
|
24
|
-
design: {
|
|
25
|
-
type: 'figma',
|
|
26
|
-
url: 'https://www.figma.com/design/CdbFJ7qUga6mtjagcPDvYK/Design-System?node-id=713-355&t=CbHvOQfEMIr7M5mO-4',
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
} satisfies Meta<typeof DatePicker>
|
|
30
|
-
|
|
31
|
-
type Story = StoryObj<typeof DatePicker>
|
|
32
|
-
|
|
33
|
-
export const Default: Story = {
|
|
34
|
-
args: {
|
|
35
|
-
placeholder: 'Selecteer datum',
|
|
36
|
-
},
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const AnchorBottomRight: Story = {
|
|
40
|
-
render: (args) => ({
|
|
41
|
-
components: { DatePicker },
|
|
42
|
-
setup() {
|
|
43
|
-
return { args }
|
|
44
|
-
},
|
|
45
|
-
template: `
|
|
46
|
-
<div style="min-height: 380px; display: flex; justify-content: flex-end; align-items: flex-start; padding: 16px;">
|
|
47
|
-
<DatePicker v-bind="args" />
|
|
48
|
-
</div>
|
|
49
|
-
`,
|
|
50
|
-
}),
|
|
51
|
-
args: {
|
|
52
|
-
placeholder: 'Selecteer datum',
|
|
53
|
-
anchor: 'bottom-right',
|
|
54
|
-
},
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export const AnchorTopLeft: Story = {
|
|
58
|
-
render: (args) => ({
|
|
59
|
-
components: { DatePicker },
|
|
60
|
-
setup() {
|
|
61
|
-
return { args }
|
|
62
|
-
},
|
|
63
|
-
template: `
|
|
64
|
-
<div style="min-height: 380px; display: flex; align-items: flex-end; padding: 16px;">
|
|
65
|
-
<DatePicker v-bind="args" />
|
|
66
|
-
</div>
|
|
67
|
-
`,
|
|
68
|
-
}),
|
|
69
|
-
args: {
|
|
70
|
-
placeholder: 'Selecteer datum',
|
|
71
|
-
anchor: 'top-left',
|
|
72
|
-
},
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export const AnchorTopRight: Story = {
|
|
76
|
-
render: (args) => ({
|
|
77
|
-
components: { DatePicker },
|
|
78
|
-
setup() {
|
|
79
|
-
return { args }
|
|
80
|
-
},
|
|
81
|
-
template: `
|
|
82
|
-
<div style="min-height: 380px; display: flex; justify-content: flex-end; align-items: flex-end; padding: 16px;">
|
|
83
|
-
<DatePicker v-bind="args" />
|
|
84
|
-
</div>
|
|
85
|
-
`,
|
|
86
|
-
}),
|
|
87
|
-
args: {
|
|
88
|
-
placeholder: 'Selecteer datum',
|
|
89
|
-
anchor: 'top-right',
|
|
90
|
-
},
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export const WithoutAutoClose: Story = {
|
|
94
|
-
args: {
|
|
95
|
-
placeholder: 'Selecteer datum',
|
|
96
|
-
autoClose: false,
|
|
97
|
-
},
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export const CustomTrigger: Story = {
|
|
101
|
-
render: (args) => ({
|
|
102
|
-
components: { DatePicker },
|
|
103
|
-
setup() {
|
|
104
|
-
const selected = ref(args.modelValue)
|
|
105
|
-
return { args, selected }
|
|
106
|
-
},
|
|
107
|
-
template: `
|
|
108
|
-
<div style="min-height: 380px; padding: 16px;">
|
|
109
|
-
<p style="font-size: 13px; color: #555; margin-bottom: 12px;">Selected: {{ selected ? selected.toLocaleDateString() : 'None' }}</p>
|
|
110
|
-
<DatePicker v-bind="args" v-model="selected">
|
|
111
|
-
<template #trigger="{ toggle, isOpen }">
|
|
112
|
-
<button
|
|
113
|
-
type="button"
|
|
114
|
-
@click="toggle"
|
|
115
|
-
class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors shadow-sm font-medium"
|
|
116
|
-
>
|
|
117
|
-
{{ isOpen ? 'Sluiten' : 'Kies een datum uit de lijst' }}
|
|
118
|
-
</button>
|
|
119
|
-
</template>
|
|
120
|
-
</DatePicker>
|
|
121
|
-
</div>
|
|
122
|
-
`,
|
|
123
|
-
}),
|
|
124
|
-
args: {
|
|
125
|
-
modelValue: null,
|
|
126
|
-
},
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export const Playground: Story = {
|
|
130
|
-
render: (args) => ({
|
|
131
|
-
components: { DatePicker },
|
|
132
|
-
setup() {
|
|
133
|
-
const selected = ref<Date | null>(null)
|
|
134
|
-
return { args, selected }
|
|
135
|
-
},
|
|
136
|
-
template: `
|
|
137
|
-
<div style="min-height: 380px; padding: 16px;">
|
|
138
|
-
<DatePicker v-bind="args" v-model="selected" />
|
|
139
|
-
<p v-if="selected" style="margin-top: 12px; font-size: 13px; color: #555;">
|
|
140
|
-
Geselecteerd: {{ selected.toLocaleDateString('nl-NL') }}
|
|
141
|
-
</p>
|
|
142
|
-
</div>
|
|
143
|
-
`,
|
|
144
|
-
}),
|
|
145
|
-
args: {
|
|
146
|
-
placeholder: 'Selecteer datum',
|
|
147
|
-
monthYearPicker: true,
|
|
148
|
-
},
|
|
149
|
-
}
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from 'vitest'
|
|
2
|
-
import { mount, type VueWrapper } from '@vue/test-utils'
|
|
3
|
-
import { nextTick } from 'vue'
|
|
4
|
-
import DatePicker from './DatePicker.vue'
|
|
5
|
-
|
|
6
|
-
const YEAR = 2026
|
|
7
|
-
const MONTH = 2 // March (0-indexed)
|
|
8
|
-
const TODAY = new Date(YEAR, MONTH, 20)
|
|
9
|
-
|
|
10
|
-
const mountDatePicker = (props = {}) => mount(DatePicker, { props, attachTo: document.body })
|
|
11
|
-
|
|
12
|
-
const openPicker = async (wrapper: VueWrapper) => {
|
|
13
|
-
await wrapper.find('button').trigger('click')
|
|
14
|
-
await nextTick()
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe('DatePicker', () => {
|
|
18
|
-
let wrapper: VueWrapper
|
|
19
|
-
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
wrapper?.unmount()
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
// ── Trigger button ───────────────────────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
it('renders trigger button with placeholder', () => {
|
|
27
|
-
wrapper = mountDatePicker({ placeholder: 'Kies een datum' })
|
|
28
|
-
expect(wrapper.text()).toContain('Kies een datum')
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('uses default placeholder when none provided', () => {
|
|
32
|
-
wrapper = mountDatePicker()
|
|
33
|
-
expect(wrapper.text()).toContain('Selecteer datum')
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
// ── Open / close ─────────────────────────────────────────────────────────
|
|
37
|
-
|
|
38
|
-
it('calendar is closed by default', () => {
|
|
39
|
-
wrapper = mountDatePicker()
|
|
40
|
-
expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(false)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('opens when trigger button is clicked', async () => {
|
|
44
|
-
wrapper = mountDatePicker()
|
|
45
|
-
await openPicker(wrapper)
|
|
46
|
-
expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(true)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('closes when trigger button is clicked again', async () => {
|
|
50
|
-
wrapper = mountDatePicker()
|
|
51
|
-
await openPicker(wrapper)
|
|
52
|
-
await wrapper.find('button').trigger('click')
|
|
53
|
-
await nextTick()
|
|
54
|
-
expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(false)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('closes when clicking outside', async () => {
|
|
58
|
-
wrapper = mountDatePicker()
|
|
59
|
-
await openPicker(wrapper)
|
|
60
|
-
document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
|
|
61
|
-
await nextTick()
|
|
62
|
-
expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(false)
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('closes on Escape key', async () => {
|
|
66
|
-
wrapper = mountDatePicker()
|
|
67
|
-
await openPicker(wrapper)
|
|
68
|
-
await wrapper.trigger('keydown', { key: 'Escape' })
|
|
69
|
-
await nextTick()
|
|
70
|
-
expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(false)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
// ── Day selection + autoClose ─────────────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
it('closes after selecting a day (autoClose default)', async () => {
|
|
76
|
-
wrapper = mountDatePicker({ modelValue: TODAY })
|
|
77
|
-
await openPicker(wrapper)
|
|
78
|
-
|
|
79
|
-
const dayButtons = wrapper
|
|
80
|
-
.findAll('button[type="button"]')
|
|
81
|
-
.filter((b) => /^\d{1,2}$/.test(b.text().trim()))
|
|
82
|
-
await dayButtons[10]!.trigger('click')
|
|
83
|
-
await nextTick()
|
|
84
|
-
|
|
85
|
-
expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(false)
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
it('stays open after selecting a day if autoClose is false', async () => {
|
|
89
|
-
wrapper = mountDatePicker({ modelValue: TODAY, autoClose: false })
|
|
90
|
-
await openPicker(wrapper)
|
|
91
|
-
|
|
92
|
-
const dayButtons = wrapper
|
|
93
|
-
.findAll('button[type="button"]')
|
|
94
|
-
.filter((b) => /^\d{1,2}$/.test(b.text().trim()))
|
|
95
|
-
await dayButtons[10]!.trigger('click')
|
|
96
|
-
await nextTick()
|
|
97
|
-
|
|
98
|
-
expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(true)
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('emits update:modelValue with the clicked date', async () => {
|
|
102
|
-
wrapper = mountDatePicker({ modelValue: TODAY })
|
|
103
|
-
await openPicker(wrapper)
|
|
104
|
-
|
|
105
|
-
const dayButtons = wrapper
|
|
106
|
-
.findAll('button[type="button"]')
|
|
107
|
-
.filter((b) => /^\d{1,2}$/.test(b.text().trim()))
|
|
108
|
-
await dayButtons[10]!.trigger('click')
|
|
109
|
-
await nextTick()
|
|
110
|
-
|
|
111
|
-
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
|
|
112
|
-
expect(wrapper.emitted('update:modelValue')![0]![0]).toBeInstanceOf(Date)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
// ── Resets to days view on reopen ─────────────────────────────────────────
|
|
116
|
-
|
|
117
|
-
it('resets to days view when closed and reopened', async () => {
|
|
118
|
-
wrapper = mountDatePicker({ modelValue: TODAY, monthYearPicker: true })
|
|
119
|
-
await openPicker(wrapper)
|
|
120
|
-
|
|
121
|
-
// open month picker
|
|
122
|
-
const header = wrapper.find('[class*="cornflower-blue-600"]')
|
|
123
|
-
await header.find('button').trigger('click')
|
|
124
|
-
await nextTick()
|
|
125
|
-
expect(wrapper.find('[class*="grid-cols-3"]').exists()).toBe(true)
|
|
126
|
-
|
|
127
|
-
// close and reopen (v-if remounts Calendar with fresh pickerView = 'days')
|
|
128
|
-
await wrapper.find('button').trigger('click')
|
|
129
|
-
await nextTick()
|
|
130
|
-
await openPicker(wrapper)
|
|
131
|
-
expect(wrapper.find('[class*="grid-cols-7"]').exists()).toBe(true)
|
|
132
|
-
expect(wrapper.find('[class*="grid-cols-3"]').exists()).toBe(false)
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
// ── Custom trigger slot ───────────────────────────────────────────────────
|
|
136
|
-
|
|
137
|
-
it('renders custom trigger through slot', () => {
|
|
138
|
-
wrapper = mount(DatePicker, {
|
|
139
|
-
slots: {
|
|
140
|
-
trigger: `
|
|
141
|
-
<template #trigger="{ toggle }">
|
|
142
|
-
<button class="custom-trigger" @click="toggle">Custom Button</button>
|
|
143
|
-
</template>
|
|
144
|
-
`,
|
|
145
|
-
},
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
expect(wrapper.find('.custom-trigger').exists()).toBe(true)
|
|
149
|
-
expect(wrapper.text()).toContain('Custom Button')
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it('toggles picker via custom trigger toggle function', async () => {
|
|
153
|
-
wrapper = mount(DatePicker, {
|
|
154
|
-
slots: {
|
|
155
|
-
trigger: `
|
|
156
|
-
<template #trigger="{ toggle }">
|
|
157
|
-
<button class="custom-trigger" @click="toggle">Toggle</button>
|
|
158
|
-
</template>
|
|
159
|
-
`,
|
|
160
|
-
},
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(false)
|
|
164
|
-
|
|
165
|
-
await wrapper.find('.custom-trigger').trigger('click')
|
|
166
|
-
await nextTick()
|
|
167
|
-
expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(true)
|
|
168
|
-
|
|
169
|
-
await wrapper.find('.custom-trigger').trigger('click')
|
|
170
|
-
await nextTick()
|
|
171
|
-
expect(wrapper.find('[class*="rounded-xl"]').exists()).toBe(false)
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
it('provides isOpen state to custom trigger', async () => {
|
|
175
|
-
wrapper = mount(DatePicker, {
|
|
176
|
-
slots: {
|
|
177
|
-
trigger: `
|
|
178
|
-
<template #trigger="{ isOpen, toggle }">
|
|
179
|
-
<button class="custom-trigger" @click="toggle">{{ isOpen ? 'OPEN' : 'CLOSED' }}</button>
|
|
180
|
-
</template>
|
|
181
|
-
`,
|
|
182
|
-
},
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
expect(wrapper.find('.custom-trigger').text()).toBe('CLOSED')
|
|
186
|
-
|
|
187
|
-
await wrapper.find('.custom-trigger').trigger('click')
|
|
188
|
-
await nextTick()
|
|
189
|
-
expect(wrapper.find('.custom-trigger').text()).toBe('OPEN')
|
|
190
|
-
})
|
|
191
|
-
})
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
-
import DropdowndButton from './DropdownButton.vue'
|
|
3
|
-
|
|
4
|
-
export default {
|
|
5
|
-
title: 'Components/Atoms/Dropdown Button',
|
|
6
|
-
component: DropdowndButton,
|
|
7
|
-
decorators: [
|
|
8
|
-
() => ({
|
|
9
|
-
template:
|
|
10
|
-
'<div style="min-height: 250px; padding: 16px; display: flex; align-items: center; justify-content: center;"><story /></div>',
|
|
11
|
-
}),
|
|
12
|
-
],
|
|
13
|
-
parameters: {
|
|
14
|
-
design: {
|
|
15
|
-
type: 'figma',
|
|
16
|
-
url: 'https://www.figma.com/design/CdbFJ7qUga6mtjagcPDvYK/Design-System?node-id=862-1453&t=1SBsWbYXUXIJUJuq-4',
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
} satisfies Meta<typeof DropdowndButton>
|
|
20
|
-
|
|
21
|
-
type Story = StoryObj<typeof DropdowndButton>
|
|
22
|
-
|
|
23
|
-
export const Default: Story = {
|
|
24
|
-
args: {
|
|
25
|
-
label: 'Actions',
|
|
26
|
-
action: () => alert('Primary action clicked'),
|
|
27
|
-
options: [
|
|
28
|
-
{ label: 'Edit', action: () => alert('Edit clicked'), icon: 'pencil' },
|
|
29
|
-
{ label: 'Delete', action: () => alert('Delete clicked'), icon: 'trash-can' },
|
|
30
|
-
],
|
|
31
|
-
},
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export const Secondary: Story = {
|
|
35
|
-
args: {
|
|
36
|
-
label: 'More info',
|
|
37
|
-
variant: 'secondary',
|
|
38
|
-
action: () => alert('Secondary main action'),
|
|
39
|
-
options: [
|
|
40
|
-
{ label: 'View Profile', action: '/profile', icon: 'user' },
|
|
41
|
-
{ label: 'Settings', action: '/settings', icon: 'gear' },
|
|
42
|
-
],
|
|
43
|
-
},
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export const LeftPlacement: Story = {
|
|
47
|
-
args: {
|
|
48
|
-
label: 'Menu',
|
|
49
|
-
placement: 'left',
|
|
50
|
-
action: () => console.log('Main action'),
|
|
51
|
-
options: [
|
|
52
|
-
{ label: 'Option 1', action: () => console.log('1') },
|
|
53
|
-
{ label: 'Option 2', action: () => console.log('2') },
|
|
54
|
-
],
|
|
55
|
-
},
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export const WithIcon: Story = {
|
|
59
|
-
args: {
|
|
60
|
-
label: 'Download',
|
|
61
|
-
icon: 'download',
|
|
62
|
-
action: '/download/all',
|
|
63
|
-
options: [
|
|
64
|
-
{ label: 'As PDF', action: '/download/pdf', icon: 'file-pdf' },
|
|
65
|
-
{ label: 'As CSV', action: '/download/csv', icon: 'file-csv' },
|
|
66
|
-
],
|
|
67
|
-
},
|
|
68
|
-
}
|