Skip to content

Commit 6d0d54d

Browse files
authored
fix: duplicate key in tabs component (#882)
This commit adds a useMemo to the Tabs component so that we can use a UUID for the keys of the Tabs children. The UUID allows us to generate a unique key, while the useMemo prevents the UUID from being recalculated unless the children change. Closes D2IQ-92719
1 parent fbe5429 commit 6d0d54d

File tree

2 files changed

+46
-54
lines changed

2 files changed

+46
-54
lines changed

packages/tabs/components/Tabs.tsx

+44-53
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from "react";
22
import { Tabs as ReactTabs, TabList, TabPanel } from "react-tabs";
33
import { injectGlobal, cx } from "@emotion/css";
4-
4+
import uuid from "uuid";
55
import { TabItemProps } from "./TabItem";
66
import { TabTitle } from "..";
77
import {
@@ -13,9 +13,7 @@ import {
1313
import { listReset } from "../../shared/styles/styleUtils";
1414
import { BreakpointConfig } from "../../shared/styles/breakpoints";
1515
import { fullHeightTabs, getTabLayout } from "../style";
16-
1716
export const defaultTabDirection = "horiz";
18-
1917
// Copy & paste from node_modules/react-tabs/style/react-tabs.css
2018
// Also changed to better fit the ui kit styles.
2119
// This is needed to give the tabs a style
@@ -24,53 +22,44 @@ injectGlobal`
2422
.react-tabs {
2523
-webkit-tap-highlight-color: transparent;
2624
}
27-
2825
.react-tabs__tab-list {
2926
${listReset};
3027
}
31-
3228
.react-tabs__tab {
3329
position: relative;
3430
cursor: pointer;
3531
font-weight: ${fontWeightMedium};
3632
color: ${themeTextColorPrimary};
37-
3833
&:after{
3934
content: "";
4035
position: absolute;
4136
background: ${themeBrandPrimary};
4237
display: none;
4338
}
4439
}
45-
4640
.react-tabs__tab:focus {
4741
outline: none;
4842
background: ${themeBgHover}
4943
}
50-
5144
.react-tabs__tab--selected {
5245
&:after{
5346
display: block;
5447
}
5548
color: ${themeBrandPrimary};
5649
}
57-
5850
.react-tabs__tab--disabled {
5951
color: GrayText;
6052
cursor: default;
6153
}
62-
6354
.react-tabs__tab-panel {
6455
display: none;
6556
flex-grow: 1;
6657
}
67-
6858
.react-tabs__tab-panel--selected {
6959
display: block;
7060
}
7161
`;
7262
/* eslint-enable */
73-
7463
export type TabDirections = "horiz" | "vert";
7564
export type TabDirection = BreakpointConfig<TabDirections>;
7665
export type TabSelected = string;
@@ -82,52 +71,55 @@ export interface TabsProps {
8271
onSelect?: (tabIndex: number) => void;
8372
direction?: TabDirection;
8473
}
85-
8674
const Tabs = ({
8775
children,
8876
selectedIndex,
8977
onSelect,
9078
direction = defaultTabDirection
9179
}: TabsProps) => {
92-
const { tabs, tabsContent } = (
93-
React.Children.toArray(children) as Array<React.ReactElement<TabItemProps>>
94-
)
95-
.filter(item => React.isValidElement<TabItemProps>(item))
96-
.reduce<{
97-
tabs: React.ReactNode[];
98-
tabsContent: React.ReactNode[];
99-
}>(
100-
(acc, item) => {
101-
const { tabs = [], tabsContent = [] } = acc;
102-
const { children } = item.props;
103-
const key = item.key ? item.key : undefined;
104-
const childrenWithKeys = React.Children.toArray(children).map(child =>
105-
React.isValidElement<typeof TabTitle>(child)
106-
? React.cloneElement(child, { key })
107-
: child
108-
);
109-
110-
const title = childrenWithKeys.find(
111-
child =>
112-
React.isValidElement<typeof TabTitle>(child) &&
113-
child.type === TabTitle
114-
);
115-
const tabChildren = childrenWithKeys.filter(
116-
child => !(React.isValidElement(child) && child.type === TabTitle)
117-
);
118-
return {
119-
tabs: [...tabs, title],
120-
tabsContent: [
121-
...tabsContent,
122-
...(tabChildren.length
123-
? [<TabPanel key={key}>{tabChildren}</TabPanel>]
124-
: [])
125-
]
126-
};
127-
},
128-
{ tabs: [], tabsContent: [] }
129-
);
130-
80+
const { tabs, tabsContent } = React.useMemo(() => {
81+
return (
82+
React.Children.toArray(children) as Array<
83+
React.ReactElement<TabItemProps>
84+
>
85+
)
86+
.filter(item => React.isValidElement<TabItemProps>(item))
87+
.reduce<{
88+
tabs: React.ReactNode[];
89+
tabsContent: React.ReactNode[];
90+
}>(
91+
(acc, item) => {
92+
const { tabs = [], tabsContent = [] } = acc;
93+
const { children } = item.props;
94+
const key = item.key ? item.key : undefined;
95+
const childrenWithKeys = React.Children.toArray(children).map(
96+
child => {
97+
return React.isValidElement<typeof TabTitle>(child)
98+
? React.cloneElement(child, { key: `${key}-${uuid()}` })
99+
: child;
100+
}
101+
);
102+
const title = childrenWithKeys.find(
103+
child =>
104+
React.isValidElement<typeof TabTitle>(child) &&
105+
child.type === TabTitle
106+
);
107+
const tabChildren = childrenWithKeys.filter(
108+
child => !(React.isValidElement(child) && child.type === TabTitle)
109+
);
110+
return {
111+
tabs: [...tabs, title],
112+
tabsContent: [
113+
...tabsContent,
114+
...(tabChildren.length
115+
? [<TabPanel key={key}>{tabChildren}</TabPanel>]
116+
: [])
117+
]
118+
};
119+
},
120+
{ tabs: [], tabsContent: [] }
121+
);
122+
}, [children]);
131123
return (
132124
<ReactTabs
133125
className={cx("react-tabs", {
@@ -146,5 +138,4 @@ const Tabs = ({
146138
</ReactTabs>
147139
);
148140
};
149-
150141
export default React.memo(Tabs);

packages/tabs/stories/Tabs.stories.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ const Template: Story<TabsProps> = ({ direction, ...args }: TabsProps) => {
3838
>
3939
<TabItem>
4040
<TabTitle>Tab 1</TabTitle>
41-
Tab content.
41+
<div>Tab content - Section 1.</div>
42+
<div>Tab content - Section 2.</div>
4243
</TabItem>
4344
<TabItem>
4445
<TabTitle>Tab 2</TabTitle>

0 commit comments

Comments
 (0)