@oslokommune/punkt-elements 13.4.1 → 13.5.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/calendar-32W9p9uc.cjs +115 -0
  3. package/dist/{calendar-DevQhOup.js → calendar-CJSxvwAq.js} +353 -340
  4. package/dist/{card-uccD6Pnv.cjs → card-BUITGoqX.cjs} +10 -10
  5. package/dist/{card-BI1NZONj.js → card-Dtw26f7i.js} +96 -76
  6. package/dist/checkbox-Gn7Wtk9h.cjs +31 -0
  7. package/dist/checkbox-ym7z6cpt.js +142 -0
  8. package/dist/{combobox-BhcqC30d.cjs → combobox-DjO0RMUB.cjs} +1 -1
  9. package/dist/{combobox-D9dGKWuZ.js → combobox-yE4aYhTi.js} +1 -1
  10. package/dist/{datepicker-CYOn3tRm.js → datepicker-BJKJBoy_.js} +102 -59
  11. package/dist/datepicker-CmTrG5GE.cjs +164 -0
  12. package/dist/index.d.ts +9 -2
  13. package/dist/pkt-calendar.cjs +1 -1
  14. package/dist/pkt-calendar.js +1 -1
  15. package/dist/pkt-card.cjs +1 -1
  16. package/dist/pkt-card.js +1 -1
  17. package/dist/pkt-checkbox.cjs +1 -1
  18. package/dist/pkt-checkbox.js +1 -1
  19. package/dist/pkt-combobox.cjs +1 -1
  20. package/dist/pkt-combobox.js +1 -1
  21. package/dist/pkt-datepicker.cjs +1 -1
  22. package/dist/pkt-datepicker.js +1 -1
  23. package/dist/pkt-index.cjs +1 -1
  24. package/dist/pkt-index.js +6 -6
  25. package/package.json +3 -3
  26. package/src/components/calendar/calendar.accessibility.test.ts +111 -0
  27. package/src/components/calendar/calendar.constraints.test.ts +110 -0
  28. package/src/components/calendar/calendar.core.test.ts +367 -0
  29. package/src/components/calendar/calendar.interaction.test.ts +139 -0
  30. package/src/components/calendar/calendar.selection.test.ts +273 -0
  31. package/src/components/calendar/calendar.ts +74 -42
  32. package/src/components/card/card.test.ts +606 -0
  33. package/src/components/card/card.ts +24 -1
  34. package/src/components/checkbox/checkbox.test.ts +535 -0
  35. package/src/components/checkbox/checkbox.ts +44 -1
  36. package/src/components/combobox/combobox.test.ts +737 -0
  37. package/src/components/combobox/combobox.ts +1 -1
  38. package/src/components/datepicker/datepicker.accessibility.test.ts +193 -0
  39. package/src/components/datepicker/datepicker.core.test.ts +322 -0
  40. package/src/components/datepicker/datepicker.input.test.ts +268 -0
  41. package/src/components/datepicker/datepicker.selection.test.ts +286 -0
  42. package/src/components/datepicker/datepicker.ts +121 -19
  43. package/src/components/datepicker/datepicker.validation.test.ts +176 -0
  44. package/dist/calendar-BZe2D4Sr.cjs +0 -108
  45. package/dist/checkbox-CTRbpbye.js +0 -120
  46. package/dist/checkbox-wJ26voZd.cjs +0 -30
  47. package/dist/datepicker-B9rhz_AF.cjs +0 -154
