@cerberus-design/react 0.2.0 → 0.3.1-next-f5a8b6a
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/build/legacy/_tsup-dts-rollup.d.ts +132 -0
- package/build/legacy/aria-helpers/tabs.aria.d.ts +1 -0
- package/build/legacy/aria-helpers/tabs.aria.js +9 -0
- package/build/legacy/aria-helpers/tabs.aria.js.map +1 -0
- package/build/legacy/chunk-36QCFAIS.js +121 -0
- package/build/legacy/chunk-36QCFAIS.js.map +1 -0
- package/build/legacy/chunk-57HOQM4E.js +65 -0
- package/build/legacy/chunk-57HOQM4E.js.map +1 -0
- package/build/legacy/chunk-HQK7SM56.js +50 -0
- package/build/legacy/chunk-HQK7SM56.js.map +1 -0
- package/build/legacy/{chunk-X6PHIZRM.js → chunk-MJB3V6J4.js} +4 -4
- package/build/legacy/chunk-ODSSU3JD.js +28 -0
- package/build/legacy/chunk-ODSSU3JD.js.map +1 -0
- package/build/legacy/chunk-RCE2XXL7.js +43 -0
- package/build/legacy/chunk-RCE2XXL7.js.map +1 -0
- package/build/legacy/components/NavMenuTrigger.js +2 -2
- package/build/legacy/components/Tab.d.ts +2 -0
- package/build/legacy/components/Tab.js +10 -0
- package/build/legacy/components/Tab.js.map +1 -0
- package/build/legacy/components/TabList.d.ts +2 -0
- package/build/legacy/components/TabList.js +7 -0
- package/build/legacy/components/TabList.js.map +1 -0
- package/build/legacy/components/TabPanel.d.ts +2 -0
- package/build/legacy/components/TabPanel.js +10 -0
- package/build/legacy/components/TabPanel.js.map +1 -0
- package/build/legacy/context/tabs.d.ts +5 -0
- package/build/legacy/context/tabs.js +12 -0
- package/build/legacy/context/tabs.js.map +1 -0
- package/build/legacy/index.d.ts +12 -0
- package/build/legacy/index.js +34 -10
- package/build/modern/_tsup-dts-rollup.d.ts +132 -0
- package/build/modern/aria-helpers/tabs.aria.d.ts +1 -0
- package/build/modern/aria-helpers/tabs.aria.js +9 -0
- package/build/modern/aria-helpers/tabs.aria.js.map +1 -0
- package/build/modern/chunk-57HOQM4E.js +65 -0
- package/build/modern/chunk-57HOQM4E.js.map +1 -0
- package/build/modern/chunk-HQK7SM56.js +50 -0
- package/build/modern/chunk-HQK7SM56.js.map +1 -0
- package/build/modern/chunk-LAUJGQO2.js +120 -0
- package/build/modern/chunk-LAUJGQO2.js.map +1 -0
- package/build/modern/{chunk-X6PHIZRM.js → chunk-MJB3V6J4.js} +4 -4
- package/build/modern/chunk-ODSSU3JD.js +28 -0
- package/build/modern/chunk-ODSSU3JD.js.map +1 -0
- package/build/modern/chunk-RCE2XXL7.js +43 -0
- package/build/modern/chunk-RCE2XXL7.js.map +1 -0
- package/build/modern/components/NavMenuTrigger.js +2 -2
- package/build/modern/components/Tab.d.ts +2 -0
- package/build/modern/components/Tab.js +10 -0
- package/build/modern/components/Tab.js.map +1 -0
- package/build/modern/components/TabList.d.ts +2 -0
- package/build/modern/components/TabList.js +7 -0
- package/build/modern/components/TabList.js.map +1 -0
- package/build/modern/components/TabPanel.d.ts +2 -0
- package/build/modern/components/TabPanel.js +10 -0
- package/build/modern/components/TabPanel.js.map +1 -0
- package/build/modern/context/tabs.d.ts +5 -0
- package/build/modern/context/tabs.js +12 -0
- package/build/modern/context/tabs.js.map +1 -0
- package/build/modern/index.d.ts +12 -0
- package/build/modern/index.js +34 -10
- package/package.json +1 -1
- package/src/aria-helpers/tabs.aria.ts +70 -0
- package/src/components/Tab.tsx +134 -0
- package/src/components/TabList.tsx +42 -0
- package/src/components/TabPanel.tsx +55 -0
- package/src/context/tabs.tsx +88 -0
- package/src/index.ts +5 -0
- /package/build/legacy/{chunk-X6PHIZRM.js.map → chunk-MJB3V6J4.js.map} +0 -0
- /package/build/modern/{chunk-X6PHIZRM.js.map → chunk-MJB3V6J4.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { Tabs_alias_1 as Tabs } from '../_tsup-dts-rollup';
|
|
2
|
+
export { useTabsContext_alias_1 as useTabsContext } from '../_tsup-dts-rollup';
|
|
3
|
+
export { TabsContextValue_alias_1 as TabsContextValue } from '../_tsup-dts-rollup';
|
|
4
|
+
export { TabsContext_alias_1 as TabsContext } from '../_tsup-dts-rollup';
|
|
5
|
+
export { TabsProps_alias_1 as TabsProps } from '../_tsup-dts-rollup';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/build/modern/index.d.ts
CHANGED
|
@@ -16,6 +16,12 @@ export { NavMenuList } from './_tsup-dts-rollup';
|
|
|
16
16
|
export { NavMenuListProps } from './_tsup-dts-rollup';
|
|
17
17
|
export { NavMenuLink } from './_tsup-dts-rollup';
|
|
18
18
|
export { NavMenuLinkProps } from './_tsup-dts-rollup';
|
|
19
|
+
export { Tab } from './_tsup-dts-rollup';
|
|
20
|
+
export { TabProps } from './_tsup-dts-rollup';
|
|
21
|
+
export { TabList } from './_tsup-dts-rollup';
|
|
22
|
+
export { TabListProps } from './_tsup-dts-rollup';
|
|
23
|
+
export { TabPanel } from './_tsup-dts-rollup';
|
|
24
|
+
export { TabPanelProps } from './_tsup-dts-rollup';
|
|
19
25
|
export { Show } from './_tsup-dts-rollup';
|
|
20
26
|
export { ShowProps } from './_tsup-dts-rollup';
|
|
21
27
|
export { Field } from './_tsup-dts-rollup';
|
|
@@ -26,6 +32,11 @@ export { useNavMenuContext } from './_tsup-dts-rollup';
|
|
|
26
32
|
export { NavTriggerRef } from './_tsup-dts-rollup';
|
|
27
33
|
export { NavMenuRef } from './_tsup-dts-rollup';
|
|
28
34
|
export { NavMenuContextValue } from './_tsup-dts-rollup';
|
|
35
|
+
export { Tabs } from './_tsup-dts-rollup';
|
|
36
|
+
export { useTabsContext } from './_tsup-dts-rollup';
|
|
37
|
+
export { TabsContextValue } from './_tsup-dts-rollup';
|
|
38
|
+
export { TabsContext } from './_tsup-dts-rollup';
|
|
39
|
+
export { TabsProps } from './_tsup-dts-rollup';
|
|
29
40
|
export { ThemeProvider } from './_tsup-dts-rollup';
|
|
30
41
|
export { useThemeContext } from './_tsup-dts-rollup';
|
|
31
42
|
export { DefaultThemes } from './_tsup-dts-rollup';
|
|
@@ -37,4 +48,5 @@ export { MODE_KEY } from './_tsup-dts-rollup';
|
|
|
37
48
|
export { useTheme } from './_tsup-dts-rollup';
|
|
38
49
|
export { createNavTriggerProps } from './_tsup-dts-rollup';
|
|
39
50
|
export { NavTriggerAriaValues } from './_tsup-dts-rollup';
|
|
51
|
+
export { useTabsKeyboardNavigation } from './_tsup-dts-rollup';
|
|
40
52
|
export { Positions } from './_tsup-dts-rollup';
|
package/build/modern/index.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TabPanel
|
|
3
|
+
} from "./chunk-RCE2XXL7.js";
|
|
4
|
+
import {
|
|
5
|
+
Input
|
|
6
|
+
} from "./chunk-LD5ZV46F.js";
|
|
1
7
|
import {
|
|
2
8
|
Label
|
|
3
9
|
} from "./chunk-OXVNTE4A.js";
|
|
@@ -10,15 +16,32 @@ import {
|
|
|
10
16
|
} from "./chunk-WSQTX34C.js";
|
|
11
17
|
import {
|
|
12
18
|
NavMenuTrigger
|
|
13
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-MJB3V6J4.js";
|
|
14
20
|
import {
|
|
15
21
|
NavMenu,
|
|
16
22
|
useNavMenuContext
|
|
17
23
|
} from "./chunk-KJUCHZHV.js";
|
|
24
|
+
import {
|
|
25
|
+
Show
|
|
26
|
+
} from "./chunk-R4H3352X.js";
|
|
27
|
+
import {
|
|
28
|
+
Tab
|
|
29
|
+
} from "./chunk-LAUJGQO2.js";
|
|
30
|
+
import {
|
|
31
|
+
TabList
|
|
32
|
+
} from "./chunk-ODSSU3JD.js";
|
|
18
33
|
import "./chunk-55J6XMHW.js";
|
|
19
34
|
import {
|
|
20
35
|
createNavTriggerProps
|
|
21
36
|
} from "./chunk-JF76VIL3.js";
|
|
37
|
+
import {
|
|
38
|
+
useTabsKeyboardNavigation
|
|
39
|
+
} from "./chunk-57HOQM4E.js";
|
|
40
|
+
import {
|
|
41
|
+
Tabs,
|
|
42
|
+
TabsContext,
|
|
43
|
+
useTabsContext
|
|
44
|
+
} from "./chunk-HQK7SM56.js";
|
|
22
45
|
import {
|
|
23
46
|
MODE_KEY,
|
|
24
47
|
THEME_KEY,
|
|
@@ -32,19 +55,13 @@ import {
|
|
|
32
55
|
import {
|
|
33
56
|
FieldMessage
|
|
34
57
|
} from "./chunk-YVUZSAJG.js";
|
|
35
|
-
import {
|
|
36
|
-
IconButton
|
|
37
|
-
} from "./chunk-BPIYUAYS.js";
|
|
38
|
-
import {
|
|
39
|
-
Input
|
|
40
|
-
} from "./chunk-LD5ZV46F.js";
|
|
41
|
-
import {
|
|
42
|
-
Show
|
|
43
|
-
} from "./chunk-R4H3352X.js";
|
|
44
58
|
import {
|
|
45
59
|
Field,
|
|
46
60
|
useFieldContext
|
|
47
61
|
} from "./chunk-ZAU4JVLL.js";
|
|
62
|
+
import {
|
|
63
|
+
IconButton
|
|
64
|
+
} from "./chunk-BPIYUAYS.js";
|
|
48
65
|
export {
|
|
49
66
|
Button,
|
|
50
67
|
Field,
|
|
@@ -59,11 +76,18 @@ export {
|
|
|
59
76
|
NavMenuTrigger,
|
|
60
77
|
Show,
|
|
61
78
|
THEME_KEY,
|
|
79
|
+
Tab,
|
|
80
|
+
TabList,
|
|
81
|
+
TabPanel,
|
|
82
|
+
Tabs,
|
|
83
|
+
TabsContext,
|
|
62
84
|
ThemeProvider,
|
|
63
85
|
createNavTriggerProps,
|
|
64
86
|
getPosition,
|
|
65
87
|
useFieldContext,
|
|
66
88
|
useNavMenuContext,
|
|
89
|
+
useTabsContext,
|
|
90
|
+
useTabsKeyboardNavigation,
|
|
67
91
|
useTheme,
|
|
68
92
|
useThemeContext
|
|
69
93
|
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { useTabsContext } from '../context/tabs'
|
|
5
|
+
|
|
6
|
+
function getNextIndex(index: number, length: number) {
|
|
7
|
+
return index === length - 1 ? 0 : index + 1
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function getPrevIndex(index: number, length: number) {
|
|
11
|
+
return index === 0 ? length - 1 : index - 1
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useTabsKeyboardNavigation() {
|
|
15
|
+
const { tabs } = useTabsContext()
|
|
16
|
+
const [activeTab, setActiveTab] = useState(-1)
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
20
|
+
const index =
|
|
21
|
+
activeTab === -1
|
|
22
|
+
? tabs.current.findIndex((tab) => tab.ariaSelected === 'true')
|
|
23
|
+
: activeTab
|
|
24
|
+
const nextIndex = getNextIndex(index, tabs.current.length)
|
|
25
|
+
const prevIndex = getPrevIndex(index, tabs.current.length)
|
|
26
|
+
|
|
27
|
+
// If the active tab is not found, do nothing
|
|
28
|
+
if (index === -1) return
|
|
29
|
+
|
|
30
|
+
switch (event.key) {
|
|
31
|
+
case 'ArrowLeft':
|
|
32
|
+
event.preventDefault()
|
|
33
|
+
setActiveTab(prevIndex)
|
|
34
|
+
tabs.current[prevIndex].focus()
|
|
35
|
+
break
|
|
36
|
+
case 'ArrowRight':
|
|
37
|
+
event.preventDefault()
|
|
38
|
+
setActiveTab(nextIndex)
|
|
39
|
+
tabs.current[nextIndex].focus()
|
|
40
|
+
break
|
|
41
|
+
case 'Home':
|
|
42
|
+
event.preventDefault()
|
|
43
|
+
setActiveTab(0)
|
|
44
|
+
tabs.current[0].focus()
|
|
45
|
+
break
|
|
46
|
+
case 'End':
|
|
47
|
+
event.preventDefault()
|
|
48
|
+
setActiveTab(tabs.current.length - 1)
|
|
49
|
+
tabs.current[tabs.current.length - 1].focus()
|
|
50
|
+
break
|
|
51
|
+
default:
|
|
52
|
+
break
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
57
|
+
|
|
58
|
+
return () => {
|
|
59
|
+
document.removeEventListener('keydown', handleKeyDown)
|
|
60
|
+
}
|
|
61
|
+
}, [activeTab, tabs.current])
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
ref: (tab: HTMLButtonElement) => {
|
|
65
|
+
if (tab && !tabs.current.includes(tab)) {
|
|
66
|
+
tabs.current.push(tab)
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useMemo, type ButtonHTMLAttributes, type MouseEvent } from 'react'
|
|
4
|
+
import { useTabsContext } from '../context/tabs'
|
|
5
|
+
import { css, cx } from '@cerberus/styled-system/css'
|
|
6
|
+
import { useTabsKeyboardNavigation } from '../aria-helpers/tabs.aria'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* This module provides a Tab component.
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface TabProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
14
|
+
value: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The Tab component provides a tab element to be used in a TabList.
|
|
19
|
+
* @definition [ARIA Target Size](https://www.w3.org/WAI/WCAG21/Understanding/target-size.html#:~:text=Understanding%20SC%202.5.,%3ATarget%20Size%20(Level%20AAA)&text=The%20size%20of%20the%20target,Equivalent)
|
|
20
|
+
* @definition [Tab docs](https://cerberus.digitalu.design/react/tabs)
|
|
21
|
+
* @param value - the id of the tab that will be tracked as the active tab and used for aria attributes
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* <Tab value="overview">
|
|
25
|
+
* Overview
|
|
26
|
+
* </Tab>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function Tab(props: TabProps) {
|
|
30
|
+
const { value, ...nativeProps } = props
|
|
31
|
+
const { active, onTabUpdate } = useTabsContext()
|
|
32
|
+
const { ref } = useTabsKeyboardNavigation()
|
|
33
|
+
const isActive = useMemo(() => active === value, [active, value])
|
|
34
|
+
|
|
35
|
+
function handleClick(e: MouseEvent<HTMLButtonElement>) {
|
|
36
|
+
props.onClick?.(e)
|
|
37
|
+
onTabUpdate(e.currentTarget.value)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<button
|
|
42
|
+
{...nativeProps}
|
|
43
|
+
{...(!isActive && { tabIndex: -1 })}
|
|
44
|
+
aria-controls={`panel:${value}`}
|
|
45
|
+
aria-selected={isActive}
|
|
46
|
+
id={value}
|
|
47
|
+
className={cx(nativeProps.className, btnStyles)}
|
|
48
|
+
onClick={handleClick}
|
|
49
|
+
role="tab"
|
|
50
|
+
ref={ref}
|
|
51
|
+
value={value}
|
|
52
|
+
/>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const btnStyles = css({
|
|
57
|
+
alignItems: 'center',
|
|
58
|
+
display: 'inline-flex',
|
|
59
|
+
borderTopLeftRadius: 'md',
|
|
60
|
+
borderTopRightRadius: 'md',
|
|
61
|
+
fontSize: 'sm',
|
|
62
|
+
fontWeight: '600',
|
|
63
|
+
gap: '2',
|
|
64
|
+
h: '2.75rem',
|
|
65
|
+
justifyContent: 'center',
|
|
66
|
+
position: 'relative',
|
|
67
|
+
pxi: '4',
|
|
68
|
+
zIndex: 'base',
|
|
69
|
+
_motionSafe: {
|
|
70
|
+
transition: 'all 200ms ease-in-out',
|
|
71
|
+
_before: {
|
|
72
|
+
transitionProperty: 'height',
|
|
73
|
+
transitionDuration: '200ms',
|
|
74
|
+
transitionTimingFunction: 'ease-in-out',
|
|
75
|
+
},
|
|
76
|
+
_after: {
|
|
77
|
+
transitionProperty: 'height',
|
|
78
|
+
transitionDuration: '200ms',
|
|
79
|
+
transitionTimingFunction: 'ease-in-out',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
_before: {
|
|
83
|
+
bgColor: 'action.border.initial',
|
|
84
|
+
bottom: '0',
|
|
85
|
+
content: '""',
|
|
86
|
+
h: '0',
|
|
87
|
+
position: 'absolute',
|
|
88
|
+
left: '0',
|
|
89
|
+
right: '0',
|
|
90
|
+
w: 'full',
|
|
91
|
+
willChange: 'height',
|
|
92
|
+
zIndex: 'decorator',
|
|
93
|
+
},
|
|
94
|
+
_after: {
|
|
95
|
+
borderTopLeftRadius: 'md',
|
|
96
|
+
borderTopRightRadius: 'md',
|
|
97
|
+
bottom: '0',
|
|
98
|
+
bgColor: 'neutral.surface.100',
|
|
99
|
+
content: '""',
|
|
100
|
+
left: '0',
|
|
101
|
+
position: 'absolute',
|
|
102
|
+
right: '0',
|
|
103
|
+
h: '0',
|
|
104
|
+
w: 'full',
|
|
105
|
+
willChange: 'height',
|
|
106
|
+
zIndex: '-1',
|
|
107
|
+
},
|
|
108
|
+
_hover: {
|
|
109
|
+
_after: {
|
|
110
|
+
h: 'full',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
_focusVisible: {
|
|
114
|
+
boxShadow: 'none',
|
|
115
|
+
outline: '3px solid',
|
|
116
|
+
outlineColor: 'action.border.focus',
|
|
117
|
+
outlineOffset: '2px',
|
|
118
|
+
},
|
|
119
|
+
_disabled: {
|
|
120
|
+
cursor: 'not-allowed',
|
|
121
|
+
opacity: '0.5',
|
|
122
|
+
},
|
|
123
|
+
_selected: {
|
|
124
|
+
color: 'action.text.200',
|
|
125
|
+
_before: {
|
|
126
|
+
h: '3px',
|
|
127
|
+
},
|
|
128
|
+
_hover: {
|
|
129
|
+
_after: {
|
|
130
|
+
h: '0',
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { cx } from '@cerberus/styled-system/css'
|
|
2
|
+
import { hstack } from '@cerberus/styled-system/patterns'
|
|
3
|
+
import type { HTMLAttributes, PropsWithChildren } from 'react'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* This module provides a TabList component.
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface TabListProps extends HTMLAttributes<HTMLDivElement> {
|
|
11
|
+
description: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The TabList component provides a container for tab elements.
|
|
16
|
+
* @param description - a description of what the tab list contains
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* <TabList description="Button details">
|
|
20
|
+
* <Tab id="overview">Overview</Tab>
|
|
21
|
+
* <Tab id="guidelines">Guidelines</Tab>
|
|
22
|
+
* </TabList>
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function TabList(props: PropsWithChildren<TabListProps>) {
|
|
26
|
+
const { description, ...nativeProps } = props
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
{...nativeProps}
|
|
30
|
+
aria-describedby={description}
|
|
31
|
+
className={cx(
|
|
32
|
+
nativeProps.className,
|
|
33
|
+
hstack({
|
|
34
|
+
borderBottom: '1px solid',
|
|
35
|
+
borderBottomColor: 'action.border.100',
|
|
36
|
+
gap: '0',
|
|
37
|
+
w: 'full',
|
|
38
|
+
}),
|
|
39
|
+
)}
|
|
40
|
+
/>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { css, cx } from '@cerberus/styled-system/css'
|
|
4
|
+
import { useMemo, type HTMLAttributes } from 'react'
|
|
5
|
+
import { useTabsContext } from '../context/tabs'
|
|
6
|
+
import { Show } from './Show'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* This module provides a TabPanel component.
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface TabPanelProps extends HTMLAttributes<HTMLDivElement> {
|
|
14
|
+
tab: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The TabPanel component provides a panel element to be used in a Tabs provider.
|
|
19
|
+
* @param tab - the value of the tab that will be tracked as the active tab and used for aria attributes
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <TabPanel tab="overview">
|
|
23
|
+
* Overview content
|
|
24
|
+
* </TabPanel>
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function TabPanel(props: TabPanelProps) {
|
|
28
|
+
const { tab, ...nativeProps } = props
|
|
29
|
+
const { active } = useTabsContext()
|
|
30
|
+
const isActive = useMemo(() => active === tab, [active, tab])
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Show when={isActive}>
|
|
34
|
+
<div
|
|
35
|
+
{...nativeProps}
|
|
36
|
+
{...(isActive && { tabIndex: 0 })}
|
|
37
|
+
aria-labelledby={tab}
|
|
38
|
+
className={cx(
|
|
39
|
+
nativeProps.className,
|
|
40
|
+
css({
|
|
41
|
+
rounded: 'md',
|
|
42
|
+
_focusVisible: {
|
|
43
|
+
boxShadow: 'none',
|
|
44
|
+
outline: '3px solid',
|
|
45
|
+
outlineColor: 'action.border.focus',
|
|
46
|
+
outlineOffset: '2px',
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
)}
|
|
50
|
+
id={`panel:${tab}`}
|
|
51
|
+
role="tabpanel"
|
|
52
|
+
/>
|
|
53
|
+
</Show>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
type MutableRefObject,
|
|
11
|
+
type PropsWithChildren,
|
|
12
|
+
} from 'react'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* This module provides a Tabs component and a hook to access its context.
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface TabsContextValue {
|
|
20
|
+
active: string
|
|
21
|
+
tabs: MutableRefObject<HTMLButtonElement[]>
|
|
22
|
+
onTabUpdate: (active: string) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const TabsContext = createContext<TabsContextValue | null>(null)
|
|
26
|
+
|
|
27
|
+
export interface TabsProps {
|
|
28
|
+
active?: string
|
|
29
|
+
cache?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The Tabs component provides a context to manage tab state.
|
|
34
|
+
* @param active - the default active tab id,
|
|
35
|
+
* @param cache - whether to cache the active tab state in local storage
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* <Tabs cache>
|
|
39
|
+
* <TabList description="Button details">
|
|
40
|
+
* <Tab id="overview">Overview</Tab>
|
|
41
|
+
* <Tab id="guidelines">Guidelines</Tab>
|
|
42
|
+
* </TabList>
|
|
43
|
+
* <TabPanels>
|
|
44
|
+
* <TabPanel id="overview">Overview content</TabPanel>
|
|
45
|
+
* <TabPanel id="guidelines">Guidelines content</TabPanel>
|
|
46
|
+
* </TabPanels>
|
|
47
|
+
* </Tabs>
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function Tabs(props: PropsWithChildren<TabsProps>): JSX.Element {
|
|
51
|
+
const { cache } = props
|
|
52
|
+
const [active, setActive] = useState(() => (cache ? '' : props.active ?? ''))
|
|
53
|
+
const tabs = useRef<HTMLButtonElement[]>([])
|
|
54
|
+
|
|
55
|
+
const value = useMemo(
|
|
56
|
+
() => ({
|
|
57
|
+
active,
|
|
58
|
+
tabs,
|
|
59
|
+
onTabUpdate: setActive,
|
|
60
|
+
}),
|
|
61
|
+
[active, setActive],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const cachedTab = window.localStorage.getItem('cerberus-tabs')
|
|
66
|
+
if (cache && cachedTab) {
|
|
67
|
+
setActive(cachedTab)
|
|
68
|
+
}
|
|
69
|
+
}, [cache])
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (cache) {
|
|
73
|
+
window.localStorage.setItem('cerberus-tabs', active)
|
|
74
|
+
}
|
|
75
|
+
}, [active, cache])
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<TabsContext.Provider value={value}>{props.children}</TabsContext.Provider>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function useTabsContext(): TabsContextValue {
|
|
83
|
+
const context = useContext(TabsContext)
|
|
84
|
+
if (!context) {
|
|
85
|
+
throw new Error('useTabsContext must be used within a Tabs Provider.')
|
|
86
|
+
}
|
|
87
|
+
return context
|
|
88
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -13,12 +13,16 @@ export * from './components/Label'
|
|
|
13
13
|
export * from './components/NavMenuTrigger'
|
|
14
14
|
export * from './components/NavMenuList'
|
|
15
15
|
export * from './components/NavMenuLink'
|
|
16
|
+
export * from './components/Tab'
|
|
17
|
+
export * from './components/TabList'
|
|
18
|
+
export * from './components/TabPanel'
|
|
16
19
|
export * from './components/Show'
|
|
17
20
|
|
|
18
21
|
// context
|
|
19
22
|
|
|
20
23
|
export * from './context/field'
|
|
21
24
|
export * from './context/navMenu'
|
|
25
|
+
export * from './context/tabs'
|
|
22
26
|
export * from './context/theme'
|
|
23
27
|
|
|
24
28
|
// hooks
|
|
@@ -28,6 +32,7 @@ export * from './hooks/useTheme'
|
|
|
28
32
|
// aria-helpers
|
|
29
33
|
|
|
30
34
|
export * from './aria-helpers/nav-menu.aria'
|
|
35
|
+
export * from './aria-helpers/tabs.aria'
|
|
31
36
|
|
|
32
37
|
// shared types
|
|
33
38
|
|
|
File without changes
|
|
File without changes
|