diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..031efb5 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,11 @@ +{ + "extends": "airbnb", + "parserOptions": { + "ecmaFeatures": { + "experimentalObjectRestSpread": true + } + }, + "env": { + "mocha": true + } +} diff --git a/.gitignore b/.gitignore index f3e8af3..14b0bf2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ # Thumbnails ._* - + # Files that might appear on external disk .Spotlight-V100 .Trashes @@ -21,4 +21,6 @@ # Node node_modules npm-debug.log -build \ No newline at end of file +build + +coverage/ diff --git a/README.md b/README.md index 8722a6d..4fa1015 100644 --- a/README.md +++ b/README.md @@ -1,182 +1,43 @@ react-skylight ============== -React-SkyLight is a simple react component for modals and dialogs, Powerful, lightweight, and unopinionated in design. +React SkyLight is a simple react component for modals and dialogs. Powerful, lightweight and customizable design. +[React skylight DEMOS and DOCS.](http://marcio.github.io/react-skylight) Installation ------------ ```sh -npm install react-skylight +npm install react-skylight --save ``` Features -------- - Very simple modal/dialog -- Unopinionated in or design, (CSS is not included, only a template is suggested (see more below). - Callback before open - Callback after open - Callback before close - Callback after close +- Callback on overlay click +- All styles can be overridden How to use -------------------- -```js +[React skylight DEMOS and DOCS.](http://marcio.github.io/react-skylight) -//Require react-skylight -var SkyLight = require('react-skylight'); -var App = React.createClass({ - showDialogWithCallBacks: function(){ - this.refs.dialogWithCallBacks.show(); - }, - showSimpleDialog: function(){ - this.refs.simpleDialog.show(); - }, - render:function(){ - return ( -
-

- - -

