@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.
- package/CHANGELOG.md +35 -0
- package/dist/calendar-32W9p9uc.cjs +115 -0
- package/dist/{calendar-DevQhOup.js → calendar-CJSxvwAq.js} +353 -340
- package/dist/{card-uccD6Pnv.cjs → card-BUITGoqX.cjs} +10 -10
- package/dist/{card-BI1NZONj.js → card-Dtw26f7i.js} +96 -76
- package/dist/checkbox-Gn7Wtk9h.cjs +31 -0
- package/dist/checkbox-ym7z6cpt.js +142 -0
- package/dist/{combobox-BhcqC30d.cjs → combobox-DjO0RMUB.cjs} +1 -1
- package/dist/{combobox-D9dGKWuZ.js → combobox-yE4aYhTi.js} +1 -1
- package/dist/{datepicker-CYOn3tRm.js → datepicker-BJKJBoy_.js} +102 -59
- package/dist/datepicker-CmTrG5GE.cjs +164 -0
- package/dist/index.d.ts +9 -2
- package/dist/pkt-calendar.cjs +1 -1
- package/dist/pkt-calendar.js +1 -1
- package/dist/pkt-card.cjs +1 -1
- package/dist/pkt-card.js +1 -1
- package/dist/pkt-checkbox.cjs +1 -1
- package/dist/pkt-checkbox.js +1 -1
- package/dist/pkt-combobox.cjs +1 -1
- package/dist/pkt-combobox.js +1 -1
- package/dist/pkt-datepicker.cjs +1 -1
- package/dist/pkt-datepicker.js +1 -1
- package/dist/pkt-index.cjs +1 -1
- package/dist/pkt-index.js +6 -6
- package/package.json +3 -3
- package/src/components/calendar/calendar.accessibility.test.ts +111 -0
- package/src/components/calendar/calendar.constraints.test.ts +110 -0
- package/src/components/calendar/calendar.core.test.ts +367 -0
- package/src/components/calendar/calendar.interaction.test.ts +139 -0
- package/src/components/calendar/calendar.selection.test.ts +273 -0
- package/src/components/calendar/calendar.ts +74 -42
- package/src/components/card/card.test.ts +606 -0
- package/src/components/card/card.ts +24 -1
- package/src/components/checkbox/checkbox.test.ts +535 -0
- package/src/components/checkbox/checkbox.ts +44 -1
- package/src/components/combobox/combobox.test.ts +737 -0
- package/src/components/combobox/combobox.ts +1 -1
- package/src/components/datepicker/datepicker.accessibility.test.ts +193 -0
- package/src/components/datepicker/datepicker.core.test.ts +322 -0
- package/src/components/datepicker/datepicker.input.test.ts +268 -0
- package/src/components/datepicker/datepicker.selection.test.ts +286 -0
- package/src/components/datepicker/datepicker.ts +121 -19
- package/src/components/datepicker/datepicker.validation.test.ts +176 -0
- package/dist/calendar-BZe2D4Sr.cjs +0 -108
- package/dist/checkbox-CTRbpbye.js +0 -120
- package/dist/checkbox-wJ26voZd.cjs +0 -30
- 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
|
-
|
|
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 })[] = []
|