I came back to this problem of trying to increment/decrement through a Menu and about to drive my head though wall figuring this out. I feel I'm almost there, but missing something. I am running into not being able to arrow right until I click on a button as well the button I clicked doesn't remove the class. Any help would much appreciated.
Button js:
...
class Button extends Component {
onClick() {
const { label, onClick } = this.props;
onClick(label);
}
render() {
const {
onClick,
props: { activeTab, label, tab, className }
} = this;
let ariaSelected = "";
if (activeTab === label || className === "active") {
ariaSelected += "true";
}
return (
<li role="presentation">
<a
className={className}
aria-selected={ariaSelected ? "true" : undefined}
onClick={e => this.onClick(e)}
role="tab"
id={"tab" + tab}
//tabIndex="-1"
>
{label}
</a>
</li>
);
}
}
..
Menu Js:
class Menu extends Component {
constructor(props) {
super(props);
this.state = {
activeTab: this.props.children[0].props.label,
cursor: 0
};
this.handleKeyDown = this.handleKeyDown.bind(this);
}
componentDidMount() {
document.addEventListener("keydown", this.handleKeyDown, false);
}
componentWillUnmount() {
document.removeEventListener("keydown", this.handleKeyDown, false);
}
handleKeyDown(e) {
const { cursor } = this.state;
const cnt = React.Children.count(this.props.children);
if (e.keyCode === 37 && cursor > 0) {
this.setState(prevState => ({
cursor: prevState.cursor - 1
}));
console.log(cursor);
} else if (e.keyCode === 39 && cursor < cnt - 1) {
this.setState(prevState => ({
cursor: prevState.cursor + 1
}));
console.log(cursor);
}
}
onClickTabItem = tab => {
this.setState({
activeTab: tab
});
};
render() {
const {
onClickTabItem,
props: { children },
state: { activeTab, cursor, className }
} = this;
return (
<div className="tabbed">
<ul role="tablist">
{children.map((child, i) => {
const { label, className } = child.props;
return (
<Tab
activeTab={activeTab}
key={label}
label={label}
onClick={onClickTabItem}
tab={i}
className={ cursor === i || activeTab === label ? "active" : null}
/>
);
})}
</ul>
<div className="tab-content">
{children.map(child => {
//if tab has label or active set, otherwise do nohthing
if (child.props.label !== activeTab) return undefined;
return child.props.children;
})}
</div>
</div>
);
}
}
You could grab the label number from activeTab instead of having a cursor variable for when arrow keys are pressed:
handleKeyDown(e) {
const cnt = React.Children.count(this.props.children);
const pos = ~~this.state.activeTab[8] // get current position
if (e.keyCode === 37 && pos > 1) {
this.setState({
activeTab: "Section " + (pos - 1)
});
} else if (e.keyCode === 39 && pos < cnt) {
this.setState({
activeTab: "Section " + (pos + 1)
});
}
}
and then where className is set change cursor === i || activeTab === label ? "active" : null to just activeTab === label ? "active" : null
edit: This way is a little cleaner, in case you plan on changing section titles, which I imagine you might.
handleKeyDown(e) {
const labels = this.props.children.map((child) => {
return child.props.label;
});
const cnt = labels.length;
const pos = labels.indexOf(this.state.activeTab); // get current position
if (e.keyCode === 37 && pos > 0) {
this.setState({
activeTab: labels[pos - 1]
});
} else if (e.keyCode === 39 && pos < cnt - 1) {
this.setState({
activeTab: labels[pos + 1]
});
}
}
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 componentsTab is a simple reuseable component that requires a title and childrenactiveTab state and compared against a mapped key (this can be easily changed/implemented for a unique id)prevProps.children.length to determine tab length in the setState callbackTabPane'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"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);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With