Skip to content

Commit e0f155f

Browse files
committed
refactor(CTabs): fully implement a controlled/uncontrolled pattern
1 parent f451d62 commit e0f155f

15 files changed

+184
-34
lines changed

packages/coreui-react/src/components/tabs/CTab.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { forwardRef, HTMLAttributes, useContext } from 'react'
22
import PropTypes from 'prop-types'
33
import classNames from 'classnames'
44

5-
import { TabsContext } from './CTabs'
5+
import { CTabsContext } from './CTabsContext'
66

77
export interface CTabProps extends HTMLAttributes<HTMLButtonElement> {
88
/**
@@ -21,7 +21,7 @@ export interface CTabProps extends HTMLAttributes<HTMLButtonElement> {
2121

2222
export const CTab = forwardRef<HTMLButtonElement, CTabProps>(
2323
({ children, className, itemKey, ...rest }, ref) => {
24-
const { _activeItemKey, setActiveItemKey, id } = useContext(TabsContext)
24+
const { _activeItemKey, setActiveItemKey, id } = useContext(CTabsContext)
2525

2626
const isActive = () => itemKey === _activeItemKey
2727

packages/coreui-react/src/components/tabs/CTabPanel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
33
import classNames from 'classnames'
44
import { Transition } from 'react-transition-group'
55

6-
import { TabsContext } from './CTabs'
6+
import { CTabsContext } from './CTabsContext'
77
import { useForkedRef } from '../../hooks'
88
import { getTransitionDurationFromElement } from '../../utils'
99

@@ -36,7 +36,7 @@ export interface CTabPanelProps extends HTMLAttributes<HTMLDivElement> {
3636

3737
export const CTabPanel = forwardRef<HTMLDivElement, CTabPanelProps>(
3838
({ children, className, itemKey, onHide, onShow, transition = true, visible, ...rest }, ref) => {
39-
const { _activeItemKey, id } = useContext(TabsContext)
39+
const { _activeItemKey, id } = useContext(CTabsContext)
4040

4141
const tabPaneRef = useRef(null)
4242
const forkedRef = useForkedRef(ref, tabPaneRef)

packages/coreui-react/src/components/tabs/CTabs.tsx

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,86 @@
1-
import React, { createContext, forwardRef, HTMLAttributes, useEffect, useId, useState } from 'react'
1+
import React, { forwardRef, HTMLAttributes, useId, useState } from 'react'
22
import PropTypes from 'prop-types'
33
import classNames from 'classnames'
44

5+
import { CTabsContext } from './CTabsContext'
6+
57
export interface CTabsProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
68
/**
7-
* The active item key.
9+
* Controls the currently active tab.
10+
*
11+
* When provided, the component operates in a controlled mode.
12+
* You must handle tab switching manually by updating this prop.
13+
*
14+
* @example
15+
* const [activeTab, setActiveTab] = useState(0);
16+
* <CTabs activeItemKey={activeTab} onChange={setActiveTab} />
817
*/
9-
activeItemKey: number | string
18+
activeItemKey?: number | string
19+
1020
/**
1121
* A string of all className you want applied to the base component.
1222
*/
1323
className?: string
24+
1425
/**
15-
* The callback is fired when the active tab changes.
26+
* Sets the initially active tab when the component mounts.
27+
*
28+
* After initialization, the component manages active tab changes internally.
29+
*
30+
* Use `defaultActiveItemKey` for uncontrolled usage.
31+
*
32+
* @example
33+
* <CTabs defaultActiveItemKey={1} />
1634
*/
17-
onChange?: (value: number | string) => void
18-
}
35+
defaultActiveItemKey?: number | string
1936

20-
export interface TabsContextProps {
21-
_activeItemKey?: number | string
22-
setActiveItemKey: React.Dispatch<React.SetStateAction<number | string | undefined>>
23-
id?: string
37+
/**
38+
* Callback fired when the active tab changes.
39+
*
40+
* - In controlled mode (`activeItemKey` provided), you must update `activeItemKey` yourself based on the value received.
41+
* - In uncontrolled mode, this callback is called after internal state updates.
42+
*
43+
* @param value - The newly selected tab key.
44+
*
45+
* @example
46+
* <CTabs onChange={(key) => console.log('Tab changed to', key)} />
47+
*/
48+
onChange?: (value: number | string) => void
2449
}
2550

