├── .npmignore ├── tests ├── .eslintrc └── index-test.js ├── src ├── .DS_Store ├── Alert │ ├── index.ts │ ├── Positions.ts │ ├── modules.d.ts │ ├── useTimeout.ts │ ├── Alert.tsx │ ├── Toast.tsx │ ├── Message.tsx │ └── ToastManager.tsx ├── styles.css └── stories │ └── index.js ├── .storybook ├── addons.js └── config.js ├── .gitignore ├── .travis.yml ├── tsconfig.lib.json ├── CONTRIBUTING.md ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmcmahen/toasted-notes/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /demo/dist 3 | /es 4 | /lib 5 | /node_modules 6 | /umd 7 | npm-debug.log* 8 | yarn-error.log 9 | .rpt2_cache 10 | build 11 | commonjs 12 | .DS_Store -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | function loadStories() { 4 | require('../src/stories'); 5 | } 6 | 7 | configure(loadStories, module); 8 | -------------------------------------------------------------------------------- /src/Alert/index.ts: -------------------------------------------------------------------------------- 1 | import Toaster from "./Toast"; 2 | 3 | const toaster = new Toaster(); 4 | 5 | export { default as Position } from "./Positions"; 6 | 7 | export default toaster; 8 | -------------------------------------------------------------------------------- /src/Alert/Positions.ts: -------------------------------------------------------------------------------- 1 | const POSITIONS = { 2 | top: "top", 3 | "top-left": "top-left", 4 | "top-right": "top-right", 5 | bottom: "bottom", 6 | "bottom-left": "bottom-left", 7 | "bottom-right": "bottom-right" 8 | }; 9 | 10 | export default POSITIONS; 11 | -------------------------------------------------------------------------------- /src/Alert/modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@reach/alert" { 2 | export interface AlertProps { 3 | children?: React.ReactElement; 4 | type?: "assertive" | "polite"; 5 | className?: string; 6 | } 7 | 8 | const Alert: React.SFC; 9 | export default Alert; 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | node_js: 5 | - 8 6 | 7 | before_install: 8 | - npm install codecov.io coveralls 9 | 10 | after_success: 11 | - cat ./coverage/lcov.info | ./node_modules/codecov.io/bin/codecov.io.js 12 | - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js 13 | 14 | branches: 15 | only: 16 | - master 17 | -------------------------------------------------------------------------------- /tests/index-test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import React from 'react' 3 | import {render, unmountComponentAtNode} from 'react-dom' 4 | 5 | import Component from 'src/' 6 | 7 | describe('Component', () => { 8 | let node 9 | 10 | beforeEach(() => { 11 | node = document.createElement('div') 12 | }) 13 | 14 | afterEach(() => { 15 | unmountComponentAtNode(node) 16 | }) 17 | 18 | it('displays a welcome message', () => { 19 | render(, node, () => { 20 | expect(node.innerHTML).toContain('Welcome to React components') 21 | }) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /src/Alert/useTimeout.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | type Callback = () => void; 4 | 5 | export function useTimeout(callback: Callback, delay: number | null) { 6 | const savedCallback = React.useRef(); 7 | 8 | // Remember the latest callback. 9 | React.useEffect(() => { 10 | savedCallback.current = callback; 11 | }, [callback]); 12 | 13 | // Set up the interval. 14 | React.useEffect(() => { 15 | function tick() { 16 | if (savedCallback.current) { 17 | savedCallback.current(); 18 | } 19 | } 20 | if (delay !== null) { 21 | let id = setTimeout(tick, delay); 22 | return () => clearTimeout(id); 23 | } 24 | }, [delay]); 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "jsx": "react", 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "noImplicitAny": true, 10 | "outDir": "build", 11 | "preserveConstEnums": true, 12 | "target": "es2017", 13 | "declaration": true, 14 | "lib": ["dom", "dom.iterable", "esnext"], 15 | "skipLibCheck": true, 16 | "esModuleInterop": true, 17 | "allowSyntheticDefaultImports": true, 18 | "strict": true, 19 | "resolveJsonModule": true 20 | }, 21 | "exclude": ["node_modules"], 22 | "include": ["src/Alert/**/*"] 23 | } 24 | -------------------------------------------------------------------------------- /src/Alert/Alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | interface Props { 4 | id: string; 5 | title: React.ReactNode | string; 6 | onClose: () => void; 7 | } 8 | 9 | const Alert = ({ id, title, onClose }: Props) => { 10 | return ( 11 |
12 | {typeof title === "string" ? ( 13 |
{title}
14 | ) : ( 15 | title 16 | )} 17 | 18 | {onClose && } 19 |
20 | ); 21 | }; 22 | 23 | const Close = ({ onClose }: { onClose: () => void }) => ( 24 | 32 | ); 33 | 34 | export default Alert; 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | [Node.js](http://nodejs.org/) >= v4 must be installed. 4 | 5 | ## Installation 6 | 7 | - Running `npm install` in the component's root directory will install everything you need for development. 8 | 9 | ## Demo Development Server 10 | 11 | - `npm start` will run a development server with the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading. 12 | 13 | ## Running Tests 14 | 15 | - `npm test` will run the tests once. 16 | 17 | - `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`. 18 | 19 | - `npm run test:watch` will run the tests on every change. 20 | 21 | ## Building 22 | 23 | - `npm run build` will build the component for publishing to npm and also bundle the demo app. 24 | 25 | - `npm run clean` will delete built resources. 26 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | .Toaster__alert { 2 | background-color: white; 3 | overflow: hidden; 4 | max-width: 650px; 5 | position: relative; 6 | border-radius: 0.4rem; 7 | display: flex; 8 | padding: 1rem; 9 | padding-right: 48px; 10 | box-shadow: rgba(52, 58, 64, 0.15) 0px 1px 10px 0px, 11 | rgba(52, 58, 64, 0.1) 0px 6px 12px 0px, 12 | rgba(52, 58, 64, 0.12) 0px 6px 15px -2px; 13 | } 14 | 15 | .Toaster__alert_text { 16 | box-sizing: border-box; 17 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 18 | "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", 19 | "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 20 | color: rgb(33, 37, 41); 21 | -webkit-font-smoothing: antialiased; 22 | font-weight: 500; 23 | line-height: 1.5; 24 | font-size: 1rem; 25 | margin: 0px; 26 | } 27 | 28 | .Toaster__alert_close { 29 | padding: 12px; 30 | outline: none; 31 | cursor: pointer; 32 | background-color: transparent; 33 | position: absolute; 34 | top: 7px; 35 | right: 4px; 36 | border-radius: 0.4rem; 37 | border: 0; 38 | -webkit-appearance: none; 39 | font-size: 1rem; 40 | font-weight: 700; 41 | line-height: 1; 42 | text-shadow: 0 1px 0 #fff; 43 | opacity: 0.5; 44 | } 45 | 46 | .Toaster__alert_close:focus { 47 | box-shadow: rgba(52, 58, 64, 0.15) 0px 0px 0px 3px; 48 | } 49 | 50 | .Toaster__message-wrapper { 51 | padding: 8px; 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "toasted-notes", 3 | "version": "3.2.0", 4 | "description": "Flexible, easy to implement toast notifications for react", 5 | "main": "commonjs/index.js", 6 | "module": "lib/index.js", 7 | "typings": "lib/index.d.ts", 8 | "style": "src/styles.css", 9 | "scripts": { 10 | "build-library-commonjs": "rimraf ./commonjs && tsc -p tsconfig.lib.json --module commonjs --outDir commonjs", 11 | "build-library": "rimraf ./lib && tsc -p tsconfig.lib.json --outDir ./lib", 12 | "prepublishOnly": "yarn run build-library && yarn run build-library-commonjs", 13 | "storybook": "start-storybook -p 9009", 14 | "build-storybook": "build-storybook" 15 | }, 16 | "dependencies": { 17 | "@reach/alert": "^0.1.2", 18 | "@types/react": "^16.8.10", 19 | "@types/react-dom": "^16.8.3" 20 | }, 21 | "peerDependencies": { 22 | "react": "^16.8.4", 23 | "react-dom": "^16.8.4" 24 | }, 25 | "devDependencies": { 26 | "@storybook/addon-actions": "^5.0.5", 27 | "@storybook/addon-links": "^5.0.5", 28 | "@storybook/react": "^5.0.5", 29 | "babel-loader": "^8.0.5", 30 | "react": "^16.8.6", 31 | "react-dom": "^16.8.6", 32 | "react-scripts": "^2.1.5", 33 | "react-spring": "^8.0.0", 34 | "rimraf": "^2.6.3", 35 | "typescript": "^3.5.0" 36 | }, 37 | "author": "Ben McMahen ", 38 | "homepage": "https://github.com/bmcmahen/toasted-notes", 39 | "license": "MIT", 40 | "repository": "", 41 | "keywords": [ 42 | "react-component", 43 | "toast", 44 | "notifications" 45 | ], 46 | "browserslist": [ 47 | ">0.2%", 48 | "not dead", 49 | "not ie <= 11", 50 | "not op_mini all" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /src/Alert/Toast.tsx: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from "react-dom"; 2 | import * as React from "react"; 3 | import ToastManager, { MessageOptionalOptions } from "./ToastManager"; 4 | import { MessageProp, PositionsType } from "./Message"; 5 | 6 | const isBrowser = 7 | typeof window !== "undefined" && typeof window.document !== "undefined"; 8 | const PORTAL_ID = "react-toast"; 9 | 10 | class Toaster { 11 | createNotification?: Function; 12 | removeAll?: Function; 13 | closeToast?: Function; 14 | 15 | constructor() { 16 | if (!isBrowser) { 17 | return; 18 | } 19 | 20 | let portalElement; 21 | const existingPortalElement = document.getElementById(PORTAL_ID); 22 | 23 | if (existingPortalElement) { 24 | portalElement = existingPortalElement; 25 | } else { 26 | const el = document.createElement("div"); 27 | el.id = PORTAL_ID; 28 | el.className = "Toaster"; 29 | if (document.body != null) { 30 | document.body.appendChild(el); 31 | } 32 | portalElement = el; 33 | } 34 | 35 | ReactDOM.render( 36 | , 37 | portalElement 38 | ); 39 | } 40 | 41 | closeAll = () => { 42 | if (this.removeAll) { 43 | this.removeAll(); 44 | } 45 | }; 46 | 47 | bindNotify = (fn: Function, removeAll: Function, closeToast: Function) => { 48 | this.createNotification = fn; 49 | this.removeAll = removeAll; 50 | this.closeToast = closeToast; 51 | }; 52 | 53 | notify = (message: MessageProp, options: MessageOptionalOptions = {}) => { 54 | if (this.createNotification) { 55 | return this.createNotification(message, options); 56 | } 57 | }; 58 | 59 | close = (id: number, position: PositionsType) => { 60 | if(this.closeToast){ 61 | this.closeToast(id, position); 62 | } 63 | } 64 | } 65 | 66 | export default Toaster; 67 | -------------------------------------------------------------------------------- /src/stories/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { storiesOf } from "@storybook/react"; 3 | import toast, { Position } from "../Alert"; 4 | import "../styles.css"; 5 | 6 | storiesOf("Toasted-notes", module) 7 | .add("pointer-events", () => ( 8 |
9 | 18 |
19 | ); 20 | }); 21 | }} 22 | > 23 | Try me 24 | 25 | 26 | Dolore eu excepteur pariatur anim. Non proident excepteur Lorem cillum 27 | aliqua do. Nulla laborum mollit quis enim velit cillum aliquip occaecat 28 | dolore commodo occaecat voluptate voluptate et. Nostrud est ex aliquip 29 | officia do dolore Lorem. Non veniam excepteur aute ullamco magna. 30 | 31 | 32 | )) 33 | .add("all directions", () => ( 34 |
35 | {Object.keys(Position).map(position => ( 36 | 48 | ))} 49 | 50 | 51 |
52 | )).add("close single", () => { 53 | function Parent({ children, ...props }) { 54 | const [t, setToast] = useState() 55 | return
{children(t, setToast)}
; 56 | } 57 | return ( 58 | {(t, setToast) => ( 59 |
60 | 70 | 71 | 72 |
) 73 | } 74 |
) 75 | }); 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Toasted-notes 4 | 5 | [![npm package](https://img.shields.io/npm/v/toasted-notes/latest.svg)](https://www.npmjs.com/package/toasted-notes) 6 | [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=toasted%20notes%20is%20a%20react%20library%20for%20creating%20simple%2C%20flexible%20toast%20notifications.&url=https://github.com/bmcmahen/toasted-notes&hashtags=react,javascript) 7 | [![Follow on Twitter](https://img.shields.io/twitter/follow/benmcmahen.svg?style=social&logo=twitter)]( 8 | https://twitter.com/intent/follow?screen_name=benmcmahen 9 | ) 10 | 11 |
12 | 13 | A simple but flexible implementation of toast style notifications for React extracted from [Sancho UI](https://github.com/bmcmahen/sancho). 14 | 15 | [View the demo and documentation](https://toasted-notes.netlify.com/). 16 | 17 | ## Features 18 | 19 | - **An imperative API.** This means that you don't need to set component state or render elements to trigger notifications. Instead, just call a function. 20 | - **Render whatever you want.** Utilize the render callback to create entirely custom notifications. 21 | - **Functional default styles.** Import the provided css for some nice styling defaults or write your own styles. 22 | 23 | ## Install 24 | 25 | Install `toasted-notes` and its peer dependency, `react-spring`, using yarn or npm. 26 | 27 | ``` 28 | yarn add toasted-notes react-spring 29 | ``` 30 | 31 | ## Example 32 | 33 | ```jsx 34 | import toaster from "toasted-notes"; 35 | import "toasted-notes/src/styles.css"; // optional styles 36 | 37 | const HelloWorld = () => ( 38 | 47 | ); 48 | ``` 49 | 50 | ## API 51 | 52 | The notify function accepts either a string, a react node, or a render callback. 53 | 54 | ```jsx 55 | // using a string 56 | toaster.notify("With a simple string"); 57 | 58 | // using jsx 59 | toaster.notify(
Hi there
); 60 | 61 | // using a render callback 62 | toaster.notify(({ onClose }) => ( 63 |
64 | My custom toaster 65 | 66 |
67 | )); 68 | ``` 69 | 70 | It also accepts options. 71 | 72 | ```javascript 73 | toaster.notify("Hello world", { 74 | position: "bottom-left", // top-left, top, top-right, bottom-left, bottom, bottom-right 75 | duration: null // This notification will not automatically close 76 | }); 77 | ``` 78 | 79 | ## Using Context 80 | 81 | One downside to the current API is that render callbacks and custom nodes won't get access to any application context, such as theming variables provided by styled-components. To ensure that render callbacks have access to the necessary context, you'll need to supply that context to the callback. 82 | 83 | ```jsx 84 | const CustomNotification = ({ title }) => { 85 | const theme = useTheme(); 86 | return
{title}
; 87 | }; 88 | 89 | const CustomNotificationWithTheme = withTheme(CustomNotification); 90 | 91 | toaster.notify(() => ); 92 | ``` 93 | 94 | ## Contributors 95 | 96 | - [Einar Löve](https://github.com/einarlove) 97 | 98 | ## License 99 | 100 | MIT 101 | 102 | ## Prior art 103 | 104 | Way back, this was originally based on the wonderful implementation of notifications in [evergreen](https://evergreen.segment.com). 105 | -------------------------------------------------------------------------------- /src/Alert/Message.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useTransition, animated } from "react-spring"; 3 | import ReachAlert from "@reach/alert"; 4 | import Alert from "./Alert"; 5 | import { useTimeout } from "./useTimeout"; 6 | import POSITIONS from "./Positions"; 7 | 8 | interface MessageCallback { 9 | id: string; 10 | onClose: () => void; 11 | } 12 | 13 | export type MessageType = "default" | "success" | "error"; 14 | 15 | export type PositionsType = keyof typeof POSITIONS; 16 | 17 | const getStyle = (position: PositionsType) => { 18 | let style = { 19 | display: "flex", 20 | flexDirection: "column", 21 | alignItems: "center" 22 | } as React.CSSProperties; 23 | 24 | if (position.includes("right")) { 25 | style.alignItems = "flex-end"; 26 | } else if (position.includes("left")) { 27 | style.alignItems = "flex-start"; 28 | } 29 | 30 | return style; 31 | }; 32 | 33 | export type MessageProp = 34 | | React.ReactNode 35 | | ((callback: MessageCallback) => React.ReactNode) 36 | | string; 37 | 38 | export interface MessageOptions { 39 | id: string; 40 | duration: number | null; 41 | type: MessageType; 42 | onRequestRemove: () => void; 43 | onRequestClose: () => void; 44 | showing: boolean; 45 | position: PositionsType; 46 | } 47 | 48 | interface Props extends MessageOptions { 49 | message: MessageProp; 50 | zIndex?: number; 51 | requestClose?: boolean; 52 | position: PositionsType; 53 | } 54 | 55 | export const Message = ({ 56 | id, 57 | message, 58 | position, 59 | onRequestRemove, 60 | requestClose = false, 61 | duration = 30000 62 | }: Props) => { 63 | const container = React.useRef(null); 64 | const [timeout, setTimeout] = React.useState(duration); 65 | const [localShow, setLocalShow] = React.useState(true); 66 | 67 | const isFromTop = 68 | position === "top-left" || position === "top-right" || position === "top"; 69 | 70 | useTimeout(close, timeout); 71 | 72 | const animation = { 73 | config: { mass: 1, tension: 185, friction: 26 }, 74 | from: { 75 | opacity: 1, 76 | height: 0, 77 | transform: `translateY(${isFromTop ? "-100%" : 0}) scale(1)` 78 | }, 79 | enter: () => (next: any) => 80 | next({ 81 | opacity: 1, 82 | height: container.current!.getBoundingClientRect().height, 83 | transform: `translateY(0) scale(1)` 84 | }), 85 | leave: { 86 | opacity: 0, 87 | height: 0, 88 | transform: `translateY(0 scale(0.9)` 89 | }, 90 | onRest 91 | } as any; 92 | 93 | const transition = useTransition(localShow, null, animation); 94 | const style = React.useMemo(() => getStyle(position), [position]); 95 | 96 | function onMouseEnter() { 97 | setTimeout(null); 98 | } 99 | 100 | function onMouseLeave() { 101 | setTimeout(duration); 102 | } 103 | 104 | function onRest() { 105 | if (!localShow) { 106 | onRequestRemove(); 107 | } 108 | } 109 | 110 | function close() { 111 | setLocalShow(false); 112 | } 113 | 114 | React.useEffect(() => { 115 | if (requestClose) { 116 | setLocalShow(false); 117 | } 118 | }, [requestClose]); 119 | 120 | function renderMessage() { 121 | if (typeof message === "string" || React.isValidElement(message)) { 122 | return ; 123 | } 124 | 125 | if (typeof message === "function") { 126 | return message({ 127 | id, 128 | onClose: close 129 | }); 130 | } 131 | 132 | return null; 133 | } 134 | 135 | return ( 136 | 137 | {transition.map( 138 | ({ key, item, props }) => 139 | item && ( 140 | 151 | 159 | {renderMessage()} 160 | 161 | 162 | ) 163 | )} 164 | 165 | ); 166 | }; 167 | -------------------------------------------------------------------------------- /src/Alert/ToastManager.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | Message, 4 | PositionsType, 5 | MessageType, 6 | MessageOptions, 7 | MessageProp 8 | } from "./Message"; 9 | 10 | interface Props { 11 | notify: (fn: Function, closeAll: Function, close: Function) => void; 12 | } 13 | 14 | export interface MessageOptionalOptions { 15 | type?: MessageType; 16 | duration?: number | null; 17 | position?: PositionsType; 18 | } 19 | 20 | interface ToastArgs extends MessageOptions { 21 | message: MessageProp; 22 | } 23 | 24 | type State = { 25 | top: Array; 26 | "top-left": Array; 27 | "top-right": Array; 28 | "bottom-left": Array; 29 | bottom: Array; 30 | "bottom-right": Array; 31 | }; 32 | 33 | const defaultState: State = { 34 | top: [], 35 | "top-left": [], 36 | "top-right": [], 37 | "bottom-left": [], 38 | bottom: [], 39 | "bottom-right": [] 40 | }; 41 | 42 | type Keys = keyof State; 43 | 44 | export default class ToastManager extends React.Component { 45 | static idCounter = 0; 46 | 47 | state: State = defaultState; 48 | 49 | constructor(props: Props) { 50 | super(props); 51 | props.notify(this.notify, this.closeAll, this.closeToast); 52 | } 53 | 54 | notify = (message: MessageProp, options: MessageOptionalOptions) => { 55 | const toast = this.createToastState(message, options); 56 | const { position } = toast; 57 | 58 | // prepend the toast for toasts positioned at the top of 59 | // the screen, otherwise append it. 60 | const isTop = position.includes("top"); 61 | 62 | this.setState(prev => { 63 | return { 64 | ...prev, 65 | [position]: isTop 66 | ? [toast, ...prev[position]] 67 | : [...prev[position], toast] 68 | }; 69 | }); 70 | return { id: toast.id, position: toast.position }; 71 | }; 72 | 73 | closeAll = () => { 74 | Object.keys(this.state).forEach(pos => { 75 | const p = pos as keyof State; 76 | const position = this.state[p]; 77 | position.forEach((toast: any) => { 78 | this.closeToast(toast.id, p); 79 | }); 80 | }); 81 | }; 82 | 83 | createToastState = ( 84 | message: MessageProp, 85 | options: MessageOptionalOptions 86 | ) => { 87 | const id = ++ToastManager.idCounter; 88 | 89 | // a bit messy, but object.position returns a number because 90 | // it's a method argument. 91 | const position = 92 | options.hasOwnProperty("position") && typeof options.position === "string" 93 | ? options.position 94 | : "top"; 95 | 96 | return { 97 | id, 98 | message, 99 | position, 100 | showing: true, 101 | duration: 102 | typeof options.duration === "undefined" ? 5000 : options.duration, 103 | onRequestRemove: () => this.removeToast(String(id), position), 104 | type: options.type 105 | }; 106 | }; 107 | 108 | closeToast = (id: string, position: PositionsType) => { 109 | this.setState(prev => { 110 | return { 111 | ...prev, 112 | [position]: prev[position].map(toast => { 113 | if (toast.id !== id) return toast; 114 | return { 115 | ...toast, 116 | requestClose: true 117 | }; 118 | }) 119 | }; 120 | }); 121 | }; 122 | 123 | // actually fully remove the toast 124 | removeToast = (id: string, position: PositionsType) => { 125 | this.setState(prev => { 126 | return { 127 | ...prev, 128 | [position]: prev[position].filter(toast => toast.id !== id) 129 | }; 130 | }); 131 | }; 132 | 133 | getStyle = (position: PositionsType) => { 134 | let style: React.CSSProperties = { 135 | maxWidth: "560px", 136 | position: "fixed", 137 | zIndex: 5500, 138 | pointerEvents: "none" 139 | }; 140 | 141 | if (position === "top" || position === "bottom") { 142 | style.margin = "0 auto"; 143 | style.textAlign = "center"; 144 | } 145 | 146 | if (position.includes("top")) { 147 | style.top = 0; 148 | } 149 | 150 | if (position.includes("bottom")) { 151 | style.bottom = 0; 152 | } 153 | 154 | if (!position.includes("left")) { 155 | style.right = 0; 156 | } 157 | 158 | if (!position.includes("right")) { 159 | style.left = 0; 160 | } 161 | 162 | return style; 163 | }; 164 | 165 | render() { 166 | return Object.keys(this.state).map(position => { 167 | const pos = position as keyof State; 168 | const toasts = this.state[pos]; 169 | return ( 170 | 175 | {toasts.map((toast: ToastArgs) => { 176 | return ; 177 | })} 178 | 179 | ); 180 | }); 181 | } 182 | } 183 | --------------------------------------------------------------------------------