-
-
Notifications
You must be signed in to change notification settings - Fork 300
/
TabsManager.tsx
202 lines (179 loc) · 5.65 KB
/
TabsManager.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
import {
createContext,
isValidElement,
ReactElement,
ReactNode,
useCallback,
useContext,
useMemo,
useState,
} from "react";
import type { TabConfig } from "./types";
export type InitializedTabConfig = TabConfig & Required<Pick<TabConfig, "id">>;
export interface TabsManagerContext {
/**
* This is an id prefix to use for all the child Tab, TabList, and TabPanel
* components.
*
* @example
* id behavior
* ```
* - `Tabs` -> id={id}
* - `Tab` ->
* - id={`${id}-tab-${index + 1}`}
* - panelId={active && `${id}-panel-${index + 1}`}
* - `TabPanel` -> id={`${id}-panel-${index + 1}`}
* ```
*/
tabsId: string;
/**
* The current active tab index to determine which tabs to animate in and out
* of view.
*/
activeIndex: number;
/**
* A function to call when the `activeIndex` should change due to keyboard
* movement or clicking on a tab.
*/
onActiveIndexChange: (activeIndex: number) => void;
/**
* The list of tabs that should be controlled by the tabs manager.
*/
tabs: readonly InitializedTabConfig[];
}
export type InitializedTabsManagerContext = Required<TabsManagerContext>;
const context = createContext<InitializedTabsManagerContext>({
tabsId: "tabs",
activeIndex: 0,
onActiveIndexChange: () => {
// do nothing
},
tabs: [],
});
/**
* This hook returns the current "state" for the tabs which can be useful if you
* need additional control or access to the tabs behavior.
*/
export function useTabs(): InitializedTabsManagerContext {
return useContext(context);
}
const { Provider } = context;
export interface TabsManagerProps
extends Omit<
TabsManagerContext,
"activeIndex" | "onActiveIndexChange" | "tabs"
> {
/**
* The index of the tab that should be active by default. This is ignored if
* the `activeIndex` prop is defined.
*/
defaultActiveIndex?: number;
/**
* If you want to control the current active index instead of relying on the
* built in behavior, you can provide an `activeIndex` prop which will be used
* instead. If this prop is defined, you **must** also provide the
* `onActiveIndexChange` so that keyboard functionality and tab changing
* behavior can still be used.
*/
activeIndex?: number;
/**
* An optional function to call when the active index changes when the
* `activeIndex` prop is not provided. If the `activeIndex` prop is provided,
* this is **required** for keyboard accessibility.
*/
onActiveIndexChange?: TabsManagerContext["onActiveIndexChange"];
/**
* The list of tabs that should be controlled by the tabs manager.
*/
tabs: readonly (TabConfig | ReactElement | string)[];
/**
* The children to render that should eventually have the `Tabs` component and
* the `TabContent` for matching specific tabs.
*/
children: ReactNode;
/**
* Boolean if all the `tabs` that have icons should be stacked instead of
* rendered inline.
*
* This is mostly a convenience prop so that you don't manually need to enable
* it for each tab in the `tabs` list and if a `tab` in the `tabs` list has
* the `stacked` attribute enabled defined, it will be used instead of this
* value.
*/
stacked?: boolean;
/**
* Boolean if the icon should appear after the text instead of before for all
* the `tabs` that have an icon. When the `stacked` prop is also enabled, it
* will cause the icon to appear below the text instead of above.
*
* This is mostly a convenience prop so that you don't manually need to enable
* it for each tab in the `tabs` list and if a `tab` in the `tabs` list has
* the `stacked` attribute enabled defined, it will be used instead of this
* value.
*/
iconAfter?: boolean;
}
/**
* The `TabsManager` is used to configure your `Tabs` component and handle some
* of the default behavior such as:
*
* - controlling the `activeIndex`
* - general tab configuration
* - callbacks when the tab has changed
* - providing an `id` prefix for all tabs for simplicity
*/
export function TabsManager({
tabsId,
defaultActiveIndex = 0,
activeIndex: propActiveIndex,
onActiveIndexChange,
tabs,
stacked = false,
iconAfter = false,
children,
}: TabsManagerProps): ReactElement {
const [localActiveIndex, setActiveIndex] = useState(defaultActiveIndex);
const handleActiveIndexChange = useCallback(
(activeIndex: number) => {
if (onActiveIndexChange) {
onActiveIndexChange(activeIndex);
}
setActiveIndex(activeIndex);
},
[onActiveIndexChange]
);
const activeIndex =
typeof propActiveIndex === "number" ? propActiveIndex : localActiveIndex;
const updateActiveIndex =
typeof propActiveIndex === "number"
? (onActiveIndexChange as TabsManagerContext["onActiveIndexChange"])
: handleActiveIndexChange;
const value = useMemo(
() => ({
activeIndex,
onActiveIndexChange: updateActiveIndex,
tabs: tabs.map((config, i) => {
let tab: TabConfig;
if (typeof config === "string" || isValidElement(config)) {
tab = { children: config };
} else {
tab = config;
}
return {
...tab,
id: tab.id || `${tabsId}-tab-${i + 1}`,
panelId:
activeIndex === i
? `${tabsId}-panel-${activeIndex + 1}`
: undefined,
stacked: typeof tab.stacked === "boolean" ? tab.stacked : stacked,
iconAfter:
typeof tab.iconAfter === "boolean" ? tab.iconAfter : iconAfter,
};
}),
tabsId,
}),
[activeIndex, iconAfter, stacked, tabs, tabsId, updateActiveIndex]
);
return <Provider value={value}>{children}</Provider>;
}