26-
export const TabsContext = createContext({} as TabsContextProps)
27-
2851
export const CTabs = forwardRef<HTMLDivElement, CTabsProps>(
29-
({ children, activeItemKey, className, onChange }, ref) => {
52+
({ children, activeItemKey, className, defaultActiveItemKey, onChange }, ref) => {
3053
const id = useId()
31-
const [_activeItemKey, setActiveItemKey] = useState(activeItemKey)
54+
const isControlled = activeItemKey !== undefined
55+
const [internalActiveItemKey, setInternalActiveItemKey] = useState<number | string | undefined>(
56+
() => (isControlled ? undefined : defaultActiveItemKey)
57+
)
58+
59+
const currentActiveItemKey = isControlled ? activeItemKey : internalActiveItemKey
60+
61+
const setActiveItemKey = (value: number | string) => {
62+
if (!isControlled) {
63+
setInternalActiveItemKey(value)
64+
}
3265

33-
useEffect(() => {
34-
_activeItemKey && onChange && onChange(_activeItemKey)
35-
}, [_activeItemKey])
66+
onChange?.(value)
67+
}
3668

3769
return (
38-
<TabsContext.Provider value={{ _activeItemKey, setActiveItemKey, id }}>
70+
<CTabsContext.Provider value={{ _activeItemKey: currentActiveItemKey, setActiveItemKey, id }}>
3971
<div className={classNames('tabs', className)} ref={ref}>
4072
{children}
4173
</div>
42-
</TabsContext.Provider>
74+
</CTabsContext.Provider>
4375
)
44-
},
76+
}
4577
)
4678

4779
CTabs.propTypes = {
48-
activeItemKey: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
80+
activeItemKey: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
4981
children: PropTypes.node,
5082
className: PropTypes.string,
83+
defaultActiveItemKey: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
5184
onChange: PropTypes.func,
5285
}
5386

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createContext } from 'react'
2+
3+
export interface CTabsContextProps {
4+
_activeItemKey?: number | string
5+
setActiveItemKey: React.Dispatch<React.SetStateAction<number | string | undefined>>
6+
id?: string
7+
}
8+
9+
export const CTabsContext = createContext({} as CTabsContextProps)

