├── .babelrc ├── .codeclimate.yml ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── dist ├── functions.js ├── index.js ├── tabs.js └── tabsKeyboardNavigationMixin.js ├── example ├── index.html ├── javascripts │ ├── out │ │ └── example.js │ ├── scale.fix.js │ └── src │ │ └── example.jsx └── stylesheets │ ├── github-light.css │ └── styles.css ├── gulpfile.js ├── config │ └── lint.js ├── index.js └── tasks │ ├── buildExample.js │ └── lint.js ├── package.json ├── readme.md ├── src ├── __test__ │ ├── functions-test.js │ ├── index-test.js │ └── tabs-test.js ├── functions.js ├── index.js ├── tabs.js └── tabsKeyboardNavigationMixin.js └── tests ├── compiler.js └── testdom.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "es2015" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | languages: 2 | JavaScript: true 3 | engines: 4 | eslint: 5 | enabled: true 6 | ratings: 7 | paths: 8 | - src/** 9 | - gulpfile.js/** 10 | exclude_paths: 11 | - dist/**/* 12 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "react/display-name": 2, 4 | "react/forbid-prop-types": 0, 5 | "react/jsx-boolean-value": 2, 6 | "react/jsx-curly-spacing": 2, 7 | "react/jsx-indent-props": [2, 2], 8 | "react/jsx-max-props-per-line": [2, {"maximum": 2}], 9 | "react/jsx-no-duplicate-props": 2, 10 | "react/jsx-no-undef": 2, 11 | "react/jsx-sort-prop-types": 2, 12 | "react/jsx-sort-props": 2, 13 | "react/jsx-uses-react": 2, 14 | "react/jsx-uses-vars": 2, 15 | "react/no-did-mount-set-state": 2, 16 | "react/no-did-update-set-state": 2, 17 | "react/no-multi-comp": 2, 18 | "react/no-unknown-property": 2, 19 | "react/prop-types": 2, 20 | "react/react-in-jsx-scope": 2, 21 | "react/self-closing-comp": 2, 22 | "react/sort-comp": 2, 23 | "react/wrap-multilines": 2 24 | }, 25 | "env": { 26 | "es6": true, 27 | "browser": true, 28 | "mocha": true 29 | }, 30 | "ecmaFeatures": { 31 | "jsx": true, 32 | "modules": true 33 | }, 34 | "plugins": [ 35 | "react", 36 | "standard" 37 | ], 38 | "extends": "standard" 39 | } 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /*.log 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /*.log 2 | /src 3 | /tests 4 | /.babelrc 5 | /.eslintrc 6 | /example 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4.2.2' 4 | before_script: 5 | - npm install -g gulp 6 | script: 7 | - gulp lint 8 | - npm test 9 | 10 | 11 | notifications: 12 | slack: interactivebcn:4pvhG2LJekisUI9Td2XYuDqD 13 | email: false 14 | env: 15 | global: 16 | secure: rtGXseZNM9ATFZb3nyZUvVHUr/aJpDx+kF9pT12iImuKRLX4cHnKpoLP8ZPN/doPgrEgg63ppxquQulb3uFuP1C1YTFFC3xslingBRkkY6hk4knIbTRozxgAodnPYGuVsvinbwmYenj79U5l8aMpzuH7YLrDI0IQq9BAPV9fUIOj/UmRCSIDaFToyB/a+FeNRPjeHuEpWTv4PyTMsuVoiDHC9hLZFzCvfzfgvVQ7SLtAKnAInHNuJ8jBODjjUIfhdTt9esPAZ2KyZ8yFlmSEcCX6t8VjZZGMzNDAdFOqF4gzy2GtKbhtWVUTgpe8tS62weroNow6ZTsWC1Tra6/Zp5Yr+31RNE17yR8se0SWFbHLNhznPPw/1O43P88/hRgRbklk5+AmBl+ZsRhSIFt8dTeAi3DUNLPVKMAaXMnIpKjn+/kfiRa+/U17BJKShxtlhmXa/50J8NWTX7kjpGGtpvPCIP6yMk3vKwaPQQ7ftNW5UqjO3ejfxD6aAY5uiTnekKFCtkCw3ZNZkELaEteTjynCmWkmT07DMxuqvc+AsIOkHHA11s/fhZOJ2zZjnu8BS/sqJvqYRcvrIdsMwFsvG2R/YerauZbJ9OXqHkkev2wy5Tes+j0KD5zFq07mYDrhBHLIkO4Jub5pmp+hgnpMWKZMKfs3SARJe2ylWwzDPKg= 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | bug: did not scale correctly in some wierd cases 2 | -------------------------------------------------------------------------------- /dist/functions.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | // Check if this is a function 5 | 6 | function isFunction(functionToCheck) { 7 | var getType = {}; 8 | return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]'; 9 | } 10 | 11 | // Checks if the element is a funciton or a renderable element and render both 12 | function renderFunction(node) { 13 | if (node) { 14 | if (isFunction(node)) { 15 | return node(); 16 | } else { 17 | return node; 18 | } 19 | } 20 | } 21 | 22 | module.exports = { 23 | isFunction: isFunction, 24 | renderFunction: renderFunction 25 | }; -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var React = require('react'); 5 | var ReactDom = require('react-dom'); 6 | 7 | var Tabs = require('./tabs.js'); 8 | var renderFunction = require('./functions.js').renderFunction; 9 | var ResizeSensor = require('css-element-queries/src/ResizeSensor'); 10 | 11 | module.exports = React.createClass({ 12 | displayName: 'tabsNavigationMenu', 13 | propTypes: { 14 | banner: React.PropTypes.shape({ // Banner content (optional) 15 | children: React.PropTypes.oneOfType([// Tab initialy selected 16 | React.PropTypes.func, React.PropTypes.node]) 17 | }), 18 | color: React.PropTypes.string, 19 | fixOffset: React.PropTypes.number, 20 | lineStyle: React.PropTypes.object, 21 | onTabChange: React.PropTypes.func, 22 | selected: React.PropTypes.oneOfType([// Tab initialy selected 23 | React.PropTypes.string, React.PropTypes.number]), 24 | selectedTabStyle: React.PropTypes.object, 25 | tabs: React.PropTypes.arrayOf(React.PropTypes.shape({ 26 | children: React.PropTypes.oneOfType([// Tab initialy selected 27 | React.PropTypes.func, React.PropTypes.node]), 28 | displayName: React.PropTypes.string.isRequired 29 | })), 30 | tabsBarClassName: React.PropTypes.string, 31 | tabsBarStyle: React.PropTypes.object, 32 | tabsClassName: React.PropTypes.string, 33 | tabsStyle: React.PropTypes.object 34 | }, 35 | getDefaultProps: function getDefaultProps() { 36 | return { 37 | fixOffset: 0, 38 | prev: 'Next', 39 | views: [] 40 | }; 41 | }, 42 | getInitialState: function getInitialState() { 43 | return { 44 | selectedTab: this.props.selected || 0, 45 | width: 300 46 | }; 47 | }, 48 | componentDidMount: function componentDidMount() { 49 | var element = ReactDom.findDOMNode(this.refs.tabsContainer); 50 | new ResizeSensor(element, this.calculateWidth); // eslint-disable-line 51 | this.calculateWidth(); 52 | }, 53 | componentWillReceiveProps: function componentWillReceiveProps(nextProps) { 54 | if (typeof nextProps.selected !== 'undefined') { 55 | if (nextProps.selected !== this.props.selected) { 56 | this.setState({ 57 | selectedTab: nextProps.selected 58 | }); 59 | } 60 | } 61 | }, 62 | componentWillUnmount: function componentWillUnmount() { 63 | var element = ReactDom.findDOMNode(this.refs.tabsContainer); 64 | ResizeSensor.detach(element); 65 | }, 66 | // Public method 67 | changeSelectedTab: function changeSelectedTab(i) { 68 | this.handleTabChange(i); 69 | }, 70 | calculateWidth: function calculateWidth() { 71 | this.setState({ 72 | width: ReactDom.findDOMNode(this.refs.tabsContainer).clientWidth 73 | }); 74 | }, 75 | handleTabChange: function handleTabChange(i) { 76 | var result = void 0; 77 | 78 | if (this.props.onTabChange) { 79 | result = this.props.onTabChange(i); 80 | } 81 | 82 | if (result !== false) { 83 | this.setState({ 84 | selectedTab: i 85 | }); 86 | } 87 | }, 88 | render: function render() { 89 | return React.createElement( 90 | 'div', 91 | { role: 'application' }, 92 | React.createElement( 93 | 'div', 94 | null, 95 | renderFunction(this.props.banner && this.props.banner.children) 96 | ), 97 | React.createElement( 98 | 'div', 99 | { ref: 'tabsContainer' }, 100 | React.createElement(Tabs, { 101 | clic: this.handleTabChange, 102 | color: this.props.color, 103 | elements: this.props.tabs.map(function (item) { 104 | return item.displayName; 105 | }), 106 | fixOffset: this.props.fixOffset, 107 | handleTabChange: this.handleTabChange, 108 | lineStyle: this.props.lineStyle, 109 | selected: this.state.selectedTab, 110 | selectedTabStyle: this.props.selectedTabStyle, 111 | tabsBarClassName: this.props.tabsBarClassName, 112 | tabsBarStyle: this.props.tabsBarStyle, 113 | tabsClassName: this.props.tabsClassName, 114 | tabsContainer: this.refs.tabsContainer, 115 | tabsStyle: this.props.tabsStyle, 116 | widthB: this.state.width 117 | }) 118 | ), 119 | React.createElement( 120 | 'div', 121 | { role: 'tabpanel' }, 122 | renderFunction(this.props.tabs[this.state.selectedTab] && this.props.tabs[this.state.selectedTab].children) 123 | ) 124 | ); 125 | } 126 | }); -------------------------------------------------------------------------------- /dist/tabs.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var Color = require('color'); 5 | var Radium = require('radium'); 6 | var React = require('react'); 7 | var ReactDom = require('react-dom'); 8 | 9 | var tabKeyMixin = require('./tabsKeyboardNavigationMixin.js'); 10 | 11 | var defaultColor = 'rgb(11, 104, 159)'; 12 | var defaultStyles = { 13 | color: defaultColor, 14 | lineStyle: { 15 | backgroundColor: defaultColor, 16 | height: 3, 17 | display: 'block', 18 | transition: 'margin-left 0.25s cubic-bezier(0.15, 0.48, 0.42, 1.13)' 19 | }, 20 | selectedTabStyle: { 21 | backgroundColor: Color(defaultColor).lighten(0.4).whiten(3.5).alpha(0.1).rgbaString(), 22 | outline: 'none' 23 | }, 24 | tabsBarStyle: { 25 | height: 55, 26 | backgroundColor: 'rgba(255, 255, 255, 0.96)', 27 | fontSize: 18 28 | }, 29 | tabsStyle: { 30 | height: '100%', 31 | paddingTop: 15, 32 | marginTop: 0, 33 | display: 'block', 34 | float: 'left', 35 | textAlign: 'center', 36 | cursor: 'pointer', 37 | WebkitUserSelect: 'none', 38 | MozUserSelect: 'none', 39 | msUserSelect: 'none', 40 | userSelect: 'none', 41 | boxSizing: 'border-box', 42 | ':focus': { 43 | boxShadow: 'inset 0 0 8px rgba(11, 104, 159, 0.3)' 44 | } 45 | } 46 | }; 47 | 48 | module.exports = Radium(React.createClass({ 49 | displayName: 'tabsNavigationMenu__tabs', 50 | propTypes: { 51 | clic: React.PropTypes.func, 52 | color: React.PropTypes.string, 53 | elements: React.PropTypes.arrayOf(React.PropTypes.string), 54 | fixOffset: React.PropTypes.number, 55 | lineStyle: React.PropTypes.object, 56 | selected: React.PropTypes.number, 57 | selectedTabStyle: React.PropTypes.object, 58 | tabsBarClassName: React.PropTypes.string, 59 | tabsBarStyle: React.PropTypes.object, 60 | tabsClassName: React.PropTypes.string, 61 | tabsContainer: React.PropTypes.any, 62 | tabsStyle: React.PropTypes.object, 63 | widthB: React.PropTypes.number 64 | }, 65 | mixins: [tabKeyMixin], 66 | getDefaultProps: function getDefaultProps() { 67 | return { 68 | clic: null, 69 | elements: ['tab1', 'tab2'], 70 | selected: 0, 71 | widthB: 300, 72 | tabsBarClassName: '', 73 | tabsClassName: '' 74 | }; 75 | }, 76 | getInitialState: function getInitialState() { 77 | return { 78 | menuFixed: false, 79 | focused: 0, 80 | focusedItem: this.props.selected 81 | }; 82 | }, 83 | componentDidMount: function componentDidMount() { 84 | window.addEventListener('scroll', this.handleElementScroll); 85 | }, 86 | componentWillUnmount: function componentWillUnmount() { 87 | window.removeEventListener('scroll', this.handleElementScroll); 88 | }, 89 | 90 | // We should handle scroll events in order to detect when the bar should be 91 | // fixed 92 | handleElementScroll: function handleElementScroll() { 93 | var top = ReactDom.findDOMNode(this.props.tabsContainer).offsetTop - this.props.fixOffset; 94 | if (window.scrollY > top) { 95 | this.setState({ 96 | menuFixed: true 97 | }); 98 | } else if (window.scrollY <= top) { 99 | this.setState({ 100 | menuFixed: false 101 | }); 102 | } 103 | }, 104 | 105 | // This modifies the styles defined by the user if a color is defined 106 | // But no color is defined inside the props styles 107 | // or if no height and paddingTop are defined 108 | styles: function styles() { 109 | var styles = { 110 | lineStyle: this.props.lineStyle || {}, 111 | selectedTabStyle: this.props.selectedTabStyle || defaultStyles.selectedTabStyle, 112 | tabsStyle: this.props.tabsStyle || {}, 113 | tabsBarStyle: this.props.tabsBarStyle || {} 114 | }; 115 | if (this.props.color) { 116 | if (!styles.lineStyle.color) { 117 | styles.lineStyle.color = this.props.color; 118 | } 119 | } 120 | 121 | if (!styles.tabsStyle[':hover']) { 122 | styles.tabsStyle[':hover'] = styles.selectedTabStyle; 123 | } 124 | 125 | if (!styles.tabsStyle[':focus']) { 126 | styles.tabsStyle[':focus'] = styles.selectedTabStyle; 127 | } 128 | 129 | if (!styles.selectedTabStyle.backgroundColor) { 130 | styles.selectedTabStyle.backgroundColor = defaultStyles.selectedTabStyle.backgroundColor; 131 | } 132 | 133 | return styles; 134 | }, 135 | 136 | // We handle the click event on our tab and send it to the parent 137 | handeClick: function handeClick(i) { 138 | if (this.props.clic) { 139 | this.props.clic(i); 140 | } 141 | }, 142 | 143 | render: function render() { 144 | var _this = this; 145 | 146 | var styles = this.styles(); // Gets the user styles for this element 147 | var filler = this.state.menuFixed ? React.createElement('div', { 148 | style: { 149 | height: (styles.tabsBarStyle.height || defaultStyles.tabsBarStyle.height || 0) + (styles.tabsBarStyle.paddingTop || defaultStyles.tabsBarStyle.paddingTop || 0) + (styles.tabsBarStyle.marginTop || defaultStyles.tabsBarStyle.marginTop || 0) 150 | } 151 | }) : null; 152 | 153 | var elementWidth = 1 / this.props.elements.length * 100; // in percentage 154 | 155 | var bar = { 156 | marginLeft: elementWidth * this.props.selected + '%', 157 | width: elementWidth + '%' 158 | }; 159 | 160 | var styleMenu = { 161 | top: this.state.menuFixed ? this.props.fixOffset : null, 162 | width: this.state.menuFixed ? this.props.widthB : null, 163 | position: this.state.menuFixed ? 'fixed' : null, 164 | zIndex: this.props.tabsBarStyle ? this.props.tabsBarStyle.zIndex : null 165 | }; 166 | 167 | // The different tabs 168 | var elements = this.props.elements.map(function (element, i) { 169 | var style = { 170 | width: elementWidth + '%' 171 | }; 172 | 173 | var tabStyles = [defaultStyles.tabsStyle, styles.tabsStyle]; 174 | 175 | var cssClass = _this.props.tabsClassName; 176 | if (_this.props.selected === i) { 177 | cssClass += ' is-selected'; 178 | tabStyles.push(defaultStyles.selectedTabStyle); 179 | tabStyles.push(styles.selectedTabStyle); 180 | } 181 | 182 | tabStyles.push(style); 183 | 184 | return React.createElement( 185 | 'span', 186 | { 187 | 'aria-expanded': _this.state.focusedItem === i, 188 | 'aria-selected': _this.state.focused > 0 ? _this.props.selected === i : false, 189 | className: cssClass, 190 | key: i, 191 | onBlur: _this.handleBlur.bind(_this, i), 192 | onClick: _this.handeClick.bind(_this, i), 193 | onFocus: _this.handleFocus.bind(_this, i), 194 | ref: 'tab-' + i, 195 | role: 'tab', 196 | style: tabStyles, 197 | tabIndex: _this.props.selected === i ? 0 : -1 }, 198 | element 199 | ); 200 | }); 201 | 202 | return React.createElement( 203 | 'div', 204 | { ref: 'bar' }, 205 | React.createElement( 206 | 'div', 207 | null, 208 | filler 209 | ), 210 | React.createElement( 211 | 'div', 212 | { style: styleMenu }, 213 | React.createElement( 214 | 'nav', 215 | { 216 | className: this.props.tabsBarClassName, 217 | 'aria-multiselectable': 'false', 218 | role: 'tablist', 219 | style: [defaultStyles.tabsBarStyle, styles.tabsBarStyle] }, 220 | elements 221 | ), 222 | React.createElement('span', { style: [defaultStyles.lineStyle, styles.lineStyle, bar] }) 223 | ) 224 | ); 225 | } 226 | })); -------------------------------------------------------------------------------- /dist/tabsKeyboardNavigationMixin.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var KEYS = { 5 | enter: 13, 6 | left: 37, 7 | right: 39, 8 | space: 32, 9 | tab: 9, 10 | home: 36, 11 | end: 35 12 | }; 13 | 14 | var KeyboardShortcutsMixin = { 15 | handleKeyPress: function handleKeyPress(event) { 16 | if (this.state.focused) { 17 | if (event.which === KEYS.space || event.which === KEYS.enter) { 18 | this.handleEnterPress(); 19 | } else if (event.which === KEYS.right) { 20 | this.handleRightPress(); 21 | } else if (event.which === KEYS.left) { 22 | this.handleLeftPress(); 23 | } else if (event.which === KEYS.home) { 24 | this.handleHomePress(); 25 | } else if (event.which === KEYS.end) { 26 | this.handleEndPress(); 27 | } 28 | } 29 | }, 30 | 31 | handleHomePress: function handleHomePress() { 32 | this.refs['tab-' + 0].focus(); 33 | }, 34 | 35 | handleEndPress: function handleEndPress() { 36 | this.refs['tab-' + (this.props.elements.length - 1)].focus(); 37 | }, 38 | 39 | handleEnterPress: function handleEnterPress() { 40 | this.props.handleTabChange(this.state.focusedItem); 41 | }, 42 | 43 | handleRightPress: function handleRightPress() { 44 | if (this.state.focusedItem < this.props.elements.length - 1) { 45 | this.refs['tab-' + (this.state.focusedItem + 1)].focus(); 46 | } 47 | }, 48 | 49 | handleLeftPress: function handleLeftPress() { 50 | if (this.state.focusedItem > 0) { 51 | this.refs['tab-' + (this.state.focusedItem - 1)].focus(); 52 | } 53 | }, 54 | 55 | handleFocus: function handleFocus(i, event) { 56 | this.setState({ 57 | focused: this.state.focused + 1, 58 | focusedItem: i 59 | }); 60 | }, 61 | 62 | handleBlur: function handleBlur(i, event) { 63 | this.setState({ 64 | focused: this.state.focused - 1 65 | }); 66 | }, 67 | 68 | componentDidMount: function componentDidMount() { 69 | document.addEventListener('keyup', this.handleKeyPress); 70 | }, 71 | 72 | componentWillUnmount: function componentWillUnmount() { 73 | document.removeEventListener('keyup', this.handleKeyPress); 74 | } 75 | }; 76 | 77 | module.exports = KeyboardShortcutsMixin; -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React-tabs-navigation by pepjo 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 |
17 |
18 |

React-tabs-navigation

19 |

react-tabs-navigation is a nice react component that enables navigation through tabs in your web app.

20 | 21 |

View the Project on GitHub pepjo/react-tabs-navigation

22 | 23 | 24 | 29 |
30 |
31 |

Example

32 |
33 | 34 | 38 |
39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /example/javascripts/scale.fix.js: -------------------------------------------------------------------------------- 1 | var metas = document.getElementsByTagName('meta'); 2 | var i; 3 | if (navigator.userAgent.match(/iPhone/i)) { 4 | for (i=0; i ( // eslint-disable-line 17 |
18 |

Documentation and examples

19 |

See github: 20 | 21 | {'https://github.com/pepjo/react-tabs-navigation'} 22 | 23 |

24 |
25 | ), 26 | displayName: 'Docs' 27 | }, 28 | { 29 | // Second tab 30 | children: () => ( // eslint-disable-line 31 |
32 |

33 | 40 | MIT License 41 |

42 |

{'Copyright (c) 2015, Pep Rodeja'}

43 | 44 |

{'Permission is hereby granted, free of charge, to any person obtaining a copy ' + 45 | 'of this software and associated documentation files (the "Software"), to deal ' + 46 | 'in the Software without restriction, including without limitation the rights ' + 47 | 'to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ' + 48 | 'copies of the Software, and to permit persons to whom the Software is ' + 49 | 'furnished to do so, subject to the following conditions:'}

50 | 51 |

{'The above copyright notice and this permission notice shall be included in ' + 52 | 'all copies or substantial portions of the Software.'}

53 | 54 |

{'THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ' + 55 | 'IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ' + 56 | 'FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ' + 57 | 'AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ' + 58 | 'LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ' + 59 | 'OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN ' + 60 | 'THE SOFTWARE.'}

61 |
62 | ), 63 | displayName: 'License' 64 | }, 65 | { 66 | // Third tab 67 | children: () => ( // eslint-disable-line 68 |
69 |

70 |
71 | Use tab to select the tabs and arrows (or home and end) to move arround 72 |
73 |
74 | 80 |
81 |

82 | 144 |
145 | ), 146 | displayName: 'Scroll' 147 | } 148 | ]} 149 | tabsBarStyle={{backgroundColor: 'rgba(242, 250, 255, 0.95)'}} 150 | /> 151 | ) 152 | 153 | let mountNode = document.getElementById('react-container') 154 | 155 | let mountedComponent = ReactDOM.render(component, mountNode) 156 | -------------------------------------------------------------------------------- /example/stylesheets/github-light.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 GitHub Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | */ 17 | 18 | .pl-c /* comment */ { 19 | color: #969896; 20 | } 21 | 22 | .pl-c1 /* constant, markup.raw, meta.diff.header, meta.module-reference, meta.property-name, support, support.constant, support.variable, variable.other.constant */, 23 | .pl-s .pl-v /* string variable */ { 24 | color: #0086b3; 25 | } 26 | 27 | .pl-e /* entity */, 28 | .pl-en /* entity.name */ { 29 | color: #795da3; 30 | } 31 | 32 | .pl-s .pl-s1 /* string source */, 33 | .pl-smi /* storage.modifier.import, storage.modifier.package, storage.type.java, variable.other, variable.parameter.function */ { 34 | color: #333; 35 | } 36 | 37 | .pl-ent /* entity.name.tag */ { 38 | color: #63a35c; 39 | } 40 | 41 | .pl-k /* keyword, storage, storage.type */ { 42 | color: #a71d5d; 43 | } 44 | 45 | .pl-pds /* punctuation.definition.string, string.regexp.character-class */, 46 | .pl-s /* string */, 47 | .pl-s .pl-pse .pl-s1 /* string punctuation.section.embedded source */, 48 | .pl-sr /* string.regexp */, 49 | .pl-sr .pl-cce /* string.regexp constant.character.escape */, 50 | .pl-sr .pl-sra /* string.regexp string.regexp.arbitrary-repitition */, 51 | .pl-sr .pl-sre /* string.regexp source.ruby.embedded */ { 52 | color: #183691; 53 | } 54 | 55 | .pl-v /* variable */ { 56 | color: #ed6a43; 57 | } 58 | 59 | .pl-id /* invalid.deprecated */ { 60 | color: #b52a1d; 61 | } 62 | 63 | .pl-ii /* invalid.illegal */ { 64 | background-color: #b52a1d; 65 | color: #f8f8f8; 66 | } 67 | 68 | .pl-sr .pl-cce /* string.regexp constant.character.escape */ { 69 | color: #63a35c; 70 | font-weight: bold; 71 | } 72 | 73 | .pl-ml /* markup.list */ { 74 | color: #693a17; 75 | } 76 | 77 | .pl-mh /* markup.heading */, 78 | .pl-mh .pl-en /* markup.heading entity.name */, 79 | .pl-ms /* meta.separator */ { 80 | color: #1d3e81; 81 | font-weight: bold; 82 | } 83 | 84 | .pl-mq /* markup.quote */ { 85 | color: #008080; 86 | } 87 | 88 | .pl-mi /* markup.italic */ { 89 | color: #333; 90 | font-style: italic; 91 | } 92 | 93 | .pl-mb /* markup.bold */ { 94 | color: #333; 95 | font-weight: bold; 96 | } 97 | 98 | .pl-md /* markup.deleted, meta.diff.header.from-file */ { 99 | background-color: #ffecec; 100 | color: #bd2c00; 101 | } 102 | 103 | .pl-mi1 /* markup.inserted, meta.diff.header.to-file */ { 104 | background-color: #eaffea; 105 | color: #55a532; 106 | } 107 | 108 | .pl-mdr /* meta.diff.range */ { 109 | color: #795da3; 110 | font-weight: bold; 111 | } 112 | 113 | .pl-mo /* meta.output */ { 114 | color: #1d3e81; 115 | } 116 | 117 | -------------------------------------------------------------------------------- /example/stylesheets/styles.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Noto Sans'; 3 | font-weight: 400; 4 | font-style: normal; 5 | src: url('../fonts/Noto-Sans-regular/Noto-Sans-regular.eot'); 6 | src: url('../fonts/Noto-Sans-regular/Noto-Sans-regular.eot?#iefix') format('embedded-opentype'), 7 | local('Noto Sans'), 8 | local('Noto-Sans-regular'), 9 | url('../fonts/Noto-Sans-regular/Noto-Sans-regular.woff2') format('woff2'), 10 | url('../fonts/Noto-Sans-regular/Noto-Sans-regular.woff') format('woff'), 11 | url('../fonts/Noto-Sans-regular/Noto-Sans-regular.ttf') format('truetype'), 12 | url('../fonts/Noto-Sans-regular/Noto-Sans-regular.svg#NotoSans') format('svg'); 13 | } 14 | 15 | @font-face { 16 | font-family: 'Noto Sans'; 17 | font-weight: 700; 18 | font-style: normal; 19 | src: url('../fonts/Noto-Sans-700/Noto-Sans-700.eot'); 20 | src: url('../fonts/Noto-Sans-700/Noto-Sans-700.eot?#iefix') format('embedded-opentype'), 21 | local('Noto Sans Bold'), 22 | local('Noto-Sans-700'), 23 | url('../fonts/Noto-Sans-700/Noto-Sans-700.woff2') format('woff2'), 24 | url('../fonts/Noto-Sans-700/Noto-Sans-700.woff') format('woff'), 25 | url('../fonts/Noto-Sans-700/Noto-Sans-700.ttf') format('truetype'), 26 | url('../fonts/Noto-Sans-700/Noto-Sans-700.svg#NotoSans') format('svg'); 27 | } 28 | 29 | @font-face { 30 | font-family: 'Noto Sans'; 31 | font-weight: 400; 32 | font-style: italic; 33 | src: url('../fonts/Noto-Sans-italic/Noto-Sans-italic.eot'); 34 | src: url('../fonts/Noto-Sans-italic/Noto-Sans-italic.eot?#iefix') format('embedded-opentype'), 35 | local('Noto Sans Italic'), 36 | local('Noto-Sans-italic'), 37 | url('../fonts/Noto-Sans-italic/Noto-Sans-italic.woff2') format('woff2'), 38 | url('../fonts/Noto-Sans-italic/Noto-Sans-italic.woff') format('woff'), 39 | url('../fonts/Noto-Sans-italic/Noto-Sans-italic.ttf') format('truetype'), 40 | url('../fonts/Noto-Sans-italic/Noto-Sans-italic.svg#NotoSans') format('svg'); 41 | } 42 | 43 | @font-face { 44 | font-family: 'Noto Sans'; 45 | font-weight: 700; 46 | font-style: italic; 47 | src: url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.eot'); 48 | src: url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.eot?#iefix') format('embedded-opentype'), 49 | local('Noto Sans Bold Italic'), 50 | local('Noto-Sans-700italic'), 51 | url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.woff2') format('woff2'), 52 | url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.woff') format('woff'), 53 | url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.ttf') format('truetype'), 54 | url('../fonts/Noto-Sans-700italic/Noto-Sans-700italic.svg#NotoSans') format('svg'); 55 | } 56 | 57 | body { 58 | background-color: #fff; 59 | padding:50px; 60 | font: 14px/1.5 "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 61 | color:#727272; 62 | font-weight:400; 63 | } 64 | 65 | h1, h2, h3, h4, h5, h6 { 66 | color:#222; 67 | margin:0 0 20px; 68 | } 69 | 70 | p, ul, ol, table, pre, dl { 71 | margin:0 0 20px; 72 | } 73 | 74 | h1, h2, h3 { 75 | line-height:1.1; 76 | } 77 | 78 | h1 { 79 | font-size:28px; 80 | } 81 | 82 | h2 { 83 | color:#393939; 84 | } 85 | 86 | h3, h4, h5, h6 { 87 | color:#494949; 88 | } 89 | 90 | a { 91 | color:#39c; 92 | text-decoration:none; 93 | } 94 | 95 | a:hover { 96 | color:#069; 97 | } 98 | 99 | a small { 100 | font-size:11px; 101 | color:#777; 102 | margin-top:-0.3em; 103 | display:block; 104 | } 105 | 106 | a:hover small { 107 | color:#777; 108 | } 109 | 110 | .wrapper { 111 | width:860px; 112 | margin:0 auto; 113 | } 114 | 115 | blockquote { 116 | border-left:1px solid #e5e5e5; 117 | margin:0; 118 | padding:0 0 0 20px; 119 | font-style:italic; 120 | } 121 | 122 | code, pre { 123 | font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal, Consolas, Liberation Mono, DejaVu Sans Mono, Courier New, monospace; 124 | color:#333; 125 | font-size:12px; 126 | } 127 | 128 | pre { 129 | padding:8px 15px; 130 | background: #f8f8f8; 131 | border-radius:5px; 132 | border:1px solid #e5e5e5; 133 | overflow-x: auto; 134 | } 135 | 136 | table { 137 | width:100%; 138 | border-collapse:collapse; 139 | } 140 | 141 | th, td { 142 | text-align:left; 143 | padding:5px 10px; 144 | border-bottom:1px solid #e5e5e5; 145 | } 146 | 147 | dt { 148 | color:#444; 149 | font-weight:700; 150 | } 151 | 152 | th { 153 | color:#444; 154 | } 155 | 156 | img { 157 | max-width:100%; 158 | } 159 | 160 | header { 161 | width:270px; 162 | float:left; 163 | position:fixed; 164 | -webkit-font-smoothing:subpixel-antialiased; 165 | } 166 | 167 | header ul { 168 | list-style:none; 169 | height:40px; 170 | padding:0; 171 | background: #f4f4f4; 172 | border-radius:5px; 173 | border:1px solid #e0e0e0; 174 | width:270px; 175 | } 176 | 177 | header li { 178 | width:89px; 179 | float:left; 180 | border-right:1px solid #e0e0e0; 181 | height:40px; 182 | } 183 | 184 | header li:first-child a { 185 | border-radius:5px 0 0 5px; 186 | } 187 | 188 | header li:last-child a { 189 | border-radius:0 5px 5px 0; 190 | } 191 | 192 | header ul a { 193 | line-height:1; 194 | font-size:11px; 195 | color:#999; 196 | display:block; 197 | text-align:center; 198 | padding-top:6px; 199 | height:34px; 200 | } 201 | 202 | header ul a:hover { 203 | color:#999; 204 | } 205 | 206 | header ul a:active { 207 | background-color:#f0f0f0; 208 | } 209 | 210 | strong { 211 | color:#222; 212 | font-weight:700; 213 | } 214 | 215 | header ul li + li + li { 216 | border-right:none; 217 | width:89px; 218 | } 219 | 220 | header ul a strong { 221 | font-size:14px; 222 | display:block; 223 | color:#222; 224 | } 225 | 226 | section { 227 | width:500px; 228 | float:right; 229 | padding-bottom:50px; 230 | } 231 | 232 | small { 233 | font-size:11px; 234 | } 235 | 236 | hr { 237 | border:0; 238 | background:#e5e5e5; 239 | height:1px; 240 | margin:0 0 20px; 241 | } 242 | 243 | footer { 244 | width:270px; 245 | float:left; 246 | bottom:50px; 247 | -webkit-font-smoothing:subpixel-antialiased; 248 | } 249 | 250 | @media print, screen and (max-width: 960px) { 251 | 252 | div.wrapper { 253 | width:auto; 254 | margin:0; 255 | } 256 | 257 | header, section, footer { 258 | float:none; 259 | position:static; 260 | width:auto; 261 | } 262 | 263 | header { 264 | padding-right:320px; 265 | } 266 | 267 | section { 268 | border:1px solid #e5e5e5; 269 | border-width:1px 0; 270 | padding:20px 0; 271 | margin:0 0 20px; 272 | } 273 | 274 | header a small { 275 | display:inline; 276 | } 277 | 278 | header ul { 279 | position:absolute; 280 | right:50px; 281 | top:52px; 282 | } 283 | } 284 | 285 | @media print, screen and (max-width: 720px) { 286 | body { 287 | word-wrap:break-word; 288 | } 289 | 290 | header { 291 | padding:0; 292 | } 293 | 294 | header ul, header p.view { 295 | position:static; 296 | } 297 | 298 | pre, code { 299 | word-wrap:normal; 300 | } 301 | } 302 | 303 | @media print, screen and (max-width: 480px) { 304 | body { 305 | padding:15px; 306 | } 307 | 308 | header ul { 309 | width:99%; 310 | } 311 | 312 | header li, header ul li + li + li { 313 | width:33%; 314 | } 315 | } 316 | 317 | @media print { 318 | body { 319 | padding:0.4in; 320 | font-size:12pt; 321 | color:#444; 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /gulpfile.js/config/lint.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | paths: [ 3 | './**/*.js', 4 | './**/*.jsx', 5 | '!./dist/**', 6 | '!./example/**', 7 | '!./node_modules/**' 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /gulpfile.js/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | gulpfile.js 3 | =========== 4 | Rather than manage one giant configuration file responsible 5 | for creating multiple tasks, each task has been broken out into 6 | its own file in gulpfile.js/tasks. Any files in that directory get 7 | automatically required below. 8 | 9 | To add a new task, simply add a new task file that directory. 10 | gulpfile.js/tasks/default.js specifies the default set of tasks to run 11 | when you run `gulp`. 12 | */ 13 | 14 | var requireDir = require('require-dir') 15 | 16 | // Require all tasks in gulp/tasks, including subfolders 17 | requireDir('./tasks', { recurse: true }) 18 | -------------------------------------------------------------------------------- /gulpfile.js/tasks/buildExample.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp') 2 | 3 | var browserify = require('browserify') 4 | var watchify = require('watchify') 5 | var babelify = require('babelify') 6 | 7 | var source = require('vinyl-source-stream') 8 | var merge = require('utils-merge') 9 | 10 | /* nicer browserify errors */ 11 | var gutil = require('gulp-util') 12 | var chalk = require('chalk') 13 | 14 | var babelifyOptions = { 15 | presets: ['react'] 16 | } 17 | 18 | function map_error (err) { 19 | if (err.fileName) { 20 | // regular error 21 | gutil.log(chalk.red(err.name) + 22 | ': ' + 23 | chalk.yellow(err.fileName.replace(__dirname + '/javascripts/src/', '')) + 24 | ': ' + 25 | 'Line ' + 26 | chalk.magenta(err.lineNumber) + 27 | ' & ' + 28 | 'Column ' + 29 | chalk.magenta(err.columnNumber || err.column) + 30 | ': ' + 31 | chalk.blue(err.description)) 32 | } else { 33 | // browserify error.. 34 | gutil.log(chalk.red(err.name) + 35 | ': ' + 36 | chalk.yellow(err.message)) 37 | } 38 | } 39 | /* */ 40 | 41 | gulp.task('example-watchify', function () { 42 | var args = merge(watchify.args, { debug: true }) 43 | var bundler = watchify(browserify('./example/javascripts/src/example.jsx', args)).transform(babelify, babelifyOptions) 44 | bundle_js(bundler) 45 | 46 | bundler.on('update', function () { 47 | bundle_js(bundler) 48 | }) 49 | }) 50 | 51 | function bundle_js (bundler) { 52 | return bundler.bundle() 53 | .on('error', map_error) 54 | .pipe(source('example.js')) 55 | .pipe(gulp.dest('example/javascripts/out')) 56 | } 57 | 58 | // Without watchify 59 | gulp.task('example-browserify', function () { 60 | var bundler = browserify('./example/javascripts/src/example.jsx', { debug: true }).transform(babelify, babelifyOptions) 61 | 62 | return bundle_js(bundler) 63 | }) 64 | -------------------------------------------------------------------------------- /gulpfile.js/tasks/lint.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var gulp = require('gulp') 4 | var eslint = require('gulp-eslint') 5 | var paths = require('../config/lint.js').paths 6 | 7 | gulp.task('lint', function () { 8 | return gulp.src(paths) 9 | // eslint() attaches the lint output to the eslint property 10 | // of the file object so it can be used by other modules. 11 | .pipe(eslint()) 12 | // eslint.format() outputs the lint results to the console. 13 | // Alternatively use eslint.formatEach() (see Docs). 14 | .pipe(eslint.format()) 15 | // To have the process exit with an error code (1) on 16 | // lint error, return the stream and pipe to failOnError last. 17 | .pipe(eslint.failAfterError()) 18 | }) 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-tabs-navigation", 3 | "version": "0.4.4", 4 | "description": "react-tabs-navigation is a nice react component that enables navigation through tabs in your web app.", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "mocha --compilers .:tests/compiler.js src/**/*-test.js", 8 | "test-watch": "mocha --compilers .:tests/compiler.js --watch --reporter min src/**/*-test.js", 9 | "build": "babel --ignore *-test.js src --watch --out-dir dist" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/pepjo/react-tabs-navigation.git" 14 | }, 15 | "peerDependencies": { 16 | "react": "^0.14.0 || ^15.0.0", 17 | "react-dom": "^0.14.0 || ^15.0.0" 18 | }, 19 | "dependencies": { 20 | "radium": "^0.18.1", 21 | "color": "^0.10.1", 22 | "css-element-queries": "^0.3.2" 23 | }, 24 | "devDependencies": { 25 | "babel-cli": "6.1.18", 26 | "babel-preset-es2015": "6.1.18", 27 | "babel-preset-react": "6.1.18", 28 | "babelify": "^7.2.0", 29 | "browserify": "^12.0.1", 30 | "chai": "^3.4.1", 31 | "chalk": "^1.1.1", 32 | "eslint": "~1.7.x", 33 | "eslint-config-standard": "^4.3.1", 34 | "eslint-plugin-react": "^3.5.1", 35 | "eslint-plugin-standard": "^1.3.0", 36 | "gulp": "^3.8.7", 37 | "gulp-eslint": "^1.0.0", 38 | "gulp-rename": "^1.2.2", 39 | "gulp-sourcemaps": "^1.6.0", 40 | "gulp-uglify": "^1.5.1", 41 | "gulp-util": "^3.0.7", 42 | "jsdom": "^7.0.2", 43 | "mocha": "^2.3.4", 44 | "react": "^15.3.0", 45 | "react-addons-test-utils": "^15.3.0", 46 | "react-dom": "^15.3.0", 47 | "react-tools": "^0.13.3", 48 | "require-dir": "^0.3.0", 49 | "skin-deep": "^0.13.0", 50 | "utils-merge": "^1.0.0", 51 | "vinyl-buffer": "^1.0.0", 52 | "vinyl-source-stream": "~1.1.0", 53 | "watchify": "^3.6.1" 54 | }, 55 | "keywords": [ 56 | "react", 57 | "reactjs", 58 | "tabs", 59 | "navigation", 60 | "component", 61 | "element", 62 | "javascript" 63 | ], 64 | "author": "Pep Rodeja", 65 | "license": "MIT" 66 | } 67 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | react-tabs-navigation 2 | ===================== 3 | [![Build Status](https://travis-ci.org/pepjo/react-tabs-navigation.svg)](https://travis-ci.org/pepjo/react-tabs-navigation) [![Code Climate](https://codeclimate.com/github/pepjo/react-tabs-navigation/badges/gpa.svg)](https://codeclimate.com/github/pepjo/react-tabs-navigation) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 4 | 5 | [![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) 6 | 7 | This react component enables navigating through tabs in your web app. 8 | It is composed of three different parts: 9 | 10 | 1. Banner 11 | 12 | This content does not change with tabs and sits on top of the tabs bar. When the user scrolls past the banner the tabs bar stick on top of the page 13 | 14 | 2. Tabs 15 | 16 | The tabs of the element. They are horizontal. You can define their styles, hover styles and selected styles. 17 | 18 | The selected tab in underlined, this animates to the newly selected tab when a new tab is selected. 19 | 20 | 3. Content 21 | 22 | The content that changes when the user changes the tab. 23 | 24 | Installing 25 | ---------- 26 | 27 | ```bash 28 | $ npm install react-tabs-navigation 29 | ``` 30 | 31 | Live demo 32 | --------- 33 | 34 | Here: http://pepjo.github.io/react-tabs-navigation/ 35 | 36 | Props 37 | ----- 38 | 39 | This component accept the following props: 40 | 41 | * banner [object] 42 | 43 | content over the tab bar 44 | 45 | * children [func|node] 46 | 47 | a node or a function that returns a node (recommended) 48 | 49 | * color [string] 50 | 51 | main color (can be overridden on lineStyles and tabStyles) 52 | 53 | * fixOffset [number] 54 | 55 | The tabs bar fixes on the sreen when you scroll pass to it. 56 | If you want it to fix below the upper limit of the document set here the offset 57 | If you want it to not fix set the offset to at least -(the height of the bar) 58 | 59 | * lineStyle [object] 60 | 61 | Styles of the underline. 62 | Use `backgroundColor` to change the color and height to change the `width` (default 3px) of the line. 63 | (Accepts Radium properties like `:hover`) 64 | 65 | * onTabChange [func] 66 | 67 | Function that gets executed when a tab changes, first argument is the index of the tab. 68 | If you return `false` the tab will not change. Of course, you will still be 69 | able to change it changing the selectedTab prop. 70 | 71 | * selected [string|number] 72 | 73 | The index or the `keyName` of the tab selected initially 74 | 75 | * selectedTabStyle [object] 76 | 77 | The style of the tab when it is selected. 78 | (Accepts Radium properties like `:hover`) 79 | 80 | * **tabs** [array] -required- 81 | 82 | An array of objects, one for each tab 83 | 84 | * children [func|node] 85 | 86 | a node or a function that returns a node (recommended) 87 | 88 | * displayName [string] 89 | 90 | the name displayed on the tab 91 | 92 | * tabsBarClassName [string] 93 | 94 | className of the tabs bar element 95 | 96 | * tabsBarStyle [object] 97 | 98 | The style of the tabs bar 99 | 100 | * tabsClassName [string] 101 | 102 | className of each tab. When they are selected they also have the class `is-selected` 103 | 104 | * tabsStyle [object] 105 | 106 | The style of the tab. 107 | (Accepts Radium properties like `:hover`) 108 | 109 | Public Methods 110 | -------------- 111 | 112 | * `changeSelectedTab(indexTab)` to change the selected tab 113 | 114 | Simple example 115 | -------------- 116 | 117 | One of the simplest examples one could use 118 | 119 | ````javascript 120 | import Tabs from 'react-tabs-navigation' 121 | 122 | ( 129 |
130 | This is the first tab content 131 |
132 | ), 133 | displayName: 'Tab 1' 134 | }, 135 | { 136 | children: () => ( 137 |
138 | This is the second tab content 139 |
140 | ), 141 | displayName: 'Tab 2' 142 | } 143 | ]} 144 | /> 145 | ```` 146 | 147 | Full example 148 | ------------ 149 | 150 | A more complete example using more functionalities 151 | 152 | ````javascript 153 | import Tabs from 'react-tabs-navigation' 154 | 155 | ( 162 |
163 | This is the first tab content 164 |
165 | ), 166 | displayName: 'Tab 1' 167 | }, 168 | { 169 | children: () => ( 170 |
171 | This is the second tab content 172 |
173 | ), 174 | displayName: 'Tab 2' 175 | } 176 | ]} 177 | /> 178 | ```` 179 | 180 | To do list 181 | ---------- 182 | 183 | - [x] Use travis 184 | - [x] Write some tests 185 | - [ ] Optional animation when changing between tabs 186 | - [x] Keyboard navigation 187 | - [ ] Optional scroll behavior (see: [this issue] (https://github.com/pepjo/react-tabs-navigation/issues/2#issuecomment-167140069)) 188 | 189 | Contribute 190 | ------------ 191 | 192 | ### Getting Started 193 | 194 | * Submit a ticket for your issue on GitHub in [Repository issues](https://github.com/pepjo/react-tabs-navigation/issues) 195 | 196 | ### Making Changes 197 | We are following [Gitflow](http://nvie.com/posts/a-successful-git-branching-model/) workflow. 198 | 199 | * Create feature branch from `master` branch called `feature/{ISSUE}` where `{ISSUE}` is GitHub issue identifier e.g. `feature/123` 200 | * Make commits of logical units 201 | * Don't forget about tests! :) 202 | * Stick to code standards 203 | * Don't forget to build `$ npm run build` !! 204 | 205 | #### Improving the example 206 | 207 | In order to build the example source code you can use `gulp example-watchify` or `gulp example-browserify` depending on the desired behaviour. 208 | 209 | ### Submiting Changes 210 | 211 | 1. Push your branch to base repository 212 | 2. Submit a pull request to `master` branch 213 | 3. Wait for someone to review your changes and merge it 214 | 4. If your pull request is tagged as `To correct` you should fix your code as soon as possible and go back to point 3. 215 | 216 | MIT License 217 | ------------ 218 | 219 | Copyright (c) 2015, Pep Rodeja 220 | 221 | Permission is hereby granted, free of charge, to any person obtaining a copy 222 | of this software and associated documentation files (the "Software"), to deal 223 | in the Software without restriction, including without limitation the rights 224 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 225 | copies of the Software, and to permit persons to whom the Software is 226 | furnished to do so, subject to the following conditions: 227 | 228 | The above copyright notice and this permission notice shall be included in 229 | all copies or substantial portions of the Software. 230 | 231 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 232 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 233 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 234 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 235 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 236 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 237 | THE SOFTWARE. 238 | -------------------------------------------------------------------------------- /src/__test__/functions-test.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | 4 | var chai = require('chai') 5 | 6 | var expect = chai.expect 7 | 8 | describe('renderFunction', function () { 9 | it('recives a function and returns its execution', function () { 10 | var renderFunction = require('../functions').renderFunction 11 | expect(renderFunction(function () { 12 | return 'hey there' 13 | })).to.equal('hey there') 14 | }) 15 | it('recives a number and returns it', function () { 16 | var renderFunction = require('../functions').renderFunction 17 | expect(renderFunction(10)).to.equal(10) 18 | }) 19 | it('recives a string and returns it', function () { 20 | var renderFunction = require('../functions').renderFunction 21 | expect(renderFunction('hey there')).to.equal('hey there') 22 | }) 23 | }) 24 | 25 | describe('isFunction', function () { 26 | it('recives a function and returns true', function () { 27 | var isFunction = require('../functions').isFunction 28 | expect(isFunction(function () {})).to.equal(true) 29 | }) 30 | 31 | it('recives a number and returns false', function () { 32 | var isFunction = require('../functions').isFunction 33 | expect(isFunction(234)).to.equal(false) 34 | }) 35 | 36 | it('recives a string and returns false', function () { 37 | var isFunction = require('../functions').isFunction 38 | expect(isFunction('234')).to.equal(false) 39 | }) 40 | 41 | it('recives a object and returns false', function () { 42 | var isFunction = require('../functions').isFunction 43 | expect(isFunction({})).to.equal(false) 44 | }) 45 | 46 | it('recives a array and returns false', function () { 47 | var isFunction = require('../functions').isFunction 48 | expect(isFunction([])).to.equal(false) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /src/__test__/index-test.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | 4 | var chai = require('chai') 5 | 6 | var sd = require('skin-deep') 7 | 8 | var expect = chai.expect 9 | 10 | var React = require('react') 11 | var Tabs = require('../index') 12 | 13 | describe('Function: display the tab\'s content when the component is loaded', function () { 14 | context('Scenario: success', function () { 15 | describe('When we select the first tab on props', function () { 16 | let tabs 17 | 18 | beforeEach(function () { 19 | tabs = sd.shallowRender( 20 | 28 | ) 29 | }) 30 | 31 | it('the first tab\'s content should be displayed', function () { 32 | const component = tabs.getRenderOutput() 33 | expect(component.props.children[2].props.children).to.equal('Hello') 34 | }) 35 | }) 36 | 37 | describe('When we select the second tab on props', function () { 38 | let tabs 39 | 40 | beforeEach(function () { 41 | tabs = sd.shallowRender( 42 | 55 | ) 56 | }) 57 | 58 | it('the second tab\'s content should be displayed', function () { 59 | let component = tabs.getRenderOutput() 60 | expect(component.props.children[2].props.children).to.equal('content2') 61 | }) 62 | }) 63 | }) 64 | }) 65 | 66 | describe('Function: change active tab when tab clicked', function () { 67 | context('Scenario: we are on the first tab', function () { 68 | describe('When the user clicks the second tab', function () { 69 | let tabs, component 70 | 71 | before(function () { 72 | tabs = sd.shallowRender( 73 | 86 | ) 87 | component = tabs.getRenderOutput() 88 | component.props.children[1].props.children.props.clic(1) 89 | }) 90 | 91 | it('the content should be the second tab\'s content', function () { 92 | expect(component.props.children[2].props.children).to.equal('content2') 93 | }) 94 | 95 | it('the highlighted tab should be the second one', function () { 96 | expect(component.props.children[1].props.children.props.selected).to.equal(1) 97 | }) 98 | }) 99 | }) 100 | }) 101 | -------------------------------------------------------------------------------- /src/__test__/tabs-test.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | 4 | /* Global variables for RADIUM */ 5 | global.navigator = {} 6 | global.navigator.userAgent = 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36' 7 | 8 | var chai = require('chai') 9 | var sd = require('skin-deep') 10 | 11 | var expect = chai.expect 12 | 13 | var React = require('react') 14 | var Tabs = require('../tabs') 15 | 16 | describe('Function: Highlight the selected tab', function () { 17 | context('Scenario: success', function () { 18 | describe('When the first tab is selected', function () { 19 | let tabs 20 | 21 | beforeEach(function () { 22 | tabs = sd.shallowRender( 23 | 27 | ) 28 | }) 29 | 30 | it('the first tab should be highlighten', function () { 31 | const component = tabs.getRenderOutput() 32 | expect(component.props.children[1].props.children[1].props.style.marginLeft).to.equal('0%') 33 | }) 34 | }) 35 | 36 | describe('When the second tab is selected', function () { 37 | let tabs 38 | 39 | beforeEach(function () { 40 | tabs = sd.shallowRender( 41 | 45 | ) 46 | }) 47 | 48 | it('the second tab should be highlighten', function () { 49 | const component = tabs.getRenderOutput() 50 | expect(component.props.children[1].props.children[1].props.style.marginLeft).to.equal('50%') 51 | }) 52 | }) 53 | }) 54 | 55 | after(function () { 56 | delete global.navigator 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /src/functions.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | 4 | // Check if this is a function 5 | function isFunction (functionToCheck) { 6 | var getType = {} 7 | return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]' 8 | } 9 | 10 | // Checks if the element is a funciton or a renderable element and render both 11 | function renderFunction (node) { 12 | if (node) { 13 | if (isFunction(node)) { 14 | return node() 15 | } else { 16 | return node 17 | } 18 | } 19 | } 20 | 21 | module.exports = { 22 | isFunction: isFunction, 23 | renderFunction: renderFunction 24 | } 25 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | 4 | var React = require('react') 5 | var ReactDom = require('react-dom') 6 | 7 | var Tabs = require('./tabs.js') 8 | var renderFunction = require('./functions.js').renderFunction 9 | var ResizeSensor = require('css-element-queries/src/ResizeSensor') 10 | 11 | module.exports = React.createClass({ 12 | displayName: 'tabsNavigationMenu', 13 | propTypes: { 14 | banner: React.PropTypes.shape({ // Banner content (optional) 15 | children: React.PropTypes.oneOfType([ // Tab initialy selected 16 | React.PropTypes.func, 17 | React.PropTypes.node 18 | ]) 19 | }), 20 | color: React.PropTypes.string, 21 | fixOffset: React.PropTypes.number, 22 | lineStyle: React.PropTypes.object, 23 | onTabChange: React.PropTypes.func, 24 | selected: React.PropTypes.oneOfType([ // Tab initialy selected 25 | React.PropTypes.string, 26 | React.PropTypes.number 27 | ]), 28 | selectedTabStyle: React.PropTypes.object, 29 | tabs: React.PropTypes.arrayOf( 30 | React.PropTypes.shape({ 31 | children: React.PropTypes.oneOfType([ // Tab initialy selected 32 | React.PropTypes.func, 33 | React.PropTypes.node 34 | ]), 35 | displayName: React.PropTypes.string.isRequired 36 | }) 37 | ), 38 | tabsBarClassName: React.PropTypes.string, 39 | tabsBarStyle: React.PropTypes.object, 40 | tabsClassName: React.PropTypes.string, 41 | tabsStyle: React.PropTypes.object 42 | }, 43 | getDefaultProps: function () { 44 | return { 45 | fixOffset: 0, 46 | prev: 'Next', 47 | views: [] 48 | } 49 | }, 50 | getInitialState: function () { 51 | return { 52 | selectedTab: this.props.selected || 0, 53 | width: 300 54 | } 55 | }, 56 | componentDidMount: function () { 57 | let element = ReactDom.findDOMNode(this.refs.tabsContainer) 58 | new ResizeSensor(element, this.calculateWidth) // eslint-disable-line 59 | this.calculateWidth() 60 | }, 61 | componentWillReceiveProps: function (nextProps) { 62 | if (typeof nextProps.selected !== 'undefined') { 63 | if (nextProps.selected !== this.props.selected) { 64 | this.setState({ 65 | selectedTab: nextProps.selected 66 | }) 67 | } 68 | } 69 | }, 70 | componentWillUnmount: function () { 71 | let element = ReactDom.findDOMNode(this.refs.tabsContainer) 72 | ResizeSensor.detach(element) 73 | }, 74 | // Public method 75 | changeSelectedTab: function (i) { 76 | this.handleTabChange(i) 77 | }, 78 | calculateWidth: function () { 79 | this.setState({ 80 | width: ReactDom.findDOMNode(this.refs.tabsContainer).clientWidth 81 | }) 82 | }, 83 | handleTabChange: function (i) { 84 | let result 85 | 86 | if (this.props.onTabChange) { 87 | result = this.props.onTabChange(i) 88 | } 89 | 90 | if (result !== false) { 91 | this.setState({ 92 | selectedTab: i 93 | }) 94 | } 95 | }, 96 | render: function () { 97 | return ( 98 |
99 |
100 | {renderFunction(this.props.banner && 101 | this.props.banner.children)} 102 |
103 |
104 | { 108 | return item.displayName 109 | })} 110 | fixOffset={this.props.fixOffset} 111 | handleTabChange={this.handleTabChange} 112 | lineStyle={this.props.lineStyle} 113 | selected={this.state.selectedTab} 114 | selectedTabStyle={this.props.selectedTabStyle} 115 | tabsBarClassName={this.props.tabsBarClassName} 116 | tabsBarStyle={this.props.tabsBarStyle} 117 | tabsClassName={this.props.tabsClassName} 118 | tabsContainer={this.refs.tabsContainer} 119 | tabsStyle={this.props.tabsStyle} 120 | widthB={this.state.width} 121 | /> 122 |
123 |
124 | {renderFunction(this.props.tabs[this.state.selectedTab] && 125 | this.props.tabs[this.state.selectedTab].children)} 126 |
127 |
128 | ) 129 | } 130 | }) 131 | -------------------------------------------------------------------------------- /src/tabs.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | 4 | var Color = require('color') 5 | var Radium = require('radium') 6 | var React = require('react') 7 | var ReactDom = require('react-dom') 8 | 9 | var tabKeyMixin = require('./tabsKeyboardNavigationMixin.js') 10 | 11 | const defaultColor = 'rgb(11, 104, 159)' 12 | const defaultStyles = { 13 | color: defaultColor, 14 | lineStyle: { 15 | backgroundColor: defaultColor, 16 | height: 3, 17 | display: 'block', 18 | transition: 'margin-left 0.25s cubic-bezier(0.15, 0.48, 0.42, 1.13)' 19 | }, 20 | selectedTabStyle: { 21 | backgroundColor: Color(defaultColor).lighten(0.4).whiten(3.5).alpha(0.1).rgbaString(), 22 | outline: 'none' 23 | }, 24 | tabsBarStyle: { 25 | height: 55, 26 | backgroundColor: 'rgba(255, 255, 255, 0.96)', 27 | fontSize: 18 28 | }, 29 | tabsStyle: { 30 | height: '100%', 31 | paddingTop: 15, 32 | marginTop: 0, 33 | display: 'block', 34 | float: 'left', 35 | textAlign: 'center', 36 | cursor: 'pointer', 37 | WebkitUserSelect: 'none', 38 | MozUserSelect: 'none', 39 | msUserSelect: 'none', 40 | userSelect: 'none', 41 | boxSizing: 'border-box', 42 | ':focus': { 43 | boxShadow: 'inset 0 0 8px rgba(11, 104, 159, 0.3)' 44 | } 45 | } 46 | } 47 | 48 | module.exports = Radium(React.createClass({ 49 | displayName: 'tabsNavigationMenu__tabs', 50 | propTypes: { 51 | clic: React.PropTypes.func, 52 | color: React.PropTypes.string, 53 | elements: React.PropTypes.arrayOf(React.PropTypes.string), 54 | fixOffset: React.PropTypes.number, 55 | lineStyle: React.PropTypes.object, 56 | selected: React.PropTypes.number, 57 | selectedTabStyle: React.PropTypes.object, 58 | tabsBarClassName: React.PropTypes.string, 59 | tabsBarStyle: React.PropTypes.object, 60 | tabsClassName: React.PropTypes.string, 61 | tabsContainer: React.PropTypes.any, 62 | tabsStyle: React.PropTypes.object, 63 | widthB: React.PropTypes.number 64 | }, 65 | mixins: [tabKeyMixin], 66 | getDefaultProps: function () { 67 | return { 68 | clic: null, 69 | elements: ['tab1', 'tab2'], 70 | selected: 0, 71 | widthB: 300, 72 | tabsBarClassName: '', 73 | tabsClassName: '' 74 | } 75 | }, 76 | getInitialState: function () { 77 | return { 78 | menuFixed: false, 79 | focused: 0, 80 | focusedItem: this.props.selected 81 | } 82 | }, 83 | componentDidMount: function () { 84 | window.addEventListener('scroll', this.handleElementScroll) 85 | }, 86 | componentWillUnmount: function () { 87 | window.removeEventListener('scroll', this.handleElementScroll) 88 | }, 89 | 90 | // We should handle scroll events in order to detect when the bar should be 91 | // fixed 92 | handleElementScroll: function () { 93 | let top = ReactDom.findDOMNode(this.props.tabsContainer).offsetTop - this.props.fixOffset 94 | if (window.scrollY > top) { 95 | this.setState({ 96 | menuFixed: true 97 | }) 98 | } else if (window.scrollY <= top) { 99 | this.setState({ 100 | menuFixed: false 101 | }) 102 | } 103 | }, 104 | 105 | // This modifies the styles defined by the user if a color is defined 106 | // But no color is defined inside the props styles 107 | // or if no height and paddingTop are defined 108 | styles: function () { 109 | let styles = { 110 | lineStyle: this.props.lineStyle || {}, 111 | selectedTabStyle: this.props.selectedTabStyle || defaultStyles.selectedTabStyle, 112 | tabsStyle: this.props.tabsStyle || {}, 113 | tabsBarStyle: this.props.tabsBarStyle || {} 114 | } 115 | if (this.props.color) { 116 | if (!styles.lineStyle.color) { 117 | styles.lineStyle.color = this.props.color 118 | } 119 | } 120 | 121 | if (!styles.tabsStyle[':hover']) { 122 | styles.tabsStyle[':hover'] = styles.selectedTabStyle 123 | } 124 | 125 | if (!styles.tabsStyle[':focus']) { 126 | styles.tabsStyle[':focus'] = styles.selectedTabStyle 127 | } 128 | 129 | if (!styles.selectedTabStyle.backgroundColor) { 130 | styles.selectedTabStyle.backgroundColor = defaultStyles.selectedTabStyle.backgroundColor 131 | } 132 | 133 | return styles 134 | }, 135 | 136 | // We handle the click event on our tab and send it to the parent 137 | handeClick: function (i) { 138 | if (this.props.clic) { 139 | this.props.clic(i) 140 | } 141 | }, 142 | 143 | render: function () { 144 | const styles = this.styles() // Gets the user styles for this element 145 | let filler = this.state.menuFixed 146 | ?
153 | : null 154 | 155 | let elementWidth = 1 / this.props.elements.length * 100 // in percentage 156 | 157 | let bar = { 158 | marginLeft: (elementWidth * this.props.selected) + '%', 159 | width: elementWidth + '%' 160 | } 161 | 162 | let styleMenu = { 163 | top: this.state.menuFixed ? this.props.fixOffset : null, 164 | width: this.state.menuFixed ? this.props.widthB : null, 165 | position: this.state.menuFixed ? 'fixed' : null, 166 | zIndex: this.props.tabsBarStyle ? this.props.tabsBarStyle.zIndex : null 167 | } 168 | 169 | // The different tabs 170 | let elements = this.props.elements.map((element, i) => { 171 | let style = { 172 | width: elementWidth + '%' 173 | } 174 | 175 | let tabStyles = [defaultStyles.tabsStyle, styles.tabsStyle] 176 | 177 | let cssClass = this.props.tabsClassName 178 | if (this.props.selected === i) { 179 | cssClass += ' is-selected' 180 | tabStyles.push(defaultStyles.selectedTabStyle) 181 | tabStyles.push(styles.selectedTabStyle) 182 | } 183 | 184 | tabStyles.push(style) 185 | 186 | return ( 187 | 0 ? this.props.selected === i : false} 190 | className={cssClass} 191 | key={i} 192 | onBlur={this.handleBlur.bind(this, i)} 193 | onClick={this.handeClick.bind(this, i)} 194 | onFocus={this.handleFocus.bind(this, i)} 195 | ref={'tab-' + i} 196 | role="tab" 197 | style={tabStyles} 198 | tabIndex={this.props.selected === i ? 0 : -1}> 199 | {element} 200 | 201 | ) 202 | }) 203 | 204 | return ( 205 |
206 |
207 | {filler} 208 |
209 |
210 | 217 | 218 |
219 |
220 | ) 221 | } 222 | })) 223 | -------------------------------------------------------------------------------- /src/tabsKeyboardNavigationMixin.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | 4 | var KEYS = { 5 | enter: 13, 6 | left: 37, 7 | right: 39, 8 | space: 32, 9 | tab: 9, 10 | home: 36, 11 | end: 35 12 | } 13 | 14 | var KeyboardShortcutsMixin = { 15 | handleKeyPress: function (event) { 16 | if (this.state.focused) { 17 | if (event.which === KEYS.space || event.which === KEYS.enter) { 18 | this.handleEnterPress() 19 | } else if (event.which === KEYS.right) { 20 | this.handleRightPress() 21 | } else if (event.which === KEYS.left) { 22 | this.handleLeftPress() 23 | } else if (event.which === KEYS.home) { 24 | this.handleHomePress() 25 | } else if (event.which === KEYS.end) { 26 | this.handleEndPress() 27 | } 28 | } 29 | }, 30 | 31 | handleHomePress: function () { 32 | this.refs['tab-' + 0].focus() 33 | }, 34 | 35 | handleEndPress: function () { 36 | this.refs['tab-' + (this.props.elements.length - 1)].focus() 37 | }, 38 | 39 | handleEnterPress: function () { 40 | this.props.handleTabChange(this.state.focusedItem) 41 | }, 42 | 43 | handleRightPress: function () { 44 | if (this.state.focusedItem < this.props.elements.length - 1) { 45 | this.refs['tab-' + (this.state.focusedItem + 1)].focus() 46 | } 47 | }, 48 | 49 | handleLeftPress: function () { 50 | if (this.state.focusedItem > 0) { 51 | this.refs['tab-' + (this.state.focusedItem - 1)].focus() 52 | } 53 | }, 54 | 55 | handleFocus: function (i, event) { 56 | this.setState({ 57 | focused: this.state.focused + 1, 58 | focusedItem: i 59 | }) 60 | }, 61 | 62 | handleBlur: function (i, event) { 63 | this.setState({ 64 | focused: this.state.focused - 1 65 | }) 66 | }, 67 | 68 | componentDidMount: function () { 69 | document.addEventListener('keyup', this.handleKeyPress) 70 | }, 71 | 72 | componentWillUnmount: function () { 73 | document.removeEventListener('keyup', this.handleKeyPress) 74 | } 75 | } 76 | 77 | module.exports = KeyboardShortcutsMixin 78 | -------------------------------------------------------------------------------- /tests/compiler.js: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/Khan/react-components/blob/master/test/compiler.js 2 | var fs = require('fs') 3 | var ReactTools = require('react-tools') 4 | var origJs = require.extensions['.js'] 5 | 6 | // A module that exports a single, stubbed-out React Component. 7 | var reactStub = 'module.exports = require("react").createClass({render:function(){return null}})' 8 | 9 | // Should this file be stubbed out for testing? 10 | function shouldStub (filename) { 11 | if (!global.reactModulesToStub) return false 12 | 13 | // Check if the file name ends with any stub path. 14 | var stubs = global.reactModulesToStub 15 | for (var i = 0; i < stubs.length; i++) { 16 | if (filename.substr(-stubs[i].length) == stubs[i]) { // eslint-disable-line eqeqeq 17 | return true 18 | } 19 | } 20 | return false 21 | } 22 | 23 | // Transform a file via JSX/Harmony or stubbing. 24 | function transform (filename) { 25 | if (shouldStub(filename)) { 26 | return reactStub 27 | } else { 28 | var content = fs.readFileSync(filename, 'utf8') 29 | return ReactTools.transform(content, {harmony: true}) 30 | } 31 | } 32 | 33 | // Install the compiler. 34 | require.extensions['.js'] = function (module, filename) { 35 | // optimization: code in a distribution should never go through JSX compiler. 36 | if (filename.indexOf('node_modules/') >= 0) { 37 | return (origJs || require.extensions['.js'])(module, filename) 38 | } 39 | 40 | return module._compile(transform(filename), filename) 41 | } 42 | -------------------------------------------------------------------------------- /tests/testdom.js: -------------------------------------------------------------------------------- 1 | // Via http://www.asbjornenge.com/wwc/testing_react_components.html 2 | module.exports = function (markup) { 3 | if (typeof document !== 'undefined') return 4 | var jsdom = require('jsdom').jsdom 5 | global.document = jsdom(markup || '') 6 | global.window = document.defaultView 7 | global.navigator = {} 8 | global.navigator.userAgent = 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36' 9 | // ... add whatever browser globals your tests might need ... 10 | } 11 | --------------------------------------------------------------------------------