@@ -0,0 +1,606 @@
1
+ import '@testing-library/jest-dom'
2
+ import { axe, toHaveNoViolations } from 'jest-axe'
3
+
4
+ expect.extend(toHaveNoViolations)
5
+
6
+ import './card'
7
+ import { PktCard } from './card'
8
+
9
+ const waitForCustomElements = async () => {
10
+ await customElements.whenDefined('pkt-card')
11
+ }
12
+
13
+ // Helper function to create card markup
14
+ const createCard = async (cardProps = '', content = 'Card content') => {
15
+ const container = document.createElement('div')
16
+ container.innerHTML = `
17
+ <pkt-card ${cardProps}>
18
+ ${content}
19
+ </pkt-card>
20
+ `
21
+ document.body.appendChild(container)
22
+ await waitForCustomElements()
23
+ return container
24
+ }
25
+
26
+ // Cleanup after each test
27
+ afterEach(() => {
28
+ document.body.innerHTML = ''
29
+ })
30
+
31
+ // Global console.warn spy to suppress validation warnings in tests
32
+ let consoleWarnSpy: jest.SpyInstance
33
+ beforeEach(() => {
34
+ consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
35
+ })
36
+
37
+ afterEach(() => {
38
+ if (consoleWarnSpy) {
39
+ consoleWarnSpy.mockRestore()
40
+ }
41
+ })
42
+
43
+ describe('PktCard', () => {
44
+ describe('Rendering and basic functionality', () => {
45
+ test('renders without errors', async () => {
46
+ const container = await createCard()
47
+
48
+ const card = container.querySelector('pkt-card') as PktCard
49
+ expect(card).toBeInTheDocument()
50
+
51
+ await card.updateComplete
52
+ expect(card).toBeTruthy()
53
+
54
+ const article = card.querySelector('article')
55
+ expect(article).toBeInTheDocument()
56
+ expect(article).toHaveClass('pkt-card')
57
+ })
58
+
59
+ test('renders content in slot', async () => {
60
+ const container = await createCard('', '<p>Test content here</p>')
61
+
62
+ const card = container.querySelector('pkt-card') as PktCard
63
+ await card.updateComplete
64
+
65
+ const content = card.querySelector('.pkt-card__content')
66
+ expect(content).toBeInTheDocument()
67
+ expect(content?.textContent).toContain('Test content here')
68
+ })
69
+
70
+ test('renders basic structure correctly', async () => {
71
+ const container = await createCard('heading="Test Heading"', 'Test content')
72
+
73
+ const card = container.querySelector('pkt-card') as PktCard
74
+ await card.updateComplete
75
+
76
+ const article = card.querySelector('article')
77
+ const wrapper = article?.querySelector('.pkt-card__wrapper')
78
+ const header = wrapper?.querySelector('.pkt-card__header')
79
+ const content = wrapper?.querySelector('.pkt-card__content')
80
+
81
+ expect(wrapper).toBeInTheDocument()
82
+ expect(header).toBeInTheDocument()
83
+ expect(content).toBeInTheDocument()
84
+ })
85
+ })
86
+
87
+ describe('Properties and attributes', () => {
88
+ test('applies default properties correctly', async () => {
89
+ const container = await createCard()
90
+
91
+ const card = container.querySelector('pkt-card') as PktCard
92
+ await card.updateComplete
93
+
94
+ expect(card.skin).toBe('outlined')
95
+ expect(card.layout).toBe('vertical')
96
+ expect(card.padding).toBe('default')
97
+ expect(card.borderOnHover).toBe(true)
98
+ expect(card.tagPosition).toBe('top')
99
+ expect(card.imageShape).toBe('square')
100
+ expect(card.openLinkInNewTab).toBe(false)
101
+ expect(card.headinglevel).toBe(3)
102
+
103
+ const article = card.querySelector('article')
104
+ expect(article).toHaveClass('pkt-card--outlined')
105
+ expect(article).toHaveClass('pkt-card--vertical')
106
+ expect(article).toHaveClass('pkt-card--padding-default')
107
+ expect(article).toHaveClass('pkt-card--border-on-hover')
108
+ })
109
+
110
+ test('applies different skin properties correctly', async () => {
111
+ const skins = ['outlined', 'outlined-beige', 'gray', 'beige', 'green', 'blue']
112
+
113
+ for (const skin of skins) {
114
+ const container = await createCard(`skin="${skin}"`)
115
+ const card = container.querySelector('pkt-card') as PktCard
116
+ await card.updateComplete
117
+
118
+ expect(card.skin).toBe(skin)
119
+ expect(card.getAttribute('skin')).toBe(skin)
120
+
121
+ const article = card.querySelector('article')
122
+ expect(article).toHaveClass(`pkt-card--${skin}`)
123
+
124
+ // Cleanup for next iteration
125
+ container.remove()
126
+ }
127
+ })
128
+
129
+ test('rejects unsupported skin values and falls back to default', async () => {
130
+ const unsupportedSkins = ['zebra', 'goldenrod', 'hotpink', 'rainbow', 'invalid']
131
+
132
+ for (const invalidSkin of unsupportedSkins) {
133
+ const container = await createCard(`skin="${invalidSkin}"`)
134
+ const card = container.querySelector('pkt-card') as PktCard
135
+ await card.updateComplete
136
+
137
+ // The component should now validate skin values and fall back to default
138
+ expect(card.skin).not.toBe(invalidSkin)
139
+ expect(card.skin).toBe('outlined') // Should fall back to default
140
+
141
+ const article = card.querySelector('article')
142
+ // Should not have the invalid CSS class
143
+ expect(article).not.toHaveClass(`pkt-card--${invalidSkin}`)
144
+ // Should have the default skin class instead
145
+ expect(article).toHaveClass('pkt-card--outlined')
146
+
147
+ // Cleanup for next iteration
148
+ container.remove()
149
+ }
150
+ })
151
+
152
+ test('validates skin values and logs warnings for invalid skins', async () => {
153
+ // Clear the global spy and create a new one for this specific test
154
+ consoleWarnSpy.mockRestore()
155
+ const localConsoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
156
+
157
+ const container = await createCard(`skin="zebra"`)
158
+ const card = container.querySelector('pkt-card') as PktCard
159
+ await card.updateComplete
160
+
161
+ // Should have logged a warning with the correct default value from spec
162
+ expect(localConsoleSpy).toHaveBeenCalledWith(
163
+ 'Invalid skin value "zebra". Using default skin "outlined".',
164
+ )
165
+
166
+ // Should fall back to default from spec
167
+ expect(card.skin).toBe('outlined')
168
+
169
+ // Restore and recreate global spy for subsequent tests
170
+ localConsoleSpy.mockRestore()
171
+ consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
172
+ })
173
+
174
+ test('applies different layout properties correctly', async () => {
175
+ const layouts = ['vertical', 'horizontal']
176
+
177
+ for (const layout of layouts) {
178
+ const container = await createCard(`layout="${layout}"`)
179
+ const card = container.querySelector('pkt-card') as PktCard
180
+ await card.updateComplete
181
+
182
+ expect(card.layout).toBe(layout)
183
+ expect(card.getAttribute('layout')).toBe(layout)
184
+
185
+ const article = card.querySelector('article')
186
+ expect(article).toHaveClass(`pkt-card--${layout}`)
187
+
188
+ // Cleanup for next iteration
189
+ container.remove()
190
+ }
191
+ })
192
+
193
+ test('applies different padding properties correctly', async () => {
194
+ const paddingOptions = ['none', 'default']
195
+
196
+ for (const padding of paddingOptions) {
197
+ const container = await createCard(`padding="${padding}"`)
198
+ const card = container.querySelector('pkt-card') as PktCard
199
+ await card.updateComplete
200
+
201
+ expect(card.padding).toBe(padding)
202
+ expect(card.getAttribute('padding')).toBe(padding)
203
+
204
+ const article = card.querySelector('article')
205
+ expect(article).toHaveClass(`pkt-card--padding-${padding}`)
206
+
207
+ // Cleanup for next iteration
208
+ container.remove()
209
+ }
210
+ })
211
+
212
+ test('handles borderOnHover property correctly', async () => {
213
+ // Test with borderOnHover false
214
+ const container = await createCard()
215
+ const card = container.querySelector('pkt-card') as PktCard
216
+ card.borderOnHover = false
217
+ await card.updateComplete
218
+
219
+ expect(card.borderOnHover).toBe(false)
220
+
221
+ const article = card.querySelector('article')
222
+ expect(article).not.toHaveClass('pkt-card--border-on-hover')
223
+ })
224
+ })
225
+
226
+ describe('Heading functionality', () => {
227
+ test('renders heading when provided', async () => {
228
+ const container = await createCard('heading="Test Card Title"')
229
+
230
+ const card = container.querySelector('pkt-card') as PktCard
231
+ await card.updateComplete
232
+
233
+ expect(card.heading).toBe('Test Card Title')
234
+
235
+ const heading = card.querySelector('pkt-heading')
236
+ expect(heading).toBeInTheDocument()
237
+ expect(heading).toHaveClass('pkt-card__heading')
238
+ expect(heading?.textContent?.trim()).toBe('Test Card Title')
239
+ })
240
+
241
+ test('renders subheading when provided', async () => {
242
+ const container = await createCard('subheading="Test Subheading"')
243
+
244
+ const card = container.querySelector('pkt-card') as PktCard
245
+ await card.updateComplete
246
+
247
+ expect(card.subheading).toBe('Test Subheading')
248
+
249
+ const subheading = card.querySelector('.pkt-card__subheading')
250
+ expect(subheading).toBeInTheDocument()
251
+ expect(subheading?.textContent).toBe('Test Subheading')
252
+ })
253
+
254
+ test('applies correct heading level', async () => {
255
+ const container = await createCard('heading="Test" headinglevel="2"')
256
+
257
+ const card = container.querySelector('pkt-card') as PktCard
258
+ await card.updateComplete
259
+
260
+ expect(card.headinglevel).toBe(2)
261
+
262
+ const heading = card.querySelector('pkt-heading')
263
+ expect(heading?.getAttribute('level')).toBe('2')
264
+ })
265
+
266
+ test('does not render header when no heading or subheading', async () => {
267
+ const container = await createCard()
268
+
269
+ const card = container.querySelector('pkt-card') as PktCard
270
+ await card.updateComplete
271
+
272
+ const header = card.querySelector('.pkt-card__header')
273
+ expect(header).not.toBeInTheDocument()
274
+ })
275
+ })
276
+
277
+ describe('Link functionality', () => {
278
+ test('renders as regular card when no clickCardLink', async () => {
279
+ const container = await createCard('heading="Test Title"')
280
+
281
+ const card = container.querySelector('pkt-card') as PktCard
282
+ await card.updateComplete
283
+
284
+ expect(card.clickCardLink).toBe(null)
285
+
286
+ const heading = card.querySelector('pkt-heading')
287
+ const link = card.querySelector('.pkt-card__link')
288
+ expect(heading).toBeInTheDocument()
289
+ expect(link).not.toBeInTheDocument()
290
+
291
+ const article = card.querySelector('article')
292
+ expect(article?.getAttribute('aria-label')).toBe('Test Title')
293
+ })
294
+
295
+ test('renders as link card when clickCardLink provided', async () => {
296
+ const container = await createCard('heading="Test Title"')
297
+ const card = container.querySelector('pkt-card') as PktCard
298
+ card.clickCardLink = '/test-url'
299
+ await card.updateComplete
300
+
301
+ expect(card.clickCardLink).toBe('/test-url')
302
+
303
+ const linkHeading = card.querySelector('.pkt-card__link-heading')
304
+ const link = card.querySelector('.pkt-card__link')
305
+ expect(linkHeading).toBeInTheDocument()
306
+ expect(link).toBeInTheDocument()
307
+ expect(link?.getAttribute('href')).toBe('/test-url')
308
+ expect(link?.textContent).toBe('Test Title')
309
+
310
+ const article = card.querySelector('article')
311
+ expect(article?.getAttribute('aria-label')).toBe('Test Title lenkekort')
312
+ })
313
+
314
+ test('handles openLinkInNewTab correctly', async () => {
315
+ const container = await createCard('heading="Test"')
316
+ const card = container.querySelector('pkt-card') as PktCard
317
+ card.clickCardLink = '/test'
318
+ card.openLinkInNewTab = true
319
+ await card.updateComplete
320
+
321
+ expect(card.openLinkInNewTab).toBe(true)
322
+
323
+ const link = card.querySelector('.pkt-card__link')
324
+ expect(link?.getAttribute('target')).toBe('_blank')
325
+ })
326
+
327
+ test('applies correct aria-label for link cards', async () => {
328
+ const container = await createCard()
329
+ const card = container.querySelector('pkt-card') as PktCard
330
+ card.clickCardLink = '/test'
331
+ card.ariaLabel = 'Custom aria label'
332
+ await card.updateComplete
333
+
334
+ const article = card.querySelector('article')
335
+ expect(article?.getAttribute('aria-label')).toBe('Custom aria label')
336
+ })
337
+ })
338
+
339
+ describe('Image functionality', () => {
340
+ test('renders image when provided', async () => {
341
+ const container = await createCard()
342
+ const card = container.querySelector('pkt-card') as PktCard
343
+ card.image = { src: '/test-image.jpg', alt: 'Test image' }
344
+ await card.updateComplete
345
+
346
+ expect(card.image.src).toBe('/test-image.jpg')
347
+ expect(card.image.alt).toBe('Test image')
348
+
349
+ const imageDiv = card.querySelector('.pkt-card__image')
350
+ const img = imageDiv?.querySelector('img')
351
+ expect(imageDiv).toBeInTheDocument()
352
+ expect(img).toBeInTheDocument()
353
+ expect(img?.getAttribute('src')).toBe('/test-image.jpg')
354
+ expect(img?.getAttribute('alt')).toBe('Test image')
355
+ })
356
+
357
+ test('does not render image when not provided', async () => {
358
+ const container = await createCard()
359
+
360
+ const card = container.querySelector('pkt-card') as PktCard
361
+ await card.updateComplete
362
+
363
+ const imageDiv = card.querySelector('.pkt-card__image')
364
+ expect(imageDiv).not.toBeInTheDocument()
365
+ })
366
+
367
+ test('applies correct image shape classes', async () => {
368
+ const shapes = ['square', 'round'] as const
369
+
370
+ for (const shape of shapes) {
371
+ const container = await createCard()
372
+ const card = container.querySelector('pkt-card') as PktCard
373
+ card.image = { src: '/test.jpg', alt: 'Test' }
374
+ card.imageShape = shape
375
+ await card.updateComplete
376
+
377
+ expect(card.imageShape).toBe(shape)
378
+
379
+ const imageDiv = card.querySelector('.pkt-card__image')
380
+ expect(imageDiv).toHaveClass(`pkt-card__image-${shape}`)
381
+
382
+ // Cleanup for next iteration
383
+ container.remove()
384
+ }
385
+ })
386
+ })
387
+
388
+ describe('Tags functionality', () => {
389
+ test('renders tags when provided', async () => {
390
+ const container = await createCard()
391
+ const card = container.querySelector('pkt-card') as PktCard
392
+ card.tags = [
393
+ { text: 'Tag 1', skin: 'blue' },
394
+ { text: 'Tag 2', skin: 'green' },
395
+ ]
396
+ await card.updateComplete
397
+
398
+ expect(card.tags).toHaveLength(2)
399
+
400
+ const tagsContainer = card.querySelector('.pkt-card__tags')
401
+ const tags = tagsContainer?.querySelectorAll('pkt-tag')
402
+ expect(tagsContainer).toBeInTheDocument()
403
+ expect(tags).toHaveLength(2)
404
+ expect(tagsContainer?.getAttribute('aria-label')).toBe('merkelapper')
405
+ })
406
+
407
+ test('renders single tag with correct aria-label', async () => {
408
+ const container = await createCard()
409
+ const card = container.querySelector('pkt-card') as PktCard
410
+ card.tags = [{ text: 'Single Tag' }]
411
+ await card.updateComplete
412
+
413
+ const tagsContainer = card.querySelector('.pkt-card__tags')
414
+ expect(tagsContainer?.getAttribute('aria-label')).toBe('merkelapp')
415
+ })
416
+
417
+ test('applies correct tag position classes', async () => {
418
+ const positions = ['top', 'bottom'] as const
419
+
420
+ for (const position of positions) {
421
+ const container = await createCard()
422
+ const card = container.querySelector('pkt-card') as PktCard
423
+ card.tags = [{ text: 'Test Tag' }]
424
+ card.tagPosition = position
425
+ await card.updateComplete
426
+
427
+ expect(card.tagPosition).toBe(position)
428
+
429
+ const tagsContainer = card.querySelector('.pkt-card__tags')
430
+ expect(tagsContainer).toHaveClass(`pkt-card__tags-${position}`)
431
+
432
+ // Cleanup for next iteration
433
+ container.remove()
434
+ }
435
+ })
436
+
437
+ test('does not render tags when array is empty', async () => {
438
+ const container = await createCard()
439
+
440
+ const card = container.querySelector('pkt-card') as PktCard
441
+ await card.updateComplete
442
+
443
+ const tagsContainer = card.querySelector('.pkt-card__tags')
444
+ expect(tagsContainer).not.toBeInTheDocument()
445
+ })
446
+ })
447
+
448
+ describe('Metadata functionality', () => {
449
+ test('renders metadata when provided', async () => {
450
+ const container = await createCard()
451
+ const card = container.querySelector('pkt-card') as PktCard
452
+ card.metaLead = 'Author Name'
453
+ card.metaTrail = '2023-12-01'
454
+ await card.updateComplete
455
+
456
+ expect(card.metaLead).toBe('Author Name')
457
+ expect(card.metaTrail).toBe('2023-12-01')
458
+
459
+ const metadata = card.querySelector('.pkt-card__metadata')
460
+ const metaLead = metadata?.querySelector('.pkt-card__metadata-lead')
461
+ const metaTrail = metadata?.querySelector('.pkt-card__metadata-trail')
462
+
463
+ expect(metadata).toBeInTheDocument()
464
+ expect(metaLead).toBeInTheDocument()
465
+ expect(metaTrail).toBeInTheDocument()
466
+ expect(metaLead?.textContent).toBe('Author Name')
467
+ expect(metaTrail?.textContent).toBe('2023-12-01')
468
+ })
469
+
470
+ test('renders only metaLead when metaTrail not provided', async () => {
471
+ const container = await createCard()
472
+ const card = container.querySelector('pkt-card') as PktCard
473
+ card.metaLead = 'Author Only'
474
+ await card.updateComplete
475
+
476
+ const metadata = card.querySelector('.pkt-card__metadata')
477
+ const metaLead = metadata?.querySelector('.pkt-card__metadata-lead')
478
+ const metaTrail = metadata?.querySelector('.pkt-card__metadata-trail')
479
+
480
+ expect(metadata).toBeInTheDocument()
481
+ expect(metaLead).toBeInTheDocument()
482
+ expect(metaTrail).not.toBeInTheDocument()
483
+ expect(metaLead?.textContent).toBe('Author Only')
484
+ })
485
+
486
+ test('renders only metaTrail when metaLead not provided', async () => {
487
+ const container = await createCard()
488
+ const card = container.querySelector('pkt-card') as PktCard
489
+ card.metaTrail = 'Date Only'
490
+ await card.updateComplete
491
+
492
+ const metadata = card.querySelector('.pkt-card__metadata')
493
+ const metaLead = metadata?.querySelector('.pkt-card__metadata-lead')
494
+ const metaTrail = metadata?.querySelector('.pkt-card__metadata-trail')
495
+
496
+ expect(metadata).toBeInTheDocument()
497
+ expect(metaLead).not.toBeInTheDocument()
498
+ expect(metaTrail).toBeInTheDocument()
499
+ expect(metaTrail?.textContent).toBe('Date Only')
500
+ })
501
+
502
+ test('does not render metadata when neither provided', async () => {
503
+ const container = await createCard()
504
+
505
+ const card = container.querySelector('pkt-card') as PktCard
506
+ await card.updateComplete
507
+
508
+ const metadata = card.querySelector('.pkt-card__metadata')
509
+ expect(metadata).not.toBeInTheDocument()
510
+ })
511
+ })
512
+
513
+ describe('Content placement and structure', () => {
514
+ test('renders content elements in correct order', async () => {
515
+ const container = await createCard(
516
+ 'heading="Test Title" subheading="Test Sub"',
517
+ 'Main content here',
518
+ )
519
+ const card = container.querySelector('pkt-card') as PktCard
520
+ card.tags = [{ text: 'Test Tag' }]
521
+ card.image = { src: '/test.jpg', alt: 'Test' }
522
+ card.metaLead = 'Author'
523
+ card.metaTrail = 'Date'
524
+ await card.updateComplete
525
+
526
+ const article = card.querySelector('article')
527
+ const children = Array.from(article?.children || [])
528
+
529
+ // Should have image first, then wrapper
530
+ expect(children[0]).toHaveClass('pkt-card__image')
531
+ expect(children[1]).toHaveClass('pkt-card__wrapper')
532
+
533
+ const wrapper = children[1]
534
+ const wrapperChildren = Array.from(wrapper?.children || [])
535
+
536
+ // Order within wrapper: tags (top), header, content, metadata
537
+ expect(wrapperChildren[0]).toHaveClass('pkt-card__tags-top')
538
+ expect(wrapperChildren[1]).toHaveClass('pkt-card__header')
539
+ expect(wrapperChildren[2]).toHaveClass('pkt-card__content')
540
+ expect(wrapperChildren[3]).toHaveClass('pkt-card__metadata')
541
+ })
542
+
543
+ test('places tags at bottom when tagPosition is bottom', async () => {
544
+ const container = await createCard('heading="Test Title"', 'Main content')
545
+ const card = container.querySelector('pkt-card') as PktCard
546
+ card.tags = [{ text: 'Test Tag' }]
547
+ card.tagPosition = 'bottom'
548
+ await card.updateComplete
549
+
550
+ const wrapper = card.querySelector('.pkt-card__wrapper')
551
+ const wrapperChildren = Array.from(wrapper?.children || [])
552
+
553
+ // Order: header, content, tags (bottom)
554
+ expect(wrapperChildren[0]).toHaveClass('pkt-card__header')
555
+ expect(wrapperChildren[1]).toHaveClass('pkt-card__content')
556
+ expect(wrapperChildren[2]).toHaveClass('pkt-card__tags-bottom')
557
+ })
558
+ })
559
+
560
+ describe('Accessibility', () => {
561
+ test('has no accessibility violations', async () => {
562
+ const container = await createCard(
563
+ 'heading="Accessible Card" aria-label="Test card"',
564
+ 'Accessible content',
565
+ )
566
+
567
+ const card = container.querySelector('pkt-card') as PktCard
568
+ await card.updateComplete
569
+
570
+ const results = await axe(card)
571
+ expect(results).toHaveNoViolations()
572
+ })
573
+
574
+ test('applies correct ARIA attributes', async () => {
575
+ const container = await createCard('heading="Test"')
576
+ const card = container.querySelector('pkt-card') as PktCard
577
+ card.ariaLabel = 'Custom accessible label'
578
+ await card.updateComplete
579
+
580
+ expect(card.ariaLabel).toBe('Custom accessible label')
581
+
582
+ const article = card.querySelector('article')
583
+ expect(article?.getAttribute('aria-label')).toBe('Custom accessible label')
584
+ })
585
+
586
+ test('falls back to heading for aria-label when no explicit aria-label', async () => {
587
+ const container = await createCard('heading="Default Aria Label"')
588
+
589
+ const card = container.querySelector('pkt-card') as PktCard
590
+ await card.updateComplete
591
+
592
+ const article = card.querySelector('article')
593
+ expect(article?.getAttribute('aria-label')).toBe('Default Aria Label')
594
+ })
595
+
596
+ test('falls back to "kort" when no heading or aria-label', async () => {
597
+ const container = await createCard()
598
+
599
+ const card = container.querySelector('pkt-card') as PktCard
600
+ await card.updateComplete
601
+
602
+ const article = card.querySelector('article')
603
+ expect(article?.getAttribute('aria-label')).toBe('kort')
604
+ })
605
+ })
606
+ })
@@ -10,6 +10,7 @@ import { IPktHeading } from '../heading'
10
10
  import specs from 'componentSpecs/card.json'
