@oslokommune/punkt-elements 13.4.2 → 13.5.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.
Files changed (41) 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-Dtw26f7i.js → card-BDz4RWxK.js} +1 -1
  5. package/dist/{card-BUITGoqX.cjs → card-DBlFf1ry.cjs} +1 -1
  6. package/dist/{datepicker-CYOn3tRm.js → datepicker-BJKJBoy_.js} +102 -59
  7. package/dist/datepicker-CmTrG5GE.cjs +164 -0
  8. package/dist/{heading-D6jXE_Mz.js → heading-Bdh9absf.js} +22 -22
  9. package/dist/heading-CNycsyMj.cjs +1 -0
  10. package/dist/index.d.ts +6 -2
  11. package/dist/pkt-calendar.cjs +1 -1
  12. package/dist/pkt-calendar.js +1 -1
  13. package/dist/pkt-card.cjs +1 -1
  14. package/dist/pkt-card.js +1 -1
  15. package/dist/pkt-datepicker.cjs +1 -1
  16. package/dist/pkt-datepicker.js +1 -1
  17. package/dist/pkt-heading.cjs +1 -1
  18. package/dist/pkt-heading.js +1 -1
  19. package/dist/pkt-index.cjs +1 -1
  20. package/dist/pkt-index.js +5 -5
  21. package/package.json +3 -3
  22. package/src/components/calendar/calendar.accessibility.test.ts +111 -0
  23. package/src/components/calendar/calendar.constraints.test.ts +110 -0
  24. package/src/components/calendar/calendar.core.test.ts +367 -0
  25. package/src/components/calendar/calendar.interaction.test.ts +139 -0
  26. package/src/components/calendar/calendar.selection.test.ts +273 -0
  27. package/src/components/calendar/calendar.ts +74 -42
  28. package/src/components/card/card.test.ts +19 -5
  29. package/src/components/consent/consent.test.ts +436 -0
  30. package/src/components/datepicker/datepicker.accessibility.test.ts +193 -0
  31. package/src/components/datepicker/datepicker.core.test.ts +322 -0
  32. package/src/components/datepicker/datepicker.input.test.ts +268 -0
  33. package/src/components/datepicker/datepicker.selection.test.ts +286 -0
  34. package/src/components/datepicker/datepicker.ts +121 -19
  35. package/src/components/datepicker/datepicker.validation.test.ts +176 -0
  36. package/src/components/heading/heading.test.ts +458 -0
  37. package/src/components/heading/heading.ts +3 -0
  38. package/src/components/helptext/helptext.test.ts +474 -0
  39. package/dist/calendar-BZe2D4Sr.cjs +0 -108
  40. package/dist/datepicker-B9rhz_AF.cjs +0 -154
  41. package/dist/heading-BRE_iFtR.cjs +0 -1
