@oslokommune/punkt-elements 13.3.1 → 13.4.1

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.
@@ -0,0 +1,514 @@
1
+ import '@testing-library/jest-dom'
2
+ import { axe, toHaveNoViolations } from 'jest-axe'
3
+ import { fireEvent } from '@testing-library/dom'
4
+
5
+ expect.extend(toHaveNoViolations)
6
+
7
+ import './button'
8
+ import { PktButton } from './button' // For type checking
9
+
10
+ const waitForCustomElements = async () => {
11
+ await Promise.all([
12
+ customElements.whenDefined('pkt-button'),
13
+ customElements.whenDefined('pkt-icon'),
14
+ ])
15
+ }
16
+
17
+ // Helper function to create button markup
18
+ const createButton = async (buttonProps = '', content = 'Test Button') => {
19
+ const container = document.createElement('div')
20
+ container.innerHTML = `
21
+ <pkt-button ${buttonProps}>
22
+ ${content}
23
+ </pkt-button>
24
+ `
25
+ document.body.appendChild(container)
26
+ await waitForCustomElements()
27
+ return container
28
+ }
29
+
30
+ // Cleanup after each test
31
+ afterEach(() => {
32
+ document.body.innerHTML = ''
33
+ })
34
+
35
+ describe('PktButton', () => {
36
+ describe('Rendering and basic functionality', () => {
37
+ test('renders without errors', async () => {
38
+ const container = await createButton()
39
+
40
+ const pktButton = container.querySelector('pkt-button') as PktButton
41
+ expect(pktButton).toBeInTheDocument()
42
+
43
+ await pktButton.updateComplete
44
+ expect(pktButton).toBeTruthy()
45
+
46
+ const button = pktButton.querySelector('button')
47
+ expect(button).toBeInTheDocument()
48
+ })
49
+
50
+ test('renders with correct structure', async () => {
51
+ const container = await createButton('variant="icon-left" icon-name="user"', 'Click Me')
52
+
53
+ const pktButton = container.querySelector('pkt-button') as PktButton
54
+ await pktButton.updateComplete
55
+
56
+ const button = pktButton.querySelector('button')
57
+ const icon = button?.querySelector('pkt-icon')
58
+ const textSpan = button?.querySelector('.pkt-btn__text')
59
+
60
+ expect(button).toHaveClass('pkt-btn')
61
+ expect(icon).toHaveClass('pkt-btn__icon')
62
+ expect(textSpan).toHaveClass('pkt-btn__text')
63
+ expect(textSpan?.textContent?.trim()).toContain('Click Me')
64
+ })
65
+
66
+ test('renders text correctly', async () => {
67
+ const container = await createButton('', 'Button Text Content')
68
+
69
+ const pktButton = container.querySelector('pkt-button') as PktButton
70
+ await pktButton.updateComplete
71
+
72
+ const textSpan = pktButton.querySelector('.pkt-btn__text')
73
+ expect(textSpan).toBeInTheDocument()
74
+ expect(textSpan?.textContent?.trim()).toContain('Button Text Content')
75
+ })
76
+ })
77
+
78
+ describe('Properties and attributes', () => {
79
+ test('applies default properties correctly', async () => {
80
+ const container = await createButton()
81
+
82
+ const pktButton = container.querySelector('pkt-button') as PktButton
83
+ await pktButton.updateComplete
84
+
85
+ expect(pktButton.size).toBe('medium')
86
+ expect(pktButton.skin).toBe('primary')
87
+ expect(pktButton.variant).toBe('label-only')
88
+ expect(pktButton.type).toBe('button')
89
+ expect(pktButton.mode).toBe('light')
90
+ expect(pktButton.disabled).toBe(false)
91
+ expect(pktButton.isLoading).toBe(false)
92
+
93
+ const button = pktButton.querySelector('button')
94
+ expect(button).toHaveClass('pkt-btn')
95
+ expect(button).toHaveClass('pkt-btn--medium')
96
+ expect(button).toHaveClass('pkt-btn--primary')
97
+ expect(button).toHaveClass('pkt-btn--label-only')
98
+ })
99
+
100
+ test('applies different size properties correctly', async () => {
101
+ const sizes = ['small', 'medium', 'large'] as const
102
+
103
+ for (const size of sizes) {
104
+ const container = await createButton(`size="${size}"`)
105
+ const pktButton = container.querySelector('pkt-button') as PktButton
106
+ await pktButton.updateComplete
107
+
108
+ expect(pktButton.size).toBe(size)
109
+
110
+ const button = pktButton.querySelector('button')
111
+ expect(button).toHaveClass(`pkt-btn--${size}`)
112
+
113
+ // Cleanup for next iteration
114
+ container.remove()
115
+ }
116
+ })
117
+
118
+ test('applies different skin properties correctly', async () => {
119
+ const skins = ['primary', 'secondary', 'tertiary'] as const
120
+
121
+ for (const skin of skins) {
122
+ const container = await createButton(`skin="${skin}"`)
123
+ const pktButton = container.querySelector('pkt-button') as PktButton
124
+ await pktButton.updateComplete
125
+
126
+ expect(pktButton.skin).toBe(skin)
127
+
128
+ const button = pktButton.querySelector('button')
129
+ expect(button).toHaveClass(`pkt-btn--${skin}`)
130
+
131
+ // Cleanup for next iteration
132
+ container.remove()
133
+ }
134
+ })
135
+
136
+ test('applies different variant properties correctly', async () => {
137
+ const variants = [
138
+ 'label-only',
139
+ 'icon-left',
140
+ 'icon-right',
141
+ 'icon-only',
142
+ 'icons-right-and-left',
143
+ ] as const
144
+
145
+ for (const variant of variants) {
146
+ const container = await createButton(
147
+ `variant="${variant}" icon-name="user" second-icon-name="star"`,
148
+ )
149
+ const pktButton = container.querySelector('pkt-button') as PktButton
150
+ await pktButton.updateComplete
151
+
152
+ expect(pktButton.variant).toBe(variant)
153
+
154
+ const button = pktButton.querySelector('button')
155
+ expect(button).toHaveClass(`pkt-btn--${variant}`)
156
+
157
+ // Check icon rendering based on variant
158
+ const icons = button?.querySelectorAll('pkt-icon:not(.pkt-btn__spinner)')
159
+ if (variant === 'label-only') {
160
+ expect(icons).toHaveLength(0)
161
+ } else if (variant === 'icons-right-and-left') {
162
+ expect(icons).toHaveLength(2)
163
+ } else {
164
+ expect(icons).toHaveLength(1)
165
+ }
166
+
167
+ // Cleanup for next iteration
168
+ container.remove()
169
+ }
170
+ })
171
+
172
+ test('applies different color properties correctly', async () => {
173
+ const colors = ['blue', 'green', 'red', 'yellow'] as const
174
+
175
+ for (const color of colors) {
176
+ const container = await createButton(`color="${color}"`)
177
+ const pktButton = container.querySelector('pkt-button') as PktButton
178
+ await pktButton.updateComplete
179
+
180
+ expect(pktButton.color).toBe(color)
181
+
182
+ const button = pktButton.querySelector('button')
183
+ expect(button).toHaveClass(`pkt-btn--${color}`)
184
+
185
+ // Cleanup for next iteration
186
+ container.remove()
187
+ }
188
+ })
189
+
190
+ test('handles type property correctly', async () => {
191
+ const types = ['button', 'submit', 'reset'] as const
192
+
193
+ for (const type of types) {
194
+ const container = await createButton(`type="${type}"`)
195
+ const pktButton = container.querySelector('pkt-button') as PktButton
196
+ await pktButton.updateComplete
197
+
198
+ expect(pktButton.type).toBe(type)
199
+
200
+ const button = pktButton.querySelector('button')
201
+ expect(button?.getAttribute('type')).toBe(type)
202
+
203
+ // Cleanup for next iteration
204
+ container.remove()
205
+ }
206
+ })
207
+
208
+ test('handles icon properties correctly', async () => {
209
+ const container = await createButton('variant="icon-left" icon-name="user"')
210
+
211
+ const pktButton = container.querySelector('pkt-button') as PktButton
212
+ await pktButton.updateComplete
213
+
214
+ expect(pktButton.iconName).toBe('user')
215
+
216
+ const icon = pktButton.querySelector('pkt-icon:not(.pkt-btn__spinner)')
217
+ expect(icon?.getAttribute('name')).toBe('user')
218
+ expect(icon).toHaveClass('pkt-btn__icon')
219
+ })
220
+
221
+ test('handles second icon for icons-right-and-left variant', async () => {
222
+ const container = await createButton('variant="icons-right-and-left"')
223
+ const pktButton = container.querySelector('pkt-button') as PktButton
224
+
225
+ // Set both icon names as properties
226
+ pktButton.iconName = 'home'
227
+ pktButton.secondIconName = 'star'
228
+ await pktButton.updateComplete
229
+
230
+ expect(pktButton.iconName).toBe('home')
231
+ expect(pktButton.secondIconName).toBe('star')
232
+
233
+ const icons = pktButton.querySelectorAll('pkt-icon:not(.pkt-btn__spinner)')
234
+ expect(icons).toHaveLength(2)
235
+ expect(icons[0]?.getAttribute('name')).toBe('home')
236
+ expect(icons[1]?.getAttribute('name')).toBe('star')
237
+ })
238
+ })
239
+
240
+ describe('Disabled state', () => {
241
+ test('handles disabled property correctly', async () => {
242
+ const container = await createButton('disabled')
243
+
244
+ const pktButton = container.querySelector('pkt-button') as PktButton
245
+ await pktButton.updateComplete
246
+
247
+ expect(pktButton.disabled).toBe(true)
248
+
249
+ const button = pktButton.querySelector('button')
250
+ expect(button).toHaveClass('pkt-btn--disabled')
251
+ expect(button?.hasAttribute('disabled')).toBe(true)
252
+ expect(button?.getAttribute('aria-disabled')).toBe('true')
253
+ })
254
+
255
+ test('prevents click events when disabled', async () => {
256
+ const container = await createButton('disabled')
257
+ const clickSpy = jest.fn()
258
+
259
+ const pktButton = container.querySelector('pkt-button') as PktButton
260
+ await pktButton.updateComplete
261
+
262
+ pktButton.addEventListener('click', clickSpy)
263
+
264
+ const button = pktButton.querySelector('button')
265
+ fireEvent.click(button!)
266
+
267
+ expect(clickSpy).not.toHaveBeenCalled()
268
+ })
269
+
270
+ test('prevents keyboard events when disabled', async () => {
271
+ const container = await createButton('disabled')
272
+ const clickSpy = jest.fn()
273
+
274
+ const pktButton = container.querySelector('pkt-button') as PktButton
275
+ await pktButton.updateComplete
276
+
277
+ pktButton.addEventListener('click', clickSpy)
278
+
279
+ const button = pktButton.querySelector('button')
280
+ fireEvent.keyDown(button!, { key: 'Enter' })
281
+ fireEvent.keyDown(button!, { key: ' ' })
282
+
283
+ expect(clickSpy).not.toHaveBeenCalled()
284
+ })
285
+
286
+ test('converts string "false" to boolean false for disabled', async () => {
287
+ const container = await createButton('disabled="false"')
288
+
289
+ const pktButton = container.querySelector('pkt-button') as PktButton
290
+ await pktButton.updateComplete
291
+
292
+ expect(pktButton.disabled).toBe(false)
293
+
294
+ const button = pktButton.querySelector('button')
295
+ expect(button).not.toHaveClass('pkt-btn--disabled')
296
+ expect(button?.hasAttribute('disabled')).toBe(false)
297
+ })
298
+ })
299
+
300
+ describe('Loading state', () => {
301
+ test('handles isLoading property correctly', async () => {
302
+ const container = await createButton()
303
+ const pktButton = container.querySelector('pkt-button') as PktButton
304
+
305
+ // Set isLoading as a property
306
+ pktButton.isLoading = true
307
+ await pktButton.updateComplete
308
+
309
+ expect(pktButton.isLoading).toBe(true)
310
+
311
+ const button = pktButton.querySelector('button')
312
+ expect(button).toHaveClass('pkt-btn--isLoading')
313
+ expect(button?.getAttribute('aria-busy')).toBe('true')
314
+ expect(button?.getAttribute('aria-disabled')).toBe('true')
315
+ })
316
+
317
+ test('renders loading spinner when isLoading is true', async () => {
318
+ const container = await createButton()
319
+ const pktButton = container.querySelector('pkt-button') as PktButton
320
+
321
+ // Set isLoading as a property
322
+ pktButton.isLoading = true
323
+ await pktButton.updateComplete
324
+
325
+ const spinner = pktButton.querySelector('.pkt-btn__spinner')
326
+ expect(spinner).toBeInTheDocument()
327
+ expect(spinner?.getAttribute('name')).toBe('spinner-blue')
328
+ })
329
+
330
+ test('prevents click events when loading', async () => {
331
+ const container = await createButton()
332
+ const clickSpy = jest.fn()
333
+
334
+ const pktButton = container.querySelector('pkt-button') as PktButton
335
+
336
+ // Set isLoading as a property
337
+ pktButton.isLoading = true
338
+ await pktButton.updateComplete
339
+
340
+ pktButton.addEventListener('click', clickSpy)
341
+
342
+ const button = pktButton.querySelector('button')
343
+ fireEvent.click(button!)
344
+
345
+ expect(clickSpy).not.toHaveBeenCalled()
346
+ })
347
+
348
+ test('uses custom loading animation path', async () => {
349
+ const customPath = 'https://custom.example.com/animations/'
350
+ const container = await createButton('isLoading')
351
+ const pktButton = container.querySelector('pkt-button') as PktButton
352
+
353
+ pktButton.loadingAnimationPath = customPath
354
+ await pktButton.updateComplete
355
+
356
+ expect(pktButton.loadingAnimationPath).toBe(customPath)
357
+
358
+ const spinner = pktButton.querySelector('.pkt-btn__spinner')
359
+ expect(spinner?.getAttribute('path')).toBe(customPath)
360
+ })
361
+
362
+ test('converts string "false" to boolean false for isLoading', async () => {
363
+ const container = await createButton('isLoading="false"')
364
+
365
+ const pktButton = container.querySelector('pkt-button') as PktButton
366
+ await pktButton.updateComplete
367
+
368
+ expect(pktButton.isLoading).toBe(false)
369
+
370
+ const button = pktButton.querySelector('button')
371
+ expect(button).not.toHaveClass('pkt-btn--isLoading')
372
+ })
373
+ })
374
+
375
+ describe('Form integration', () => {
376
+ test('handles form attribute correctly', async () => {
377
+ const container = await createButton('form="test-form" type="submit"')
378
+
379
+ const pktButton = container.querySelector('pkt-button') as PktButton
380
+ await pktButton.updateComplete
381
+
382
+ expect(pktButton.form).toBe('test-form')
383
+
384
+ const button = pktButton.querySelector('button')
385
+ expect(button?.getAttribute('form')).toBe('test-form')
386
+ })
387
+
388
+ test('works as submit button', async () => {
389
+ const container = await createButton('type="submit"')
390
+
391
+ const pktButton = container.querySelector('pkt-button') as PktButton
392
+ await pktButton.updateComplete
393
+
394
+ const button = pktButton.querySelector('button')
395
+ expect(button?.getAttribute('type')).toBe('submit')
396
+ })
397
+ })
398
+
399
+ describe('Click functionality', () => {
400
+ test('allows click events when not disabled or loading', async () => {
401
+ const container = await createButton()
402
+ const clickSpy = jest.fn()
403
+
404
+ const pktButton = container.querySelector('pkt-button') as PktButton
405
+ await pktButton.updateComplete
406
+
407
+ pktButton.addEventListener('click', clickSpy)
408
+
409
+ const button = pktButton.querySelector('button')
410
+ fireEvent.click(button!)
411
+
412
+ expect(clickSpy).toHaveBeenCalledTimes(1)
413
+ })
414
+
415
+ test('allows keyboard activation when not disabled or loading', async () => {
416
+ const container = await createButton()
417
+ const clickSpy = jest.fn()
418
+
419
+ const pktButton = container.querySelector('pkt-button') as PktButton
420
+ await pktButton.updateComplete
421
+
422
+ pktButton.addEventListener('click', clickSpy)
423
+
424
+ const button = pktButton.querySelector('button')
425
+ fireEvent.keyDown(button!, { key: 'Enter' })
426
+
427
+ // Note: Native button handles Enter key, so we just test that events aren't prevented
428
+ // The actual click event would be triggered by the browser
429
+ })
430
+ })
431
+
432
+ describe('Accessibility', () => {
433
+ test('has correct ARIA attributes', async () => {
434
+ const container = await createButton('disabled')
435
+ const pktButton = container.querySelector('pkt-button') as PktButton
436
+
437
+ // Set isLoading as a property
438
+ pktButton.isLoading = true
439
+ await pktButton.updateComplete
440
+
441
+ const button = pktButton.querySelector('button')
442
+
443
+ expect(button?.getAttribute('aria-disabled')).toBe('true')
444
+ expect(button?.getAttribute('aria-busy')).toBe('true')
445
+ expect(button?.hasAttribute('disabled')).toBe(true)
446
+ })
447
+
448
+ test('provides semantic button structure', async () => {
449
+ const container = await createButton()
450
+
451
+ const pktButton = container.querySelector('pkt-button') as PktButton
452
+ await pktButton.updateComplete
453
+
454
+ const button = pktButton.querySelector('button')
455
+
456
+ expect(button).toBeInTheDocument()
457
+ expect(button?.tagName.toLowerCase()).toBe('button')
458
+ expect(button?.getAttribute('type')).toBe('button')
459
+ })
460
+
461
+ test('renders with no WCAG errors with axe - default button', async () => {
462
+ const container = await createButton('', 'Click me')
463
+
464
+ const pktButton = container.querySelector('pkt-button') as PktButton
465
+ await pktButton.updateComplete
466
+
467
+ const results = await axe(container)
468
+ expect(results).toHaveNoViolations()
469
+ })
470
+
471
+ test('renders with no WCAG errors with axe - icon button', async () => {
472
+ const container = await createButton(
473
+ 'variant="icon-left" icon-name="user" skin="secondary"',
474
+ 'User Profile',
475
+ )
476
+
477
+ const pktButton = container.querySelector('pkt-button') as PktButton
478
+ await pktButton.updateComplete
479
+
480
+ const results = await axe(container)
481
+ expect(results).toHaveNoViolations()
482
+ })
483
+
484
+ test('renders with no WCAG errors with axe - disabled button', async () => {
485
+ const container = await createButton('disabled', 'Disabled Button')
486
+
487
+ const pktButton = container.querySelector('pkt-button') as PktButton
488
+ await pktButton.updateComplete
489
+
490
+ const results = await axe(container)
491
+ expect(results).toHaveNoViolations()
492
+ })
493
+
494
+ test('renders with no WCAG errors with axe - loading button', async () => {
495
+ const container = await createButton('isLoading', 'Loading...')
496
+
497
+ const pktButton = container.querySelector('pkt-button') as PktButton
498
+ await pktButton.updateComplete
499
+
500
+ const results = await axe(container)
501
+ expect(results).toHaveNoViolations()
502
+ })
503
+
504
+ test('renders with no WCAG errors with axe - submit button', async () => {
505
+ const container = await createButton('type="submit" color="green"', 'Submit Form')
506
+
507
+ const pktButton = container.querySelector('pkt-button') as PktButton
508
+ await pktButton.updateComplete
509
+
510
+ const results = await axe(container)
511
+ expect(results).toHaveNoViolations()
512
+ })
513
+ })
514
+ })
@@ -7,6 +7,8 @@ import { classMap } from 'lit/directives/class-map.js'
7
7
  import { ifDefined } from 'lit/directives/if-defined.js'
