diff --git a/.gitignore b/.gitignore index 123ae94..4787bc4 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ build/Release # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules + +# Ignore compiled files +lib diff --git a/.zuul.yml b/.zuul.yml index 3098ff9..71c8396 100644 --- a/.zuul.yml +++ b/.zuul.yml @@ -3,4 +3,4 @@ browsers: - name: chrome version: latest browserify: - - transform: reactify \ No newline at end of file + - transform: babelify diff --git a/components/Dropdown.js b/components/Dropdown.js deleted file mode 100644 index 4412882..0000000 --- a/components/Dropdown.js +++ /dev/null @@ -1,82 +0,0 @@ -var React = require('react'); -var cx = require('classnames'); - -var DropdownTrigger = require('./DropdownTrigger.js'); -var DropdownContent = require('./DropdownContent.js'); - -var Dropdown = React.createClass({ - getInitialState: function(){ - return { - active: false - }; - }, - componentDidMount: function () { - window.addEventListener( 'click', this._onWindowClick ); - }, - componentWillUnmount: function () { - window.removeEventListener( 'click', this._onWindowClick ); - }, - render: function () { - // create component classes - var active = this.isActive(); - var dropdown_classes = cx({ - dropdown: true, - 'dropdown--active': active - }); - if( this.props.className ){ - dropdown_classes += ' ' + this.props.className; - } - // stick callback on trigger element - var children = React.Children.map( this.props.children, function( child ){ - if( child.type === DropdownTrigger ){ - child = React.cloneElement( child, { - ref: 'trigger', - onClick: this._onToggleClick - }); - } - return child; - }, this); - return React.createElement('div', { - className: dropdown_classes - }, children); - }, - isActive: function(){ - return ( typeof this.props.active === 'boolean' ) ? - this.props.active : - this.state.active; - }, - hide: function(){ - this.setState({ - active: false - }); - if( this.props.onHide ){ - this.props.onHide(); - } - }, - show: function(){ - this.setState({ - active: true - }); - if( this.props.onShow ){ - this.props.onShow(); - } - }, - _onWindowClick: function( event ){ - var dropdown_element = this.getDOMNode(); - if( event.target !== dropdown_element && !dropdown_element.contains( event.target ) && this.isActive() ){ - this.hide(); - } - }, - _onToggleClick: function( event ){ - event.preventDefault(); - if( this.isActive() ){ - this.hide(); - } else { - this.show(); - } - } -}); - -module.exports = Dropdown; -module.exports.DropdownTrigger = DropdownTrigger; -module.exports.DropdownContent = DropdownContent; \ No newline at end of file diff --git a/components/Dropdown.jsx b/components/Dropdown.jsx new file mode 100644 index 0000000..5062007 --- /dev/null +++ b/components/Dropdown.jsx @@ -0,0 +1,84 @@ +import React, { cloneElement, createClass } from 'react'; +import { findDOMNode } from 'react-dom'; +import cx from 'classnames'; + +import DropdownTrigger from './DropdownTrigger.js'; +import DropdownContent from './DropdownContent.js'; + +var Dropdown = createClass({ + getInitialState: function(){ + return { + active: false + }; + }, + getDefaultProps: function(){ + return { + className: '' + } + }, + componentDidMount: function () { + window.addEventListener( 'click', this._onWindowClick ); + }, + componentWillUnmount: function () { + window.removeEventListener( 'click', this._onWindowClick ); + }, + render: function () { + const { children, className } = this.props; + // create component classes + const active = this.isActive(); + var dropdown_classes = cx({ + dropdown: true, + 'dropdown--active': active + }); + dropdown_classes += ' ' + className; + // stick callback on trigger element + const bound_children = React.Children.map( children, child => { + if( child.type === DropdownTrigger ){ + child = cloneElement( child, { + ref: 'trigger', + onClick: this._onToggleClick + }); + } + return child; + }); + return
{bound_children}
; + }, + isActive: function(){ + return ( typeof this.props.active === 'boolean' ) ? + this.props.active : + this.state.active; + }, + hide: function(){ + this.setState({ + active: false + }); + if( this.props.onHide ){ + this.props.onHide(); + } + }, + show: function(){ + this.setState({ + active: true + }); + if( this.props.onShow ){ + this.props.onShow(); + } + }, + _onWindowClick: function( event ){ + const dropdown_element = findDOMNode( this ); + if( event.target !== dropdown_element && !dropdown_element.contains( event.target ) && this.isActive() ){ + this.hide(); + } + }, + _onToggleClick: function( event ){ + event.preventDefault(); + if( this.isActive() ){ + this.hide(); + } else { + this.show(); + } + } +}); + +export { DropdownTrigger, DropdownContent }; +export default Dropdown; diff --git a/components/DropdownContent.js b/components/DropdownContent.js deleted file mode 100644 index c680ffa..0000000 --- a/components/DropdownContent.js +++ /dev/null @@ -1,15 +0,0 @@ -var React = require('react'); - -var DropdownContent = React.createClass({ - render: function(){ - var dropdown_content_classes = 'dropdown__content'; - if( this.props.className ){ - dropdown_content_classes += ' ' + this.props.className; - } - return React.createElement( 'div', { - className: dropdown_content_classes - }, this.props.children ); - } -}); - -module.exports = DropdownContent; \ No newline at end of file diff --git a/components/DropdownContent.jsx b/components/DropdownContent.jsx new file mode 100644 index 0000000..af53401 --- /dev/null +++ b/components/DropdownContent.jsx @@ -0,0 +1,20 @@ +import React, { createClass, PropTypes } from 'react'; + +const DropdownContent = createClass({ + propTypes: { + children: PropTypes.any, + className: PropTypes.string + }, + getDefaultProps: function(){ + return { + className: '' + } + }, + render: function(){ + const { children, className } = this.props; + const classes = 'dropdown__content ' + className; + return
{children}
; + } +}); + +export default DropdownContent; diff --git a/components/DropdownTrigger.js b/components/DropdownTrigger.js deleted file mode 100644 index 321a408..0000000 --- a/components/DropdownTrigger.js +++ /dev/null @@ -1,17 +0,0 @@ -var React = require('react'); - -var DropdownTrigger = React.createClass({ - render: function(){ - var dropdown_trigger_classes = 'dropdown__trigger'; - if( this.props.className ){ - dropdown_trigger_classes += ' ' + this.props.className; - } - return React.createElement( 'a', { - className: dropdown_trigger_classes, - href: '#dropdown-trigger', - onClick: this.props.onClick - }, this.props.children ); - } -}); - -module.exports = DropdownTrigger; \ No newline at end of file diff --git a/components/DropdownTrigger.jsx b/components/DropdownTrigger.jsx new file mode 100644 index 0000000..a6ad80e --- /dev/null +++ b/components/DropdownTrigger.jsx @@ -0,0 +1,25 @@ +import React, { createClass, PropTypes } from 'react'; + +const DropdownTrigger = createClass({ + propTypes: { + children: PropTypes.any, + className: PropTypes.string, + onClick: PropTypes.func + }, + getDefaultProps: function(){ + return { + className: '' + }; + }, + render: function(){ + const { children, className, onClick } = this.props; + const classes = 'dropdown__trigger ' + className; + return ( + + {children} + + ); + } +}); + +export default DropdownTrigger; diff --git a/package.json b/package.json index 6c137ac..53d74b7 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,16 @@ "name": "react-simple-dropdown", "version": "0.1.1", "description": "Non-prescriptive React.js dropdown toolkit", - "main": "components/Dropdown.js", + "main": "lib/components/Dropdown.js", "scripts": { - "test": "zuul -- test/**/*.{js,jsx}", - "test:browser": "zuul --local 55555 -- test/**/*.{js,jsx}" + "prepublish": "npm run build", + "postpublish": "npm run clean", + "test": "npm run build && zuul -- test/**/*.{js,jsx}", + "test:browser": "zuul --local 55555 -- test/**/*.{js,jsx}", + "build": "babel components/**/* --out-dir lib", + "watch": "npm run build -- -w", + "dev": "npm-run-all --parallel watch test:browser", + "clean": "trash lib" }, "repository": { "type": "git", @@ -31,15 +37,31 @@ "classnames": "^2.1.2" }, "devDependencies": { - "browserify": "^10.2.4", + "babel": "^6.3.13", + "babel-cli": "^6.3.15", + "babel-preset-es2015": "^6.3.13", + "babel-preset-react": "^6.3.13", + "babelify": "^7.2.0", "dom-classes": "0.0.1", - "reactify": "^1.1.1", + "mkdirp": "^0.5.1", + "npm-run-all": "^1.4.0", + "react": "0.14.x", + "react-addons-test-utils": "^0.14.3", + "react-dom": "0.14.x", "simple-mock": "^0.3.1", "tape": "^4.0.0", + "trash-cli": "^1.2.0", "watchify": "^3.2.3", "zuul": "^3.1.0" }, "peerDependencies": { - "react": "0.14.x" + "react": "0.14.x", + "react-dom": "0.14.x" + }, + "babel": { + "presets": [ + "es2015", + "react" + ] } } diff --git a/test/components/Dropdown.jsx b/test/components/Dropdown.jsx index 0fc111a..6c79085 100644 --- a/test/components/Dropdown.jsx +++ b/test/components/Dropdown.jsx @@ -1,96 +1,94 @@ -var test = require('tape'); -var React = require('react/addons'); -var TestUtils = React.addons.TestUtils; -var smock = require('simple-mock'); -var domClasses = require('dom-classes'); -var hasClass = domClasses.has; +import test from 'tape'; +import React, { createClass } from 'react'; +import { findDOMNode } from 'react-dom'; +import { findRenderedComponentWithType, findRenderedDOMComponentWithClass, renderIntoDocument, Simulate } from 'react-addons-test-utils'; +import smock from 'simple-mock'; +import domClasses, { has as hasClass } from 'dom-classes'; -var Dropdown = require('../../components/Dropdown.js'); -var DropdownTrigger = Dropdown.DropdownTrigger; -var DropdownContent = Dropdown.DropdownContent; +import Dropdown, { DropdownTrigger, DropdownContent } from '../../lib/components/Dropdown.js'; -var TestApp = React.createClass({ - getInitialState: function(){ - return {}; - }, - render: function(){ - return ( - - - - - ); - } +var TestApp = createClass({ + getInitialState: function(){ + return {}; + }, + render: function(){ + return ( + + + + + ); + } }); -var test_app = TestUtils.renderIntoDocument( ); -var dropdown = TestUtils.findRenderedComponentWithType( test_app, Dropdown ); -var dropdown_element = TestUtils.findRenderedDOMComponentWithClass( dropdown, 'dropdown' ); -var dropdown_element_dom_node = React.findDOMNode( dropdown_element ); -var trigger = TestUtils.findRenderedComponentWithType( test_app, DropdownTrigger ); -var trigger_element = TestUtils.findRenderedDOMComponentWithClass( trigger, 'dropdown__trigger' ); -var content = TestUtils.findRenderedComponentWithType( test_app, DropdownContent ); -var content_element = TestUtils.findRenderedDOMComponentWithClass( content, 'dropdown__content' ); +var test_app = renderIntoDocument( ); +var dropdown = findRenderedComponentWithType( test_app, Dropdown ); +var dropdown_element = findRenderedDOMComponentWithClass( dropdown, 'dropdown' ); +var dropdown_element_dom_node = findDOMNode( dropdown_element ); +var trigger = findRenderedComponentWithType( test_app, DropdownTrigger ); +var trigger_element = findRenderedDOMComponentWithClass( trigger, 'dropdown__trigger' ); +var content = findRenderedComponentWithType( test_app, DropdownContent ); +var content_element = findRenderedDOMComponentWithClass( content, 'dropdown__content' ); test( 'Merges classes from props with default element class', function( t ){ - t.plan( 3 ); - t.equal( domClasses( dropdown_element_dom_node ).length, 1, 'has one class when `className` is empty' ); - test_app.setState({ - className: 'test' - }); - t.ok( hasClass( dropdown_element_dom_node, 'dropdown' ), 'has class `dropdown`' ); - t.ok( hasClass( dropdown_element_dom_node, 'test' ), 'has class `test`' ); - test_app.setState({ - className: null - }); + t.plan( 3 ); + t.equal( domClasses( dropdown_element_dom_node ).length, 1, 'has one class when `className` is empty' ); + test_app.setState({ + className: 'test' + }); + t.ok( hasClass( dropdown_element_dom_node, 'dropdown' ), 'has class `dropdown`' ); + t.ok( hasClass( dropdown_element_dom_node, 'test' ), 'has class `test`' ); + test_app.setState({ + className: null + }); }); test( 'Dropdown is toggled when DropdownTrigger is clicked', function( t ){ - t.plan( 4 ); - var onShowCallback = smock.stub(); - var onHideCallback = smock.stub(); - test_app.setState({ - onShow: onShowCallback, - onHide: onHideCallback - }); - TestUtils.Simulate.click( trigger_element ); - t.ok( hasClass( dropdown_element_dom_node, 'dropdown--active' ), 'has class `dropdown--active` after trigger is clicked' ); - t.equal( onShowCallback.callCount, 1, '`onShow` function was called' ); - TestUtils.Simulate.click( trigger_element ); - t.notOk( hasClass( dropdown_element_dom_node, 'dropdown--active' ), 'does not have class `dropdown--active` after trigger is clicked again' ); - t.equal( onHideCallback.callCount, 1, '`onHide` function was called' ); - test_app.setState({ - onShow: null, - onHide: null - }); + t.plan( 4 ); + var onShowCallback = smock.stub(); + var onHideCallback = smock.stub(); + test_app.setState({ + onShow: onShowCallback, + onHide: onHideCallback + }); + Simulate.click( trigger_element ); + t.ok( hasClass( dropdown_element_dom_node, 'dropdown--active' ), 'has class `dropdown--active` after trigger is clicked' ); + t.equal( onShowCallback.callCount, 1, '`onShow` function was called' ); + Simulate.click( trigger_element ); + t.notOk( hasClass( dropdown_element_dom_node, 'dropdown--active' ), 'does not have class `dropdown--active` after trigger is clicked again' ); + t.equal( onHideCallback.callCount, 1, '`onHide` function was called' ); + test_app.setState({ + onShow: null, + onHide: null + }); }); test( 'Dropdown state can be manually set with props', function( t ){ - t.plan( 2 ); - test_app.setState({ - active: true - }); - t.ok( hasClass( dropdown_element_dom_node, 'dropdown--active' ), 'has class `dropdown--active` when `active` is set to `true`' ); - test_app.setState({ - active: false - }); - t.notOk( hasClass( dropdown_element_dom_node, 'dropdown--active' ), 'does not have class `dropdown--active` when `active` is set to `false`' ); - test_app.setState({ - active: null - }); + t.plan( 2 ); + test_app.setState({ + active: true + }); + t.ok( hasClass( dropdown_element_dom_node, 'dropdown--active' ), 'has class `dropdown--active` when `active` is set to `true`' ); + test_app.setState({ + active: false + }); + t.notOk( hasClass( dropdown_element_dom_node, 'dropdown--active' ), 'does not have class `dropdown--active` when `active` is set to `false`' ); + test_app.setState({ + active: null + }); }); test( 'Dropdown hides itself when area outside dropdown is clicked', function( t ){ - t.plan( 2 ); - dropdown.setState({ - active: true - }); - TestUtils.Simulate.click( content_element ); - t.ok( hasClass( dropdown_element_dom_node, 'dropdown--active' ), 'has class `dropdown--active` after content element is clicked' ); - document.body.click(); - t.notOk( hasClass( dropdown_element_dom_node, 'dropdown--active' ), 'does not have class `dropdown--active` after document body is clicked' ); -}); \ No newline at end of file + t.plan( 2 ); + dropdown.setState({ + active: true + }); + Simulate.click( content_element ); + t.ok( hasClass( dropdown_element_dom_node, 'dropdown--active' ), 'has class `dropdown--active` after content element is clicked' ); + document.body.click(); + t.notOk( hasClass( dropdown_element_dom_node, 'dropdown--active' ), 'does not have class `dropdown--active` after document body is clicked' ); +}); diff --git a/test/components/DropdownContent.jsx b/test/components/DropdownContent.jsx index aab2545..2253e85 100644 --- a/test/components/DropdownContent.jsx +++ b/test/components/DropdownContent.jsx @@ -1,33 +1,33 @@ -var test = require('tape'); -var React = require('react/addons'); -var TestUtils = React.addons.TestUtils; -var domClasses = require('dom-classes'); -var hasClass = domClasses.has; +import test from 'tape'; +import React, { createClass } from 'react'; +import { findDOMNode } from 'react-dom'; +import { findRenderedComponentWithType, findRenderedDOMComponentWithClass, renderIntoDocument } from 'react-addons-test-utils'; +import domClasses, { has as hasClass } from 'dom-classes'; -var DropdownContent = require('../../components/DropdownContent.js'); +import DropdownContent from '../../lib/components/DropdownContent.js'; -var TestApp = React.createClass({ - getInitialState: function(){ - return {}; - }, - render: function(){ - return ; - } +var TestApp = createClass({ + getInitialState: function(){ + return {}; + }, + render: function(){ + return ; + } }); -var test_app = TestUtils.renderIntoDocument( ); -var dropdown_content = TestUtils.findRenderedComponentWithType( test_app, DropdownContent ); -var dropdown_content_element = TestUtils.findRenderedDOMComponentWithClass( dropdown_content, 'dropdown__content' ); -var dropdown_content_dom_node = React.findDOMNode( dropdown_content ); +var test_app = renderIntoDocument( ); +var dropdown_content = findRenderedComponentWithType( test_app, DropdownContent ); +var dropdown_content_element = findRenderedDOMComponentWithClass( dropdown_content, 'dropdown__content' ); +var dropdown_content_dom_node = findDOMNode( dropdown_content ); test( 'Merges classes from props with default element class', function( t ){ - t.plan( 3 ); - t.equal( domClasses( dropdown_content_dom_node ).length, 1, 'has one class when `className` is empty' ); - test_app.setState({ - className: 'test' - }); - t.ok( hasClass( dropdown_content_dom_node, 'dropdown__content' ), 'has class `dropdown__content`' ); - t.ok( hasClass( dropdown_content_dom_node, 'test' ), 'has class `test`' ); - test_app.setState({ - className: null - }); -}); \ No newline at end of file + t.plan( 3 ); + t.equal( domClasses( dropdown_content_dom_node ).length, 1, 'has one class when `className` is empty' ); + test_app.setState({ + className: 'test' + }); + t.ok( hasClass( dropdown_content_dom_node, 'dropdown__content' ), 'has class `dropdown__content`' ); + t.ok( hasClass( dropdown_content_dom_node, 'test' ), 'has class `test`' ); + test_app.setState({ + className: null + }); +}); diff --git a/test/components/DropdownTrigger.jsx b/test/components/DropdownTrigger.jsx index 95acb5e..4e77f59 100644 --- a/test/components/DropdownTrigger.jsx +++ b/test/components/DropdownTrigger.jsx @@ -1,50 +1,38 @@ -var test = require('tape'); -var React = require('react/addons'); -var TestUtils = React.addons.TestUtils; -var domClasses = require('dom-classes'); -var hasClass = domClasses.has; +import test from 'tape'; +import React, { createClass } from 'react'; +import { findDOMNode } from 'react-dom'; +import { findRenderedComponentWithType, findRenderedDOMComponentWithClass, renderIntoDocument } from 'react-addons-test-utils'; +import domClasses, { has as hasClass } from 'dom-classes'; -var DropdownTrigger = require('../../components/DropdownTrigger.js'); +import DropdownTrigger from '../../lib/components/DropdownTrigger.js'; -var TestApp = React.createClass({ - getInitialState: function(){ - return {}; - }, - render: function(){ - return ( - - ); - } +var TestApp = createClass({ + getInitialState: function(){ + return {}; + }, + render: function(){ + return ( + + ); + } }); -var test_app = TestUtils.renderIntoDocument( ); -var dropdown_trigger = TestUtils.findRenderedComponentWithType( test_app, DropdownTrigger ); -var dropdown_trigger_element = TestUtils.findRenderedDOMComponentWithClass( dropdown_trigger, 'dropdown__trigger' ); -var dropdown_trigger_dom_node = React.findDOMNode( dropdown_trigger ); +var test_app = renderIntoDocument( ); +var dropdown_trigger = findRenderedComponentWithType( test_app, DropdownTrigger ); +var dropdown_trigger_element = findRenderedDOMComponentWithClass( dropdown_trigger, 'dropdown__trigger' ); +var dropdown_trigger_dom_node = findDOMNode( dropdown_trigger ); test( 'Merges classes from props with default element class', function( t ){ - t.plan( 3 ); - t.equal( domClasses( dropdown_trigger_dom_node ).length, 1, 'has one class when `className` is empty' ); - test_app.setState({ - className: 'test' - }); - t.ok( hasClass( dropdown_trigger_dom_node, 'dropdown__trigger' ), 'has class `dropdown__trigger`' ); - t.ok( hasClass( dropdown_trigger_dom_node, 'test' ), 'has class `test`' ); - test_app.setState({ - className: null - }); + t.plan( 3 ); + t.equal( domClasses( dropdown_trigger_dom_node ).length, 1, 'has one class when `className` is empty' ); + test_app.setState({ + className: 'test' + }); + t.ok( hasClass( dropdown_trigger_dom_node, 'dropdown__trigger' ), 'has class `dropdown__trigger`' ); + t.ok( hasClass( dropdown_trigger_dom_node, 'test' ), 'has class `test`' ); + test_app.setState({ + className: null + }); }); - -test( 'Attaches onClick handler to element', function( t ){ - t.plan( 1 ); - var clickHandler = function(){}; - test_app.setState({ - onClick: clickHandler - }); - t.equal( dropdown_trigger_element.props.onClick, clickHandler, '`clickHandler` passed to DropdownTrigger and attached to anchor element are equal' ); - test_app.setState({ - onClick: null - }); -}); \ No newline at end of file