├── .eslintrc.js
├── .gitignore
├── .prettierignore
├── .release-it.json
├── .travis.yml
├── LICENSE.md
├── README.md
├── demo
└── src
│ ├── index.js
│ ├── plain-react
│ ├── index.js
│ └── styles.css
│ └── redux
│ ├── components
│ ├── index.js
│ └── styles.css
│ └── index.js
├── nwb.config.js
├── package.json
├── prettier.config.js
├── src
├── components
│ ├── TabContent.js
│ ├── TabLink.js
│ └── Tabs.js
└── index.js
├── test
├── .eslintrc
├── TabContent.test.js
├── TabLink.test.js
└── Tabs.test.js
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['airbnb', 'prettier', 'prettier/react'],
3 | parser: 'babel-eslint',
4 | env: {
5 | browser: true,
6 | es6: true,
7 | jest: true,
8 | },
9 | settings: {
10 | ecmascript: 6,
11 | jsx: true,
12 | },
13 | rules: {
14 | 'no-underscore-dangle': 0,
15 | 'no-return-assign': [2, 'except-parens'],
16 | 'newline-before-return': 2,
17 | 'no-nested-ternary': 0,
18 | 'import/no-extraneous-dependencies': [
19 | 2,
20 | { devDependencies: ['demo/**/*.js', 'test/**/*.js'] },
21 | ],
22 |
23 | 'react/require-default-props': 0,
24 | 'react/prefer-stateless-function': 0,
25 | 'react/forbid-prop-types': 0,
26 | 'react/jsx-filename-extension': 0,
27 | 'react/no-array-index-key': 0,
28 | 'react/no-danger': 0,
29 | 'react/sort-comp': 0,
30 |
31 | 'jsx-a11y/click-events-have-key-events': 0,
32 | 'jsx-a11y/no-static-element-interactions': 0,
33 | },
34 | };
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /es
3 | /lib
4 | /umd
5 | /demo/dist
6 | public
7 | npm-debug.log*
8 | *.orig
9 | package-lock.json
10 | yarn-error.log
11 | .cache
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | es/
2 | lib/
3 | umd/
4 | node_modules/
5 |
--------------------------------------------------------------------------------
/.release-it.json:
--------------------------------------------------------------------------------
1 | {
2 | "buildCommand": "npm run build",
3 | "github": {
4 | "release": false
5 | },
6 | "prompt": {
7 | "src": {
8 | "publish": true,
9 | "release": false
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 | language: node_js
3 | node_js:
4 | - stable
5 | services:
6 | - xvfb
7 | addons:
8 | chrome: stable
9 | cache:
10 | directories:
11 | - node_modules
12 | notifications:
13 | email: false
14 | branches:
15 | only:
16 | - master
17 | before_script:
18 | - export DISPLAY=:99.0
19 | script:
20 | - npm run lint
21 | - npm test -- --single-run
22 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Patrik Piskay
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React tabs (Redux compatible)
2 |
3 | [](https://travis-ci.org/patrik-piskay/react-tabs-redux) [](https://www.npmjs.com/package/react-tabs-redux)
4 |
5 | Simple, fully customizable, accessible React tabs component that can be used in plain React application or with any Flux-like architecture with external application state, e.g. Redux.
6 |
7 | 
8 |
9 | ## Installation
10 |
11 | $ yarn add react-tabs-redux
12 | or
13 | $ npm install react-tabs-redux
14 |
15 | ## Usage
16 |
17 | - **with plain React** (using component's internal state) - [see example](https://github.com/patrik-piskay/react-tabs-redux/tree/master/examples/plain-react)
18 |
19 | ```jsx
20 |
21 | Tab1
22 | Tab2
23 | Tab3
24 |
25 | /* content for tab #1 */
26 | /* content for tab #2 */
27 | /* content for tab #3 */
28 |
29 | ```
30 |
31 | - **with Redux** (or any other external state management library) - [see example](https://github.com/patrik-piskay/react-tabs-redux/tree/master/examples/redux)
32 |
33 | The only change needed from _plain React_ example is to provide `handleSelect` and `selectedTab` (`name` as well if you want to have multiple `` instances in your app) props to `` component so that you are able to save and retrieve information about which tab is currently active from your external application state.
34 |
35 | ```jsx
36 | instance so you can keep track of
38 | multiple instances in your external state */
39 | name="tabs1"
40 | handleSelect={(selectedTab, namespace) => {
41 | // fire Redux action here
42 | }}
43 | /* selected tab name retrieved from external state */
44 | selectedTab="tab2"
45 | >
46 | Tab1
47 | Tab2
48 | Tab3
49 |
50 | ...
51 | ...
52 | ...
53 |
54 | ```
55 |
56 | ---
57 |
58 | By default, the first `` component is set to active. You can change this by specifying `default` in props of the `` component you want to become active instead.
59 |
60 | ```jsx
61 |
62 | Tab1
63 |
64 | Tab2
65 |
66 | Tab3
67 | ...
68 | ... // this gets visible
69 | ...
70 |
71 | ```
72 |
73 | ---
74 |
75 | `` and `` **don't need** to be first level children of `` component. You can put them as deep in your markup as you need to (_e.g. because of styling_) making `` component **fully customizable**.
76 |
77 | This will work then:
78 |
79 | ```jsx
80 |
81 |
82 | -
83 | Tab1
84 |
85 | -
86 | Tab2
87 |
88 | -
89 | Tab3
90 |
91 |
92 |
93 |
94 | ...
95 | ...
96 | ...
97 |
98 |
99 | ```
100 |
101 | ---
102 |
103 | If, for performance or other reasons, you wish to render only the content of the active tab to HTML and not render anything for the rest (not visible content), you may set an `renderActiveTabContentOnly` prop on `` component.
104 |
105 | ```jsx
106 |
107 |
108 | -
109 |
110 | Tab1
111 |
112 |
113 | -
114 | Tab2
115 |
116 | -
117 | Tab3
118 |
119 |
120 |
121 |
122 | Content 1 /* rendered in HTML */
123 | Content 2 /* empty in HTML */
124 | Content 3 /* empty in HTML */
125 |
126 |
127 | ```
128 |
129 | ---
130 |
131 | ### `` props
132 |
133 | | Prop name | Type | Default value | Description |
134 | | :------------------------- | :---------------------------- | :------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
135 | | name | _string_ | | Sets namespace of a `` component. Useful when multiple instances are used |
136 | | onChange | _function(selectedTab, name)_ | | Called everytime the selected tab is changed. _(it gets called for the initial selected tab too)_ |
137 | | handleSelect | _function(tab, name)_ | | **(Optional) control prop** - called everytime a Tab Link is clicked
_Useful in an "external state" scenario (Redux, etc.), use `onChange` for tracking purposes._ |
138 | | selectedTab | _string_ | | **(Optional) control prop** - controls which tab is currently active.
_Useful in an "external state" scenario (Redux, etc.), when not set, the `` component will maintain the "selected tab" state._ |
139 | | activeLinkStyle | _object_ | | Style that gets applied to the selected `` |
140 | | visibleTabStyle | _object_ | | Style that gets applied to the visible `` |
141 | | disableInlineStyles | _boolean_ | false | Useful if you are using `className` to style the components and don't want the default inline styles to get applied. |
142 | | renderActiveTabContentOnly | _boolean_ | false | _Performance_: When set, only the visible content gets actually rendered to DOM _(instead of all `` being rendered and hidden)_. |
143 | | tabComponent | _string_ | | DOM element all `` render to.
_This can be set on `` level too, by setting the `component` prop on `` components._ |
144 |
145 | ### `` props
146 |
147 | | Prop name | Type | Default value | Description |
148 | | :-------------- | :-------- | ----------------- | --------------------------------------------------------------------- |
149 | | component | _string_ | "button" | DOM element `` renders to. |
150 | | className | _string_ | "tab-link" | Class name that's applied to elements |
151 | | activeClassName | _string_ | "tab-link-active" | Class name that's applied to the element when it's active |
152 | | default | _boolean_ | | Set tab as default |
153 |
154 | ### `` props
155 |
156 | | Prop name | Type | Default value | Description |
157 | | :-------------- | :------- | --------------------- | ------------------------------------------------------------------------- |
158 | | className | _string_ | "tab-content" | Class name that's applied to elements |
159 | | activeClassName | _string_ | "tab-content-visible" | Class name that's applied to the element when it's visible |
160 |
161 | ---
162 |
163 | See more in [examples](https://github.com/patrik-piskay/react-tabs-redux/tree/master/examples)
164 |
165 | ## Styling components
166 |
167 | #### Class names
168 |
169 | There is a couple of class names dynamically added to the components:
170 |
171 | `` will receive `tab-link` class name with `tab-link-active` added when tab is active.
172 |
173 | ```jsx
174 | /* will receive `className="tab-link"` in props */
175 | Tab1
176 |
177 | /* will receive `className="tab-link tab-link-active"` in props */
178 | Tab1
179 | ```
180 |
181 | To override the default class names, `` accepts a `className` prop, as well as an `activeClassName` prop.
182 |
183 | ---
184 |
185 | `` will receive `tab-content` class name with `tab-content-visible` added when the content is visible (its corresponding `` is active).
186 |
187 | ```jsx
188 | /* will receive `className="tab-content"` or `className="tab-content tab-content-visible"` in props */
189 | ...
190 | ```
191 |
192 | To override the default class names, `` accepts a `className` prop, as well as a `visibleClassName` prop.
193 |
194 | #### Inline styles
195 |
196 | If you prefer to use inline styles, you can set `style` in props of each of ``, `` and `` components.
197 |
198 | To apply style for an active tab link, set the style as `activeLinkStyle` in props of `` component.
199 | To apply style for a visible tab content, set the style as `visibleTabStyle` in props of `` component.
200 |
201 | By default, react-tabs-redux will apply `display: none` styles to the appropriate `` component, and `font-weight: bold` to the appropriate `` component. If you would like to use classes to handle all of the styling, and disable even the default inline styles, you may pass `disableInlineStyles` as a prop to the parent `` component.
202 |
203 | ```jsx
204 | ` */}
207 | visibleTabStyle={/* style that will be applied on the visible `` */}
208 | disableInlineStyles={/* Boolean to toggle all inline styles */}
209 | >
210 | Tab1
211 | Tab2
212 |
213 | ...
214 | ...
215 |
216 | ```
217 |
218 | ---
219 |
220 | In each [example](https://github.com/patrik-piskay/react-tabs-redux/tree/master/demo) there is one `` components styled using class names and the other one styled using inline styles.
221 |
222 | ## License
223 |
224 | MIT
225 |
--------------------------------------------------------------------------------
/demo/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 |
4 | import { Tabs, TabLink, TabContent } from '../../src';
5 | import PlainReactExample from './plain-react';
6 | import ReduxExample from './redux';
7 |
8 | const styles = {
9 | tabLink: {
10 | height: '40px',
11 | padding: '0 15px',
12 | },
13 | activeLinkStyle: {
14 | border: '1px solid black',
15 | },
16 | content: {
17 | padding: '15px',
18 | },
19 | };
20 |
21 | const Demo = () => (
22 |
23 |
24 | React example
25 |
26 |
27 | Redux example
28 |
29 |
30 |
31 |
32 | React internal state example
33 |
34 |
35 |
36 | Using Redux
37 |
38 |
39 |
40 |
41 | );
42 |
43 | render(, document.querySelector('#demo'));
44 |
--------------------------------------------------------------------------------
/demo/src/plain-react/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Tabs, TabLink, TabContent } from '../../../src';
4 |
5 | import './styles.css';
6 |
7 | const styles = {
8 | tabs: {
9 | width: '400px',
10 | display: 'inline-block',
11 | marginRight: '30px',
12 | verticalAlign: 'top',
13 | },
14 | links: {
15 | margin: 0,
16 | padding: 0,
17 | },
18 | tabLink: {
19 | height: '30px',
20 | lineHeight: '30px',
21 | padding: '0 15px',
22 | cursor: 'pointer',
23 | border: 'none',
24 | borderBottom: '2px solid transparent',
25 | display: 'inline-block',
26 | },
27 | activeLinkStyle: {
28 | borderBottom: '2px solid #333',
29 | },
30 | visibleTabStyle: {
31 | display: 'inline-block',
32 | },
33 | content: {
34 | padding: '0 15px',
35 | },
36 | };
37 |
38 | const App = () => (
39 |
40 |
console.log(`Tab selected: ${tab}`)} // eslint-disable-line no-console
43 | >
44 |
45 | Tab1
46 | Tab2
47 | Tab3
48 |
49 |
50 |
51 |
52 | Tab1 content
53 |
54 | Lorem ipsum dolor sit amet, in vel malorum adipiscing. Duis deleniti
55 | ei cum, amet graece nec an. Eu vix sumo atqui apeirian, nullam
56 | integre accusamus his at, animal feugiat in sed.
57 |
58 |
59 | Pro vitae percipit no. Per ignota audire no. Ex hinc mutat delicata
60 | sit, sit eu erant tempor vivendo. Ad modus nusquam recusabo sit. Per
61 | ne deserunt periculis, ad sea saepe perfecto expetendis, est nonumy
62 | contentiones voluptatibus cu.
63 |
64 |
65 |
66 | Tab2 content
67 | ¯\_(ツ)_/¯
68 |
69 |
70 | Tab3 content
71 | (╯°□°)╯︵ ┻━┻)
72 |
73 |
74 |
75 |
76 |
81 |
82 |
83 | Tab1
84 |
85 |
86 | Tab2
87 |
88 |
89 | Tab3
90 |
91 |
92 |
93 |
94 |
95 | Tab1 content
96 |
97 | Lorem ipsum dolor sit amet, in vel malorum adipiscing. Duis deleniti
98 | ei cum, amet graece nec an. Eu vix sumo atqui apeirian, nullam
99 | integre accusamus his at, animal feugiat in sed.
100 |
101 |
102 | Pro vitae percipit no. Per ignota audire no. Ex hinc mutat delicata
103 | sit, sit eu erant tempor vivendo. Ad modus nusquam recusabo sit. Per
104 | ne deserunt periculis, ad sea saepe perfecto expetendis, est nonumy
105 | contentiones voluptatibus cu.
106 |
107 |
108 |
109 | Tab2 content
110 | ¯\_(ツ)_/¯
111 |
112 |
113 | Tab3 content
114 | (╯°□°)╯︵ ┻━┻)
115 |
116 |
117 |
118 |
119 | );
120 |
121 | export default App;
122 |
--------------------------------------------------------------------------------
/demo/src/plain-react/styles.css:
--------------------------------------------------------------------------------
1 | #plain-react .tabs {
2 | width: 400px;
3 | display: inline-block;
4 | margin-right: 30px;
5 | vertical-align: top;
6 | }
7 |
8 | #plain-react .tabs-1 .tab-links {
9 | margin: 0;
10 | padding: 0;
11 | border: 1px solid #ccc;
12 | border-bottom: 1px solid #868686;
13 | border-top-left-radius: 5px;
14 | border-top-right-radius: 5px;
15 | height: 35px;
16 | position: relative;
17 | top: 5px;
18 | }
19 |
20 | #plain-react .tabs-1 .tab-link {
21 | padding: 0 15px;
22 | cursor: pointer;
23 | border: 1px solid transparent;
24 | background: transparent;
25 | display: inline-block;
26 | position: relative;
27 | height: 40px;
28 | line-height: 41px;
29 | top: -4px;
30 | left: -1px;
31 | outline: none;
32 | }
33 |
34 | #plain-react .tabs-1 .tab-link:hover,
35 | #plain-react .tabs-1 .tab-link:focus {
36 | color: #666464;
37 | }
38 |
39 | #plain-react .tabs-1 .tab-link-active,
40 | #plain-react .tabs-1 .tab-link-active:hover,
41 | #plain-react .tabs-1 .tab-link-active:focus {
42 | color: black;
43 | font-weight: bold;
44 | border: 1px solid #868686;
45 | border-bottom: 1px solid white;
46 | border-top-left-radius: 5px;
47 | border-top-right-radius: 5px;
48 | background-color: white;
49 | }
50 |
51 | #plain-react .tabs-1 .content {
52 | padding: 15px;
53 | border: 1px solid #868686;
54 | border-top: 1px solid transparent;
55 | }
56 |
57 | #plain-react .tabs-1 .tabcontent: {
58 | }
59 |
--------------------------------------------------------------------------------
/demo/src/redux/components/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { Tabs, TabLink, TabContent } from '../../../../src';
5 |
6 | import './styles.css';
7 |
8 | const styles = {
9 | tabs: {
10 | width: '400px',
11 | display: 'inline-block',
12 | marginRight: '30px',
13 | verticalAlign: 'top',
14 | },
15 | links: {
16 | margin: 0,
17 | padding: 0,
18 | },
19 | tabLink: {
20 | height: '30px',
21 | lineHeight: '30px',
22 | padding: '0 15px',
23 | cursor: 'pointer',
24 | border: 'none',
25 | borderBottom: '2px solid transparent',
26 | display: 'inline-block',
27 | },
28 | activeLinkStyle: {
29 | borderBottom: '2px solid #333',
30 | },
31 | visibleTabStyle: {
32 | display: 'inline-block',
33 | },
34 | content: {
35 | padding: '0 15px',
36 | },
37 | };
38 |
39 | const App = props => (
40 |
41 |
State:
42 |
43 |
44 | tabs 1: {String(props.tabs1)}
45 |
46 |
47 | tabs 2: {String(props.tabs2)}
48 |
49 |
50 |
51 |
58 | console.log(`Tab selected: ${tab} in namespace ${namespace}`) // eslint-disable-line no-console
59 | }
60 | >
61 |
62 | Tab1
63 | Tab2
64 | Tab3
65 |
66 |
67 |
68 |
69 | Tab1 content
70 |
71 | Lorem ipsum dolor sit amet, in vel malorum adipiscing. Duis
72 | deleniti ei cum, amet graece nec an. Eu vix sumo atqui apeirian,
73 | nullam integre accusamus his at, animal feugiat in sed.
74 |
75 |
76 | Pro vitae percipit no. Per ignota audire no. Ex hinc mutat
77 | delicata sit, sit eu erant tempor vivendo. Ad modus nusquam
78 | recusabo sit. Per ne deserunt periculis, ad sea saepe perfecto
79 | expetendis, est nonumy contentiones voluptatibus cu.
80 |
81 |
82 |
83 | Tab2 content
84 | ¯\_(ツ)_/¯
85 |
86 |
87 | Tab3 content
88 | (╯°□°)╯︵ ┻━┻)
89 |
90 |
91 |
92 |
93 |
98 | console.log(`Tab selected: ${tab} in namespace ${namespace}`) // eslint-disable-line no-console
99 | }
100 | selectedTab={props.tabs2}
101 | activeLinkStyle={styles.activeLinkStyle}
102 | visibleTabStyle={styles.visibleTabStyle}
103 | style={styles.tabs}
104 | >
105 |
106 |
107 | Tab1
108 |
109 |
110 | Tab2
111 |
112 |
113 | Tab3
114 |
115 |
116 |
117 |
118 |
119 | Tab1 content
120 |
121 | Lorem ipsum dolor sit amet, in vel malorum adipiscing. Duis
122 | deleniti ei cum, amet graece nec an. Eu vix sumo atqui apeirian,
123 | nullam integre accusamus his at, animal feugiat in sed.
124 |
125 |
126 | Pro vitae percipit no. Per ignota audire no. Ex hinc mutat
127 | delicata sit, sit eu erant tempor vivendo. Ad modus nusquam
128 | recusabo sit. Per ne deserunt periculis, ad sea saepe perfecto
129 | expetendis, est nonumy contentiones voluptatibus cu.
130 |
131 |
132 |
133 | Tab2 content
134 | ¯\_(ツ)_/¯
135 |
136 |
137 | Tab3 content
138 | (╯°□°)╯︵ ┻━┻)
139 |
140 |
141 |
142 |
143 |
144 | );
145 |
146 | App.propTypes = {
147 | tabs1: PropTypes.string,
148 | tabs2: PropTypes.string,
149 | changeSelectedTab: PropTypes.func,
150 | };
151 |
152 | export default App;
153 |
--------------------------------------------------------------------------------
/demo/src/redux/components/styles.css:
--------------------------------------------------------------------------------
1 | #redux .tabs {
2 | width: 400px;
3 | display: inline-block;
4 | margin-right: 30px;
5 | vertical-align: top;
6 | }
7 |
8 | #redux .tabs-1 .tab-links {
9 | margin: 0;
10 | padding: 0;
11 | border: 1px solid #ccc;
12 | border-bottom: 1px solid #868686;
13 | border-top-left-radius: 5px;
14 | border-top-right-radius: 5px;
15 | height: 35px;
16 | position: relative;
17 | top: 5px;
18 | }
19 |
20 | #redux .tabs-1 .tab-link {
21 | padding: 0 15px;
22 | cursor: pointer;
23 | border: 1px solid transparent;
24 | background: transparent;
25 | display: inline-block;
26 | position: relative;
27 | height: 40px;
28 | line-height: 41px;
29 | top: -4px;
30 | left: -1px;
31 | outline: none;
32 | }
33 |
34 | #redux .tabs-1 .tab-link:hover,
35 | #redux .tabs-1 .tab-link:focus {
36 | color: #666464;
37 | }
38 |
39 | #redux .tabs-1 .tab-link-active,
40 | #redux .tabs-1 .tab-link-active:hover,
41 | #redux .tabs-1 .tab-link-active:focus {
42 | color: black;
43 | font-weight: bold;
44 | border: 1px solid #868686;
45 | border-bottom: 1px solid white;
46 | border-top-left-radius: 5px;
47 | border-top-right-radius: 5px;
48 | background-color: white;
49 | }
50 |
51 | #redux .tabs-1 .content {
52 | padding: 15px;
53 | border: 1px solid #868686;
54 | border-top: 1px solid transparent;
55 | }
56 |
57 | #redux .tabs-1 .tabcontent: {
58 | }
59 |
60 | #redux code {
61 | display: inline-block;
62 | margin-bottom: 30px;
63 | padding: 5px;
64 | background-color: #e8e8e8;
65 | }
66 |
--------------------------------------------------------------------------------
/demo/src/redux/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createStore } from 'redux';
3 | import { Provider, connect } from 'react-redux';
4 |
5 | import App from './components';
6 |
7 | // Action
8 |
9 | const CHANGE_SELECTED_TAB = 'CHANGE_SELECTED_TAB';
10 |
11 | function changeSelectedTab(selectedTab, tabNamespace) {
12 | return {
13 | type: CHANGE_SELECTED_TAB,
14 | tab: selectedTab,
15 | namespace: tabNamespace,
16 | };
17 | }
18 |
19 | // Reducer
20 |
21 | const initialState = {
22 | tabs1: null,
23 | tabs2: null,
24 | };
25 |
26 | function tabsReducer(state = initialState, action) {
27 | switch (action.type) {
28 | case CHANGE_SELECTED_TAB:
29 | return {
30 | ...state,
31 | [action.namespace]: action.tab,
32 | };
33 |
34 | default:
35 | return state;
36 | }
37 | }
38 |
39 | // Store
40 |
41 | const store = createStore(tabsReducer);
42 |
43 | // App
44 |
45 | const ReduxExample = connect(
46 | state => state,
47 | { changeSelectedTab },
48 | )(App);
49 |
50 | export default () => (
51 |
52 |
53 |
54 | );
55 |
--------------------------------------------------------------------------------
/nwb.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | type: 'react-component',
3 | npm: {
4 | esModules: true,
5 | umd: {
6 | global: 'ReactTabs',
7 | externals: {
8 | react: 'React',
9 | },
10 | },
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-tabs-redux",
3 | "version": "4.0.0",
4 | "description": "Simple, fully customizable React tabs component that can be used in plain React application or with any Flux-like architecture, e.g. Redux",
5 | "author": "Patrik Piskay",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/patrik-piskay/react-tabs-redux"
9 | },
10 | "license": "MIT",
11 | "keywords": [
12 | "react",
13 | "tabs",
14 | "redux",
15 | "flux"
16 | ],
17 | "files": [
18 | "es",
19 | "lib",
20 | "umd"
21 | ],
22 | "main": "lib/index.js",
23 | "jsnext:main": "es/index.js",
24 | "husky": {
25 | "hooks": {
26 | "pre-commit": "lint-staged"
27 | }
28 | },
29 | "lint-staged": {
30 | "*.js": [
31 | "prettier --write",
32 | "git add"
33 | ]
34 | },
35 | "scripts": {
36 | "start": "nwb serve-react-demo",
37 | "build": "nwb build-react-component",
38 | "clean": "nwb clean-module && nwb clean-demo",
39 | "lint": "eslint src/**/*.js demo/src/*.js",
40 | "test": "yarn lint && yarn test:browser",
41 | "test:browser": "nwb test --single-run",
42 | "prettier": "prettier --write \"src/**/*.js\" \"test/**/*.js\"",
43 | "release": "release-it"
44 | },
45 | "devDependencies": {
46 | "babel-eslint": "^8.0.0",
47 | "eslint": "^4.9.0",
48 | "eslint-config-airbnb": "16.0.0",
49 | "eslint-config-prettier": "^2.6.0",
50 | "eslint-plugin-import": "^2.14.0",
51 | "eslint-plugin-jsx-a11y": "^6.1.2",
52 | "eslint-plugin-react": "7.4.0",
53 | "husky": "^1.1.2",
54 | "lint-staged": "^7.3.0",
55 | "nwb": "^0.23.0",
56 | "prettier": "^1.14.3",
57 | "react": "^16.5.2",
58 | "react-addons-test-utils": "^0.14.6",
59 | "react-dom": "^16.5.2",
60 | "react-redux": "^5.0.7",
61 | "react-test-renderer": "^16.5.2",
62 | "redux": "^4.0.1",
63 | "release-it": "^7.6.2"
64 | },
65 | "peerDependencies": {
66 | "react": "^0.14.5 || >=15"
67 | },
68 | "dependencies": {
69 | "classnames": "^2.2.6",
70 | "prop-types": "^15.6.2"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 80,
3 | singleQuote: true,
4 | trailingComma: 'all',
5 |
6 | /* defaults */
7 | semi: true,
8 | useTabs: false,
9 | tabWidth: 2,
10 | bracketSpacing: true,
11 | jsxBracketSameLine: false,
12 | };
13 |
--------------------------------------------------------------------------------
/src/components/TabContent.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import classNames from 'classnames';
4 |
5 | export const styles = {
6 | hidden: {
7 | display: 'none',
8 | },
9 | };
10 |
11 | class TabContent extends Component {
12 | static displayName = 'TabContent';
13 |
14 | canRenderChildren() {
15 | return this.props.isVisible || !this.props.renderActiveTabContentOnly;
16 | }
17 |
18 | render() {
19 | const visibleStyle = this.props.visibleStyle || {};
20 | const displayStyle = this.props.isVisible ? visibleStyle : styles.hidden;
21 | const className = this.props.className || 'tab-content';
22 | const visibleClassName =
23 | this.props.visibleClassName || 'tab-content-visible';
24 |
25 | return (
26 |
42 | {this.canRenderChildren() && this.props.children}
43 |
44 | );
45 | }
46 | }
47 |
48 | TabContent.propTypes = {
49 | children: PropTypes.node,
50 | for: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, // eslint-disable-line react/no-unused-prop-types
51 | visibleStyle: PropTypes.object,
52 | isVisible: PropTypes.bool,
53 | renderActiveTabContentOnly: PropTypes.bool,
54 | disableInlineStyles: PropTypes.bool,
55 | className: PropTypes.string,
56 | visibleClassName: PropTypes.string,
57 | style: PropTypes.object,
58 | };
59 |
60 | export default TabContent;
61 |
--------------------------------------------------------------------------------
/src/components/TabLink.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import classNames from 'classnames';
4 |
5 | export const defaultActiveStyle = {
6 | fontWeight: 'bold',
7 | };
8 |
9 | class TabLink extends Component {
10 | static displayName = 'TabLink';
11 |
12 | handleClick = e => {
13 | this.props.handleSelect(this.props.to, this.props.namespace);
14 |
15 | if (this.props.onClick) {
16 | this.props.onClick(e);
17 | }
18 | };
19 |
20 | handleKeyPress = e => {
21 | if (e.key === ' ' || e.key === 'Enter') {
22 | e.preventDefault();
23 |
24 | this.handleClick(e);
25 | }
26 | };
27 |
28 | render() {
29 | const {
30 | to,
31 | handleSelect,
32 | isActive,
33 | namespace,
34 | activeStyle,
35 | disableInlineStyles,
36 | className,
37 | activeClassName,
38 | style,
39 | ...passedProps
40 | } = this.props;
41 |
42 | const _className = className || 'tab-link';
43 | const _activeClassName = activeClassName || 'tab-link-active';
44 | const _style = {
45 | ...style,
46 | ...((isActive && (activeStyle || defaultActiveStyle)) || {}),
47 | };
48 | const componentType = this.props.component || 'button';
49 |
50 | return React.createElement(
51 | componentType,
52 | {
53 | id: `tab-${to}`,
54 | role: 'tab',
55 | 'aria-selected': isActive ? 'true' : 'false',
56 | 'aria-controls': `tabpanel-${to}`,
57 | className: classNames({
58 | [_className]: true,
59 | [_activeClassName]: isActive,
60 | }),
61 | style: (!disableInlineStyles && _style) || undefined,
62 | ...passedProps,
63 | onKeyPress: this.handleKeyPress,
64 | onClick: this.handleClick,
65 | },
66 | this.props.children,
67 | );
68 | }
69 | }
70 |
71 | TabLink.propTypes = {
72 | to: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
73 | component: PropTypes.string,
74 | handleSelect: PropTypes.func,
75 | onClick: PropTypes.func,
76 | children: PropTypes.node,
77 | isActive: PropTypes.bool,
78 | namespace: PropTypes.string,
79 | activeStyle: PropTypes.object,
80 | disableInlineStyles: PropTypes.bool,
81 | className: PropTypes.string,
82 | activeClassName: PropTypes.string,
83 | style: PropTypes.object,
84 | default: PropTypes.bool,
85 | };
86 |
87 | export default TabLink;
88 |
--------------------------------------------------------------------------------
/src/components/Tabs.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const findDefaultTab = children => {
5 | let firstLink;
6 | let firstDefaultLink;
7 |
8 | const traverse = child => {
9 | if (!child || !child.props || firstDefaultLink) {
10 | return;
11 | }
12 |
13 | if (child.type.displayName === 'TabLink') {
14 | firstLink = firstLink || child.props.to;
15 | firstDefaultLink =
16 | firstDefaultLink || (child.props.default && child.props.to);
17 | }
18 |
19 | React.Children.forEach(child.props.children, traverse);
20 | };
21 |
22 | React.Children.forEach(children, traverse);
23 |
24 | return firstDefaultLink || firstLink;
25 | };
26 |
27 | class Tabs extends Component {
28 | state = {
29 | selectedTab: this.props.selectedTab || findDefaultTab(this.props.children),
30 | };
31 |
32 | componentDidMount() {
33 | this.props.onChange(this.state.selectedTab, this.props.name);
34 | }
35 |
36 | componentDidUpdate(prevProps, prevState) {
37 | if (this.state.selectedTab !== prevState.selectedTab) {
38 | this.props.onChange(this.state.selectedTab, this.props.name);
39 | }
40 | }
41 |
42 | componentWillReceiveProps(newProps) {
43 | if (this.props.selectedTab !== newProps.selectedTab) {
44 | this.setState({
45 | selectedTab: newProps.selectedTab,
46 | });
47 | }
48 | }
49 |
50 | handleSelect = tab => {
51 | this.setState({
52 | selectedTab: tab,
53 | });
54 | };
55 |
56 | transformChildren(
57 | children,
58 | {
59 | handleSelect,
60 | selectedTab,
61 | activeLinkStyle,
62 | visibleTabStyle,
63 | disableInlineStyles,
64 | name,
65 | tabComponent,
66 | },
67 | ) {
68 | if (typeof children !== 'object') {
69 | return children;
70 | }
71 |
72 | return React.Children.map(children, child => {
73 | if (!child) {
74 | return child;
75 | }
76 | if (child.type.displayName === 'TabLink') {
77 | return React.cloneElement(child, {
78 | handleSelect,
79 | isActive: child.props.to === selectedTab,
80 | activeStyle: activeLinkStyle,
81 | disableInlineStyles,
82 | namespace: name,
83 | component: child.props.component || tabComponent,
84 | });
85 | }
86 |
87 | if (child.type.displayName === 'TabContent') {
88 | return React.cloneElement(child, {
89 | isVisible: child.props.for === selectedTab,
90 | visibleStyle: visibleTabStyle,
91 | disableInlineStyles,
92 | renderActiveTabContentOnly: this.props.renderActiveTabContentOnly,
93 | });
94 | }
95 |
96 | return React.cloneElement(
97 | child,
98 | {},
99 | this.transformChildren(child.props && child.props.children, {
100 | handleSelect,
101 | selectedTab,
102 | activeLinkStyle,
103 | visibleTabStyle,
104 | disableInlineStyles,
105 | name,
106 | tabComponent,
107 | }),
108 | );
109 | });
110 | }
111 |
112 | render() {
113 | const {
114 | handleSelect: handleSelectProp,
115 | selectedTab: selectedTabProp,
116 | activeLinkStyle,
117 | visibleTabStyle,
118 | disableInlineStyles,
119 | name,
120 | renderActiveTabContentOnly, // eslint-disable-line
121 | tabComponent,
122 | ...divProps
123 | } = this.props;
124 | const handleSelect = handleSelectProp || this.handleSelect;
125 |
126 | const children = this.transformChildren(this.props.children, {
127 | handleSelect,
128 | selectedTab: this.state.selectedTab,
129 | activeLinkStyle,
130 | visibleTabStyle,
131 | disableInlineStyles,
132 | name,
133 | tabComponent,
134 | });
135 |
136 | return {children}
;
137 | }
138 | }
139 |
140 | Tabs.propTypes = {
141 | name: PropTypes.string,
142 | tabComponent: PropTypes.string,
143 | children: PropTypes.node,
144 | onChange: PropTypes.func,
145 | handleSelect: PropTypes.func,
146 | selectedTab: PropTypes.string,
147 | activeLinkStyle: PropTypes.object,
148 | visibleTabStyle: PropTypes.object,
149 | disableInlineStyles: PropTypes.bool,
150 | renderActiveTabContentOnly: PropTypes.bool,
151 | };
152 |
153 | Tabs.defaultProps = {
154 | onChange: () => {},
155 | };
156 |
157 | export default Tabs;
158 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Tabs from './components/Tabs';
2 | import TabContent from './components/TabContent';
3 | import TabLink from './components/TabLink';
4 |
5 | export { Tabs, TabContent, TabLink };
6 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | }
5 | }
--------------------------------------------------------------------------------
/test/TabContent.test.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import React from 'react';
3 | import ShallowRenderer from 'react-test-renderer/shallow';
4 |
5 | import TabContent, { styles } from '../src/components/TabContent';
6 |
7 | describe('TabContent component', () => {
8 | it('should have correct default values set', () => {
9 | const renderer = new ShallowRenderer();
10 | renderer.render();
11 | const result1 = renderer.getRenderOutput();
12 |
13 | renderer.render();
14 | const result2 = renderer.getRenderOutput();
15 |
16 | assert.equal(result1.props.className, 'tab-content');
17 | assert.equal(result1.props.id, 'tabpanel-tab1');
18 | assert.equal(result1.props.role, 'tabpanel');
19 | assert.equal(result1.props['aria-labelledby'], 'tab-tab1');
20 | assert.deepEqual(result1.props.style, styles.hidden);
21 | assert.deepEqual(result2.props.style, styles.hidden);
22 | });
23 |
24 | it('should not set hidden styles when "isVisible" prop is set', () => {
25 | const renderer = new ShallowRenderer();
26 | renderer.render();
27 | const result = renderer.getRenderOutput();
28 |
29 | assert.deepEqual(result.props.style, {});
30 | });
31 |
32 | it('should use custom styles when provided', () => {
33 | const style = { backgroundColor: 'green' };
34 | const renderer = new ShallowRenderer();
35 | renderer.render();
36 | const result = renderer.getRenderOutput();
37 |
38 | assert.deepEqual(result.props.style, {
39 | ...style,
40 | ...styles.visible,
41 | });
42 | });
43 |
44 | it('should not set inline styles when "disableInlineStyles" props is set', () => {
45 | const style = { backgroundColor: 'green' };
46 | const renderer = new ShallowRenderer();
47 |
48 | renderer.render(
49 | ,
50 | );
51 | const result = renderer.getRenderOutput();
52 |
53 | assert.equal(result.props.style, undefined);
54 | });
55 |
56 | it('should support custom class names', () => {
57 | const renderer = new ShallowRenderer();
58 |
59 | renderer.render();
60 |
61 | const result1 = renderer.getRenderOutput();
62 |
63 | renderer.render(
64 | ,
70 | );
71 | const result2 = renderer.getRenderOutput();
72 |
73 | assert.equal(
74 | result1.props.className
75 | .split(' ')
76 | .sort()
77 | .join(' '),
78 | 'tab-content tab-content-visible',
79 | );
80 |
81 | assert.equal(
82 | result2.props.className
83 | .split(' ')
84 | .sort()
85 | .join(' '),
86 | 'test-custom-class test-custom-class--visible',
87 | );
88 | });
89 | });
90 |
--------------------------------------------------------------------------------
/test/TabLink.test.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import React from 'react';
3 | import ShallowRenderer from 'react-test-renderer/shallow';
4 | import ReactTestUtils from 'react-dom/test-utils';
5 |
6 | import TabLink, { defaultActiveStyle } from '../src/components/TabLink';
7 |
8 | describe('TabLink component', () => {
9 | it('should have correct props set on an inactive tab', () => {
10 | const linkStyle = { color: 'red' };
11 |
12 | const renderer = new ShallowRenderer();
13 | renderer.render(
14 | {}} style={linkStyle} />,
15 | );
16 | const result = renderer.getRenderOutput();
17 |
18 | assert.equal(result.type, 'button');
19 | assert.equal(result.props.className, 'tab-link');
20 | assert.equal(result.props.id, 'tab-tab1');
21 | assert.equal(result.props.role, 'tab');
22 | assert.equal(result.props['aria-selected'], 'false');
23 | assert.equal(result.props['aria-controls'], 'tabpanel-tab1');
24 | assert.deepEqual(result.props.style, linkStyle);
25 | });
26 |
27 | it('should have correct props set on an active tab', () => {
28 | const linkStyle = { color: 'red' };
29 |
30 | const renderer = new ShallowRenderer();
31 | renderer.render(
32 | {}} style={linkStyle} isActive />,
33 | );
34 | const result = renderer.getRenderOutput();
35 |
36 | assert.equal(result.type, 'button');
37 | assert.equal(result.props.className, 'tab-link tab-link-active');
38 | assert.equal(result.props.id, 'tab-tab1');
39 | assert.equal(result.props.role, 'tab');
40 | assert.equal(result.props['aria-selected'], 'true');
41 | assert.equal(result.props['aria-controls'], 'tabpanel-tab1');
42 | assert.deepEqual(result.props.style, {
43 | ...linkStyle,
44 | ...defaultActiveStyle,
45 | });
46 | });
47 |
48 | it('should pass extra props to TabLink', () => {
49 | const renderer = new ShallowRenderer();
50 | renderer.render(
51 | {}}
54 | isActive
55 | tabIndex="-1"
56 | disabled
57 | />,
58 | );
59 | const result = renderer.getRenderOutput();
60 |
61 | assert.equal(result.props.tabIndex, '-1');
62 | assert.equal(result.props.disabled, true);
63 | });
64 |
65 | it('should have "activeStyle" prop content set on an active tab when provided (instead of default active style)', () => {
66 | const linkStyle = { color: 'red' };
67 | const activeStyle = { textDecoration: 'underline' };
68 |
69 | const renderer = new ShallowRenderer();
70 | renderer.render(
71 | {}}
74 | style={linkStyle}
75 | isActive
76 | activeStyle={activeStyle}
77 | />,
78 | );
79 | const result = renderer.getRenderOutput();
80 |
81 | assert.equal(result.props.className, 'tab-link tab-link-active');
82 | assert.deepEqual(result.props.style, {
83 | ...linkStyle,
84 | ...activeStyle,
85 | });
86 | });
87 |
88 | it('should have onClick handler set', () => {
89 | const renderer = new ShallowRenderer();
90 | renderer.render( {}} />);
91 | const result = renderer.getRenderOutput();
92 |
93 | assert.equal(typeof result.props.onClick, 'function');
94 | });
95 |
96 | it('should call "handleSelect" function on click providing "tab" and "namespace" values', () => {
97 | let clickedTab = '';
98 | let clickedNamespace = '';
99 |
100 | const renderer = new ShallowRenderer();
101 | renderer.render(
102 | {
106 | clickedTab = tab;
107 | clickedNamespace = namespace;
108 | }}
109 | />,
110 | );
111 | const result = renderer.getRenderOutput();
112 |
113 | result.props.onClick();
114 |
115 | assert.equal(clickedTab, 'tab1');
116 | assert.equal(clickedNamespace, 'tabs');
117 | });
118 |
119 | it('should call custom "onChange" function if provided', () => {
120 | let clickedTab = '';
121 | let clickedNamespace = '';
122 | let customOnClick = false;
123 |
124 | const renderer = new ShallowRenderer();
125 | renderer.render(
126 | {
130 | clickedTab = tab;
131 | clickedNamespace = namespace;
132 | }}
133 | onClick={() => {
134 | customOnClick = true;
135 | }}
136 | />,
137 | );
138 | const result = renderer.getRenderOutput();
139 |
140 | result.props.onClick();
141 |
142 | assert.equal(clickedTab, 'tab1');
143 | assert.equal(clickedNamespace, 'tabs');
144 | assert.equal(customOnClick, true);
145 | });
146 |
147 | it('should have "isActive" prop when initialized', () => {
148 | const tabs = ReactTestUtils.renderIntoDocument(
149 | {}} />,
150 | );
151 |
152 | const tabLink = ReactTestUtils.findRenderedDOMComponentWithClass(
153 | tabs,
154 | 'tab-link',
155 | );
156 |
157 | assert.equal(tabLink.getAttribute('class'), 'tab-link tab-link-active');
158 | });
159 |
160 | it('should not set inline styles when "disableInlineStyles" props is set', () => {
161 | const linkStyle = { color: 'red' };
162 |
163 | const renderer = new ShallowRenderer();
164 | renderer.render(
165 | {}}
168 | style={linkStyle}
169 | isActive
170 | disableInlineStyles
171 | />,
172 | );
173 | const result = renderer.getRenderOutput();
174 |
175 | assert.equal(result.props.style, undefined);
176 | });
177 |
178 | it('should support custom class names', () => {
179 | const renderer = new ShallowRenderer();
180 |
181 | renderer.render();
182 |
183 | const result1 = renderer.getRenderOutput();
184 |
185 | renderer.render(
186 | ,
192 | );
193 |
194 | const result2 = renderer.getRenderOutput();
195 |
196 | assert.equal(
197 | result1.props.className
198 | .split(' ')
199 | .sort()
200 | .join(' '),
201 | 'tab-link tab-link-active',
202 | );
203 |
204 | assert.equal(
205 | result2.props.className
206 | .split(' ')
207 | .sort()
208 | .join(' '),
209 | 'tab-link-custom tab-link-custom--active',
210 | );
211 | });
212 |
213 | it('should render into "div"', () => {
214 | const renderer = new ShallowRenderer();
215 | renderer.render();
216 | const result = renderer.getRenderOutput();
217 |
218 | assert.equal(result.type, 'div');
219 | });
220 | });
221 |
--------------------------------------------------------------------------------
/test/Tabs.test.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert';
2 | import React from 'react';
3 | import TestRenderer from 'react-test-renderer';
4 | import ShallowRenderer from 'react-test-renderer/shallow';
5 | import ReactTestUtils from 'react-dom/test-utils';
6 |
7 | import Tabs from '../src/components/Tabs';
8 | import { TabLink, TabContent } from '../src/index';
9 |
10 | describe('Tabs component', () => {
11 | it('should have correct default values set', () => {
12 | const style = { backgroundColor: 'green' };
13 |
14 | const renderer = new ShallowRenderer();
15 | renderer.render(
16 |
17 |
18 |
19 | ,
20 | );
21 | const result = renderer.getRenderOutput();
22 |
23 | assert.equal(result.type, 'div');
24 | assert.deepEqual(result.props.style, style);
25 | assert.deepEqual(result.props.className, 'tabs');
26 | });
27 |
28 | it('should set correct default props to each component', () => {
29 | const renderer = new ShallowRenderer();
30 | renderer.render(
31 |
32 |
33 |
34 | ,
35 | );
36 | const result = renderer.getRenderOutput();
37 | const tabLinks = result.props.children;
38 |
39 | tabLinks.forEach((tabLink, index) => {
40 | assert.equal(tabLink.props.namespace, 'tabs');
41 | assert.equal(tabLink.props.isActive, index === 0);
42 | assert.equal(typeof tabLink.props.activeStyle, 'undefined');
43 | assert.equal(typeof tabLink.props.handleSelect, 'function');
44 | });
45 | });
46 |
47 | it('should set "activeLinkStyle" prop to each component', () => {
48 | const activeLinkStyle = { color: 'red' };
49 |
50 | const renderer = new ShallowRenderer();
51 | renderer.render(
52 |
53 |
54 |
55 | ,
56 | );
57 | const result = renderer.getRenderOutput();
58 | const tabLinks = result.props.children;
59 |
60 | tabLinks.forEach(tabLink => {
61 | assert.deepEqual(tabLink.props.activeStyle, activeLinkStyle);
62 | });
63 | });
64 |
65 | it('should set correct default props to each component', () => {
66 | const renderer = new ShallowRenderer();
67 | renderer.render(
68 |
69 |
70 |
71 | ,
72 | );
73 | const result = renderer.getRenderOutput();
74 | const tabContents = result.props.children;
75 |
76 | tabContents.forEach(tabLink => {
77 | assert.equal(tabLink.props.isVisible, false);
78 | });
79 | });
80 |
81 | it('should set the first TabLink to active and its content to visible when initialized', () => {
82 | const tabs = ReactTestUtils.renderIntoDocument(
83 |
84 |
85 |
86 |
87 |
88 | ,
89 | );
90 |
91 | const tabLinks = ReactTestUtils.scryRenderedDOMComponentsWithClass(
92 | tabs,
93 | 'tab-link',
94 | );
95 | const tabContents = ReactTestUtils.scryRenderedDOMComponentsWithClass(
96 | tabs,
97 | 'tab-content',
98 | );
99 |
100 | assert.equal(tabLinks[0].getAttribute('class'), 'tab-link tab-link-active');
101 | assert.equal(tabLinks[1].getAttribute('class'), 'tab-link');
102 |
103 | assert.equal(tabContents[0].style.display, '');
104 | assert.equal(tabContents[1].style.display, 'none');
105 | });
106 |
107 | it('should set the TabLink with "default" prop to active and its content to visible when initialized', () => {
108 | const tabs = ReactTestUtils.renderIntoDocument(
109 |
110 |
111 |
112 |
113 |
114 | ,
115 | );
116 |
117 | const tabLinks = ReactTestUtils.scryRenderedDOMComponentsWithClass(
118 | tabs,
119 | 'tab-link',
120 | );
121 | const tabContents = ReactTestUtils.scryRenderedDOMComponentsWithClass(
122 | tabs,
123 | 'tab-content',
124 | );
125 |
126 | assert.equal(tabLinks[0].getAttribute('class'), 'tab-link');
127 | assert.equal(tabLinks[1].getAttribute('class'), 'tab-link tab-link-active');
128 |
129 | assert.equal(tabContents[0].style.display, 'none');
130 | assert.equal(tabContents[1].style.display, '');
131 | });
132 |
133 | it('should set TabContent to visible when TabLink is clicked', () => {
134 | const tabs = ReactTestUtils.renderIntoDocument(
135 |
136 |
137 |
138 |
139 |
140 | ,
141 | );
142 |
143 | const tabLinks = ReactTestUtils.scryRenderedDOMComponentsWithClass(
144 | tabs,
145 | 'tab-link',
146 | );
147 | const tabContents = ReactTestUtils.scryRenderedDOMComponentsWithClass(
148 | tabs,
149 | 'tab-content',
150 | );
151 |
152 | ReactTestUtils.Simulate.click(tabLinks[1]);
153 |
154 | assert.equal(tabLinks[0].getAttribute('class'), 'tab-link');
155 | assert.equal(tabLinks[1].getAttribute('class'), 'tab-link tab-link-active');
156 |
157 | assert.equal(tabContents[0].getAttribute('class'), 'tab-content');
158 | assert.equal(tabContents[0].style.display, 'none');
159 | assert.equal(
160 | tabContents[1].getAttribute('class'),
161 | 'tab-content tab-content-visible',
162 | );
163 | assert.equal(tabContents[1].style.display, '');
164 | });
165 |
166 | it('should use custom styles for visible TabContent', () => {
167 | const visibleTabStyle = {
168 | display: 'inline-block',
169 | backgroundColor: 'red',
170 | };
171 |
172 | const tabs = ReactTestUtils.renderIntoDocument(
173 |
174 |
175 |
176 | ,
177 | );
178 |
179 | const tabContents = ReactTestUtils.scryRenderedDOMComponentsWithClass(
180 | tabs,
181 | 'tab-content',
182 | );
183 |
184 | assert.equal(tabContents[0].style.display, 'inline-block');
185 | assert.equal(tabContents[0].style.backgroundColor, 'red');
186 | });
187 |
188 | it('should call custom "handleSelect" function when TabLink is clicked', () => {
189 | let namespace = '';
190 | let tab = '';
191 |
192 | const customSelectHandler = (selectedTab, selectedNamespace) => {
193 | tab = selectedTab;
194 | namespace = selectedNamespace;
195 | };
196 |
197 | const tabs = ReactTestUtils.renderIntoDocument(
198 |
199 |
200 |
201 | ,
202 | );
203 |
204 | const tabLinks = ReactTestUtils.scryRenderedDOMComponentsWithClass(
205 | tabs,
206 | 'tab-link',
207 | );
208 |
209 | // handleSelect should not be called during initialization
210 | assert.equal(tab, '');
211 | assert.equal(namespace, '');
212 |
213 | ReactTestUtils.Simulate.click(tabLinks[1]);
214 |
215 | assert.equal(tab, 'tab2');
216 | assert.equal(namespace, 'tabs');
217 | });
218 |
219 | it('should use "selectedTab" prop on to set selected tab', () => {
220 | const tabs = ReactTestUtils.renderIntoDocument(
221 |
222 |
223 |
224 |
225 |
226 | ,
227 | );
228 |
229 | const tabLinks = ReactTestUtils.scryRenderedDOMComponentsWithClass(
230 | tabs,
231 | 'tab-link',
232 | );
233 | const tabContents = ReactTestUtils.scryRenderedDOMComponentsWithClass(
234 | tabs,
235 | 'tab-content',
236 | );
237 |
238 | assert.equal(tabLinks[0].getAttribute('class'), 'tab-link');
239 | assert.equal(tabLinks[1].getAttribute('class'), 'tab-link tab-link-active');
240 |
241 | assert.equal(tabContents[0].style.display, 'none');
242 | assert.equal(tabContents[1].style.display, '');
243 | });
244 |
245 | it('should render only content of active tab', () => {
246 | const tabs = ReactTestUtils.renderIntoDocument(
247 |
248 |
249 |
250 | tabcontent1
251 | tabcontent2
252 | ,
253 | );
254 |
255 | const tabContents = ReactTestUtils.scryRenderedDOMComponentsWithClass(
256 | tabs,
257 | 'tab-content',
258 | );
259 |
260 | assert.equal(tabContents[0].textContent, '');
261 | assert.equal(tabContents[1].textContent, 'tabcontent2');
262 | });
263 |
264 | it('should render content of all tab, not just the active one', () => {
265 | const tabs = ReactTestUtils.renderIntoDocument(
266 |
267 |
268 |
269 | tabcontent1
270 | tabcontent2
271 | ,
272 | );
273 |
274 | const tabContents = ReactTestUtils.scryRenderedDOMComponentsWithClass(
275 | tabs,
276 | 'tab-content',
277 | );
278 |
279 | assert.equal(tabContents[0].textContent, 'tabcontent1');
280 | assert.equal(tabContents[1].textContent, 'tabcontent2');
281 | });
282 | it('should not crash when a child is null', () => {
283 | const showTab3 = false;
284 | const tabs = ReactTestUtils.renderIntoDocument(
285 |
286 |
287 |
288 | {showTab3 && }
289 | tabcontent1
290 | tabcontent2
291 | {showTab3 && tabcontent3}
292 | ,
293 | );
294 |
295 | const tabContents = ReactTestUtils.scryRenderedDOMComponentsWithClass(
296 | tabs,
297 | 'tab-content',
298 | );
299 |
300 | assert.equal(tabContents[0].textContent, 'tabcontent1');
301 | assert.equal(tabContents[1].textContent, 'tabcontent2');
302 | });
303 |
304 | it('should set "disableInlineStyles" prop to each child component', () => {
305 | const renderer = new ShallowRenderer();
306 | renderer.render(
307 |
308 |
309 |
310 |
311 |
312 | ,
313 | );
314 | const result = renderer.getRenderOutput();
315 | const tabsChildren = result.props.children;
316 |
317 | tabsChildren.forEach(child => {
318 | assert.equal(child.props.disableInlineStyles, true);
319 | });
320 | });
321 |
322 | it('should call "onChange" function on tab change', () => {
323 | const clicks = [];
324 |
325 | const onChange = (selectedTab, selectedNamespace) => {
326 | clicks.push([selectedTab, selectedNamespace]);
327 | };
328 |
329 | const tabs = ReactTestUtils.renderIntoDocument(
330 |
331 |
332 |
333 | ,
334 | );
335 |
336 | const tabLinks = ReactTestUtils.scryRenderedDOMComponentsWithClass(
337 | tabs,
338 | 'tab-link',
339 | );
340 |
341 | // onChange should be called after the initial render
342 | assert.deepEqual(clicks, [['tab2', 'tabs']]);
343 |
344 | ReactTestUtils.Simulate.click(tabLinks[1]);
345 |
346 | assert.deepEqual(clicks, [['tab2', 'tabs']]);
347 |
348 | ReactTestUtils.Simulate.click(tabLinks[0]);
349 |
350 | assert.deepEqual(clicks, [['tab2', 'tabs'], ['tab1', 'tabs']]);
351 | });
352 |
353 | it('should set correct "component" prop to component', () => {
354 | const tabs = TestRenderer.create(
355 |
356 |
357 |
358 | ,
359 | );
360 | const tabLinks = tabs.toJSON().children;
361 |
362 | assert.equal(tabLinks[0].type, 'div');
363 | assert.equal(tabLinks[1].type, 'a');
364 | });
365 | });
366 |
--------------------------------------------------------------------------------