packages/docs/content/api/CTabs.api.mdx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ import CTabs from '@coreui/react/src/components/tabs/CTabs'
2222
</tr>
2323
<tr>
2424
<td colSpan="3">
25-
<p>The active item key.</p>
25+
<p>Controls the currently active tab.</p>
26+
<p>When provided, the component operates in a controlled mode.<br />
27+
You must handle tab switching manually by updating this prop.</p>
28+
<JSXDocs code={`const [activeTab, setActiveTab] = useState(0);
29+
<CTabs activeItemKey={activeTab} onChange={setActiveTab} />`} />
2630
</td>
2731
</tr>
2832
<tr id="ctabs-class-name">
@@ -35,14 +39,32 @@ import CTabs from '@coreui/react/src/components/tabs/CTabs'
3539
<p>A string of all className you want applied to the base component.</p>
3640
</td>
3741
</tr>
42+
<tr id="ctabs-default-active-item-key">
43+
<td className="text-primary fw-semibold">defaultActiveItemKey<a href="#ctabs-default-active-item-key" aria-label="CTabs defaultActiveItemKey permalink" className="anchor-link after">#</a></td>
44+
<td>-</td>
45+
<td><code>{`string`}</code>, <code>{`number`}</code></td>
46+
</tr>
47+
<tr>
48+
<td colSpan="3">
49+
<p>Sets the initially active tab when the component mounts.</p>
50+
<p>After initialization, the component manages active tab changes internally.</p>
51+
<p>Use <code>{`defaultActiveItemKey`}</code> for uncontrolled usage.</p>
52+
<JSXDocs code={`<CTabs defaultActiveItemKey={1} />`} />
53+
</td>
54+
</tr>
3855
<tr id="ctabs-on-change">
3956
<td className="text-primary fw-semibold">onChange<a href="#ctabs-on-change" aria-label="CTabs onChange permalink" className="anchor-link after">#</a></td>
4057
<td>-</td>
4158
<td><code>{`(value: string | number) => void`}</code></td>
4259
</tr>
4360
<tr>
4461
<td colSpan="3">
45-
<p>The callback is fired when the active tab changes.</p>
62+
<p>Callback fired when the active tab changes.</p>
63+
<ul>
64+
<li>In controlled mode (<code>{`activeItemKey`}</code> provided), you must update <code>{`activeItemKey`}</code> yourself based on the value received.</li>
65+
<li>In uncontrolled mode, this callback is called after internal state updates.</li>
66+
</ul>
67+
<JSXDocs code={`<CTabs onChange={(key) => console.log('Tab changed to', key)} />`} />
4668
</td>
4769
</tr>
4870
</tbody>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React, { useState } from 'react'
2+
import { CTab, CTabContent, CTabList, CTabPanel, CTabs } from '@coreui/react'
3+
4+
export const TabsControlledExample = () => {
5+
const [activeTab, setActiveTab] = useState('profile')
6+
7+
return (
8+
<CTabs activeItemKey={activeTab} onChange={setActiveTab}>
9+
<CTabList variant="tabs">
10+
<CTab itemKey="home">Home</CTab>
11+
<CTab itemKey="profile">Profile</CTab>
12+
<CTab itemKey="contact">Contact</CTab>
13+
<CTab disabled itemKey="disabled">
14+
Disabled
15+
</CTab>
16+
</CTabList>
17+
<CTabContent>
18+
<CTabPanel className="p-3" itemKey="home">
19+
Home tab content
20+
</CTabPanel>
21+
<CTabPanel className="p-3" itemKey="profile">
22+
Profile tab content
23+
</CTabPanel>
24+
<CTabPanel className="p-3" itemKey="contact">
25+
Contact tab content
26+
</CTabPanel>
27+
<CTabPanel className="p-3" itemKey="disabled">
28+
Disabled tab content
29+
</CTabPanel>
30+
</CTabContent>
31+
</CTabs>
32+
)
33+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React, { useState } from 'react'
2+
import { CTab, CTabContent, CTabList, CTabPanel, CTabs } from '@coreui/react'
3+
4+
export const TabsControlledExample = () => {
5+
const [activeTab, setActiveTab] = useState<number | string>('profile')
6+
7+
return (
8+
<CTabs activeItemKey={activeTab} onChange={setActiveTab}>
9+
<CTabList variant="tabs">
10+
<CTab itemKey="home">Home</CTab>
11+
<CTab itemKey="profile">Profile</CTab>
12+
<CTab itemKey="contact">Contact</CTab>
13+
<CTab disabled itemKey="disabled">
14+
Disabled
15+
</CTab>
16+
</CTabList>
17+
<CTabContent>
18+
<CTabPanel className="p-3" itemKey="home">
19+
Home tab content
20+
</CTabPanel>
21+
<CTabPanel className="p-3" itemKey="profile">
22+
Profile tab content
23+
</CTabPanel>
24+
<CTabPanel className="p-3" itemKey="contact">
25+
Contact tab content
26+
</CTabPanel>
27+
<CTabPanel className="p-3" itemKey="disabled">
28+
Disabled tab content
29+
</CTabPanel>
30+
</CTabContent>
31+
</CTabs>
32+
)
33+
}

packages/docs/content/components/tabs/examples/TabsExample.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { CTab, CTabContent, CTabList, CTabPanel, CTabs } from '@coreui/react'
33

44
export const TabsExample = () => {
55
return (
6-
<CTabs activeItemKey="profile">
6+
<CTabs defaultActiveItemKey="profile">
77
<CTabList variant="tabs">
88
<CTab itemKey="home">Home</CTab>
99
<CTab itemKey="profile">Profile</CTab>

packages/docs/content/components/tabs/examples/TabsPillsExample.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { CTab, CTabContent, CTabList, CTabPanel, CTabs } from '@coreui/react'
33

44
export const TabsPillsExample = () => {
55
return (
6-
<CTabs activeItemKey={2}>
6+
<CTabs defaultActiveItemKey={2}>
77
<CTabList variant="pills">
88
<CTab aria-controls="home-tab-pane" itemKey={1}>Home</CTab>
99
<CTab aria-controls="profile-tab-pane" itemKey={2}>Profile</CTab>

packages/docs/content/components/tabs/examples/TabsUnderlineBorderExample.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { CTab, CTabContent, CTabList, CTabPanel, CTabs } from '@coreui/react'
33

44
export const TabsUnderlineBorderExample = () => {
55
return (
6-
<CTabs activeItemKey={2}>
6+
<CTabs defaultActiveItemKey={2}>
77
<CTabList variant="underline-border">
88
<CTab aria-controls="home-tab-pane" itemKey={1}>Home</CTab>
99
<CTab aria-controls="profile-tab-pane" itemKey={2}>Profile</CTab>

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy