-
Notifications
You must be signed in to change notification settings - Fork 86
/
Tabs.tsx
222 lines (198 loc) · 6.71 KB
/
Tabs.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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
import { Children, cloneElement, isValidElement, useState, useRef } from 'react';
import type * as React from 'react';
import Tab from './Tab';
import TabPanel, { TabPanelProps } from './TabPanel';
import classnames from 'classnames';
import get from 'lodash/get';
export interface TabsProps {
/**
* Must only contain `TabPanel` components
*/
children: React.ReactNode;
/**
* Sets the initial selected state to the specified `TabPanel` id. Use this
* for an uncontrolled component; otherwise, use the `selectedId` property.
* If no selected id is specified, the first `TabPanel` will be selected.
*/
defaultSelectedId?: string;
/**
* Sets the initial selected state to the specified `TabPanel` id. Use this
* in combination with `onChange` for a controlled component; otherwise, set
* `defaultSelectedId`.
*/
selectedId?: string;
/**
* A callback function that's invoked when the selected tab is changed.
* `(selectedId, prevSelectedId) => void`
*/
onChange?: (selectedId: string, prevSelectedId: string) => any;
/**
* Additional classes to be added to the component wrapping the tabs
*/
tablistClassName?: string;
}
/** CONSTANTS
* Adding in the constant values for keycodes
* to handle onKeyDown events
*/
const LEFT_ARROW = 'ArrowLeft';
const RIGHT_ARROW = 'ArrowRight';
/**
* Determine if a React component is a TabPanel
* @param {React.Node} child - a React component
* @return {Boolean} Is this a TabPanel component?
*/
const isTabPanel = (child): boolean => {
const componentName = get(child, 'type.displayName') || get(child, 'type.name');
// Check child.type first and as a fallback, check child.type.displayName follow by child.type.name
return child && (child.type === TabPanel || componentName === 'TabPanel');
};
/**
* Get the id of the first TabPanel child
* @param {Object} props
* @return {String} The id
*/
const getDefaultSelectedId = (props): string => {
let selectedId;
// TODO: Use the panelChildren method to pass in an array
// of panels, instead of doing it here...
Children.forEach(props.children, function (child) {
if (isTabPanel(child) && !selectedId) {
selectedId = child.props.id;
}
});
return selectedId;
};
/**
* Generate an id for a panel's associated tab if one doesn't yet exist
* @param {Object} TabPanel component
* @return {String} Tab ID
*/
const panelTabId = (panel): string => {
return panel.props.tabId ?? `${panel.props.id}__tab`;
};
/**
* `Tabs` is a container component that manages the state of your tabs for you.
* In most cases, you'll want to use this component rather than the
* presentational components (`Tab`, `TabPanel`) on their own.
*
* A `TabPanel` is a presentational component which accepts a tab's content as
* its `children`.
*
* For information about how and when to use this component,
* [refer to its full documentation page](https://design.cms.gov/components/tabs/).
*/
export const Tabs = (props: TabsProps) => {
const initialSelectedId = props.defaultSelectedId || getDefaultSelectedId(props);
const [internalSelectedId, setSelectedId] = useState(initialSelectedId);
const isControlled = props.selectedId !== undefined;
const selectedId = isControlled ? props.selectedId : internalSelectedId;
const listClasses = classnames('ds-c-tabs', props.tablistClassName);
// using useRef hook to keep track of elements to focus
const tabsRef = useRef({});
/**
* Update the URL in the browser without adding a new entry to the history.
* @param {String} url - Absolute or relative URL
*/
const replaceState = (url: string): void => {
if (window.history) {
window.history.replaceState({}, document.title, url);
}
};
const panelChildren = (): React.ReactNode[] => {
return Children.toArray(props.children).filter(isTabPanel);
};
const handleSelectedTabChange = (newSelectedId: string): void => {
const { onChange } = props;
if (onChange) {
onChange(newSelectedId, selectedId);
}
tabsRef.current[newSelectedId].focus();
replaceState(tabsRef.current[newSelectedId].href);
setSelectedId(newSelectedId);
};
const handleTabClick = (evt: React.MouseEvent, panelId: string): void => {
evt.preventDefault();
handleSelectedTabChange(panelId);
};
const handleTabKeyDown = (evt: React.KeyboardEvent, panelId: string): void => {
const tabs = panelChildren();
const tabIndex = tabs.findIndex((elem: React.ReactElement) => elem.props.id === panelId);
let target;
switch (evt.key) {
case LEFT_ARROW:
evt.preventDefault();
if (tabIndex === 0) {
const prevTab = tabs[tabs.length - 1] as React.ReactElement;
target = prevTab.props.id;
} else {
const prevTab = tabs[tabIndex - 1] as React.ReactElement;
target = prevTab.props.id;
}
handleSelectedTabChange(target);
break;
case RIGHT_ARROW:
evt.preventDefault();
if (tabIndex === tabs.length - 1) {
const currentTab = tabs[0] as React.ReactElement;
target = currentTab.props.id;
} else {
const nextTab = tabs[tabIndex + 1] as React.ReactElement;
target = nextTab.props.id;
}
handleSelectedTabChange(target);
break;
default:
break;
}
};
const renderChildren = (): React.ReactNode => {
return Children.map(props.children, (child) => {
if (isTabPanel(child) && isValidElement(child)) {
// Extend props on panels before rendering. Also removes any props
// that don't need passed into TabPanel but are used to generate
// the Tab components
return cloneElement(child as React.ReactElement<TabPanelProps>, {
selected: selectedId === child.props.id,
tab: undefined,
tabHref: undefined,
tabId: panelTabId(child),
});
}
return child;
});
};
const renderTabs = (): React.ReactNode => {
const panels = panelChildren() as React.ReactElement[];
const tabs = panels.map((panel) => {
return (
<Tab
className={panel.props.tabClassName}
href={panel.props.tabHref}
disabled={panel.props.disabled}
id={panelTabId(panel)}
key={panel.key}
onClick={handleTabClick}
onKeyDown={handleTabKeyDown}
panelId={panel.props.id}
ref={(tab) => {
tabsRef.current[panel.props.id] = tab;
}}
selected={selectedId === panel.props.id}
>
{panel.props.tab}
</Tab>
);
});
return tabs;
};
return (
<div>
<div className={listClasses} role="tablist">
{renderTabs()}
</div>
{renderChildren()}
</div>
);
};
export default Tabs;