11
11
  import '@/components/icon'
12
12
  import '@/components/tag'
13
+ import '@/components/heading'
13
14
  import { IAriaAttributes } from '@/types/aria'
14
15
 
15
16
  export type TCardSkin = 'outlined' | 'outlined-beige' | 'gray' | 'beige' | 'green' | 'blue'
@@ -67,7 +68,29 @@ export class PktCard extends PktElement implements IPktCard {
67
68
  @property({ type: String }) imageShape: TCardImageShape = 'square'
68
69
  @property({ type: Boolean }) openLinkInNewTab: boolean = false
69
70
  @property({ type: String }) padding: TCardPadding = specs.props.padding.default as TCardPadding
70
- @property({ type: String }) skin: TCardSkin = specs.props.skin.default as TCardSkin
71
+
72
+ @property({
73
+ type: String,
74
+ converter: {
75
+ fromAttribute: (value: string | null): TCardSkin => {
76
+ const validSkins = specs.props.skin.type as TCardSkin[]
77
+
78
+ if (value && validSkins.includes(value as TCardSkin)) {
79
+ return value as TCardSkin
80
+ } else {
81
+ if (value && !validSkins.includes(value as TCardSkin)) {
82
+ console.warn(
83
+ `Invalid skin value "${value}". Using default skin "${specs.props.skin.default}".`,
84
+ )
85
+ }
86
+ return specs.props.skin.default as TCardSkin
87
+ }
88
+ },
89
+ toAttribute: (value: TCardSkin): string => value,
90
+ },
91
+ })
92
+ skin: TCardSkin = specs.props.skin.default as TCardSkin
93
+
71
94
  @property({ type: String }) subheading: string = ''
72
95
  @property({ type: String }) tagPosition: 'top' | 'bottom' = 'top'
73
96
  @property({ type: Array }) tags: (Omit<IPktTag, 'closeTag'> & { text: string })[] = []