- I have callbacks! - - Hello, I dont have any callback. - -
- ) - }, - _executeBeforeFirstModalOpen: function(){ - alert('Executed before open'); - }, - _executeAfterFirstModalOpen: function(){ - alert('Executed after open'); - }, - _executeBeforeFirstModalClose: function(){ - alert('Executed before close'); - }, - _executeAfterFirstModalClose: function(){ - alert('Executed after close'); - } -}); - -React.render(, document.getElementById("content")); - -``` - - - -Options -------------------- - -####title: (String) -A title for your modal. -``` html -Modal Content -``` -####showOverlay: (Boolean) -Show modal with an overlay (true) or without an overlay (false). - -``` html -Modal With Overlay - -Modal Without Overlay -``` - -####beforeOpen, afterOpen, beforeClose and afterClose: (Function) -A callback functions to execute before and after open and before and after close a modal. You can use just the one you want. -``` html -Modal Content -``` - -##New in 0.2.0 version - -Overlay, dialog and closeButton styles now accept an object that represent your styles. - -If you not declare any style, skyLight will apply the default styles, but if you send an object with one or more properties, your object will override the default property. - -####overlayStyles: (Object) -An object that represent the styles of overlay: -```js -//Default overlay SkyLight styles: -overlayStyles: { - position: 'fixed', - top: 0, - left: 0, - width: '100%', - height: '100%', - zIndex: 99, - backgroundColor: 'rgba(0,0,0,0.3)' -} -``` - -####dialogStyles: (Object) -An object that represent the styles of dialog. -```js -//Default dialog SkyLight styles: -dialogStyles: { - width: '50%', - height: '400px', - position: 'fixed', - top: '50%', - left: '50%', - marginTop: '-200px', - marginLeft: '-25%', - backgroundColor: '#fff', - borderRadius: '2px', - zIndex: 100, - padding: '10px', - boxShadow: '0 0 4px rgba(0,0,0,.14),0 4px 8px rgba(0,0,0,.28)' -} -``` - -####closeButtonStyle: (Object) -An object that represent the styles of close button -```js -//Default close button SkyLight styles: -closeButtonStyle: { - cursor: 'pointer', - float: 'right', - fontSize: '1.6em', - margin: '-15px 0' -} -``` - -### An Example with new styles, overriding dialog background color to red - - -```js - -var dialogStyles = { - backgroundColor: '#f03' -}; - -Modal Content -``` - - -CSS --------------------- - -External css is no more needed! ##Enjoy! ## Release History - + * 2016-08-31   v0.4.1   Polyfill Object.assign() to works in IE + * 2016-04-27 v0.4.0 Fix issue #35 (numeric string value for CSS property), up to react 15.0.1 and merged pull request to support Stateless (thanks @darthtrevino) + * 2016-01-09   v0.3.0   Rewrite to ES2015, overlay callback and new site. + * 2015-04-08   v0.2.0   Improvements * 2015-02-03   v0.1.4   Changed skylight.js to skylight.jsx and adjust of namespace diff --git a/index.js b/index.js deleted file mode 100644 index 658ca3a..0000000 --- a/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./src/skylight.jsx'); diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..0b711a7 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,52 @@ +const path = require('path'); +const isTddMode = process.argv.indexOf('--tdd') > -1; + +module.exports = config => { + config.set({ + basePath: '', + frameworks: ['mocha', 'chai'], + files: [ + './node_modules/phantomjs-polyfill-object-assign/object-assign-polyfill.js', + 'test/**/*.spec.jsx', + 'test/assign.spec.js', + ], + exclude: [], + preprocessors: { + 'test/**/*.spec.{js,jsx}': ['webpack', 'sourcemap'], + }, + webpack: { + resolve: { + extensions: ['', '.js', '.jsx'], + }, + module: { + preLoaders: [ + { + test: /\.(js|jsx)$/, + include: path.resolve('src/'), + loader: 'isparta', + }, + { + test: /\.(js|jsx)?$/, + loader: 'eslint', + exclude: /node_modules/, + }, + ], + loaders: [ + { + test: /\.(js|jsx)?$/, + loader: 'babel', + exclude: /node_modules/, + }, + ], + }, + }, + reporters: ['progress', 'coverage'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: isTddMode, + browsers: isTddMode ? ['Chrome'] : [ 'PhantomJS'], + singleRun: !isTddMode, + concurrency: Infinity, + }); +}; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..fb9b478 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,25 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _skylight = require('./skylight'); + +Object.defineProperty(exports, 'default', { + enumerable: true, + get: function get() { + return _interopRequireDefault(_skylight).default; + } +}); + +var _skylightstateless = require('./skylightstateless'); + +Object.defineProperty(exports, 'SkyLightStateless', { + enumerable: true, + get: function get() { + return _interopRequireDefault(_skylightstateless).default; + } +}); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } \ No newline at end of file diff --git a/lib/skylight.js b/lib/skylight.js new file mode 100644 index 0000000..81c2878 --- /dev/null +++ b/lib/skylight.js @@ -0,0 +1,123 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _react = require('react'); + +var _react2 = _interopRequireDefault(_react); + +var _skylightstateless = require('./skylightstateless'); + +var _skylightstateless2 = _interopRequireDefault(_skylightstateless); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var isOpening = function isOpening(s1, s2) { + return !s1.isVisible && s2.isVisible; +}; +var isClosing = function isClosing(s1, s2) { + return s1.isVisible && !s2.isVisible; +}; + +var SkyLight = (function (_React$Component) { + _inherits(SkyLight, _React$Component); + + function SkyLight(props) { + _classCallCheck(this, SkyLight); + + var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(SkyLight).call(this, props)); + + _this.state = { isVisible: false }; + return _this; + } + + _createClass(SkyLight, [{ + key: 'componentWillUpdate', + value: function componentWillUpdate(nextProps, nextState) { + if (isOpening(this.state, nextState) && this.props.beforeOpen) { + this.props.beforeOpen(); + } + + if (isClosing(this.state, nextState) && this.props.beforeClose) { + this.props.beforeClose(); + } + } + }, { + key: 'componentDidUpdate', + value: function componentDidUpdate(prevProps, prevState) { + if (isOpening(prevState, this.state) && this.props.afterOpen) { + this.props.afterOpen(); + } + + if (isClosing(prevState, this.state) && this.props.afterClose) { + this.props.afterClose(); + } + } + }, { + key: 'show', + value: function show() { + this.setState({ isVisible: true }); + } + }, { + key: 'hide', + value: function hide() { + this.setState({ isVisible: false }); + } + }, { + key: '_onOverlayClicked', + value: function _onOverlayClicked() { + if (this.props.hideOnOverlayClicked) { + this.hide(); + } + + if (this.props.onOverlayClicked) { + this.props.onOverlayClicked(); + } + } + }, { + key: 'render', + value: function render() { + var _this2 = this; + + return _react2.default.createElement(_skylightstateless2.default, _extends({}, this.props, { + isVisible: this.state.isVisible, + onOverlayClicked: function onOverlayClicked() { + return _this2._onOverlayClicked(); + }, + onCloseClicked: function onCloseClicked() { + return _this2.hide(); + } + })); + } + }]); + + return SkyLight; +})(_react2.default.Component); + +exports.default = SkyLight; + +SkyLight.displayName = 'SkyLight'; + +SkyLight.propTypes = _extends({}, _skylightstateless2.default.sharedPropTypes, { + afterClose: _react2.default.PropTypes.func, + afterOpen: _react2.default.PropTypes.func, + beforeClose: _react2.default.PropTypes.func, + beforeOpen: _react2.default.PropTypes.func, + hideOnOverlayClicked: _react2.default.PropTypes.bool +}); + +SkyLight.defaultProps = _extends({}, _skylightstateless2.default.defaultProps, { + hideOnOverlayClicked: false +}); \ No newline at end of file diff --git a/lib/skylightstateless.js b/lib/skylightstateless.js new file mode 100644 index 0000000..7bfc4e8 --- /dev/null +++ b/lib/skylightstateless.js @@ -0,0 +1,132 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _react = require('react'); + +var _react2 = _interopRequireDefault(_react); + +var _styles = require('./styles'); + +var _styles2 = _interopRequireDefault(_styles); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var SkyLightStateless = (function (_React$Component) { + _inherits(SkyLightStateless, _React$Component); + + function SkyLightStateless() { + _classCallCheck(this, SkyLightStateless); + + return _possibleConstructorReturn(this, Object.getPrototypeOf(SkyLightStateless).apply(this, arguments)); + } + + _createClass(SkyLightStateless, [{ + key: 'onOverlayClicked', + value: function onOverlayClicked() { + if (this.props.onOverlayClicked) { + this.props.onOverlayClicked(); + } + } + }, { + key: 'onCloseClicked', + value: function onCloseClicked() { + if (this.props.onCloseClicked) { + this.props.onCloseClicked(); + } + } + }, { + key: 'render', + value: function render() { + var _this2 = this; + + var mergeStyles = function mergeStyles(key) { + return Object.assign({}, _styles2.default[key], _this2.props[key]); + }; + var isVisible = this.props.isVisible; + + var dialogStyles = mergeStyles('dialogStyles'); + var overlayStyles = mergeStyles('overlayStyles'); + var closeButtonStyle = mergeStyles('closeButtonStyle'); + var titleStyle = mergeStyles('titleStyle'); + overlayStyles.display = dialogStyles.display = 'block'; + + var overlay = undefined; + if (this.props.showOverlay) { + overlay = _react2.default.createElement('div', { className: 'skylight-overlay', + onClick: function onClick() { + return _this2.onOverlayClicked(); + }, + style: overlayStyles + }); + } + + return isVisible ? _react2.default.createElement( + 'section', + { className: 'skylight-wrapper' }, + overlay, + _react2.default.createElement( + 'div', + { className: 'skylight-dialog', style: dialogStyles }, + _react2.default.createElement( + 'a', + { role: 'button', className: 'skylight-close-button', + onClick: function onClick() { + return _this2.onCloseClicked(); + }, + style: closeButtonStyle + }, + '×' + ), + _react2.default.createElement( + 'h2', + { style: titleStyle }, + this.props.title + ), + this.props.children + ) + ) : _react2.default.createElement('div', null); + } + }]); + + return SkyLightStateless; +})(_react2.default.Component); + +exports.default = SkyLightStateless; + +SkyLightStateless.displayName = 'SkyLightStateless'; + +SkyLightStateless.sharedPropTypes = { + closeButtonStyle: _react2.default.PropTypes.object, + dialogStyles: _react2.default.PropTypes.object, + onCloseClicked: _react2.default.PropTypes.func, + onOverlayClicked: _react2.default.PropTypes.func, + overlayStyles: _react2.default.PropTypes.object, + showOverlay: _react2.default.PropTypes.bool, + title: _react2.default.PropTypes.string, + titleStyle: _react2.default.PropTypes.object +}; + +SkyLightStateless.propTypes = _extends({}, SkyLightStateless.sharedPropTypes, { + isVisible: _react2.default.PropTypes.bool +}); + +SkyLightStateless.defaultProps = { + title: '', + showOverlay: true, + overlayStyles: _styles2.default.overlayStyles, + dialogStyles: _styles2.default.dialogStyles, + closeButtonStyle: _styles2.default.closeButtonStyle +}; \ No newline at end of file diff --git a/lib/styles.js b/lib/styles.js new file mode 100644 index 0000000..54d4f4e --- /dev/null +++ b/lib/styles.js @@ -0,0 +1,43 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var styles = { + overlayStyles: { + position: 'fixed', + top: '0px', + left: '0px', + width: '100%', + height: '100%', + zIndex: '99', + backgroundColor: 'rgba(0,0,0,0.3)' + }, + dialogStyles: { + width: '50%', + height: '400px', + position: 'fixed', + top: '50%', + left: '50%', + marginTop: '-200px', + marginLeft: '-25%', + backgroundColor: '#fff', + borderRadius: '2px', + zIndex: '100', + padding: '15px', + boxShadow: '0px 0px 4px rgba(0,0,0,.14),0px 4px 8px rgba(0,0,0,.28)' + }, + title: { + marginTop: '0px' + }, + closeButtonStyle: { + cursor: 'pointer', + position: 'absolute', + fontSize: '1.8em', + right: '10px', + top: '0px' + } +}; + +exports.default = styles; \ No newline at end of file diff --git a/package.json b/package.json index ac20aea..5ebdbf3 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,14 @@ { "name": "react-skylight", - "version": "0.2.0", + "version": "0.4.1", "description": "A react component for modals and dialogs.", - "main": "index.js", + "main": "lib/index.js", "scripts": { - "test": "jest" + "build": "babel src -d lib", + "watch": "babel src --watch -d lib", + "develop": "karma start --tdd", + "karma": "karma start", + "test": "npm-run-all karma build" }, "repository": { "type": "git", @@ -23,30 +27,52 @@ "dialog" ], "author": "Marcio Gasparotto", + "contributors": [ + { + "name": "Chris Trevino", + "email": "darthtrevino@gmail.com" + } + ], "license": "MIT", - "browserify": { - "transform": [ - [ - "reactify" - ] - ] - }, "peerDependencies": { - "react": ">=0.12.0 <1.0.0" - }, - "dependencies": { - "react": "*", - "react-tools": "*", - "browserify": "^8.0.3", - "reactify": "^0.17.1" - }, - "jest": { - "scriptPreprocessor": "preprocessor.js", - "unmockedModulePathPatterns": [ - "node_modules/react" - ] + "react": "^0.14.0 || ^15.0.1" }, "devDependencies": { - "jest-cli": "^0.2.1" + "babel-core": "^6.3.26", + "babel-eslint": "^6.0.2", + "babel-loader": "^6.2.1", + "babel-preset-es2015": "^6.3.13", + "babel-preset-react": "^6.5.0", + "babel-preset-stage-0": "^6.3.13", + "chai": "^3.5.0", + "eslint": "^2.7.0", + "eslint-config-airbnb": "^6.2.0", + "eslint-loader": "^1.3.0", + "eslint-plugin-react": "^4.3.0", + "isparta-loader": "^2.0.0", + "jest-cli": "^0.10.0", + "karma": "^0.13.22", + "karma-chai": "^0.1.0", + "karma-chrome-launcher": "^0.2.3", + "karma-coverage": "^0.5.5", + "karma-mocha": "^0.2.2", + "karma-phantomjs-launcher": "^1.0.0", + "karma-sourcemap-loader": "^0.3.7", + "karma-webpack": "^1.7.0", + "mocha": "^2.4.5", + "npm-run-all": "^1.7.0", + "phantomjs-polyfill-object-assign": "0.0.2", + "phantomjs-prebuilt": "^2.1.7", + "react": "^15.0.1", + "react-addons-test-utils": "^15.0.1 || ^0.14.0", + "webpack": "^1.13.0" + }, + "babel": { + "presets": [ + "es2015", + "react", + "stage-0" + ], + "plugins": [] } } diff --git a/preprocessor.js b/preprocessor.js deleted file mode 100644 index 300111c..0000000 --- a/preprocessor.js +++ /dev/null @@ -1,7 +0,0 @@ - -var ReactTools = require('react-tools'); -module.exports = { - process: function(src) { - return ReactTools.transform(src); - } -}; \ No newline at end of file diff --git a/src/__tests__/skylight-test.js b/src/__tests__/skylight-test.js deleted file mode 100644 index fbc38a8..0000000 --- a/src/__tests__/skylight-test.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Created by Gasparotto on 06/01/15. - */ -jest.dontMock('../skylight.jsx'); - -describe('SkyLight', function() { - - var React; - var SkyLight; - var TestUtils; - - beforeEach(function(){ - React = require('react/addons'); - SkyLight = require('../skylight.jsx'); - TestUtils = React.addons.TestUtils; - }); - - it('Show a title in h2 tag', function() { - - var titleTest = "My title test"; - var skylight = TestUtils.renderIntoDocument( - - ); - - var h2Title = TestUtils.findRenderedDOMComponentWithTag(skylight, 'h2'); - expect(h2Title.getDOMNode().textContent).toEqual(titleTest); - }); - - it('Show a content', function() { - - var text = 'Hi Modal :D'; - var className = 'test'; - var root = React.createElement('div', { className: className }, text); - - var skylight = TestUtils.renderIntoDocument( - {root} - ); - - var content = TestUtils.findRenderedDOMComponentWithClass(skylight, className); - expect(content.getDOMNode().textContent).toEqual(text); - - }); - - - it('Overlay cant be rendered if showOverlay is false', function() { - var skylight = TestUtils.renderIntoDocument( - - ); - var className = 'skylight-dialog__overlay'; - var overlay = TestUtils.scryRenderedDOMComponentsWithClass(skylight, className); - expect(overlay.length).toEqual(0); - }); - - - it('Should be set display:block when isVisible it was changed to true', function() { - - var skylight = TestUtils.renderIntoDocument( - - ); - - var instance = TestUtils.findRenderedComponentWithType(skylight, ); - var modalStyle = TestUtils.findRenderedDOMComponentWithClass(skylight, 'skylight-dialog'); - expect(modalStyle.getDOMNode().style.display).toEqual('none'); - instance.setState({isVisible: true}); - expect(modalStyle.getDOMNode().style.display).toEqual('block'); - instance.setState({isVisible: false}); - expect(modalStyle.getDOMNode().style.display).toEqual('none'); - }); - - -}); \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..aae586f --- /dev/null +++ b/src/index.js @@ -0,0 +1,2 @@ +export { default as default } from './skylight'; +export { default as SkyLightStateless } from './skylightstateless'; diff --git a/src/skylight.jsx b/src/skylight.jsx index b9fd888..7ee800a 100644 --- a/src/skylight.jsx +++ b/src/skylight.jsx @@ -1,89 +1,76 @@ -var styles = require('./styles'); -var extend = require('util')._extend; +import React from 'react'; +import SkylightStateless from './skylightstateless'; -var SkyLight = React.createClass({ - propTypes: { - title: React.PropTypes.string, - showOverlay: React.PropTypes.bool, - beforeOpen: React.PropTypes.func, - afterOpen: React.PropTypes.func, - beforeClose: React.PropTypes.func, - afterClose: React.PropTypes.func, - overlayStyles: React.PropTypes.object, - dialogStyles: React.PropTypes.object, - closeButtonStyle: React.PropTypes.object - }, - getDefaultProps: function () { - return { - title: '', - showOverlay: true, - overlayStyles: styles.overlayStyles, - dialogStyles: styles.dialogStyles, - closeButtonStyle: styles.closeButtonStyle - }; - }, - getInitialState: function () { - return { - isVisible: false - }; - }, - show: function () { - this.setState({isVisible: true}); - }, - hide: function () { - this.setState({isVisible: false}); - }, - componentWillUpdate: function (nextProps, nextState) { - if (nextState.isVisible && this.props.beforeOpen) { - this.props.beforeOpen(); - } +const isOpening = (s1, s2) => !s1.isVisible && s2.isVisible; +const isClosing = (s1, s2) => s1.isVisible && !s2.isVisible; - if (!nextState.isVisible && this.props.beforeClose) { - this.props.beforeClose(); - } - }, - componentDidUpdate: function (prevProps, prevState) { - if (!prevState.isVisible && this.props.afterOpen) { - this.props.afterOpen(); - } +export default class SkyLight extends React.Component { - if (prevState.isVisible && this.props.afterClose) { - this.props.afterClose(); - } - }, - render: function () { + constructor(props) { + super(props); + this.state = { isVisible: false }; + } - var overlay; - var contentClassName = 'skylight-content'; + componentWillUpdate(nextProps, nextState) { + if (isOpening(this.state, nextState) && this.props.beforeOpen) { + this.props.beforeOpen(); + } + + if (isClosing(this.state, nextState) && this.props.beforeClose) { + this.props.beforeClose(); + } + } + + componentDidUpdate(prevProps, prevState) { + if (isOpening(prevState, this.state) && this.props.afterOpen) { + this.props.afterOpen(); + } - var overlayStyles = extend(styles.overlayStyles, this.props.overlayStyles); - var closeButtonStyle = extend(styles.closeButtonStyle = this.props.closeButtonStyle); + if (isClosing(prevState, this.state) && this.props.afterClose) { + this.props.afterClose(); + } + } - if (this.state.isVisible) { - overlayStyles.display = 'block'; - } else { - overlayStyles.display = 'none'; - } + show() { + this.setState({ isVisible: true }); + } - if (this.props.showOverlay) { - overlay = (
); - } + hide() { + this.setState({ isVisible: false }); + } - if (this.props.dialogClass !== null) { - contentClassName += ' ' + this.props.dialogClass; - } + _onOverlayClicked() { + if (this.props.hideOnOverlayClicked) { + this.hide(); + } - return ( -
- {overlay} -
- × -

{this.props.title}

- {this.props.children} -
-
- ); + if (this.props.onOverlayClicked) { + this.props.onOverlayClicked(); } -}); + } + + render() { + return ( this._onOverlayClicked()} + onCloseClicked={() => this.hide()} + />); + } +} + +SkyLight.displayName = 'SkyLight'; + +SkyLight.propTypes = { + ...SkylightStateless.sharedPropTypes, + afterClose: React.PropTypes.func, + afterOpen: React.PropTypes.func, + beforeClose: React.PropTypes.func, + beforeOpen: React.PropTypes.func, + hideOnOverlayClicked: React.PropTypes.bool, +}; -module.exports = SkyLight; +SkyLight.defaultProps = { + ...SkylightStateless.defaultProps, + hideOnOverlayClicked: false, +}; diff --git a/src/skylightstateless.jsx b/src/skylightstateless.jsx new file mode 100644 index 0000000..dc4d87f --- /dev/null +++ b/src/skylightstateless.jsx @@ -0,0 +1,80 @@ +import React from 'react'; +import styles from './styles'; +import assign from './utils/assign'; + +export default class SkyLightStateless extends React.Component { + + onOverlayClicked() { + if (this.props.onOverlayClicked) { + this.props.onOverlayClicked(); + } + } + + onCloseClicked() { + if (this.props.onCloseClicked) { + this.props.onCloseClicked(); + } + } + + render() { + const mergeStyles = key => assign({}, styles[key], this.props[key]); + const { isVisible } = this.props; + const dialogStyles = mergeStyles('dialogStyles'); + const overlayStyles = mergeStyles('overlayStyles'); + const closeButtonStyle = mergeStyles('closeButtonStyle'); + const titleStyle = mergeStyles('titleStyle'); + overlayStyles.display = dialogStyles.display = 'block'; + + let overlay; + if (this.props.showOverlay) { + overlay = ( +
this.onOverlayClicked()} + style={overlayStyles} + /> + ); + } + + return isVisible ? ( +
+ {overlay} +
+ this.onCloseClicked()} + style={closeButtonStyle} + > + × + +

{this.props.title}

+ {this.props.children} +
+
+ ) :
; + } +} + +SkyLightStateless.displayName = 'SkyLightStateless'; + +SkyLightStateless.sharedPropTypes = { + closeButtonStyle: React.PropTypes.object, + dialogStyles: React.PropTypes.object, + onCloseClicked: React.PropTypes.func, + onOverlayClicked: React.PropTypes.func, + overlayStyles: React.PropTypes.object, + showOverlay: React.PropTypes.bool, + title: React.PropTypes.string, + titleStyle: React.PropTypes.object, +}; + +SkyLightStateless.propTypes = { + ...SkyLightStateless.sharedPropTypes, + isVisible: React.PropTypes.bool, +}; + +SkyLightStateless.defaultProps = { + title: '', + showOverlay: true, + overlayStyles: styles.overlayStyles, + dialogStyles: styles.dialogStyles, + closeButtonStyle: styles.closeButtonStyle, +}; diff --git a/src/styles.js b/src/styles.js index 2bcffba..ed140bf 100644 --- a/src/styles.js +++ b/src/styles.js @@ -1,6 +1,10 @@ +const styles = { + overlayStyles: {}, + dialogStyles: {}, + title: { + marginTop: '0px', + }, + closeButtonStyle: {}, +}; -module.exports = { - overlayStyles: {}, - dialogStyles: {}, - closeButtonStyle: {} -}; \ No newline at end of file +export default styles; diff --git a/src/utils/assign.js b/src/utils/assign.js new file mode 100644 index 0000000..3d9fbc6 --- /dev/null +++ b/src/utils/assign.js @@ -0,0 +1,18 @@ +export default function (target, ...args) { + if (target === null) { + throw new TypeError('Cannot convert undefined or null to object'); + } + + const newTarget = target; + for (let index = 0; index < args.length; index++) { + const source = args[index]; + if (source !== null) { + for (const key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + newTarget[key] = source[key]; + } + } + } + } + return newTarget; +} diff --git a/styles-examples/default.css b/styles-examples/default.css deleted file mode 100644 index d3ad37c..0000000 --- a/styles-examples/default.css +++ /dev/null @@ -1,31 +0,0 @@ -.skylight-dialog { - width: 50%; - height: 400px; - position: fixed; - top: 50%; - left: 50%; - margin-top: -200px; - margin-left: -25%; - background-color: #fff; - border-radius: 2px; - z-index: 100; - padding: 10px; - box-shadow: 0 0 4px rgba(0,0,0,.14),0 4px 8px rgba(0,0,0,.28); - overflow: auto; -} - -.skylight-dialog--close { - cursor: pointer; - float: right; - font-size: 1.6em; -} - -.skylight-dialog__overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 99; - background-color: rgba(0,0,0,0.3); -} diff --git a/test/assign.spec.js b/test/assign.spec.js new file mode 100644 index 0000000..98be966 --- /dev/null +++ b/test/assign.spec.js @@ -0,0 +1,29 @@ +import assign from '../src/utils/assign'; +import { expect } from 'chai'; + +describe('Assign function', () => { + it('should throw error when target is null', () => { + expect(assign.bind(null, null, {})).to.throw(TypeError); + }); + + it('should accept null source', () => { + expect(assign({}, null)).to.deep.equal({}); + }); + + it('should merge ', () => { + const first = { a: 1 }; + const second = { + b: { + c: 2, + }, + }; + const expected = { + a: 1, + b: { + c: 2, + }, + }; + + expect(assign({}, first, second)).to.deep.equal(expected); + }); +}); diff --git a/test/skylight.spec.jsx b/test/skylight.spec.jsx new file mode 100644 index 0000000..c858d34 --- /dev/null +++ b/test/skylight.spec.jsx @@ -0,0 +1,90 @@ +/* eslint-disable no-unused-expressions */ +/* eslint-disable no-return-assign */ +import React from 'react'; +import { expect } from 'chai'; +import Skylight from '../src/skylight'; +import SkylightInteractor from './SkylightInteractor'; + +describe('The Skylight component', () => { + it('will not render initially', () => { + const rendered = new SkylightInteractor(); + expect(rendered.isOpen()).to.be.false; + }); + + it('will render on show()', () => { + const rendered = new SkylightInteractor(); + rendered.show(); + expect(rendered.isOpen()).to.be.true; + }); + + it('will hide on hide()', () => { + const rendered = new SkylightInteractor(); + rendered.show(); + rendered.hide(); + expect(rendered.isOpen()).to.be.false; + }); + + it('will emit beforeOpen and afterOpen events when opening', () => { + let beforeTriggered = false; + let afterTriggered = false; + const onBefore = () => beforeTriggered = true; + const onAfter = () => { + expect(beforeTriggered).to.be.true; + afterTriggered = true; + }; + const rendered = new SkylightInteractor( + + ); + expect(beforeTriggered).to.be.false; + expect(afterTriggered).to.be.false; + rendered.show(); + expect(beforeTriggered).to.be.true; + expect(afterTriggered).to.be.true; + }); + + it('will emit beforeClose and afterClose events when closing', () => { + let beforeTriggered = false; + let afterTriggered = false; + const onBefore = () => beforeTriggered = true; + const onAfter = () => { + expect(beforeTriggered).to.be.true; + afterTriggered = true; + }; + const rendered = new SkylightInteractor( + + ); + rendered.show(); + expect(beforeTriggered).to.be.false; + expect(afterTriggered).to.be.false; + rendered.hide(); + expect(beforeTriggered).to.be.true; + expect(afterTriggered).to.be.true; + }); + + it('will emit an onOverlayClicked event', () => { + let clicked = false; + const rendered = new SkylightInteractor( + clicked = true} /> + ); + rendered.show(); + rendered.clickOnOverlay(); + expect(clicked).to.be.true; + expect(rendered.isOpen()).to.be.true; + }); + + it('will close when the overlay is clicked when hideOnOverlayClicked prop is true', () => { + const rendered = new SkylightInteractor( + + ); + rendered.show(); + rendered.clickOnOverlay(); + expect(rendered.isOpen()).to.be.false; + }); + + it('will hide when the close button is clicked', () => { + const rendered = new SkylightInteractor(); + rendered.show(); + rendered.clickOnClose(); + expect(rendered.isOpen()).to.be.false; + }); +}); diff --git a/test/skylightinteractor.js b/test/skylightinteractor.js new file mode 100644 index 0000000..93dc102 --- /dev/null +++ b/test/skylightinteractor.js @@ -0,0 +1,45 @@ +/* eslint-disable no-unused-expressions */ +/* eslint-disable no-return-assign */ +import { + renderIntoDocument, + scryRenderedDOMComponentsWithClass, + findRenderedDOMComponentWithClass, + Simulate, +} from 'react-addons-test-utils'; + +/** + * A test wrapper for skylight components that performs DOM interaction. + */ +export default class SkylightInteractor { + constructor(jsx) { + this._component = renderIntoDocument(jsx); + } + + show() { + this._component.show(); + } + + hide() { + this._component.hide(); + } + + isOverlayVisible() { + const found = scryRenderedDOMComponentsWithClass(this._component, 'skylight-overlay'); + return found.length === 1; + } + + clickOnOverlay() { + const overlay = findRenderedDOMComponentWithClass(this._component, 'skylight-overlay'); + Simulate.click(overlay); + } + + clickOnClose() { + const closeButton = findRenderedDOMComponentWithClass(this._component, 'skylight-close-button'); + Simulate.click(closeButton); + } + + isOpen() { + const found = scryRenderedDOMComponentsWithClass(this._component, 'skylight-overlay'); + return found.length === 1; + } +} diff --git a/test/skylightstateless.spec.jsx b/test/skylightstateless.spec.jsx new file mode 100644 index 0000000..f526ace --- /dev/null +++ b/test/skylightstateless.spec.jsx @@ -0,0 +1,55 @@ +/* eslint-disable no-unused-expressions */ +/* eslint-disable no-return-assign */ +import React from 'react'; +import { expect } from 'chai'; +import SkylightStateless from '../src/skylightstateless'; +import SkylightInteractor from './SkylightInteractor'; + +describe('The SkylightStateless component', () => { + it('will not render when it is not visible', () => { + const rendered = new SkylightInteractor(); + expect(rendered.isOpen()).to.be.false; + }); + + it('will render when it is visible', () => { + const rendered = new SkylightInteractor(); + expect(rendered.isOpen()).to.be.true; + }); + + it('will not render the overlay when the showOverlay prop is false', () => { + const rendered = new SkylightInteractor( + + ); + expect(rendered.isOverlayVisible()).to.be.false; + }); + + it('will emit an event when the overlay is clicked', () => { + let clicked = false; + const rendered = new SkylightInteractor( + clicked = true} /> + ); + rendered.clickOnOverlay(); + expect(clicked).to.be.true; + }); + + it('will emit an event when the close button is clicked', () => { + let clicked = false; + const rendered = new SkylightInteractor( + clicked = true} /> + ); + rendered.clickOnClose(); + expect(clicked).to.be.true; + }); + + it('will not blow up when no onCloseClicked prop is set', () => { + const rendered = new SkylightInteractor(); + rendered.clickOnClose(); + // no error thrown + }); + + it('will not blow up when no onOverlayClicked prop is set', () => { + const rendered = new SkylightInteractor(); + rendered.clickOnOverlay(); + // no error thrown + }); +});