@oslokommune/punkt-elements 13.7.0 → 13.9.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.
@@ -0,0 +1,495 @@
1
+ import '@testing-library/jest-dom'
2
+ import { axe, toHaveNoViolations } from 'jest-axe'
3
+ import { vi } from 'vitest'
4
+
5
+ expect.extend(toHaveNoViolations)
6
+
7
+ // Import the components
8
+ import './tabs'
9
+ import './tabitem'
10
+
11
+ // Import component classes
12
+ import { PktTabs } from './tabs'
13
+
14
+ const waitForCustomElements = async () => {
15
+ await Promise.all([
16
+ customElements.whenDefined('pkt-tabs'),
17
+ customElements.whenDefined('pkt-tab-item'),
18
+ ])
19
+ }
20
+
21
+ // Helper function to create tabs markup
22
+ const createTabs = async (tabsProps = '', tabItemsMarkup = '') => {
23
+ const container = document.createElement('div')
24
+ const defaultMarkup = `
25
+ <pkt-tab-item index="0" active>Tab 1</pkt-tab-item>
26
+ <pkt-tab-item index="1">Tab 2</pkt-tab-item>
27
+ <pkt-tab-item index="2">Tab 3</pkt-tab-item>
28
+ `
29
+ container.innerHTML = `
30
+ <pkt-tabs ${tabsProps}>
31
+ ${tabItemsMarkup || defaultMarkup}
32
+ </pkt-tabs>
33
+ `
34
+ document.body.appendChild(container)
35
+ await waitForCustomElements()
36
+
37
+ const tabs = container.querySelector('pkt-tabs') as PktTabs
38
+ await tabs.updateComplete
39
+
40
+ // Wait for tab items to be updated
41
+ const tabItems = container.querySelectorAll('pkt-tab-item')
42
+ await Promise.all(
43
+ Array.from(tabItems).map(
44
+ (item) => (item as HTMLElement & { updateComplete: Promise<boolean> }).updateComplete,
45
+ ),
46
+ )
47
+
48
+ return container
49
+ }
50
+
51
+ // Cleanup after each test
52
+ afterEach(() => {
53
+ document.body.innerHTML = ''
54
+ })
55
+
56
+ describe('PktTabs', () => {
57
+ describe('Rendering and basic functionality', () => {
58
+ test('renders without errors', async () => {
59
+ const container = await createTabs()
60
+ const tabs = container.querySelector('pkt-tabs') as PktTabs
61
+ expect(tabs).toBeInTheDocument()
62
+ })
63
+
64
+ test('renders tab items from children', async () => {
65
+ const container = await createTabs()
66
+ const tabItems = container.querySelectorAll('pkt-tab-item')
67
+ expect(tabItems).toHaveLength(3)
68
+ expect(tabItems[0].textContent?.trim()).toBe('Tab 1')
69
+ expect(tabItems[1].textContent?.trim()).toBe('Tab 2')
70
+ expect(tabItems[2].textContent?.trim()).toBe('Tab 3')
71
+ })
72
+
73
+ test('applies active class to the active tab', async () => {
74
+ const container = await createTabs()
75
+ const tabItems = container.querySelectorAll('pkt-tab-item')
76
+
77
+ const firstTabButton = tabItems[0].querySelector('button, a')
78
+ const secondTabButton = tabItems[1].querySelector('button, a')
79
+
80
+ expect(firstTabButton).toHaveClass('active')
81
+ expect(secondTabButton).not.toHaveClass('active')
82
+ })
83
+
84
+ test('renders as button when no href is provided', async () => {
85
+ const container = await createTabs()
86
+ const firstTabItem = container.querySelector('pkt-tab-item')
87
+ const button = firstTabItem?.querySelector('button')
88
+ const link = firstTabItem?.querySelector('a')
89
+
90
+ expect(button).toBeInTheDocument()
91
+ expect(link).not.toBeInTheDocument()
92
+ expect(button?.tagName).toBe('BUTTON')
93
+ })
94
+
95
+ test('renders as link when href is provided', async () => {
96
+ const container = await createTabs(
97
+ '',
98
+ '<pkt-tab-item index="0" href="/first">First Tab</pkt-tab-item>',
99
+ )
100
+ const firstTabItem = container.querySelector('pkt-tab-item')
101
+ const link = firstTabItem?.querySelector('a')
102
+ const button = firstTabItem?.querySelector('button')
103
+
104
+ expect(link).toBeInTheDocument()
105
+ expect(button).not.toBeInTheDocument()
106
+ expect(link?.getAttribute('href')).toBe('/first')
107
+ })
108
+
109
+ test('applies default arrowNav property correctly', async () => {
110
+ const container = await createTabs()
111
+ const tabs = container.querySelector('pkt-tabs') as PktTabs
112
+ await tabs.updateComplete
113
+
114
+ expect(tabs.arrowNav).toBe(true)
115
+ expect(tabs.disableArrowNav).toBe(false)
116
+ })
117
+
118
+ test('applies custom arrowNav property correctly', async () => {
119
+ const container = await createTabs('disable-arrow-nav')
120
+ const tabs = container.querySelector('pkt-tabs') as PktTabs
121
+ await tabs.updateComplete
122
+
123
+ // When disable-arrow-nav is present, effective arrowNav should be false
124
+ expect(tabs.disableArrowNav).toBe(true)
125
+ expect(tabs.arrowNav).toBe(true) // arrowNav prop itself is still true by default
126
+ })
127
+
128
+ test('applies disableArrowNav property correctly', async () => {
129
+ const container = await createTabs('disable-arrow-nav="true"')
130
+ const tabs = container.querySelector('pkt-tabs') as PktTabs
131
+ await tabs.updateComplete
132
+
133
+ expect(tabs.disableArrowNav).toBe(true)
134
+ })
135
+ })
136
+
137
+ describe('Tab item props', () => {
138
+ test('renders icon when icon prop is provided', async () => {
139
+ const container = await createTabs(
140
+ '',
141
+ '<pkt-tab-item index="0" icon="user">Tab with icon</pkt-tab-item>',
142
+ )
143
+ const icon = container.querySelector('pkt-icon')
144
+ expect(icon).toBeInTheDocument()
145
+ expect(icon?.getAttribute('name')).toBe('user')
146
+ expect(icon).toHaveClass('pkt-icon--small')
147
+ })
148
+
149
+ test('renders tag when tag prop is provided', async () => {
150
+ const container = await createTabs(
151
+ '',
152
+ '<pkt-tab-item index="0" tag="New" tag-skin="blue">Tab with tag</pkt-tab-item>',
153
+ )
154
+ const tag = container.querySelector('pkt-tag')
155
+ expect(tag).toBeInTheDocument()
156
+ expect(tag?.textContent?.trim()).toBe('New')
157
+ expect(tag?.getAttribute('skin')).toBe('blue')
158
+ })
159
+
160
+ test('applies controls attribute when provided', async () => {
161
+ const container = await createTabs(
162
+ '',
163
+ '<pkt-tab-item index="0" controls="panel-1">Tab 1</pkt-tab-item>',
164
+ )
165
+ const button = container.querySelector('button')
166
+ expect(button?.getAttribute('aria-controls')).toBe('panel-1')
167
+ })
168
+ })
169
+
170
+ describe('Click interactions', () => {
171
+ test('dispatches tab-selected event when tab is clicked', async () => {
172
+ const container = await createTabs()
173
+ const tabs = container.querySelector('pkt-tabs') as PktTabs
174
+
175
+ const eventListener = vi.fn()
176
+ tabs.addEventListener('tab-selected', eventListener)
177
+
178
+ const secondTabItem = container.querySelectorAll('pkt-tab-item')[1]
179
+ const button = secondTabItem.querySelector('button')
180
+ button?.click()
181
+
182
+ expect(eventListener).toHaveBeenCalled()
183
+ expect(eventListener.mock.calls[0][0].detail.index).toBe(1)
184
+ })
185
+
186
+ test('dispatches tab-selected event with correct index for each tab', async () => {
187
+ const container = await createTabs()
188
+ const tabs = container.querySelector('pkt-tabs') as PktTabs
189
+
190
+ const eventListener = vi.fn()
191
+ tabs.addEventListener('tab-selected', eventListener)
192
+
193
+ const tabItems = container.querySelectorAll('pkt-tab-item')
194
+
195
+ // Click first tab
196
+ tabItems[0].querySelector('button')?.click()
197
+ expect(eventListener).toHaveBeenCalledTimes(1)
198
+ expect(eventListener.mock.calls[0][0].detail.index).toBe(0)
199
+
200
+ // Click third tab
201
+ tabItems[2].querySelector('button')?.click()
202
+ expect(eventListener).toHaveBeenCalledTimes(2)
203
+ expect(eventListener.mock.calls[1][0].detail.index).toBe(2)
204
+ })
205
+ })
206
+
207
+ describe('Keyboard navigation', () => {
208
+ test('handles ArrowRight keyboard navigation', async () => {
209
+ const container = await createTabs()
210
+ const tabItems = container.querySelectorAll('pkt-tab-item')
211
+
212
+ const firstButton = tabItems[0].querySelector('button') as HTMLButtonElement
213
+ const secondButton = tabItems[1].querySelector('button') as HTMLButtonElement
214
+
215
+ firstButton.focus()
216
+ expect(document.activeElement).toBe(firstButton)
217
+
218
+ // Simulate ArrowRight key
219
+ const keyEvent = new KeyboardEvent('keyup', { code: 'ArrowRight', bubbles: true })
220
+ firstButton.dispatchEvent(keyEvent)
221
+
222
+ // Wait for focus to change
223
+ await new Promise((resolve) => setTimeout(resolve, 50))
224
+
225
+ expect(document.activeElement).toBe(secondButton)
226
+ })
227
+
228
+ test('handles ArrowLeft keyboard navigation', async () => {
229
+ const container = await createTabs()
230
+ const tabItems = container.querySelectorAll('pkt-tab-item')
231
+
232
+ const firstButton = tabItems[0].querySelector('button') as HTMLButtonElement
233
+ const secondButton = tabItems[1].querySelector('button') as HTMLButtonElement
234
+
235
+ secondButton.focus()
236
+ expect(document.activeElement).toBe(secondButton)
237
+
238
+ // Simulate ArrowLeft key
239
+ const keyEvent = new KeyboardEvent('keyup', { code: 'ArrowLeft', bubbles: true })
240
+ secondButton.dispatchEvent(keyEvent)
241
+
242
+ // Wait for focus to change
243
+ await new Promise((resolve) => setTimeout(resolve, 50))
244
+
245
+ expect(document.activeElement).toBe(firstButton)
246
+ })
247
+
248
+ test('stays on last tab when navigating past it with ArrowRight', async () => {
249
+ const container = await createTabs()
250
+ const tabItems = container.querySelectorAll('pkt-tab-item')
251
+
252
+ const thirdButton = tabItems[2].querySelector('button') as HTMLButtonElement
253
+
254
+ thirdButton.focus()
255
+ expect(document.activeElement).toBe(thirdButton)
256
+
257
+ // Simulate ArrowRight key
258
+ const keyEvent = new KeyboardEvent('keyup', { code: 'ArrowRight', bubbles: true })
259
+ thirdButton.dispatchEvent(keyEvent)
260
+
261
+ // Wait
262
+ await new Promise((resolve) => setTimeout(resolve, 50))
263
+
264
+ // Should stay on third tab
265
+ expect(document.activeElement).toBe(thirdButton)
266
+ })
267
+
268
+ test('stays on first tab when navigating before it with ArrowLeft', async () => {
269
+ const container = await createTabs()
270
+ const tabItems = container.querySelectorAll('pkt-tab-item')
271
+
272
+ const firstButton = tabItems[0].querySelector('button') as HTMLButtonElement
273
+
274
+ firstButton.focus()
275
+ expect(document.activeElement).toBe(firstButton)
276
+
277
+ // Simulate ArrowLeft key
278
+ const keyEvent = new KeyboardEvent('keyup', { code: 'ArrowLeft', bubbles: true })
279
+ firstButton.dispatchEvent(keyEvent)
280
+
281
+ // Wait
282
+ await new Promise((resolve) => setTimeout(resolve, 50))
283
+
284
+ // Should stay on first tab
285
+ expect(document.activeElement).toBe(firstButton)
286
+ })
287
+
288
+ test('dispatches tab-selected event when Space key is pressed', async () => {
289
+ const container = await createTabs()
290
+ const tabs = container.querySelector('pkt-tabs') as PktTabs
291
+
292
+ const eventListener = vi.fn()
293
+ tabs.addEventListener('tab-selected', eventListener)
294
+
295
+ const secondTabItem = container.querySelectorAll('pkt-tab-item')[1]
296
+ const button = secondTabItem.querySelector('button') as HTMLButtonElement
297
+
298
+ // Simulate Space key
299
+ const keyEvent = new KeyboardEvent('keyup', { code: 'Space', bubbles: true })
300
+ button.dispatchEvent(keyEvent)
301
+
302
+ // Wait
303
+ await new Promise((resolve) => setTimeout(resolve, 50))
304
+
305
+ expect(eventListener).toHaveBeenCalled()
306
+ expect(eventListener.mock.calls[0][0].detail.index).toBe(1)
307
+ })
308
+
309
+ test('dispatches tab-selected event when ArrowDown key is pressed', async () => {
310
+ const container = await createTabs()
311
+ const tabs = container.querySelector('pkt-tabs') as PktTabs
312
+
313
+ const eventListener = vi.fn()
314
+ tabs.addEventListener('tab-selected', eventListener)
315
+
316
+ const secondTabItem = container.querySelectorAll('pkt-tab-item')[1]
317
+ const button = secondTabItem.querySelector('button') as HTMLButtonElement
318
+
319
+ // Simulate ArrowDown key
320
+ const keyEvent = new KeyboardEvent('keyup', { code: 'ArrowDown', bubbles: true })
321
+ button.dispatchEvent(keyEvent)
322
+
323
+ // Wait
324
+ await new Promise((resolve) => setTimeout(resolve, 50))
325
+
326
+ expect(eventListener).toHaveBeenCalled()
327
+ expect(eventListener.mock.calls[0][0].detail.index).toBe(1)
328
+ })
329
+
330
+ test('disables keyboard navigation when arrowNav is false', async () => {
331
+ const container = await createTabs('disable-arrow-nav')
332
+ const tabs = container.querySelector('pkt-tabs') as PktTabs
333
+ await tabs.updateComplete
334
+
335
+ const tabItems = container.querySelectorAll('pkt-tab-item')
336
+
337
+ const firstButton = tabItems[0].querySelector('button') as HTMLButtonElement
338
+
339
+ firstButton.focus()
340
+ expect(document.activeElement).toBe(firstButton)
341
+
342
+ // Simulate ArrowRight key
343
+ const keyEvent = new KeyboardEvent('keyup', { code: 'ArrowRight', bubbles: true })
344
+ firstButton.dispatchEvent(keyEvent)
345
+
346
+ // Wait
347
+ await new Promise((resolve) => setTimeout(resolve, 50))
348
+
349
+ // Should NOT move to second tab
350
+ expect(document.activeElement).toBe(firstButton)
351
+ })
352
+
353
+ test('disables keyboard navigation when disableArrowNav is true', async () => {
354
+ const container = await createTabs('disable-arrow-nav="true"')
355
+ const tabItems = container.querySelectorAll('pkt-tab-item')
356
+
357
+ const firstButton = tabItems[0].querySelector('button') as HTMLButtonElement
358
+
359
+ firstButton.focus()
360
+ expect(document.activeElement).toBe(firstButton)
361
+
362
+ // Simulate ArrowRight key
363
+ const keyEvent = new KeyboardEvent('keyup', { code: 'ArrowRight', bubbles: true })
364
+ firstButton.dispatchEvent(keyEvent)
365
+
366
+ // Wait
367
+ await new Promise((resolve) => setTimeout(resolve, 50))
368
+
369
+ // Should NOT move to second tab
370
+ expect(document.activeElement).toBe(firstButton)
371
+ })
372
+
373
+ test('disableArrowNav overrides arrowNav when both are set', async () => {
374
+ const container = await createTabs('arrow-nav="true" disable-arrow-nav="true"')
375
+ const tabItems = container.querySelectorAll('pkt-tab-item')
376
+
377
+ const firstButton = tabItems[0].querySelector('button') as HTMLButtonElement
378
+
379
+ firstButton.focus()
380
+
381
+ // Simulate ArrowRight key
382
+ const keyEvent = new KeyboardEvent('keyup', { code: 'ArrowRight', bubbles: true })
383
+ firstButton.dispatchEvent(keyEvent)
384
+
385
+ // Wait
386
+ await new Promise((resolve) => setTimeout(resolve, 50))
387
+
388
+ // Should NOT move to second tab (disableArrowNav overrides arrowNav)
389
+ expect(document.activeElement).toBe(firstButton)
390
+ })
391
+ })
392
+
393
+ describe('Accessibility', () => {
394
+ test('should not have any accessibility violations', async () => {
395
+ const container = await createTabs()
396
+ const results = await axe(container)
397
+ expect(results).toHaveNoViolations()
398
+ })
399
+
400
+ test('sets correct ARIA attributes on buttons when arrowNav is true', async () => {
401
+ const container = await createTabs()
402
+ const tabItems = container.querySelectorAll('pkt-tab-item')
403
+
404
+ const firstButton = tabItems[0].querySelector('button')
405
+ const secondButton = tabItems[1].querySelector('button')
406
+
407
+ expect(firstButton).toHaveAttribute('role', 'tab')
408
+ expect(firstButton).toHaveAttribute('aria-selected', 'true')
409
+ expect(secondButton).toHaveAttribute('role', 'tab')
410
+ expect(secondButton).toHaveAttribute('aria-selected', 'false')
411
+ })
412
+
413
+ test('does not set tab role or aria-selected on links when arrowNav is false', async () => {
414
+ const container = await createTabs(
415
+ 'disable-arrow-nav',
416
+ `
417
+ <pkt-tab-item index="0" href="/" active>Home</pkt-tab-item>
418
+ <pkt-tab-item index="1" href="/about">About</pkt-tab-item>
419
+ `,
420
+ )
421
+ await new Promise((resolve) => setTimeout(resolve, 50))
422
+ const tabItems = container.querySelectorAll('pkt-tab-item')
423
+
424
+ const homeLink = tabItems[0].querySelector('a')
425
+ const aboutLink = tabItems[1].querySelector('a')
426
+
427
+ expect(homeLink).not.toHaveAttribute('role')
428
+ expect(homeLink).not.toHaveAttribute('aria-selected')
429
+ expect(aboutLink).not.toHaveAttribute('role')
430
+ expect(aboutLink).not.toHaveAttribute('aria-selected')
431
+ })
432
+
433
+ test('works with href links when arrowNav is false (no WCAG violations)', async () => {
434
+ const container = await createTabs(
435
+ 'disable-arrow-nav',
436
+ `
437
+ <pkt-tab-item index="0" href="/" active>Home</pkt-tab-item>
438
+ <pkt-tab-item index="1" href="/about">About</pkt-tab-item>
439
+ `,
440
+ )
441
+ await new Promise((resolve) => setTimeout(resolve, 50))
442
+ const results = await axe(container)
443
+ expect(results).toHaveNoViolations()
444
+ })
445
+
446
+ test('sets role="tablist" when arrowNav is true', async () => {
447
+ const container = await createTabs()
448
+ const tabs = container.querySelector('pkt-tabs') as PktTabs
449
+ const tabList = tabs.querySelector('.pkt-tabs__list')
450
+
451
+ expect(tabList).toHaveAttribute('role', 'tablist')
452
+ })
453
+
454
+ test('sets role="navigation" when arrowNav is false', async () => {
455
+ const container = await createTabs('disable-arrow-nav')
456
+ const tabs = container.querySelector('pkt-tabs') as PktTabs
457
+ await tabs.updateComplete
458
+ const tabList = tabs.querySelector('.pkt-tabs__list')
459
+
460
+ expect(tabList).toHaveAttribute('role', 'navigation')
461
+ })
462
+ })
463
+
464
+ describe('Multiple tab items', () => {
465
+ test('works with many tab items', async () => {
466
+ const container = await createTabs(
467
+ '',
468
+ `
469
+ <pkt-tab-item index="0" active>Tab 1</pkt-tab-item>
470
+ <pkt-tab-item index="1">Tab 2</pkt-tab-item>
471
+ <pkt-tab-item index="2">Tab 3</pkt-tab-item>
472
+ <pkt-tab-item index="3">Tab 4</pkt-tab-item>
473
+ <pkt-tab-item index="4">Tab 5</pkt-tab-item>
474
+ `,
475
+ )
476
+
477
+ const tabItems = container.querySelectorAll('pkt-tab-item')
478
+ expect(tabItems).toHaveLength(5)
479
+
480
+ // Test navigation from first to last
481
+ const firstButton = tabItems[0].querySelector('button') as HTMLButtonElement
482
+ firstButton.focus()
483
+
484
+ // Navigate through all tabs
485
+ for (let i = 0; i < 4; i++) {
486
+ const keyEvent = new KeyboardEvent('keyup', { code: 'ArrowRight', bubbles: true })
487
+ ;(document.activeElement as HTMLElement).dispatchEvent(keyEvent)
488
+ await new Promise((resolve) => setTimeout(resolve, 50))
489
+ }
490
+
491
+ const lastButton = tabItems[4].querySelector('button') as HTMLButtonElement
492
+ expect(document.activeElement).toBe(lastButton)
493
+ })
494
+ })
495
+ })
@@ -0,0 +1,103 @@
1
+ import { PktElement } from '@/base-elements/element'
2
+ import { PktSlotController } from '@/controllers/pkt-slot-controller'
3
+ import { html, PropertyValues } from 'lit'
4
+ import { customElement, property, state } from 'lit/decorators.js'
5
+ import { provide } from '@lit/context'
6
+ import { createRef, Ref, ref } from 'lit/directives/ref.js'
7
+ import { tabsContext, type TabsContext } from './tabs-context'
8
+
9
+ export interface IPktTabs {
10
+ arrowNav?: boolean
11
+ disableArrowNav?: boolean
12
+ }
13
+
14
+ @customElement('pkt-tabs')
15
+ export class PktTabs extends PktElement<IPktTabs> implements IPktTabs {
16
+ @property({ type: Boolean, reflect: true, attribute: 'arrow-nav' }) arrowNav: boolean = true
17
+ @property({ type: Boolean, reflect: true, attribute: 'disable-arrow-nav' })
18
+ disableArrowNav: boolean = false
19
+
20
+ @state() private tabRefs: Array<HTMLAnchorElement | HTMLButtonElement | null> = []
21
+ @state() private tabCount: number = 0
22
+
23
+ private get useArrowNav(): boolean {
24
+ return this.arrowNav && !this.disableArrowNav
25
+ }
26
+
27
+ defaultSlot: Ref<HTMLElement> = createRef()
28
+ slotController!: PktSlotController
29
+
30
+ constructor() {
31
+ super()
32
+ this.slotController = new PktSlotController(this, this.defaultSlot)
33
+ }
34
+
35
+ // Provide context to child tab items
36
+ @provide({ context: tabsContext })
37
+ @state()
38
+ private context: TabsContext = {
39
+ useArrowNav: this.useArrowNav,
40
+ registerTab: this.registerTab.bind(this),
41
+ handleClick: this.handleClick.bind(this),
42
+ handleKeyUp: this.handleKeyUp.bind(this),
43
+ }
44
+
45
+ // Update context when properties change
46
+ updated(changedProperties: PropertyValues) {
47
+ if (changedProperties.has('arrowNav') || changedProperties.has('disableArrowNav')) {
48
+ this.context = {
49
+ ...this.context,
50
+ useArrowNav: this.useArrowNav,
51
+ }
52
+ }
53
+ }
54
+
55
+ private registerTab(element: HTMLElement, index: number) {
56
+ this.tabRefs[index] = element as HTMLAnchorElement | HTMLButtonElement
57
+ this.tabCount = Math.max(this.tabCount, index + 1)
58
+ }
59
+
60
+ private handleClick(index: number) {
61
+ this.dispatchEvent(
62
+ new CustomEvent('tab-selected', {
63
+ detail: { index },
64
+ bubbles: true,
65
+ composed: true,
66
+ }),
67
+ )
68
+ }
69
+
70
+ private handleKeyUp(keyEvent: KeyboardEvent, index: number) {
71
+ if (!this.useArrowNav) return
72
+
73
+ if (keyEvent.code === 'ArrowLeft' && index !== 0) {
74
+ this.tabRefs[index - 1]?.focus()
75
+ }
76
+
77
+ if (keyEvent.code === 'ArrowRight' && index < this.tabCount - 1) {
78
+ this.tabRefs[index + 1]?.focus()
79
+ }
80
+
81
+ if (keyEvent.code === 'ArrowDown' || keyEvent.code === 'Space') {
82
+ this.dispatchEvent(
83
+ new CustomEvent('tab-selected', {
84
+ detail: { index },
85
+ bubbles: true,
86
+ composed: true,
87
+ }),
88
+ )
89
+ }
90
+ }
91
+
92
+ render() {
93
+ const role = this.useArrowNav ? 'tablist' : 'navigation'
94
+
95
+ return html`
96
+ <div class="pkt-tabs">
97
+ <div class="pkt-tabs__list" role=${role} ${ref(this.defaultSlot)}></div>
98
+ </div>
99
+ `
100
+ }
101
+ }
102
+
103
+ export default PktTabs