@@ -0,0 +1,436 @@
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 './consent'
8
+ import { PktConsent } from './consent'
9
+
10
+ const waitForCustomElements = async () => {
11
+ await customElements.whenDefined('pkt-consent')
12
+ await customElements.whenDefined('pkt-button')
13
+ }
14
+
15
+ // Helper function to create consent markup
16
+ const createConsent = async (consentProps = '') => {
17
+ const container = document.createElement('div')
18
+ container.innerHTML = `
19
+ <pkt-consent ${consentProps}></pkt-consent>
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
+ // Clean up any injected scripts
30
+ const existingScript = document.querySelector('#oslo-consent-script')
31
+ if (existingScript) {
32
+ existingScript.remove()
33
+ }
34
+ const existingStyles = document.querySelector('#oslo-consent-styles')
35
+ if (existingStyles) {
36
+ existingStyles.remove()
37
+ }
38
+ // Clean up global variables
39
+ delete window.cookieBanner_googleAnalyticsId
40
+ delete window.cookieBanner_hotjarId
41
+ delete window.cookieBanner_devMode
42
+ delete window.cookieBanner_cookieDomain
43
+ delete window.cookieBanner_cookieSecure
44
+ delete window.cookieBanner_cookieExpiryDays
45
+ delete window.cookieBanner
46
+ delete window.__cookieEvents
47
+ })
48
+
49
+ describe('PktConsent', () => {
50
+ describe('Rendering and basic functionality', () => {
51
+ test('renders without errors', async () => {
52
+ const container = await createConsent()
53
+
54
+ const consent = container.querySelector('pkt-consent') as PktConsent
55
+ expect(consent).toBeInTheDocument()
56
+
57
+ await consent.updateComplete
58
+ expect(consent).toBeTruthy()
59
+ })
60
+
61
+ test('renders with default trigger type as button', async () => {
62
+ const container = await createConsent()
63
+
64
+ const consent = container.querySelector('pkt-consent') as PktConsent
65
+ await consent.updateComplete
66
+
67
+ expect(consent.triggerType).toBe('button')
68
+
69
+ const button = consent.querySelector('pkt-button')
70
+ expect(button).toBeInTheDocument()
71
+ })
72
+
73
+ test('renders with custom trigger text', async () => {
74
+ const customText = 'Custom consent settings'
75
+ const container = await createConsent(`triggerText="${customText}"`)
76
+
77
+ const consent = container.querySelector('pkt-consent') as PktConsent
78
+ await consent.updateComplete
79
+
80
+ expect(consent.triggerText).toBe(customText)
81
+ const button = consent.querySelector('pkt-button')
82
+ expect(button?.textContent?.trim()).toBe(customText)
83
+ })
84
+ })
85
+
86
+ describe('Properties and attributes', () => {
87
+ test('applies default properties correctly', async () => {
88
+ const container = await createConsent()
89
+
90
+ const consent = container.querySelector('pkt-consent') as PktConsent
91
+ await consent.updateComplete
92
+
93
+ expect(consent.devMode).toBe(false)
94
+ expect(consent.triggerType).toBe('button')
95
+ expect(consent.i18nLanguage).toBe('nb')
96
+ expect(consent.hotjarId).toBe(null)
97
+ expect(consent.googleAnalyticsId).toBe(null)
98
+ expect(consent.cookieDomain).toBe(null)
99
+ expect(consent.cookieSecure).toBe(null)
100
+ expect(consent.cookieExpiryDays).toBe(null)
101
+ })
102
+
103
+ test('sets dev mode correctly', async () => {
104
+ const container = await createConsent('devMode')
105
+
106
+ const consent = container.querySelector('pkt-consent') as PktConsent
107
+ await consent.updateComplete
108
+
109
+ expect(consent.devMode).toBe(true)
110
+ })
111
+
112
+ test('sets analytics IDs correctly', async () => {
113
+ const googleId = 'GA-123456789'
114
+ const hotjarId = 'HJ-987654321'
115
+ const container = await createConsent(
116
+ `googleAnalyticsId="${googleId}" hotjarId="${hotjarId}"`,
117
+ )
118
+
119
+ const consent = container.querySelector('pkt-consent') as PktConsent
120
+ await consent.updateComplete
121
+
122
+ expect(consent.googleAnalyticsId).toBe(googleId)
123
+ expect(consent.hotjarId).toBe(hotjarId)
124
+ })
125
+
126
+ test('sets cookie configuration correctly', async () => {
127
+ const domain = '.example.com'
128
+ const secure = 'true'
129
+ const expiryDays = '365'
130
+ const container = await createConsent(
131
+ `cookieDomain="${domain}" cookieSecure="${secure}" cookieExpiryDays="${expiryDays}"`,
132
+ )
133
+
134
+ const consent = container.querySelector('pkt-consent') as PktConsent
135
+ await consent.updateComplete
136
+
137
+ expect(consent.cookieDomain).toBe(domain)
138
+ expect(consent.cookieSecure).toBe(secure)
139
+ expect(consent.cookieExpiryDays).toBe(expiryDays)
140
+ })
141
+
142
+ test('sets language correctly', async () => {
143
+ const container = await createConsent('i18nLanguage="en"')
144
+
145
+ const consent = container.querySelector('pkt-consent') as PktConsent
146
+ await consent.updateComplete
147
+
148
+ expect(consent.i18nLanguage).toBe('en')
149
+ })
150
+ })
151
+
152
+ describe('Trigger types', () => {
153
+ test('renders as button trigger type', async () => {
154
+ const container = await createConsent('triggerType="button"')
155
+
156
+ const consent = container.querySelector('pkt-consent') as PktConsent
157
+ await consent.updateComplete
158
+
159
+ expect(consent.triggerType).toBe('button')
160
+ const button = consent.querySelector('pkt-button')
161
+ expect(button).toBeInTheDocument()
162
+ expect(button?.getAttribute('skin')).toBe(null) // Default button
163
+ })
164
+
165
+ test('renders as link trigger type', async () => {
166
+ const container = await createConsent('triggerType="link"')
167
+
168
+ const consent = container.querySelector('pkt-consent') as PktConsent
169
+ await consent.updateComplete
170
+
171
+ expect(consent.triggerType).toBe('link')
172
+ const link = consent.querySelector('a.pkt-link')
173
+ expect(link).toBeInTheDocument()
174
+ expect(link?.getAttribute('href')).toBe('#')
175
+ })
176
+
177
+ test('renders as footer link trigger type', async () => {
178
+ const container = await createConsent('triggerType="footerlink"')
179
+
180
+ const consent = container.querySelector('pkt-consent') as PktConsent
181
+ await consent.updateComplete
182
+
183
+ expect(consent.triggerType).toBe('footerlink')
184
+ const footerLink = consent.querySelector('a.pkt-footer__link')
185
+ expect(footerLink).toBeInTheDocument()
186
+
187
+ const icon = footerLink?.querySelector('pkt-icon')
188
+ expect(icon).toBeInTheDocument()
189
+ expect(icon?.getAttribute('name')).toBe('chevron-right')
190
+ })
191
+
192
+ test('renders as icon trigger type', async () => {
193
+ const container = await createConsent('triggerType="icon"')
194
+
195
+ const consent = container.querySelector('pkt-consent') as PktConsent
196
+ await consent.updateComplete
197
+
198
+ expect(consent.triggerType).toBe('icon')
199
+ const button = consent.querySelector('pkt-button')
200
+ expect(button).toBeInTheDocument()
201
+ expect(button?.getAttribute('skin')).toBe('tertiary')
202
+ expect(button?.getAttribute('variant')).toBe('icon-only')
203
+ expect(button?.getAttribute('iconName')).toBe('cookie')
204
+ })
205
+ })
206
+
207
+ describe('Event handling', () => {
208
+ beforeEach(() => {
209
+ // Mock the entire window.cookieBanner object
210
+ Object.defineProperty(window, 'cookieBanner', {
211
+ value: {
212
+ cookieConsent: {
213
+ validateConsentCookie: jest.fn().mockResolvedValue(true),
214
+ getConsentCookie: jest.fn().mockReturnValue('mock-cookie'),
215
+ },
216
+ openCookieModal: jest.fn(),
217
+ },
218
+ writable: true,
219
+ })
220
+
221
+ // Mock window.__cookieEvents
222
+ Object.defineProperty(window, '__cookieEvents', {
223
+ value: {
224
+ on: jest.fn(),
225
+ off: jest.fn(),
226
+ },
227
+ writable: true,
228
+ })
229
+
230
+ // Use fake timers to control setTimeout
231
+ jest.useFakeTimers()
232
+ })
233
+
234
+ afterEach(() => {
235
+ // Clean up mocks and timers
236
+ jest.useRealTimers()
237
+ delete (window as any).cookieBanner
238
+ delete (window as any).__cookieEvents
239
+ })
240
+
241
+ test('handles click event on button trigger', async () => {
242
+ const container = await createConsent()
243
+ const consent = container.querySelector('pkt-consent') as PktConsent
244
+ await consent.updateComplete
245
+
246
+ const button = consent.querySelector('pkt-button')
247
+ expect(button).toBeInTheDocument()
248
+
249
+ fireEvent.click(button!)
250
+
251
+ // Fast-forward time to trigger setTimeout
252
+ jest.runAllTimers()
253
+
254
+ expect(window.cookieBanner.openCookieModal).toHaveBeenCalled()
255
+ })
256
+
257
+ test('handles click event on link trigger', async () => {
258
+ const container = await createConsent('triggerType="link"')
259
+ const consent = container.querySelector('pkt-consent') as PktConsent
260
+ await consent.updateComplete
261
+
262
+ const link = consent.querySelector('a.pkt-link')
263
+ expect(link).toBeInTheDocument()
264
+
265
+ fireEvent.click(link!)
266
+
267
+ // Fast-forward time to trigger setTimeout
268
+ jest.runAllTimers()
269
+
270
+ expect(window.cookieBanner.openCookieModal).toHaveBeenCalled()
271
+ })
272
+
273
+ test('dispatches toggle-consent event', async () => {
274
+ const container = await createConsent()
275
+ const consent = container.querySelector('pkt-consent') as PktConsent
276
+ await consent.updateComplete
277
+
278
+ let eventFired = false
279
+ let eventDetail: any = null
280
+
281
+ consent.addEventListener('toggle-consent', (e: Event) => {
282
+ eventFired = true
283
+ eventDetail = (e as CustomEvent).detail
284
+ })
285
+
286
+ // Mock consent data
287
+ const mockConsent = {
288
+ value: JSON.stringify({
289
+ items: [
290
+ { name: 'analytics', consent: true },
291
+ { name: 'marketing', consent: false },
292
+ ],
293
+ }),
294
+ }
295
+
296
+ consent.emitCookieConsents(mockConsent)
297
+
298
+ expect(eventFired).toBe(true)
299
+ expect(eventDetail).toEqual({
300
+ analytics: true,
301
+ marketing: false,
302
+ })
303
+ })
304
+ })
305
+
306
+ describe('Utility methods', () => {
307
+ test('returnJsonOrObject handles JSON strings', async () => {
308
+ const container = await createConsent()
309
+ const consent = container.querySelector('pkt-consent') as PktConsent
310
+
311
+ const jsonString = '{"test": "value"}'
312
+ const result = consent.returnJsonOrObject(jsonString)
313
+
314
+ expect(result).toEqual({ test: 'value' })
315
+ })
316
+
317
+ test('returnJsonOrObject handles non-JSON objects', async () => {
318
+ const container = await createConsent()
319
+ const consent = container.querySelector('pkt-consent') as PktConsent
320
+
321
+ const nonJsonObject = { test: 'value' }
322
+ const result = consent.returnJsonOrObject(nonJsonObject)
323
+
324
+ expect(result).toEqual(nonJsonObject)
325
+ })
326
+
327
+ test('returnJsonOrObject handles invalid JSON gracefully', async () => {
328
+ const container = await createConsent()
329
+ const consent = container.querySelector('pkt-consent') as PktConsent
330
+
331
+ const invalidJson = 'invalid json string'
332
+ const result = consent.returnJsonOrObject(invalidJson)
333
+
334
+ expect(result).toBe(invalidJson)
335
+ })
336
+ })
337
+
338
+ describe('Accessibility', () => {
339
+ test('button trigger is accessible', async () => {
340
+ const container = await createConsent('triggerText="Cookie settings"')
341
+
342
+ const consent = container.querySelector('pkt-consent') as PktConsent
343
+ await consent.updateComplete
344
+
345
+ const results = await axe(container)
346
+ expect(results).toHaveNoViolations()
347
+
348
+ const button = consent.querySelector('pkt-button')
349
+ expect(button).toBeInTheDocument()
350
+ expect(button?.textContent?.trim()).toBe('Cookie settings')
351
+ })
352
+
353
+ test('link trigger is accessible', async () => {
354
+ const container = await createConsent('triggerType="link" triggerText="Cookie settings"')
355
+
356
+ const consent = container.querySelector('pkt-consent') as PktConsent
357
+ await consent.updateComplete
358
+
359
+ const results = await axe(container)
360
+ expect(results).toHaveNoViolations()
361
+
362
+ const link = consent.querySelector('a')
363
+ expect(link).toBeInTheDocument()
364
+ expect(link?.textContent?.trim()).toBe('Cookie settings')
365
+ })
366
+
367
+ test('footer link trigger is accessible', async () => {
368
+ const container = await createConsent(
369
+ 'triggerType="footerlink" triggerText="Cookie settings"',
370
+ )
371
+
372
+ const consent = container.querySelector('pkt-consent') as PktConsent
373
+ await consent.updateComplete
374
+
375
+ const results = await axe(container)
376
+ expect(results).toHaveNoViolations()
377
+
378
+ const link = consent.querySelector('a.pkt-footer__link')
379
+ expect(link).toBeInTheDocument()
380
+ expect(link?.textContent?.trim()).toContain('Cookie settings')
381
+ })
382
+
383
+ test('icon trigger is accessible', async () => {
384
+ const container = await createConsent('triggerType="icon" triggerText="Cookie settings"')
385
+
386
+ const consent = container.querySelector('pkt-consent') as PktConsent
387
+ await consent.updateComplete
388
+
389
+ const results = await axe(container)
390
+ expect(results).toHaveNoViolations()
391
+
392
+ const button = consent.querySelector('pkt-button')
393
+ expect(button).toBeInTheDocument()
394
+ expect(button?.textContent?.trim()).toContain('Cookie settings')
395
+ })
396
+ })
397
+
398
+ describe('Integration and lifecycle', () => {
399
+ test('sets global variables on firstUpdated', async () => {
400
+ const googleId = 'GA-123456789'
401
+ const hotjarId = 'HJ-987654321'
402
+ const container = await createConsent(
403
+ `devMode googleAnalyticsId="${googleId}" hotjarId="${hotjarId}" cookieDomain=".test.com"`,
404
+ )
405
+
406
+ const consent = container.querySelector('pkt-consent') as PktConsent
407
+ await consent.updateComplete
408
+
409
+ expect(window.cookieBanner_googleAnalyticsId).toBe(googleId)
410
+ expect(window.cookieBanner_hotjarId).toBe(hotjarId)
411
+ expect(window.cookieBanner_devMode).toBe(true)
412
+ expect(window.cookieBanner_cookieDomain).toBe('.test.com')
413
+ })
414
+
415
+ test('cleans up event listeners on disconnect', async () => {
416
+ const container = await createConsent()
417
+ const consent = container.querySelector('pkt-consent') as PktConsent
418
+ await consent.updateComplete
419
+
420
+ // Mock the event system
421
+ window.__cookieEvents = {
422
+ on: jest.fn(),
423
+ off: jest.fn(),
424
+ }
425
+
426
+ // Set up a handler
427
+ const mockHandler = jest.fn()
428
+ consent['_cookieEventHandler'] = mockHandler
429
+
430
+ // Disconnect the component
431
+ consent.disconnectedCallback()
432
+
433
+ expect(window.__cookieEvents.off).toHaveBeenCalledWith('CookieManager.setCookie', mockHandler)
434
+ })
435
+ })
436
+ })
@@ -0,0 +1,193 @@
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 './datepicker'
8
+ import '../calendar/calendar'
9
+ import { PktDatepicker } from './datepicker'
10
+
11
+ const waitForCustomElements = async () => {
12
+ await customElements.whenDefined('pkt-datepicker')
13
+ await customElements.whenDefined('pkt-calendar')
14
+ }
15
+
16
+ // Helper function to create datepicker markup
17
+ const createDatepicker = async (datepickerProps = '') => {
18
+ const container = document.createElement('div')
19
+ container.innerHTML = `
20
+ <pkt-datepicker ${datepickerProps}></pkt-datepicker>
21
+ `
22
+ document.body.appendChild(container)
23
+ await waitForCustomElements()
24
+ return container
25
+ }
26
+
27
+ // Cleanup after each test
28
+ afterEach(() => {
29
+ document.body.innerHTML = ''
30
+ })
31
+
32
+ describe('PktDatepicker', () => {
33
+ describe('Event handling', () => {
34
+ test('dispatches change event when value changes', async () => {
35
+ const container = await createDatepicker()
36
+
37
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
38
+ await datepicker.updateComplete
39
+
40
+ let changeEventFired = false
41
+ datepicker.addEventListener('change', () => {
42
+ changeEventFired = true
43
+ })
44
+
45
+ const input = datepicker.querySelector('input') as HTMLInputElement
46
+ fireEvent.change(input, { target: { value: '2024-06-15' } })
47
+ fireEvent.blur(input)
48
+ await datepicker.updateComplete
49
+
50
+ expect(changeEventFired).toBe(true)
51
+ })
52
+
53
+ test('dispatches input event during typing', async () => {
54
+ const container = await createDatepicker()
55
+
56
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
57
+ await datepicker.updateComplete
58
+
59
+ let inputEventFired = false
60
+ datepicker.addEventListener('input', () => {
61
+ inputEventFired = true
62
+ })
63
+
64
+ const input = datepicker.querySelector('input') as HTMLInputElement
65
+ fireEvent.input(input, { target: { value: '2024-06-15' } })
66
+ await datepicker.updateComplete
67
+
68
+ expect(inputEventFired).toBe(true)
69
+ })
70
+
71
+ test('dispatches focus and blur events', async () => {
72
+ const container = await createDatepicker()
73
+
74
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
75
+ await datepicker.updateComplete
76
+
77
+ let focusEventFired = false
78
+ let blurEventFired = false
79
+
80
+ datepicker.addEventListener('focus', () => {
81
+ focusEventFired = true
82
+ })
83
+ datepicker.addEventListener('blur', () => {
84
+ blurEventFired = true
85
+ })
86
+
87
+ const input = datepicker.querySelector('input') as HTMLInputElement
88
+ fireEvent.focus(input)
89
+ await datepicker.updateComplete
90
+ fireEvent.blur(input)
91
+ await datepicker.updateComplete
92
+
93
+ expect(focusEventFired).toBe(true)
94
+ expect(blurEventFired).toBe(true)
95
+ })
96
+ })
97
+
98
+ describe('Accessibility', () => {
99
+ test('has no accessibility violations', async () => {
100
+ const container = await createDatepicker(
101
+ 'label="Test Datepicker" helptext="Select your date"',
102
+ )
103
+
104
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
105
+ await datepicker.updateComplete
106
+
107
+ const results = await axe(datepicker)
108
+ expect(results).toHaveNoViolations()
109
+ })
110
+
111
+ test('associates label with input correctly', async () => {
112
+ const container = await createDatepicker('id="test-datepicker" label="Test Label"')
113
+
114
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
115
+ await datepicker.updateComplete
116
+
117
+ const label = datepicker.querySelector('label')
118
+ const input = datepicker.querySelector('input')
119
+
120
+ expect(label).toHaveAttribute('for')
121
+ expect(input).toHaveAttribute('id')
122
+ expect(label?.getAttribute('for')).toBe(input?.getAttribute('id'))
123
+ })
124
+
125
+ test('has proper ARIA attributes for calendar button', async () => {
126
+ const container = await createDatepicker('label="Test Datepicker"')
127
+
128
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
129
+ await datepicker.updateComplete
130
+
131
+ const calendarButton = datepicker.querySelector('button[type="button"]')
132
+ expect(calendarButton).toBeInTheDocument()
133
+
134
+ // Check initial state
135
+ expect(datepicker.calendarOpen).toBe(false)
136
+
137
+ // Open calendar
138
+ fireEvent.click(calendarButton!)
139
+ await datepicker.updateComplete
140
+
141
+ expect(datepicker.calendarOpen).toBe(true)
142
+ })
143
+
144
+ test('provides proper screen reader announcements', async () => {
145
+ const container = await createDatepicker('label="Test Datepicker"')
146
+
147
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
148
+ await datepicker.updateComplete
149
+
150
+ const input = datepicker.querySelector('input')
151
+ expect(input).toHaveAttribute('aria-describedby')
152
+ })
153
+
154
+ test('handles focus management correctly', async () => {
155
+ const container = await createDatepicker()
156
+
157
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
158
+ await datepicker.updateComplete
159
+
160
+ const calendarButton = datepicker.querySelector('button[type="button"]') as HTMLElement
161
+
162
+ // Focus should be manageable
163
+ if (calendarButton) {
164
+ calendarButton.focus()
165
+ expect(document.activeElement).toBe(calendarButton)
166
+ }
167
+ })
168
+
169
+ test('supports keyboard-only interaction', async () => {
170
+ const container = await createDatepicker()
171
+
172
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
173
+ await datepicker.updateComplete
174
+
175
+ const calendarButton = datepicker.querySelector('button[type="button"]') as HTMLElement
176
+
177
+ // Should open with keyboard
178
+ if (calendarButton) {
179
+ calendarButton.focus()
180
+ fireEvent.keyDown(calendarButton, { key: 'Enter' })
181
+ await datepicker.updateComplete
182
+
183
+ expect(datepicker.calendarOpen).toBe(true)
184
+ }
185
+
186
+ // Should close with keyboard
187
+ fireEvent.keyDown(datepicker, { key: 'Escape' })
188
+ await datepicker.updateComplete
189
+
190
+ expect(datepicker.calendarOpen).toBe(false)
191
+ })
192
+ })
193
+ })