From 0eb43e50f6eaf0897e1fe0e5e61cb267b8df35fc Mon Sep 17 00:00:00 2001 From: mzabriskie Date: Mon, 10 Aug 2015 18:39:04 -0600 Subject: [PATCH] [added] Support for disabling tabs --- examples/basic/app.js | 6 ++- lib/components/Tab.js | 2 + lib/components/Tabs.js | 75 +++++++++++++++++++++------ lib/components/__tests__/Tabs-test.js | 11 ++++ lib/helpers/styles.js | 5 ++ 5 files changed, 83 insertions(+), 16 deletions(-) diff --git a/examples/basic/app.js b/examples/basic/app.js index 1ac738e218..fd4afb177d 100644 --- a/examples/basic/app.js +++ b/examples/basic/app.js @@ -60,7 +60,8 @@ const App = React.createClass({ Mario - Luigi + Luigi + Peach Yoshi @@ -72,6 +73,9 @@ const App = React.createClass({

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.

Source: Wikipedia

+ +

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.

+

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.

Source: Wikipedia

diff --git a/lib/components/Tab.js b/lib/components/Tab.js index 67b38414cd..6a120c3989 100644 --- a/lib/components/Tab.js +++ b/lib/components/Tab.js @@ -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, @@ -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} diff --git a/lib/components/Tabs.js b/lib/components/Tabs.js index 5868da5fa5..8fb7120e52 100644 --- a/lib/components/Tabs.js +++ b/lib/components/Tabs.js @@ -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', @@ -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); @@ -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 @@ -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) : diff --git a/lib/components/__tests__/Tabs-test.js b/lib/components/__tests__/Tabs-test.js index 189cab52cd..0a91338234 100644 --- a/lib/components/__tests__/Tabs-test.js +++ b/lib/components/__tests__/Tabs-test.js @@ -14,10 +14,12 @@ function createTabs(props = {}) { Foo Bar Baz + Qux Hello Foo Hello Bar Hello Baz + Hello Qux
); } @@ -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'); }); }); @@ -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 () { diff --git a/lib/helpers/styles.js b/lib/helpers/styles.js index 6e60dfbbb9..5769d56410 100644 --- a/lib/helpers/styles.js +++ b/lib/helpers/styles.js @@ -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%)',