├── .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 |
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 |
25 | ),
26 | displayName: 'Docs'
27 | },
28 | {
29 | // Second tab
30 | children: () => ( // eslint-disable-line
31 |
32 |
33 |
38 |
39 |
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 | {
75 | console.log(mountedComponent)
76 | mountedComponent.changeSelectedTab(0)
77 | }}>
78 | Go to the first tab
79 |
80 |
81 |
82 |
83 | This
84 | is
85 | super
86 | large
87 | so
88 | you
89 | can
90 | scroll
91 | down
92 | .
93 | This
94 | is
95 | super
96 | large
97 | so
98 | you
99 | can
100 | scroll
101 | down
102 | .
103 | This
104 | is
105 | super
106 | large
107 | so
108 | you
109 | can
110 | scroll
111 | down
112 | .
113 | This
114 | is
115 | super
116 | large
117 | so
118 | you
119 | can
120 | scroll
121 | down
122 | .
123 | This
124 | is
125 | super
126 | large
127 | so
128 | you
129 | can
130 | scroll
131 | down
132 | .
133 | This
134 | is
135 | super
136 | large
137 | so
138 | you
139 | can
140 | scroll
141 | down
142 | .
143 |
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 | [](https://travis-ci.org/pepjo/react-tabs-navigation) [](https://codeclimate.com/github/pepjo/react-tabs-navigation) [](http://standardjs.com/)
4 |
5 | [](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 |
215 | {elements}
216 |
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 |
--------------------------------------------------------------------------------