├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── devServer.js ├── example ├── bower_components │ └── primer-css │ │ ├── .bower.json │ │ ├── .bowerrc │ │ ├── .editorconfig │ │ ├── CONTRIBUTING.md │ │ ├── LICENSE.md │ │ ├── MAINTAINING.md │ │ ├── README.md │ │ ├── bower.json │ │ ├── css │ │ ├── .primer-stats.md │ │ └── primer.css │ │ ├── package.json │ │ └── scss │ │ ├── _alerts.scss │ │ ├── _avatars.scss │ │ ├── _base.scss │ │ ├── _blankslate.scss │ │ ├── _buttons.scss │ │ ├── _counter.scss │ │ ├── _filter-list.scss │ │ ├── _flex-table.scss │ │ ├── _forms.scss │ │ ├── _layout.scss │ │ ├── _menu.scss │ │ ├── _mixins.scss │ │ ├── _normalize.scss │ │ ├── _states.scss │ │ ├── _tabnav.scss │ │ ├── _tooltips.scss │ │ ├── _truncate.scss │ │ ├── _type.scss │ │ ├── _utility.scss │ │ ├── _variables.scss │ │ └── primer.scss ├── build │ ├── 43a3909d45ff1632beed7e4fff7e04d5.png │ ├── app.css │ └── app.js ├── example.gif ├── index.html └── src │ ├── images │ ├── congruent_pentagon.png │ └── screenshot.jpg │ ├── scripts │ ├── App.jsx │ ├── CustomElement.jsx │ ├── NotificationGenerator.jsx │ └── showcase.js │ └── styles │ ├── base.sass │ ├── generator.sass │ └── variables.sass ├── karma.conf.js ├── package.json ├── src ├── NotificationContainer.jsx ├── NotificationItem.jsx ├── NotificationSystem.jsx ├── constants.js ├── helpers.js └── styles.js ├── test └── notification-system.test.js ├── tests.webpack.js ├── webpack.config.dev.js ├── webpack.config.prod.js ├── webpack.config.umd.dev.js ├── webpack.config.umd.prod.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb/legacy", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "es6": true, 7 | "mocha": true 8 | }, 9 | "plugins": [ 10 | "react", 11 | "import" 12 | ], 13 | "parserOptions": { 14 | "ecmaFeatures": { 15 | "modules": true, 16 | "jsx": true 17 | } 18 | }, 19 | "rules": { 20 | "react/display-name": 0, 21 | "react/jsx-curly-spacing": [2, "always"], 22 | "react/jsx-no-duplicate-props": 2, 23 | "react/jsx-no-undef": 2, 24 | "react/jsx-uses-react": 2, 25 | "react/jsx-uses-vars": 2, 26 | "react/no-did-update-set-state": 2, 27 | "react/no-multi-comp": 2, 28 | "react/no-unknown-property": 2, 29 | "react/prop-types": 2, 30 | "react/react-in-jsx-scope": 2, 31 | "react/self-closing-comp": 2, 32 | "react/jsx-wrap-multilines": 2, 33 | "react/sort-comp": 0, 34 | 35 | "import/extensions": 2, 36 | 37 | "space-before-function-paren": 0, 38 | "quotes": [2, "single", "avoid-escape"], 39 | "jsx-quotes": [2, "prefer-double"], 40 | "comma-dangle": [2, "never"], 41 | "indent": [2, 2], 42 | "object-curly-spacing": [2, "always"], 43 | "no-undef": 2, 44 | "no-underscore-dangle": 0, 45 | "func-names": 0, 46 | "no-else-return": 0, 47 | "no-console": 0, 48 | "no-throw-literal": 0, 49 | "id-length": 0, 50 | "max-len": 0 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | coverage/ 3 | node_modules/ 4 | dist/ 5 | example/node_modules/ 6 | example/build/.module-cache/ 7 | *.log* 8 | .idea 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | example/ 3 | coverage/ 4 | test/ 5 | dist/.module-cache/ 6 | .gitignore 7 | .git/ 8 | webpack.* 9 | karma.config.js 10 | devServer.js 11 | tests.webpack.js 12 | .travis.yml 13 | .editorconfig 14 | *.log 15 | .idea/ 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | services: 5 | - xvfb 6 | before_script: 7 | - export DISPLAY=:99.0 8 | addons: 9 | chrome: "stable" 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 0.3.0 - Nov 14, 2019 4 | 5 | * Updated to class components and removed future deprecated lifecycle methods (thanks to @oskarer) 6 | 7 | ## 0.2.17 - Feb 21, 2018 8 | 9 | * Dismissible enhancements (thanks to @thepeted) 10 | 11 | ## 0.2.16 - Oct 19, 2017 12 | 13 | * Support for React 16 (thanks to @marudor) 14 | 15 | ## 0.2.15 - Aug 1, 2017 16 | 17 | * UMD build now specifies the library name (thanks to @franckamayou) 18 | 19 | ## 0.2.14 - May 3, 2017 20 | 21 | * Ability to [edit notifications](https://github.com/igorprado/react-notification-system#removenotificationnotification). (thanks to @syndbg) 22 | * Removed deprecation warning. Now using `prop-types` and `create-react-class`packages. (thanks to @andrewBalekha) 23 | * Fix calling `onRemove` before updating the notifications state. (thanks to @szdc) 24 | 25 | ## 0.2.13 - Mar 14, 2017 26 | 27 | * UMD support. (thanks to @jochenberger) 28 | 29 | ## 0.2.12 - Mar 01, 2017 30 | 31 | * Adds support for enter and exit animations for NotificationItem. (thanks to @OriR) 32 | 33 | ## 0.2.11 - Dec 06, 2016 34 | 35 | * Added `clearNotifications()` method (thanks to @saaqibz) 36 | 37 | ## 0.2.10 - Aug 29, 2016 38 | 39 | * Allows children content to override `action`. (thanks to @gor181) 40 | 41 | ## 0.2.9 - Aug 25, 2016 42 | 43 | * Improved CSS styles for better performance 44 | * Merged pull request to avoid warnings related to component state 45 | 46 | ## 0.2.7 - Nov 20, 2015 47 | 48 | **React 15 support:** 49 | 50 | * Version 0.2.x now supports React 15 too. 51 | 52 | 53 | ## 0.2.6 - Nov 20, 2015 54 | 55 | **Bugfix from PR:** 56 | 57 | * Fix wrapper styles override. 58 | 59 | 60 | ## 0.2.5 - Oct 15, 2015 61 | 62 | **Implemented enhancements:** 63 | 64 | * Action property no longer needs a callback, just a label. 65 | 66 | ## 0.2.4 - Oct 12, 2015 67 | 68 | **Implemented enhancements:** 69 | 70 | * Added React and ReactDOM as peerDependencies and devDependencies to help on component development. 71 | 72 | ## 0.2.3 - Oct 11, 2015 73 | 74 | **Implemented enhancements:** 75 | 76 | * Possibility to remove notification by uid. 77 | * Added onAdd property to notification object. 78 | * Improved styles. 79 | 80 | ## 0.2.2 - Oct 10, 2015 81 | 82 | ** Removed unused code** 83 | 84 | * Some unnecessary `console.logs` was left behind. 85 | 86 | ## 0.2.1 - Oct 9, 2015 87 | 88 | **Implemented enhancements:** 89 | 90 | * Improved function to get specific style based on element. 91 | * Improved notification styles. 92 | * Added ESLint and linted all src files. 93 | 94 | ## 0.2.0 - Oct 9, 2015 95 | 96 | **Implemented enhancements:** 97 | 98 | * Now supports React 0.14! 99 | 100 | ## 0.1.17 - Oct 9, 2015 101 | 102 | **Implemented enhancements, merged pull requrests:** 103 | 104 | * Fix dismissible false to not require an action. 105 | * Added CHANGELOG and LICENSE files. 106 | 107 | ## 0.1.15 - Oct 1, 2015 108 | 109 | **Implemented enhancements:** 110 | 111 | * `addNotification()` method now returns the notification object. 112 | * Added method `removeNotification()` to remove a notification programmatically based on returned notification object. 113 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Igor Prado 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Notification System 2 | 3 | [![npm version](https://badge.fury.io/js/react-notification-system.svg)](http://badge.fury.io/js/react-notification-system) [![npm](https://img.shields.io/npm/dm/react-notification-system.svg)](https://www.npmjs.com/package/react-notification-system) [![Dependency Status](https://david-dm.org/igorprado/react-notification-system.svg)](https://david-dm.org/igorprado/react-notification-system) [![devDependency Status](https://david-dm.org/igorprado/react-notification-system/dev-status.svg)](https://david-dm.org/igorprado/react-notification-system#info=devDependencies) [![Build Status](https://travis-ci.org/igorprado/react-notification-system.svg?branch=master)](https://travis-ci.org/igorprado/react-notification-system) [![Coverage Status](https://coveralls.io/repos/igorprado/react-notification-system/badge.svg?branch=master&service=github)](https://coveralls.io/github/igorprado/react-notification-system?branch=master) 4 | 5 | > A complete and totally customizable component for notifications in React. 6 | 7 | _Initially built for [Eterpret](http://dev.eterpret.com) @ [Scalable Path](http://www.scalablepath.com)._ 8 | 9 | Screenshot 10 | 11 | ## Installing 12 | 13 | This component is available as CommonJS and UMD module. Install via NPM running: 14 | 15 | ``` 16 | npm install react-notification-system 17 | ``` 18 | 19 | ### Important 20 | 21 | For **React ^0.14.x** or **React ^15.x.x**, use version 0.2.x: 22 | 23 | ``` 24 | npm install react-notification-system@0.2.x 25 | ``` 26 | 27 | For **React 0.13.x**, use version 0.1.x: 28 | 29 | ``` 30 | npm install react-notification-system@0.1.x 31 | ``` 32 | 33 | 34 | 35 | ## Using 36 | 37 | For optimal appearance, this component **must be rendered on a top level HTML element** in your application to avoid position conflicts. 38 | 39 | Here is a basic example. For a more advanced usage, please see the [example code](https://github.com/igorprado/react-notification-system/blob/master/example/src/scripts/App.jsx). 40 | 41 | 42 | Class-based components can also be used as follows 43 | ```jsx 44 | import React from 'react'; 45 | import ReactDOM from 'react-dom'; 46 | import NotificationSystem from 'react-notification-system'; 47 | 48 | export default class MyComponent extends React.Component { 49 | notificationSystem = React.createRef(); 50 | 51 | addNotification = event => { 52 | event.preventDefault(); 53 | const notification = this.notificationSystem.current; 54 | notification.addNotification({ 55 | message: 'Notification message', 56 | level: 'success' 57 | }); 58 | }; 59 | 60 | render() { 61 | return ( 62 |
63 | 64 | 65 |
66 | ); 67 | } 68 | } 69 | 70 | ReactDOM.render( 71 | React.createElement(MyComponent), 72 | document.getElementById('app') 73 | ); 74 | ``` 75 | 76 | ## Methods 77 | 78 | ### `addNotification(notification)` 79 | 80 | Add a notification object. This displays the notification based on the [object](#creating-a-notification) you passed. 81 | 82 | Returns the notification object to be used to programmatically dismiss a notification. 83 | 84 | ### `removeNotification(notification)` 85 | 86 | Remove a notification programmatically. You can pass an object returned by `addNotification()` or by `onAdd()` callback. If passing an object, you need to make sure it must contain the `uid` property. You can pass only the `uid` too: `removeNotification(uid)`. 87 | 88 | 89 | ### `editNotification(notification, newProperties)` 90 | 91 | Edit a notification programmatically. You can pass an object previously returned by `addNotification()` or by `onAdd()` callback as `notification`. If passing an object as `notification`, you need to make sure it must contain the `uid` property. You can pass only the `uid` too: `editNotification(uid, newProperties)`. 92 | 93 | 94 | ### `clearNotifications()` 95 | 96 | Removes ALL notifications programatically. 97 | 98 | ## Creating a notification 99 | 100 | The notification object has the following properties: 101 | 102 | | Name | Type | Default | Description | 103 | |------------ |--------------- |--------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 104 | | title | string | null | Title of the notification | 105 | | message | string | null | Message of the notification | 106 | | level | string | null | Level of the notification. Available: **success**, **error**, **warning** and **info** | 107 | | position | string | tr | Position of the notification. Available: **tr (top right)**, **tl (top left)**, **tc (top center)**, **br (bottom right)**, **bl (bottom left)**, **bc (bottom center)** | 108 | | autoDismiss | integer | 5 | Delay in seconds for the notification go away. Set this to **0** to not auto-dismiss the notification | 109 | | dismissible | string | both | Settings controlling how the user can dismiss the notification and whether the dismiss button is visible. Available: **both (The disable button is visible and the user can click anywhere on the notification to dismiss)**, **click (The disable button is NOT visible and the user can click anywhere on the notification to dismiss)**, **button (The user can click on the disable button to dismiss the notifiction)**, **none (None [See more](#dismissible))** | 110 | | action | object | null | Add a button with label and callback function (callback is optional). [See more](#action) | 111 | | children | element,string | null | Adds custom content, and overrides `action` (if defined) [See more](#children) | 112 | | onAdd | function | null | A callback function that will be called when the notification is successfully added. The first argument is the original notification e.g. `function (notification) { console.log(notification.title + 'was added'); }` | 113 | | onRemove | function | null | A callback function that will be called when the notification is about to be removed. The first argument is the original notification e.g. `function (notification) { console.log(notification.title + 'was removed'); }` | 114 | | uid | integer/string | null | Overrides the internal `uid`. Useful if you are managing your notifications id. Notifications with same `uid` won't be displayed. | 115 | 116 | 117 | ### Dismissible 118 | 119 | If set to 'none', the button will only be dismissible programmatically or after autoDismiss timeout. [See more](#removenotificationnotification) 120 | 121 | ### Action 122 | 123 | Add a button and a callback function to the notification. If this button is clicked, the callback function is called (if provided) and the notification is dismissed. 124 | 125 | ```js 126 | notification = { 127 | [...], 128 | action: { 129 | label: 'Button name', 130 | callback: function() { 131 | console.log('Notification button clicked!'); 132 | } 133 | } 134 | } 135 | 136 | ``` 137 | 138 | ### Children 139 | 140 | Add custom content / react elements 141 | 142 | ```js 143 | notification = { 144 | [...], 145 | children: ( 146 |
147 |

Hello World

148 | Anchor 149 |
150 | ) 151 | } 152 | 153 | ``` 154 | 155 | ## Styles 156 | 157 | This component was made to work as plug and play. For that, a handcrafted style was added to it and is used as inline CSS. 158 | 159 | You can change this style by overriding the default inline styles or disable all inline styles and use your own styles. 160 | 161 | ### Overriding 162 | 163 | For this, use the `style` prop to pass an object with your styles. Your object must be something like this: 164 | 165 | ```js 166 | var style = { 167 | NotificationItem: { // Override the notification item 168 | DefaultStyle: { // Applied to every notification, regardless of the notification level 169 | margin: '10px 5px 2px 1px' 170 | }, 171 | 172 | success: { // Applied only to the success notification item 173 | color: 'red' 174 | } 175 | } 176 | } 177 | 178 | 179 | 180 | ``` 181 | 182 | Refer to [this file](https://github.com/igorprado/react-notification-system/blob/master/src/styles.js) to see what can you override. 183 | 184 | ### Disabling inline styles 185 | 186 | To disable all inline styles, just pass `false` to the prop `style`. 187 | 188 | ```js 189 | 190 | ``` 191 | 192 | Here is the notification HTML: 193 | 194 | ```html 195 |
196 |
197 |
198 |

Default title

199 |
Default message
200 | × 201 |
202 | 203 |
204 |
205 |
206 |
207 | 208 | ``` 209 | 210 | #### Important 211 | 212 | Using this method you have to take care of **every style**, from containers positions to animations. To control animations, use the classes `notification-visible` and `notification-hidden`. If your CSS styles will not handle any animation (transition), you need to set the prop `noAnimation` to `true` when adding the Notification System component: 213 | 214 | ```js 215 | 216 | ``` 217 | 218 | See [#74](https://github.com/igorprado/react-notification-system/issues/74) for more details. 219 | 220 | ### Appending/Prepending notifications 221 | 222 | You can control where should new notification appear (on the top or bottom of current notifications, defaults to bottom) by setting `newOnTop` boolean prop on `` component: 223 | 224 | ```js 225 | 226 | ``` 227 | 228 | This will render new notifications on top of current ones 229 | 230 | ## Roadmap 231 | 232 | * Improve tests and coverage 233 | * Improve performance 234 | 235 | ## Contributions 236 | 237 | Clone this repo by running: 238 | 239 | ``` 240 | git clone git@github.com:igorprado/react-notification-system.git 241 | ``` 242 | 243 | Enter the project folder and install the dependencies: 244 | 245 | ``` 246 | npm install 247 | ``` 248 | 249 | To start a development server and use the `example` app to load the component, type: 250 | 251 | ``` 252 | npm start 253 | ``` 254 | 255 | Open `http://localhost:8000`. 256 | 257 | --- 258 | 259 | Run the tests: 260 | 261 | ``` 262 | npm test 263 | ``` 264 | 265 | You can find the coverage details under `coverage/` folder. 266 | 267 | After that, just edit the files under `src/` and `example/src/app.js`. It uses React hot reload. 268 | 269 | This component is under construction. I will add more guidelines to who wants to contribute. 270 | -------------------------------------------------------------------------------- /devServer.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var webpack = require('webpack'); 4 | var config = require('./webpack.config.dev'); 5 | 6 | var app = express(); 7 | var compiler = webpack(config); 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | noInfo: true, 11 | publicPath: '/' + config.output.publicPath 12 | })); 13 | 14 | app.use(require('webpack-hot-middleware')(compiler)); 15 | 16 | app.get('*', function(req, res) { 17 | res.sendFile(path.join(__dirname, 'example/index.html')); 18 | }); 19 | 20 | app.listen(8000, 'localhost', function(err) { 21 | if (err) { 22 | console.log(err); 23 | return; 24 | } 25 | 26 | console.log('Listening at http://localhost:8000'); 27 | }); 28 | -------------------------------------------------------------------------------- /example/bower_components/primer-css/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "primer-css", 3 | "ignore": [ 4 | "docs/", 5 | ".gitignore", 6 | ".hound.yml", 7 | ".scss-lint.yml", 8 | "_config.yml", 9 | "Gemfile", 10 | "Gemfile.lock", 11 | "Gruntfile.js" 12 | ], 13 | "main": [ 14 | "scss/primer.scss" 15 | ], 16 | "dependencies": { 17 | "octicons": "*" 18 | }, 19 | "homepage": "https://github.com/primer/primer", 20 | "version": "2.3.5", 21 | "_release": "2.3.5", 22 | "_resolution": { 23 | "type": "version", 24 | "tag": "v2.3.5", 25 | "commit": "7c10c74c64e1788b8ccfee92031fbfa19d2088cd" 26 | }, 27 | "_source": "git://github.com/primer/primer.git", 28 | "_target": "~2.3.5", 29 | "_originalSource": "primer-css", 30 | "_direct": true 31 | } -------------------------------------------------------------------------------- /example/bower_components/primer-css/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "docs/bower_components" 3 | } -------------------------------------------------------------------------------- /example/bower_components/primer-css/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /example/bower_components/primer-css/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | [fork]: https://github.com/github/primer/fork 4 | [pr]: https://github.com/github/primer/compare 5 | [style]: http://primercss.io/guidelines/ 6 | 7 | Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. 8 | 9 | After you open your first pull request, you will be asked to accept [this license agreement](https://cla.github.com/). Let us know in the PR if you have any hesitation or concerns. 10 | 11 | ## Using the issue tracker 12 | 13 | The [issue tracker](https://github.com/primer/primer/issues) is the preferred channel for [bug reports](#bug-reports), [features requests](#feature-requests) and [submitting pull requests](#pull-requests), but please respect the following restrictions: 14 | 15 | * Please **do not** use the issue tracker for personal support requests. 16 | * Please **do not** derail or troll issues. Keep the discussion on topic and respect the opinions of others. 17 | * Please **do not** open issues or pull requests regarding the code in [`Normalize`](https://github.com/necolas/normalize.css) (open them in their respective repositories). 18 | 19 | ## Bug reports 20 | 21 | A bug is a _demonstrable problem_ that is caused by the code in the repository. Good bug reports are extremely helpful, so thanks! 22 | 23 | Guidelines for bug reports: 24 | 25 | 0. **Validate and lint your code** — [validate your HTML](http://html5.validator.nu) to ensure your problem isn't caused by a simple error in your own code. 26 | 27 | 1. **Use the GitHub issue search** — check if the issue has already been reported. 28 | 29 | 2. **Check if the issue has been fixed** — try to reproduce it using the latest `master` or development branch in the repository. 30 | 31 | 3. **Isolate the problem** — ideally create a [reduced test case](https://css-tricks.com/reduced-test-cases/) and a live example. [This JS Bin](http://jsbin.com/lefey/1/edit?html,output) is a helpful template. 32 | 33 | A good bug report shouldn't leave others needing to chase you up for more information. Please try to be as detailed as possible in your report. What is your environment? What steps will reproduce the issue? What browser(s) and OS experience the problem? Do other browsers show the bug differently? What would you expect to be the outcome? All these details will help people to fix any potential bugs. 34 | 35 | Example: 36 | 37 | > Short and descriptive example bug report title 38 | > 39 | > A summary of the issue and the browser/OS environment in which it occurs. If 40 | > suitable, include the steps required to reproduce the bug. 41 | > 42 | > 1. This is the first step 43 | > 2. This is the second step 44 | > 3. Further steps, etc. 45 | > 46 | > `` - a link to the reduced test case 47 | > 48 | > Any other information you want to share that is relevant to the issue being reported. This might include the lines of code that you have identified as causing the bug, and potential solutions (and your opinions on their merits). 49 | 50 | ## Feature requests 51 | 52 | Feature requests are welcome. But take a moment to find out whether your idea fits with the scope and aims of the project. It's up to *you* to make a strong case to convince the project's developers of the merits of this feature. Please provide as much detail and context as possible. 53 | 54 | ## Pull requests 55 | 56 | Good pull requests—patches, improvements, new features—are a fantastic help. They should remain focused in scope and avoid containing unrelated commits. 57 | 58 | **Please ask first** before embarking on any significant pull request (e.g. implementing features, refactoring code, porting to a different language), otherwise you risk spending a lot of time working on something that the project's developers might not want to merge into the project. 59 | 60 | Adhering to the following process is the best way to get your work included in the project: 61 | 62 | 1. Fork and clone the repository 63 | 2. Configure and install the dependencies: `bower install` 64 | 3. Create a new branch: `git checkout -b my-branch-name` 65 | 4. Make your change, add tests, and make sure the tests still pass 66 | 5. Push to your fork and [submit a pull request](https://help.github.com/articles/creating-a-pull-request/) 67 | 6. Pat your self on the back and wait for your pull request to be reviewed and merged. 68 | 69 | Here are a few things you can do that will increase the likelihood of your pull request being accepted: 70 | 71 | - Follow the [style guide][style]. 72 | - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. 73 | - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 74 | 75 | ## Resources 76 | 77 | - [Contributing to Open Source on GitHub](https://guides.github.com/activities/contributing-to-open-source/) 78 | - [Using Pull Requests](https://help.github.com/articles/using-pull-requests/) 79 | - [GitHub Help](https://help.github.com) 80 | -------------------------------------------------------------------------------- /example/bower_components/primer-css/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 GitHub, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/bower_components/primer-css/MAINTAINING.md: -------------------------------------------------------------------------------- 1 | # Maintaining 2 | 3 | Steps for updating and releasing changes to Primer and it's site. 4 | 5 | ## Versioning 6 | 7 | Primer follows the semantic versioning approach: 8 | 9 | - Bug fixes and docs updates are patch releases, so `1.0.x`. 10 | - New additions are minor updates, so `1.x.x`. 11 | - Deleting or rewriting anything are major updates, so `x.x.x`. 12 | 13 | ## Changelogs and milestones 14 | 15 | Changelogs are handled with dedicated tracking issues ([see example](https://github.com/primer/primer/issues/108)). When starting work on a new release: 16 | 17 | 1. Open a new milestone. 18 | 2. Open a new tracking issue and immediately lock it. (No comments are needed, ship lists are just for us.) 19 | 3. As you close issues and merge pull requests, add a link to those threads to the tracking issue. 20 | 21 | When the release and milestone are about ready to ship, move on the the releasing flow. 22 | 23 | ## Releasing 24 | 25 | Have a new version to release? Hell yeah, let's do it. 26 | 27 | 1. Bump the version numbers in `_config.yml` for our docs and `package.json` for dependency management. 28 | 2. Run `$ grunt` to generate the latest compiled CSS and Parker stats. 29 | 3. Recompile Jekyll for the latest docs changes. 30 | 4. Punt any remaining open issues and PRs on the milestone to the next milestone, then close that milestone. 31 | 5. Head to and create a new release. Title it `vX.X.X` and post the changelog to the body. 32 | 6. Run `$ grunt publish` to push the latest docs and CSS changes to . 33 | 7. Rejoice! 34 | -------------------------------------------------------------------------------- /example/bower_components/primer-css/README.md: -------------------------------------------------------------------------------- 1 | # Primer 2 | 3 | Primer is the CSS toolkit that powers GitHub's front-end design. It's purposefully limited to common components to provide our developers with the most flexibility, and to keep GitHub uniquely *GitHubby*. It's built with SCSS and available via Bower, so it's easy to include all or part of it within your own project. 4 | 5 | [**Read the Primer documentation**](http://primercss.io) to learn more. 6 | 7 | _**Heads up!** We love open source, but Primer is unlikely to add new features that are not used in GitHub.com. It's first and foremost our CSS toolkit. We really love to share though, so hopefully that means we're still friends <3._ 8 | 9 | ## Contents 10 | 11 | - [Install](#install) 12 | - [Usage](#usage) 13 | - [Documentation](#documentation) 14 | - [Dependencies](#dependencies) 15 | - [Running locally](#running-locally) 16 | - [Publishing](#publishing) 17 | - [Primer stats](#primer-stats) 18 | - [Updating](#updating) 19 | - [Contributing](#contributing) 20 | - [Versioning](#versioning) 21 | - [License](#license) 22 | 23 | ## Install 24 | 25 | ### Manually 26 | 27 | Download the [latest release](https://github.com/primer/primer/releases/latest) and copy the SCSS files over to your own project. Once your files are in place, jump to the [usage guidelines](#usage) for including Primer into your own CSS. 28 | 29 | ### Bower 30 | 31 | ``` 32 | $ bower install primer-css --save 33 | ``` 34 | 35 | ### Things to know 36 | 37 | **Hey, GitHubbers!** For GitHub.com, you'll need to `cd` into `vendor/assets` and run `bower install` there. Be sure to commit and push all the changes, including the `bower.json` and everything under `bower_components`. 38 | 39 | ## Usage 40 | 41 | Once included, simply `@import` either the master SCSS file, or the individual files as you need them. 42 | 43 | ```scss 44 | // Example: All of Primer 45 | @import "primer-css/scss/primer"; 46 | 47 | // Example: Individual files 48 | @import "primer-css/scss/variables"; 49 | @import "primer-css/scss/mixins"; 50 | @import "primer-css/scss/base"; 51 | ``` 52 | 53 | ## Documentation 54 | 55 | Primer's documentation is built with Jekyll and published to `http://primercss.io` via the `gh-pages` branch. 56 | 57 | ### Dependencies 58 | 59 | You'll need the following installed: 60 | 61 | - Latest Jekyll (minimum v2.2.0): `$ gem install jekyll` 62 | - Latest Rouge: `$ gem install rouge` 63 | - Latest Sass: `$ gem install sass` 64 | - Latest Grunt CLI: `$ npm install -g grunt-cli` 65 | - [Node.js and npm](http://nodejs.org/download/) 66 | 67 | Chances are you have all this already if you work on `github/github` or similar projects. If you have all those set up, now you can install the dependencies: 68 | 69 | ```bash 70 | $ npm install 71 | $ bower install 72 | ``` 73 | 74 | ### Running locally 75 | 76 | From the Terminal, start a local Jekyll server: 77 | 78 | ```bash 79 | $ jekyll serve 80 | ``` 81 | 82 | Open a second Terminal tab to automatically recompile the Sass files, run autoprefixer, and update our [Primer stats file](#primer-stats): 83 | 84 | ```bash 85 | $ grunt watch 86 | ``` 87 | 88 | Alternatively, you can manually run `grunt` and `jekyll serve` when needed. 89 | 90 | ### Publishing 91 | 92 | Use the included Grunt task to generate and publish Primer's docs to the `gh-pages` branch. 93 | 94 | ```bash 95 | $ grunt publish 96 | ``` 97 | 98 | This takes the `_site` directory, generates it's own Git repository there, and publishes the contents to the `gh-pages` branch here on GitHub. Changes are reflected in the hosted docs within a minute or so. 99 | 100 | ### Primer stats 101 | 102 | When compiling or watching the Sass files, Primer will automatically generate a `.primer-stats.md` file. This is tracked in the Git repository to provide us historical and contextual information on the changes we introduce. For example, we'll know when the number of selectors or declarations rises sharply within a single change. 103 | 104 | ## Updating 105 | 106 | Within `bower.json`, update to a new release by changing the version number that follows the `#` in the dependency URL. 107 | 108 | ```json 109 | { 110 | "name": "myapp", 111 | "dependencies": { 112 | "primer-css": "x.x.x" 113 | } 114 | } 115 | ``` 116 | 117 | To pull down the updated package, `cd` into `vendor/assets`, and run `bower install`. 118 | 119 | ``` 120 | $ cd vendor/assets 121 | $ bower install 122 | ``` 123 | 124 | Check in `bower.json` and all changes under `vendor/assets/bower_components`. 125 | 126 | ## Development 127 | 128 | Development of Primer happens in our primary branch, `master`. For stable versions, see the [releases page](https://github.com/primer/primer/releases). `master` will always be up to date with the latest changes, including those which have yet to be released. 129 | 130 | ## Contributing 131 | 132 | By contributing to Primer, you agree to the terms presented in [this license agreement](https://cla.github.com/). *More information will be provided here soon.* 133 | 134 | When contributing changes to Primer, be sure to do the following steps when opening a pull request: 135 | 136 | 1. Bump the version number in `bower.json` (it's purely placebo right now, but it's good habit) and `package.json`. 137 | 2. Run `grunt` and commit the changes. This compiles the SCSS to CSS so we can do basic analysis on the number of selectors, file size, etc. 138 | 139 | In addition, please read through our [contributing guidelines](https://github.com/primer/primer/blob/master/CONTRIBUTING.md). Included are directions for opening issues, coding standards, and notes on development. 140 | 141 | All HTML and CSS should conform to the [style guidelines](http://primercss.io/guidelines). 142 | 143 | Editor preferences are available in the [editor config](https://github.com/primer/primer/blob/master/.editorconfig) for easy use in common text editors. Read more and download plugins at . 144 | 145 | ## Versioning 146 | 147 | For transparency into our release cycle and in striving to maintain backward compatibility, Primer is maintained under [the Semantic Versioning guidelines](http://semver.org/). Sometimes we screw up, but we'll adhere to those rules whenever possible. 148 | 149 | ## License 150 | 151 | Created by and copyright GitHub, Inc. Released under the [MIT license](LICENSE.md). 152 | -------------------------------------------------------------------------------- /example/bower_components/primer-css/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "primer-css", 3 | "ignore": [ 4 | "docs/", 5 | ".gitignore", 6 | ".hound.yml", 7 | ".scss-lint.yml", 8 | "_config.yml", 9 | "Gemfile", 10 | "Gemfile.lock", 11 | "Gruntfile.js" 12 | ], 13 | "main": [ 14 | "scss/primer.scss" 15 | ], 16 | "dependencies": { 17 | "octicons": "*" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/bower_components/primer-css/css/.primer-stats.md: -------------------------------------------------------------------------------- 1 | # [primer]( http://primercss.io ) 2 | 3 | **Version:** `2.3.3` 4 | 5 | ## Parker Report 6 | 7 | ### css/primer.css 8 | 9 | - **Total Stylesheets:** 1 10 | - **Total Stylesheet Size:** 28889 11 | - **Total Media Queries:** 1 12 | - **Total Rules:** 372 13 | - **Selectors Per Rule:** 1.521505376344086 14 | - **Total Selectors:** 566 15 | - **Identifiers Per Selector:** 2.2703180212014136 16 | - **Specificity Per Selector:** 17.32155477031802 17 | - **Top Selector Specificity:** 50 18 | - **Top Selector Specificity Selector:** .fullscreen-overlay-enabled.dark-theme .tooltipped .tooltipped-s:before 19 | - **Total Id Selectors:** 0 20 | - **Total Identifiers:** 1285 21 | - **Total Declarations:** 924 22 | - **Total Unique Colors:** 81 23 | - **Total Important Keywords:** 1 24 | -------------------------------------------------------------------------------- /example/bower_components/primer-css/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "primer-css", 3 | "version": "2.3.5", 4 | "homepage": "http://primercss.io", 5 | "author": "GitHub, Inc.", 6 | "scss": "./scss/primer.scss", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/primer/primer.git" 11 | }, 12 | "devDependencies": { 13 | "autoprefixer-core": "~5.2.1", 14 | "grunt": "~0.4.5", 15 | "grunt-build-control": "~0.2.0", 16 | "grunt-jekyll": "~0.4.2", 17 | "grunt-parker": "~0.1.0", 18 | "grunt-sass": "~0.18.0", 19 | "grunt-contrib-watch": "~0.6.1", 20 | "grunt-postcss": "~0.5.1" 21 | }, 22 | "description": "Primer is the CSS toolkit that powers GitHub's front-end design. It's purposefully limited to common components to provide our developers with the most flexibility, and to keep GitHub uniquely *GitHubby*. It's built with SCSS and available via Bower, so it's easy to include all or part of it within your own project.", 23 | "bugs": { 24 | "url": "https://github.com/primer/primer/issues" 25 | }, 26 | "main": "css/primer.css", 27 | "directories": { 28 | "doc": "docs" 29 | }, 30 | "dependencies": { 31 | "grunt-jekyll": "^0.4.2", 32 | "grunt-autoprefixer": "^2.2.0", 33 | "grunt-contrib-watch": "^0.6.1", 34 | "grunt-sass": "^0.18.1", 35 | "grunt-parker": "^0.1.3", 36 | "grunt": "^0.4.5", 37 | "grunt-build-control": "^0.2.2" 38 | }, 39 | "scripts": { 40 | "test": "echo \"Error: no test specified\" && exit 1" 41 | }, 42 | "keywords": [ 43 | "primer", 44 | "css", 45 | "github", 46 | "primercss" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /example/bower_components/primer-css/scss/_alerts.scss: -------------------------------------------------------------------------------- 1 | // Default flash 2 | .flash { 3 | position: relative; 4 | padding: 15px; 5 | font-size: 14px; 6 | line-height: 1.5; 7 | color: #246; 8 | background-color: #e2eef9; 9 | border: 1px solid #bac6d3; 10 | border-radius: 3px; 11 | 12 | p:last-child { 13 | margin-bottom: 0; 14 | } 15 | } 16 | 17 | // Contain the flash messages 18 | .flash-messages { 19 | margin-bottom: 20px; 20 | } 21 | 22 | // Close button 23 | .flash-close { 24 | float: right; 25 | width: 34px; 26 | height: 44px; 27 | margin: -11px; 28 | color: inherit; 29 | line-height: 40px; 30 | text-align: center; 31 | cursor: pointer; 32 | opacity: 0.6; 33 | // Undo ` 160 | 161 | ); 162 | } 163 | 164 | if (notification.position === 'in') { 165 | error.position = 'text-danger'; 166 | } 167 | 168 | if (notification.level === 'in') { 169 | error.level = 'text-danger'; 170 | } 171 | 172 | if (!notification.dismissible && !notification.actionState) { 173 | error.dismissible = 'text-danger'; 174 | error.action = 'text-danger'; 175 | } 176 | 177 | return ( 178 |
179 |

Notification generator

180 |

Open your console to see some logs from the component.

181 | 182 |
183 | 184 | 185 | Leave empty to hide. 186 |
187 | 188 |
189 | 190 | 191 | 192 | 195 | 196 |
197 | 198 |
199 | 200 | 209 | Open console to see the error after creating a notification. 210 |
211 | 212 |
213 | 214 | 221 | Open console to see the error after creating a notification. 222 |
223 | 224 |
225 | 226 | 232 |
233 | 234 |
235 | 236 | 237 | secs (0 means infinite) 238 |
239 | 240 |
241 |
242 | 245 |
246 | { action } 247 |
248 |
249 |
250 | 253 |
254 |
255 | This notification will be only dismissible programmatically or after "autoDismiss" timeout (if set). 256 | 257 | { removeButton } 258 | 259 |
260 | 261 |
262 | 263 |
264 | ); 265 | } 266 | } 267 | 268 | NotificationGenerator.propTypes = { 269 | notifications: PropTypes.func.isRequired, 270 | allowHTML: PropTypes.func, 271 | newOnTop: PropTypes.func 272 | }; 273 | 274 | module.exports = NotificationGenerator; 275 | -------------------------------------------------------------------------------- /example/src/scripts/showcase.js: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | const CustomElement = require('./CustomElement'); 3 | 4 | const showcase = [ 5 | { 6 | title: 'Hey, it\'s good to see you!', 7 | message: 'Now you can see how easy it is to use notifications in React!', 8 | level: 'success', 9 | position: 'tr', 10 | action: { 11 | label: 'Awesome!', 12 | callback: function() { 13 | console.log('Clicked'); 14 | } 15 | } 16 | }, 17 | { 18 | title: 'Hey, it\'s good to see you!', 19 | message: 'I come with custom content!', 20 | level: 'success', 21 | position: 'tr', 22 | children: ( 23 |
24 | 25 |
26 | ) 27 | }, 28 | { 29 | title: 'I\'ll be here forever!', 30 | message: 'Just kidding, you can click me.', 31 | level: 'success', 32 | position: 'tr', 33 | autoDismiss: 0 34 | }, 35 | { 36 | title: 'I don\'t have a dismiss button...', 37 | message: 'But you can still click to get rid of me.', 38 | autoDismiss: 0, 39 | level: 'success', 40 | position: 'tr', 41 | dismissible: 'click' 42 | }, 43 | { 44 | title: 'Bad things can happen too!', 45 | message: 'Four notification types: `success`, `error`, `warning` and `info`', 46 | level: 'error', 47 | position: 'tl' 48 | }, 49 | { 50 | title: 'Advise!', 51 | message: 'Showing all possible notifications works better on a larger screen', 52 | level: 'info', 53 | position: 'tc' 54 | }, 55 | { 56 | title: 'Warning!', 57 | message: 'It\'s not a good idea show all these notifications at the same time!', 58 | level: 'warning', 59 | position: 'bc', 60 | action: { 61 | label: 'Got it!' 62 | } 63 | }, 64 | { 65 | title: 'Success!', 66 | message: 'I\'m out of ideas', 67 | level: 'success', 68 | position: 'bl' 69 | }, 70 | { 71 | title: 'I\'m here forever...', 72 | message: 'Until you click me.', 73 | autoDismiss: 0, 74 | level: 'error', 75 | position: 'br' 76 | }, 77 | { 78 | title: 'I\'m here forever...', 79 | message: 'Until you click the dismiss button.', 80 | autoDismiss: 0, 81 | level: 'error', 82 | position: 'br', 83 | dismissible: 'button' 84 | } 85 | ]; 86 | 87 | module.exports = showcase; 88 | 89 | -------------------------------------------------------------------------------- /example/src/styles/base.sass: -------------------------------------------------------------------------------- 1 | // Import defaults 2 | @import "../../bower_components/primer-css/scss/variables" 3 | @import "../../bower_components/primer-css/scss/mixins" 4 | 5 | // My overrides & variables 6 | @import "variables" 7 | 8 | // Import base and used components 9 | @import "../../bower_components/primer-css/scss/normalize" 10 | @import "../../bower_components/primer-css/scss/base" 11 | @import "../../bower_components/primer-css/scss/type" 12 | @import "../../bower_components/primer-css/scss/layout" 13 | @import "../../bower_components/primer-css/scss/forms" 14 | @import "../../bower_components/primer-css/scss/utility" 15 | @import "../../bower_components/primer-css/scss/buttons" 16 | 17 | html, body 18 | height: 100% 19 | 20 | .header 21 | padding: 50px 15px 0 22 | text-align: center 23 | border-bottom: 6px solid $blue-green 24 | position: relative 25 | height: auto 26 | 27 | @media(min-width: 520px) 28 | padding: 150px 0 0 29 | 30 | .overlay 31 | background: url(../images/congruent_pentagon.png) top left repeat 32 | position: absolute 33 | top: 0 34 | left: 0 35 | width: 100% 36 | height: 100% 37 | opacity: 0.3 38 | background-attachment: fixed 39 | 40 | .content 41 | position: relative 42 | 43 | .gradient 44 | background: #00b7ea; /* Old browsers */ 45 | background: -moz-linear-gradient(-45deg, #00b7ea 0%, #8ca246 100%); /* FF3.6+ */ 46 | background: -webkit-gradient(linear, left top, right bottom, color-stop(0%,#00b7ea), color-stop(100%,#8ca246)); /* Chrome,Safari4+ */ 47 | background: -webkit-linear-gradient(-45deg, #00b7ea 0%,#8ca246 100%); /* Chrome10+,Safari5.1+ */ 48 | background: -o-linear-gradient(-45deg, #00b7ea 0%,#8ca246 100%); /* Opera 11.10+ */ 49 | background: -ms-linear-gradient(-45deg, #00b7ea 0%,#8ca246 100%); /* IE10+ */ 50 | background: linear-gradient(135deg, #00b7ea 0%,#8ca246 100%); /* W3C */ 51 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#00b7ea', endColorstr='#8ca246',GradientType=1 ); /* IE6-9 fallback on horizontal gradient */ 52 | background-attachment: fixed 53 | 54 | .title 55 | color: #FFF 56 | font-size: 40px 57 | letter-spacing: -1px 58 | 59 | @media(min-width: 520px) 60 | font-size: 64px 61 | 62 | .subtitle 63 | color: $blue-green 64 | font-size: 20px 65 | 66 | @media(min-width: 520px) 67 | font-size: 28px 68 | 69 | .versions 70 | color: lighten($blue-green, 5%) 71 | 72 | .btn-show-magic, .btn-show-magic:active, .btn-show-magic:focus 73 | margin-top: 30px 74 | padding: 18px 20px 75 | font-size: 18px 76 | line-height: 28px 77 | border-color: #FFF 78 | background: transparent 79 | color: #FFF 80 | transition: all 0.3s ease-in-out 81 | 82 | @media(min-width: 520px) 83 | margin-top: 50px 84 | padding: 18px 30px 85 | font-size: 24px 86 | line-height: 32px 87 | 88 | &:hover 89 | border-color: $blue-green 90 | color: #FFF 91 | background-color: $blue-green 92 | box-shadow: none 93 | 94 | &:focus 95 | border-color: $blue-green 96 | 97 | .width-warning 98 | display: block 99 | font-size: 12px 100 | margin: 10px 0 101 | color: #9e0000 102 | 103 | @media(min-width: 520px) 104 | display: none 105 | 106 | .more-magic 107 | display: block 108 | color: #FFF 109 | margin-top: 5px 110 | color: $blue-green 111 | 112 | .github-buttons 113 | margin-top: 20px 114 | padding: 20px 0 115 | 116 | @media(min-width: 520px) 117 | margin-top: 70px 118 | 119 | iframe 120 | margin: 0 10px 121 | 122 | .wrapper 123 | width: 90%; 124 | margin: 20px auto 0 125 | position: relative 126 | 127 | @media(min-width: 520px) 128 | width: auto 129 | max-width: 520px 130 | margin: 40px auto 0 131 | 132 | 133 | h2 134 | color: $blue-green 135 | 136 | a 137 | color: $blue-green 138 | font-weight: bold 139 | 140 | .hide 141 | display: none !important 142 | 143 | .footer 144 | color: #FFF 145 | text-align: center 146 | padding: 20px 0 147 | position: relative 148 | -------------------------------------------------------------------------------- /example/src/styles/generator.sass: -------------------------------------------------------------------------------- 1 | // My overrides & variables 2 | @import "variables" 3 | 4 | .generator 5 | 6 | h2 7 | text-align: center 8 | font-size: 32px; 9 | 10 | .form-group 11 | margin: 15px 0 20px 12 | 13 | label 14 | font-size: $body-font-size - 1 15 | 16 | &>label 17 | width: 20% 18 | display: inline-block 19 | 20 | .form-control 21 | width: 80% 22 | font-size: $body-font-size 23 | 24 | small 25 | font-size: 80% 26 | display: block 27 | margin: 5px 0 0 20% 28 | 29 | .btn-notify 30 | padding: 12px 0 31 | font-size: 18px 32 | -------------------------------------------------------------------------------- /example/src/styles/variables.sass: -------------------------------------------------------------------------------- 1 | // Primer overrides 2 | $body-font: 'Roboto', sans-serif 3 | $body-font-size: 16px 4 | 5 | // Variables 6 | $blue-green: #0C6D6D 7 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | var webpack = require('webpack'); 4 | 5 | var coverage; 6 | var reporters; 7 | if (process.env.CONTINUOUS_INTEGRATION) { 8 | coverage = { 9 | type: 'lcov', 10 | dir: 'coverage/' 11 | }; 12 | reporters = ['coverage', 'coveralls']; 13 | } 14 | else { 15 | coverage = { 16 | type: 'html', 17 | dir: 'coverage/' 18 | }; 19 | reporters = ['progress', 'coverage']; 20 | } 21 | 22 | module.exports = function (config) { 23 | config.set({ 24 | browsers: ['Firefox'], 25 | browserNoActivityTimeout: 30000, 26 | frameworks: ['mocha', 'chai', 'sinon-chai'], 27 | files: ['tests.webpack.js'], 28 | preprocessors: {'tests.webpack.js': ['webpack', 'sourcemap']}, 29 | reporters: reporters, 30 | coverageReporter: coverage, 31 | webpack: { 32 | devtool: 'inline-source-map', 33 | module: { 34 | loaders: [ 35 | // TODO: fix sourcemaps 36 | // see: https://github.com/deepsweet/isparta-loader/issues/1 37 | { 38 | test: /\.js$|.jsx$/, 39 | loader: 'babel?presets=airbnb', 40 | exclude: /node_modules/ 41 | }, 42 | { 43 | test: /\.js$|.jsx$/, 44 | loader: 'isparta?{babel: {stage: 0}}', 45 | exclude: /node_modules|test|utils/ 46 | } 47 | ] 48 | }, 49 | plugins: [ 50 | new webpack.DefinePlugin({ 51 | 'process.env': { 52 | BROWSER: JSON.stringify(true), 53 | NODE_ENV: JSON.stringify('test') 54 | } 55 | }) 56 | ], 57 | resolve: { 58 | extensions: ['', '.js', '.jsx'], 59 | modulesDirectories: ['node_modules', 'src'] 60 | } 61 | }, 62 | webpackServer: { 63 | noInfo: true 64 | } 65 | }); 66 | }; 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-notification-system", 3 | "version": "0.4.0", 4 | "description": "A React Notification System fully customized", 5 | "main": "dist/NotificationSystem.js", 6 | "scripts": { 7 | "test": "karma start --single-run", 8 | "test-watch": "karma start", 9 | "prepare-build": "npm run lint && rimraf dist/", 10 | "prebuild": "npm run prepare-build", 11 | "build": "jsx -x jsx ./src ./dist & jsx ./src ./dist && webpack --stats --config webpack.config.umd.prod.js && webpack --stats --config webpack.config.umd.dev.js", 12 | "lint": "eslint src --ext .jsx,.js", 13 | "start": "NODE_ENV=development node devServer.js", 14 | "build:example": "rimraf example/build/ && webpack --stats --config webpack.config.prod.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/igorprado/react-notification-system" 19 | }, 20 | "keywords": [ 21 | "react", 22 | "notification", 23 | "notification system", 24 | "component", 25 | "react component", 26 | "react-component" 27 | ], 28 | "author": "Igor Prado", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/igorprado/react-notification-system/issues" 32 | }, 33 | "homepage": "https://github.com/igorprado/react-notification-system", 34 | "dependencies": { 35 | "object-assign": "^4.0.1", 36 | "prop-types": "^15.5.6" 37 | }, 38 | "peerDependencies": { 39 | "react": "0.14.x || ^15.0.0 || ^16.0.0", 40 | "react-dom": "0.14.x || ^15.0.0 || ^16.0.0" 41 | }, 42 | "devDependencies": { 43 | "autoprefixer-loader": "^3.1.0", 44 | "babel-core": "^6.14.0", 45 | "babel-eslint": "^6.1.2", 46 | "babel-loader": "^6.2.5", 47 | "babel-plugin-react-class-display-name": "^0.1.0", 48 | "babel-plugin-react-transform": "^2.0.2", 49 | "babel-preset-airbnb": "^2.0.0", 50 | "chai": "^4.1.2", 51 | "css-loader": "^0.24.0", 52 | "eslint": "4.9.0", 53 | "eslint-config-airbnb": "^16.0.0", 54 | "eslint-plugin-import": "^2.7.0", 55 | "eslint-plugin-jsx-a11y": "^6.0.2", 56 | "eslint-plugin-react": "^7.4.0", 57 | "express": "^4.13.3", 58 | "extract-text-webpack-plugin": "^0.8.2", 59 | "file-loader": "^0.8.4", 60 | "isparta-loader": "^1.0.0", 61 | "karma": "^1.7.1", 62 | "karma-chai-plugins": "^0.9.0", 63 | "karma-chrome-launcher": "^2.2.0", 64 | "karma-cli": "^1.0.1", 65 | "karma-coverage": "^1.1.1", 66 | "karma-coveralls": "^1.1.2", 67 | "karma-firefox-launcher": "^1.0.1", 68 | "karma-mocha": "^1.3.0", 69 | "karma-sourcemap-loader": "^0.3.6", 70 | "karma-webpack": "^1.7.0", 71 | "mocha": "^4.0.1", 72 | "node-sass": "^4.13.0", 73 | "react": "^16.11.0", 74 | "react-dom": "^16.11.0", 75 | "react-tools": "^0.13.2", 76 | "react-transform-catch-errors": "^1.0.0", 77 | "react-transform-hmr": "^1.0.1", 78 | "redbox-react": "^1.1.1", 79 | "rimraf": "^2.4.3", 80 | "sass-loader": "^3.0.0", 81 | "style-loader": "^0.12.4", 82 | "webpack": "^1.12.2", 83 | "webpack-dev-middleware": "^1.2.0", 84 | "webpack-hot-middleware": "^2.4.1" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/NotificationContainer.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var PropTypes = require('prop-types'); 3 | var NotificationItem = require('./NotificationItem'); 4 | var Constants = require('./constants'); 5 | 6 | class NotificationContainer extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | // Fix position if width is overrided 10 | this._style = props.getStyles.container(props.position); 11 | 12 | if ( 13 | props.getStyles.overrideWidth && 14 | (props.position === Constants.positions.tc || 15 | props.position === Constants.positions.bc) 16 | ) { 17 | this._style.marginLeft = -(props.getStyles.overrideWidth / 2); 18 | } 19 | } 20 | 21 | render() { 22 | var notifications; 23 | 24 | if ( 25 | [ 26 | Constants.positions.bl, 27 | Constants.positions.br, 28 | Constants.positions.bc 29 | ].indexOf(this.props.position) > -1 30 | ) { 31 | this.props.notifications.reverse(); 32 | } 33 | 34 | notifications = this.props.notifications.map((notification) => { 35 | return ( 36 | 46 | ); 47 | }); 48 | 49 | return ( 50 |
54 | {notifications} 55 |
56 | ); 57 | } 58 | } 59 | 60 | NotificationContainer.propTypes = { 61 | position: PropTypes.string.isRequired, 62 | notifications: PropTypes.array.isRequired, 63 | getStyles: PropTypes.object, 64 | onRemove: PropTypes.func, 65 | noAnimation: PropTypes.bool, 66 | allowHTML: PropTypes.bool, 67 | children: PropTypes.oneOfType([PropTypes.string, PropTypes.element]) 68 | }; 69 | 70 | module.exports = NotificationContainer; 71 | -------------------------------------------------------------------------------- /src/NotificationItem.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var PropTypes = require('prop-types'); 3 | var ReactDOM = require('react-dom'); 4 | var Constants = require('./constants'); 5 | var Helpers = require('./helpers'); 6 | var merge = require('object-assign'); 7 | 8 | /* From Modernizr */ 9 | var whichTransitionEvent = function() { 10 | var el = document.createElement('fakeelement'); 11 | var transition; 12 | var transitions = { 13 | transition: 'transitionend', 14 | OTransition: 'oTransitionEnd', 15 | MozTransition: 'transitionend', 16 | WebkitTransition: 'webkitTransitionEnd' 17 | }; 18 | 19 | Object.keys(transitions).forEach(function(transitionKey) { 20 | if (el.style[transitionKey] !== undefined) { 21 | transition = transitions[transitionKey]; 22 | } 23 | }); 24 | 25 | return transition; 26 | }; 27 | 28 | function _allowHTML(string) { 29 | return { __html: string }; 30 | } 31 | 32 | class NotificationItem extends React.Component { 33 | constructor(props) { 34 | super(props); 35 | this._notificationTimer = null; 36 | this._height = 0; 37 | this._noAnimation = null; 38 | this._isMounted = false; 39 | this._removeCount = 0; 40 | 41 | this.state = { 42 | visible: undefined, 43 | removed: false 44 | }; 45 | 46 | const getStyles = props.getStyles; 47 | const level = props.notification.level; 48 | const dismissible = props.notification.dismissible; 49 | 50 | this._noAnimation = props.noAnimation; 51 | 52 | this._styles = { 53 | notification: getStyles.byElement('notification')(level), 54 | title: getStyles.byElement('title')(level), 55 | dismiss: getStyles.byElement('dismiss')(level), 56 | messageWrapper: getStyles.byElement('messageWrapper')(level), 57 | actionWrapper: getStyles.byElement('actionWrapper')(level), 58 | action: getStyles.byElement('action')(level) 59 | }; 60 | 61 | if (!dismissible || dismissible === 'none' || dismissible === 'button') { 62 | this._styles.notification.cursor = 'default'; 63 | } 64 | 65 | this._getCssPropertyByPosition = this._getCssPropertyByPosition.bind(this); 66 | this._defaultAction = this._defaultAction.bind(this); 67 | this._hideNotification = this._hideNotification.bind(this); 68 | this._removeNotification = this._removeNotification.bind(this); 69 | this._dismiss = this._dismiss.bind(this); 70 | this._showNotification = this._showNotification.bind(this); 71 | this._onTransitionEnd = this._onTransitionEnd.bind(this); 72 | this._handleMouseEnter = this._handleMouseEnter.bind(this); 73 | this._handleMouseLeave = this._handleMouseLeave.bind(this); 74 | this._handleNotificationClick = this._handleNotificationClick.bind(this); 75 | } 76 | 77 | _getCssPropertyByPosition() { 78 | var position = this.props.notification.position; 79 | var css = {}; 80 | 81 | switch (position) { 82 | case Constants.positions.tl: 83 | case Constants.positions.bl: 84 | css = { 85 | property: 'left', 86 | value: -200 87 | }; 88 | break; 89 | 90 | case Constants.positions.tr: 91 | case Constants.positions.br: 92 | css = { 93 | property: 'right', 94 | value: -200 95 | }; 96 | break; 97 | 98 | case Constants.positions.tc: 99 | css = { 100 | property: 'top', 101 | value: -100 102 | }; 103 | break; 104 | 105 | case Constants.positions.bc: 106 | css = { 107 | property: 'bottom', 108 | value: -100 109 | }; 110 | break; 111 | 112 | default: 113 | } 114 | 115 | return css; 116 | } 117 | 118 | _defaultAction(event) { 119 | var notification = this.props.notification; 120 | 121 | event.preventDefault(); 122 | this._hideNotification(); 123 | if (typeof notification.action.callback === 'function') { 124 | notification.action.callback(); 125 | } 126 | } 127 | 128 | _hideNotification() { 129 | if (this._notificationTimer) { 130 | this._notificationTimer.clear(); 131 | } 132 | 133 | if (this._isMounted) { 134 | this.setState({ 135 | visible: false, 136 | removed: true 137 | }); 138 | } 139 | 140 | if (this._noAnimation) { 141 | this._removeNotification(); 142 | } 143 | } 144 | 145 | _removeNotification() { 146 | this.props.onRemove(this.props.notification.uid); 147 | } 148 | 149 | _dismiss() { 150 | if (!this.props.notification.dismissible) { 151 | return; 152 | } 153 | 154 | this._hideNotification(); 155 | } 156 | 157 | _showNotification() { 158 | setTimeout(() => { 159 | if (this._isMounted) { 160 | this.setState({ 161 | visible: true 162 | }); 163 | } 164 | }, 50); 165 | } 166 | 167 | _onTransitionEnd() { 168 | if (this._removeCount > 0) return; 169 | if (this.state.removed) { 170 | this._removeCount += 1; 171 | this._removeNotification(); 172 | } 173 | } 174 | 175 | componentDidMount() { 176 | var self = this; 177 | var transitionEvent = whichTransitionEvent(); 178 | var notification = this.props.notification; 179 | var element = ReactDOM.findDOMNode(this); 180 | 181 | this._height = element.offsetHeight; 182 | 183 | this._isMounted = true; 184 | 185 | // Watch for transition end 186 | if (!this._noAnimation) { 187 | if (transitionEvent) { 188 | element.addEventListener(transitionEvent, this._onTransitionEnd); 189 | } else { 190 | this._noAnimation = true; 191 | } 192 | } 193 | 194 | if (notification.autoDismiss) { 195 | this._notificationTimer = new Helpers.Timer(function() { 196 | self._hideNotification(); 197 | }, notification.autoDismiss * 1000); 198 | } 199 | 200 | this._showNotification(); 201 | } 202 | 203 | _handleMouseEnter() { 204 | var notification = this.props.notification; 205 | if (notification.autoDismiss) { 206 | this._notificationTimer.pause(); 207 | } 208 | } 209 | 210 | _handleMouseLeave() { 211 | var notification = this.props.notification; 212 | if (notification.autoDismiss) { 213 | this._notificationTimer.resume(); 214 | } 215 | } 216 | 217 | _handleNotificationClick() { 218 | var dismissible = this.props.notification.dismissible; 219 | if ( 220 | dismissible === 'both' || 221 | dismissible === 'click' || 222 | dismissible === true 223 | ) { 224 | this._dismiss(); 225 | } 226 | } 227 | 228 | componentWillUnmount() { 229 | var element = ReactDOM.findDOMNode(this); 230 | var transitionEvent = whichTransitionEvent(); 231 | element.removeEventListener(transitionEvent, this._onTransitionEnd); 232 | this._isMounted = false; 233 | } 234 | 235 | render() { 236 | var notification = this.props.notification; 237 | var className = 'notification notification-' + notification.level; 238 | var notificationStyle = merge({}, this._styles.notification); 239 | var cssByPos = this._getCssPropertyByPosition(); 240 | var dismiss = null; 241 | var actionButton = null; 242 | var title = null; 243 | var message = null; 244 | 245 | if (this.props.notification.className) { 246 | className += ' ' + this.props.notification.className; 247 | } 248 | 249 | if (this.state.visible) { 250 | className += ' notification-visible'; 251 | } else if (this.state.visible === false) { 252 | className += ' notification-hidden'; 253 | } 254 | 255 | if (notification.dismissible === 'none') { 256 | className += ' notification-not-dismissible'; 257 | } 258 | 259 | if (this.props.getStyles.overrideStyle) { 260 | if (!this.state.visible && !this.state.removed) { 261 | notificationStyle[cssByPos.property] = cssByPos.value; 262 | } 263 | 264 | if (this.state.visible && !this.state.removed) { 265 | notificationStyle.height = this._height; 266 | notificationStyle[cssByPos.property] = 0; 267 | } 268 | 269 | if (this.state.removed) { 270 | notificationStyle.overlay = 'hidden'; 271 | notificationStyle.height = 0; 272 | notificationStyle.marginTop = 0; 273 | notificationStyle.paddingTop = 0; 274 | notificationStyle.paddingBottom = 0; 275 | } 276 | 277 | if (this._styles.notification.isVisible && this._styles.notification.isHidden) { 278 | notificationStyle.opacity = this.state.visible 279 | ? this._styles.notification.isVisible.opacity 280 | : this._styles.notification.isHidden.opacity; 281 | } 282 | } 283 | 284 | if (notification.title) { 285 | title = ( 286 |

287 | {notification.title} 288 |

289 | ); 290 | } 291 | 292 | if (notification.message) { 293 | if (this.props.allowHTML) { 294 | message = ( 295 |
300 | ); 301 | } else { 302 | message = ( 303 |
307 | {notification.message} 308 |
309 | ); 310 | } 311 | } 312 | 313 | if ( 314 | notification.dismissible === 'both' || 315 | notification.dismissible === 'button' || 316 | notification.dismissible === true 317 | ) { 318 | dismiss = ( 319 | 325 | × 326 | 327 | ); 328 | } 329 | 330 | if (notification.action) { 331 | actionButton = ( 332 |
336 | 343 |
344 | ); 345 | } 346 | 347 | if (notification.children) { 348 | actionButton = notification.children; 349 | } 350 | 351 | return ( 352 |
360 | {title} 361 | {message} 362 | {dismiss} 363 | {actionButton} 364 |
365 | ); 366 | } 367 | } 368 | 369 | NotificationItem.propTypes = { 370 | notification: PropTypes.object, 371 | getStyles: PropTypes.object, 372 | onRemove: PropTypes.func, 373 | allowHTML: PropTypes.bool, 374 | noAnimation: PropTypes.bool, 375 | children: PropTypes.oneOfType([PropTypes.string, PropTypes.element]) 376 | }; 377 | 378 | NotificationItem.defaultProps = { 379 | noAnimation: false, 380 | onRemove: function() {}, 381 | allowHTML: false 382 | }; 383 | 384 | module.exports = NotificationItem; 385 | -------------------------------------------------------------------------------- /src/NotificationSystem.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var PropTypes = require('prop-types'); 3 | var merge = require('object-assign'); 4 | var NotificationContainer = require('./NotificationContainer'); 5 | var Constants = require('./constants'); 6 | var Styles = require('./styles'); 7 | 8 | class NotificationSystem extends React.Component { 9 | constructor() { 10 | super(); 11 | this.state = { 12 | notifications: [] 13 | }; 14 | this.uid = 3400; 15 | this._isMounted = false; 16 | this.overrideWidth = null; 17 | this.overrideStyle = {}; 18 | this.elements = { 19 | notification: 'NotificationItem', 20 | title: 'Title', 21 | messageWrapper: 'MessageWrapper', 22 | dismiss: 'Dismiss', 23 | action: 'Action', 24 | actionWrapper: 'ActionWrapper' 25 | }; 26 | 27 | this.setOverrideStyle = this.setOverrideStyle.bind(this); 28 | this.wrapper = this.wrapper.bind(this); 29 | this.container = this.container.bind(this); 30 | this.byElement = this.byElement.bind(this); 31 | this._didNotificationRemoved = this._didNotificationRemoved.bind(this); 32 | this.addNotification = this.addNotification.bind(this); 33 | this.getNotificationRef = this.getNotificationRef.bind(this); 34 | this.removeNotification = this.removeNotification.bind(this); 35 | this.editNotification = this.editNotification.bind(this); 36 | this.clearNotifications = this.clearNotifications.bind(this); 37 | 38 | this._getStyles = { 39 | overrideWidth: this.overrideWidth, 40 | overrideStyle: this.overrideStyle, 41 | elements: this.elements, 42 | setOverrideStyle: this.setOverrideStyle, 43 | wrapper: this.wrapper, 44 | container: this.container, 45 | byElement: this.byElement 46 | }; 47 | } 48 | 49 | componentDidMount() { 50 | this.setOverrideStyle(this.props.style); 51 | this._isMounted = true; 52 | } 53 | 54 | componentWillUnmount() { 55 | this._isMounted = false; 56 | } 57 | 58 | setOverrideStyle(style) { 59 | this.overrideStyle = style; 60 | } 61 | 62 | wrapper() { 63 | if (!this.overrideStyle) return {}; 64 | return merge({}, Styles.Wrapper, this.overrideStyle.Wrapper); 65 | } 66 | 67 | container(position) { 68 | var override = this.overrideStyle.Containers || {}; 69 | if (!this.overrideStyle) return {}; 70 | 71 | this.overrideWidth = Styles.Containers.DefaultStyle.width; 72 | 73 | if (override.DefaultStyle && override.DefaultStyle.width) { 74 | this.overrideWidth = override.DefaultStyle.width; 75 | } 76 | 77 | if (override[position] && override[position].width) { 78 | this.overrideWidth = override[position].width; 79 | } 80 | 81 | return merge( 82 | {}, 83 | Styles.Containers.DefaultStyle, 84 | Styles.Containers[position], 85 | override.DefaultStyle, 86 | override[position] 87 | ); 88 | } 89 | 90 | byElement(element) { 91 | return (level) => { 92 | var _element = this.elements[element]; 93 | var override = this.overrideStyle[_element] || {}; 94 | if (!this.overrideStyle) return {}; 95 | return merge( 96 | {}, 97 | Styles[_element].DefaultStyle, 98 | Styles[_element][level], 99 | override.DefaultStyle, 100 | override[level] 101 | ); 102 | }; 103 | } 104 | 105 | _didNotificationRemoved(uid) { 106 | var notification; 107 | var notifications = this.state.notifications.filter(function(toCheck) { 108 | if (toCheck.uid === uid) { 109 | notification = toCheck; 110 | return false; 111 | } 112 | return true; 113 | }); 114 | 115 | if (this._isMounted) { 116 | this.setState({ notifications: notifications }); 117 | } 118 | 119 | if (notification && notification.onRemove) { 120 | notification.onRemove(notification); 121 | } 122 | } 123 | 124 | addNotification(notification) { 125 | var _notification = merge({}, Constants.notification, notification); 126 | var notifications = this.state.notifications; 127 | var i; 128 | 129 | 130 | if (!_notification.level) { 131 | throw new Error('notification level is required.'); 132 | } 133 | 134 | if (Object.keys(Constants.levels).indexOf(_notification.level) === -1) { 135 | throw new Error("'" + _notification.level + "' is not a valid level."); 136 | } 137 | 138 | // eslint-disable-next-line 139 | if (isNaN(_notification.autoDismiss)) { 140 | throw new Error("'autoDismiss' must be a number."); 141 | } 142 | 143 | if ( 144 | Object.keys(Constants.positions).indexOf(_notification.position) === -1 145 | ) { 146 | throw new Error("'" + _notification.position + "' is not a valid position."); 147 | } 148 | 149 | // Some preparations 150 | _notification.position = _notification.position.toLowerCase(); 151 | _notification.level = _notification.level.toLowerCase(); 152 | _notification.autoDismiss = parseInt(_notification.autoDismiss, 10); 153 | 154 | _notification.uid = _notification.uid || this.uid; 155 | _notification.ref = 'notification-' + _notification.uid; 156 | this.uid += 1; 157 | 158 | 159 | // do not add if the notification already exists based on supplied uid 160 | for (i = 0; i < notifications.length; i += 1) { 161 | if (notifications[i].uid === _notification.uid) { 162 | return false; 163 | } 164 | } 165 | 166 | if (this.props.newOnTop) { 167 | notifications.unshift(_notification); 168 | } else { 169 | notifications.push(_notification); 170 | } 171 | 172 | 173 | if (typeof _notification.onAdd === 'function') { 174 | notification.onAdd(_notification); 175 | } 176 | 177 | this.setState({ 178 | notifications: notifications 179 | }); 180 | 181 | return _notification; 182 | } 183 | 184 | getNotificationRef(notification) { 185 | var foundNotification = null; 186 | 187 | Object.keys(this.refs).forEach((container) => { 188 | if (container.indexOf('container') > -1) { 189 | Object.keys(this.refs[container].refs).forEach((_notification) => { 190 | var uid = notification.uid ? notification.uid : notification; 191 | if (_notification === 'notification-' + uid) { 192 | // NOTE: Stop iterating further and return the found notification. 193 | // Since UIDs are uniques and there won't be another notification found. 194 | foundNotification = this.refs[container].refs[_notification]; 195 | } 196 | }); 197 | } 198 | }); 199 | 200 | return foundNotification; 201 | } 202 | 203 | removeNotification(notification) { 204 | var foundNotification = this.getNotificationRef(notification); 205 | return foundNotification && foundNotification._hideNotification(); 206 | } 207 | 208 | editNotification(notification, newNotification) { 209 | var foundNotification = null; 210 | // NOTE: Find state notification to update by using 211 | // `setState` and forcing React to re-render the component. 212 | var uid = notification.uid ? notification.uid : notification; 213 | 214 | var newNotifications = this.state.notifications.filter(function(stateNotification) { 215 | if (uid === stateNotification.uid) { 216 | foundNotification = stateNotification; 217 | return false; 218 | } 219 | 220 | return true; 221 | }); 222 | 223 | if (!foundNotification) { 224 | return; 225 | } 226 | 227 | newNotifications.push(merge({}, foundNotification, newNotification)); 228 | 229 | this.setState({ 230 | notifications: newNotifications 231 | }); 232 | } 233 | 234 | clearNotifications() { 235 | Object.keys(this.refs).forEach((container) => { 236 | if (container.indexOf('container') > -1) { 237 | Object.keys(this.refs[container].refs).forEach((_notification) => { 238 | this.refs[container].refs[_notification]._hideNotification(); 239 | }); 240 | } 241 | }); 242 | } 243 | 244 | render() { 245 | var containers = null; 246 | var notifications = this.state.notifications; 247 | 248 | if (notifications.length) { 249 | containers = Object.keys(Constants.positions).map((position) => { 250 | var _notifications = notifications.filter((notification) => { 251 | return position === notification.position; 252 | }); 253 | 254 | if (!_notifications.length) { 255 | return null; 256 | } 257 | 258 | return ( 259 | 269 | ); 270 | }); 271 | } 272 | 273 | return ( 274 |
275 | {containers} 276 |
277 | ); 278 | } 279 | } 280 | 281 | NotificationSystem.propTypes = { 282 | style: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]), 283 | noAnimation: PropTypes.bool, 284 | allowHTML: PropTypes.bool, 285 | newOnTop: PropTypes.bool 286 | }; 287 | 288 | NotificationSystem.defaultProps = { 289 | style: {}, 290 | noAnimation: false, 291 | allowHTML: false, 292 | newOnTop: false 293 | }; 294 | 295 | module.exports = NotificationSystem; 296 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | var CONSTANTS = { 2 | 3 | // Positions 4 | positions: { 5 | tl: 'tl', 6 | tr: 'tr', 7 | tc: 'tc', 8 | bl: 'bl', 9 | br: 'br', 10 | bc: 'bc' 11 | }, 12 | 13 | // Levels 14 | levels: { 15 | success: 'success', 16 | error: 'error', 17 | warning: 'warning', 18 | info: 'info' 19 | }, 20 | 21 | // Notification defaults 22 | notification: { 23 | title: null, 24 | message: null, 25 | level: null, 26 | position: 'tr', 27 | autoDismiss: 5, 28 | dismissible: 'both', 29 | action: null 30 | } 31 | }; 32 | 33 | 34 | module.exports = CONSTANTS; 35 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | var Helpers = { 2 | Timer: function(callback, delay) { 3 | var timerId; 4 | var start; 5 | var remaining = delay; 6 | 7 | this.pause = function() { 8 | clearTimeout(timerId); 9 | remaining -= new Date() - start; 10 | }; 11 | 12 | this.resume = function() { 13 | start = new Date(); 14 | clearTimeout(timerId); 15 | timerId = setTimeout(callback, remaining); 16 | }; 17 | 18 | this.clear = function() { 19 | clearTimeout(timerId); 20 | }; 21 | 22 | this.resume(); 23 | } 24 | }; 25 | 26 | module.exports = Helpers; 27 | -------------------------------------------------------------------------------- /src/styles.js: -------------------------------------------------------------------------------- 1 | // Used for calculations 2 | var defaultWidth = 320; 3 | var defaultColors = { 4 | success: { 5 | rgb: '94, 164, 0', 6 | hex: '#5ea400' 7 | }, 8 | error: { 9 | rgb: '236, 61, 61', 10 | hex: '#ec3d3d' 11 | }, 12 | warning: { 13 | rgb: '235, 173, 23', 14 | hex: '#ebad1a' 15 | }, 16 | info: { 17 | rgb: '54, 156, 199', 18 | hex: '#369cc7' 19 | } 20 | }; 21 | var defaultShadowOpacity = '0.9'; 22 | 23 | var STYLES = { 24 | 25 | Wrapper: {}, 26 | Containers: { 27 | DefaultStyle: { 28 | fontFamily: 'inherit', 29 | position: 'fixed', 30 | width: defaultWidth, 31 | padding: '0 10px 10px 10px', 32 | zIndex: 9998, 33 | WebkitBoxSizing: 'border-box', 34 | MozBoxSizing: 'border-box', 35 | boxSizing: 'border-box', 36 | height: 'auto' 37 | }, 38 | 39 | tl: { 40 | top: '0px', 41 | bottom: 'auto', 42 | left: '0px', 43 | right: 'auto' 44 | }, 45 | 46 | tr: { 47 | top: '0px', 48 | bottom: 'auto', 49 | left: 'auto', 50 | right: '0px' 51 | }, 52 | 53 | tc: { 54 | top: '0px', 55 | bottom: 'auto', 56 | margin: '0 auto', 57 | left: '50%', 58 | marginLeft: -(defaultWidth / 2) 59 | }, 60 | 61 | bl: { 62 | top: 'auto', 63 | bottom: '0px', 64 | left: '0px', 65 | right: 'auto' 66 | }, 67 | 68 | br: { 69 | top: 'auto', 70 | bottom: '0px', 71 | left: 'auto', 72 | right: '0px' 73 | }, 74 | 75 | bc: { 76 | top: 'auto', 77 | bottom: '0px', 78 | margin: '0 auto', 79 | left: '50%', 80 | marginLeft: -(defaultWidth / 2) 81 | } 82 | 83 | }, 84 | 85 | NotificationItem: { 86 | DefaultStyle: { 87 | position: 'relative', 88 | width: '100%', 89 | cursor: 'pointer', 90 | borderRadius: '2px', 91 | fontSize: '13px', 92 | margin: '10px 0 0', 93 | padding: '10px', 94 | display: 'block', 95 | WebkitBoxSizing: 'border-box', 96 | MozBoxSizing: 'border-box', 97 | boxSizing: 'border-box', 98 | opacity: 0, 99 | transition: '0.3s ease-in-out', 100 | WebkitTransform: 'translate3d(0, 0, 0)', 101 | transform: 'translate3d(0, 0, 0)', 102 | willChange: 'transform, opacity', 103 | 104 | isHidden: { 105 | opacity: 0 106 | }, 107 | 108 | isVisible: { 109 | opacity: 1 110 | } 111 | }, 112 | 113 | success: { 114 | borderTop: '2px solid ' + defaultColors.success.hex, 115 | backgroundColor: '#f0f5ea', 116 | color: '#4b583a', 117 | WebkitBoxShadow: '0 0 1px rgba(' + defaultColors.success.rgb + ',' + defaultShadowOpacity + ')', 118 | MozBoxShadow: '0 0 1px rgba(' + defaultColors.success.rgb + ',' + defaultShadowOpacity + ')', 119 | boxShadow: '0 0 1px rgba(' + defaultColors.success.rgb + ',' + defaultShadowOpacity + ')' 120 | }, 121 | 122 | error: { 123 | borderTop: '2px solid ' + defaultColors.error.hex, 124 | backgroundColor: '#f4e9e9', 125 | color: '#412f2f', 126 | WebkitBoxShadow: '0 0 1px rgba(' + defaultColors.error.rgb + ',' + defaultShadowOpacity + ')', 127 | MozBoxShadow: '0 0 1px rgba(' + defaultColors.error.rgb + ',' + defaultShadowOpacity + ')', 128 | boxShadow: '0 0 1px rgba(' + defaultColors.error.rgb + ',' + defaultShadowOpacity + ')' 129 | }, 130 | 131 | warning: { 132 | borderTop: '2px solid ' + defaultColors.warning.hex, 133 | backgroundColor: '#f9f6f0', 134 | color: '#5a5343', 135 | WebkitBoxShadow: '0 0 1px rgba(' + defaultColors.warning.rgb + ',' + defaultShadowOpacity + ')', 136 | MozBoxShadow: '0 0 1px rgba(' + defaultColors.warning.rgb + ',' + defaultShadowOpacity + ')', 137 | boxShadow: '0 0 1px rgba(' + defaultColors.warning.rgb + ',' + defaultShadowOpacity + ')' 138 | }, 139 | 140 | info: { 141 | borderTop: '2px solid ' + defaultColors.info.hex, 142 | backgroundColor: '#e8f0f4', 143 | color: '#41555d', 144 | WebkitBoxShadow: '0 0 1px rgba(' + defaultColors.info.rgb + ',' + defaultShadowOpacity + ')', 145 | MozBoxShadow: '0 0 1px rgba(' + defaultColors.info.rgb + ',' + defaultShadowOpacity + ')', 146 | boxShadow: '0 0 1px rgba(' + defaultColors.info.rgb + ',' + defaultShadowOpacity + ')' 147 | } 148 | }, 149 | 150 | Title: { 151 | DefaultStyle: { 152 | fontSize: '14px', 153 | margin: '0 0 5px 0', 154 | padding: 0, 155 | fontWeight: 'bold' 156 | }, 157 | 158 | success: { 159 | color: defaultColors.success.hex 160 | }, 161 | 162 | error: { 163 | color: defaultColors.error.hex 164 | }, 165 | 166 | warning: { 167 | color: defaultColors.warning.hex 168 | }, 169 | 170 | info: { 171 | color: defaultColors.info.hex 172 | } 173 | 174 | }, 175 | 176 | MessageWrapper: { 177 | DefaultStyle: { 178 | margin: 0, 179 | padding: 0 180 | } 181 | }, 182 | 183 | Dismiss: { 184 | DefaultStyle: { 185 | cursor: 'pointer', 186 | fontFamily: 'Arial', 187 | fontSize: '17px', 188 | position: 'absolute', 189 | top: '4px', 190 | right: '5px', 191 | lineHeight: '15px', 192 | backgroundColor: '#dededf', 193 | color: '#ffffff', 194 | borderRadius: '50%', 195 | width: '14px', 196 | height: '14px', 197 | fontWeight: 'bold', 198 | textAlign: 'center' 199 | }, 200 | 201 | success: { 202 | color: '#f0f5ea', 203 | backgroundColor: '#b0ca92' 204 | }, 205 | 206 | error: { 207 | color: '#f4e9e9', 208 | backgroundColor: '#e4bebe' 209 | }, 210 | 211 | warning: { 212 | color: '#f9f6f0', 213 | backgroundColor: '#e1cfac' 214 | }, 215 | 216 | info: { 217 | color: '#e8f0f4', 218 | backgroundColor: '#a4becb' 219 | } 220 | }, 221 | 222 | Action: { 223 | DefaultStyle: { 224 | background: '#ffffff', 225 | borderRadius: '2px', 226 | padding: '6px 20px', 227 | fontWeight: 'bold', 228 | margin: '10px 0 0 0', 229 | border: 0 230 | }, 231 | 232 | success: { 233 | backgroundColor: defaultColors.success.hex, 234 | color: '#ffffff' 235 | }, 236 | 237 | error: { 238 | backgroundColor: defaultColors.error.hex, 239 | color: '#ffffff' 240 | }, 241 | 242 | warning: { 243 | backgroundColor: defaultColors.warning.hex, 244 | color: '#ffffff' 245 | }, 246 | 247 | info: { 248 | backgroundColor: defaultColors.info.hex, 249 | color: '#ffffff' 250 | } 251 | }, 252 | 253 | ActionWrapper: { 254 | DefaultStyle: { 255 | margin: 0, 256 | padding: 0 257 | } 258 | } 259 | }; 260 | 261 | module.exports = STYLES; 262 | -------------------------------------------------------------------------------- /test/notification-system.test.js: -------------------------------------------------------------------------------- 1 | /* global sinon */ 2 | 3 | import React, { Component } from 'react'; 4 | import TestUtils from 'react-dom/test-utils'; 5 | import { expect } from 'chai'; 6 | import NotificationSystem from 'NotificationSystem'; 7 | import { positions, levels } from 'constants'; 8 | import merge from 'object-assign'; 9 | 10 | const defaultNotification = { 11 | title: 'This is a title', 12 | message: 'This is a message', 13 | level: 'success' 14 | }; 15 | 16 | const style = { 17 | Containers: { 18 | DefaultStyle: { 19 | width: 600 20 | }, 21 | 22 | tl: { 23 | width: 800 24 | } 25 | } 26 | }; 27 | 28 | describe('Notification Component', function() { 29 | let node; 30 | let instance; 31 | let component; 32 | let clock; 33 | let notificationObj; 34 | const ref = 'notificationSystem'; 35 | 36 | this.timeout(10000); 37 | 38 | beforeEach(() => { 39 | // We need to create this wrapper so we can use refs 40 | class ElementWrapper extends Component { 41 | render() { 42 | return ; 43 | } 44 | } 45 | node = window.document.createElement('div'); 46 | instance = TestUtils.renderIntoDocument(React.createElement(ElementWrapper), node); 47 | component = instance.refs[ref]; 48 | notificationObj = merge({}, defaultNotification); 49 | 50 | clock = sinon.useFakeTimers(); 51 | }); 52 | 53 | afterEach(() => { 54 | clock.restore(); 55 | }); 56 | 57 | it('should be rendered', done => { 58 | component = TestUtils.findRenderedDOMComponentWithClass(instance, 'notifications-wrapper'); 59 | expect(component).to.not.be.null; 60 | done(); 61 | }); 62 | 63 | it('should hold the component ref', done => { 64 | expect(component).to.not.be.null; 65 | done(); 66 | }); 67 | 68 | it('should render a single notification', done => { 69 | component.addNotification(defaultNotification); 70 | let notification = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'notification'); 71 | expect(notification.length).to.equal(1); 72 | done(); 73 | }); 74 | 75 | it('should not set a notification visibility class when the notification is initially added', done => { 76 | component.addNotification(defaultNotification); 77 | let notification = TestUtils.findRenderedDOMComponentWithClass(instance, 'notification'); 78 | expect(notification.className).to.not.match(/notification-hidden/); 79 | expect(notification.className).to.not.match(/notification-visible/); 80 | done(); 81 | }); 82 | 83 | it('should set the notification class to visible after added', done => { 84 | component.addNotification(defaultNotification); 85 | let notification = TestUtils.findRenderedDOMComponentWithClass(instance, 'notification'); 86 | expect(notification.className).to.match(/notification/); 87 | clock.tick(400); 88 | expect(notification.className).to.match(/notification-visible/); 89 | done(); 90 | }); 91 | 92 | it('should add additional classes to the notification if specified', done => { 93 | component.addNotification(Object.assign({},defaultNotification, {className: 'FOO'})); 94 | let notification = TestUtils.findRenderedDOMComponentWithClass(instance, 'notification'); 95 | expect(notification.className).to.contain(' FOO'); 96 | done(); 97 | }); 98 | 99 | it('should render notifications in all positions with all levels', done => { 100 | let count = 0; 101 | for (let position of Object.keys(positions)) { 102 | for (let level of Object.keys(levels)) { 103 | notificationObj.position = positions[position]; 104 | notificationObj.level = levels[level]; 105 | component.addNotification(notificationObj); 106 | count++; 107 | } 108 | } 109 | 110 | let containers = []; 111 | 112 | for (let position of Object.keys(positions)) { 113 | containers.push(TestUtils.findRenderedDOMComponentWithClass(instance, 'notifications-' + positions[position])); 114 | } 115 | 116 | containers.forEach(function(container) { 117 | for (let level of Object.keys(levels)) { 118 | let notification = container.getElementsByClassName('notification-' + levels[level]); 119 | expect(notification).to.not.be.null; 120 | } 121 | }); 122 | 123 | let notifications = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'notification'); 124 | expect(notifications.length).to.equal(count); 125 | done(); 126 | }); 127 | 128 | it('should render multiple notifications', done => { 129 | const randomNumber = Math.floor(Math.random(5, 10)); 130 | 131 | for (let i = 1; i <= randomNumber; i++) { 132 | component.addNotification(defaultNotification); 133 | } 134 | 135 | let notifications = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'notification'); 136 | expect(notifications.length).to.equal(randomNumber); 137 | done(); 138 | }); 139 | 140 | it('should not render notifications with the same uid', done => { 141 | notificationObj.uid = 500; 142 | component.addNotification(notificationObj); 143 | component.addNotification(notificationObj); 144 | let notification = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'notification'); 145 | expect(notification.length).to.equal(1); 146 | done(); 147 | }); 148 | 149 | it('should remove a notification after autoDismiss', function(done) { 150 | notificationObj.autoDismiss = 2; 151 | component.addNotification(notificationObj); 152 | clock.tick(3000); 153 | let notification = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'notification'); 154 | expect(notification.length).to.equal(0); 155 | done(); 156 | }); 157 | 158 | it('should remove a notification using returned object', done => { 159 | let notificationCreated = component.addNotification(defaultNotification); 160 | let notification = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'notification'); 161 | expect(notification.length).to.equal(1); 162 | 163 | component.removeNotification(notificationCreated); 164 | clock.tick(1000); 165 | let notificationRemoved = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'notification'); 166 | expect(notificationRemoved.length).to.equal(0); 167 | done(); 168 | }); 169 | 170 | it('should remove a notification using uid', done => { 171 | let notificationCreated = component.addNotification(defaultNotification); 172 | let notification = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'notification'); 173 | expect(notification.length).to.equal(1); 174 | 175 | component.removeNotification(notificationCreated.uid); 176 | clock.tick(200); 177 | let notificationRemoved = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'notification'); 178 | expect(notificationRemoved.length).to.equal(0); 179 | done(); 180 | }); 181 | 182 | it('should edit an existing notification using returned object', (done) => { 183 | const notificationCreated = component.addNotification(defaultNotification); 184 | const notification = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'notification'); 185 | expect(notification.length).to.equal(1); 186 | 187 | const newTitle = 'foo'; 188 | const newContent = 'foobar'; 189 | 190 | component.editNotification(notificationCreated, { title: newTitle, message: newContent }); 191 | clock.tick(1000); 192 | const notificationEdited = TestUtils.findRenderedDOMComponentWithClass(instance, 'notification'); 193 | expect(notificationEdited.getElementsByClassName('notification-title')[0].textContent).to.equal(newTitle); 194 | expect(notificationEdited.getElementsByClassName('notification-message')[0].textContent).to.equal(newContent); 195 | done(); 196 | }); 197 | 198 | it('should edit an existing notification using uid', (done) => { 199 | const notificationCreated = component.addNotification(defaultNotification); 200 | const notification = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'notification'); 201 | expect(notification.length).to.equal(1); 202 | 203 | const newTitle = 'foo'; 204 | const newContent = 'foobar'; 205 | 206 | component.editNotification(notificationCreated.uid, { title: newTitle, message: newContent }); 207 | clock.tick(1000); 208 | const notificationEdited = TestUtils.findRenderedDOMComponentWithClass(instance, 'notification'); 209 | expect(notificationEdited.getElementsByClassName('notification-title')[0].textContent).to.equal(newTitle); 210 | expect(notificationEdited.getElementsByClassName('notification-message')[0].textContent).to.equal(newContent); 211 | done(); 212 | }); 213 | 214 | it('should remove all notifications', done => { 215 | component.addNotification(defaultNotification); 216 | component.addNotification(defaultNotification); 217 | component.addNotification(defaultNotification); 218 | let notification = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'notification'); 219 | expect(notification.length).to.equal(3); 220 | component.clearNotifications(); 221 | clock.tick(200); 222 | let notificationRemoved = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'notification'); 223 | expect(notificationRemoved.length).to.equal(0); 224 | done(); 225 | }); 226 | 227 | it('should dismiss notification on click', done => { 228 | component.addNotification(notificationObj); 229 | let notification = TestUtils.findRenderedDOMComponentWithClass(instance, 'notification'); 230 | TestUtils.Simulate.click(notification); 231 | clock.tick(1000); 232 | let notificationRemoved = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'notification'); 233 | expect(notificationRemoved.length).to.equal(0); 234 | done(); 235 | }); 236 | 237 | it('should dismiss notification on click of dismiss button', done => { 238 | component.addNotification(notificationObj); 239 | let dismissButton = TestUtils.findRenderedDOMComponentWithClass(instance, 'notification-dismiss'); 240 | TestUtils.Simulate.click(dismissButton); 241 | clock.tick(1000); 242 | let notificationRemoved = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'notification'); 243 | expect(notificationRemoved.length).to.equal(0); 244 | done(); 245 | }); 246 | 247 | it('should not render title if not provided', done => { 248 | delete notificationObj.title; 249 | component.addNotification(notificationObj); 250 | let notification = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'notification-title'); 251 | expect(notification.length).to.equal(0); 252 | done(); 253 | }); 254 | 255 | it('should not render message if not provided', done => { 256 | delete notificationObj.message; 257 | component.addNotification(notificationObj); 258 | let notification = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'notification-message'); 259 | expect(notification.length).to.equal(0); 260 | done(); 261 | }); 262 | 263 | it('should not dismiss the notificaion on click if dismissible is false', done => { 264 | notificationObj.dismissible = false; 265 | component.addNotification(notificationObj); 266 | let notification = TestUtils.findRenderedDOMComponentWithClass(instance, 'notification'); 267 | TestUtils.Simulate.click(notification); 268 | let notificationAfterClicked = TestUtils.findRenderedDOMComponentWithClass(instance, 'notification'); 269 | expect(notificationAfterClicked).to.not.be.null; 270 | done(); 271 | }); 272 | 273 | it('should not dismiss the notification on click if dismissible is none', done => { 274 | notificationObj.dismissible = 'none'; 275 | component.addNotification(notificationObj); 276 | let notification = TestUtils.findRenderedDOMComponentWithClass(instance, 'notification'); 277 | TestUtils.Simulate.click(notification); 278 | let notificationAfterClicked = TestUtils.findRenderedDOMComponentWithClass(instance, 'notification'); 279 | expect(notificationAfterClicked).to.exist; 280 | done(); 281 | }); 282 | 283 | it('should not dismiss the notification on click if dismissible is button', done => { 284 | notificationObj.dismissible = 'button'; 285 | component.addNotification(notificationObj); 286 | let notification = TestUtils.findRenderedDOMComponentWithClass(instance, 'notification'); 287 | TestUtils.Simulate.click(notification); 288 | let notificationAfterClicked = TestUtils.findRenderedDOMComponentWithClass(instance, 'notification'); 289 | expect(notificationAfterClicked).to.exist; 290 | done(); 291 | }); 292 | 293 | it('should render a button if action property is passed', done => { 294 | defaultNotification.action = { 295 | label: 'Click me', 296 | callback: function() {} 297 | }; 298 | 299 | component.addNotification(defaultNotification); 300 | let button = TestUtils.findRenderedDOMComponentWithClass(instance, 'notification-action-button'); 301 | expect(button).to.not.be.null; 302 | done(); 303 | }); 304 | 305 | it('should execute a callback function when notification button is clicked', done => { 306 | let testThis = false; 307 | notificationObj.action = { 308 | label: 'Click me', 309 | callback: function() { 310 | testThis = true; 311 | } 312 | }; 313 | 314 | component.addNotification(notificationObj); 315 | let button = TestUtils.findRenderedDOMComponentWithClass(instance, 'notification-action-button'); 316 | TestUtils.Simulate.click(button); 317 | expect(testThis).to.equal(true); 318 | done(); 319 | }); 320 | 321 | it('should accept an action without callback function defined', done => { 322 | notificationObj.action = { 323 | label: 'Click me' 324 | }; 325 | 326 | component.addNotification(notificationObj); 327 | let button = TestUtils.findRenderedDOMComponentWithClass(instance, 'notification-action-button'); 328 | TestUtils.Simulate.click(button); 329 | let notification = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'notification'); 330 | expect(notification.length).to.equal(0); 331 | done(); 332 | }); 333 | 334 | it('should execute a callback function on add a notification', done => { 335 | let testThis = false; 336 | notificationObj.onAdd = function() { 337 | testThis = true; 338 | }; 339 | 340 | component.addNotification(notificationObj); 341 | expect(testThis).to.equal(true); 342 | done(); 343 | }); 344 | 345 | it('should execute a callback function on remove a notification', done => { 346 | let testThis = false; 347 | notificationObj.onRemove = function() { 348 | testThis = true; 349 | }; 350 | 351 | component.addNotification(notificationObj); 352 | let notification = TestUtils.findRenderedDOMComponentWithClass(instance, 'notification'); 353 | TestUtils.Simulate.click(notification); 354 | expect(testThis).to.equal(true); 355 | done(); 356 | }); 357 | 358 | it('should render a children if passed', done => { 359 | defaultNotification.children = ( 360 |
361 | ); 362 | 363 | component.addNotification(defaultNotification); 364 | let customContainer = TestUtils.findRenderedDOMComponentWithClass(instance, 'custom-container'); 365 | expect(customContainer).to.not.be.null; 366 | done(); 367 | }); 368 | 369 | it('should pause the timer if a notification has a mouse enter', done => { 370 | notificationObj.autoDismiss = 2; 371 | component.addNotification(notificationObj); 372 | let notification = TestUtils.findRenderedDOMComponentWithClass(instance, 'notification'); 373 | TestUtils.Simulate.mouseEnter(notification); 374 | clock.tick(4000); 375 | let _notification = TestUtils.findRenderedDOMComponentWithClass(instance, 'notification'); 376 | expect(_notification).to.not.be.null; 377 | done(); 378 | }); 379 | 380 | it('should resume the timer if a notification has a mouse leave', done => { 381 | notificationObj.autoDismiss = 2; 382 | component.addNotification(notificationObj); 383 | let notification = TestUtils.findRenderedDOMComponentWithClass(instance, 'notification'); 384 | TestUtils.Simulate.mouseEnter(notification); 385 | clock.tick(800); 386 | TestUtils.Simulate.mouseLeave(notification); 387 | clock.tick(2000); 388 | let _notification = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'notification'); 389 | expect(_notification.length).to.equal(0); 390 | done(); 391 | }); 392 | 393 | it('should allow HTML inside messages', done => { 394 | defaultNotification.message = 'Strong'; 395 | component.addNotification(defaultNotification); 396 | let notification = TestUtils.findRenderedDOMComponentWithClass(instance, 'notification-message'); 397 | let htmlElement = notification.getElementsByClassName('allow-html-strong'); 398 | expect(htmlElement.length).to.equal(1); 399 | done(); 400 | }); 401 | 402 | it('should render containers with a overriden width', done => { 403 | notificationObj.position = 'tc'; 404 | component.addNotification(notificationObj); 405 | let notification = TestUtils.findRenderedDOMComponentWithClass(instance, 'notifications-tc'); 406 | let width = notification.style.width; 407 | expect(width).to.equal('600px'); 408 | done(); 409 | }); 410 | 411 | it('should render a notification with specific style based on position', done => { 412 | notificationObj.position = 'bc'; 413 | component.addNotification(notificationObj); 414 | let notification = TestUtils.findRenderedDOMComponentWithClass(instance, 'notification'); 415 | let bottomPosition = notification.style.bottom; 416 | expect(bottomPosition).to.equal('-100px'); 417 | done(); 418 | }); 419 | 420 | it('should render containers with a overriden width for a specific position', done => { 421 | notificationObj.position = 'tl'; 422 | component.addNotification(notificationObj); 423 | let notification = TestUtils.findRenderedDOMComponentWithClass(instance, 'notifications-tl'); 424 | let width = notification.style.width; 425 | expect(width).to.equal('800px'); 426 | done(); 427 | }); 428 | 429 | it('should throw an error if no level is defined', done => { 430 | delete notificationObj.level; 431 | expect(() => component.addNotification(notificationObj)).to.throw(/notification level is required/); 432 | done(); 433 | }); 434 | 435 | it('should throw an error if a invalid level is defined', done => { 436 | notificationObj.level = 'invalid'; 437 | expect(() => component.addNotification(notificationObj)).to.throw(/is not a valid level/); 438 | done(); 439 | }); 440 | 441 | it('should throw an error if a invalid position is defined', done => { 442 | notificationObj.position = 'invalid'; 443 | expect(() => component.addNotification(notificationObj)).to.throw(/is not a valid position/); 444 | done(); 445 | }); 446 | 447 | it('should throw an error if autoDismiss is not a number', done => { 448 | notificationObj.autoDismiss = 'string'; 449 | expect(() => component.addNotification(notificationObj)).to.throw(/\'autoDismiss\' must be a number./); 450 | done(); 451 | }); 452 | 453 | it('should render 2nd notification below 1st one', done => { 454 | component.addNotification(merge({}, defaultNotification, {title: '1st'})); 455 | component.addNotification(merge({}, defaultNotification, {title: '2nd'})); 456 | 457 | const notifications = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'notification'); 458 | expect(notifications[0].getElementsByClassName('notification-title')[0].textContent).to.equal('1st'); 459 | expect(notifications[1].getElementsByClassName('notification-title')[0].textContent).to.equal('2nd'); 460 | done(); 461 | }); 462 | }); 463 | 464 | 465 | describe('Notification Component with newOnTop=true', function() { 466 | let node; 467 | let instance; 468 | let component; 469 | let clock; 470 | let notificationObj; 471 | const ref = 'notificationSystem'; 472 | 473 | this.timeout(10000); 474 | 475 | beforeEach(() => { 476 | // We need to create this wrapper so we can use refs 477 | class ElementWrapper extends Component { 478 | render() { 479 | return ; 480 | } 481 | } 482 | node = window.document.createElement("div"); 483 | instance = TestUtils.renderIntoDocument(React.createElement(ElementWrapper), node); 484 | component = instance.refs[ref]; 485 | notificationObj = merge({}, defaultNotification); 486 | 487 | clock = sinon.useFakeTimers(); 488 | }); 489 | 490 | afterEach(() => { 491 | clock.restore(); 492 | }); 493 | 494 | it('should render 2nd notification above 1st one', done => { 495 | component.addNotification(merge({}, defaultNotification, {title: '1st'})); 496 | component.addNotification(merge({}, defaultNotification, {title: '2nd'})); 497 | 498 | const notifications = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'notification'); 499 | expect(notifications[0].getElementsByClassName('notification-title')[0].textContent).to.equal('2nd'); 500 | expect(notifications[1].getElementsByClassName('notification-title')[0].textContent).to.equal('1st'); 501 | done(); 502 | }); 503 | }); -------------------------------------------------------------------------------- /tests.webpack.js: -------------------------------------------------------------------------------- 1 | // Browser ES6 Polyfill 2 | // require('babel/polyfill'); 3 | const context = require.context('./test', true, /\.test\.jsx$|\.test\.js$/); 4 | context.keys().forEach(context); 5 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | var JS_REGEX = /\.js$|\.jsx$|\.es6$|\.babel$/; 5 | 6 | module.exports = { 7 | devtool: 'eval', 8 | entry: [ 9 | 'webpack-hot-middleware/client', 10 | './example/src/scripts/App' 11 | ], 12 | output: { 13 | path: path.join(__dirname, 'example/build'), 14 | filename: 'app.js', 15 | publicPath: 'build/' 16 | }, 17 | plugins: [ 18 | new webpack.HotModuleReplacementPlugin(), 19 | new webpack.NoErrorsPlugin() 20 | ], 21 | resolve: { 22 | extensions: ['', '.js', '.jsx', '.sass'], 23 | modulesDirectories: ['node_modules', 'src'] 24 | }, 25 | module: { 26 | loaders: [ 27 | { 28 | test: JS_REGEX, 29 | include: [ 30 | path.resolve(__dirname, 'src'), 31 | path.resolve(__dirname, 'example/src') 32 | ], 33 | loader: 'babel?presets=airbnb' 34 | }, 35 | { 36 | test: /\.sass$/, 37 | loaders: [ 38 | 'style-loader', 39 | 'css-loader', 40 | 'autoprefixer-loader?browsers=last 2 version', 41 | 'sass-loader?indentedSyntax=sass&includePaths[]=' + path.resolve(__dirname, 'example/src') 42 | ] 43 | }, 44 | { 45 | test: /\.(jpe?g|png|gif|svg|woff|eot|ttf)$/, 46 | loader: 'file-loader', 47 | exclude: /node_modules/ 48 | } 49 | ] 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | 5 | var JS_REGEX = /\.js$|\.jsx$|\.es6$|\.babel$/; 6 | 7 | var sassLoaders = [ 8 | 'css-loader', 9 | 'autoprefixer-loader?browsers=last 2 version', 10 | 'sass-loader?indentedSyntax=sass&includePaths[]=' + path.resolve(__dirname, './example/src') 11 | ]; 12 | 13 | module.exports = { 14 | entry: [ 15 | './example/src/scripts/App' 16 | ], 17 | output: { 18 | path: path.join(__dirname, 'example/build'), 19 | filename: 'app.js', 20 | publicPath: '../build/' 21 | }, 22 | plugins: [ 23 | new ExtractTextPlugin('app.css', { allChunks: true }), 24 | // set env 25 | new webpack.DefinePlugin({ 26 | 'process.env': { 27 | BROWSER: JSON.stringify(true), 28 | NODE_ENV: JSON.stringify('production') 29 | } 30 | }), 31 | 32 | // optimizations 33 | new webpack.optimize.DedupePlugin(), 34 | new webpack.optimize.OccurenceOrderPlugin(), 35 | new webpack.optimize.UglifyJsPlugin({ 36 | compress: { 37 | warnings: false, 38 | screw_ie8: true, 39 | sequences: true, 40 | dead_code: true, 41 | drop_debugger: true, 42 | comparisons: true, 43 | conditionals: true, 44 | evaluate: true, 45 | booleans: true, 46 | loops: true, 47 | unused: true, 48 | hoist_funs: true, 49 | if_return: true, 50 | join_vars: true, 51 | cascade: true, 52 | drop_console: false 53 | }, 54 | output: { 55 | comments: false 56 | } 57 | }) 58 | ], 59 | resolve: { 60 | extensions: ['', '.js', '.jsx', '.sass'], 61 | modulesDirectories: ['node_modules', 'src'] 62 | }, 63 | module: { 64 | loaders: [ 65 | { 66 | test: JS_REGEX, 67 | include: [ 68 | path.resolve(__dirname, 'src'), 69 | path.resolve(__dirname, 'example/src') 70 | ], 71 | loader: 'babel?presets=airbnb' 72 | }, 73 | { 74 | test: /\.sass$/, 75 | loader: ExtractTextPlugin.extract('style-loader', sassLoaders.join('!')) 76 | }, 77 | { 78 | test: /\.(jpe?g|png|gif|svg|woff|eot|ttf)$/, 79 | loader: 'file-loader', 80 | exclude: /node_modules/ 81 | } 82 | ] 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /webpack.config.umd.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | var JS_REGEX = /\.js$|\.jsx$|\.es6$|\.babel$/; 5 | 6 | module.exports = { 7 | entry: [ 8 | './src/NotificationSystem.jsx' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'react-notification-system.js', 13 | libraryTarget: 'umd', 14 | library: "ReactNotificationSystem" 15 | }, 16 | externals: [ 17 | { 18 | react: { 19 | root: 'React', 20 | commonjs2: 'react', 21 | commonjs: 'react', 22 | amd: 'react' 23 | } 24 | }, 25 | { 26 | 'react-dom': { 27 | root: 'ReactDOM', 28 | commonjs2: 'react-dom', 29 | commonjs: 'react-dom', 30 | amd: 'react-dom' 31 | } 32 | } 33 | ], 34 | plugins: [ 35 | new webpack.NoErrorsPlugin() 36 | ], 37 | resolve: { 38 | extensions: ['', '.js', '.jsx'], 39 | modulesDirectories: ['node_modules', 'src'] 40 | }, 41 | module: { 42 | loaders: [ 43 | { 44 | test: JS_REGEX, 45 | include: [ 46 | path.resolve(__dirname, 'src'), 47 | path.resolve(__dirname, 'example/src') 48 | ], 49 | loader: 'babel?presets=airbnb' 50 | } 51 | ] 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /webpack.config.umd.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | var JS_REGEX = /\.js$|\.jsx$|\.es6$|\.babel$/; 5 | 6 | module.exports = { 7 | entry: [ 8 | './src/NotificationSystem.jsx' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'react-notification-system.min.js', 13 | libraryTarget: 'umd', 14 | library: "ReactNotificationSystem" 15 | }, 16 | devtool: 'source-map', 17 | externals: [ 18 | { 19 | react: { 20 | root: 'React', 21 | commonjs2: 'react', 22 | commonjs: 'react', 23 | amd: 'react' 24 | } 25 | }, 26 | { 27 | 'react-dom': { 28 | root: 'ReactDOM', 29 | commonjs2: 'react-dom', 30 | commonjs: 'react-dom', 31 | amd: 'react-dom' 32 | } 33 | } 34 | ], 35 | plugins: [ 36 | // set env 37 | new webpack.DefinePlugin({ 38 | 'process.env': { 39 | BROWSER: JSON.stringify(true), 40 | NODE_ENV: JSON.stringify('production') 41 | } 42 | }), 43 | 44 | // optimizations 45 | new webpack.optimize.DedupePlugin(), 46 | new webpack.optimize.OccurenceOrderPlugin(), 47 | new webpack.optimize.UglifyJsPlugin({ 48 | compress: { 49 | warnings: false, 50 | screw_ie8: true, 51 | sequences: true, 52 | dead_code: true, 53 | drop_debugger: true, 54 | comparisons: true, 55 | conditionals: true, 56 | evaluate: true, 57 | booleans: true, 58 | loops: true, 59 | unused: true, 60 | hoist_funs: true, 61 | if_return: true, 62 | join_vars: true, 63 | cascade: true, 64 | drop_console: false 65 | }, 66 | output: { 67 | comments: false 68 | } 69 | }) 70 | ], 71 | resolve: { 72 | extensions: ['', '.js', '.jsx'], 73 | modulesDirectories: ['node_modules', 'src'] 74 | }, 75 | module: { 76 | loaders: [ 77 | { 78 | test: JS_REGEX, 79 | include: [ 80 | path.resolve(__dirname, 'src'), 81 | path.resolve(__dirname, 'example/src') 82 | ], 83 | loader: 'babel?presets=airbnb' 84 | } 85 | ] 86 | } 87 | }; 88 | --------------------------------------------------------------------------------