Skip to content

Commit

Permalink
[added] Support for disabling tabs
Browse files Browse the repository at this point in the history
  • Loading branch information
mzabriskie committed Aug 11, 2015
1 parent 8a3b74a commit 0eb43e5
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 16 deletions.
6 changes: 5 additions & 1 deletion examples/basic/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ const App = React.createClass({
<Tabs>
<TabList>
<Tab>Mario</Tab>
<Tab>Luigi</Tab>
<Tab disabled={true}>Luigi</Tab>
<Tab>Peach</Tab>
<Tab>Yoshi</Tab>
</TabList>

Expand All @@ -72,6 +73,9 @@ const App = React.createClass({
<p>Luigi (Japanese: ルイージ Hepburn: Ruīji?) is a fictional character featured in video games and related media released by Nintendo. Created by prominent game designer Shigeru Miyamoto, Luigi is portrayed as the slightly younger but taller fraternal twin brother of Nintendo's mascot Mario, and appears in many games throughout the Mario franchise, frequently as a sidekick to his brother.</p>
<p>Source: <a href="http://en.wikipedia.org/wiki/Luigi" target="_blank">Wikipedia</a></p>
</TabPanel>
<TabPanel>
<p>Princess Peach (Japanese: ピーチ姫 Hepburn: Pīchi-hime?) is a character in Nintendo's Mario franchise. Originally created by Shigeru Miyamoto, Peach is the princess of the fictional Mushroom Kingdom, which is constantly under attack by Bowser. She often plays the damsel in distress role within the series and is the lead female.[1] She is often portrayed as Mario's love interest and has appeared in nearly all the Mario games to date with the notable exception of Super Princess Peach, where she is the main playable character.</p>
</TabPanel>
<TabPanel>
<p>Yoshi (ヨッシー Yosshī?) /ˈjoʊʃi/ or /ˈjɒʃi/, once romanized as Yossy, is a fictional anthropomorphic dinosaur (referred to as a dragon at times) who appears in video games published by Nintendo. He debuted in Super Mario World (1990) on the Super Nintendo Entertainment System as Mario and Luigi's sidekick (a role he has often reprised), and he later established his own series with several platform and puzzle games, including Super Mario World 2: Yoshi's Island. He has also appeared in many of the spin-off Mario games including the Mario Party, the Mario Kart, and the Super Smash Bros. series, as well as in other various Mario sports titles. Yoshi also appears in New Super Mario Bros. Wii (2009) as the characters' companion and steed, similar to his original debut role in Super Mario World. Yoshi belongs to the species of the same name which comes in various colors, with green being the most common.</p>
<p>Source: <a href="http://en.wikipedia.org/wiki/Yoshi" target="_blank">Wikipedia</a></p>
Expand Down
2 changes: 2 additions & 0 deletions lib/components/Tab.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module.exports = React.createClass({
propTypes: {
id: PropTypes.string,
selected: PropTypes.bool,
disabled: PropTypes.bool,
panelId: PropTypes.string,
children: PropTypes.oneOfType([
PropTypes.object,
Expand Down Expand Up @@ -51,6 +52,7 @@ module.exports = React.createClass({
id={this.props.id}
aria-selected={this.props.selected ? 'true' : 'false'}
aria-expanded={this.props.selected ? 'true' : 'false'}
aria-disabled={this.props.disabled ? 'true' : 'false'}
aria-controls={this.props.panelId}
>
{this.props.children}
Expand Down
75 changes: 60 additions & 15 deletions lib/components/Tabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ function isTabNode(node) {
return node.nodeName === 'LI' && node.getAttribute('role') === 'tab';
}

// Determine if a tab node is disabled
function isTabDisabled(node) {
return node.getAttribute('aria-disabled') === 'true';
}

module.exports = React.createClass({
displayName: 'Tabs',

Expand Down Expand Up @@ -42,8 +47,12 @@ module.exports = React.createClass({
let node = e.target;
do {
if (isTabNode(node)) {
const index = [].slice.call(node.parentNode.children).indexOf(node);
this.setSelected(index);
if (isTabDisabled(node)) {
return;
}

const index = [].slice.call(node.parentNode.children).indexOf(node);
this.setSelected(index);
return;
}
} while ((node = node.parentNode) !== null);
Expand All @@ -52,29 +61,18 @@ module.exports = React.createClass({
handleKeyDown(e) {
if (isTabNode(e.target)) {
let index = this.state.selectedIndex;
const max = this.getTabsCount() - 1;
let preventDefault = false;

// Select next tab to the left
if (e.keyCode === 37 || e.keyCode === 38) {
index -= 1;
index = this.getPrevTab(index);
preventDefault = true;

// Wrap back to last tab if index is negative
if (index < 0) {
index = max;
}
}
// Select next tab to the right
/* eslint brace-style:0 */
else if (e.keyCode === 39 || e.keyCode === 40) {
index += 1;
index = this.getNextTab(index);
preventDefault = true;

// Wrap back to first tab if index exceeds max
if (index > max) {
index = 0;
}
}

// This prevents scrollbars from moving around
Expand Down Expand Up @@ -104,6 +102,53 @@ module.exports = React.createClass({
}
},

getNextTab(index) {
const count = this.getTabsCount();

// Look for non-disabled tab from index to the last tab on the right
for (let i = index + 1; i < count; i++) {
const tab = this.getTab(i);
if (!isTabDisabled(tab.getDOMNode())) {
return i;
}
}

// If no tab found, continue searching from first on left to index
for (let i = 0; i < index; i++) {
const tab = this.getTab(i);
if (!isTabDisabled(tab.getDOMNode())) {
return i;
}
}

// No tabs are disabled, return index
return index;
},

getPrevTab(index) {
let i = index;

// Look for non-disabled tab from index to first tab on the left
while (i--) {
const tab = this.getTab(i);
if (!isTabDisabled(tab.getDOMNode())) {
return i;
}
}

// If no tab found, continue searching from last tab on right to index
i = this.getTabsCount();
while (i-- > index) {
const tab = this.getTab(i);
if (!isTabDisabled(tab.getDOMNode())) {
return i;
}
}

// No tabs are disabled, return index
return index;
},

getTabsCount() {
return this.props.children && this.props.children[0] ?
React.Children.count(this.props.children[0].props.children) :
Expand Down
11 changes: 11 additions & 0 deletions lib/components/__tests__/Tabs-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ function createTabs(props = {}) {
<Tab>Foo</Tab>
<Tab>Bar</Tab>
<Tab><a>Baz</a></Tab>
<Tab disabled={true}>Qux</Tab>
</TabList>
<TabPanel>Hello Foo</TabPanel>
<TabPanel>Hello Bar</TabPanel>
<TabPanel>Hello Baz</TabPanel>
<TabPanel>Hello Qux</TabPanel>
</Tabs>
);
}
Expand Down Expand Up @@ -74,6 +76,8 @@ describe('react-tabs', function() {
equal(tab.getAttribute('aria-controls'), panel.getAttribute('id'));
equal(panel.getAttribute('aria-labeledby'), tab.getAttribute('id'));
}

equal(tabs.getTab(3).getDOMNode().getAttribute('aria-disabled'), 'true');
});
});

Expand All @@ -92,6 +96,13 @@ describe('react-tabs', function() {
assertTabSelected(tabs, 2);
});

it('should not change selectedIndex when clicking a disabled tab', function() {
const tabs = TestUtils.renderIntoDocument(createTabs({selectedIndex: 0}));

TestUtils.Simulate.click(tabs.getTab(3).getDOMNode());
assertTabSelected(tabs, 0);
});

// TODO: Can't seem to make this fail when removing fix :`(
// See https://github.com/mzabriskie/react-tabs/pull/7
// it('should preserve selectedIndex when typing', function () {
Expand Down
5 changes: 5 additions & 0 deletions lib/helpers/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ module.exports = {
'-webkit-border-radius': '5px 5px 0 0'
},

'.react-tabs [role=tab][aria-disabled=true]': {
'color': 'GrayText',
'cursor': 'default'
},

'.react-tabs [role=tab]:focus': {
'box-shadow': '0 0 5px hsl(208, 99%, 50%)',
'border-color': 'hsl(208, 99%, 50%)',
Expand Down

0 comments on commit 0eb43e5

Please sign in to comment.