@oslokommune/punkt-react 16.8.1 → 16.8.3
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/index.d.ts +2 -0
- package/dist/punkt-react.es.js +163 -88
- package/dist/punkt-react.umd.js +20 -17
- package/package.json +4 -4
- package/src/components/backlink/BackLink.tsx +4 -2
- package/src/components/tabs/TabItem.tsx +35 -7
- package/src/components/tabs/Tabs.test.tsx +107 -9
- package/src/components/tabs/Tabs.tsx +55 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oslokommune/punkt-react",
|
|
3
|
-
"version": "16.8.
|
|
3
|
+
"version": "16.8.3",
|
|
4
4
|
"description": "React komponentbibliotek til Punkt, et designsystem laget av Oslo Origo",
|
|
5
5
|
"homepage": "https://punkt.oslo.kommune.no",
|
|
6
6
|
"author": "Team Designsystem, Oslo Origo",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@lit-labs/ssr-dom-shim": "^1.2.1",
|
|
41
41
|
"@lit/react": "^1.0.7",
|
|
42
|
-
"@oslokommune/punkt-elements": "^16.
|
|
42
|
+
"@oslokommune/punkt-elements": "^16.8.2",
|
|
43
43
|
"classnames": "^2.5.1",
|
|
44
44
|
"prettier": "^3.3.3",
|
|
45
45
|
"react-hook-form": "^7.53.0"
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@eslint/eslintrc": "^3.3.3",
|
|
51
51
|
"@eslint/js": "^9.37.0",
|
|
52
52
|
"@oslokommune/punkt-assets": "^16.0.0",
|
|
53
|
-
"@oslokommune/punkt-css": "^16.
|
|
53
|
+
"@oslokommune/punkt-css": "^16.8.2",
|
|
54
54
|
"@testing-library/jest-dom": "^6.5.0",
|
|
55
55
|
"@testing-library/react": "^16.0.1",
|
|
56
56
|
"@testing-library/user-event": "^14.5.2",
|
|
@@ -109,5 +109,5 @@
|
|
|
109
109
|
"url": "https://github.com/oslokommune/punkt/issues"
|
|
110
110
|
},
|
|
111
111
|
"license": "MIT",
|
|
112
|
-
"gitHead": "
|
|
112
|
+
"gitHead": "58054bd20de8af3227968c9fcd4dbd66442c9fda"
|
|
113
113
|
}
|
|
@@ -18,7 +18,7 @@ export interface IPktBackLink extends Omit<AnchorHTMLAttributes<HTMLAnchorElemen
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export const PktBackLink = forwardRef<HTMLElement, IPktBackLink>(
|
|
21
|
-
({ href = '/', text = 'Forsiden', ariaLabel, renderLink, ...props }, ref) => {
|
|
21
|
+
({ href = '/', text = 'Forsiden', ariaLabel, renderLink, className, ...props }, ref) => {
|
|
22
22
|
const linkChildren = (
|
|
23
23
|
<>
|
|
24
24
|
<PktIcon className="pkt-back-link__icon pkt-icon pkt-link__icon" name="chevron-thin-left" aria-hidden="true" />
|
|
@@ -34,8 +34,10 @@ export const PktBackLink = forwardRef<HTMLElement, IPktBackLink>(
|
|
|
34
34
|
</a>
|
|
35
35
|
))
|
|
36
36
|
|
|
37
|
+
const classes = ['pkt-back-link', className].filter(Boolean).join(' ')
|
|
38
|
+
|
|
37
39
|
return (
|
|
38
|
-
<nav className=
|
|
40
|
+
<nav className={classes} aria-label={ariaLabel || 'Gå tilbake til forrige side'} ref={ref}>
|
|
39
41
|
{linkRenderer({
|
|
40
42
|
href,
|
|
41
43
|
className: 'pkt-link pkt-link--icon-left',
|
|
@@ -11,6 +11,7 @@ export type TSkin = IPktTag['skin']
|
|
|
11
11
|
export interface IPktTabItem {
|
|
12
12
|
children: ReactNode
|
|
13
13
|
active?: boolean
|
|
14
|
+
disabled?: boolean
|
|
14
15
|
href?: string
|
|
15
16
|
onClick?: (event: MouseEvent) => void
|
|
16
17
|
icon?: string
|
|
@@ -21,23 +22,39 @@ export interface IPktTabItem {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export const PktTabItem = forwardRef<HTMLAnchorElement | HTMLButtonElement, IPktTabItem>(
|
|
24
|
-
({ children, active, href, onClick, icon, controls, tag, tagSkin, index = 0 }, ref) => {
|
|
25
|
+
({ children, active, disabled = false, href, onClick, icon, controls, tag, tagSkin, index = 0 }, ref) => {
|
|
25
26
|
const { arrowNav, registerTabRef, handleKeyPress, selectTab } = useTabsContext()
|
|
27
|
+
const isActive = !!active && !disabled
|
|
26
28
|
|
|
27
29
|
const handleClick = (event: MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => {
|
|
30
|
+
if (disabled) {
|
|
31
|
+
event.preventDefault()
|
|
32
|
+
event.stopPropagation()
|
|
33
|
+
return
|
|
34
|
+
}
|
|
28
35
|
selectTab(index)
|
|
29
36
|
onClick?.(event)
|
|
30
37
|
}
|
|
31
38
|
|
|
39
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLAnchorElement | HTMLButtonElement>) => {
|
|
40
|
+
if (disabled && (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar')) {
|
|
41
|
+
event.preventDefault()
|
|
42
|
+
event.stopPropagation()
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
handleKeyPress(index, event)
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
const commonProps = {
|
|
33
|
-
'aria-selected': arrowNav ?
|
|
50
|
+
'aria-selected': arrowNav ? isActive : undefined,
|
|
34
51
|
'aria-controls': controls,
|
|
35
52
|
role: arrowNav ? 'tab' : undefined,
|
|
36
|
-
|
|
53
|
+
onKeyDown: handleKeyDown,
|
|
37
54
|
onClick: handleClick,
|
|
38
|
-
tabIndex:
|
|
55
|
+
tabIndex: disabled ? -1 : isActive || !arrowNav ? undefined : -1,
|
|
39
56
|
ref: (el: HTMLAnchorElement | HTMLButtonElement | null) => {
|
|
40
|
-
registerTabRef(index, el)
|
|
57
|
+
registerTabRef(index, el, disabled)
|
|
41
58
|
if (typeof ref === 'function') {
|
|
42
59
|
ref(el)
|
|
43
60
|
} else if (ref) {
|
|
@@ -60,14 +77,25 @@ export const PktTabItem = forwardRef<HTMLAnchorElement | HTMLButtonElement, IPkt
|
|
|
60
77
|
|
|
61
78
|
if (href) {
|
|
62
79
|
return (
|
|
63
|
-
<a
|
|
80
|
+
<a
|
|
81
|
+
{...commonProps}
|
|
82
|
+
href={disabled ? undefined : href}
|
|
83
|
+
aria-disabled={disabled || undefined}
|
|
84
|
+
className={`pkt-tabs__link ${isActive ? 'active' : ''} ${disabled ? 'pkt-tabs__item--disabled' : ''}`}
|
|
85
|
+
>
|
|
64
86
|
{content}
|
|
65
87
|
</a>
|
|
66
88
|
)
|
|
67
89
|
}
|
|
68
90
|
|
|
69
91
|
return (
|
|
70
|
-
<button
|
|
92
|
+
<button
|
|
93
|
+
{...commonProps}
|
|
94
|
+
type="button"
|
|
95
|
+
disabled={disabled}
|
|
96
|
+
aria-disabled={disabled || undefined}
|
|
97
|
+
className={`pkt-tabs__button pkt-link-button ${isActive ? 'active' : ''} ${disabled ? 'pkt-tabs__item--disabled' : ''}`}
|
|
98
|
+
>
|
|
71
99
|
{content}
|
|
72
100
|
</button>
|
|
73
101
|
)
|
|
@@ -205,7 +205,7 @@ describe('PktTabItem children format', () => {
|
|
|
205
205
|
const secondTab = getByText('Second Tab')
|
|
206
206
|
|
|
207
207
|
firstTab.focus()
|
|
208
|
-
fireEvent.
|
|
208
|
+
fireEvent.keyDown(firstTab, { key: 'ArrowRight' })
|
|
209
209
|
|
|
210
210
|
expect(secondTab).toHaveFocus()
|
|
211
211
|
})
|
|
@@ -225,7 +225,7 @@ describe('PktTabItem children format', () => {
|
|
|
225
225
|
const secondTab = getByText('Second Tab')
|
|
226
226
|
|
|
227
227
|
secondTab.focus()
|
|
228
|
-
fireEvent.
|
|
228
|
+
fireEvent.keyDown(secondTab, { key: 'ArrowLeft' })
|
|
229
229
|
|
|
230
230
|
expect(firstTab).toHaveFocus()
|
|
231
231
|
})
|
|
@@ -244,7 +244,7 @@ describe('PktTabItem children format', () => {
|
|
|
244
244
|
const thirdTab = getByText('Third Tab')
|
|
245
245
|
|
|
246
246
|
thirdTab.focus()
|
|
247
|
-
fireEvent.
|
|
247
|
+
fireEvent.keyDown(thirdTab, { key: 'ArrowRight' })
|
|
248
248
|
|
|
249
249
|
expect(thirdTab).toHaveFocus()
|
|
250
250
|
})
|
|
@@ -263,7 +263,7 @@ describe('PktTabItem children format', () => {
|
|
|
263
263
|
const firstTab = getByText('First Tab')
|
|
264
264
|
|
|
265
265
|
firstTab.focus()
|
|
266
|
-
fireEvent.
|
|
266
|
+
fireEvent.keyDown(firstTab, { key: 'ArrowLeft' })
|
|
267
267
|
|
|
268
268
|
expect(firstTab).toHaveFocus()
|
|
269
269
|
})
|
|
@@ -278,7 +278,7 @@ describe('PktTabItem children format', () => {
|
|
|
278
278
|
)
|
|
279
279
|
|
|
280
280
|
const secondTab = getByText('Second Tab')
|
|
281
|
-
fireEvent.
|
|
281
|
+
fireEvent.keyDown(secondTab, { key: ' ' })
|
|
282
282
|
|
|
283
283
|
expect(handleTabSelected).toHaveBeenCalledWith(1)
|
|
284
284
|
})
|
|
@@ -293,7 +293,7 @@ describe('PktTabItem children format', () => {
|
|
|
293
293
|
)
|
|
294
294
|
|
|
295
295
|
const secondTab = getByText('Second Tab')
|
|
296
|
-
fireEvent.
|
|
296
|
+
fireEvent.keyDown(secondTab, { key: 'ArrowDown' })
|
|
297
297
|
|
|
298
298
|
expect(handleTabSelected).toHaveBeenCalledWith(1)
|
|
299
299
|
})
|
|
@@ -312,7 +312,7 @@ describe('PktTabItem children format', () => {
|
|
|
312
312
|
const secondTab = getByText('Second Tab')
|
|
313
313
|
|
|
314
314
|
firstTab.focus()
|
|
315
|
-
fireEvent.
|
|
315
|
+
fireEvent.keyDown(firstTab, { key: 'ArrowRight' })
|
|
316
316
|
|
|
317
317
|
expect(secondTab).not.toHaveFocus()
|
|
318
318
|
})
|
|
@@ -331,7 +331,7 @@ describe('PktTabItem children format', () => {
|
|
|
331
331
|
const secondTab = getByText('Second Tab')
|
|
332
332
|
|
|
333
333
|
firstTab.focus()
|
|
334
|
-
fireEvent.
|
|
334
|
+
fireEvent.keyDown(firstTab, { key: 'ArrowRight' })
|
|
335
335
|
|
|
336
336
|
expect(secondTab).not.toHaveFocus()
|
|
337
337
|
})
|
|
@@ -351,7 +351,7 @@ describe('PktTabItem children format', () => {
|
|
|
351
351
|
|
|
352
352
|
// Even though arrowNav is true, disableArrowNav should override it
|
|
353
353
|
firstTab.focus()
|
|
354
|
-
fireEvent.
|
|
354
|
+
fireEvent.keyDown(firstTab, { key: 'ArrowRight' })
|
|
355
355
|
|
|
356
356
|
expect(secondTab).not.toHaveFocus()
|
|
357
357
|
})
|
|
@@ -377,6 +377,104 @@ describe('PktTabItem children format', () => {
|
|
|
377
377
|
expect(handleTabSelected).toHaveBeenCalledWith(2)
|
|
378
378
|
})
|
|
379
379
|
|
|
380
|
+
describe('Disabled tabs', () => {
|
|
381
|
+
it('does not call onTabSelected or action for disabled button tab from tabs prop', () => {
|
|
382
|
+
const onTabSelected = vi.fn()
|
|
383
|
+
const action = vi.fn()
|
|
384
|
+
const { getByText } = render(
|
|
385
|
+
<PktTabs
|
|
386
|
+
onTabSelected={onTabSelected}
|
|
387
|
+
tabs={[
|
|
388
|
+
{ text: 'Enabled', action: vi.fn() },
|
|
389
|
+
{ text: 'Disabled', action, disabled: true },
|
|
390
|
+
]}
|
|
391
|
+
/>,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
fireEvent.click(getByText('Disabled'))
|
|
395
|
+
|
|
396
|
+
expect(action).not.toHaveBeenCalled()
|
|
397
|
+
expect(onTabSelected).not.toHaveBeenCalled()
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
it('renders disabled button tab as native disabled', () => {
|
|
401
|
+
const { getByText } = render(
|
|
402
|
+
<PktTabs>
|
|
403
|
+
<PktTabItem index={0} disabled>
|
|
404
|
+
Disabled button
|
|
405
|
+
</PktTabItem>
|
|
406
|
+
</PktTabs>,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
expect(getByText('Disabled button')).toBeDisabled()
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it('renders disabled link tab with aria-disabled and blocks interaction', () => {
|
|
413
|
+
const handleTabSelected = vi.fn()
|
|
414
|
+
const handleClick = vi.fn()
|
|
415
|
+
const { getByText } = render(
|
|
416
|
+
<PktTabs onTabSelected={handleTabSelected}>
|
|
417
|
+
<PktTabItem index={0} href="/enabled">
|
|
418
|
+
Enabled link
|
|
419
|
+
</PktTabItem>
|
|
420
|
+
<PktTabItem index={1} href="/disabled" disabled onClick={handleClick}>
|
|
421
|
+
Disabled link
|
|
422
|
+
</PktTabItem>
|
|
423
|
+
</PktTabs>,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
const disabledLink = getByText('Disabled link')
|
|
427
|
+
fireEvent.click(disabledLink)
|
|
428
|
+
fireEvent.keyDown(disabledLink, { key: 'Enter' })
|
|
429
|
+
|
|
430
|
+
expect(disabledLink).toHaveAttribute('aria-disabled', 'true')
|
|
431
|
+
expect(disabledLink).toHaveAttribute('tabindex', '-1')
|
|
432
|
+
expect(disabledLink).not.toHaveAttribute('href')
|
|
433
|
+
expect(handleClick).not.toHaveBeenCalled()
|
|
434
|
+
expect(handleTabSelected).not.toHaveBeenCalled()
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it('skips disabled tabs during arrow navigation', () => {
|
|
438
|
+
const { getByText } = render(
|
|
439
|
+
<PktTabs>
|
|
440
|
+
<PktTabItem index={0} active>
|
|
441
|
+
First
|
|
442
|
+
</PktTabItem>
|
|
443
|
+
<PktTabItem index={1} disabled>
|
|
444
|
+
Disabled
|
|
445
|
+
</PktTabItem>
|
|
446
|
+
<PktTabItem index={2}>Third</PktTabItem>
|
|
447
|
+
</PktTabs>,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
const first = getByText('First')
|
|
451
|
+
const third = getByText('Third')
|
|
452
|
+
|
|
453
|
+
first.focus()
|
|
454
|
+
fireEvent.keyDown(first, { key: 'ArrowRight' })
|
|
455
|
+
|
|
456
|
+
expect(third).toHaveFocus()
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
it('prevents disabled active tab from being selectable', () => {
|
|
460
|
+
const handleTabSelected = vi.fn()
|
|
461
|
+
const { getByText } = render(
|
|
462
|
+
<PktTabs onTabSelected={handleTabSelected}>
|
|
463
|
+
<PktTabItem index={0} active disabled>
|
|
464
|
+
Disabled active
|
|
465
|
+
</PktTabItem>
|
|
466
|
+
<PktTabItem index={1}>Enabled</PktTabItem>
|
|
467
|
+
</PktTabs>,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
const disabledTab = getByText('Disabled active')
|
|
471
|
+
expect(disabledTab).toHaveAttribute('aria-selected', 'false')
|
|
472
|
+
|
|
473
|
+
fireEvent.keyDown(disabledTab, { key: 'Enter' })
|
|
474
|
+
expect(handleTabSelected).not.toHaveBeenCalled()
|
|
475
|
+
})
|
|
476
|
+
})
|
|
477
|
+
|
|
380
478
|
describe('Accessibility', () => {
|
|
381
479
|
it('should not have any accessibility violations', async () => {
|
|
382
480
|
const { container } = render(
|
|
@@ -19,8 +19,12 @@ export type TSkin = 'blue' | 'green' | 'red' | 'beige' | 'yellow' | 'grey' | 'gr
|
|
|
19
19
|
// Context for passing tab navigation logic to children
|
|
20
20
|
interface ITabsContext {
|
|
21
21
|
arrowNav: boolean
|
|
22
|
-
registerTabRef: (
|
|
23
|
-
|
|
22
|
+
registerTabRef: (
|
|
23
|
+
index: number,
|
|
24
|
+
el: HTMLAnchorElement | HTMLButtonElement | null,
|
|
25
|
+
disabled?: boolean,
|
|
26
|
+
) => void
|
|
27
|
+
handleKeyPress: (index: number, event: KeyboardEvent<HTMLAnchorElement | HTMLButtonElement>) => void
|
|
24
28
|
selectTab: (index: number) => void
|
|
25
29
|
}
|
|
26
30
|
|
|
@@ -38,6 +42,7 @@ export interface IPktTab {
|
|
|
38
42
|
text: string
|
|
39
43
|
href?: string
|
|
40
44
|
action?: (index: number) => void
|
|
45
|
+
disabled?: boolean
|
|
41
46
|
icon?: string
|
|
42
47
|
controls?: string
|
|
43
48
|
tag?: {
|
|
@@ -61,6 +66,7 @@ export const PktTabs = forwardRef(
|
|
|
61
66
|
ref: Ref<HTMLDivElement>,
|
|
62
67
|
): JSX.Element => {
|
|
63
68
|
const tabRefs = useRef<Array<HTMLAnchorElement | HTMLButtonElement | null>>([])
|
|
69
|
+
const disabledMap = useRef<Record<number, boolean>>({})
|
|
64
70
|
|
|
65
71
|
const useArrowNav = arrowNav && !disableArrowNav
|
|
66
72
|
|
|
@@ -70,9 +76,19 @@ export const PktTabs = forwardRef(
|
|
|
70
76
|
|
|
71
77
|
useEffect(() => {
|
|
72
78
|
tabRefs.current = tabRefs.current.slice(0, tabCount)
|
|
79
|
+
Object.keys(disabledMap.current).forEach((key) => {
|
|
80
|
+
const index = Number(key)
|
|
81
|
+
if (index >= tabCount) delete disabledMap.current[index]
|
|
82
|
+
})
|
|
73
83
|
}, [tabCount])
|
|
74
84
|
|
|
85
|
+
const isTabDisabled = (index: number): boolean => {
|
|
86
|
+
if (tabs) return !!tabs[index]?.disabled
|
|
87
|
+
return !!disabledMap.current[index]
|
|
88
|
+
}
|
|
89
|
+
|
|
75
90
|
const selectTab = (index: number): void => {
|
|
91
|
+
if (isTabDisabled(index)) return
|
|
76
92
|
const tab = tabs?.[index]
|
|
77
93
|
if (tab?.action) {
|
|
78
94
|
tab.action(index)
|
|
@@ -80,31 +96,56 @@ export const PktTabs = forwardRef(
|
|
|
80
96
|
if (onTabSelected) onTabSelected(index)
|
|
81
97
|
}
|
|
82
98
|
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
99
|
+
const findEnabledIndex = (startIndex: number, direction: -1 | 1): number | null => {
|
|
100
|
+
let current = startIndex + direction
|
|
101
|
+
|
|
102
|
+
while (current >= 0 && current < tabCount) {
|
|
103
|
+
if (!isTabDisabled(current)) return current
|
|
104
|
+
current += direction
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const handleKeyPress = (index: number, event: KeyboardEvent<HTMLAnchorElement | HTMLButtonElement>) => {
|
|
111
|
+
if (!useArrowNav) return
|
|
112
|
+
|
|
113
|
+
if (event.key === 'ArrowLeft') {
|
|
114
|
+
event.preventDefault()
|
|
115
|
+
const previousEnabled = findEnabledIndex(index, -1)
|
|
116
|
+
if (previousEnabled !== null) tabRefs.current[previousEnabled]?.focus()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (event.key === 'ArrowRight') {
|
|
120
|
+
event.preventDefault()
|
|
121
|
+
const nextEnabled = findEnabledIndex(index, 1)
|
|
122
|
+
if (nextEnabled !== null) tabRefs.current[nextEnabled]?.focus()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar' || event.key === 'ArrowDown') {
|
|
126
|
+
event.preventDefault()
|
|
127
|
+
if (!isTabDisabled(index)) {
|
|
92
128
|
selectTab(index)
|
|
93
129
|
}
|
|
94
130
|
}
|
|
95
131
|
}
|
|
96
132
|
|
|
97
|
-
const registerTabRef = (
|
|
133
|
+
const registerTabRef = (
|
|
134
|
+
index: number,
|
|
135
|
+
el: HTMLAnchorElement | HTMLButtonElement | null,
|
|
136
|
+
disabled = false,
|
|
137
|
+
) => {
|
|
98
138
|
tabRefs.current[index] = el
|
|
139
|
+
disabledMap.current[index] = disabled
|
|
99
140
|
}
|
|
100
141
|
|
|
101
142
|
// If tabs as prop instead of children
|
|
102
143
|
const tabItems = tabs?.map((tab, index) => (
|
|
103
144
|
<PktTabItem
|
|
104
145
|
key={index}
|
|
105
|
-
active={tab.active}
|
|
146
|
+
active={tab.disabled ? false : tab.active}
|
|
147
|
+
disabled={tab.disabled}
|
|
106
148
|
href={tab.href}
|
|
107
|
-
onClick={() => selectTab(index)}
|
|
108
149
|
icon={tab.icon}
|
|
109
150
|
controls={tab.controls}
|
|
110
151
|
tag={tab.tag?.text}
|