@nexxtmove/ui 0.1.22 → 0.1.23
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 +75 -8
- package/dist/index.js +2408 -252
- package/dist/nuxt.d.ts +1 -0
- package/dist/nuxt.js +49 -0
- package/package.json +23 -13
- package/src/assets/images/Template_aanvragen.jpg +0 -0
- package/src/assets/svg/type_post.svg +1 -0
- package/src/assets/svg/type_reel.svg +54 -0
- package/src/assets/svg/type_story.svg +46 -0
- package/src/assets/svg/type_tiktok.svg +1 -0
- package/src/assets/video/Template_aanvragen.mp4 +0 -0
- package/src/components/AnimatedNumber/AnimatedNumber.stories.ts +15 -0
- package/src/components/AnimatedNumber/AnimatedNumber.test.ts +56 -0
- package/src/components/AnimatedNumber/AnimatedNumber.vue +61 -0
- package/src/components/Button/Button.stories.ts +212 -0
- package/src/components/Button/Button.test.ts +318 -0
- package/src/components/Button/Button.vue +67 -0
- package/src/components/Calendar/Calendar.stories.ts +91 -0
- package/src/components/Calendar/Calendar.test.ts +269 -0
- package/src/components/Calendar/Calendar.vue +221 -0
- package/src/components/Calendar/_CalendarDayView.test.ts +145 -0
- package/src/components/Calendar/_CalendarDayView.vue +156 -0
- package/src/components/Calendar/_CalendarHeader.test.ts +86 -0
- package/src/components/Calendar/_CalendarHeader.vue +123 -0
- package/src/components/Calendar/_CalendarMonthView.test.ts +68 -0
- package/src/components/Calendar/_CalendarMonthView.vue +70 -0
- package/src/components/Calendar/_CalendarYearView.vue +77 -0
- package/src/components/Calendar/calendar.types.ts +10 -0
- package/src/components/Chip/Chip.stories.ts +42 -0
- package/src/components/Chip/Chip.test.ts +51 -0
- package/src/components/Chip/Chip.vue +37 -0
- package/src/components/DatePicker/DatePicker.stories.ts +149 -0
- package/src/components/DatePicker/DatePicker.test.ts +191 -0
- package/src/components/DatePicker/DatePicker.vue +142 -0
- package/src/components/Header/Header.stories.ts +48 -0
- package/src/components/Header/Header.test.ts +169 -0
- package/src/components/Header/Header.vue +42 -0
- package/src/components/Icon/Icon.stories.ts +50 -0
- package/src/components/Icon/Icon.test.ts +73 -0
- package/src/components/Icon/Icon.vue +20 -0
- package/src/components/InfoBlock/InfoBlock.stories.ts +90 -0
- package/src/components/InfoBlock/InfoBlock.test.ts +101 -0
- package/src/components/InfoBlock/InfoBlock.vue +70 -0
- package/src/components/ProgressBar/ProgressBar.stories.ts +30 -0
- package/src/components/ProgressBar/ProgressBar.test.ts +314 -0
- package/src/components/ProgressBar/ProgressBar.vue +102 -0
- package/src/components/SocialIcons/SocialIcons.stories.ts +34 -0
- package/src/components/SocialIcons/SocialIcons.test.ts +58 -0
- package/src/components/SocialIcons/SocialIcons.vue +58 -0
- package/src/components/SocialMediaCustomTemplate/SocialMediaCustomTemplate.stories.ts +11 -0
- package/src/components/SocialMediaCustomTemplate/SocialMediaCustomTemplate.test.ts +131 -0
- package/src/components/SocialMediaCustomTemplate/SocialMediaCustomTemplate.vue +55 -0
- package/src/components/SocialMediaTemplate/SocialMediaTemplate.stories.ts +71 -0
- package/src/components/SocialMediaTemplate/SocialMediaTemplate.test.ts +466 -0
- package/src/components/SocialMediaTemplate/SocialMediaTemplate.vue +130 -0
- package/src/components/SocialMediaType/SocialMediaType.stories.ts +43 -0
- package/src/components/SocialMediaType/SocialMediaType.test.ts +126 -0
- package/src/components/SocialMediaType/SocialMediaType.vue +117 -0
- package/src/components/StepperHeader/StepperHeader.stories.ts +47 -0
- package/src/components/StepperHeader/StepperHeader.test.ts +244 -0
- package/src/components/StepperHeader/StepperHeader.vue +37 -0
- package/src/components.json +16 -0
- package/src/env.d.ts +23 -0
- package/src/index.css +2 -0
- package/src/index.ts +15 -0
- package/src/nuxt.ts +50 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import ProgressBar from './ProgressBar.vue'
|
|
4
|
+
|
|
5
|
+
describe('ProgressBar', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.clearAllMocks()
|
|
8
|
+
vi.stubGlobal('console', { warn: vi.fn() })
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('renders correct percentage for first step', () => {
|
|
12
|
+
const wrapper = mount(ProgressBar, {
|
|
13
|
+
props: {
|
|
14
|
+
steps: 5,
|
|
15
|
+
currentStep: 0,
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const progressbar = wrapper.find('[role="progressbar"]')
|
|
20
|
+
expect(progressbar.attributes('aria-valuenow')).toBe('0')
|
|
21
|
+
|
|
22
|
+
const innerBar = progressbar.find('div')
|
|
23
|
+
expect(innerBar.attributes('style')).toContain('width: 0%')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('renders correct percentage for middle step', () => {
|
|
27
|
+
const wrapper = mount(ProgressBar, {
|
|
28
|
+
props: {
|
|
29
|
+
steps: 5,
|
|
30
|
+
currentStep: 2,
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const progressbar = wrapper.find('[role="progressbar"]')
|
|
35
|
+
expect(progressbar.attributes('aria-valuenow')).toBe('50')
|
|
36
|
+
expect(progressbar.find('div').attributes('style')).toContain('width: 50%')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('renders correct percentage for last step', () => {
|
|
40
|
+
const wrapper = mount(ProgressBar, {
|
|
41
|
+
props: {
|
|
42
|
+
steps: 5,
|
|
43
|
+
currentStep: 4,
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const progressbar = wrapper.find('[role="progressbar"]')
|
|
48
|
+
expect(progressbar.attributes('aria-valuenow')).toBe('100')
|
|
49
|
+
expect(progressbar.find('div').attributes('style')).toContain('width: 100%')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('clamps currentStep within range', () => {
|
|
53
|
+
const wrapperLow = mount(ProgressBar, {
|
|
54
|
+
props: { steps: 5, currentStep: -1 },
|
|
55
|
+
})
|
|
56
|
+
expect(wrapperLow.find('[role="progressbar"]').attributes('aria-valuenow')).toBe('0')
|
|
57
|
+
|
|
58
|
+
const wrapperHigh = mount(ProgressBar, {
|
|
59
|
+
props: { steps: 5, currentStep: 10 },
|
|
60
|
+
})
|
|
61
|
+
expect(wrapperHigh.find('[role="progressbar"]').attributes('aria-valuenow')).toBe('100')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('handles edge case of 0 or 1 steps', () => {
|
|
65
|
+
const wrapperZero = mount(ProgressBar, {
|
|
66
|
+
props: { steps: 0, currentStep: 0 },
|
|
67
|
+
})
|
|
68
|
+
expect(wrapperZero.find('[role="progressbar"]').attributes('aria-valuenow')).toBe('0')
|
|
69
|
+
|
|
70
|
+
const wrapperOne = mount(ProgressBar, {
|
|
71
|
+
props: { steps: 1, currentStep: 0 },
|
|
72
|
+
})
|
|
73
|
+
expect(wrapperOne.find('[role="progressbar"]').attributes('aria-valuenow')).toBe('100')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('handles empty stepTitles array', () => {
|
|
77
|
+
const wrapper = mount(ProgressBar, {
|
|
78
|
+
props: { steps: 5, currentStep: 0, stepTitles: [] },
|
|
79
|
+
})
|
|
80
|
+
expect(wrapper.find('ol').exists()).toBe(false)
|
|
81
|
+
expect(wrapper.find('.tabular-nums').exists()).toBe(false)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('displays correct step text without titles', () => {
|
|
85
|
+
const wrapper = mount(ProgressBar, {
|
|
86
|
+
props: { steps: 5, currentStep: 2 },
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
expect(wrapper.find('[role="progressbar"]').attributes('aria-valuetext')).toBe('Step 3 of 5')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('displays correct step text with titles', () => {
|
|
93
|
+
const stepTitles = ['Start', 'Middle', 'End']
|
|
94
|
+
const wrapper = mount(ProgressBar, {
|
|
95
|
+
props: {
|
|
96
|
+
steps: 3,
|
|
97
|
+
currentStep: 1,
|
|
98
|
+
stepTitles,
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
expect(wrapper.find('[role="progressbar"]').attributes('aria-valuetext')).toBe(
|
|
103
|
+
'Step 2 of 3: Middle',
|
|
104
|
+
)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('renders step titles and marks current step', () => {
|
|
108
|
+
const stepTitles = ['Step 1', 'Step 2', 'Step 3']
|
|
109
|
+
const wrapper = mount(ProgressBar, {
|
|
110
|
+
props: {
|
|
111
|
+
steps: 3,
|
|
112
|
+
currentStep: 1,
|
|
113
|
+
stepTitles,
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const items = wrapper.findAll('li')
|
|
118
|
+
expect(items).toHaveLength(3)
|
|
119
|
+
expect(items[1]!.attributes('aria-current')).toBe('step')
|
|
120
|
+
expect(items[1]!.classes()).toContain('text-sapphire-500')
|
|
121
|
+
expect(items[0]!.attributes('aria-current')).toBeUndefined()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('uses default progress label', () => {
|
|
125
|
+
const wrapper = mount(ProgressBar, {
|
|
126
|
+
props: {
|
|
127
|
+
steps: 5,
|
|
128
|
+
currentStep: 0,
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
expect(wrapper.find('[role="progressbar"]').attributes('aria-label')).toBe('Voortgang')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('uses custom progress label', () => {
|
|
135
|
+
const wrapper = mount(ProgressBar, {
|
|
136
|
+
props: {
|
|
137
|
+
steps: 5,
|
|
138
|
+
currentStep: 0,
|
|
139
|
+
progressLabel: 'My Progress',
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
expect(wrapper.find('[role="progressbar"]').attributes('aria-label')).toBe('My Progress')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('warns when stepTitles length mismatch', () => {
|
|
146
|
+
const spy = vi.spyOn(console, 'warn')
|
|
147
|
+
mount(ProgressBar, {
|
|
148
|
+
props: {
|
|
149
|
+
steps: 5,
|
|
150
|
+
currentStep: 0,
|
|
151
|
+
stepTitles: ['Only one'],
|
|
152
|
+
},
|
|
153
|
+
})
|
|
154
|
+
expect(spy).toHaveBeenCalledWith(expect.stringContaining('[NexxtProgressBar]'))
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('handles missing title in titles array gracefully', () => {
|
|
158
|
+
const wrapper = mount(ProgressBar, {
|
|
159
|
+
props: {
|
|
160
|
+
steps: 3,
|
|
161
|
+
currentStep: 2, // Last step
|
|
162
|
+
stepTitles: ['Only First'], // Only index 0 exists
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
// clampedStep is 2, stepTitles[2] is undefined
|
|
166
|
+
expect(wrapper.find('[role="progressbar"]').attributes('aria-valuetext')).toBe('Step 3 of 3')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('has responsive container query classes', () => {
|
|
170
|
+
const wrapper = mount(ProgressBar, {
|
|
171
|
+
props: {
|
|
172
|
+
steps: 5,
|
|
173
|
+
currentStep: 0,
|
|
174
|
+
stepTitles: ['Step 1', 'Step 2', 'Step 3', 'Step 4', 'Step 5'],
|
|
175
|
+
},
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// Verify the root is a container
|
|
179
|
+
expect(wrapper.classes()).toContain('@container')
|
|
180
|
+
|
|
181
|
+
// Verify the digit counter hides when the container is large
|
|
182
|
+
const counter = wrapper.find('.tabular-nums')
|
|
183
|
+
expect(counter.classes()).toContain('@md:hidden')
|
|
184
|
+
|
|
185
|
+
// Verify list items hide titles when the container is small
|
|
186
|
+
const firstTitle = wrapper.find('li')
|
|
187
|
+
expect(firstTitle.classes()).toContain('@max-md:hidden')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('updates animation direction correctly based on step change', async () => {
|
|
191
|
+
const wrapper = mount(ProgressBar, {
|
|
192
|
+
props: {
|
|
193
|
+
steps: 5,
|
|
194
|
+
currentStep: 2,
|
|
195
|
+
stepTitles: ['1', '2', '3', '4', '5'],
|
|
196
|
+
},
|
|
197
|
+
global: {
|
|
198
|
+
stubs: {
|
|
199
|
+
// Stubbing Transition lets us inspect the dynamic classes passed to it
|
|
200
|
+
Transition: {
|
|
201
|
+
template: '<slot />',
|
|
202
|
+
props: ['enterFromClass', 'leaveToClass'],
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// Increment: Step 3 -> 4
|
|
209
|
+
await wrapper.setProps({ currentStep: 3 })
|
|
210
|
+
let transition = wrapper.findComponent({ name: 'Transition' })
|
|
211
|
+
expect(transition.props('enterFromClass')).toBe('translate-y-full')
|
|
212
|
+
expect(transition.props('leaveToClass')).toBe('-translate-y-full')
|
|
213
|
+
|
|
214
|
+
// Decrement: Step 4 -> 2
|
|
215
|
+
await wrapper.setProps({ currentStep: 1 })
|
|
216
|
+
transition = wrapper.findComponent({ name: 'Transition' })
|
|
217
|
+
expect(transition.props('enterFromClass')).toBe('-translate-y-full')
|
|
218
|
+
expect(transition.props('leaveToClass')).toBe('translate-y-full')
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('emits update:currentStep when a previous step is clicked', async () => {
|
|
222
|
+
const stepTitles = ['Step 1', 'Step 2', 'Step 3']
|
|
223
|
+
const wrapper = mount(ProgressBar, {
|
|
224
|
+
props: {
|
|
225
|
+
steps: 3,
|
|
226
|
+
currentStep: 2,
|
|
227
|
+
stepTitles,
|
|
228
|
+
},
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
const items = wrapper.findAll('li')
|
|
232
|
+
|
|
233
|
+
// Click first step (index 0, previous)
|
|
234
|
+
await items[0]!.trigger('click')
|
|
235
|
+
expect(wrapper.emitted('update:currentStep')).toBeTruthy()
|
|
236
|
+
expect(wrapper.emitted('update:currentStep')![0]).toEqual([0])
|
|
237
|
+
|
|
238
|
+
// Click second step (index 1, previous)
|
|
239
|
+
await items[1]!.trigger('click')
|
|
240
|
+
expect(wrapper.emitted('update:currentStep')![1]).toEqual([1])
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('does not emit update:currentStep when the current or a future step is clicked', async () => {
|
|
244
|
+
const stepTitles = ['Step 1', 'Step 2', 'Step 3']
|
|
245
|
+
const wrapper = mount(ProgressBar, {
|
|
246
|
+
props: {
|
|
247
|
+
steps: 3,
|
|
248
|
+
currentStep: 1,
|
|
249
|
+
stepTitles,
|
|
250
|
+
},
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
const items = wrapper.findAll('li')
|
|
254
|
+
|
|
255
|
+
// Click current step (index 1)
|
|
256
|
+
await items[1]!.trigger('click')
|
|
257
|
+
expect(wrapper.emitted('update:currentStep')).toBeFalsy()
|
|
258
|
+
|
|
259
|
+
// Click future step (index 2)
|
|
260
|
+
await items[2]!.trigger('click')
|
|
261
|
+
expect(wrapper.emitted('update:currentStep')).toBeFalsy()
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('applies interactive classes only to previous steps', () => {
|
|
265
|
+
const stepTitles = ['Step 1', 'Step 2', 'Step 3']
|
|
266
|
+
const wrapper = mount(ProgressBar, {
|
|
267
|
+
props: {
|
|
268
|
+
steps: 3,
|
|
269
|
+
currentStep: 1,
|
|
270
|
+
stepTitles,
|
|
271
|
+
},
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
const items = wrapper.findAll('li')
|
|
275
|
+
|
|
276
|
+
// Previous step
|
|
277
|
+
expect(items[0]!.classes()).toContain('cursor-pointer')
|
|
278
|
+
expect(items[0]!.classes()).toContain('hover:text-sapphire-400')
|
|
279
|
+
expect(items[0]!.attributes('role')).toBe('button')
|
|
280
|
+
expect(items[0]!.attributes('tabindex')).toBe('0')
|
|
281
|
+
expect(items[0]!.attributes('aria-label')).toBe('Ga naar Step 1')
|
|
282
|
+
|
|
283
|
+
// Current step
|
|
284
|
+
expect(items[1]!.classes()).not.toContain('cursor-pointer')
|
|
285
|
+
expect(items[1]!.attributes('role')).toBeUndefined()
|
|
286
|
+
expect(items[1]!.attributes('tabindex')).toBeUndefined()
|
|
287
|
+
|
|
288
|
+
// Future step
|
|
289
|
+
expect(items[2]!.classes()).not.toContain('cursor-pointer')
|
|
290
|
+
expect(items[2]!.attributes('role')).toBeUndefined()
|
|
291
|
+
expect(items[2]!.attributes('tabindex')).toBeUndefined()
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('emits update:currentStep when Enter or Space is pressed on a previous step', async () => {
|
|
295
|
+
const stepTitles = ['Step 1', 'Step 2', 'Step 3']
|
|
296
|
+
const wrapper = mount(ProgressBar, {
|
|
297
|
+
props: {
|
|
298
|
+
steps: 3,
|
|
299
|
+
currentStep: 2,
|
|
300
|
+
stepTitles,
|
|
301
|
+
},
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
const items = wrapper.findAll('li')
|
|
305
|
+
|
|
306
|
+
// Press Enter on first step
|
|
307
|
+
await items[0]!.trigger('keydown.enter')
|
|
308
|
+
expect(wrapper.emitted('update:currentStep')![0]).toEqual([0])
|
|
309
|
+
|
|
310
|
+
// Press Space on second step
|
|
311
|
+
await items[1]!.trigger('keydown.space')
|
|
312
|
+
expect(wrapper.emitted('update:currentStep')![1]).toEqual([1])
|
|
313
|
+
})
|
|
314
|
+
})
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, watchEffect } from 'vue'
|
|
3
|
+
import AnimatedNumber from '../AnimatedNumber/AnimatedNumber.vue'
|
|
4
|
+
|
|
5
|
+
interface NexxtProgressBarProps {
|
|
6
|
+
steps: number
|
|
7
|
+
currentStep: number
|
|
8
|
+
progressLabel?: string
|
|
9
|
+
stepTitles?: string[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
defineOptions({
|
|
13
|
+
name: 'NexxtProgressBar',
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
defineEmits(['update:currentStep'])
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
progressLabel = 'Voortgang',
|
|
20
|
+
stepTitles,
|
|
21
|
+
steps,
|
|
22
|
+
currentStep,
|
|
23
|
+
} = defineProps<NexxtProgressBarProps>()
|
|
24
|
+
|
|
25
|
+
const clampedStep = computed(() => Math.min(Math.max(0, currentStep), Math.max(0, steps - 1)))
|
|
26
|
+
const displayStep = computed(() => (steps > 0 ? clampedStep.value + 1 : 0))
|
|
27
|
+
|
|
28
|
+
const percentage = computed(() => {
|
|
29
|
+
if (steps <= 1) return steps === 1 ? 100 : 0
|
|
30
|
+
return (clampedStep.value / (steps - 1)) * 100
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const hasTitles = computed(() => Boolean(stepTitles?.length))
|
|
34
|
+
|
|
35
|
+
watchEffect(() => {
|
|
36
|
+
if (stepTitles && stepTitles.length !== steps) {
|
|
37
|
+
console.warn(
|
|
38
|
+
`[NexxtProgressBar] The "stepTitles" array length (${stepTitles.length}) must match the "steps" prop (${steps}).`,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<template>
|
|
45
|
+
<div class="@container">
|
|
46
|
+
<div class="flex flex-col @md:gap-2">
|
|
47
|
+
<div class="flex items-center justify-between gap-2">
|
|
48
|
+
<div
|
|
49
|
+
class="progress-bar progress-bar-bar h-2 w-full overflow-hidden rounded-full"
|
|
50
|
+
role="progressbar"
|
|
51
|
+
:aria-label="progressLabel"
|
|
52
|
+
aria-valuemin="0"
|
|
53
|
+
aria-valuemax="100"
|
|
54
|
+
:aria-valuenow="Math.round(percentage)"
|
|
55
|
+
:aria-valuetext="
|
|
56
|
+
hasTitles && stepTitles?.[clampedStep]
|
|
57
|
+
? `Step ${displayStep} of ${steps}: ${stepTitles[clampedStep]}`
|
|
58
|
+
: `Step ${displayStep} of ${steps}`
|
|
59
|
+
"
|
|
60
|
+
>
|
|
61
|
+
<div
|
|
62
|
+
class="progress-bar-progress h-full transition-all duration-500 ease-out"
|
|
63
|
+
:style="{ width: `${percentage}%` }"
|
|
64
|
+
:aria-hidden="true"
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
<div
|
|
68
|
+
v-if="hasTitles"
|
|
69
|
+
class="flex items-center small-normal tabular-nums @md:hidden"
|
|
70
|
+
aria-hidden="true"
|
|
71
|
+
>
|
|
72
|
+
<AnimatedNumber :value="displayStep" />
|
|
73
|
+
<span>/{{ steps }}</span>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<ol
|
|
77
|
+
v-if="hasTitles"
|
|
78
|
+
class="flex justify-between @max-md:order-first @max-md:justify-center"
|
|
79
|
+
aria-label="Progress steps"
|
|
80
|
+
>
|
|
81
|
+
<li
|
|
82
|
+
v-for="(title, index) in stepTitles"
|
|
83
|
+
:key="title"
|
|
84
|
+
class="rounded-xs transition-colors duration-500 focus-visible:outline-3 @max-md:hidden @max-md:body-normal @min-md:small-normal"
|
|
85
|
+
:class="[
|
|
86
|
+
index === clampedStep ? 'text-sapphire-500 @max-md:inline' : '',
|
|
87
|
+
index < clampedStep ? 'cursor-pointer duration-200! hover:text-sapphire-400' : '',
|
|
88
|
+
]"
|
|
89
|
+
:aria-current="index === clampedStep ? 'step' : undefined"
|
|
90
|
+
:role="index < clampedStep ? 'button' : undefined"
|
|
91
|
+
:tabindex="index < clampedStep ? 0 : undefined"
|
|
92
|
+
:aria-label="index < clampedStep ? `Ga naar ${title}` : undefined"
|
|
93
|
+
@click="index < clampedStep && $emit('update:currentStep', index)"
|
|
94
|
+
@keydown.enter="index < clampedStep && $emit('update:currentStep', index)"
|
|
95
|
+
@keydown.space.prevent="index < clampedStep && $emit('update:currentStep', index)"
|
|
96
|
+
>
|
|
97
|
+
{{ title }}
|
|
98
|
+
</li>
|
|
99
|
+
</ol>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</template>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import SocialIcons from './SocialIcons.vue'
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
title: 'Components/Molecules/Social Icons',
|
|
6
|
+
component: SocialIcons,
|
|
7
|
+
parameters: {
|
|
8
|
+
design: {
|
|
9
|
+
type: 'figma',
|
|
10
|
+
url: 'https://www.figma.com/design/CdbFJ7qUga6mtjagcPDvYK/Design-System?node-id=524-17&t=CbHvOQfEMIr7M5mO-11',
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
} satisfies Meta<typeof SocialIcons>
|
|
14
|
+
|
|
15
|
+
type Story = StoryObj<typeof SocialIcons>
|
|
16
|
+
|
|
17
|
+
export const Default: Story = {
|
|
18
|
+
args: {
|
|
19
|
+
facebook: true,
|
|
20
|
+
instagram: true,
|
|
21
|
+
linkedin: true,
|
|
22
|
+
x: true,
|
|
23
|
+
google: true,
|
|
24
|
+
tiktok: true,
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const Selective: Story = {
|
|
29
|
+
args: {
|
|
30
|
+
facebook: true,
|
|
31
|
+
linkedin: true,
|
|
32
|
+
instagram: true,
|
|
33
|
+
},
|
|
34
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import SocialIcons from './SocialIcons.vue'
|
|
4
|
+
|
|
5
|
+
describe('SocialIcons', () => {
|
|
6
|
+
it('renders nothing when no props are set', () => {
|
|
7
|
+
const wrapper = mount(SocialIcons)
|
|
8
|
+
expect(wrapper.findAll('.rounded-full')).toHaveLength(0)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('renders only the requested icons', () => {
|
|
12
|
+
const wrapper = mount(SocialIcons, {
|
|
13
|
+
props: {
|
|
14
|
+
facebook: true,
|
|
15
|
+
instagram: true,
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const icons = wrapper.findAll('.rounded-full')
|
|
20
|
+
expect(icons).toHaveLength(2)
|
|
21
|
+
|
|
22
|
+
// Check for specific background colors to verify which icons are rendered
|
|
23
|
+
expect(icons[0]!.classes()).toContain('bg-brands-facebook')
|
|
24
|
+
expect(icons[1]!.classes()).toContain('bg-brands-instagram')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('has correct accessibility attributes', () => {
|
|
28
|
+
const wrapper = mount(SocialIcons, {
|
|
29
|
+
props: {
|
|
30
|
+
facebook: true,
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const list = wrapper.find('ul')
|
|
35
|
+
expect(list.exists()).toBe(true)
|
|
36
|
+
expect(list.attributes('aria-label')).toBe('Sociale media kanalen')
|
|
37
|
+
|
|
38
|
+
const item = wrapper.find('li')
|
|
39
|
+
expect(item.exists()).toBe(true)
|
|
40
|
+
expect(item.attributes('aria-label')).toBe('Facebook')
|
|
41
|
+
expect(item.findComponent({ name: 'NexxtIcon' }).attributes('aria-hidden')).toBe('true')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('renders all icons when all props are true', () => {
|
|
45
|
+
const wrapper = mount(SocialIcons, {
|
|
46
|
+
props: {
|
|
47
|
+
facebook: true,
|
|
48
|
+
instagram: true,
|
|
49
|
+
linkedin: true,
|
|
50
|
+
x: true,
|
|
51
|
+
google: true,
|
|
52
|
+
tiktok: true,
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
expect(wrapper.findAll('.rounded-full')).toHaveLength(6)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import Icon from '../Icon/Icon.vue'
|
|
4
|
+
|
|
5
|
+
export interface NexxtSocialIconsProps {
|
|
6
|
+
facebook?: boolean
|
|
7
|
+
instagram?: boolean
|
|
8
|
+
linkedin?: boolean
|
|
9
|
+
x?: boolean
|
|
10
|
+
google?: boolean
|
|
11
|
+
tiktok?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
defineOptions({
|
|
15
|
+
name: 'NexxtSocialIcons',
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const props = defineProps<NexxtSocialIconsProps>()
|
|
19
|
+
|
|
20
|
+
const socialPlatforms = [
|
|
21
|
+
{ name: 'facebook', label: 'Facebook', icon: 'facebook-f' },
|
|
22
|
+
{ name: 'instagram', label: 'Instagram', icon: 'instagram' },
|
|
23
|
+
{ name: 'linkedin', label: 'LinkedIn', icon: 'linkedin-in' },
|
|
24
|
+
{ name: 'x', label: 'X (voorheen Twitter)', icon: 'x-twitter' },
|
|
25
|
+
{ name: 'google', label: 'Google', icon: 'google' },
|
|
26
|
+
{ name: 'tiktok', label: 'TikTok', icon: 'tiktok' },
|
|
27
|
+
] as const
|
|
28
|
+
|
|
29
|
+
const brandColors: Record<string, string> = {
|
|
30
|
+
facebook: 'bg-brands-facebook',
|
|
31
|
+
instagram: 'bg-brands-instagram',
|
|
32
|
+
linkedin: 'bg-brands-linkedin',
|
|
33
|
+
x: 'bg-brands-x',
|
|
34
|
+
google: 'bg-brands-google',
|
|
35
|
+
tiktok: 'bg-brands-tiktok',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const activeIcons = computed(() =>
|
|
39
|
+
socialPlatforms.filter((platform) => props[platform.name as keyof NexxtSocialIconsProps]),
|
|
40
|
+
)
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<template>
|
|
44
|
+
<ul
|
|
45
|
+
class="m-0 inline-flex list-none items-start justify-start gap-0.5 p-0"
|
|
46
|
+
aria-label="Sociale media kanalen"
|
|
47
|
+
>
|
|
48
|
+
<li
|
|
49
|
+
v-for="platform in activeIcons"
|
|
50
|
+
:key="platform.name"
|
|
51
|
+
class="inline-flex h-5 w-5 items-center justify-center rounded-full outline-1 outline-white"
|
|
52
|
+
:class="brandColors[platform.name]"
|
|
53
|
+
:aria-label="platform.label"
|
|
54
|
+
>
|
|
55
|
+
<Icon :name="platform.icon" type="brands" class="text-[10px] text-white" aria-hidden="true" />
|
|
56
|
+
</li>
|
|
57
|
+
</ul>
|
|
58
|
+
</template>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import SocialMediaCustomTemplate from './SocialMediaCustomTemplate.vue'
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
title: 'Components/Molecules/Social Media Custom Template',
|
|
6
|
+
component: SocialMediaCustomTemplate,
|
|
7
|
+
} satisfies Meta<typeof SocialMediaCustomTemplate>
|
|
8
|
+
|
|
9
|
+
type Story = StoryObj<typeof SocialMediaCustomTemplate>
|
|
10
|
+
|
|
11
|
+
export const Default: Story = {}
|