├── .babelrc ├── .eslintrc ├── .gitignore ├── .nvmrc ├── .pnp.cjs ├── .yarnrc.yml ├── LICENSE ├── README.md ├── build └── index.js ├── jest.config.js ├── package.json ├── src ├── __snapshots__ │ └── index.spec.jsx.snap ├── index.jsx └── index.spec.jsx ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "browser": true, 5 | "jest": true 6 | }, 7 | "plugins": ["testing-library"] 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /coverage 3 | 4 | .DS_Store 5 | .vscode 6 | .idea 7 | .yarn 8 | 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v14.7.0 2 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 3 | spec: "@yarnpkg/plugin-interactive-tools" 4 | 5 | yarnPath: .yarn/releases/yarn-3.0.1.cjs 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Daniel Sneijers 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Flash Message ✨ 2 | 3 | Simple component that unmounts a component after a given delay. It adds no styling or animations, you can use other components like [react-transition-group](https://github.com/reactjs/react-transition-group) for that. 4 | 5 | ## Basic Example 6 | 7 | ```jsx 8 | import React from 'react'; 9 | import { render } from 'react-dom'; 10 | import FlashMessage from 'react-flash-message' 11 | 12 | const Message = () => ( 13 | 14 | I will disapper in 5 seconds! 15 | 16 | ) 17 | 18 | render(Message, document.body); 19 | ``` 20 | 21 | ## API 22 | 23 | ### Component 24 | 25 | ```jsx 26 | import FlashMessage from 'react-flash-message'; 27 | 28 | // inside render 29 | 30 |

Message

31 |
; 32 | ``` 33 | 34 | ### Props 35 | 36 | | Prop | Type | Default | Description | 37 | | ---------------- | ------ | ------- | ---------------------------------------------------------------- | 38 | | `duration` | number | 5000 | Number of milliseconds the component will show | 39 | | `persistOnHover` | bool | true | Will not remove the component when the user hovers on it if true | 40 | 41 | ## Issues 42 | 43 | Feel free to contribute. Submit a Pull Request or open an issue for further discussion. 44 | 45 | ## License 46 | 47 | MIT 48 | -------------------------------------------------------------------------------- /build/index.js: -------------------------------------------------------------------------------- 1 | (()=>{var e={159:(e,t,r)=>{"use strict";var n=r(929);function o(){}function i(){}i.resetWarningCache=o,e.exports=function(){function e(e,t,r,o,i,u){if(u!==n){var s=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw s.name="Invariant Violation",s}}function t(){return e}e.isRequired=e;var r={array:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:i,resetWarningCache:o};return r.PropTypes=r,r}},15:(e,t,r)=>{e.exports=r(159)()},929:e=>{"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var i=t[n]={exports:{}};return e[n](i,i.exports,r),i.exports}r.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},r.d=(e,t)=>{for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var n={};(()=>{"use strict";r.r(n),r.d(n,{default:()=>l});const e=require("react");var t=r.n(e),o=r(15);function i(e){return(i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function u(e,t){for(var r=0;r/node_modules/', 6 | ], 7 | testEnvironment: "jsdom", 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-flash-message", 3 | "version": "1.0.8", 4 | "description": "Simple component that unmounts a component after a given delay.", 5 | "main": "build/index.js", 6 | "author": "Daniel Sneijers (https://github.com/danielsneijers)", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/danielsneijers/react-flash-message.git" 10 | }, 11 | "license": "MIT", 12 | "scripts": { 13 | "start": "webpack --mode development --watch", 14 | "build": "webpack --mode production", 15 | "test": "jest", 16 | "lint": "eslint --ext .js --ext .jsx src --fix", 17 | "prettier": "prettier --trailing-comma es5 --single-quote --write 'src/**/*.{js,jsx}'", 18 | "precommit": "lint-staged" 19 | }, 20 | "lint-staged": { 21 | "linters": { 22 | "*.{js,jsx}": [ 23 | "yarn prettier", 24 | "yarn lint", 25 | "git add" 26 | ] 27 | } 28 | }, 29 | "dependencies": { 30 | "prop-types": "^15.7.2" 31 | }, 32 | "devDependencies": { 33 | "@babel/core": "^7.15.0", 34 | "@babel/plugin-transform-react-jsx": "^7.14.9", 35 | "@babel/preset-env": "^7.15.0", 36 | "@babel/preset-react": "^7.14.5", 37 | "@testing-library/jest-dom": "^5.14.1", 38 | "@testing-library/react": "^12.0.0", 39 | "babel-jest": "^27.0.6", 40 | "babel-loader": "^8.2.2", 41 | "eslint": "^7.32.0", 42 | "eslint-config-airbnb": "^18.2.1", 43 | "eslint-plugin-import": "^2.24.0", 44 | "eslint-plugin-jsx-a11y": "^6.4.1", 45 | "eslint-plugin-react": "^7.24.0", 46 | "eslint-plugin-testing-library": "^4.11.0", 47 | "extract-text-webpack-plugin": "^3.0.2", 48 | "html-webpack-plugin": "^5.3.2", 49 | "husky": "^7.0.1", 50 | "jest": "^27.0.6", 51 | "lint-staged": "^11.1.2", 52 | "prettier": "^2.3.2", 53 | "react": "^17.0.2", 54 | "react-dom": "^17.0.2", 55 | "regenerator-runtime": "^0.13.9", 56 | "webpack": "^5.50.0", 57 | "webpack-cli": "^4.8.0", 58 | "webpack-dev-server": "^4.0.0" 59 | }, 60 | "peerDependencies": { 61 | "react": ">=15 <= 17", 62 | "react-dom": ">=15 <= 17" 63 | }, 64 | "packageManager": "yarn@3.0.1" 65 | } 66 | -------------------------------------------------------------------------------- /src/__snapshots__/index.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` mounts and unmounts children 1`] = ` 4 | Array [ 5 | Array [ 6 | "render", 7 | ], 8 | Array [ 9 | "componentDidMount", 10 | ], 11 | ] 12 | `; 13 | 14 | exports[` mounts and unmounts children 2`] = ` 15 | Array [ 16 | Array [ 17 | "render", 18 | ], 19 | Array [ 20 | "componentDidMount", 21 | ], 22 | Array [ 23 | "componentWillUnmount", 24 | ], 25 | ] 26 | `; 27 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { node, number, bool } from 'prop-types'; 3 | 4 | class FlashMessage extends Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.state = { isVisible: true }; 9 | 10 | this.hide = this.hide.bind(this); 11 | this.resumeTimer = this.resumeTimer.bind(this); 12 | this.pauseTimer = this.pauseTimer.bind(this); 13 | } 14 | 15 | componentDidMount() { 16 | const { duration } = this.props; 17 | this.remaining = duration; 18 | this.resumeTimer(); 19 | } 20 | 21 | componentWillUnmount() { 22 | clearTimeout(this.timer); 23 | } 24 | 25 | hide() { 26 | this.setState({ isVisible: false }); 27 | } 28 | 29 | resumeTimer() { 30 | window.clearTimeout(this.timer); 31 | 32 | this.start = new Date(); 33 | this.timer = setTimeout(this.hide, this.remaining); 34 | } 35 | 36 | pauseTimer() { 37 | const { persistOnHover } = this.props; 38 | if (persistOnHover) { 39 | clearTimeout(this.timer); 40 | 41 | this.remaining -= new Date() - this.start; 42 | } 43 | } 44 | 45 | render() { 46 | const { isVisible } = this.state; 47 | const { children } = this.props; 48 | 49 | return isVisible ? ( 50 |
51 | {children} 52 |
53 | ) : null; 54 | } 55 | } 56 | 57 | FlashMessage.defaultProps = { 58 | duration: 5000, 59 | children: null, 60 | persistOnHover: true, 61 | }; 62 | 63 | FlashMessage.propTypes = { 64 | children: node, 65 | duration: number, 66 | persistOnHover: bool, 67 | }; 68 | 69 | export default FlashMessage; 70 | -------------------------------------------------------------------------------- /src/index.spec.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file, react/prop-types, react/no-unescaped-entities */ 2 | import '@testing-library/jest-dom'; 3 | import React, { PureComponent } from 'react'; 4 | import { render, fireEvent } from '@testing-library/react'; 5 | import FlashMessage from './index'; 6 | 7 | class TestComponent extends PureComponent { 8 | componentDidMount() { 9 | const { track } = this.props; 10 | track('componentDidMount'); 11 | } 12 | 13 | componentWillUnmount() { 14 | const { track } = this.props; 15 | track('componentWillUnmount'); 16 | } 17 | 18 | render() { 19 | const { track } = this.props; 20 | track('render'); 21 | return

Hej, I'm a message!

; 22 | } 23 | } 24 | 25 | describe('', () => { 26 | beforeAll(() => { 27 | const constantDate = new Date('2018-02-13T04:41:20'); 28 | 29 | /* eslint-disable-next-line no-global-assign */ 30 | Date = class extends Date { 31 | constructor() { 32 | return constantDate; 33 | } 34 | }; 35 | }); 36 | 37 | beforeEach(() => { 38 | jest.useFakeTimers(); 39 | }); 40 | 41 | it('renders and sets a timer', () => { 42 | const message = 'test'; 43 | const { queryByText } = render( 44 | 45 | {message} 46 | , 47 | ); 48 | 49 | expect(queryByText(message)).toBeInTheDocument(); 50 | }); 51 | 52 | it('disappears after x seconds', () => { 53 | const message = 'test'; 54 | const { queryByText } = render( 55 | 56 | {message} 57 | , 58 | ); 59 | 60 | expect(queryByText(message)).toBeInTheDocument(); 61 | 62 | jest.advanceTimersByTime(2000); 63 | 64 | expect(queryByText(message)).not.toBeInTheDocument(); 65 | }); 66 | 67 | /* Disabled because clearTimeout isn't mocked properly in new jest 68 | it('removes existing timer before umounting', () => { 69 | const { unmount } = render( 70 | 71 | 72 | , 73 | ); 74 | 75 | expect(clearTimeout).toHaveBeenCalledTimes(1); 76 | 77 | unmount(); 78 | 79 | expect(clearTimeout).toHaveBeenCalledTimes(2); 80 | })*/ 81 | 82 | it('mounts and unmounts children', () => { 83 | const tracker = jest.fn(); 84 | render( 85 | 86 | 87 | , 88 | ); 89 | 90 | expect(tracker.mock.calls).toMatchSnapshot(); 91 | 92 | jest.advanceTimersByTime(5000); 93 | 94 | expect(tracker.mock.calls).toMatchSnapshot(); 95 | }); 96 | 97 | it("doesn't unmount on hover when flag is set", () => { 98 | const message = 'test'; 99 | const { queryByText } = render({message}); 100 | 101 | fireEvent.mouseEnter(queryByText(message)); 102 | jest.advanceTimersByTime(2000); 103 | 104 | expect(queryByText(message)).toBeInTheDocument(); 105 | 106 | fireEvent.mouseLeave(queryByText(message)); 107 | jest.advanceTimersByTime(1000); 108 | 109 | expect(queryByText(message)).not.toBeInTheDocument(); 110 | }); 111 | 112 | it('does unmount on hover when flag is set', () => { 113 | const message = 'test'; 114 | const { queryByText } = render( 115 | 116 | {message} 117 | , 118 | ); 119 | 120 | fireEvent.mouseEnter(queryByText(message)); 121 | jest.advanceTimersByTime(2000); 122 | 123 | expect(queryByText(message)).not.toBeInTheDocument(); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | const webpack = require('webpack'); 3 | // const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | const baseConfig = { 6 | entry: './src/index.jsx', 7 | // devtool: 'inline-source-map', 8 | // devServer: { 9 | // hot: true, 10 | // contentBase: 'build', 11 | // publicPath: '/', 12 | // stats: 'errors-only', 13 | // }, 14 | output: { 15 | path: resolve('./build'), 16 | filename: 'index.js', 17 | libraryTarget: 'commonjs2', 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.jsx?$/, 23 | loader: 'babel-loader', 24 | exclude: /node-modules/, 25 | }, 26 | ], 27 | }, 28 | // plugins: [ 29 | // new HtmlWebpackPlugin({ 30 | // title: 'Title', 31 | // template: './src/index.html', 32 | // minify: { useShortDoctype: true }, 33 | // hash: false, 34 | // }), 35 | // ], 36 | externals: { 37 | react: 'react', // this line is just to use the React dependency of our parent-testing-project instead of using our own React. 38 | }, 39 | resolve: { 40 | extensions: ['.js', '.jsx'], 41 | modules: ['node_modules'], 42 | }, 43 | }; 44 | 45 | module.exports = baseConfig; 46 | --------------------------------------------------------------------------------