|
1 | | -import React, { createContext, forwardRef, HTMLAttributes, useEffect, useId, useState } from 'react' |
| 1 | +import React, { forwardRef, HTMLAttributes, useId, useState } from 'react' |
2 | 2 | import PropTypes from 'prop-types' |
3 | 3 | import classNames from 'classnames' |
4 | 4 |
|
| 5 | +import { CTabsContext } from './CTabsContext' |
| 6 | + |
5 | 7 | export interface CTabsProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> { |
6 | 8 | /** |
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} /> |
8 | 17 | */ |
9 | | - activeItemKey: number | string |
| 18 | + activeItemKey?: number | string |
| 19 | + |
10 | 20 | /** |
11 | 21 | * A string of all className you want applied to the base component. |
12 | 22 | */ |
13 | 23 | className?: string |
| 24 | + |
14 | 25 | /** |
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} /> |
16 | 34 | */ |
17 | | - onChange?: (value: number | string) => void |
18 | | -} |
| 35 | + defaultActiveItemKey?: number | string |
19 | 36 |
|
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 |
24 | 49 | } |
25 | 50 |
|
26 | | -export const TabsContext = createContext({} as TabsContextProps) |
27 | | - |
28 | 51 | export const CTabs = forwardRef<HTMLDivElement, CTabsProps>( |
29 | | - ({ children, activeItemKey, className, onChange }, ref) => { |
| 52 | + ({ children, activeItemKey, className, defaultActiveItemKey, onChange }, ref) => { |
30 | 53 | 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 | + } |
32 | 65 |
|
33 | | - useEffect(() => { |
34 | | - _activeItemKey && onChange && onChange(_activeItemKey) |
35 | | - }, [_activeItemKey]) |
| 66 | + onChange?.(value) |
| 67 | + } |
36 | 68 |
|
37 | 69 | return ( |
38 | | - <TabsContext.Provider value={{ _activeItemKey, setActiveItemKey, id }}> |
| 70 | + <CTabsContext.Provider value={{ _activeItemKey: currentActiveItemKey, setActiveItemKey, id }}> |
39 | 71 | <div className={classNames('tabs', className)} ref={ref}> |
40 | 72 | {children} |
41 | 73 | </div> |
42 | | - </TabsContext.Provider> |
| 74 | + </CTabsContext.Provider> |
43 | 75 | ) |
44 | | - }, |
| 76 | + } |
45 | 77 | ) |
46 | 78 |
|
47 | 79 | CTabs.propTypes = { |
48 | | - activeItemKey: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, |
| 80 | + activeItemKey: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), |
49 | 81 | children: PropTypes.node, |
50 | 82 | className: PropTypes.string, |
| 83 | + defaultActiveItemKey: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), |
51 | 84 | onChange: PropTypes.func, |
52 | 85 | } |
53 | 86 |
|
|
0 commit comments