diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000000..bd20dc1799 --- /dev/null +++ b/.babelrc @@ -0,0 +1,6 @@ +{ + "presets": [ + "es2015", + "react" + ] +} diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..75fa6231bb --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +/webpack.* diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000000..ea2fb30012 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,11 @@ +{ + "extends": "airbnb", + "env": { + "browser": true, + "node": true + }, + "rules": { + "react/prefer-es6-class": 0, + "react/prefer-stateless-function": 0 + } +} diff --git a/.gitignore b/.gitignore index 174efd6e3f..b3827e69a0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ build/ node_modules/ npm-debug.log +lib/ diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 1d5446f3da..0000000000 --- a/.npmignore +++ /dev/null @@ -1,9 +0,0 @@ -.idea -*.iml -build -example -script -specs -.travis.yml -karma.conf.js -webpack.config.js \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 4af97fc1b4..4a52886c57 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,13 +4,9 @@ node_js: - "4" - "5" - stable - -before_script: - - export DISPLAY=:99.0 - - sh -e /etc/init.d/xvfb start branches: only: - master -email: - on_failure: change - on_success: never +script: + - npm run lint + - npm test diff --git a/bower.json b/bower.json index a3c15280aa..745b94c51c 100644 --- a/bower.json +++ b/bower.json @@ -17,8 +17,10 @@ "build", "examples", "lib", + "src", "node_modules", "specs", - "package.json" + "package.json", + "webpack.*" ] -} \ No newline at end of file +} diff --git a/examples/basic/app.js b/examples/basic/app.js index d5459d3ddc..fe51d3e8da 100644 --- a/examples/basic/app.js +++ b/examples/basic/app.js @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { Tab, Tabs, TabList, TabPanel } from '../../lib/main'; +import { Tab, Tabs, TabList, TabPanel } from '../../src/main'; const App = React.createClass({ render() { @@ -83,7 +83,7 @@ const App = React.createClass({ ); - } + }, }); -ReactDOM.render(, document.getElementById('example')); +ReactDOM.render(, document.getElementById('example')); diff --git a/examples/conditional/app.js b/examples/conditional/app.js index 505462bfee..bb4cb5a926 100644 --- a/examples/conditional/app.js +++ b/examples/conditional/app.js @@ -1,13 +1,13 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { Tab, Tabs, TabList, TabPanel } from '../../lib/main'; +import { Tab, Tabs, TabList, TabPanel } from '../../src/main'; const App = React.createClass({ getInitialState() { return { showA: true, showB: true, - showC: true + showC: true, }; }, @@ -19,35 +19,35 @@ const App = React.createClass({ render() { return ( -
+


+

+

+

- { this.state.showA && Tab A } - { this.state.showB && Tab B } - { this.state.showC && Tab C } + {this.state.showA && Tab A} + {this.state.showB && Tab B} + {this.state.showC && Tab C} - { this.state.showA && This is tab A } - { this.state.showB && This is tab B } - { this.state.showC && This is tab C } + {this.state.showA && This is tab A} + {this.state.showB && This is tab B} + {this.state.showC && This is tab C}
); - } + }, }); -ReactDOM.render(, document.getElementById('example')); +ReactDOM.render(, document.getElementById('example')); diff --git a/examples/dyno/app.js b/examples/dyno/app.js index 885adc1491..98601c18ef 100644 --- a/examples/dyno/app.js +++ b/examples/dyno/app.js @@ -1,7 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import Modal from 'react-modal'; -import { Tab, Tabs, TabList, TabPanel } from '../../lib/main'; +import { Tab, Tabs, TabList, TabPanel } from '../../src/main'; Modal.setAppElement(document.getElementById('example')); Modal.injectCSS(); @@ -12,17 +12,17 @@ const App = React.createClass({ isModalOpen: false, selectedIndex: -1, tabs: [ - {label: 'Foo', content: 'This is foo'}, - {label: 'Bar', content: 'This is bar'}, - {label: 'Baz', content: 'This is baz'}, - {label: 'Zap', content: 'This is zap'} - ] + { label: 'Foo', content: 'This is foo' }, + { label: 'Bar', content: 'This is bar' }, + { label: 'Baz', content: 'This is baz' }, + { label: 'Zap', content: 'This is zap' }, + ], }; }, render() { return ( -
+

@@ -43,13 +43,13 @@ const App = React.createClass({

Add a Tab

-
-

-
-

+
+

+
+

{' '}
@@ -59,13 +59,13 @@ const App = React.createClass({ openModal() { this.setState({ - isModalOpen: true + isModalOpen: true, }); }, closeModal() { this.setState({ - isModalOpen: false + isModalOpen: false, }); }, @@ -75,10 +75,10 @@ const App = React.createClass({ this.state.tabs.push({ label: label, - content: content + content: content, }); this.setState({ - selectedIndex: this.state.tabs.length - 1 + selectedIndex: this.state.tabs.length - 1, }); this.closeModal(); }, @@ -86,7 +86,7 @@ const App = React.createClass({ removeTab(index) { this.state.tabs.splice(index, 1); this.forceUpdate(); - } + }, }); -ReactDOM.render(, document.getElementById('example')); +ReactDOM.render(, document.getElementById('example')); diff --git a/examples/focus/app.js b/examples/focus/app.js index 11ea454b53..361145f14f 100644 --- a/examples/focus/app.js +++ b/examples/focus/app.js @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { Tab, Tabs, TabList, TabPanel } from '../../lib/main'; +import { Tab, Tabs, TabList, TabPanel } from '../../src/main'; const App = React.createClass({ handleInputChange() { @@ -9,7 +9,7 @@ const App = React.createClass({ render() { return ( -
+
First @@ -28,7 +28,7 @@ const App = React.createClass({
); - } + }, }); -ReactDOM.render(, document.getElementById('example')); +ReactDOM.render(, document.getElementById('example')); diff --git a/lib/components/__tests__/Tab-test.js b/lib/components/__tests__/Tab-test.js deleted file mode 100644 index 5127ab324e..0000000000 --- a/lib/components/__tests__/Tab-test.js +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import { findDOMNode } from 'react-dom'; -import TestUtils from 'react-addons-test-utils'; -import { Tab } from '../../main'; -import { equal } from 'assert'; - -/* eslint func-names:0 */ -describe('Tab', function() { - it('should have sane defaults', function() { - const tab = TestUtils.renderIntoDocument(); - const node = findDOMNode(tab); - - equal(node.className, 'ReactTabs__Tab'); - equal(node.getAttribute('role'), 'tab'); - equal(node.getAttribute('aria-selected'), 'false'); - equal(node.getAttribute('aria-expanded'), 'false'); - equal(node.getAttribute('aria-disabled'), 'false'); - equal(node.getAttribute('aria-controls'), null); - equal(node.getAttribute('id'), null); - equal(node.innerHTML, ''); - }); - - it('should accept className', function() { - const tab = TestUtils.renderIntoDocument(); - const node = findDOMNode(tab); - - equal(node.className, 'ReactTabs__Tab foobar'); - }); - - it('should support being selected', function() { - const tab = TestUtils.renderIntoDocument(Hello); - const node = findDOMNode(tab); - - equal(node.className, 'ReactTabs__Tab ReactTabs__Tab--selected'); - equal(node.getAttribute('aria-selected'), 'true'); - equal(node.getAttribute('aria-expanded'), 'true'); - equal(node.getAttribute('aria-controls'), '1234'); - equal(node.getAttribute('id'), 'abcd'); - equal(node.innerHTML, 'Hello'); - }); - - it('should support being disabled', function() { - const tab = TestUtils.renderIntoDocument(); - const node = findDOMNode(tab); - - equal(node.className, 'ReactTabs__Tab ReactTabs__Tab--disabled'); - }); -}); diff --git a/lib/components/__tests__/TabList-test.js b/lib/components/__tests__/TabList-test.js deleted file mode 100644 index f9ce2dccbb..0000000000 --- a/lib/components/__tests__/TabList-test.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { findDOMNode } from 'react-dom'; -import TestUtils from 'react-addons-test-utils'; -import { TabList } from '../../main'; -import { equal } from 'assert'; - -/* eslint func-names:0 */ -describe('Tab', function() { - it('should have sane defaults', function() { - const tabList = TestUtils.renderIntoDocument(); - const node = findDOMNode(tabList); - - equal(node.className, 'ReactTabs__TabList'); - equal(node.getAttribute('role'), 'tablist'); - }); - - it('should accept className', function() { - const tabList = TestUtils.renderIntoDocument(); - const node = findDOMNode(tabList); - - equal(node.className, 'ReactTabs__TabList foobar'); - }); -}); diff --git a/lib/components/__tests__/TabPanel-test.js b/lib/components/__tests__/TabPanel-test.js deleted file mode 100644 index 2071adf78e..0000000000 --- a/lib/components/__tests__/TabPanel-test.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import { findDOMNode } from 'react-dom'; -import TestUtils from 'react-addons-test-utils'; -import { TabPanel } from '../../main'; -import { equal } from 'assert'; - -/* eslint func-names:0 */ -describe('Tab', function() { - it('should have sane defaults', function() { - const tabPanel = TestUtils.renderIntoDocument(Hola); - const node = findDOMNode(tabPanel); - - equal(node.className, 'ReactTabs__TabPanel'); - equal(node.getAttribute('role'), 'tabpanel'); - equal(node.getAttribute('aria-labelledby'), null); - equal(node.getAttribute('id'), null); - equal(node.innerHTML, ''); - equal(node.style.display, 'none'); - }); - - it('should accept className', function() { - const tabPanel = TestUtils.renderIntoDocument(); - const node = findDOMNode(tabPanel); - - equal(node.className, 'ReactTabs__TabPanel foobar'); - }); - - it('should support being selected', function() { - const tabPanel = TestUtils.renderIntoDocument(Hola); - const node = findDOMNode(tabPanel); - - equal(node.className, 'ReactTabs__TabPanel ReactTabs__TabPanel--selected'); - equal(node.getAttribute('aria-labelledby'), '1234'); - equal(node.getAttribute('id'), 'abcd'); - equal(node.innerHTML, 'Hola'); - equal(node.style.display, ''); - }); -}); - diff --git a/lib/components/__tests__/Tabs-test.js b/lib/components/__tests__/Tabs-test.js deleted file mode 100644 index 2e78244c5d..0000000000 --- a/lib/components/__tests__/Tabs-test.js +++ /dev/null @@ -1,260 +0,0 @@ -import React from 'react'; -import { findDOMNode } from 'react-dom'; -import TestUtils from 'react-addons-test-utils'; -import { Tabs, Tab, TabPanel, TabList } from '../../main'; -import { ok, equal } from 'assert'; - -function createTabs(props = { - selectedIndex: 0, - focus: false, - onSelect: null, - forceRenderTabPanel: false, - className: null -}) { - return ( - - - Foo - Bar - Baz - Qux - - Hello Foo - Hello Bar - Hello Baz - Hello Qux - - ); -} - -function assertTabSelected(tabs, index) { - equal(findDOMNode(tabs.getTab(index)).getAttribute('tabindex'), '0'); - equal(findDOMNode(tabs.getTab(index)).getAttribute('selected'), 'selected'); - equal(findDOMNode(tabs.getTab(index)).getAttribute('aria-selected'), 'true'); - equal(findDOMNode(tabs.getTab(index)).getAttribute('aria-expanded'), 'true'); - equal(findDOMNode(tabs.getPanel(index)).style.display, ''); -} - -/* eslint func-names:0 */ -describe('react-tabs', function() { - describe('props', function() { - it('should default to selectedIndex being 0', function() { - const tabs = TestUtils.renderIntoDocument(createTabs()); - - assertTabSelected(tabs, 0); - }); - - it('should honor selectedIndex prop', function() { - const tabs = TestUtils.renderIntoDocument(createTabs({selectedIndex: 1})); - - assertTabSelected(tabs, 1); - }); - - it('should call onSelect when selection changes', function() { - const called = {index: -1, last: -1}; - const tabs = TestUtils.renderIntoDocument(createTabs({ - onSelect: function(index, last) { - called.index = index; - called.last = last; - } - })); - - tabs.setSelected(2); - equal(called.index, 2); - equal(called.last, 0); - }); - - it('should have a default className', function() { - const tabs = TestUtils.renderIntoDocument(createTabs()); - const node = findDOMNode(tabs); - - equal(node.className, 'ReactTabs react-tabs'); - }); - - it('should accept className', function() { - const tabs = TestUtils.renderIntoDocument(createTabs({className: 'foobar'})); - const node = findDOMNode(tabs); - - equal(node.className, 'ReactTabs react-tabs foobar'); - }); - }); - - describe('a11y', function() { - it('should have appropriate role and aria attributes', function() { - const tabs = TestUtils.renderIntoDocument(createTabs()); - - equal(findDOMNode(tabs.getTabList()).getAttribute('role'), 'tablist'); - - for (let i = 0, l = tabs.getTabsCount(); i < l; i++) { - const tab = findDOMNode(tabs.getTab(i)); - const panel = findDOMNode(tabs.getPanel(i)); - - equal(tab.getAttribute('role'), 'tab'); - equal(panel.getAttribute('role'), 'tabpanel'); - - equal(tab.getAttribute('aria-controls'), panel.getAttribute('id')); - equal(panel.getAttribute('aria-labelledby'), tab.getAttribute('id')); - } - - equal(findDOMNode(tabs.getTab(3)).getAttribute('aria-disabled'), 'true'); - }); - }); - - describe('interaction', function() { - it('should update selectedIndex when clicked', function() { - const tabs = TestUtils.renderIntoDocument(createTabs()); - - TestUtils.Simulate.click(findDOMNode(tabs.getTab(1))); - assertTabSelected(tabs, 1); - }); - - it('should update selectedIndex when tab child is clicked', function() { - const tabs = TestUtils.renderIntoDocument(createTabs()); - - TestUtils.Simulate.click(findDOMNode(tabs.getTab(2)).firstChild); - assertTabSelected(tabs, 2); - }); - - it('should not change selectedIndex when clicking a disabled tab', function() { - const tabs = TestUtils.renderIntoDocument(createTabs({selectedIndex: 0})); - - TestUtils.Simulate.click(findDOMNode(tabs.getTab(3))); - assertTabSelected(tabs, 0); - }); - - // TODO: Can't seem to make this fail when removing fix :`( - // See https://github.com/rackt/react-tabs/pull/7 - // it('should preserve selectedIndex when typing', function () { - // let App = React.createClass({ - // handleKeyDown: function () { this.forceUpdate(); }, - // render: function () { - // return ( - // - // - // First - // Second - // - // 1st - // - // - // ); - // } - // }); - // - // let tabs = TestUtils.renderIntoDocument().refs.tabs; - // let input = tabs.getDOMNode().querySelector('input'); - // - // input.focus(); - // TestUtils.Simulate.keyDown(input, { - // keyCode: 'a'.charCodeAt() - // }); - // - // assertTabSelected(tabs, 1); - // }); - }); - - describe('performance', function() { - it('should only render the active tab panel', function() { - const tabs = TestUtils.renderIntoDocument(createTabs()); - - equal(findDOMNode(tabs.getPanel(0)).innerHTML, 'Hello Foo'); - equal(findDOMNode(tabs.getPanel(1)).innerHTML, ''); - equal(findDOMNode(tabs.getPanel(2)).innerHTML, ''); - - TestUtils.Simulate.click(findDOMNode(tabs.getTab(1))); - equal(findDOMNode(tabs.getPanel(0)).innerHTML, ''); - equal(findDOMNode(tabs.getPanel(1)).innerHTML, 'Hello Bar'); - equal(findDOMNode(tabs.getPanel(2)).innerHTML, ''); - - TestUtils.Simulate.click(findDOMNode(tabs.getTab(2))); - equal(findDOMNode(tabs.getPanel(0)).innerHTML, ''); - equal(findDOMNode(tabs.getPanel(1)).innerHTML, ''); - equal(findDOMNode(tabs.getPanel(2)).innerHTML, 'Hello Baz'); - }); - - it('should render all tabs if forceRenderTabPanel is true', function() { - const tabs = TestUtils.renderIntoDocument(createTabs({forceRenderTabPanel: true})); - equal(findDOMNode(tabs.getPanel(0)).innerHTML, 'Hello Foo'); - equal(findDOMNode(tabs.getPanel(1)).innerHTML, 'Hello Bar'); - equal(findDOMNode(tabs.getPanel(2)).innerHTML, 'Hello Baz'); - }); - }); - - describe('validation', function() { - it('should result with warning when tabs/panels are imbalanced', function() { - const tabs = TestUtils.renderIntoDocument( - - - Foo - - - ); - - const result = Tabs.propTypes.children(tabs.props, 'children', 'Tabs'); - ok(result instanceof Error); - }); - - it('should result with a warning when wrong element is found', function() { - const tabs = TestUtils.renderIntoDocument( - - - -
- - - - ); - - const result = Tabs.propTypes.children(tabs.props, 'children', 'Tabs'); - ok(result instanceof Error); - }); - - it('should be okay with rendering without any children', function() { - let error = false; - try { - TestUtils.renderIntoDocument( - - ); - } catch (e) { - error = true; - } - - ok(!error); - }); - - it('should be okay with rendering just TabList', function() { - let error = false; - try { - TestUtils.renderIntoDocument( - - - - ); - } catch (e) { - error = true; - } - - ok(!error); - }); - - it('should gracefully render null', function() { - let error = false; - try { - TestUtils.renderIntoDocument( - - - Tab A - { false && Tab B } - - Content A - { false && Content B } - - ); - } catch (e) { - error = true; - } - - ok(!error); - }); - }); -}); diff --git a/lib/main.js b/lib/main.js deleted file mode 100644 index f33cdcc393..0000000000 --- a/lib/main.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - Tabs: require('./components/Tabs'), - TabList: require('./components/TabList'), - Tab: require('./components/Tab'), - TabPanel: require('./components/TabPanel') -}; diff --git a/package.json b/package.json index 0f28abbc1e..1e3dce3da3 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,17 @@ "description": "React tabs component", "main": "lib/main.js", "scripts": { - "test": "./node_modules/.bin/rackt test --single-run --browsers Firefox", - "start": "./node_modules/.bin/rackt server" + "clean": "rimraf lib", + "build:commonjs": "babel src/ --out-dir lib/ --ignore __tests__,__mocks__", + "build:umd": "webpack --devtool source-map --config webpack.build.js", + "build:umd:min": "cross-env MINIFY=1 webpack --devtool source-map --config webpack.build.js", + "build": "npm run clean && npm run build:commonjs && npm run bundle", + "bundle": "mkdir -p dist && npm run build:umd && npm run build:umd:min", + "lint": "eslint src", + "preversion": "npm run lint && npm test && npm run bundle && git add dist/ && git commit -m 'Publish: build bower distribution'", + "prepublish": "npm run build", + "test": "jest", + "start": "webpack-dev-server --inline --content-base examples/" }, "repository": { "type": "git", @@ -16,6 +25,10 @@ "bugs": { "url": "https://github.com/rackt/react-tabs/issues" }, + "files": [ + "dist", + "lib" + ], "homepage": "https://github.com/rackt/react-tabs", "keywords": [ "react", @@ -28,14 +41,40 @@ "react-dom": "^0.14.7" }, "devDependencies": { - "rackt-cli": "^0.5.4", + "babel-cli": "^6.9.0", + "babel-core": "^6.9.1", + "babel-jest": "^12.1.0", + "babel-loader": "^6.2.4", + "babel-preset-es2015": "^6.9.0", + "babel-preset-react": "^6.5.0", + "cross-env": "^1.0.8", + "enzyme": "^2.3.0", + "eslint": "^2.11.1", + "eslint-config-airbnb": "^9.0.1", + "eslint-plugin-import": "^1.8.0", + "eslint-plugin-jsx-a11y": "^1.2.2", + "eslint-plugin-react": "^5.1.1", + "jest-cli": "^12.1.1", "react": "^0.14.7", + "react-addons-test-utils": "^0.14.7", "react-dom": "^0.14.7", - "react-modal": "rackt/react-modal", - "react-addons-test-utils": "^0.14.7" + "react-modal": "reactjs/react-modal", + "rimraf": "^2.5.2", + "webpack": "^1.13.1", + "webpack-dev-server": "^1.14.1" }, "dependencies": { "classnames": "^2.2", "js-stylesheet": "^0.0.1" + }, + "jest": { + "automock": false, + "testPathDirs": [ + "src" + ], + "unmockedModulePathPatterns": [ + "node_modules", + "babel" + ] } -} \ No newline at end of file +} diff --git a/src/__tests__/main-test.js b/src/__tests__/main-test.js new file mode 100644 index 0000000000..e94db46825 --- /dev/null +++ b/src/__tests__/main-test.js @@ -0,0 +1,15 @@ +/* global describe, it, expect */ +import { Tab, Tabs, TabList, TabPanel } from '../main'; +import TabComponent from '../components/Tab'; +import TabListComponent from '../components/TabList'; +import TabsComponent from '../components/Tabs'; +import TabPanelComponent from '../components/TabPanel'; + +describe('', () => { + it('should correctly export all components', () => { + expect(Tab).toEqual(TabComponent); + expect(TabList).toEqual(TabListComponent); + expect(Tabs).toEqual(TabsComponent); + expect(TabPanel).toEqual(TabPanelComponent); + }); +}); diff --git a/lib/components/Tab.js b/src/components/Tab.js similarity index 90% rename from lib/components/Tab.js rename to src/components/Tab.js index 51090143ed..f323a36da1 100644 --- a/lib/components/Tab.js +++ b/src/components/Tab.js @@ -1,4 +1,4 @@ -import React, {PropTypes} from 'react'; +import React, { PropTypes } from 'react'; import { findDOMNode } from 'react-dom'; import cx from 'classnames'; @@ -27,8 +27,8 @@ module.exports = React.createClass({ children: PropTypes.oneOfType([ PropTypes.array, PropTypes.object, - PropTypes.string - ]) + PropTypes.string, + ]), }, getDefaultProps() { @@ -36,7 +36,7 @@ module.exports = React.createClass({ focus: false, selected: false, id: null, - panelId: null + panelId: null, }; }, @@ -56,7 +56,7 @@ module.exports = React.createClass({ this.props.className, { 'ReactTabs__Tab--selected': this.props.selected, - 'ReactTabs__Tab--disabled': this.props.disabled + 'ReactTabs__Tab--disabled': this.props.disabled, } )} role="tab" @@ -69,5 +69,5 @@ module.exports = React.createClass({ {this.props.children} ); - } + }, }); diff --git a/lib/components/TabList.js b/src/components/TabList.js similarity index 84% rename from lib/components/TabList.js rename to src/components/TabList.js index 2f98267b11..f5b96a673e 100644 --- a/lib/components/TabList.js +++ b/src/components/TabList.js @@ -1,4 +1,4 @@ -import React, {PropTypes} from 'react'; +import React, { PropTypes } from 'react'; import cx from 'classnames'; module.exports = React.createClass({ @@ -8,8 +8,8 @@ module.exports = React.createClass({ className: PropTypes.string, children: PropTypes.oneOfType([ PropTypes.object, - PropTypes.array - ]) + PropTypes.array, + ]), }, render() { @@ -24,5 +24,5 @@ module.exports = React.createClass({ {this.props.children} ); - } + }, }); diff --git a/lib/components/TabPanel.js b/src/components/TabPanel.js similarity index 81% rename from lib/components/TabPanel.js rename to src/components/TabPanel.js index 7ed7888b06..81343d2f6e 100644 --- a/lib/components/TabPanel.js +++ b/src/components/TabPanel.js @@ -1,4 +1,4 @@ -import React, {PropTypes} from 'react'; +import React, { PropTypes } from 'react'; import cx from 'classnames'; module.exports = React.createClass({ @@ -12,19 +12,19 @@ module.exports = React.createClass({ children: PropTypes.oneOfType([ PropTypes.array, PropTypes.object, - PropTypes.string - ]) + PropTypes.string, + ]), }, contextTypes: { - forceRenderTabPanel: PropTypes.bool + forceRenderTabPanel: PropTypes.bool, }, getDefaultProps() { return { selected: false, id: null, - tabId: null + tabId: null, }; }, @@ -39,16 +39,16 @@ module.exports = React.createClass({ 'ReactTabs__TabPanel', this.props.className, { - 'ReactTabs__TabPanel--selected': this.props.selected + 'ReactTabs__TabPanel--selected': this.props.selected, } )} role="tabpanel" id={this.props.id} aria-labelledby={this.props.tabId} - style={{display: this.props.selected ? null : 'none'}} + style={{ display: this.props.selected ? null : 'none' }} > {children}
); - } + }, }); diff --git a/lib/components/Tabs.js b/src/components/Tabs.js similarity index 92% rename from lib/components/Tabs.js rename to src/components/Tabs.js index ad99d53fbe..200a2c9ef8 100644 --- a/lib/components/Tabs.js +++ b/src/components/Tabs.js @@ -1,4 +1,4 @@ -import React, {PropTypes, cloneElement} from 'react'; +import React, { PropTypes, cloneElement } from 'react'; import { findDOMNode } from 'react-dom'; import cx from 'classnames'; import jss from 'js-stylesheet'; @@ -26,24 +26,24 @@ module.exports = React.createClass({ onSelect: PropTypes.func, focus: PropTypes.bool, children: childrenPropType, - forceRenderTabPanel: PropTypes.bool + forceRenderTabPanel: PropTypes.bool, }, childContextTypes: { - forceRenderTabPanel: PropTypes.bool + forceRenderTabPanel: PropTypes.bool, }, statics: { setUseDefaultStyles(use) { useDefaultStyles = use; - } + }, }, getDefaultProps() { return { selectedIndex: -1, focus: false, - forceRenderTabPanel: false + forceRenderTabPanel: false, }; }, @@ -53,13 +53,13 @@ module.exports = React.createClass({ getChildContext() { return { - forceRenderTabPanel: this.props.forceRenderTabPanel + forceRenderTabPanel: this.props.forceRenderTabPanel, }; }, componentDidMount() { if (useDefaultStyles) { - jss(require('../helpers/styles.js')); + jss(require('../helpers/styles.js')); // eslint-disable-line global-require } }, @@ -67,47 +67,6 @@ module.exports = React.createClass({ this.setState(this.copyPropsToState(newProps)); }, - handleClick(e) { - let node = e.target; - do { - if (isTabNode(node)) { - if (isTabDisabled(node)) { - return; - } - - const index = [].slice.call(node.parentNode.children).indexOf(node); - this.setSelected(index); - return; - } - } while ((node = node.parentNode) !== null); - }, - - handleKeyDown(e) { - if (isTabNode(e.target)) { - let index = this.state.selectedIndex; - let preventDefault = false; - - // Select next tab to the left - if (e.keyCode === 37 || e.keyCode === 38) { - index = this.getPrevTab(index); - preventDefault = true; - } - // Select next tab to the right - /* eslint brace-style:0 */ - else if (e.keyCode === 39 || e.keyCode === 40) { - index = this.getNextTab(index); - preventDefault = true; - } - - // This prevents scrollbars from moving around - if (preventDefault) { - e.preventDefault(); - } - - this.setSelected(index, true); - } - }, - setSelected(index, focus) { // Don't do anything if nothing has changed if (index === this.state.selectedIndex) return; @@ -188,11 +147,11 @@ module.exports = React.createClass({ }, getTab(index) { - return this.refs['tabs-' + index]; + return this.refs[`tabs-${index}`]; }, getPanel(index) { - return this.refs['panels-' + index]; + return this.refs[`panels-${index}`]; }, getChildren() { @@ -234,7 +193,7 @@ module.exports = React.createClass({ return null; } - const ref = 'tabs-' + index; + const ref = `tabs-${index}`; const id = tabIds[index]; const panelId = panelIds[index]; const selected = state.selectedIndex === index; @@ -247,9 +206,9 @@ module.exports = React.createClass({ id, panelId, selected, - focus + focus, }); - }) + }), }); // Reset index for panels @@ -257,7 +216,7 @@ module.exports = React.createClass({ } // Clone TabPanel components to have refs else { - const ref = 'panels-' + index; + const ref = `panels-${index}`; const id = panelIds[index]; const tabId = tabIds[index]; const selected = state.selectedIndex === index; @@ -268,7 +227,7 @@ module.exports = React.createClass({ ref, id, tabId, - selected + selected, }); } @@ -276,6 +235,73 @@ module.exports = React.createClass({ }); }, + handleKeyDown(e) { + if (isTabNode(e.target)) { + let index = this.state.selectedIndex; + let preventDefault = false; + + // Select next tab to the left + if (e.keyCode === 37 || e.keyCode === 38) { + index = this.getPrevTab(index); + preventDefault = true; + } + // Select next tab to the right + /* eslint brace-style:0 */ + else if (e.keyCode === 39 || e.keyCode === 40) { + index = this.getNextTab(index); + preventDefault = true; + } + + // This prevents scrollbars from moving around + if (preventDefault) { + e.preventDefault(); + } + + this.setSelected(index, true); + } + }, + + handleClick(e) { + let node = e.target; + do { // eslint-disable-line no-cond-assign + if (isTabNode(node)) { + if (isTabDisabled(node)) { + return; + } + + const index = [].slice.call(node.parentNode.children).indexOf(node); + this.setSelected(index); + return; + } + } while ((node = node.parentNode) !== null); + }, + + // This is an anti-pattern, so sue me + copyPropsToState(props) { + let selectedIndex = props.selectedIndex; + + // If no selectedIndex prop was supplied, then try + // preserving the existing selectedIndex from state. + // If the state has not selectedIndex, default + // to the first tab in the TabList. + // + // TODO: Need automation testing around this + // Manual testing can be done using examples/focus + // See 'should preserve selectedIndex when typing' in specs/Tabs.spec.js + if (selectedIndex === -1) { + if (this.state && this.state.selectedIndex) { + selectedIndex = this.state.selectedIndex; + } else { + selectedIndex = 0; + } + } + + return { + selectedIndex, + focus: props.focus, + }; + }, + render() { // This fixes an issue with focus management. // @@ -310,30 +336,4 @@ module.exports = React.createClass({
); }, - - // This is an anti-pattern, so sue me - copyPropsToState(props) { - let selectedIndex = props.selectedIndex; - - // If no selectedIndex prop was supplied, then try - // preserving the existing selectedIndex from state. - // If the state has not selectedIndex, default - // to the first tab in the TabList. - // - // TODO: Need automation testing around this - // Manual testing can be done using examples/focus - // See 'should preserve selectedIndex when typing' in specs/Tabs.spec.js - if (selectedIndex === -1) { - if (this.state && this.state.selectedIndex) { - selectedIndex = this.state.selectedIndex; - } else { - selectedIndex = 0; - } - } - - return { - selectedIndex: selectedIndex, - focus: props.focus - }; - } }); diff --git a/src/components/__tests__/Tab-test.js b/src/components/__tests__/Tab-test.js new file mode 100644 index 0000000000..0f3e758ff8 --- /dev/null +++ b/src/components/__tests__/Tab-test.js @@ -0,0 +1,47 @@ +/* global jest, describe, it, expect */ +import React from 'react'; +import { shallow } from 'enzyme'; +import Tab from '../Tab'; + +describe('', () => { + it('should have sane defaults', () => { + const wrapper = shallow(); + + expect(wrapper.hasClass('ReactTabs__Tab')).toBe(true); + expect(wrapper.prop('role')).toBe('tab'); + expect(wrapper.prop('aria-selected')).toBe('false'); + expect(wrapper.prop('aria-expanded')).toBe('false'); + expect(wrapper.prop('aria-disabled')).toBe('false'); + expect(wrapper.prop('aria-controls')).toBe(null); + expect(wrapper.prop('id')).toBe(null); + expect(wrapper.children().length).toBe(0); + }); + + it('should accept className', () => { + const wrapper = shallow(); + + expect(wrapper.hasClass('ReactTabs__Tab')).toBe(true); + expect(wrapper.hasClass('foobar')).toBe(true); + }); + + it('should support being selected', () => { + const wrapper = shallow(Hello); + + expect(wrapper.hasClass('ReactTabs__Tab')).toBe(true); + expect(wrapper.hasClass('ReactTabs__Tab--selected')).toBe(true); + expect(wrapper.prop('aria-selected')).toBe('true'); + expect(wrapper.prop('aria-expanded')).toBe('true'); + expect(wrapper.prop('aria-disabled')).toBe('false'); + expect(wrapper.prop('aria-controls')).toBe('1234'); + expect(wrapper.prop('id')).toBe('abcd'); + expect(wrapper.text()).toBe('Hello'); + }); + + it('should support being disabled', () => { + const wrapper = shallow(); + + expect(wrapper.hasClass('ReactTabs__Tab')).toBe(true); + expect(wrapper.hasClass('ReactTabs__Tab--disabled')).toBe(true); + expect(wrapper.prop('aria-disabled')).toBe('true'); + }); +}); diff --git a/src/components/__tests__/TabList-test.js b/src/components/__tests__/TabList-test.js new file mode 100644 index 0000000000..9ffb57dc02 --- /dev/null +++ b/src/components/__tests__/TabList-test.js @@ -0,0 +1,20 @@ +/* global jest, describe, it, expect */ +import React from 'react'; +import { shallow } from 'enzyme'; +import TabList from '../TabList'; + +describe('', () => { + it('should have sane defaults', () => { + const wrapper = shallow(); + + expect(wrapper.hasClass('ReactTabs__TabList')).toBe(true); + expect(wrapper.prop('role')).toBe('tablist'); + }); + + it('should accept className', () => { + const wrapper = shallow(); + + expect(wrapper.hasClass('ReactTabs__TabList')).toBe(true); + expect(wrapper.hasClass('foobar')).toBe(true); + }); +}); diff --git a/src/components/__tests__/TabPanel-test.js b/src/components/__tests__/TabPanel-test.js new file mode 100644 index 0000000000..88d7a8dcc6 --- /dev/null +++ b/src/components/__tests__/TabPanel-test.js @@ -0,0 +1,39 @@ +/* global jest, describe, it, expect */ +import React from 'react'; +import { shallow } from 'enzyme'; +import TabPanel from '../TabPanel'; + +describe('Tab', () => { + it('should have sane defaults', () => { + const wrapper = shallow(Hola); + + expect(wrapper.hasClass('ReactTabs__TabPanel')).toBe(true); + expect(wrapper.prop('role')).toBe('tabpanel'); + expect(wrapper.prop('aria-labelledby')).toBe(null); + expect(wrapper.prop('id')).toBe(null); + expect(wrapper.children().length).toBe(0); + expect(wrapper.children().length).toBe(0); + expect(wrapper.prop('style')).not.toBe(null); + expect(wrapper.prop('style').display).toBe('none'); + }); + + it('should accept className', () => { + const wrapper = shallow(); + + expect(wrapper.hasClass('ReactTabs__TabPanel')).toBe(true); + expect(wrapper.hasClass('foobar')).toBe(true); + }); + + it('should support being selected', () => { + const wrapper = shallow(Hola); + + expect(wrapper.hasClass('ReactTabs__TabPanel')).toBe(true); + expect(wrapper.hasClass('ReactTabs__TabPanel--selected')).toBe(true); + expect(wrapper.prop('aria-labelledby')).toBe('1234'); + expect(wrapper.prop('id')).toBe('abcd'); + expect(wrapper.text()).toBe('Hola'); + expect(wrapper.prop('style')).not.toBe(null); + expect(wrapper.prop('style').display).toBe(null); + }); +}); + diff --git a/src/components/__tests__/Tabs-test.js b/src/components/__tests__/Tabs-test.js new file mode 100644 index 0000000000..5ba890603b --- /dev/null +++ b/src/components/__tests__/Tabs-test.js @@ -0,0 +1,245 @@ +/* global jest, describe, it, expect */ +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import Tab from '../Tab'; +import TabList from '../TabList'; +import TabPanel from '../TabPanel'; +import Tabs from '../Tabs'; + +function createTabs(props = { + selectedIndex: 0, + focus: false, + onSelect: null, + forceRenderTabPanel: false, + className: null, +}) { + return ( + + + Foo + Bar + Baz + Qux + + Hello Foo + Hello Bar + Hello Baz + Hello Qux + + ); +} + +function assertTabSelected(wrapper, index) { + const tab = wrapper.childAt(0).childAt(index); + const panel = wrapper.childAt(index + 1); + + expect(tab.prop('selected')).toBe(true); + expect(panel.prop('selected')).toBe(true); +} + +describe('react-tabs', () => { + describe('props', () => { + it('should default to selectedIndex being 0', () => { + const wrapper = shallow(createTabs()); + + assertTabSelected(wrapper, 0); + }); + + it('should honor selectedIndex prop', () => { + const wrapper = shallow(createTabs({ selectedIndex: 1 })); + + assertTabSelected(wrapper, 1); + }); + + it('should call onSelect when selection changes', () => { + const called = { index: -1, last: -1 }; + const wrapper = shallow(createTabs({ + onSelect(index, last) { + called.index = index; + called.last = last; + }, + })); + + wrapper.instance().setSelected(2); + expect(called.index).toBe(2); + expect(called.last).toBe(0); + }); + + it('should have a default className', () => { + const wrapper = shallow(createTabs()); + + expect(wrapper.hasClass('ReactTabs')).toBe(true); + expect(wrapper.hasClass('react-tabs')).toBe(true); + }); + + it('should accept className', () => { + const wrapper = shallow(createTabs({ className: 'foobar' })); + + expect(wrapper.hasClass('ReactTabs')).toBe(true); + expect(wrapper.hasClass('react-tabs')).toBe(true); + expect(wrapper.hasClass('foobar')).toBe(true); + }); + }); + + describe('child props', () => { + it('should set disabled on disabled node', () => { + const wrapper = mount(createTabs()); + const tablist = wrapper.childAt(0); + + expect(tablist.childAt(3).prop('disabled')).toBe(true); + }); + + it('should set ids correctly', () => { + const wrapper = mount(createTabs()); + const tablist = wrapper.childAt(0); + + for (let i = 0, l = wrapper.instance().getTabsCount(); i < l; i++) { + const tab = tablist.childAt(i); + const panel = wrapper.childAt(i + 1); + + expect(tab.prop('id')).toBe(panel.prop('tabId')); + expect(panel.prop('id')).toBe(tab.prop('panelId')); + } + }); + }); + + describe('interaction', () => { + it('should update selectedIndex when clicked', () => { + const wrapper = mount(createTabs()); + wrapper.childAt(0).childAt(1).simulate('click'); + + assertTabSelected(wrapper, 1); + }); + + it('should update selectedIndex when tab child is clicked', () => { + const wrapper = mount(createTabs()); + const tablist = wrapper.childAt(0); + tablist.childAt(2).first().simulate('click'); + + assertTabSelected(wrapper, 2); + }); + + it('should not change selectedIndex when clicking a disabled tab', () => { + const wrapper = mount(createTabs({ selectedIndex: 0 })); + + wrapper.childAt(0).childAt(3).simulate('click'); + assertTabSelected(wrapper, 0); + }); + + // TODO: Can't seem to make this fail when removing fix :`( + // See https://github.com/rackt/react-tabs/pull/7 + // it('should preserve selectedIndex when typing', function () { + // let App = React.createClass({ + // handleKeyDown: function () { this.forceUpdate(); }, + // render: function () { + // return ( + // + // + // First + // Second + // + // 1st + // + // + // ); + // } + // }); + // + // let tabs = TestUtils.renderIntoDocument().refs.tabs; + // let input = tabs.getDOMNode().querySelector('input'); + // + // input.focus(); + // TestUtils.Simulate.keyDown(input, { + // keyCode: 'a'.charCodeAt() + // }); + // + // assertTabSelected(tabs, 1); + // }); + }); + + describe('performance', () => { + it('should only render the active tab panel', () => { + const wrapper = mount(createTabs()); + + expect(wrapper.childAt(1).text()).toBe('Hello Foo'); + expect(wrapper.childAt(2).text()).toBe(''); + expect(wrapper.childAt(3).text()).toBe(''); + + wrapper.childAt(0).childAt(1).simulate('click'); + + expect(wrapper.childAt(1).text()).toBe(''); + expect(wrapper.childAt(2).text()).toBe('Hello Bar'); + expect(wrapper.childAt(3).text()).toBe(''); + + + wrapper.childAt(0).childAt(2).simulate('click'); + + expect(wrapper.childAt(1).text()).toBe(''); + expect(wrapper.childAt(2).text()).toBe(''); + expect(wrapper.childAt(3).text()).toBe('Hello Baz'); + }); + + it('should render all tabs if forceRenderTabPanel is true', () => { + const wrapper = mount(createTabs({ forceRenderTabPanel: true })); + + expect(wrapper.childAt(1).text()).toBe('Hello Foo'); + expect(wrapper.childAt(2).text()).toBe('Hello Bar'); + expect(wrapper.childAt(3).text()).toBe('Hello Baz'); + }); + }); + + describe('validation', () => { + it('should result with warning when tabs/panels are imbalanced', () => { + const wrapper = shallow( + + + Foo + + + ); + + const result = Tabs.propTypes.children(wrapper.props(), 'children', 'Tabs'); + expect(result instanceof Error).toBe(true); + }); + + it('should result with a warning when wrong element is found', () => { + const wrapper = shallow( + + + +
+ + + + ); + + const result = Tabs.propTypes.children(wrapper.props(), 'children', 'Tabs'); + expect(result instanceof Error).toBe(true); + }); + + it('should be okay with rendering without any children', () => { + expect(() => shallow()).not.toThrow(); + }); + + it('should be okay with rendering just TabList', () => { + expect(() => shallow( + + + + )).not.toThrow(); + }); + + it('should gracefully render null', () => { + expect(() => shallow( + + + Tab A + {false && Tab B} + + Content A + {false && Content B} + + )).not.toThrow(); + }); + }); +}); diff --git a/lib/helpers/childrenPropType.js b/src/helpers/childrenPropType.js similarity index 77% rename from lib/helpers/childrenPropType.js rename to src/helpers/childrenPropType.js index 4eac704c60..94be6e64fe 100644 --- a/lib/helpers/childrenPropType.js +++ b/src/helpers/childrenPropType.js @@ -27,7 +27,7 @@ module.exports = function childrenPropTypes(props, propName) { tabsCount++; } else { error = new Error( - 'Expected `Tab` but found `' + (c.type.displayName || c.type) + '`' + `Expected 'Tab' but found '${c.type.displayName || c.type}'` ); } }); @@ -35,15 +35,15 @@ module.exports = function childrenPropTypes(props, propName) { panelsCount++; } else { error = new Error( - 'Expected `TabList` or `TabPanel` but found `' + (child.type.displayName || child.type) + '`' + `Expected 'TabList' or 'TabPanel' but found '${child.type.displayName || child.type}'` ); } }); if (tabsCount !== panelsCount) { error = new Error( - 'There should be an equal number of `Tabs` and `TabPanels`. ' + - 'Received ' + tabsCount + ' `Tabs` and ' + panelsCount + ' `TabPanels`.' + "There should be an equal number of 'Tabs' and 'TabPanels'." + + `Received ${tabsCount} 'Tabs' and ${panelsCount} 'TabPanels'.` ); } diff --git a/lib/helpers/styles.js b/src/helpers/styles.js similarity index 51% rename from lib/helpers/styles.js rename to src/helpers/styles.js index b122cda56d..a444e6b640 100644 --- a/lib/helpers/styles.js +++ b/src/helpers/styles.js @@ -1,48 +1,48 @@ module.exports = { '.react-tabs [role=tablist]': { 'border-bottom': '1px solid #aaa', - 'margin': '0 0 10px', - 'padding': '0' + margin: '0 0 10px', + padding: '0', }, '.react-tabs [role=tab]': { - 'display': 'inline-block', - 'border': '1px solid transparent', + display: 'inline-block', + border: '1px solid transparent', 'border-bottom': 'none', - 'bottom': '-1px', - 'position': 'relative', + bottom: '-1px', + position: 'relative', 'list-style': 'none', - 'padding': '6px 12px', - 'cursor': 'pointer' + padding: '6px 12px', + cursor: 'pointer', }, '.react-tabs [role=tab][aria-selected=true]': { - 'background': '#fff', + background: '#fff', 'border-color': '#aaa', - 'color': 'black', + color: 'black', 'border-radius': '5px 5px 0 0', '-moz-border-radius': '5px 5px 0 0', - '-webkit-border-radius': '5px 5px 0 0' + '-webkit-border-radius': '5px 5px 0 0', }, '.react-tabs [role=tab][aria-disabled=true]': { - 'color': 'GrayText', - 'cursor': 'default' + color: 'GrayText', + cursor: 'default', }, '.react-tabs [role=tab]:focus': { 'box-shadow': '0 0 5px hsl(208, 99%, 50%)', 'border-color': 'hsl(208, 99%, 50%)', - 'outline': 'none' + outline: 'none', }, '.react-tabs [role=tab]:focus:after': { - 'content': '""', - 'position': 'absolute', - 'height': '5px', - 'left': '-4px', - 'right': '-4px', - 'bottom': '-5px', - 'background': '#fff' - } + content: '""', + position: 'absolute', + height: '5px', + left: '-4px', + right: '-4px', + bottom: '-5px', + background: '#fff', + }, }; diff --git a/lib/helpers/uuid.js b/src/helpers/uuid.js similarity index 73% rename from lib/helpers/uuid.js rename to src/helpers/uuid.js index 0f71d77561..7e9e17ee47 100644 --- a/lib/helpers/uuid.js +++ b/src/helpers/uuid.js @@ -1,5 +1,5 @@ // Get a universally unique identifier let count = 0; module.exports = function uuid() { - return 'react-tabs-' + count++; + return `react-tabs-${count++}`; }; diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000000..c6fd0fd0b1 --- /dev/null +++ b/src/main.js @@ -0,0 +1,4 @@ +export { default as Tabs } from './components/Tabs'; +export { default as TabList } from './components/TabList'; +export { default as Tab } from './components/Tab'; +export { default as TabPanel } from './components/TabPanel'; diff --git a/webpack.build.js b/webpack.build.js new file mode 100644 index 0000000000..743ee3ba26 --- /dev/null +++ b/webpack.build.js @@ -0,0 +1,40 @@ +var path = require('path'); +var webpack = require('webpack'); +var BASE_DIR = process.cwd(); +var COMPONENT_FILE = 'react-tabs'; +var COMPONENT_NAME = 'ReactTabs'; +var plugins = []; + +function getPackageMain() { + return require(path.resolve(BASE_DIR, 'package.json')).main; +} + +if (process.env.MINIFY) { + plugins.push( + new webpack.optimize.UglifyJsPlugin() + ); + COMPONENT_FILE += '.min'; +} + +module.exports = { + entry: path.resolve(BASE_DIR, getPackageMain()), + output: { + filename: path.resolve(BASE_DIR, 'dist/' + COMPONENT_FILE + '.js'), + library: COMPONENT_NAME, + libraryTarget: 'umd' + }, + externals: { + 'react': 'React', + 'react-dom': 'ReactDOM' + }, + module: { + loaders: [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel-loader' + } + ] + }, + plugins: plugins +}; diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000000..57c306186a --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,48 @@ +var fs = require('fs'); +var path = require('path'); +var webpack = require('webpack'); +var EXAMPLES_DIR = path.resolve(process.cwd(), 'examples'); + +function buildEntries() { + return fs.readdirSync(EXAMPLES_DIR).reduce(function (entries, dir) { + if (dir === 'build') { + return entries; + } + + var isDraft = dir.charAt(0) === '_'; + var isDirectory = fs.lstatSync(path.join(EXAMPLES_DIR, dir)).isDirectory(); + + if (!isDraft && isDirectory) { + entries[dir] = path.join(EXAMPLES_DIR, dir, 'app.js'); + } + + return entries; + }, {}); +} + +module.exports = { + + entry: buildEntries(), + + output: { + filename: '[name].js', + chunkFilename: '[id].chunk.js', + path: 'examples/__build__', + publicPath: '/__build__/' + }, + + module: { + loaders: [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel-loader' + } + ] + }, + + plugins: [ + new webpack.optimize.CommonsChunkPlugin('shared.js') + ] + +};