The above has quite a bit of repetitive and unnecessary code. I've spent some time refactoring and making it a bit more modular/easier to use.
Working example: https://codesandbox.io/s/rlwq35oz4o
Changes:
Tabs
acts like a container for multiple Tab
components
Tab
is a simple reuseable component that requires a title
and children
- All tabs are now managed by
activeTab
state and compared against a mapped key
(this can be easily changed/implemented for a unique id
)
- Utilizing
prevProps.children.length
to determine tab length in the setState
callback
- Changed
TabPane
's a
(link element) to b
(an unformatted text element) because it caused a styling issue with links embedded into the TabBody
utilizing the same role="tablist"
- Added
word-wrap: break-word;
to a
elements within the TabBody
to prevent them from breaking the section
for small screens.
index.js
import React from "react";
import { render } from "react-dom";
import Tabs, { Tab } from "./components/Tabs";
import "./styles.css";
const App = () => (
<Tabs>
<Tab title="Section 1">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam euismod,
tortor nec pharetra ultricies, ante erat imperdiet velit, nec laoreet enim
lacus a velit.<a href="#">Nam luctus</a>, enim in interdum condimentum,
nisl diam iaculis lorem, vel volutpat mi leo sit amet lectus. Praesent non
odio bibendum magna bibendum accumsan.
</Tab>
<Tab title="Section 2">
Nullam at diam nec arcu suscipit auctor non a erat. Sed et magna semper,
eleifend magna non, facilisis nisl. Proin et est et lorem dictum finibus
ut nec turpis. Aenean nisi tortor, euismod a mauris a, mattis scelerisque
tortor. Sed dolor risus, varius a nibh id, condimentum lacinia est. In
lacinia cursus odio a aliquam. Curabitur tortor magna, laoreet ut rhoncus
at, sodales consequat
</Tab>
<Tab title="Section 3">
Phasellus ac tristique orci. Nulla maximus
<a href="">justo nec dignissim consequat</a>. Sed vehicula diam sit amet
mi efficitur vehicula in in nisl. Aliquam erat volutpat. Suspendisse lorem
turpis, accumsan consequat consectetur gravida,
<a href="#">pellentesque ac ante</a>. Aliquam in commodo ligula, sit amet
mollis neque. Vestibulum at facilisis massa.
</Tab>
<Tab title="Section 4">
Nam luctus, enim in interdum condimentum, nisl diam iaculis lorem, vel
volutpat mi leo sit amet lectus. Praesent non odio bibendum magna bibendum
accumsan. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam
euismod, tortor nec pharetra ultricies, ante erat imperdiet velit, nec
laoreet enim lacus a velit.
</Tab>
</Tabs>
);
render(<App />, document.getElementById("root"));
components/Tabs.js
import React, { Component } from "react";
import PropTypes from "prop-types";
import TabPane from "./TabPane";
import TabBody from "./TabBody";
export const Tab = ({ title, children }) => <div tab={title}>{children}</div>;
Tab.propTypes = {
children: PropTypes.node.isRequired,
title: PropTypes.string.isRequired
};
class Tabs extends Component {
state = { activeTab: 0 };
componentDidMount = () => {
document.addEventListener("keydown", this.handleKeyPress, false);
window.focus();
}
componentWillUnmount = () =>
document.removeEventListener("keydown", this.handleKeyPress, false);
handleClickTabItem = ({ target: { id } }) =>
this.setState({ activeTab: ~~id });
handleKeyPress = ({ keyCode }) => {
if (keyCode === 37 || keyCode === 39) {
this.setState((prevState, prevProps) => {
const nextTab = keyCode === 37
? prevState.activeTab - 1
: prevState.activeTab + 1;
return nextTab >= 0 && nextTab < prevProps.children.length
? { activeTab: nextTab }
: null;
});
}
};
render = () => {
const { activeTab } = this.state;
const { children } = this.props;
return (
<div className="tabbed">
<ul role="tablist">
{children.map(({ props }, key) => (
<TabPane
key={key}
activeTab={activeTab}
onClickTabItem={this.handleClickTabItem}
id={key}
{...props}
/>
))}
<div className="tab-content">
{children.map(({ props }, key) =>
key === activeTab ? (
<TabBody key={key} id={key} {...props} />
) : null
)}
</div>
</ul>
</div>
);
};
}
Tabs.propTypes = {
children: PropTypes.node.isRequired
};
export default Tabs;
components/TabPane.js
import React from "react";
import PropTypes from "prop-types";
const TabPane = ({ activeTab, id, onClickTabItem, title, ...rest }) => (
<li role="presentation">
<b
id={id}
aria-selected={activeTab === id ? "true" : null}
onClick={onClickTabItem}
role="tab"
{...rest}
>
{title}
</b>
</li>
);
TabPane.propTypes = {
activeTab: PropTypes.number.isRequired,
id: PropTypes.number.isRequired,
onClickTabItem: PropTypes.func.isRequired,
title: PropTypes.string.isRequired
};
export default TabPane;
components/TabBody.js
import React from "react";
import PropTypes from "prop-types";
const TabBody = ({ title, id, children }) => (
<section id={id} role="tabpanel" tabIndex="-1" aria-labelledby={id}>
<h2>{title}</h2>
<div>{children}</div>
</section>
);
TabBody.propTypes = {
children: PropTypes.node.isRequired,
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired
};
export default TabBody;
styles.css
body {
max-width: 40rem;
padding: 0 1rem;
font-size: 125%;
line-height: 1.5;
margin: 1.5rem auto;
font-family: "Lato", Arial, sans-serif;
font-size: 16px;
}
* {
color: inherit;
margin: 0;
}
[role="tablist"] {
padding: 0;
}
[role="tablist"] li,
[role="tablist"] b {
display: inline-block;
}
[role="tablist"] b {
text-decoration: none;
padding: 0.5rem 1em;
cursor: pointer;
}
[role="tablist"] a {
text-decoration: none;
padding-left: 0.2rem;
word-wrap: break-word;
}
[role="tablist"] [aria-selected] {
border: 2px solid;
background: #fff;
border-bottom: 0;
position: relative;
top: 2px;
}
[role="tabpanel"] {
border: 2px solid;
padding: 1.5rem;
}
[role="tabpanel"] * + * {
margin-top: 0.75rem;
}
*:focus {
outline: none;
box-shadow: inset 0 0 0 4px lightBlue;
}
@media (max-width: 550px) {
[role="tablist"] li,
[role="tablist"] b {
display: block;
position: static;
}
[role="tablist"] b {
border: 2px solid #222 !important;
}
[role="tablist"] li + li b {
border-top: 0 !important;
}
[role="tablist"] [aria-selected] {
position: static;
}
[role="tablist"] [aria-selected]::after {
content: "\0020⬅";
}
[role="tabpanel"] {
border-top: 0;
}
}
section a {
color: rgb(66, 133, 244);
}