8
8
  import { createRef, Ref, ref } from 'lit/directives/ref.js'
9
9
 
10
+ import '@/components/icon'
11
+
10
12
  // Allow global override of animation assets path
11
13
  window.pktAnimationPath =
12
14
  window.pktAnimationPath || 'https://punkt-cdn.oslo.kommune.no/latest/animations/'
@@ -1,13 +0,0 @@
1
- "use strict";const i=require("./element-6DBpyGQm.cjs"),c=require("./if-defined-Cni-RHLS.cjs");var p=Object.defineProperty,o=Object.getOwnPropertyDescriptor,n=(k,t,a,r)=>{for(var e=r>1?void 0:r?o(t,a):t,l=k.length-1,s;l>=0;l--)(s=k[l])&&(e=(r?s(t,a,e):s(e))||e);return r&&e&&p(t,a,e),e};exports.PktBackLink=class extends i.PktElement{constructor(){super(...arguments),this.href="",this.text="Forsiden",this.ariaLabel=""}attributeChangedCallback(t,a,r){t==="arialabel"&&this.removeAttribute("arialabel"),t==="href"&&this.removeAttribute("href"),t==="text"&&this.removeAttribute("text"),super.attributeChangedCallback(t,a,r)}render(){return i.x`<nav
2
- class="pkt-back-link"
3
- aria-label=${this.ariaLabel||"Gå tilbake til forrige side"}
4
- >
5
- <a href=${c.o(this.href||"/")} class="pkt-link pkt-link--icon-left"
6
- ><pkt-icon
7
- class="pkt-back-link__icon pkt-icon pkt-link__icon"
8
- name="chevron-thin-left"
9
- aria-hidden="true"
10
- ></pkt-icon
11
- ><span class="pkt-back-link__text">${this.text}</span></a
12
- >
13
- </nav>`}};n([i.n({type:String,reflect:!0})],exports.PktBackLink.prototype,"href",2);n([i.n({type:String,reflect:!0})],exports.PktBackLink.prototype,"text",2);n([i.n({type:String,reflect:!0})],exports.PktBackLink.prototype,"ariaLabel",2);exports.PktBackLink=n([i.t("pkt-backlink")],exports.PktBackLink);