Repository: fkhadra/react-toastify
Branch: main
Commit: e1fa4760cea8
Files: 71
Total size: 158.9 KB
Directory structure:
gitextract_tvelgcr9/
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── workflows/
│ └── build.yaml
├── .gitignore
├── .nycrc.json
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── cypress/
│ └── support/
│ ├── commands.ts
│ ├── component-index.html
│ ├── component.ts
│ └── style.css
├── cypress.config.ts
├── lefthook.yml
├── package.json
├── playground/
│ ├── .eslintrc.cjs
│ ├── .gitignore
│ ├── index.html
│ ├── package.json
│ ├── src/
│ │ ├── components/
│ │ │ ├── App.tsx
│ │ │ ├── Checkbox.tsx
│ │ │ ├── ContainerCode.tsx
│ │ │ ├── Header.tsx
│ │ │ ├── Radio.tsx
│ │ │ ├── ToastCode.tsx
│ │ │ └── constants.ts
│ │ ├── index.css
│ │ ├── main.tsx
│ │ └── vite-env.d.ts
│ ├── tsconfig.json
│ ├── tsconfig.node.json
│ └── vite.config.ts
├── src/
│ ├── addons/
│ │ └── use-notification-center/
│ │ ├── NotificationCenter.cy.tsx
│ │ ├── index.ts
│ │ └── useNotificationCenter.ts
│ ├── components/
│ │ ├── CloseButton.cy.tsx
│ │ ├── CloseButton.tsx
│ │ ├── Icons.cy.tsx
│ │ ├── Icons.tsx
│ │ ├── ProgressBar.cy.tsx
│ │ ├── ProgressBar.tsx
│ │ ├── Toast.cy.tsx
│ │ ├── Toast.tsx
│ │ ├── ToastContainer.tsx
│ │ ├── Transitions.tsx
│ │ └── index.tsx
│ ├── core/
│ │ ├── containerObserver.ts
│ │ ├── genToastId.ts
│ │ ├── index.ts
│ │ ├── store.ts
│ │ ├── toast.cy.tsx
│ │ └── toast.ts
│ ├── hooks/
│ │ ├── index.ts
│ │ ├── useIsomorphicLayoutEffect.ts
│ │ ├── useToast.ts
│ │ └── useToastContainer.ts
│ ├── index.ts
│ ├── style.css
│ ├── tests.cy.tsx
│ ├── types.ts
│ └── utils/
│ ├── collapseToast.ts
│ ├── constant.ts
│ ├── cssTransition.tsx
│ ├── index.ts
│ ├── mapper.ts
│ └── propValidator.ts
├── tsconfig.json
├── tsup.config.ts
└── vite.config.mts
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: fkhadra
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
**Do you want to request a _feature_ or report a _bug_?**
**What is the current behavior?**
**If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Your bug will get fixed much faster if we can run your code and it doesn't have dependencies other than React. Paste the link to your [Stackblitz](https://stackblitz.com/edit/react-toastify-getting-started) example below:**
**What is the expected behavior?**
**Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?**
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
**Before submitting a pull request,** please make sure the following is done:
1. Fork [the repository](https://github.com/fkhadra/react-toastify) and create your branch from `main`.
2. Run `pnpm i` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`pnpm test`).
5. Run `pnpm start` to test your changes in the playground.
6. Update the readme is needed
7. Update the typescript definition is needed
8. Format your code with [prettier](https://github.com/prettier/prettier) (`pnpm prettier`).
9. Make sure your code lints (`pnpm lint:fix`).
For new features, please make sure that there is an issue related to it.
**Learn more about contributing [here](https://github.com/fkhadra/react-toastify/blob/master/CONTRIBUTING.md)**
================================================
FILE: .github/workflows/build.yaml
================================================
name: React-toastify CI
on: [pull_request, push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- name: Install node
uses: actions/setup-node@v4.1.0
with:
node-version: '22.x'
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Install dependencies
run: pnpm i
# - name: Lint
# run: yarn lint
- name: Setup
run: pnpm run setup
- name: Build
run: pnpm build
- name: Test
run: pnpm run test:run
- uses: actions/upload-artifact@v3
if: failure()
with:
name: cypress-screenshots
path: cypress/screenshots
- uses: actions/upload-artifact@v3
if: always()
with:
name: cypress-videos
path: cypress/videos
- name: Coveralls GitHub Action
uses: coverallsapp/github-action@v2.3.4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
.idea/
node_modules/
lib/
.sass-cache/
npm-debug.log
coverage/
yarn-error.log
.DS_STORE
cjs/
esm/
dist/
.cache
/addons
.nyc_output
cypress/videos/*
cypress/screenshots/*
.husky
================================================
FILE: .nycrc.json
================================================
{
"all": true,
"extends": "@istanbuljs/nyc-config-typescript",
"check-coverage": true,
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"cypress/**/*.*",
"src/types.ts",
"**/*.d.ts",
"**/*.cy.tsx",
"**/*.cy.ts"
]
}
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at fdkhadra@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
When contributing to this repository, please first discuss the change you wish to make via issue before making a change.
Please note we have a code of conduct, please follow it in all your interactions with the project.
## General Guidelines
- Before starting to work on something, please open an issue first
- If adding a new feature, write the corresponding test
- Ensure that nothing get broke. You can use the playground for that
- If applicable, update the [documentation](https://github.com/fkhadra/react-toastify-doc)
- Use prettier before committing 😭
- When solving a bug, please provide the steps to reproduce it(codesandbox or stackblitz are our best friends for that)
- Tchill 👌
## Setup
### Pre-requisites
- *Node:* `^18.0.0`
- *Yarn*
### Install
Clone the repository and create a local branch:
```sh
git clone https://github.com/fkhadra/react-toastify.git
cd react-toastify
git checkout -b my-branch
```
Install dependencies:
```sh
pnpm install
// then
pnpm setup
```
## Developing
```sh
# launch the playground
pnpm start
# Run tests 💩
pnpm test
# Prettify all the things
pnpm prettier
```
### Playground dir
The playground let you test your changes, it's like the demo of react-toastify. Most of the time you don't need to modify it unless you add new features.
### Src
- [toast:](https://github.com/fkhadra/react-toastify/blob/main/src/core/toast.ts) Contain the exposed api (`toast.success...`).
## License
By contributing, you agree that your contributions will be licensed under its [MIT License](https://github.com/fkhadra/react-toastify/blob/main/LICENSE).
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023 Fadi Khadra
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# React-Toastify
[](https://opencollective.com/react-toastify) 







🎉 React-Toastify allows you to add notifications to your app with ease.
## Installation
```
$ npm install --save react-toastify
$ yarn add react-toastify
```
```jsx
import React from 'react';
import { ToastContainer, toast } from 'react-toastify';
function App(){
const notify = () => toast("Wow so easy!");
return (
<div>
<button onClick={notify}>Notify!</button>
<ToastContainer />
</div>
);
}
```
## Documentation
Check the [documentation](https://fkhadra.github.io/react-toastify/introduction) to get you started!
## Features
- Easy to set up for real, you can make it work in less than 10sec!
- Super easy to customize
- RTL support
- Swipe to close 👌
- Can choose swipe direction
- Super easy to use an animation of your choice. Works well with animate.css for example
- Can display a react component inside the toast!
- Has ```onOpen``` and ```onClose``` hooks. Both can access the props passed to the react component rendered inside the toast
- Can remove a toast programmatically
- Define behavior per toast
- Pause toast when the window loses focus 👁
- Fancy progress bar to display the remaining time
- Possibility to update a toast
- You can control the progress bar a la `nprogress` 😲
- You can limit the number of toast displayed at the same time
- Dark mode 🌒
- Pause timer programmaticaly
- Stacked notifications!
- And much more !
## Demo
[A demo is worth a thousand words](https://fkhadra.github.io/react-toastify/introduction)
## Contribute
Show your ❤️ and support by giving a ⭐. Any suggestions are welcome! Take a look at the contributing guide.
You can also find me on [reactiflux](https://www.reactiflux.com/). My pseudo is Fadi.
## Contributors
### Code Contributors
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
<a href="https://github.com/fkhadra/react-toastify/graphs/contributors"><img src="https://opencollective.com/react-toastify/contributors.svg?width=890&button=false" /></a>
### Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/react-toastify/contribute)]
#### Individuals
<a href="https://opencollective.com/react-toastify"><img src="https://opencollective.com/react-toastify/individuals.svg?width=890"></a>
#### Organizations
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/react-toastify/contribute)]
<a href="https://opencollective.com/react-toastify/organization/0/website"><img src="https://opencollective.com/react-toastify/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/react-toastify/organization/1/website"><img src="https://opencollective.com/react-toastify/organization/1/avatar.svg"></a>
<a href="https://opencollective.com/react-toastify/organization/2/website"><img src="https://opencollective.com/react-toastify/organization/2/avatar.svg"></a>
<a href="https://opencollective.com/react-toastify/organization/3/website"><img src="https://opencollective.com/react-toastify/organization/3/avatar.svg"></a>
<a href="https://opencollective.com/react-toastify/organization/4/website"><img src="https://opencollective.com/react-toastify/organization/4/avatar.svg"></a>
<a href="https://opencollective.com/react-toastify/organization/5/website"><img src="https://opencollective.com/react-toastify/organization/5/avatar.svg"></a>
<a href="https://opencollective.com/react-toastify/organization/6/website"><img src="https://opencollective.com/react-toastify/organization/6/avatar.svg"></a>
<a href="https://opencollective.com/react-toastify/organization/7/website"><img src="https://opencollective.com/react-toastify/organization/7/avatar.svg"></a>
<a href="https://opencollective.com/react-toastify/organization/8/website"><img src="https://opencollective.com/react-toastify/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/react-toastify/organization/9/website"><img src="https://opencollective.com/react-toastify/organization/9/avatar.svg"></a>
## Release Notes
You can find the release note for the latest release [here](https://github.com/fkhadra/react-toastify/releases/latest)
You can browse them all [here](https://github.com/fkhadra/react-toastify/releases)
## License
Licensed under MIT
================================================
FILE: cypress/support/commands.ts
================================================
/// <reference types="cypress" />
/// <reference types="@testing-library/cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
declare global {
namespace Cypress {
interface Chainable {
resolveEntranceAnimation(): void;
}
}
}
import '@4tw/cypress-drag-drop';
import '@testing-library/cypress/add-commands';
Cypress.Commands.add('resolveEntranceAnimation', () => {
cy.wait(800);
});
================================================
FILE: cypress/support/component-index.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Components App</title>
</head>
<body>
<div data-cy-root></div>
</body>
</html>
================================================
FILE: cypress/support/component.ts
================================================
// ***********************************************************
// This example support/component.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
// cypress/support/e2e.js
import '@cypress/code-coverage/support';
import './commands';
import './style.css';
import '../../src/style.css';
// Alternatively you can use CommonJS syntax:
// require('./commands')
import { mount } from 'cypress/react18';
// Augment the Cypress namespace to include type definitions for
// your custom command.
// Alternatively, can be defined in cypress/support/component.d.ts
// with a <reference path="./component" /> at the top of your spec.
declare global {
namespace Cypress {
interface Chainable {
mount: typeof mount;
}
}
}
Cypress.Commands.add('mount', mount);
// Example use:
// cy.mount(<MyComponent />)
================================================
FILE: cypress/support/style.css
================================================
[data-cy-root]{
height: 80vh;
}
================================================
FILE: cypress.config.ts
================================================
import { defineConfig } from 'cypress';
export default defineConfig({
component: {
setupNodeEvents(on, config) {
require('@cypress/code-coverage/task')(on, config);
return config;
},
devServer: {
framework: 'react',
bundler: 'vite'
}
}
});
================================================
FILE: lefthook.yml
================================================
pre-commit:
parallel: true
commands:
lint-staged:
glob: "*.{js,ts,jsx,tsx,css}"
run: pnpm lint-staged
================================================
FILE: package.json
================================================
{
"version": "11.0.5",
"license": "MIT",
"description": "React notification made easy",
"keywords": [
"react",
"notification",
"toast",
"react-component",
"react-toastify",
"push",
"alert",
"snackbar",
"message"
],
"files": [
"dist",
"addons"
],
"scripts": {
"prepare": "lefthook install",
"setup": "pnpm link .",
"start": "cd playground && pnpm dev",
"test": "cypress open --component",
"test:run": "cypress run --component -b chrome",
"prettier": "prettier --write src",
"build": "tsup && cp src/style.css dist/ReactToastify.css && rm dist/unstyled.css*"
},
"peerDependencies": {
"react": "^18 || ^19",
"react-dom": "^18 || ^19"
},
"prettier": {
"printWidth": 120,
"semi": true,
"singleQuote": true,
"trailingComma": "none",
"arrowParens": "avoid"
},
"name": "react-toastify",
"repository": {
"type": "git",
"url": "git+https://github.com/fkhadra/react-toastify.git"
},
"author": "Fadi Khadra <fdkhadra@gmail.com> (https://fkhadra.github.io)",
"bugs": {
"url": "https://github.com/fkhadra/react-toastify/issues"
},
"homepage": "https://github.com/fkhadra/react-toastify#readme",
"devDependencies": {
"@4tw/cypress-drag-drop": "^2.2.5",
"@cypress/code-coverage": "^3.13.9",
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@testing-library/cypress": "^10.0.2",
"@types/node": "^22.10.2",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"@vitejs/plugin-react": "^4.3.4",
"coveralls": "^3.1.1",
"cypress": "^13.16.1",
"lefthook": "^1.9.2",
"lint-staged": "^15.2.11",
"postcss": "^8.4.49",
"prettier": "3.4.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tsup": "^8.3.5",
"typescript": "^5.7.2",
"vite": "^6.0.3",
"vite-plugin-istanbul": "^6.0.2"
},
"dependencies": {
"clsx": "^2.1.1"
},
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"module": "dist/index.mjs",
"source": "src/index.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
},
"./unstyled": {
"types": "./dist/unstyled.d.ts",
"import": "./dist/unstyled.mjs",
"require": "./dist/unstyled.js"
},
"./dist/ReactToastify.css": "./dist/ReactToastify.css",
"./ReactToastify.css": "./dist/ReactToastify.css",
"./package.json": "./package.json",
"./addons/use-notification-center": {
"types": "./addons/use-notification-center/index.d.ts",
"import": "./addons/use-notification-center/index.mjs",
"require": "./addons/use-notification-center/index.js"
},
"./notification-center": {
"types": "./addons/use-notification-center/index.d.ts",
"import": "./addons/use-notification-center/index.mjs",
"require": "./addons/use-notification-center/index.js"
}
},
"lint-staged": {
"*.{js,jsx,ts,tsx,md,html,css}": "prettier --write"
}
}
================================================
FILE: playground/.eslintrc.cjs
================================================
module.exports = {
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': 'warn',
},
}
================================================
FILE: playground/.gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
================================================
FILE: playground/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
================================================
FILE: playground/package.json
================================================
{
"name": "playground",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.7.2",
"vite": "^6.0.1"
}
}
================================================
FILE: playground/src/components/App.tsx
================================================
/**
* The playground could use some love 💖. To the brave soul reading this
* message, any help would be appreciated 🙏
*
* The code is full of bad assertion 😆
*/
import { Checkbox } from './Checkbox';
import { ContainerCode, ContainerCodeProps } from './ContainerCode';
import { Header } from './Header';
import { Radio } from './Radio';
import { ToastCode, ToastCodeProps } from './ToastCode';
import { flags, positions, themes, transitions, typs } from './constants';
import React from 'react';
import { Id, toast, ToastContainer } from '../../../src';
import { defaultProps } from '../../../src/components/ToastContainer';
// Attach to window. Can be useful to debug
// @ts-ignore
window.toast = toast;
class App extends React.Component {
state = App.getDefaultState();
toastId: Id;
resolvePromise = true;
static getDefaultState() {
return {
...defaultProps,
transition: 'bounce',
type: 'default',
progress: '',
disableAutoClose: false,
limit: 0,
theme: 'light'
};
}
handleReset = () =>
this.setState({
...App.getDefaultState()
});
clearAll = () => toast.dismiss();
showToast = () => {
this.toastId =
this.state.type === 'default'
? toast('🦄 Wow so easy !', { progress: this.state.progress })
: toast[this.state.type]('🚀 Wow so easy !', {
progress: this.state.progress
});
};
firePromise = () => {
toast.promise(
new Promise((resolve, reject) => {
setTimeout(() => {
this.resolvePromise ? resolve(null) : reject(null);
this.resolvePromise = !this.resolvePromise;
}, 3000);
}),
{
pending: 'Promise is pending',
success: 'Promise resolved 👌',
error: 'Promise rejected 🤯'
}
);
};
updateToast = () => toast.update(this.toastId, { progress: this.state.progress });
handleAutoCloseDelay = e =>
this.setState({
autoClose: e.target.value > 0 ? parseInt(e.target.value, 10) : 1
});
isDefaultProps() {
return (
this.state.position === 'top-right' &&
this.state.autoClose === 5000 &&
!this.state.disableAutoClose &&
!this.state.hideProgressBar &&
!this.state.newestOnTop &&
!this.state.rtl &&
this.state.pauseOnFocusLoss &&
this.state.pauseOnHover &&
this.state.closeOnClick &&
this.state.draggable &&
this.state.theme === 'light'
);
}
handleRadioOrSelect = e =>
this.setState({
[e.target.name]: e.target.name === 'limit' ? parseInt(e.target.value, 10) : e.target.value
});
toggleCheckbox = e =>
this.setState({
[e.target.name]: !this.state[e.target.name]
});
renderFlags() {
return flags.map(({ id, label }) => (
<li key={id}>
<Checkbox id={id} label={label} onChange={this.toggleCheckbox} checked={this.state[id]} />
</li>
));
}
render() {
return (
<main>
<Header />
<div className="container">
<p>
By default, all toasts will inherit ToastContainer's props. Props defined on toast supersede
ToastContainer's props. Props marked with * can only be set on the ToastContainer. The demo is not
exhaustive, check the repo for more!
</p>
<section className="container__options">
<div>
<h3>Position</h3>
<ul>
<Radio
options={positions}
name="position"
checked={this.state.position as string}
onChange={this.handleRadioOrSelect}
/>
</ul>
</div>
<div>
<h3>Type</h3>
<ul>
<Radio options={typs} name="type" checked={this.state.type} onChange={this.handleRadioOrSelect} />
</ul>
</div>
<div>
<h3>Options</h3>
<div className="options_wrapper">
<label htmlFor="autoClose">
Delay
<input
type="number"
name="autoClose"
id="autoClose"
value={this.state.autoClose as unknown as string}
onChange={this.handleAutoCloseDelay}
disabled={this.state.disableAutoClose}
/>
ms
</label>
<label htmlFor="transition">
Transition
<select
name="transition"
id="transition"
onChange={this.handleRadioOrSelect}
value={this.state.transition}
>
{Object.keys(transitions).map(k => (
<option key={k} value={k}>
{k}
</option>
))}
</select>
</label>
<label htmlFor="theme">
Theme
<select name="theme" id="theme" onChange={this.handleRadioOrSelect} value={this.state.theme}>
{themes.map(k => (
<option key={k} value={k}>
{k}
</option>
))}
</select>
</label>
<label htmlFor="progress">
Progress
<input
type="number"
name="progress"
id="progress"
value={this.state.progress}
onChange={this.handleRadioOrSelect}
/>
</label>
<label htmlFor="limit">
Limit
<input
type="number"
name="limit"
id="limit"
value={this.state.limit}
onChange={this.handleRadioOrSelect}
/>
</label>
</div>
<ul>{this.renderFlags()}</ul>
</div>
</section>
<section>
<ContainerCode
{...(this.state as unknown as ContainerCodeProps)}
isDefaultProps={this.isDefaultProps() as boolean}
/>
<ToastCode {...(this.state as unknown as ToastCodeProps)} />
</section>
<div className="cta__wrapper">
<ul className="container__actions">
<li>
<button className="btn" onClick={this.showToast}>
<span role="img" aria-label="show alert">
🚀
</span>{' '}
Show Toast
</button>
</li>
<li>
<button className="btn" onClick={this.firePromise}>
Promise
</button>
</li>
<li>
<button className="btn" onClick={this.updateToast}>
Update
</button>
</li>
<li>
<button className="btn bg-red" onClick={this.clearAll}>
<span role="img" aria-label="clear all">
💩
</span>{' '}
Clear All
</button>
</li>
<li>
<button className="btn bg-blue" onClick={this.handleReset}>
<span role="img" aria-label="reset options">
🔄
</span>{' '}
Reset
</button>
</li>
</ul>
</div>
</div>
<ToastContainer
{...this.state}
transition={transitions[this.state.transition]}
autoClose={this.state.disableAutoClose ? false : this.state.autoClose}
/>
<ToastContainer containerId="xxx" position="top-left" autoClose={false} theme="dark" limit={3} />
<ToastContainer limit={3} containerId="yyy" autoClose={false} position="top-right" />
</main>
);
}
}
export { App };
================================================
FILE: playground/src/components/Checkbox.tsx
================================================
import * as React from 'react';
interface CheckboxProps {
label: string;
id: string;
checked: boolean;
onChange: (e: React.ChangeEvent) => void;
}
export const Checkbox = ({ label, onChange, id, checked }: CheckboxProps) => (
<label htmlFor={id}>
<input
id={id}
type="checkbox"
name={id}
checked={checked}
onChange={onChange}
/>
{label}
</label>
);
================================================
FILE: playground/src/components/ContainerCode.tsx
================================================
import * as React from 'react';
import { ToastContainerProps } from '../../../src';
function getProp<L, R>(prop: L, value: R) {
return value ? (
<div>
<span className="code__props">{prop}</span>
</div>
) : (
<div>
<span className="code__props">{prop}</span>
{`={false}`}
</div>
);
}
export interface ContainerCodeProps extends Partial<ToastContainerProps> {
isDefaultProps: boolean;
disableAutoClose: boolean;
}
export const ContainerCode: React.FC<ContainerCodeProps> = ({
position,
disableAutoClose,
autoClose,
hideProgressBar,
newestOnTop,
closeOnClick,
pauseOnHover,
rtl,
pauseOnFocusLoss,
isDefaultProps,
draggable,
theme
}) => (
<div>
<h3>Toast Container</h3>
<div className="code">
<div>
<span>{`<`}</span>
<span className="code__component">ToastContainer</span>
</div>
<div>
<span className="code__props">position</span>
{`="${position}"`}
</div>
<div>
<span className="code__props">theme</span>
{`="${theme}"`}
</div>
<div>
<span className="code__props">autoClose</span>
{`={${disableAutoClose ? false : autoClose}}`}
</div>
{!disableAutoClose ? getProp('hideProgressBar', hideProgressBar) : ''}
{getProp('newestOnTop', newestOnTop)}
{getProp('closeOnClick', closeOnClick)}
{getProp('rtl', rtl)}
{getProp('pauseOnFocusLoss', pauseOnFocusLoss)}
{getProp('draggable', draggable)}
{!disableAutoClose ? getProp('pauseOnHover', pauseOnHover) : ''}
<div>
<span>{`/>`}</span>
</div>
{isDefaultProps && (
<div>
<div>{`{/* Same as */}`}</div>
<span>{`<`}</span>
<span className="code__component">ToastContainer</span>
<span>{'/>'}</span>
</div>
)}
</div>
</div>
);
================================================
FILE: playground/src/components/Header.tsx
================================================
import * as React from 'react';
export const Header = () => (
<header>
<a
href="https://github.com/fkhadra/react-toastify"
className="github-corner"
aria-label="View source on Github"
>
<svg
width="80"
height="80"
viewBox="0 0 250 250"
style={{
fill: '#FD6C6C',
color: '#fff',
position: 'absolute',
top: 0,
border: 0,
left: 0,
transform: 'scale(-1, 1)'
}}
aria-hidden="true"
>
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z" />
<path
d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
fill="currentColor"
style={{ transformOrigin: '130px 106px' }}
className="octo-arm"
/>
<path
d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
fill="currentColor"
className="octo-body"
/>
</svg>
</a>
<h1>Welcome to React-toastify</h1>
<h5>React notification made easy !</h5>
<div>
<a className="btn" href="https://fkhadra.github.io/react-toastify">
<span role="img" aria-label="link to github">
📖
</span>{' '}
Documentation
</a>
<a
className="btn"
href="https://github.com/fkhadra/react-toastify/stargazers"
>
<span role="img" aria-label="link to github">
⭐️
</span>{' '}
Become a stargazer
</a>
</div>
</header>
);
================================================
FILE: playground/src/components/Radio.tsx
================================================
import * as React from 'react';
interface RadioProps {
options: Record<string, string>;
name: string;
onChange: (e: React.ChangeEvent) => void;
checked: string | boolean;
}
export const Radio = ({
options,
name,
onChange,
checked = false
}: RadioProps) => (
<>
{Object.keys(options).map(k => {
const option = options[k];
return (
<li key={`${name}-${option}`}>
<label htmlFor={option}>
<input
id={option}
type="radio"
name={name}
value={option}
checked={option === checked}
onChange={onChange}
/>
{option}
</label>
</li>
);
})}
</>
);
================================================
FILE: playground/src/components/ToastCode.tsx
================================================
import * as React from 'react';
import { themes } from './constants';
function getType(type: string) {
switch (type) {
case 'default':
default:
return 'toast';
case 'success':
return 'toast.success';
case 'error':
return 'toast.error';
case 'info':
return 'toast.info';
case 'warning':
return 'toast.warn';
}
}
export interface ToastCodeProps {
position: string;
disableAutoClose: boolean;
autoClose: boolean | number;
hideProgressBar: boolean;
closeOnClick: boolean;
pauseOnHover: boolean;
type: string;
draggable: boolean;
progress: number;
theme: typeof themes[number];
}
export const ToastCode: React.FC<ToastCodeProps> = ({
position,
disableAutoClose,
autoClose,
hideProgressBar,
closeOnClick,
pauseOnHover,
type,
draggable,
progress,
theme
}) => (
<div>
<h3>Toast Emitter</h3>
<div className="code">
<div>
<span className="code__component">{getType(type)}</span>
{`('🦄 Wow so easy!', { `}
</div>
<div>
<span className="code__props">position</span>
{`: "${position}"`},
</div>
<div>
<span className="code__props">theme</span>
{`: "${theme}"`},
</div>
<div>
<span className="code__props">autoClose</span>
{`: ${disableAutoClose ? false : autoClose}`},
</div>
<div>
<span className="code__props">hideProgressBar</span>
{`: ${hideProgressBar ? 'true' : 'false'}`},
</div>
<div>
<span className="code__props">closeOnClick</span>
{`: ${closeOnClick ? 'true' : 'false'}`},
</div>
<div>
<span className="code__props">pauseOnHover</span>
{`: ${pauseOnHover ? 'true' : 'false'}`},
</div>
<div>
<span className="code__props">draggable</span>
{`: ${draggable ? 'true' : 'false'}`},
</div>
{!Number.isNaN(progress) && (
<div>
<span className="code__props">progress</span>
{`: ${progress}`},
</div>
)}
<div>{`});`}</div>
</div>
</div>
);
================================================
FILE: playground/src/components/constants.ts
================================================
import { Bounce, Slide, Flip, Zoom } from '../../../src/index';
export const flags = [
{
id: 'disableAutoClose',
label: 'Disable auto-close'
},
{
id: 'hideProgressBar',
label: 'Hide progress bar(less fanciness!)'
},
{
id: 'newestOnTop',
label: 'Newest on top*'
},
{
id: 'closeOnClick',
label: 'Close on click'
},
{
id: 'pauseOnHover',
label: 'Pause delay on hover'
},
{
id: 'pauseOnFocusLoss',
label: 'Pause toast when the window loses focus'
},
{
id: 'rtl',
label: 'Right to left layout*'
},
{
id: 'draggable',
label: 'Allow to drag and close the toast'
}
];
export const transitions = {
bounce: Bounce,
slide: Slide,
zoom: Zoom,
flip: Flip
};
export const themes = <const>['light', 'dark', 'colored'];
export const positions = {
TOP_LEFT: 'top-left',
TOP_RIGHT: 'top-right',
TOP_CENTER: 'top-center',
BOTTOM_LEFT: 'bottom-left',
BOTTOM_RIGHT: 'bottom-right',
BOTTOM_CENTER: 'bottom-center'
};
export const typs = {
INFO: 'info',
SUCCESS: 'success',
WARNING: 'warning',
ERROR: 'error',
DEFAULT: 'default'
};
================================================
FILE: playground/src/index.css
================================================
@import url(https://fonts.googleapis.com/css?family=Titillium+Web);
body {
margin: 0;
padding: 0;
font-family: 'Titillium Web', sans-serif;
min-height: 100vh;
background: linear-gradient(110deg, #1d4350, #a43931);
color: #fff;
}
* {
box-sizing: border-box;
}
main {
display: grid;
grid-template-rows: auto 1fr;
grid-gap: 20px;
}
header {
background: #222;
text-align: center;
padding: 30px 0;
}
h3 {
color: #fff;
}
header h1 {
margin-top: 0;
}
ul {
list-style: none;
padding: 0;
}
input[type='number'] {
padding: 8px;
background-color: transparent;
box-shadow: none;
border: 1px solid;
margin: 0 5px;
border-radius: 5px;
border-color: #ac557b;
color: #fff;
width: 100px;
}
input[type='radio'] {
margin-right: 8px;
}
select {
padding: 8px;
padding: 8px;
background-color: transparent;
box-shadow: none;
border: 1px solid;
border-top-color: currentcolor;
border-right-color: currentcolor;
border-bottom-color: currentcolor;
border-left-color: currentcolor;
border-top-color: currentcolor;
border-right-color: currentcolor;
border-bottom-color: currentcolor;
border-left-color: currentcolor;
margin: 0 5px;
border-radius: 5px;
border-color: #ac557b;
color: #fff;
-webkit-appearance: none;
-moz-appearance: none;
}
.container {
max-width: 1080px;
margin: auto;
width: 100%;
background: rgba(255, 255, 255, 0.1);
padding: 20px;
border-radius: 10px;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.container p {
grid-column: 1 / -1;
font-size: 13px;
font-style: italic;
background: #222;
padding: 5px;
border-left: 3px solid #a9547e;
}
.container__options {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
.container__options div:last-child {
grid-column: 1 / -1;
}
.container__actions {
display: flex;
}
.cta__wrapper {
grid-column: span 2;
}
.btn {
color: #fff;
text-decoration: none;
padding: 8px 16px;
margin: 0 15px 0 0;
background: linear-gradient(100deg, #e96443, #904e95);
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12),
0 3px 1px -2px rgba(0, 0, 0, 0.2);
border: none;
text-transform: capitalize;
cursor: pointer;
transition: transform 0.3s;
min-width: 120px;
}
.btn:hover {
transform: scale(1.1);
}
.bg-red {
background: #d13c3c;
}
.bg-blue {
background: #3b4149;
}
.code {
font-family: 'Source Code Pro', Menlo, Monaco, Courier, monospace;
font-size: 12px;
line-height: 1.4;
font-style: normal;
border-left: 3px solid #a9547e;
padding-left: 20px;
background: #222;
}
.code__component {
color: #66d9ef;
}
.code__props {
color: #a6e22e;
}
.code div {
margin-left: 20px;
}
.code div:first-child,
.code div:last-child {
margin: 0;
}
.github-corner:hover .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
.options_wrapper {
display: grid;
grid-template-columns: repeat(3, auto);
gap: 18px;
}
@keyframes octocat-wave {
0%,
100% {
transform: rotate(0);
}
20%,
60% {
transform: rotate(-25deg);
}
40%,
80% {
transform: rotate(10deg);
}
}
@media (max-width: 500px) {
.github-corner:hover .octo-arm {
animation: none;
}
.github-corner .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
}
================================================
FILE: playground/src/main.tsx
================================================
import React from 'react';
import ReactDOM from 'react-dom/client';
import { App } from './components/App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
================================================
FILE: playground/src/vite-env.d.ts
================================================
/// <reference types="vite/client" />
================================================
FILE: playground/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
================================================
FILE: playground/tsconfig.node.json
================================================
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
================================================
FILE: playground/vite.config.ts
================================================
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
================================================
FILE: src/addons/use-notification-center/NotificationCenter.cy.tsx
================================================
import React from 'react';
import { toast, ToastContainer } from 'react-toastify';
import { NotificationCenterItem, useNotificationCenter, UseNotificationCenterParams } from './useNotificationCenter';
function TestComponent(props: UseNotificationCenterParams) {
const [content, setContent] = React.useState('');
const [updateId, setUpdateId] = React.useState('');
const { unreadCount, markAllAsRead, markAsRead, notifications, remove, add, clear, update } = useNotificationCenter(
props || {}
);
const flex = {
display: 'flex',
gap: '1rem',
alignItems: 'center'
};
return (
<div>
<div style={flex}>
<button onClick={() => toast('hello')}>display notification</button>
<button onClick={markAllAsRead}>markAllAsRead</button>
<button onClick={clear}>clear</button>
<button onClick={() => add({ content })}>addNotification</button>
<button onClick={() => update(updateId, { content })}>updateNotification</button>
</div>
<ul>
<li>
<span>count</span>
<span data-testid="count">{notifications.length}</span>
</li>
<li>
<span>unread count</span>
<span data-testid="unreadCount">{unreadCount}</span>
</li>
</ul>
<input data-testid="content" type="text" onChange={e => setContent(e.target.value)} value={content} />
<input data-testid="updateId" type="text" onChange={e => setUpdateId(e.target.value)} value={updateId} />
<ul data-testid="notifications">
{notifications.map(el => (
<li key={el.id} style={flex}>
{/* @ts-ignore */}
<span data-testid={`content-${el.id}`}>{el.content}</span>
<span data-testid={`read-${el.id}`}>{el.read.toString()}</span>
<button data-testid={`markAsRead-${el.id}`} onClick={() => markAsRead(el.id)}>
markAsRead
</button>
<button data-testid={`remove-${el.id}`} onClick={() => remove(el.id)}>
remove
</button>
</li>
))}
</ul>
<ToastContainer />
</div>
);
}
describe('NotificationCenter', () => {
beforeEach(() => {
cy.mount(<TestComponent />);
});
it('listen for new notifications', () => {
cy.findByTestId('count').should('contain.text', 0);
cy.findByTestId('unreadCount').should('contain.text', 0);
// hacky asf???
cy.wait(1000).then(() => {
toast('msg');
cy.findByTestId('count').should('contain.text', 1, { timeout: 10000 });
cy.findByTestId('unreadCount').should('contain.text', 1);
});
});
it('add notification', () => {
cy.findByTestId('count').should('contain.text', 0);
cy.findByTestId('unreadCount').should('contain.text', 0);
cy.findByTestId('content').type('something');
cy.findByText('addNotification').click();
cy.findByText('something').should('exist');
cy.findByTestId('count').should('contain.text', 1);
cy.findByTestId('unreadCount').should('contain.text', 1);
});
it('update', () => {
const id = toast('msg');
cy.resolveEntranceAnimation();
cy.findByRole('alert').should('exist');
setTimeout(() => {
toast.update(id, {
render: 'msg updated'
});
}, 0);
cy.findAllByText('msg updated').should('exist');
});
describe('with initial state', () => {
const initialState: NotificationCenterItem[] = [
{
id: 1,
createdAt: Date.now(),
read: false,
content: 'noti1'
},
{
id: 2,
createdAt: Date.now(),
read: true,
content: 'noti2'
}
];
beforeEach(() => {
cy.mount(<TestComponent data={initialState} />);
});
it('handle initial state', () => {
cy.findByTestId('count').should('contain.text', initialState.length);
cy.findByTestId('unreadCount').should('contain.text', 1);
initialState.forEach(v => {
cy.findByText(v.content as string).should('exist');
});
});
it('clear all', () => {
cy.findByTestId('count').should('contain.text', initialState.length);
cy.findByTestId('unreadCount').should('contain.text', 1);
cy.findByText('clear').click();
cy.findByTestId('count').should('contain.text', 0);
cy.findByTestId('unreadCount').should('contain.text', 0);
});
it('mark all as read', () => {
cy.findByTestId('unreadCount').should('contain.text', 1);
cy.findByText('markAllAsRead').click();
cy.findByTestId('unreadCount').should('contain.text', 0);
});
});
});
================================================
FILE: src/addons/use-notification-center/index.ts
================================================
export * from './useNotificationCenter';
================================================
FILE: src/addons/use-notification-center/useNotificationCenter.ts
================================================
import { useState, useEffect, useRef } from 'react';
import { toast, ToastItem, Id } from 'react-toastify';
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
export interface NotificationCenterItem<Data = {}> extends Optional<ToastItem<Data>, 'content' | 'data' | 'status'> {
read: boolean;
createdAt: number;
}
export type SortFn<Data> = (l: NotificationCenterItem<Data>, r: NotificationCenterItem<Data>) => number;
export type FilterFn<Data = {}> = (item: NotificationCenterItem<Data>) => boolean;
export interface UseNotificationCenterParams<Data = {}> {
/**
* initial data to rehydrate the notification center
*/
data?: NotificationCenterItem<Data>[];
/**
* By default, the notifications are sorted from the newest to the oldest using
* the `createdAt` field. Use this to provide your own sort function
*
* Usage:
* ```
* // old notifications first
* useNotificationCenter({
* sort: ((l, r) => l.createdAt - r.createdAt)
* })
* ```
*/
sort?: SortFn<Data>;
/**
* Keep the toast that meets the condition specified in the callback function.
*
* Usage:
* ```
* // keep only the toasts when hidden is set to false
* useNotificationCenter({
* filter: item => item.data.hidden === false
* })
* ```
*/
filter?: FilterFn<Data>;
}
export interface UseNotificationCenter<Data> {
/**
* Contains all the notifications
*/
notifications: NotificationCenterItem<Data>[];
/**
* Clear all notifications
*/
clear(): void;
/**
* Mark all notification as read
*/
markAllAsRead(): void;
/**
* Mark all notification as read or not.
*
* Usage:
* ```
* markAllAsRead(false) // mark all notification as not read
*
* markAllAsRead(true) // same as calling markAllAsRead()
* ```
*/
markAllAsRead(read?: boolean): void;
/**
* Mark one or more notifications as read.
*
* Usage:
* ```
* markAsRead("anId")
* markAsRead(["a","list", "of", "id"])
* ```
*/
markAsRead(id: Id | Id[]): void;
/**
* Mark one or more notifications as read.The second parameter let you mark the notification as read or not.
*
* Usage:
* ```
* markAsRead("anId", false)
* markAsRead(["a","list", "of", "id"], false)
*
* markAsRead("anId", true) // same as markAsRead("anId")
* ```
*/
markAsRead(id: Id | Id[], read?: boolean): void;
/**
* Remove one or more notifications
*
* Usage:
* ```
* remove("anId")
* remove(["a","list", "of", "id"])
* ```
*/
remove(id: Id | Id[]): void;
/**
* Push a notification to the notification center.
* Returns null when an item with the given id already exists
*
* Usage:
* ```
* const id = add({id: "id", content: "test", data: { foo: "hello" } })
*
* // Return the id of the notification, generate one if none provided
* const id = add({ data: {title: "a title", text: "some text"} })
* ```
*/
add(item: Partial<NotificationCenterItem<Data>>): Id | null;
/**
* Update the notification that match the id
* Returns null when no matching notification found
*
* Usage:
* ```
* const id = update("anId", {content: "test", data: { foo: "hello" } })
*
* // It's also possible to update the id
* const id = update("anId"m { id:"anotherOne", data: {title: "a title", text: "some text"} })
* ```
*/
update(id: Id, item: Partial<NotificationCenterItem<Data>>): Id | null;
/**
* Retrieve one or more notifications
*
* Usage:
* ```
* find("anId")
* find(["a","list", "of", "id"])
* ```
*/
find(id: Id): NotificationCenterItem<Data> | undefined;
/**
* Retrieve one or more notifications
*
* Usage:
* ```
* find("anId")
* find(["a","list", "of", "id"])
* ```
*/
find(id: Id[]): NotificationCenterItem<Data>[] | undefined;
/**
* Retrieve the count for unread notifications
*/
unreadCount: number;
/**
* Sort notifications using the newly provided function
*
* Usage:
* ```
* // old notifications first
* sort((l, r) => l.createdAt - r.createdAt)
* ```
*/
sort(sort: SortFn<Data>): void;
}
export function useNotificationCenter<Data = {}>(
params: UseNotificationCenterParams<Data> = {}
): UseNotificationCenter<Data> {
const sortFn = useRef(params.sort || defaultSort);
const filterFn = useRef(params.filter || null);
const [notifications, setNotifications] = useState<NotificationCenterItem<Data>[]>(() => {
if (params.data) {
return filterFn.current
? params.data.filter(filterFn.current).sort(sortFn.current)
: [...params.data].sort(sortFn.current);
}
return [];
});
useEffect(() => {
return toast.onChange(item => {
if (item.status === 'added' || item.status === 'updated') {
const newItem = decorate(item as NotificationCenterItem<Data>);
if (filterFn.current && !filterFn.current(newItem)) return;
setNotifications(prev => {
let nextState: NotificationCenterItem<Data>[] = [];
const updateIdx = prev.findIndex(v => v.id === newItem.id);
if (updateIdx !== -1) {
nextState = prev.slice();
Object.assign(nextState[updateIdx], newItem, {
createdAt: Date.now()
});
} else if (prev.length === 0) {
nextState = [newItem];
} else {
nextState = [newItem, ...prev];
}
return nextState.sort(sortFn.current);
});
}
});
}, []);
const remove = (id: Id | Id[]) => {
setNotifications(prev => prev.filter(Array.isArray(id) ? v => !id.includes(v.id) : v => v.id !== id));
};
const clear = () => {
setNotifications([]);
};
const markAllAsRead = (read = true) => {
setNotifications(prev =>
prev.map(v => {
v.read = read;
return v;
})
);
};
const markAsRead = (id: Id | Id[], read = true) => {
let map = (v: NotificationCenterItem<Data>) => {
if (v.id === id) v.read = read;
return v;
};
if (Array.isArray(id)) {
map = v => {
if (id.includes(v.id)) v.read = read;
return v;
};
}
setNotifications(prev => prev.map(map));
};
const find = (id: Id | Id[]) => {
return Array.isArray(id) ? notifications.filter(v => id.includes(v.id)) : notifications.find(v => v.id === id);
};
const add = (item: Partial<NotificationCenterItem<Data>>) => {
if (notifications.find(v => v.id === item.id)) return null;
const newItem = decorate(item);
setNotifications(prev => [...prev, newItem].sort(sortFn.current));
return newItem.id;
};
const update = (id: Id, item: Partial<NotificationCenterItem<Data>>) => {
const index = notifications.findIndex(v => v.id === id);
if (index !== -1) {
setNotifications(prev => {
const nextState = [...prev];
Object.assign(nextState[index], item, {
createdAt: item.createdAt || Date.now()
});
return nextState.sort(sortFn.current);
});
return item.id as Id;
}
return null;
};
const sort = (compareFn: SortFn<Data>) => {
sortFn.current = compareFn;
setNotifications(prev => prev.slice().sort(compareFn));
};
return {
notifications,
clear,
markAllAsRead,
markAsRead,
add,
update,
remove,
// @ts-ignore fixme: overloading issue
find,
sort,
get unreadCount() {
return notifications.reduce((prev, cur) => (!cur.read ? prev + 1 : prev), 0);
}
};
}
export function decorate<Data>(item: NotificationCenterItem<Data> | Partial<NotificationCenterItem<Data>>) {
if (item.id == null) item.id = Date.now().toString(36).substring(2, 9);
if (!item.createdAt) item.createdAt = Date.now();
if (item.read == null) item.read = false;
return item as NotificationCenterItem<Data>;
}
// newest to oldest
function defaultSort<Data>(l: NotificationCenterItem<Data>, r: NotificationCenterItem<Data>) {
return r.createdAt - l.createdAt;
}
================================================
FILE: src/components/CloseButton.cy.tsx
================================================
import React from 'react';
import { CloseButton } from './CloseButton';
describe('CloseButton', () => {
it('call close toast when clicking', () => {
const closeToast = cy.stub().as('closeToast');
cy.mount(<CloseButton closeToast={closeToast} type="default" theme="light" />);
cy.get('@closeToast').should('not.have.been.called');
cy.findByRole('button').click();
cy.get('@closeToast').should('have.been.called');
});
it('have a default aria-label', () => {
cy.mount(<CloseButton closeToast={cy.stub} type="default" theme="light" />);
cy.findByLabelText('close').should('exist');
});
it('set aria-label', () => {
cy.mount(<CloseButton closeToast={cy.stub} type="default" theme="light" ariaLabel="foobar" />);
cy.findByLabelText('foobar').should('exist');
});
});
================================================
FILE: src/components/CloseButton.tsx
================================================
import React from 'react';
import { Default } from '../utils';
import { CloseToastFunc, Theme, TypeOptions } from '../types';
export interface CloseButtonProps {
closeToast: CloseToastFunc;
type: TypeOptions;
ariaLabel?: string;
theme: Theme;
}
export function CloseButton({ closeToast, theme, ariaLabel = 'close' }: CloseButtonProps) {
return (
<button
className={`${Default.CSS_NAMESPACE}__close-button ${Default.CSS_NAMESPACE}__close-button--${theme}`}
type="button"
onClick={e => {
e.stopPropagation();
closeToast(true);
}}
aria-label={ariaLabel}
>
<svg aria-hidden="true" viewBox="0 0 14 16">
<path
fillRule="evenodd"
d="M7.71 8.23l3.75 3.75-1.48 1.48-3.75-3.75-3.75 3.75L1 11.98l3.75-3.75L1 4.48 2.48 3l3.75 3.75L9.98 3l1.48 1.48-3.75 3.75z"
/>
</svg>
</button>
);
}
================================================
FILE: src/components/Icons.cy.tsx
================================================
import React from 'react';
import { TypeOptions } from '../types';
import { IconParams, getIcon } from './Icons';
const props: IconParams = {
theme: 'light',
type: 'default',
isLoading: false
};
describe('Icons', () => {
it('handle function', () => {
const C = getIcon({
...props,
icon: () => <div>icon</div>
});
cy.mount(C);
cy.findByText('icon').should('exist');
});
it('handle react element', () => {
const C = getIcon({
...props,
icon: <div>icon</div>
});
cy.mount(C);
cy.findByText('icon').should('exist');
});
it('handle loader', () => {
const C = getIcon({
...props,
isLoading: true
});
cy.mount(C);
cy.get('[data-cy-root]').should('have.length', 1);
});
it('handle built-in icons', () => {
for (const t of ['info', 'warning', 'success', 'error', 'spinner']) {
const C = getIcon({
...props,
type: t as TypeOptions
});
cy.mount(C);
cy.get('[data-cy-root]').should('have.length', 1);
}
});
});
================================================
FILE: src/components/Icons.tsx
================================================
import React, { cloneElement, isValidElement } from 'react';
import { Theme, ToastProps, TypeOptions } from '../types';
import { Default, isFn } from '../utils';
/**
* Used when providing custom icon
*/
export interface IconProps {
theme: Theme;
type: TypeOptions;
isLoading?: boolean;
}
export type BuiltInIconProps = React.SVGProps<SVGSVGElement> & IconProps;
const Svg: React.FC<BuiltInIconProps> = ({ theme, type, isLoading, ...rest }) => (
<svg
viewBox="0 0 24 24"
width="100%"
height="100%"
fill={theme === 'colored' ? 'currentColor' : `var(--toastify-icon-color-${type})`}
{...rest}
/>
);
function Warning(props: BuiltInIconProps) {
return (
<Svg {...props}>
<path d="M23.32 17.191L15.438 2.184C14.728.833 13.416 0 11.996 0c-1.42 0-2.733.833-3.443 2.184L.533 17.448a4.744 4.744 0 000 4.368C1.243 23.167 2.555 24 3.975 24h16.05C22.22 24 24 22.044 24 19.632c0-.904-.251-1.746-.68-2.44zm-9.622 1.46c0 1.033-.724 1.823-1.698 1.823s-1.698-.79-1.698-1.822v-.043c0-1.028.724-1.822 1.698-1.822s1.698.79 1.698 1.822v.043zm.039-12.285l-.84 8.06c-.057.581-.408.943-.897.943-.49 0-.84-.367-.896-.942l-.84-8.065c-.057-.624.25-1.095.779-1.095h1.91c.528.005.84.476.784 1.1z" />
</Svg>
);
}
function Info(props: BuiltInIconProps) {
return (
<Svg {...props}>
<path d="M12 0a12 12 0 1012 12A12.013 12.013 0 0012 0zm.25 5a1.5 1.5 0 11-1.5 1.5 1.5 1.5 0 011.5-1.5zm2.25 13.5h-4a1 1 0 010-2h.75a.25.25 0 00.25-.25v-4.5a.25.25 0 00-.25-.25h-.75a1 1 0 010-2h1a2 2 0 012 2v4.75a.25.25 0 00.25.25h.75a1 1 0 110 2z" />
</Svg>
);
}
function Success(props: BuiltInIconProps) {
return (
<Svg {...props}>
<path d="M12 0a12 12 0 1012 12A12.014 12.014 0 0012 0zm6.927 8.2l-6.845 9.289a1.011 1.011 0 01-1.43.188l-4.888-3.908a1 1 0 111.25-1.562l4.076 3.261 6.227-8.451a1 1 0 111.61 1.183z" />
</Svg>
);
}
function Error(props: BuiltInIconProps) {
return (
<Svg {...props}>
<path d="M11.983 0a12.206 12.206 0 00-8.51 3.653A11.8 11.8 0 000 12.207 11.779 11.779 0 0011.8 24h.214A12.111 12.111 0 0024 11.791 11.766 11.766 0 0011.983 0zM10.5 16.542a1.476 1.476 0 011.449-1.53h.027a1.527 1.527 0 011.523 1.47 1.475 1.475 0 01-1.449 1.53h-.027a1.529 1.529 0 01-1.523-1.47zM11 12.5v-6a1 1 0 012 0v6a1 1 0 11-2 0z" />
</Svg>
);
}
function Spinner() {
return <div className={`${Default.CSS_NAMESPACE}__spinner`} />;
}
export const Icons = {
info: Info,
warning: Warning,
success: Success,
error: Error,
spinner: Spinner
};
const maybeIcon = (type: string): type is keyof typeof Icons => type in Icons;
export type IconParams = Pick<ToastProps, 'theme' | 'icon' | 'type' | 'isLoading'>;
export function getIcon({ theme, type, isLoading, icon }: IconParams) {
let Icon: React.ReactNode = null;
const iconProps = { theme, type };
if (icon === false) {
// hide
} else if (isFn(icon)) {
Icon = icon({ ...iconProps, isLoading });
} else if (isValidElement(icon)) {
Icon = cloneElement(icon, iconProps);
} else if (isLoading) {
Icon = Icons.spinner();
} else if (maybeIcon(type)) {
Icon = Icons[type](iconProps);
}
return Icon;
}
================================================
FILE: src/components/ProgressBar.cy.tsx
================================================
import React from 'react';
import { Theme } from '../types';
import { ProgressBar } from './ProgressBar';
const getProps = () => ({
delay: 5000,
isRunning: true,
rtl: false,
closeToast: cy.stub,
isIn: true,
theme: ['colored', 'light', 'dark'][Math.floor(Math.random() * 3)] as Theme
});
const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div
style={{
padding: '1rem',
position: 'fixed',
top: 0,
left: 0,
right: 0,
outline: '1px solid'
}}
>
{children}
</div>
);
describe('ProgressBar', () => {
it('merge className', () => {
cy.mount(
<Wrapper>
<ProgressBar {...getProps()} className="test" />
</Wrapper>
);
cy.get('.test').should('exist');
});
it('merge className in function form', () => {
cy.mount(
<Wrapper>
<ProgressBar {...getProps()} className={() => 'test'} />
</Wrapper>
);
cy.get('.test').should('exist');
});
it('trigger closeToast when animation end', () => {
const closeToast = cy.stub().as('closeToast');
const delay = 1000;
cy.mount(
<Wrapper>
<ProgressBar {...getProps()} closeToast={closeToast} delay={delay} />
</Wrapper>
);
cy.get('@closeToast').should('not.have.been.called');
cy.wait(delay);
cy.get('@closeToast').should('have.been.called');
});
it('hide the progress bar', () => {
cy.mount(
<Wrapper>
<ProgressBar {...getProps()} hide />
</Wrapper>
);
cy.get('[role=progressbar]').should('exist').should('not.be.visible');
});
it('pause the progress bar', () => {
cy.mount(
<Wrapper>
<ProgressBar {...getProps()} isRunning={false} />
</Wrapper>
);
cy.findByRole('progressbar').should('have.attr', 'style').and('include', 'animation-play-state: paused');
});
it('control progress bar', () => {
cy.mount(
<Wrapper>
<ProgressBar {...getProps()} controlledProgress progress={0.7} />
</Wrapper>
);
cy.findByRole('progressbar').should('have.attr', 'style').and('include', 'scaleX(0.7)');
});
});
================================================
FILE: src/components/ProgressBar.tsx
================================================
import React from 'react';
import cx from 'clsx';
import { Default, isFn, Type } from '../utils';
import { Theme, ToastClassName, TypeOptions } from '../types';
export interface ProgressBarProps {
/**
* The animation delay which determine when to close the toast
*/
delay: number;
/**
* The animation is running or paused
*/
isRunning: boolean;
/**
* Func to close the current toast
*/
closeToast: () => void;
/**
* Optional type : info, success ...
*/
type?: TypeOptions;
/**
* The theme that is currently used
*/
theme: Theme;
/**
* Hide or not the progress bar
*/
hide?: boolean;
/**
* Optional className
*/
className?: ToastClassName;
/**
* Tell whether a controlled progress bar is used
*/
controlledProgress?: boolean;
/**
* Controlled progress value
*/
progress?: number | string;
/**
* Support rtl content
*/
rtl?: boolean;
/**
* Tell if the component is visible on screen or not
*/
isIn?: boolean;
}
export function ProgressBar({
delay,
isRunning,
closeToast,
type = Type.DEFAULT,
hide,
className,
controlledProgress,
progress,
rtl,
isIn,
theme
}: ProgressBarProps) {
const isHidden = hide || (controlledProgress && progress === 0);
const style: React.CSSProperties = {
animationDuration: `${delay}ms`,
animationPlayState: isRunning ? 'running' : 'paused'
};
if (controlledProgress) style.transform = `scaleX(${progress})`;
const defaultClassName = cx(
`${Default.CSS_NAMESPACE}__progress-bar`,
controlledProgress
? `${Default.CSS_NAMESPACE}__progress-bar--controlled`
: `${Default.CSS_NAMESPACE}__progress-bar--animated`,
`${Default.CSS_NAMESPACE}__progress-bar-theme--${theme}`,
`${Default.CSS_NAMESPACE}__progress-bar--${type}`,
{
[`${Default.CSS_NAMESPACE}__progress-bar--rtl`]: rtl
}
);
const classNames = isFn(className)
? className({
rtl,
type,
defaultClassName
})
: cx(defaultClassName, className);
// 🧐 controlledProgress is derived from progress
// so if controlledProgress is set
// it means that this is also the case for progress
const animationEvent = {
[controlledProgress && (progress as number)! >= 1 ? 'onTransitionEnd' : 'onAnimationEnd']:
controlledProgress && (progress as number)! < 1
? null
: () => {
isIn && closeToast();
}
};
// TODO: add aria-valuenow, aria-valuemax, aria-valuemin
return (
<div className={`${Default.CSS_NAMESPACE}__progress-bar--wrp`} data-hidden={isHidden}>
<div
className={`${Default.CSS_NAMESPACE}__progress-bar--bg ${Default.CSS_NAMESPACE}__progress-bar-theme--${theme} ${Default.CSS_NAMESPACE}__progress-bar--${type}`}
/>
<div
role="progressbar"
aria-hidden={isHidden ? 'true' : 'false'}
aria-label="notification timer"
className={classNames}
style={style}
{...animationEvent}
/>
</div>
);
}
================================================
FILE: src/components/Toast.cy.tsx
================================================
import React from 'react';
import { DraggableDirection, ToastProps } from '../types';
import { Default } from '../utils';
import { Toast } from './Toast';
import { defaultProps } from './ToastContainer';
const REQUIRED_PROPS = {
...defaultProps,
isIn: true,
autoClose: false,
closeToast: () => {},
type: 'default',
toastId: 'id',
key: 'key',
collapseAll: () => {}
} as ToastProps;
const cssClasses = {
rtl: `.${Default.CSS_NAMESPACE}__toast--rtl`,
closeOnClick: `.${Default.CSS_NAMESPACE}__toast--close-on-click`,
progressBar: `.${Default.CSS_NAMESPACE}__progress-bar`,
progressBarController: `.${Default.CSS_NAMESPACE}__progress-bar--controlled`,
closeButton: `.${Default.CSS_NAMESPACE}__close-button`,
container: `.${Default.CSS_NAMESPACE}__toast-container`
};
const progressBar = {
isRunning: () => {
cy.wait(100);
cy.findByRole('progressbar').should('have.attr', 'style').and('include', 'animation-play-state: running');
},
isPaused: () => {
cy.wait(100);
cy.findByRole('progressbar')
.should('have.attr', 'style')
.and('include', 'animation-play-state: paused')
.as('pause progress bar');
},
isControlled: (progress: number) => {
cy.wait(100);
cy.get(cssClasses.progressBarController).should('exist');
cy.findByRole('progressbar').should('have.attr', 'style').and('include', `scaleX(${progress})`);
}
};
describe('Toast', () => {
for (const { name, className } of [
{
name: 'string',
className: 'container-class'
},
{
name: 'function',
className: () => 'container-class'
}
]) {
it(`merge container when using ${name}`, () => {
cy.mount(
<Toast {...REQUIRED_PROPS} className={className}>
FooBar
</Toast>
);
cy.get('.container-class').should('exist');
});
}
it('support rtl', () => {
cy.mount(
<Toast {...REQUIRED_PROPS} rtl>
FooBar
</Toast>
);
cy.get(cssClasses.rtl).should('have.css', 'direction', 'rtl');
});
describe('closeOnClick', () => {
it('call closeToast when enabled', () => {
const closeToast = cy.stub().as('closeToast');
cy.mount(
<Toast {...REQUIRED_PROPS} closeOnClick closeToast={closeToast}>
FooBar
</Toast>
);
cy.findByRole('alert').click();
cy.get('@closeToast').should('have.been.called');
});
it('does not call closeToast when disabled', () => {
const closeToast = cy.stub().as('closeToast');
cy.mount(
<Toast {...REQUIRED_PROPS} closeOnClick={false} closeToast={closeToast}>
FooBar
</Toast>
);
cy.findByRole('alert').click();
cy.get('@closeToast').should('not.have.been.called');
});
});
describe('autoClose', () => {
it('does not render progress bar when false', () => {
cy.mount(
<Toast {...REQUIRED_PROPS} autoClose={false}>
FooBar
</Toast>
);
cy.findByRole('progressbar').should('not.exist');
});
it('resume and pause progress bar', () => {
cy.mount(
<Toast {...REQUIRED_PROPS} autoClose={5000}>
hello
</Toast>
);
cy.resolveEntranceAnimation();
cy.findByRole('alert').should('be.visible').trigger('mouseover');
progressBar.isPaused();
cy.findByRole('alert').trigger('mouseout');
progressBar.isRunning();
cy.findByRole('alert').trigger('mouseover');
progressBar.isPaused();
});
});
it('does not render close button when closeButton is false', () => {
cy.mount(
<Toast {...REQUIRED_PROPS} closeButton={false}>
FooBar
</Toast>
);
cy.findByLabelText('close').should('not.exist');
});
it('resume and pause progress bar when pauseOnFocusLoss is enabled', () => {
cy.mount(
<Toast {...REQUIRED_PROPS} autoClose={5000} pauseOnFocusLoss>
hello
</Toast>
);
cy.resolveEntranceAnimation();
progressBar.isRunning();
cy.window().blur();
progressBar.isPaused();
cy.window().focus();
progressBar.isRunning();
});
it('does not pause progress bar when pauseOnHover is disabled', () => {
cy.mount(
<Toast {...REQUIRED_PROPS} autoClose={5000} pauseOnHover={false}>
hello
</Toast>
);
cy.resolveEntranceAnimation();
cy.findByRole('alert').trigger('mouseover');
progressBar.isRunning();
});
describe('controller progress bar', () => {
it('set the correct progress value bar disregarding autoClose value', () => {
cy.mount(
<Toast {...REQUIRED_PROPS} progress={0.3} autoClose={false}>
hello
</Toast>
);
cy.resolveEntranceAnimation();
progressBar.isControlled(0.3);
cy.mount(
<Toast {...REQUIRED_PROPS} progress={0.3} autoClose={5000}>
hello
</Toast>
);
cy.resolveEntranceAnimation();
progressBar.isControlled(0.3);
});
it('call closeToast when progress value is >= 1', () => {
const closeToast = cy.stub().as('closeToast');
cy.mount(
<Toast {...REQUIRED_PROPS} progress={1.1} closeToast={closeToast}>
hello
</Toast>
);
cy.findByRole('progressbar').trigger('transitionend');
cy.get('@closeToast').should('have.been.called');
});
});
it('call closeToast when autoClose duration exceeded', () => {
const closeToast = cy.stub().as('closeToast');
cy.mount(
<Toast {...REQUIRED_PROPS} autoClose={200} closeToast={closeToast}>
hello
</Toast>
);
cy.get('@closeToast').should('have.been.called');
});
it('attach specified attributes: role, id, etc...', () => {
const style: React.CSSProperties = {
background: 'purple'
};
cy.mount(
<Toast {...REQUIRED_PROPS} role="status" toastId="foo" style={style}>
hello
</Toast>
);
cy.resolveEntranceAnimation();
cy.findByRole('status').should('exist');
cy.get('#foo').should('exist');
cy.findByRole('status').should('have.attr', 'style').and('include', 'background: purple');
});
for (const { type, value } of [
{
type: 'string',
value: 'hello'
},
{
type: 'react element',
value: <div>hello</div>
},
{
type: 'function',
value: () => <div>hello</div>
}
]) {
it(`render ${type}`, () => {
cy.mount(<Toast {...REQUIRED_PROPS}>{value}</Toast>);
cy.findByText('hello').should('exist');
});
}
it('override default closeButton', () => {
cy.mount(
<Toast {...REQUIRED_PROPS} closeButton={<span>💩</span>}>
hello
</Toast>
);
cy.resolveEntranceAnimation();
cy.findByText('💩').should('exist');
});
it('fallback to default closeButton', () => {
cy.mount(
<Toast {...REQUIRED_PROPS} closeButton={true}>
hello
</Toast>
);
cy.resolveEntranceAnimation();
cy.findByLabelText('close').should('exist');
});
describe('Drag event', () => {
beforeEach(() => {
cy.viewport('macbook-16');
});
for (const { axis, delta } of [
{ axis: 'x', delta: { deltaX: -300 } },
{ axis: 'y', delta: { deltaY: 300 } }
]) {
it(`close toast when dragging on ${axis}-axis`, () => {
cy.mount(
<div style={{ width: '300px', position: 'fixed', right: 0 }}>
<Toast
{...REQUIRED_PROPS}
autoClose={5000}
draggable
draggableDirection={axis as DraggableDirection}
closeToast={cy.stub().as('closeToast')}
>
hello
</Toast>
</div>
);
cy.resolveEntranceAnimation();
cy.findByRole('alert').move(delta);
cy.get('@closeToast').should('have.been.called');
});
}
for (const { axis, delta } of [
{ axis: 'x', delta: { deltaX: -100 } },
{ axis: 'y', delta: { deltaY: 40 } }
]) {
it(`does not close toast when dragging on ${axis}-axis`, () => {
cy.mount(
<div style={{ width: '300px', position: 'fixed', right: 0 }}>
<Toast
{...REQUIRED_PROPS}
autoClose={5000}
draggable
draggableDirection={axis as DraggableDirection}
closeToast={cy.stub().as('closeToast')}
>
hello
</Toast>
</div>
);
cy.resolveEntranceAnimation();
cy.findByRole('alert').move(delta);
cy.get('@closeToast').should('not.have.been.called');
});
}
});
});
================================================
FILE: src/components/Toast.tsx
================================================
import cx from 'clsx';
import React, { cloneElement, isValidElement } from 'react';
import { useToast } from '../hooks/useToast';
import { ToastProps } from '../types';
import { Default, isFn, renderContent } from '../utils';
import { CloseButton } from './CloseButton';
import { ProgressBar } from './ProgressBar';
import { getIcon } from './Icons';
export const Toast: React.FC<ToastProps> = props => {
const { isRunning, preventExitTransition, toastRef, eventHandlers, playToast } = useToast(props);
const {
closeButton,
children,
autoClose,
onClick,
type,
hideProgressBar,
closeToast,
transition: Transition,
position,
className,
style,
progressClassName,
updateId,
role,
progress,
rtl,
toastId,
deleteToast,
isIn,
isLoading,
closeOnClick,
theme,
ariaLabel
} = props;
const defaultClassName = cx(
`${Default.CSS_NAMESPACE}__toast`,
`${Default.CSS_NAMESPACE}__toast-theme--${theme}`,
`${Default.CSS_NAMESPACE}__toast--${type}`,
{
[`${Default.CSS_NAMESPACE}__toast--rtl`]: rtl
},
{
[`${Default.CSS_NAMESPACE}__toast--close-on-click`]: closeOnClick
}
);
const cssClasses = isFn(className)
? className({
rtl,
position,
type,
defaultClassName
})
: cx(defaultClassName, className);
const icon = getIcon(props);
const isProgressControlled = !!progress || !autoClose;
const closeButtonProps = { closeToast, type, theme };
let Close: React.ReactNode = null;
if (closeButton === false) {
// hide
} else if (isFn(closeButton)) {
Close = closeButton(closeButtonProps);
} else if (isValidElement(closeButton)) {
Close = cloneElement(closeButton, closeButtonProps);
} else {
Close = CloseButton(closeButtonProps);
}
return (
<Transition
isIn={isIn}
done={deleteToast}
position={position}
preventExitTransition={preventExitTransition}
nodeRef={toastRef}
playToast={playToast}
>
<div
id={toastId as string}
tabIndex={0}
onClick={onClick}
data-in={isIn}
className={cssClasses}
{...eventHandlers}
style={style}
ref={toastRef}
{...(isIn && { role: role, 'aria-label': ariaLabel })}
>
{icon != null && (
<div
className={cx(`${Default.CSS_NAMESPACE}__toast-icon`, {
[`${Default.CSS_NAMESPACE}--animate-icon ${Default.CSS_NAMESPACE}__zoom-enter`]: !isLoading
})}
>
{icon}
</div>
)}
{renderContent(children, props, !isRunning)}
{Close}
{!props.customProgressBar && (
<ProgressBar
{...(updateId && !isProgressControlled ? { key: `p-${updateId}` } : {})}
rtl={rtl}
theme={theme}
delay={autoClose as number}
isRunning={isRunning}
isIn={isIn}
closeToast={closeToast}
hide={hideProgressBar}
type={type}
className={progressClassName}
controlledProgress={isProgressControlled}
progress={progress || 0}
/>
)}
</div>
</Transition>
);
};
================================================
FILE: src/components/ToastContainer.tsx
================================================
import cx from 'clsx';
import React, { useEffect, useRef, useState } from 'react';
import { toast } from '../core';
import { useToastContainer } from '../hooks';
import { useIsomorphicLayoutEffect } from '../hooks/useIsomorphicLayoutEffect';
import { ToastContainerProps, ToastPosition } from '../types';
import { Default, Direction, isFn, parseClassName } from '../utils';
import { Toast } from './Toast';
import { Bounce } from './Transitions';
export const defaultProps: ToastContainerProps = {
position: 'top-right',
transition: Bounce,
autoClose: 5000,
closeButton: true,
pauseOnHover: true,
pauseOnFocusLoss: true,
draggable: 'touch',
draggablePercent: Default.DRAGGABLE_PERCENT as number,
draggableDirection: Direction.X,
role: 'alert',
theme: 'light',
'aria-label': 'Notifications Alt+T',
hotKeys: e => e.altKey && e.code === 'KeyT'
};
export function ToastContainer(props: ToastContainerProps) {
let containerProps: ToastContainerProps = {
...defaultProps,
...props
};
const stacked = props.stacked;
const [collapsed, setIsCollapsed] = useState(true);
const containerRef = useRef<HTMLDivElement>(null);
const { getToastToRender, isToastActive, count } = useToastContainer(containerProps);
const { className, style, rtl, containerId, hotKeys } = containerProps;
function getClassName(position: ToastPosition) {
const defaultClassName = cx(
`${Default.CSS_NAMESPACE}__toast-container`,
`${Default.CSS_NAMESPACE}__toast-container--${position}`,
{ [`${Default.CSS_NAMESPACE}__toast-container--rtl`]: rtl }
);
return isFn(className)
? className({
position,
rtl,
defaultClassName
})
: cx(defaultClassName, parseClassName(className));
}
function collapseAll() {
if (stacked) {
setIsCollapsed(true);
toast.play();
}
}
useIsomorphicLayoutEffect(() => {
if (stacked) {
const nodes = containerRef.current!.querySelectorAll('[data-in="true"]');
const gap = 12;
const isTop = containerProps.position?.includes('top');
let usedHeight = 0;
let prevS = 0;
Array.from(nodes)
.reverse()
.forEach((n, i) => {
const node = n as HTMLElement;
node.classList.add(`${Default.CSS_NAMESPACE}__toast--stacked`);
if (i > 0) node.dataset.collapsed = `${collapsed}`;
if (!node.dataset.pos) node.dataset.pos = isTop ? 'top' : 'bot';
const y = usedHeight * (collapsed ? 0.2 : 1) + (collapsed ? 0 : gap * i);
node.style.setProperty('--y', `${isTop ? y : y * -1}px`);
node.style.setProperty('--g', `${gap}`);
node.style.setProperty('--s', `${1 - (collapsed ? prevS : 0)}`);
usedHeight += node.offsetHeight;
prevS += 0.025;
});
}
}, [collapsed, count, stacked]);
useEffect(() => {
function focusFirst(e: KeyboardEvent) {
const node = containerRef.current;
if (hotKeys(e)) {
(node.querySelector('[tabIndex="0"]') as HTMLElement)?.focus();
setIsCollapsed(false);
toast.pause();
}
if (e.key === 'Escape' && (document.activeElement === node || node?.contains(document.activeElement))) {
setIsCollapsed(true);
toast.play();
}
}
document.addEventListener('keydown', focusFirst);
return () => {
document.removeEventListener('keydown', focusFirst);
};
}, [hotKeys]);
return (
<section
ref={containerRef}
className={Default.CSS_NAMESPACE as string}
id={containerId as string}
onMouseEnter={() => {
if (stacked) {
setIsCollapsed(false);
toast.pause();
}
}}
onMouseLeave={collapseAll}
aria-live="polite"
aria-atomic="false"
aria-relevant="additions text"
aria-label={containerProps['aria-label']}
>
{getToastToRender((position, toastList) => {
const containerStyle: React.CSSProperties = !toastList.length
? { ...style, pointerEvents: 'none' }
: { ...style };
return (
<div
tabIndex={-1}
className={getClassName(position)}
data-stacked={stacked}
style={containerStyle}
key={`c-${position}`}
>
{toastList.map(({ content, props: toastProps }) => {
return (
<Toast
{...toastProps}
stacked={stacked}
collapseAll={collapseAll}
isIn={isToastActive(toastProps.toastId, toastProps.containerId)}
key={`t-${toastProps.key}`}
>
{content}
</Toast>
);
})}
</div>
);
})}
</section>
);
}
================================================
FILE: src/components/Transitions.tsx
================================================
import { cssTransition, Default } from '../utils';
const getConfig = (animationName: string, appendPosition = false) => ({
enter: `${Default.CSS_NAMESPACE}--animate ${Default.CSS_NAMESPACE}__${animationName}-enter`,
exit: `${Default.CSS_NAMESPACE}--animate ${Default.CSS_NAMESPACE}__${animationName}-exit`,
appendPosition
});
const Bounce = cssTransition(getConfig('bounce', true));
const Slide = cssTransition(getConfig('slide', true));
const Zoom = cssTransition(getConfig('zoom'));
const Flip = cssTransition(getConfig('flip'));
export { Bounce, Slide, Zoom, Flip };
================================================
FILE: src/components/index.tsx
================================================
export * from './CloseButton';
export * from './ProgressBar';
export { ToastContainer } from './ToastContainer';
export * from './Transitions';
export * from './Toast';
export * from './Icons';
================================================
FILE: src/core/containerObserver.ts
================================================
import {
Id,
NotValidatedToastProps,
OnChangeCallback,
Toast,
ToastContainerProps,
ToastContent,
ToastProps
} from '../types';
import { canBeRendered, getAutoCloseDelay, isNum, parseClassName, toToastItem } from '../utils';
type Notify = () => void;
export type ContainerObserver = ReturnType<typeof createContainerObserver>;
export function createContainerObserver(
id: Id,
containerProps: ToastContainerProps,
dispatchChanges: OnChangeCallback
) {
let toastKey = 1;
let toastCount = 0;
let queue: Toast[] = [];
let snapshot: Toast[] = [];
let props = containerProps;
const toasts = new Map<Id, Toast>();
const listeners = new Set<Notify>();
const observe = (notify: Notify) => {
listeners.add(notify);
return () => listeners.delete(notify);
};
const notify = () => {
snapshot = Array.from(toasts.values());
listeners.forEach(cb => cb());
};
const shouldIgnoreToast = ({ containerId, toastId, updateId }: NotValidatedToastProps) => {
const containerMismatch = containerId ? containerId !== id : id !== 1;
const isDuplicate = toasts.has(toastId) && updateId == null;
return containerMismatch || isDuplicate;
};
const toggle = (v: boolean, id?: Id) => {
toasts.forEach(t => {
if (id == null || id === t.props.toastId) t.toggle?.(v);
});
};
const markAsRemoved = (v: Toast) => {
v.props?.onClose?.(v.removalReason);
v.isActive = false;
};
const removeToast = (id?: Id) => {
if (id == null) {
toasts.forEach(markAsRemoved);
} else {
const t = toasts.get(id);
if (t) markAsRemoved(t);
}
notify();
};
const clearQueue = () => {
toastCount -= queue.length;
queue = [];
};
const addActiveToast = (toast: Toast) => {
const { toastId, updateId } = toast.props;
const isNew = updateId == null;
if (toast.staleId) toasts.delete(toast.staleId);
toast.isActive = true;
toasts.set(toastId, toast);
notify();
dispatchChanges(toToastItem(toast, isNew ? 'added' : 'updated'));
if (isNew) toast.props.onOpen?.();
};
const buildToast = <TData = unknown>(content: ToastContent<TData>, options: NotValidatedToastProps) => {
if (shouldIgnoreToast(options)) return;
const { toastId, updateId, data, staleId, delay } = options;
const isNotAnUpdate = updateId == null;
if (isNotAnUpdate) toastCount++;
const toastProps = {
...props,
style: props.toastStyle,
key: toastKey++,
...Object.fromEntries(Object.entries(options).filter(([_, v]) => v != null)),
toastId,
updateId,
data,
isIn: false,
className: parseClassName(options.className || props.toastClassName),
progressClassName: parseClassName(options.progressClassName || props.progressClassName),
autoClose: options.isLoading ? false : getAutoCloseDelay(options.autoClose, props.autoClose),
closeToast(reason?: true) {
toasts.get(toastId)!.removalReason = reason;
removeToast(toastId);
},
deleteToast() {
const toastToRemove = toasts.get(toastId);
if (toastToRemove == null) return;
dispatchChanges(toToastItem(toastToRemove, 'removed'));
toasts.delete(toastId);
toastCount--;
if (toastCount < 0) toastCount = 0;
if (queue.length > 0) {
addActiveToast(queue.shift());
return;
}
notify();
}
} as ToastProps;
toastProps.closeButton = props.closeButton;
if (options.closeButton === false || canBeRendered(options.closeButton)) {
toastProps.closeButton = options.closeButton;
} else if (options.closeButton === true) {
toastProps.closeButton = canBeRendered(props.closeButton) ? props.closeButton : true;
}
const activeToast = {
content,
props: toastProps,
staleId
} as Toast;
// not handling limit + delay by design. Waiting for user feedback first
if (props.limit && props.limit > 0 && toastCount > props.limit && isNotAnUpdate) {
queue.push(activeToast);
} else if (isNum(delay)) {
setTimeout(() => {
addActiveToast(activeToast);
}, delay);
} else {
addActiveToast(activeToast);
}
};
return {
id,
props,
observe,
toggle,
removeToast,
toasts,
clearQueue,
buildToast,
setProps(p: ToastContainerProps) {
props = p;
},
setToggle: (id: Id, fn: (v: boolean) => void) => {
const t = toasts.get(id);
if (t) t.toggle = fn;
},
isToastActive: (id: Id) => toasts.get(id)?.isActive,
getSnapshot: () => snapshot
};
}
================================================
FILE: src/core/genToastId.ts
================================================
let TOAST_ID = 1;
export const genToastId = () => `${TOAST_ID++}`;
================================================
FILE: src/core/index.ts
================================================
export * from './toast';
================================================
FILE: src/core/store.ts
================================================
import {
ClearWaitingQueueParams,
Id,
NotValidatedToastProps,
OnChangeCallback,
ToastContainerProps,
ToastContent,
ToastItem,
ToastOptions
} from '../types';
import { Default, canBeRendered, isId } from '../utils';
import { ContainerObserver, createContainerObserver } from './containerObserver';
interface EnqueuedToast {
content: ToastContent<any>;
options: NotValidatedToastProps;
}
interface RemoveParams {
id?: Id;
containerId: Id;
}
const containers = new Map<Id, ContainerObserver>();
let renderQueue: EnqueuedToast[] = [];
const listeners = new Set<OnChangeCallback>();
const dispatchChanges = (data: ToastItem) => listeners.forEach(cb => cb(data));
const hasContainers = () => containers.size > 0;
function flushRenderQueue() {
renderQueue.forEach(v => pushToast(v.content, v.options));
renderQueue = [];
}
export const getToast = (id: Id, { containerId }: ToastOptions) =>
containers.get(containerId || Default.CONTAINER_ID)?.toasts.get(id);
export function isToastActive(id: Id, containerId?: Id) {
if (containerId) return !!containers.get(containerId)?.isToastActive(id);
let isActive = false;
containers.forEach(c => {
if (c.isToastActive(id)) isActive = true;
});
return isActive;
}
export function removeToast(params?: Id | RemoveParams) {
if (!hasContainers()) {
renderQueue = renderQueue.filter(v => params != null && v.options.toastId !== params);
return;
}
if (params == null || isId(params)) {
containers.forEach(c => {
c.removeToast(params as Id);
});
} else if (params && ('containerId' in params || 'id' in params)) {
const container = containers.get(params.containerId);
container
? container.removeToast(params.id)
: containers.forEach(c => {
c.removeToast(params.id);
});
}
}
export const clearWaitingQueue = (p: ClearWaitingQueueParams = {}) => {
containers.forEach(c => {
if (c.props.limit && (!p.containerId || c.id === p.containerId)) {
c.clearQueue();
}
});
};
export function pushToast<TData>(content: ToastContent<TData>, options: NotValidatedToastProps) {
if (!canBeRendered(content)) return;
if (!hasContainers()) renderQueue.push({ content, options });
containers.forEach(c => {
c.buildToast(content, options);
});
}
interface ToggleToastParams {
id?: Id;
containerId?: Id;
}
type RegisterToggleOpts = {
id: Id;
containerId?: Id;
fn: (v: boolean) => void;
};
export function registerToggle(opts: RegisterToggleOpts) {
containers.get(opts.containerId || Default.CONTAINER_ID)?.setToggle(opts.id, opts.fn);
}
export function toggleToast(v: boolean, opt?: ToggleToastParams) {
containers.forEach(c => {
if (opt == null || !opt?.containerId) {
c.toggle(v, opt?.id);
} else if (opt?.containerId === c.id) {
c.toggle(v, opt?.id);
}
});
}
export function registerContainer(props: ToastContainerProps) {
const id = props.containerId || Default.CONTAINER_ID;
return {
subscribe(notify: () => void) {
const container = createContainerObserver(id, props, dispatchChanges);
containers.set(id, container);
const unobserve = container.observe(notify);
flushRenderQueue();
return () => {
unobserve();
containers.delete(id);
};
},
setProps(p: ToastContainerProps) {
containers.get(id)?.setProps(p);
},
getSnapshot() {
return containers.get(id)?.getSnapshot();
}
};
}
export function onChange(cb: OnChangeCallback) {
listeners.add(cb);
return () => {
listeners.delete(cb);
};
}
================================================
FILE: src/core/toast.cy.tsx
================================================
import React from 'react';
import { ToastContainer } from '../components';
import { toast } from './toast';
beforeEach(() => {
cy.viewport('macbook-15');
});
describe('without container', () => {
it('enqueue toasts till container is mounted', () => {
toast('msg1');
toast('msg2');
cy.findByText('msg1').should('not.exist');
cy.findByText('msg2').should('not.exist');
cy.mount(<ToastContainer autoClose={false} />);
cy.resolveEntranceAnimation();
cy.findByText('msg1').should('exist');
cy.findByText('msg2').should('exist');
});
it('remove toast from render queue', () => {
toast('msg1');
const id = toast('msg2');
toast.dismiss(id);
cy.mount(<ToastContainer autoClose={false} />);
cy.resolveEntranceAnimation();
cy.findByText('msg1').should('exist');
cy.findByText('msg2').should('not.exist');
});
});
describe('with container', () => {
beforeEach(() => {
cy.mount(
<>
<ToastContainer autoClose={false} closeOnClick />
<button onClick={() => toast('msg')}>display msg</button>
</>
);
});
it('render toast', () => {
cy.mount(
<>
<ToastContainer autoClose={false} closeOnClick />
<button onClick={() => toast('msg')}>display msg</button>
</>
);
cy.findByRole('button').click();
cy.findByText('msg').should('exist');
});
it('return a new id each time a notification is pushed', () => {
const firstId = toast('Hello');
const secondId = toast('Hello');
expect(firstId).not.to.be.eq(secondId);
});
it('use the provided toastId from options', () => {
const toastId = 11;
const id = toast('Hello', { toastId });
expect(id).to.be.eq(toastId);
});
it('handle change event', () => {
toast.onChange(cy.stub().as('onChange'));
const id = 'qq';
cy.mount(
<>
<button
onClick={() => {
toast('msg', { data: 'xxxx', toastId: id });
}}
>
display msg
</button>
<button
onClick={() => {
toast.update(id, {
render: 'world'
});
}}
>
update
</button>
<button onClick={() => toast.dismiss(id)}>remove</button>
<ToastContainer />
</>
);
cy.findByRole('button', { name: 'display msg' }).click();
cy.get('@onChange').should('have.been.calledWithMatch', {
status: 'added',
content: 'msg',
data: 'xxxx'
});
cy.findByRole('button', { name: 'update' }).click();
cy.get('@onChange').should('have.been.calledWithMatch', {
status: 'updated',
content: 'world'
});
// cy.wait(1000);
// cy.findByRole('button', { name: 'remove' }).click();
//
// cy.get('@onChange').should('have.been.calledWithMatch', {
// status: 'removed'
// });
});
it('unsubscribe from change event', () => {
const unsub = toast.onChange(cy.stub().as('onChange'));
unsub();
cy.findByRole('button').click();
cy.get('@onChange').should('not.have.been.called');
});
describe('sa', () => {
// it('be able remove toast programmatically', () => {
// const id = 'test';
//
// cy.mount(
// <>
// <button
// onClick={() => {
// toast('msg', { toastId: id });
// }}
// >
// display msg
// </button>
// <button onClick={() => toast.dismiss(id)}>remove</button>
// <ToastContainer />
// </>
// );
//
// cy.findByRole('button', { name: 'display msg' }).click();
// cy.findByText('msg').should('exist');
//
// cy.findByRole('button', { name: 'remove' }).click();
// cy.resolveEntranceAnimation();
// cy.findByText('msg').should('not.exist');
// });
it('pause and resume notification', () => {
const id = toast('msg', {
autoClose: 10000
});
cy.findByRole('progressbar').as('progressBar');
cy.get('@progressBar')
.should('have.attr', 'style')
.and('include', 'animation-play-state: running')
.then(() => {
toast.pause({ id });
cy.get('@progressBar')
.should('have.attr', 'style')
.and('include', 'animation-play-state: paused')
.then(() => {
toast.play({ id });
cy.get('@progressBar').should('have.attr', 'style').and('include', 'animation-play-state: running');
});
});
});
});
describe('update function', () => {
it('update an existing toast', () => {
const id = toast('msg');
cy.resolveEntranceAnimation();
cy.findByText('msg')
.should('exist')
.then(() => {
toast.update(id, {
render: 'foobar'
});
cy.findByText('msg').should('not.exist');
cy.findByText('foobar').should('exist');
})
.then(() => {
toast.update(id, {
render: 'bazbar'
});
cy.findByText('foobar').should('not.exist');
cy.findByText('bazbar').should('exist');
});
});
it('keep the same content', () => {
const id = toast('msg');
cy.resolveEntranceAnimation();
cy.findByText('msg').should('exist');
cy.get('.myClass')
.should('not.exist')
.then(() => {
toast.update(id, {
className: 'myClass'
});
cy.get('.myClass').should('exist');
cy.findByText('msg').should('exist');
});
});
it('update a toast only when it exists', () => {
toast.update(0, {
render: 'msg'
});
cy.resolveEntranceAnimation();
cy.findByText('msg').should('not.exist');
});
it('update the toastId', () => {
const id = toast('msg');
const nextId = 123;
cy.resolveEntranceAnimation();
cy.findByText('msg')
.should('exist')
.then(() => {
expect(toast.isActive(id)).to.be.true;
toast.update(id, {
render: 'foobar',
toastId: nextId
});
});
cy.findByText('foobar')
.should('exist')
.then(() => {
expect(toast.isActive(id)).to.be.false;
expect(toast.isActive(nextId)).to.be.true;
});
});
});
it('can append classNames', () => {
toast('msg', {
className: 'class1',
progressClassName: 'class3'
});
cy.get('.class1').should('exist');
cy.get('.class3').should('exist');
});
it('uses syntactic sugar for different notification type', () => {
toast('default');
toast.success('success');
toast.error('error');
toast.warning('warning');
toast.info('info');
toast.warn('warn');
toast.dark('dark');
cy.resolveEntranceAnimation();
cy.findByText('default').should('exist');
cy.findByText('success').should('exist');
cy.findByText('error').should('exist');
cy.findByText('warning').should('exist');
cy.findByText('info').should('exist');
cy.findByText('warn').should('exist');
cy.findByText('dark').should('exist');
});
it('handle controlled progress bar', () => {
const id = toast('msg', {
progress: 0.3
});
cy.resolveEntranceAnimation();
cy.findByRole('progressbar')
.should('have.attr', 'style')
.and('include', 'scaleX(0.3)')
.then(() => {
toast.done(id);
cy.findByRole('progressbar').should('have.attr', 'style').and('include', 'scaleX(1)');
});
});
it('handle rejected promise', () => {
function rejectPromise() {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('oops'));
}, 2000);
});
}
toast.promise<unknown, Error>(rejectPromise, {
pending: 'loading',
error: {
render(props) {
return <>{props.data?.message}</>;
}
}
});
cy.resolveEntranceAnimation();
cy.findByText('loading').should('exist');
cy.wait(2000);
cy.findByText('loading').should('not.exist');
cy.findByText('oops').should('exist');
});
it('handle resolved promise', () => {
function resolvePromise() {
return new Promise<string>((resolve, _) => {
setTimeout(() => {
resolve('it worked');
}, 2000);
});
}
toast.promise<string>(resolvePromise, {
pending: 'loading',
success: {
render(props) {
return <>{props.data}</>;
}
}
});
cy.resolveEntranceAnimation();
cy.findByText('loading').should('exist');
cy.wait(2000);
cy.findByText('loading').should('not.exist');
cy.findByText('it worked').should('exist');
});
it('support onOpen and onClose callback', () => {
const id = 'hello';
cy.mount(
<>
<button
onClick={() => {
toast('msg', {
toastId: id,
onOpen: cy.stub().as('onOpen'),
onClose: cy.stub().as('onClose')
});
}}
>
display msg
</button>
<button onClick={() => toast.dismiss(id)}>remove</button>
<ToastContainer />
</>
);
cy.findByRole('button', { name: 'display msg' }).click();
cy.get('@onOpen').should('have.been.calledOnce');
cy.findByRole('button', { name: 'remove' }).click();
cy.get('@onClose').should('have.been.calledOnce');
});
xit('remove all toasts', () => {
cy.mount(
<>
<button
onClick={() => {
toast('msg1');
// toast('msg2');
}}
>
display msg
</button>
<button
onClick={() => {
toast.dismiss();
}}
>
remove
</button>
<ToastContainer />
</>
);
cy.findByRole('button', { name: 'display msg' }).click();
cy.findByText('msg1').should('exist');
cy.findByRole('button', { name: 'remove' }).click();
cy.wait(2000);
cy.findByText('msg1').should('not.exist');
});
});
describe.skip('with multi containers', () => {
const Containers = {
First: 'first',
Second: 'second',
Third: 'third'
};
it('clear waiting queue for a given container', () => {
cy.mount(
<>
<div style={{ display: 'grid', placeItems: 'center' }}>
<button
onClick={() => {
toast('msg1-c1', {
containerId: Containers.First
});
toast('msg2-c1', {
containerId: Containers.First
});
}}
>
first
</button>
<button
onClick={() => {
toast('msg1-c2', {
containerId: Containers.Second
});
toast('msg2-c2', {
containerId: Containers.Second
});
}}
>
second
</button>
<button
onClick={() => {
toast.clearWaitingQueue({ containerId: Containers.First });
}}
>
clear
</button>
</div>
<ToastContainer autoClose={false} position="top-left" limit={1} containerId={Containers.First} closeOnClick />
<ToastContainer autoClose={false} position="top-right" limit={1} containerId={Containers.Second} closeOnClick />
</>
);
cy.findByRole('button', { name: 'first' }).click();
cy.findByRole('button', { name: 'second' }).click();
cy.resolveEntranceAnimation();
cy.findByText('msg2-c1').should('not.exist');
cy.findByText('msg2-c2').should('not.exist');
cy.findByText('msg1-c1').should('exist');
cy.findByText('msg1-c2').should('exist');
cy.findByText('msg1-c1').then(() => {
cy.findByRole('button', { name: 'clear' }).click();
cy.findByText('msg1-c1')
.click()
.then(() => {
cy.resolveEntranceAnimation();
cy.findByText('msg1-c1').should('not.exist');
cy.findByText('msg2-c1').should('not.exist');
});
});
});
it('update a toast even when using multi containers', () => {
const id = 'boo';
cy.mount(
<>
<button
onClick={() => {
toast('second container', {
toastId: id,
containerId: Containers.Second
});
}}
>
notify
</button>
<button
onClick={() => {
toast.update(id, {
render: 'second container updated',
containerId: Containers.Second
});
}}
>
update
</button>
<ToastContainer autoClose={false} position="top-right" containerId={Containers.Second} closeOnClick />
</>
);
cy.findByRole('button', { name: 'notify' }).click();
cy.resolveEntranceAnimation();
cy.findByText('second container')
.should('exist')
.then(() => {
cy.findByRole('button', { name: 'update' }).click();
cy.findByText('second container updated').should('exist');
});
});
xit('remove toast for a given container', () => {
const toastId = '123';
cy.mount(
<>
<div style={{ display: 'grid', placeItems: 'center' }}>
<button
onClick={() => {
toast('second container', {
toastId,
containerId: Containers.Second
});
}}
>
notify
</button>
<button
onClick={() => {
toast.dismiss({
containerId: Containers.Second,
id: toastId
});
}}
>
clear
</button>
</div>
<ToastContainer autoClose={false} position="top-right" containerId={Containers.Second} closeOnClick />
</>
);
cy.findByRole('button', { name: 'notify' }).click();
cy.resolveEntranceAnimation();
cy.findByText('second container')
.should('exist')
.then(() => {
cy.findByRole('button', { name: 'clear' }).click();
cy.findByText('second container').should('not.exist');
});
});
xit('remove all toasts for a given container', () => {
const toastId = '123';
cy.mount(
<>
<div style={{ display: 'grid', placeItems: 'center' }}>
<button
onClick={() => {
toast('first container', {
toastId,
containerId: Containers.First
});
toast('third container', {
toastId,
containerId: Containers.Third
});
toast('third container second toast', {
containerId: Containers.Third
});
}}
>
notify
</button>
<button
onClick={() => {
toast.dismiss({
containerId: Containers.Third
});
}}
>
clear third
</button>
<button
onClick={() => {
toast.dismiss({ containerId: 'Non-Existing Container Id' });
}}
>
clear non-existent
</button>
</div>
<ToastContainer autoClose={false} position="top-left" containerId={Containers.First} closeOnClick />
<ToastContainer autoClose={false} position="top-right" containerId={Containers.Second} closeOnClick />
<ToastContainer autoClose={false} position="top-right" containerId={Containers.Third} closeOnClick />
</>
);
cy.findByRole('button', { name: 'notify' }).click();
cy.resolveEntranceAnimation();
cy.findByText('first container').should('exist');
cy.findByText('third container second toast').should('exist');
cy.findByText('third container')
.should('exist')
.then(() => {
cy.findByRole('button', { name: 'clear third' }).click();
cy.resolveEntranceAnimation();
cy.findByText('first container').should('exist');
cy.findByText('third container').should('not.exist');
cy.findByText('third container second toast').should('not.exist');
cy.findByText('first container')
.should('exist')
.then(() => {
cy.findByRole('button', { name: 'clear non-existent' }).click();
cy.findByText('first container').should('not.exist');
cy.findByText('third container').should('not.exist');
});
});
});
describe('with limit', () => {
beforeEach(() => {
cy.mount(<ToastContainer autoClose={false} limit={2} closeOnClick />);
});
it('limit the number of toast displayed', () => {
toast('msg1');
toast('msg2');
toast('msg3');
cy.resolveEntranceAnimation();
cy.findByText('msg3').should('not.exist');
cy.findByText('msg1').should('exist');
cy.findByText('msg2')
.should('exist')
.click()
.then(() => {
cy.resolveEntranceAnimation();
cy.findByText('msg3').should('exist');
});
});
it('clear waiting queue', () => {
toast('msg1');
toast('msg2');
toast('msg3');
cy.resolveEntranceAnimation();
cy.findByText('msg3').should('not.exist');
cy.findByText('msg1').should('exist');
cy.findByText('msg2')
.should('exist')
.then(() => {
toast.clearWaitingQueue();
cy.findByText('msg2')
.click()
.then(() => {
cy.resolveEntranceAnimation();
cy.findByText('msg3').should('not.exist');
});
});
});
});
});
describe('with stacked container', () => {
it('render toasts', () => {
cy.mount(<ToastContainer autoClose={false} stacked />);
toast('hello 1');
toast('hello 2');
toast('hello 3');
cy.findByText('hello 1').should('exist').and('not.be.visible');
cy.findByText('hello 2').should('exist').and('not.be.visible');
cy.findByText('hello 3').should('exist').and('be.visible');
});
});
================================================
FILE: src/core/toast.ts
================================================
import {
ClearWaitingQueueFunc,
Id,
IdOpts,
NotValidatedToastProps,
OnChangeCallback,
ToastContent,
ToastOptions,
ToastProps,
TypeOptions,
UpdateOptions
} from '../types';
import { isFn, isNum, isStr, Type } from '../utils';
import { genToastId } from './genToastId';
import { clearWaitingQueue, getToast, isToastActive, onChange, pushToast, removeToast, toggleToast } from './store';
/**
* Generate a toastId or use the one provided
*/
function getToastId<TData>(options?: ToastOptions<TData>) {
return options && (isStr(options.toastId) || isNum(options.toastId)) ? options.toastId : genToastId();
}
/**
* If the container is not mounted, the toast is enqueued
*/
function dispatchToast<TData>(content: ToastContent<TData>, options: NotValidatedToastProps): Id {
pushToast(content, options);
return options.toastId;
}
/**
* Merge provided options with the defaults settings and generate the toastId
*/
function mergeOptions<TData>(type: string, options?: ToastOptions<TData>) {
return {
...options,
type: (options && options.type) || type,
toastId: getToastId(options)
} as NotValidatedToastProps;
}
function createToastByType(type: string) {
return <TData = unknown>(content: ToastContent<TData>, options?: ToastOptions<TData>) =>
dispatchToast(content, mergeOptions(type, options));
}
function toast<TData = unknown>(content: ToastContent<TData>, options?: ToastOptions<TData>) {
return dispatchToast(content, mergeOptions(Type.DEFAULT, options));
}
toast.loading = <TData = unknown>(content: ToastContent<TData>, options?: ToastOptions<TData>) =>
dispatchToast(
content,
mergeOptions(Type.DEFAULT, {
isLoading: true,
autoClose: false,
closeOnClick: false,
closeButton: false,
draggable: false,
...options
})
);
export interface ToastPromiseParams<TData = unknown, TError = unknown, TPending = unknown> {
pending?: string | UpdateOptions<TPending>;
success?: string | UpdateOptions<TData>;
error?: string | UpdateOptions<TError>;
}
function handlePromise<TData = unknown, TError = unknown, TPending = unknown>(
promise: Promise<TData> | (() => Promise<TData>),
{ pending, error, success }: ToastPromiseParams<TData, TError, TPending>,
options?: ToastOptions<TData>
) {
let id: Id;
if (pending) {
id = isStr(pending)
? toast.loading(pending, options)
: toast.loading(pending.render, {
...options,
...(pending as ToastOptions)
} as ToastOptions<TPending>);
}
const resetParams = {
isLoading: null,
autoClose: null,
closeOnClick: null,
closeButton: null,
draggable: null
};
const resolver = <T>(type: TypeOptions, input: string | UpdateOptions<T> | undefined, result: T) => {
// Remove the toast if the input has not been provided. This prevents the toast from hanging
// in the pending state if a success/error toast has not been provided.
if (input == null) {
toast.dismiss(id);
return;
}
const baseParams = {
type,
...resetParams,
...options,
data: result
};
const params = isStr(input) ? { render: input } : input;
// if the id is set we know that it's an update
if (id) {
toast.update(id, {
...baseParams,
...params
} as UpdateOptions);
} else {
// using toast.promise without loading
toast(params!.render, {
...baseParams,
...params
} as ToastOptions<T>);
}
return result;
};
const p = isFn(promise) ? promise() : promise;
//call the resolvers only when needed
p.then(result => resolver('success', success, result)).catch(err => resolver('error', error, err));
return p;
}
/**
* Supply a promise or a function that return a promise and the notification will be updated if it resolves or fails.
* When the promise is pending a spinner is displayed by default.
* `toast.promise` returns the provided promise so you can chain it.
*
* Simple example:
*
* ```
* toast.promise(MyPromise,
* {
* pending: 'Promise is pending',
* success: 'Promise resolved 👌',
* error: 'Promise rejected 🤯'
* }
* )
*
* ```
*
* Advanced usage:
* ```
* toast.promise<{name: string}, {message: string}, undefined>(
* resolveWithSomeData,
* {
* pending: {
* render: () => "I'm loading",
* icon: false,
* },
* success: {
* render: ({data}) => `Hello ${data.name}`,
* icon: "🟢",
* },
* error: {
* render({data}){
* // When the promise reject, data will contains the error
* return <MyErrorComponent message={data.message} />
* }
* }
* }
* )
* ```
*/
toast.promise = handlePromise;
toast.success = createToastByType(Type.SUCCESS);
toast.info = createToastByType(Type.INFO);
toast.error = createToastByType(Type.ERROR);
toast.warning = createToastByType(Type.WARNING);
toast.warn = toast.warning;
toast.dark = (content: ToastContent, options?: ToastOptions) =>
dispatchToast(
content,
mergeOptions(Type.DEFAULT, {
theme: 'dark',
...options
})
);
interface RemoveParams {
id?: Id;
containerId: Id;
}
function dismiss(params: RemoveParams): void;
function dismiss(params?: Id): void;
function dismiss(params?: Id | RemoveParams) {
removeToast(params);
}
/**
* Remove toast programmatically
*
* - Remove all toasts:
* ```
* toast.dismiss()
* ```
*
* - Remove all toasts that belongs to a given container
* ```
* toast.dismiss({ container: "123" })
* ```
*
* - Remove toast that has a given id regardless the container
* ```
* toast.dismiss({ id: "123" })
* ```
*
* - Remove toast that has a given id for a specific container
* ```
* toast.dismiss({ id: "123", containerId: "12" })
* ```
*/
toast.dismiss = dismiss;
/**
* Clear waiting queue when limit is used
*/
toast.clearWaitingQueue = clearWaitingQueue as ClearWaitingQueueFunc;
/**
* Check if a toast is active
*
* - Check regardless the container
* ```
* toast.isActive("123")
* ```
*
* - Check in a specific container
* ```
* toast.isActive("123", "containerId")
* ```
*/
toast.isActive = isToastActive;
/**
* Update a toast, see https://fkhadra.github.io/react-toastify/update-toast/ for more
*
* Example:
* ```
* // With a string
* toast.update(toastId, {
* render: "New content",
* type: "info",
* });
*
* // Or with a component
* toast.update(toastId, {
* render: MyComponent
* });
*
* // Or a function
* toast.update(toastId, {
* render: () => <div>New content</div>
* });
*
* // Apply a transition
* toast.update(toastId, {
* render: "New Content",
* type: toast.TYPE.INFO,
* transition: Rotate
* })
* ```
*/
toast.update = <TData = unknown>(toastId: Id, options: UpdateOptions<TData> = {}) => {
const toast = getToast(toastId, options as ToastOptions);
if (toast) {
const { props: oldOptions, content: oldContent } = toast;
const nextOptions = {
delay: 100,
...oldOptions,
...options,
toastId: options.toastId || toastId,
updateId: genToastId()
} as ToastProps & UpdateOptions;
if (nextOptions.toastId !== toastId) nextOptions.staleId = toastId;
const content = nextOptions.render || oldContent;
delete nextOptions.render;
dispatchToast(content, nextOptions);
}
};
/**
* Used for controlled progress bar. It will automatically close the notification.
*
* If you don't want your notification to be clsoed when the timer is done you should use `toast.update` instead as follow instead:
*
* ```
* toast.update(id, {
* progress: null, // remove controlled progress bar
* render: "ok",
* type: "success",
* autoClose: 5000 // set autoClose to the desired value
* });
* ```
*/
toast.done = (id: Id) => {
toast.update(id, {
progress: 1
});
};
/**
* Subscribe to change when a toast is added, removed and updated
*
* Usage:
* ```
* const unsubscribe = toast.onChange((payload) => {
* switch (payload.status) {
* case "added":
* // new toast added
* break;
* case "updated":
* // toast updated
* break;
* case "removed":
* // toast has been removed
* break;
* }
* })
* ```
*/
toast.onChange = onChange as (cb: OnChangeCallback) => () => void;
/**
* Play a toast(s) timer progammatically
*
* Usage:
*
* - Play all toasts
* ```
* toast.play()
* ```
*
* - Play all toasts for a given container
* ```
* toast.play({ containerId: "123" })
* ```
*
* - Play toast that has a given id regardless the container
* ```
* toast.play({ id: "123" })
* ```
*
* - Play toast that has a given id for a specific container
* ```
* toast.play({ id: "123", containerId: "12" })
* ```
*/
toast.play = (opts?: IdOpts) => toggleToast(true, opts);
/**
* Pause a toast(s) timer progammatically
*
* Usage:
*
* - Pause all toasts
* ```
* toast.pause()
* ```
*
* - Pause all toasts for a given container
* ```
* toast.pause({ containerId: "123" })
* ```
*
* - Pause toast that has a given id regardless the container
* ```
* toast.pause({ id: "123" })
* ```
*
* - Pause toast that has a given id for a specific container
* ```
* toast.pause({ id: "123", containerId: "12" })
* ```
*/
toast.pause = (opts?: IdOpts) => toggleToast(false, opts);
export { toast };
================================================
FILE: src/hooks/index.ts
================================================
export * from './useToastContainer';
export * from './useToast';
================================================
FILE: src/hooks/useIsomorphicLayoutEffect.ts
================================================
import { useEffect, useLayoutEffect } from 'react';
export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
================================================
FILE: src/hooks/useToast.ts
================================================
import { DOMAttributes, useEffect, useRef, useState } from 'react';
import { ToastProps } from '../types';
import { Default, Direction } from '../utils';
import { registerToggle } from '../core/store';
interface Draggable {
start: number;
delta: number;
removalDistance: number;
canCloseOnClick: boolean;
canDrag: boolean;
didMove: boolean;
}
export function useToast(props: ToastProps) {
const [isRunning, setIsRunning] = useState(false);
const [preventExitTransition, setPreventExitTransition] = useState(false);
const toastRef = useRef<HTMLDivElement>(null);
const drag = useRef<Draggable>({
start: 0,
delta: 0,
removalDistance: 0,
canCloseOnClick: true,
canDrag: false,
didMove: false
}).current;
const { autoClose, pauseOnHover, closeToast, onClick, closeOnClick } = props;
registerToggle({
id: props.toastId,
containerId: props.containerId,
fn: setIsRunning
});
useEffect(() => {
if (props.pauseOnFocusLoss) {
bindFocusEvents();
return () => {
unbindFocusEvents();
};
}
}, [props.pauseOnFocusLoss]);
function bindFocusEvents() {
if (!document.hasFocus()) pauseToast();
window.addEventListener('focus', playToast);
window.addEventListener('blur', pauseToast);
}
function unbindFocusEvents() {
window.removeEventListener('focus', playToast);
window.removeEventListener('blur', pauseToast);
}
function onDragStart(e: React.PointerEvent<HTMLElement>) {
if (props.draggable === true || props.draggable === e.pointerType) {
bindDragEvents();
const toast = toastRef.current!;
drag.canCloseOnClick = true;
drag.canDrag = true;
toast.style.transition = 'none';
if (props.draggableDirection === Direction.X) {
drag.start = e.clientX;
drag.removalDistance = toast.offsetWidth * (props.draggablePercent / 100);
} else {
drag.start = e.clientY;
drag.removalDistance =
(toast.offsetHeight *
(props.draggablePercent === Default.DRAGGABLE_PERCENT
? props.draggablePercent * 1.5
: props.draggablePercent)) /
100;
}
}
}
function onDragTransitionEnd(e: React.PointerEvent<HTMLElement>) {
const { top, bottom, left, right } = toastRef.current!.getBoundingClientRect();
if (
e.nativeEvent.type !== 'touchend' &&
props.pauseOnHover &&
e.clientX >= left &&
e.clientX <= right &&
e.clientY >= top &&
e.clientY <= bottom
) {
pauseToast();
} else {
playToast();
}
}
function playToast() {
setIsRunning(true);
}
function pauseToast() {
setIsRunning(false);
}
function bindDragEvents() {
drag.didMove = false;
document.addEventListener('pointermove', onDragMove);
document.addEventListener('pointerup', onDragEnd);
}
function unbindDragEvents() {
document.removeEventListener('pointermove', onDragMove);
document.removeEventListener('pointerup', onDragEnd);
}
function onDragMove(e: PointerEvent) {
const toast = toastRef.current!;
if (drag.canDrag && toast) {
drag.didMove = true;
if (isRunning) pauseToast();
if (props.draggableDirection === Direction.X) {
drag.delta = e.clientX - drag.start;
} else {
drag.delta = e.clientY - drag.start;
}
// prevent false positive during a toast click
if (drag.start !== e.clientX) drag.canCloseOnClick = false;
const translate =
props.draggableDirection === 'x' ? `${drag.delta}px, var(--y)` : `0, calc(${drag.delta}px + var(--y))`;
toast.style.transform = `translate3d(${translate},0)`;
toast.style.opacity = `${1 - Math.abs(drag.delta / drag.removalDistance)}`;
}
}
function onDragEnd() {
unbindDragEvents();
const toast = toastRef.current!;
if (drag.canDrag && drag.didMove && toast) {
drag.canDrag = false;
if (Math.abs(drag.delta) > drag.removalDistance) {
setPreventExitTransition(true);
props.closeToast(true);
props.collapseAll();
return;
}
toast.style.transition = 'transform 0.2s, opacity 0.2s';
toast.style.removeProperty('transform');
toast.style.removeProperty('opacity');
}
}
const eventHandlers: DOMAttributes<HTMLElement> = {
onPointerDown: onDragStart,
onPointerUp: onDragTransitionEnd
};
if (autoClose && pauseOnHover) {
eventHandlers.onMouseEnter = pauseToast;
// progress control is delegated to the container
if (!props.stacked) eventHandlers.onMouseLeave = playToast;
}
// prevent toast from closing when user drags the toast
if (closeOnClick) {
eventHandlers.onClick = (e: React.MouseEvent) => {
onClick && onClick(e);
drag.canCloseOnClick && closeToast(true);
};
}
return {
playToast,
pauseToast,
isRunning,
preventExitTransition,
toastRef,
eventHandlers
};
}
================================================
FILE: src/hooks/useToastContainer.ts
================================================
import { useRef, useSyncExternalStore } from 'react';
import { isToastActive, registerContainer } from '../core/store';
import { Toast, ToastContainerProps, ToastPosition } from '../types';
export function useToastContainer(props: ToastContainerProps) {
const { subscribe, getSnapshot, setProps } = useRef(registerContainer(props)).current;
setProps(props);
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)?.slice();
function getToastToRender<T>(cb: (position: ToastPosition, toastList: Toast[]) => T) {
if (!snapshot) return [];
const toRender = new Map<ToastPosition, Toast[]>();
if (props.newestOnTop) snapshot.reverse();
snapshot.forEach(toast => {
const { position } = toast.props;
toRender.has(position) || toRender.set(position, []);
toRender.get(position)!.push(toast);
});
return Array.from(toRender, p => cb(p[0], p[1]));
}
return {
getToastToRender,
isToastActive,
count: snapshot?.length
};
}
================================================
FILE: src/index.ts
================================================
import './style.css';
export { cssTransition, collapseToast } from './utils';
export { ToastContainer, Bounce, Flip, Slide, Zoom, Icons } from './components';
export type { IconProps, CloseButton } from './components';
export type { ToastPromiseParams } from './core';
export { toast } from './core';
export type {
TypeOptions,
Theme,
ToastPosition,
ToastContentProps,
ToastContent,
ToastTransition,
ToastClassName,
ClearWaitingQueueParams,
DraggableDirection,
ToastOptions,
UpdateOptions,
ToastContainerProps,
ToastTransitionProps,
Id,
ToastItem,
ClearWaitingQueueFunc,
OnChangeCallback,
ToastIcon
} from './types';
export type { CloseButtonProps } from './components/CloseButton';
================================================
FILE: src/style.css
================================================
:root {
--toastify-color-light: #fff;
--toastify-color-dark: #121212;
--toastify-color-info: #3498db;
--toastify-color-success: #07bc0c;
--toastify-color-warning: #f1c40f;
--toastify-color-error: hsl(6, 78%, 57%);
--toastify-color-transparent: rgba(255, 255, 255, 0.7);
--toastify-icon-color-info: var(--toastify-color-info);
--toastify-icon-color-success: var(--toastify-color-success);
--toastify-icon-color-warning: var(--toastify-color-warning);
--toastify-icon-color-error: var(--toastify-color-error);
--toastify-container-width: fit-content;
--toastify-toast-width: 320px;
--toastify-toast-offset: 16px;
--toastify-toast-top: max(var(--toastify-toast-offset), env(safe-area-inset-top));
--toastify-toast-right: max(var(--toastify-toast-offset), env(safe-area-inset-right));
--toastify-toast-left: max(var(--toastify-toast-offset), env(safe-area-inset-left));
--toastify-toast-bottom: max(var(--toastify-toast-offset), env(safe-area-inset-bottom));
--toastify-toast-background: #fff;
--toastify-toast-padding: 14px;
--toastify-toast-min-height: 64px;
--toastify-toast-max-height: 800px;
--toastify-toast-bd-radius: 6px;
--toastify-toast-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
--toastify-font-family: sans-serif;
--toastify-z-index: 9999;
--toastify-text-color-light: #757575;
--toastify-text-color-dark: #fff;
/* Used only for colored theme */
--toastify-text-color-info: #fff;
--toastify-text-color-success: #fff;
--toastify-text-color-warning: #fff;
--toastify-text-color-error: #fff;
--toastify-spinner-color: #616161;
--toastify-spinner-color-empty-area: #e0e0e0;
--toastify-color-progress-light: linear-gradient(to right, #4cd964, #5ac8fa, #007aff, #34aadc, #5856d6, #ff2d55);
--toastify-color-progress-dark: #bb86fc;
--toastify-color-progress-info: var(--toastify-color-info);
--toastify-color-progress-success: var(--toastify-color-success);
--toastify-color-progress-warning: var(--toastify-color-warning);
--toastify-color-progress-error: var(--toastify-color-error);
/* used to control the opacity of the progress trail */
--toastify-color-progress-bgo: 0.2;
}
.Toastify__toast-container {
z-index: var(--toastify-z-index);
-webkit-transform: translate3d(0, 0, var(--toastify-z-index));
position: fixed;
width: var(--toastify-container-width);
box-sizing: border-box;
color: #fff;
display: flex;
flex-direction: column;
}
.Toastify__toast-container--top-left {
top: var(--toastify-toast-top);
left: var(--toastify-toast-left);
}
.Toastify__toast-container--top-center {
top: var(--toastify-toast-top);
left: 50%;
transform: translateX(-50%);
align-items: center;
}
.Toastify__toast-container--top-right {
top: var(--toastify-toast-top);
right: var(--toastify-toast-right);
align-items: end;
}
.Toastify__toast-container--bottom-left {
bottom: var(--toastify-toast-bottom);
left: var(--toastify-toast-left);
}
.Toastify__toast-container--bottom-center {
bottom: var(--toastify-toast-bottom);
left: 50%;
transform: translateX(-50%);
align-items: center;
}
.Toastify__toast-container--bottom-right {
bottom: var(--toastify-toast-bottom);
right: var(--toastify-toast-right);
align-items: end;
}
.Toastify__toast {
--y: 0;
position: relative;
touch-action: none;
width: var(--toastify-toast-width);
min-height: var(--toastify-toast-min-height);
box-sizing: border-box;
margin-bottom: 1rem;
padding: var(--toastify-toast-padding);
border-radius: var(--toastify-toast-bd-radius);
box-shadow: var(--toastify-toast-shadow);
max-height: var(--toastify-toast-max-height);
font-family: var(--toastify-font-family);
/* webkit only issue #791 */
z-index: 0;
/* inner swag */
display: flex;
flex: 1 auto;
align-items: center;
word-break: break-word;
}
@media only screen and (max-width: 480px) {
.Toastify__toast-container {
width: 100vw;
left: env(safe-area-inset-left);
margin: 0;
}
.Toastify__toast-container--top-left,
.Toastify__toast-container--top-center,
.Toastify__toast-container--top-right {
top: env(safe-area-inset-top);
transform: translateX(0);
}
.Toastify__toast-container--bottom-left,
.Toastify__toast-container--bottom-center,
.Toastify__toast-container--bottom-right {
bottom: env(safe-area-inset-bottom);
transform: translateX(0);
}
.Toastify__toast-container--rtl {
right: env(safe-area-inset-right);
left: initial;
}
.Toastify__toast {
--toastify-toast-width: 100%;
margin-bottom: 0;
border-radius: 0;
}
}
.Toastify__toast-container[data-stacked='true'] {
width: var(--toastify-toast-width);
}
.Toastify__toast--stacked {
position: absolute;
width: 100%;
transform: translate3d(0, var(--y), 0) scale(var(--s));
transition: transform 0.3s;
}
.Toastify__toast--stacked[data-collapsed] .Toastify__toast-body,
.Toastify__toast--stacked[data-collapsed] .Toastify__close-button {
transition: opacity 0.1s;
}
.Toastify__toast--stacked[data-collapsed='false'] {
overflow: visible;
}
.Toastify__toast--stacked[data-collapsed='true']:not(:last-child) > * {
opacity: 0;
}
.Toastify__toast--stacked:after {
content: '';
position: absolute;
left: 0;
right: 0;
height: calc(var(--g) * 1px);
bottom: 100%;
}
.Toastify__toast--stacked[data-pos='top'] {
top: 0;
}
.Toastify__toast--stacked[data-pos='bot'] {
bottom: 0;
}
.Toastify__toast--stacked[data-pos='bot'].Toastify__toast--stacked:before {
transform-origin: top;
}
.Toastify__toast--stacked[data-pos='top'].Toastify__toast--stacked:before {
transform-origin: bottom;
}
.Toastify__toast--stacked:before {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 100%;
transform: scaleY(3);
z-index: -1;
}
.Toastify__toast--rtl {
direction: rtl;
}
.Toastify__toast--close-on-click {
cursor: pointer;
}
.Toastify__toast-icon {
margin-inline-end: 10px;
width: 22px;
flex-shrink: 0;
display: flex;
}
.Toastify--animate {
animation-fill-mode: both;
animation-duration: 0.5s;
}
.Toastify--animate-icon {
animation-fill-mode: both;
animation-duration: 0.3s;
}
.Toastify__toast-theme--dark {
background: var(--toastify-color-dark);
color: var(--toastify-text-color-dark);
}
.Toastify__toast-theme--light {
background: var(--toastify-color-light);
color: var(--toastify-text-color-light);
}
.Toastify__toast-theme--colored.Toastify__toast--default {
background: var(--toastify-color-light);
color: var(--toastify-text-color-light);
}
.Toastify__toast-theme--colored.Toastify__toast--info {
color: var(--toastify-text-color-info);
background: var(--toastify-color-info);
}
.Toastify__toast-theme--colored.Toastify__toast--success {
color: var(--toastify-text-color-success);
background: var(--toastify-color-success);
}
.Toastify__toast-theme--colored.Toastify__toast--warning {
color: var(--toastify-text-color-warning);
background: var(--toastify-color-warning);
}
.Toastify__toast-theme--colored.Toastify__toast--error {
color: var(--toastify-text-color-error);
background: var(--toastify-color-error);
}
.Toastify__progress-bar-theme--light {
background: var(--toastify-color-progress-light);
}
.Toastify__progress-bar-theme--dark {
background: var(--toastify-color-progress-dark);
}
.Toastify__progress-bar--info {
background: var(--toastify-color-progress-info);
}
.Toastify__progress-bar--success {
background: var(--toastify-color-progress-success);
}
.Toastify__progress-bar--warning {
background: var(--toastify-color-progress-warning);
}
.Toastify__progress-bar--error {
background: var(--toastify-color-progress-error);
}
.Toastify__progress-bar-theme--colored.Toastify__progress-bar--info,
.Toastify__progress-bar-theme--colored.Toastify__progress-bar--success,
.Toastify__progress-bar-theme--colored.Toastify__progress-bar--warning,
.Toastify__progress-bar-theme--colored.Toastify__progress-bar--error {
background: var(--toastify-color-transparent);
}
.Toastify__close-button {
color: #fff;
position: absolute;
top: 6px;
right: 6px;
background: transparent;
outline: none;
border: none;
padding: 0;
cursor: pointer;
opacity: 0.7;
transition: 0.3s ease;
z-index: 1;
}
.Toastify__toast--rtl .Toastify__close-button {
left: 6px;
right: unset;
}
.Toastify__close-button--light {
color: #000;
opacity: 0.3;
}
.Toastify__close-button > svg {
fill: currentColor;
height: 16px;
width: 14px;
}
.Toastify__close-button:hover,
.Toastify__close-button:focus {
opacity: 1;
}
@keyframes Toastify__trackProgress {
0% {
transform: scaleX(1);
}
100% {
transform: scaleX(0);
}
}
.Toastify__progress-bar {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
opacity: 0.7;
transform-origin: left;
}
.Toastify__progress-bar--animated {
animation: Toastify__trackProgress linear 1 forwards;
}
.Toastify__progress-bar--controlled {
transition: transform 0.2s;
}
.Toastify__progress-bar--rtl {
right: 0;
left: initial;
transform-origin: right;
border-bottom-left-radius: initial;
}
.Toastify__progress-bar--wrp {
position: absolute;
overflow: hidden;
bottom: 0;
left: 0;
width: 100%;
height: 5px;
border-bottom-left-radius: var(--toastify-toast-bd-radius);
border-bottom-right-radius: var(--toastify-toast-bd-radius);
}
.Toastify__progress-bar--wrp[data-hidden='true'] {
opacity: 0;
}
.Toastify__progress-bar--bg {
opacity: var(--toastify-color-progress-bgo);
width: 100%;
height: 100%;
}
.Toastify__spinner {
width: 20px;
height: 20px;
box-sizing: border-box;
border: 2px solid;
border-radius: 100%;
border-color: var(--toastify-spinner-color-empty-area);
border-right-color: var(--toastify-spinner-color);
animation: Toastify__spin 0.65s linear infinite;
}
@keyframes Toastify__bounceInRight {
from,
60%,
75%,
90%,
to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
from {
opacity: 0;
transform: translate3d(3000px, 0, 0);
}
60% {
opacity: 1;
transform: translate3d(-25px, 0, 0);
}
75% {
transform: translate3d(10px, 0, 0);
}
90% {
transform: translate3d(-5px, 0, 0);
}
to {
transform: none;
}
}
@keyframes Toastify__bounceOutRight {
20% {
opacity: 1;
transform: translate3d(-20px, var(--y), 0);
}
to {
opacity: 0;
transform: translate3d(2000px, var(--y), 0);
}
}
@keyframes Toastify__bounceInLeft {
from,
60%,
75%,
90%,
to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
transform: translate3d(-3000px, 0, 0);
}
60% {
opacity: 1;
transform: translate3d(25px, 0, 0);
}
75% {
transform: translate3d(-10px, 0, 0);
}
90% {
transform: translate3d(5px, 0, 0);
}
to {
transform: none;
}
}
@keyframes Toastify__bounceOutLeft {
20% {
opacity: 1;
transform: translate3d(20px, var(--y), 0);
}
to {
opacity: 0;
transform: translate3d(-2000px, var(--y), 0);
}
}
@keyframes Toastify__bounceInUp {
from,
60%,
75%,
90%,
to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
from {
opacity: 0;
transform: translate3d(0, 3000px, 0);
}
60% {
opacity: 1;
transform: translate3d(0, -20px, 0);
}
75% {
transform: translate3d(0, 10px, 0);
}
90% {
transform: translate3d(0, -5px, 0);
}
to {
transform: translate3d(0, 0, 0);
}
}
@keyframes Toastify__bounceOutUp {
20% {
transform: translate3d(0, calc(var(--y) - 10px), 0);
}
40%,
45% {
opacity: 1;
transform: translate3d(0, calc(var(--y) + 20px), 0);
}
to {
opacity: 0;
transform: translate3d(0, -2000px, 0);
}
}
@keyframes Toastify__bounceInDown {
from,
60%,
75%,
90%,
to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
transform: translate3d(0, -3000px, 0);
}
60% {
opacity: 1;
transform: translate3d(0, 25px, 0);
}
75% {
transform: translate3d(0, -10px, 0);
}
90% {
transform: translate3d(0, 5px, 0);
}
to {
transform: none;
}
}
@keyframes Toastify__bounceOutDown {
20% {
transform: translate3d(0, calc(var(--y) - 10px), 0);
}
40%,
45% {
opacity: 1;
transform: translate3d(0, calc(var(--y) + 20px), 0);
}
to {
opacity: 0;
transform: translate3d(0, 2000px, 0);
}
}
.Toastify__bounce-enter--top-left,
.Toastify__bounce-enter--bottom-left {
animation-name: Toastify__bounceInLeft;
}
.Toastify__bounce-enter--top-right,
.Toastify__bounce-enter--bottom-right {
animation-name: Toastify__bounceInRight;
}
.Toastify__bounce-enter--top-center {
animation-name: Toastify__bounceInDown;
}
.Toastify__bounce-enter--bottom-center {
animation-name: Toastify__bounceInUp;
}
.Toastify__bounce-exit--top-left,
.Toastify__bounce-exit--bottom-left {
animation-name: Toastify__bounceOutLeft;
}
.Toastify__bounce-exit--top-right,
.Toastify__bounce-exit--bottom-right {
animation-name: Toastify__bounceOutRight;
}
.Toastify__bounce-exit--top-center {
animation-name: Toastify__bounceOutUp;
}
.Toastify__bounce-exit--bottom-center {
animation-name: Toastify__bounceOutDown;
}
@keyframes Toastify__zoomIn {
from {
opacity: 0;
transform: scale3d(0.3, 0.3, 0.3);
}
50% {
opacity: 1;
}
}
@keyframes Toastify__zoomOut {
from {
opacity: 1;
}
50% {
opacity: 0;
transform: translate3d(0, var(--y), 0) scale3d(0.3, 0.3, 0.3);
}
to {
opacity: 0;
}
}
.Toastify__zoom-enter {
animation-name: Toastify__zoomIn;
}
.Toastify__zoom-exit {
animation-name: Toastify__zoomOut;
}
@keyframes Toastify__flipIn {
from {
transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
animation-timing-function: ease-in;
opacity: 0;
}
40% {
transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
animation-timing-function: ease-in;
}
60% {
transform: perspective(400px) rotate3d(1, 0, 0, 10deg);
opacity: 1;
}
80% {
transform: perspective(400px) rotate3d(1, 0, 0, -5deg);
}
to {
transform: perspective(400px);
}
}
@keyframes Toastify__flipOut {
from {
transform: translate3d(0, var(--y), 0) perspective(400px);
}
30% {
transform: translate3d(0, var(--y), 0) perspective(400px) rotate3d(1, 0, 0, -20deg);
opacity: 1;
}
to {
transform: translate3d(0, var(--y), 0) perspective(400px) rotate3d(1, 0, 0, 90deg);
opacity: 0;
}
}
.Toastify__flip-enter {
animation-name: Toastify__flipIn;
}
.Toastify__flip-exit {
animation-name: Toastify__flipOut;
}
@keyframes Toastify__slideInRight {
from {
transform: translate3d(110%, 0, 0);
visibility: visible;
}
to {
transform: translate3d(0, var(--y), 0);
}
}
@keyframes Toastify__slideInLeft {
from {
transform: translate3d(-110%, 0, 0);
visibility: visible;
}
to {
transform: translate3d(0, var(--y), 0);
}
}
@keyframes Toastify__slideInUp {
from {
transform: translate3d(0, 110%, 0);
visibility: visible;
}
to {
transform: translate3d(0, var(--y), 0);
}
}
@keyframes Toastify__slideInDown {
from {
transform: translate3d(0, -110%, 0);
visibility: visible;
}
to {
transform: translate3d(0, var(--y), 0);
}
}
@keyframes Toastify__slideOutRight {
from {
transform: translate3d(0, var(--y), 0);
}
to {
visibility: hidden;
transform: translate3d(110%, var(--y), 0);
}
}
@keyframes Toastify__slideOutLeft {
from {
transform: translate3d(0, var(--y), 0);
}
to {
visibility: hidden;
transform: translate3d(-110%, var(--y), 0);
}
}
@keyframes Toastify__slideOutDown {
from {
transform: translate3d(0, var(--y), 0);
}
to {
visibility: hidden;
transform: translate3d(0, 500px, 0);
}
}
@keyframes Toastify__slideOutUp {
from {
transform: translate3d(0, var(--y), 0);
}
to {
visibility: hidden;
transform: translate3d(0, -500px, 0);
}
}
.Toastify__slide-enter--top-left,
.Toastify__slide-enter--bottom-left {
animation-name: Toastify__slideInLeft;
}
.Toastify__slide-enter--top-right,
.Toastify__slide-enter--bottom-right {
animation-name: Toastify__slideInRight;
}
.Toastify__slide-enter--top-center {
animation-name: Toastify__slideInDown;
}
.Toastify__slide-enter--bottom-center {
animation-name: Toastify__slideInUp;
}
.Toastify__slide-exit--top-left,
.Toastify__slide-exit--bottom-left {
animation-name: Toastify__slideOutLeft;
animation-timing-function: ease-in;
animation-duration: 0.3s;
}
.Toastify__slide-exit--top-right,
.Toastify__slide-exit--bottom-right {
animation-name: Toastify__slideOutRight;
animation-timing-function: ease-in;
animation-duration: 0.3s;
}
.Toastify__slide-exit--top-center {
animation-name: Toastify__slideOutUp;
animation-timing-function: ease-in;
animation-duration: 0.3s;
}
.Toastify__slide-exit--bottom-center {
animation-name: Toastify__slideOutDown;
animation-timing-function: ease-in;
animation-duration: 0.3s;
}
@keyframes Toastify__spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
================================================
FILE: src/tests.cy.tsx
================================================
import React from 'react';
import { ToastContainer } from './components';
import { toast } from './core';
import { ToastContentProps } from './types';
it('allows to specify the reason when calling closeToast', () => {
const onCloseFunc = cy.stub().as('onCloseFunc');
function CustomNotification({ closeToast }: ToastContentProps) {
return (
<button
onClick={() => {
closeToast('foobar');
}}
>
closeme
</button>
);
}
cy.mount(
<div>
<button
onClick={() => {
toast(CustomNotification, {
onClose: onCloseFunc
});
}}
>
notify
</button>
<ToastContainer autoClose={false} />
</div>
);
cy.findByRole('button', { name: 'notify' }).click();
cy.findByRole('alert').should('exist');
cy.findByRole('button', { name: 'closeme' }).click();
cy.get('@onCloseFunc').should('have.been.calledWith', 'foobar');
});
it('focus notification when alt+t is pressed', () => {
cy.mount(
<div>
<button
onClick={() => {
toast('hello', {
ariaLabel: 'notification'
});
}}
>
notify
</button>
<ToastContainer autoClose={false} />
</div>
);
cy.findByRole('button', { name: 'notify' }).click();
cy.resolveEntranceAnimation();
cy.findByRole('alert').should('exist');
cy.get('body').type('{alt+t}');
cy.focused().should('have.attr', 'role', 'alert').and('have.attr', 'aria-label', 'notification');
});
================================================
FILE: src/types.ts
================================================
import React, { HTMLAttributes } from 'react';
import { CloseButtonProps, IconProps } from './components';
import { clearWaitingQueue } from './core/store';
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
export type TypeOptions = 'info' | 'success' | 'warning' | 'error' | 'default';
export type Theme = 'light' | 'dark' | 'colored' | (string & {});
export type ToastPosition = 'top-right' | 'top-center' | 'top-left' | 'bottom-right' | 'bottom-center' | 'bottom-left';
export type CloseToastFunc = ((reason?: boolean | string) => void) & ((e: React.MouseEvent) => void);
export interface ToastContentProps<Data = unknown> {
closeToast: CloseToastFunc;
toastProps: ToastProps;
isPaused: boolean;
data: Data;
}
export type ToastContent<T = unknown> = React.ReactNode | ((props: ToastContentProps<T>) => React.ReactNode);
export type ToastIcon = false | ((props: IconProps) => React.ReactNode) | React.ReactElement<IconProps>;
export type Id = number | string;
export type ToastTransition = React.FC<ToastTransitionProps> | React.ComponentClass<ToastTransitionProps>;
/**
* ClassName for the elements - can take a function to build a classname or a raw string that is cx'ed to defaults
*/
export type ToastClassName =
| ((context?: { type?: TypeOptions; defaultClassName?: string; position?: ToastPosition; rtl?: boolean }) => string)
| string;
export interface ClearWaitingQueueParams {
containerId?: Id;
}
export type DraggableDirection = 'x' | 'y';
interface CommonOptions {
/**
* Pause the timer when the mouse hover the toast.
* `Default: true`
*/
pauseOnHover?: boolean;
/**
* Pause the toast when the window loses focus.
* `Default: true`
*/
pauseOnFocusLoss?: boolean;
/**
* Remove the toast when clicked.
* `Default: false`
*/
closeOnClick?: boolean;
/**
* Set the delay in ms to close the toast automatically.
* Use `false` to prevent the toast from closing.
* `Default: 5000`
*/
autoClose?: number | false;
/**
* Set the default position to use.
* `One of: 'top-right', 'top-center', 'top-left', 'bottom-right', 'bottom-center', 'bottom-left'`
* `Default: 'top-right'`
*/
position?: ToastPosition;
/**
* Pass a custom close button.
* To remove the close button pass `false`
*/
closeButton?: boolean | ((props: CloseButtonProps) => React.ReactNode) | React.ReactElement<CloseButtonProps>;
/**
* An optional css class to set for the progress bar.
*/
progressClassName?: ToastClassName;
/**
* Hide or show the progress bar.
* `Default: false`
*/
hideProgressBar?: boolean;
/**
* Pass a custom transition see https://fkhadra.github.io/react-toastify/custom-animation/
*/
transition?: ToastTransition;
/**
* Allow toast to be draggable
* `Default: 'touch'`
*/
draggable?: boolean | 'mouse' | 'touch';
/**
* The percentage of the toast's width it takes for a drag to dismiss a toast
* `Default: 80`
*/
draggablePercent?: number;
/**
* Specify in which direction should you swipe to dismiss the toast
* `Default: "x"`
*/
draggableDirection?: DraggableDirection;
/**
* Define the ARIA role for the toast
* `Default: alert`
* https://www.w3.org/WAI/PF/aria/roles
*/
role?: string;
/**
* Set id to handle multiple container
*/
containerId?: Id;
/**
* Fired when clicking inside toaster
*/
onClick?: (event: React.MouseEvent) => void;
/**
* Support right to left display.
* `Default: false`
*/
rtl?: boolean;
/**
* Used to display a custom icon. Set it to `false` to prevent
* the icons from being displayed
*/
icon?: ToastIcon;
/**
* Theme to use.
* `One of: 'light', 'dark', 'colored'`
* `Default: 'light'`
*/
theme?: Theme;
/**
* When set to `true` the built-in progress bar won't be rendered at all. Autoclose delay won't have any effect as well
* This is only used when you want to replace the progress bar with your own.
*
* See https://stackblitz.com/edit/react-toastify-custom-progress-bar?file=src%2FApp.tsx for an example.
*/
customProgressBar?: boolean;
}
export interface ToastOptions<Data = unknown> extends CommonOptions {
/**
* An optional css class to set.
*/
className?: ToastClassName;
/**
* Called when toast is mounted.
*/
onOpen?: () => void;
/**
* Called when toast is unmounted.
* The callback first argument is the closure reason.
* It is "true" when the notification is closed by a user action like clicking on the close button.
*/
onClose?: (reason?: boolean | string) => void;
/**
* An optional inline style to apply.
*/
style?: React.CSSProperties;
/**
* Set the toast type.
* `One of: 'info', 'success', 'warning', 'error', 'default'`
*/
type?: TypeOptions;
/**
* Set a custom `toastId`
*/
toastId?: Id;
/**
* Used during update
*/
updateId?: Id;
/**
* Set the percentage for the controlled progress bar. `Value must be between 0 and 1.`
*/
progress?: number;
/**
* Let you provide any data, useful when you are using your own component
*/
data?: Data;
/**
* Let you specify the aria-label
*/
ariaLabel?: string;
/**
* Add a delay in ms before the toast appear.
*/
delay?: number;
isLoading?: boolean;
}
export interface UpdateOptions<T = unknown> extends Nullable<ToastOptions<T>> {
/**
* Used to update a toast.
* Pass any valid ReactNode(string, number, component)
*/
render?: ToastContent<T>;
}
export interface ToastContainerProps extends CommonOptions, Pick<HTMLAttributes<HTMLElement>, 'aria-label'> {
/**
* An optional css class to set.
*/
className?: ToastClassName;
/**
* Will stack the toast with the newest on the top.
*/
stacked?: boolean;
/**
* Whether or not to display the newest toast on top.
* `Default: false`
*/
newestOnTop?: boolean;
/**
* An optional inline style to apply.
*/
style?: React.CSSProperties;
/**
* An optional inline style to apply for the toast.
*/
toastStyle?: React.CSSProperties;
/**
* An optional css class for the toast.
*/
toastClassName?: ToastClassName;
/**
* Limit the number of toast displayed at the same time
*/
limit?: number;
/**
* Shortcut to focus the first notification with the keyboard
* `default: Alt+t`
*
* ```
* // focus when user presses ⌘ + F
* const matchShortcut = (e: KeyboardEvent) => e.metaKey && e.key === 'f'
* ```
*/
hotKeys?: (e: KeyboardEvent) => boolean;
}
export interface ToastTransitionProps {
isIn: boolean;
done: () => void;
position: ToastPosition | string;
preventExitTransition: boolean;
nodeRef: React.RefObject<HTMLElement>;
children?: React.ReactNode;
playToast(): void;
}
/**
* @INTERNAL
*/
export interface ToastProps extends ToastOptions {
isIn: boolean;
staleId?: Id;
toastId: Id;
key: Id;
transition: ToastTransition;
closeToast: CloseToastFunc;
position: ToastPosition;
children?: ToastContent;
draggablePercent: number;
draggableDirection?: DraggableDirection;
progressClassName?: ToastClassName;
className?: ToastClassName;
deleteToast: () => void;
theme: Theme;
type: TypeOptions;
collapseAll: () => void;
stacked?: boolean;
}
/**
* @INTERNAL
*/
export interface NotValidatedToastProps extends Partial<ToastProps> {
toastId: Id;
}
/**
* @INTERNAL
*/
export interface Toast {
content: ToastContent;
props: ToastProps;
toggle?: (v: boolean) => void;
removalReason?: true | undefined;
isActive: boolean;
staleId?: Id;
}
export type ToastItemStatus = 'added' | 'removed' | 'updated';
export interface ToastItem<Data = {}> {
content: ToastContent<Data>;
id: Id;
theme?: Theme;
type?: TypeOptions;
isLoading?: boolean;
containerId?: Id;
data: Data;
icon?: ToastIcon;
status: ToastItemStatus;
reason?: boolean | string;
}
export type OnChangeCallback = (toast: ToastItem) => void;
export type IdOpts = {
id?: Id;
containerId?: Id;
};
export type ClearWaitingQueueFunc = typeof clearWaitingQueue;
================================================
FILE: src/utils/collapseToast.ts
================================================
import { Default } from './constant';
/**
* Used to collapse toast after exit animation
*/
export function collapseToast(node: HTMLElement, done: () => void, duration = Default.COLLAPSE_DURATION) {
const { scrollHeight, style } = node;
requestAnimationFrame(() => {
style.minHeight = 'initial';
style.height = scrollHeight + 'px';
style.transition = `all ${duration}ms`;
requestAnimationFrame(() => {
style.height = '0';
style.padding = '0';
style.margin = '0';
setTimeout(done, duration as number);
});
});
}
================================================
FILE: src/utils/constant.ts
================================================
export const enum Type {
INFO = 'info',
SUCCESS = 'success',
WARNING = 'warning',
ERROR = 'error',
DEFAULT = 'default'
}
export const enum Default {
COLLAPSE_DURATION = 300,
DEBOUNCE_DURATION = 50,
CSS_NAMESPACE = 'Toastify',
DRAGGABLE_PERCENT = 80,
CONTAINER_ID = 1
}
export const enum Direction {
X = 'x',
Y = 'y'
}
================================================
FILE: src/utils/cssTransition.tsx
================================================
import React, { useEffect, useLayoutEffect, useRef } from 'react';
import { collapseToast } from './collapseToast';
import { Default } from './constant';
import { ToastTransitionProps } from '../types';
export interface CSSTransitionProps {
/**
* Css class to apply when toast enter
*/
enter: string;
/**
* Css class to apply when toast leave
*/
exit: string;
/**
* Append current toast position to the classname.
* If multiple classes are provided, only the last one will get the position
* For instance `myclass--top-center`...
* `Default: false`
*/
appendPosition?: boolean;
/**
* Collapse toast smoothly when exit animation end
* `Default: true`
*/
collapse?: boolean;
/**
* Collapse transition duration
* `Default: 300`
*/
collapseDuration?: number;
}
const enum AnimationStep {
Enter,
Exit
}
/**
* Css animation that just work.
* You could use animate.css for instance
*
*
* ```
* cssTransition({
* enter: "animate__animated animate__bounceIn",
* exit: "animate__animated animate__bounceOut"
* })
* ```
*
*/
export function cssTransition({
enter,
exit,
appendPosition = false,
collapse = true,
collapseDuration = Default.COLLAPSE_DURATION
}: CSSTransitionProps) {
return function ToastTransition({
children,
position,
preventExitTransition,
done,
nodeRef,
isIn,
playToast
}: ToastTransitionProps) {
const enterClassName = appendPosition ? `${enter}--${position}` : enter;
const exitClassName = appendPosition ? `${exit}--${position}` : exit;
const animationStep = useRef(AnimationStep.Enter);
useLayoutEffect(() => {
const node = nodeRef.current!;
const classToToken = enterClassName.split(' ');
const onEntered = (e: AnimationEvent) => {
if (e.target !== nodeRef.current) return;
playToast();
node.removeEventListener('animationend', onEntered);
node.removeEventListener('animationcancel', onEntered);
if (animationStep.current === AnimationStep.Enter && e.type !== 'animationcancel') {
node.classList.remove(...classToToken);
}
};
const onEnter = () => {
node.classList.add(...classToToken);
node.addEventListener('animationend', onEntered);
node.addEventListener('animationcancel', onEntered);
};
onEnter();
}, []);
useEffect(() => {
const node = nodeRef.current!;
const onExited = () => {
node.removeEventListener('animationend', onExited);
collapse ? collapseToast(node, done, collapseDuration) : done();
};
const onExit = () => {
animationStep.current = AnimationStep.Exit;
node.className += ` ${exitClassName}`;
node.addEventListener('animationend', onExited);
};
if (!isIn) preventExitTransition ? onExited() : onExit();
}, [isIn]);
return <>{children}</>;
};
}
================================================
FILE: src/utils/index.ts
================================================
export * from './propValidator';
export * from './constant';
export * from './cssTransition';
export * from './collapseToast';
export * from './mapper';
================================================
FILE: src/utils/mapper.ts
================================================
import { Toast, ToastContentProps, ToastItem, ToastItemStatus, ToastProps } from '../types';
import { cloneElement, isValidElement, ReactElement } from 'react';
import { isFn, isStr } from './propValidator';
export function toToastItem(toast: Toast, status: ToastItemStatus): ToastItem {
return {
content: renderContent(toast.content, toast.props),
containerId: toast.props.containerId,
id: toast.props.toastId,
theme: toast.props.theme,
type: toast.props.type,
data: toast.props.data || {},
isLoading: toast.props.isLoading,
icon: toast.props.icon,
reason: toast.removalReason,
status
};
}
export function renderContent(content: unknown, props: ToastProps, isPaused: boolean = false) {
if (isValidElement(content) && !isStr(content.type)) {
return cloneElement<ToastContentProps>(content as ReactElement<any>, {
closeToast: props.closeToast,
toastProps: props,
data: props.data,
isPaused
});
} else if (isFn(content)) {
return content({
closeToast: props.closeToast,
toastProps: props,
data: props.data,
isPaused
});
}
return content;
}
================================================
FILE: src/utils/propValidator.ts
================================================
import { isValidElement } from 'react';
import { Id } from '../types';
export const isNum = (v: any): v is Number => typeof v === 'number' && !isNaN(v);
export const isStr = (v: any): v is String => typeof v === 'string';
export const isFn = (v: any): v is Function => typeof v === 'function';
export const isId = (v: unknown): v is Id => isStr(v) || isNum(v);
export const parseClassName = (v: any) => (isStr(v) || isFn(v) ? v : null);
export const getAutoCloseDelay = (toastAutoClose?: false | number, containerAutoClose?: false | number) =>
toastAutoClose === false || (isNum(toastAutoClose) && toastAutoClose > 0) ? toastAutoClose : containerAutoClose;
export const canBeRendered = <T>(content: T): boolean =>
isValidElement(content) || isStr(content) || isFn(content) || isNum(content);
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"jsx": "react",
"moduleResolution": "node",
"esModuleInterop": true,
"lib": ["es2015", "dom"]
}
}
================================================
FILE: tsup.config.ts
================================================
import { defineConfig, Options } from 'tsup';
const injectFunc = `
function injectStyle(css) {
if (!css || typeof document === 'undefined') return
const head = document.head || document.getElementsByTagName('head')[0]
const style = document.createElement('style')
style.type = 'text/css'
if(head.firstChild) {
head.insertBefore(style, head.firstChild)
} else {
head.appendChild(style)
}
if(style.styleSheet) {
style.styleSheet.cssText = css
} else {
style.appendChild(document.createTextNode(css))
}
}
`;
const baseConfig: Options = {
minify: true,
target: 'es2018',
sourcemap: true,
dts: true,
format: ['esm', 'cjs'],
injectStyle: css => {
return `${injectFunc}injectStyle(${css});`;
},
banner: {
js: '"use client";'
}
};
export default defineConfig([
{
...baseConfig,
entry: ['src/index.ts'],
external: ['react'],
clean: ['dist']
},
{
...baseConfig,
injectStyle: false,
entry: { unstyled: 'src/index.ts' },
external: ['react'],
clean: ['dist']
},
{
...baseConfig,
entry: {
'use-notification-center/index': 'src/addons/use-notification-center/index.ts'
},
external: ['react', 'react-toastify'],
clean: ['addons'],
outDir: 'addons'
}
]);
================================================
FILE: vite.config.mts
================================================
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import istanbul from 'vite-plugin-istanbul';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
istanbul({
cypress: true,
requireEnv: false
})
]
});
gitextract_tvelgcr9/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE.md │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ └── build.yaml ├── .gitignore ├── .nycrc.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cypress/ │ └── support/ │ ├── commands.ts │ ├── component-index.html │ ├── component.ts │ └── style.css ├── cypress.config.ts ├── lefthook.yml ├── package.json ├── playground/ │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── src/ │ │ ├── components/ │ │ │ ├── App.tsx │ │ │ ├── Checkbox.tsx │ │ │ ├── ContainerCode.tsx │ │ │ ├── Header.tsx │ │ │ ├── Radio.tsx │ │ │ ├── ToastCode.tsx │ │ │ └── constants.ts │ │ ├── index.css │ │ ├── main.tsx │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── src/ │ ├── addons/ │ │ └── use-notification-center/ │ │ ├── NotificationCenter.cy.tsx │ │ ├── index.ts │ │ └── useNotificationCenter.ts │ ├── components/ │ │ ├── CloseButton.cy.tsx │ │ ├── CloseButton.tsx │ │ ├── Icons.cy.tsx │ │ ├── Icons.tsx │ │ ├── ProgressBar.cy.tsx │ │ ├── ProgressBar.tsx │ │ ├── Toast.cy.tsx │ │ ├── Toast.tsx │ │ ├── ToastContainer.tsx │ │ ├── Transitions.tsx │ │ └── index.tsx │ ├── core/ │ │ ├── containerObserver.ts │ │ ├── genToastId.ts │ │ ├── index.ts │ │ ├── store.ts │ │ ├── toast.cy.tsx │ │ └── toast.ts │ ├── hooks/ │ │ ├── index.ts │ │ ├── useIsomorphicLayoutEffect.ts │ │ ├── useToast.ts │ │ └── useToastContainer.ts │ ├── index.ts │ ├── style.css │ ├── tests.cy.tsx │ ├── types.ts │ └── utils/ │ ├── collapseToast.ts │ ├── constant.ts │ ├── cssTransition.tsx │ ├── index.ts │ ├── mapper.ts │ └── propValidator.ts ├── tsconfig.json ├── tsup.config.ts └── vite.config.mts
SYMBOL INDEX (107 symbols across 28 files)
FILE: cypress.config.ts
method setupNodeEvents (line 5) | setupNodeEvents(on, config) {
FILE: cypress/support/commands.ts
type Chainable (line 31) | interface Chainable {
FILE: cypress/support/component.ts
type Chainable (line 34) | interface Chainable {
FILE: playground/src/components/App.tsx
class App (line 23) | class App extends React.Component {
method getDefaultState (line 28) | static getDefaultState() {
method isDefaultProps (line 79) | isDefaultProps() {
method renderFlags (line 105) | renderFlags() {
method render (line 113) | render() {
FILE: playground/src/components/Checkbox.tsx
type CheckboxProps (line 3) | interface CheckboxProps {
FILE: playground/src/components/ContainerCode.tsx
function getProp (line 4) | function getProp<L, R>(prop: L, value: R) {
type ContainerCodeProps (line 17) | interface ContainerCodeProps extends Partial<ToastContainerProps> {
FILE: playground/src/components/Radio.tsx
type RadioProps (line 3) | interface RadioProps {
FILE: playground/src/components/ToastCode.tsx
function getType (line 5) | function getType(type: string) {
type ToastCodeProps (line 21) | interface ToastCodeProps {
FILE: src/addons/use-notification-center/NotificationCenter.cy.tsx
function TestComponent (line 6) | function TestComponent(props: UseNotificationCenterParams) {
FILE: src/addons/use-notification-center/useNotificationCenter.ts
type Optional (line 4) | type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
type NotificationCenterItem (line 6) | interface NotificationCenterItem<Data = {}> extends Optional<ToastItem<D...
type SortFn (line 11) | type SortFn<Data> = (l: NotificationCenterItem<Data>, r: NotificationCen...
type FilterFn (line 13) | type FilterFn<Data = {}> = (item: NotificationCenterItem<Data>) => boolean;
type UseNotificationCenterParams (line 15) | interface UseNotificationCenterParams<Data = {}> {
type UseNotificationCenter (line 49) | interface UseNotificationCenter<Data> {
function useNotificationCenter (line 179) | function useNotificationCenter<Data = {}>(
function decorate (line 307) | function decorate<Data>(item: NotificationCenterItem<Data> | Partial<Not...
function defaultSort (line 315) | function defaultSort<Data>(l: NotificationCenterItem<Data>, r: Notificat...
FILE: src/components/CloseButton.tsx
type CloseButtonProps (line 5) | interface CloseButtonProps {
function CloseButton (line 12) | function CloseButton({ closeToast, theme, ariaLabel = 'close' }: CloseBu...
FILE: src/components/Icons.tsx
type IconProps (line 9) | interface IconProps {
type BuiltInIconProps (line 15) | type BuiltInIconProps = React.SVGProps<SVGSVGElement> & IconProps;
function Warning (line 27) | function Warning(props: BuiltInIconProps) {
function Info (line 35) | function Info(props: BuiltInIconProps) {
function Success (line 43) | function Success(props: BuiltInIconProps) {
function Error (line 51) | function Error(props: BuiltInIconProps) {
function Spinner (line 59) | function Spinner() {
type IconParams (line 73) | type IconParams = Pick<ToastProps, 'theme' | 'icon' | 'type' | 'isLoadin...
function getIcon (line 75) | function getIcon({ theme, type, isLoading, icon }: IconParams) {
FILE: src/components/ProgressBar.tsx
type ProgressBarProps (line 7) | interface ProgressBarProps {
function ProgressBar (line 64) | function ProgressBar({
FILE: src/components/Toast.cy.tsx
constant REQUIRED_PROPS (line 7) | const REQUIRED_PROPS = {
FILE: src/components/ToastContainer.tsx
function ToastContainer (line 28) | function ToastContainer(props: ToastContainerProps) {
FILE: src/core/containerObserver.ts
type Notify (line 12) | type Notify = () => void;
type ContainerObserver (line 14) | type ContainerObserver = ReturnType<typeof createContainerObserver>;
function createContainerObserver (line 16) | function createContainerObserver(
FILE: src/core/genToastId.ts
constant TOAST_ID (line 1) | let TOAST_ID = 1;
FILE: src/core/store.ts
type EnqueuedToast (line 14) | interface EnqueuedToast {
type RemoveParams (line 19) | interface RemoveParams {
function flushRenderQueue (line 32) | function flushRenderQueue() {
function isToastActive (line 40) | function isToastActive(id: Id, containerId?: Id) {
function removeToast (line 51) | function removeToast(params?: Id | RemoveParams) {
function pushToast (line 79) | function pushToast<TData>(content: ToastContent<TData>, options: NotVali...
type ToggleToastParams (line 88) | interface ToggleToastParams {
type RegisterToggleOpts (line 93) | type RegisterToggleOpts = {
function registerToggle (line 99) | function registerToggle(opts: RegisterToggleOpts) {
function toggleToast (line 103) | function toggleToast(v: boolean, opt?: ToggleToastParams) {
function registerContainer (line 113) | function registerContainer(props: ToastContainerProps) {
function onChange (line 137) | function onChange(cb: OnChangeCallback) {
FILE: src/core/toast.cy.tsx
function rejectPromise (line 301) | function rejectPromise() {
method render (line 312) | render(props) {
function resolvePromise (line 327) | function resolvePromise() {
method render (line 338) | render(props) {
FILE: src/core/toast.ts
function getToastId (line 20) | function getToastId<TData>(options?: ToastOptions<TData>) {
function dispatchToast (line 27) | function dispatchToast<TData>(content: ToastContent<TData>, options: Not...
function mergeOptions (line 35) | function mergeOptions<TData>(type: string, options?: ToastOptions<TData>) {
function createToastByType (line 43) | function createToastByType(type: string) {
function toast (line 48) | function toast<TData = unknown>(content: ToastContent<TData>, options?: ...
type ToastPromiseParams (line 65) | interface ToastPromiseParams<TData = unknown, TError = unknown, TPending...
function handlePromise (line 71) | function handlePromise<TData = unknown, TError = unknown, TPending = unk...
type RemoveParams (line 192) | interface RemoveParams {
function dismiss (line 199) | function dismiss(params?: Id | RemoveParams) {
FILE: src/hooks/useToast.ts
type Draggable (line 7) | interface Draggable {
function useToast (line 16) | function useToast(props: ToastProps) {
FILE: src/hooks/useToastContainer.ts
function useToastContainer (line 5) | function useToastContainer(props: ToastContainerProps) {
FILE: src/tests.cy.tsx
function CustomNotification (line 9) | function CustomNotification({ closeToast }: ToastContentProps) {
FILE: src/types.ts
type Nullable (line 5) | type Nullable<T> = {
type TypeOptions (line 9) | type TypeOptions = 'info' | 'success' | 'warning' | 'error' | 'default';
type Theme (line 11) | type Theme = 'light' | 'dark' | 'colored' | (string & {});
type ToastPosition (line 13) | type ToastPosition = 'top-right' | 'top-center' | 'top-left' | 'bottom-r...
type CloseToastFunc (line 15) | type CloseToastFunc = ((reason?: boolean | string) => void) & ((e: React...
type ToastContentProps (line 17) | interface ToastContentProps<Data = unknown> {
type ToastContent (line 24) | type ToastContent<T = unknown> = React.ReactNode | ((props: ToastContent...
type ToastIcon (line 26) | type ToastIcon = false | ((props: IconProps) => React.ReactNode) | React...
type Id (line 28) | type Id = number | string;
type ToastTransition (line 30) | type ToastTransition = React.FC<ToastTransitionProps> | React.ComponentC...
type ToastClassName (line 35) | type ToastClassName =
type ClearWaitingQueueParams (line 39) | interface ClearWaitingQueueParams {
type DraggableDirection (line 43) | type DraggableDirection = 'x' | 'y';
type CommonOptions (line 45) | interface CommonOptions {
type ToastOptions (line 164) | interface ToastOptions<Data = unknown> extends CommonOptions {
type UpdateOptions (line 226) | interface UpdateOptions<T = unknown> extends Nullable<ToastOptions<T>> {
type ToastContainerProps (line 234) | interface ToastContainerProps extends CommonOptions, Pick<HTMLAttributes...
type ToastTransitionProps (line 283) | interface ToastTransitionProps {
type ToastProps (line 296) | interface ToastProps extends ToastOptions {
type NotValidatedToastProps (line 319) | interface NotValidatedToastProps extends Partial<ToastProps> {
type Toast (line 326) | interface Toast {
type ToastItemStatus (line 335) | type ToastItemStatus = 'added' | 'removed' | 'updated';
type ToastItem (line 337) | interface ToastItem<Data = {}> {
type OnChangeCallback (line 350) | type OnChangeCallback = (toast: ToastItem) => void;
type IdOpts (line 352) | type IdOpts = {
type ClearWaitingQueueFunc (line 357) | type ClearWaitingQueueFunc = typeof clearWaitingQueue;
FILE: src/utils/collapseToast.ts
function collapseToast (line 6) | function collapseToast(node: HTMLElement, done: () => void, duration = D...
FILE: src/utils/constant.ts
type Type (line 1) | const enum Type {
type Default (line 9) | const enum Default {
type Direction (line 17) | const enum Direction {
FILE: src/utils/cssTransition.tsx
type CSSTransitionProps (line 7) | interface CSSTransitionProps {
type AnimationStep (line 39) | const enum AnimationStep {
function cssTransition (line 57) | function cssTransition({
FILE: src/utils/mapper.ts
function toToastItem (line 5) | function toToastItem(toast: Toast, status: ToastItemStatus): ToastItem {
function renderContent (line 20) | function renderContent(content: unknown, props: ToastProps, isPaused: bo...
Condensed preview — 71 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (174K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 63,
"preview": "# These are supported funding model platforms\n\ngithub: fkhadra\n"
},
{
"path": ".github/ISSUE_TEMPLATE.md",
"chars": 601,
"preview": "**Do you want to request a _feature_ or report a _bug_?**\n\n**What is the current behavior?**\n\n**If the current behavior "
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 808,
"preview": "**Before submitting a pull request,** please make sure the following is done:\n\n1. Fork [the repository](https://github.c"
},
{
"path": ".github/workflows/build.yaml",
"chars": 1043,
"preview": "name: React-toastify CI\n\non: [pull_request, push]\n\njobs:\n build:\n runs-on: ubuntu-latest\n \n steps:\n - uses:"
},
{
"path": ".gitignore",
"chars": 176,
"preview": ".idea/\nnode_modules/\nlib/\n.sass-cache/\nnpm-debug.log\ncoverage/\nyarn-error.log\n.DS_STORE\ncjs/\nesm/\ndist/\n.cache\n/addons\n."
},
{
"path": ".nycrc.json",
"chars": 266,
"preview": "{\n \"all\": true,\n \"extends\": \"@istanbuljs/nyc-config-typescript\",\n \"check-coverage\": true,\n \"include\": [\n \"src/**/"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3349,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
},
{
"path": "CONTRIBUTING.md",
"chars": 1696,
"preview": "# Contributing \n\n:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:\n\nWhen contributing to this r"
},
{
"path": "LICENSE",
"chars": 1068,
"preview": "MIT License\n\nCopyright (c) 2023 Fadi Khadra\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
},
{
"path": "README.md",
"chars": 5439,
"preview": "# React-Toastify\n\n[ {\n"
},
{
"path": "lefthook.yml",
"chars": 127,
"preview": " pre-commit:\n parallel: true\n commands:\n lint-staged:\n glob: \"*.{js,ts,jsx,tsx,css}\"\n run: pnpm lint"
},
{
"path": "package.json",
"chars": 3037,
"preview": "{\n \"version\": \"11.0.5\",\n \"license\": \"MIT\",\n \"description\": \"React notification made easy\",\n \"keywords\": [\n \"react"
},
{
"path": "playground/.eslintrc.cjs",
"chars": 391,
"preview": "module.exports = {\n env: { browser: true, es2020: true },\n extends: [\n 'eslint:recommended',\n 'plugin:@typescrip"
},
{
"path": "playground/.gitignore",
"chars": 253,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
},
{
"path": "playground/index.html",
"chars": 366,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <link rel=\"icon\" type=\"image/svg+xml\" href=\"/"
},
{
"path": "playground/package.json",
"chars": 539,
"preview": "{\n \"name\": \"playground\",\n \"private\": true,\n \"version\": \"0.0.0\",\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\","
},
{
"path": "playground/src/components/App.tsx",
"chars": 8218,
"preview": "/**\n * The playground could use some love 💖. To the brave soul reading this\n * message, any help would be appreciated 🙏\n"
},
{
"path": "playground/src/components/Checkbox.tsx",
"chars": 405,
"preview": "import * as React from 'react';\n\ninterface CheckboxProps {\n label: string;\n id: string;\n checked: boolean;\n onChange"
},
{
"path": "playground/src/components/ContainerCode.tsx",
"chars": 1894,
"preview": "import * as React from 'react';\nimport { ToastContainerProps } from '../../../src';\n\nfunction getProp<L, R>(prop: L, val"
},
{
"path": "playground/src/components/Header.tsx",
"chars": 2148,
"preview": "import * as React from 'react';\n\nexport const Header = () => (\n <header>\n <a\n href=\"https://github.com/fkhadra/"
},
{
"path": "playground/src/components/Radio.tsx",
"chars": 740,
"preview": "import * as React from 'react';\n\ninterface RadioProps {\n options: Record<string, string>;\n name: string;\n onChange: ("
},
{
"path": "playground/src/components/ToastCode.tsx",
"chars": 2126,
"preview": "import * as React from 'react';\n\nimport { themes } from './constants';\n\nfunction getType(type: string) {\n switch (type)"
},
{
"path": "playground/src/components/constants.ts",
"chars": 1140,
"preview": "import { Bounce, Slide, Flip, Zoom } from '../../../src/index';\n\nexport const flags = [\n {\n id: 'disableAutoClose',\n"
},
{
"path": "playground/src/index.css",
"chars": 3323,
"preview": "@import url(https://fonts.googleapis.com/css?family=Titillium+Web);\nbody {\n margin: 0;\n padding: 0;\n font-family: 'Ti"
},
{
"path": "playground/src/main.tsx",
"chars": 265,
"preview": "import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport { App } from './components/App';\nimport './in"
},
{
"path": "playground/src/vite-env.d.ts",
"chars": 38,
"preview": "/// <reference types=\"vite/client\" />\n"
},
{
"path": "playground/tsconfig.json",
"chars": 569,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"lib\": [\"DOM\", \"DOM.Iterable\", \"ESNext\"],\n \"module\": \"ESNext\",\n "
},
{
"path": "playground/tsconfig.node.json",
"chars": 213,
"preview": "{\n \"compilerOptions\": {\n \"composite\": true,\n \"skipLibCheck\": true,\n \"module\": \"ESNext\",\n \"moduleResolution\""
},
{
"path": "playground/vite.config.ts",
"chars": 163,
"preview": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\n\n// https://vitejs.dev/config/\nexport defau"
},
{
"path": "src/addons/use-notification-center/NotificationCenter.cy.tsx",
"chars": 4615,
"preview": "import React from 'react';\n\nimport { toast, ToastContainer } from 'react-toastify';\nimport { NotificationCenterItem, use"
},
{
"path": "src/addons/use-notification-center/index.ts",
"chars": 41,
"preview": "export * from './useNotificationCenter';\n"
},
{
"path": "src/addons/use-notification-center/useNotificationCenter.ts",
"chars": 8105,
"preview": "import { useState, useEffect, useRef } from 'react';\nimport { toast, ToastItem, Id } from 'react-toastify';\n\ntype Option"
},
{
"path": "src/components/CloseButton.cy.tsx",
"chars": 816,
"preview": "import React from 'react';\nimport { CloseButton } from './CloseButton';\n\ndescribe('CloseButton', () => {\n it('call clos"
},
{
"path": "src/components/CloseButton.tsx",
"chars": 893,
"preview": "import React from 'react';\nimport { Default } from '../utils';\nimport { CloseToastFunc, Theme, TypeOptions } from '../ty"
},
{
"path": "src/components/Icons.cy.tsx",
"chars": 1062,
"preview": "import React from 'react';\nimport { TypeOptions } from '../types';\nimport { IconParams, getIcon } from './Icons';\n\nconst"
},
{
"path": "src/components/Icons.tsx",
"chars": 3155,
"preview": "import React, { cloneElement, isValidElement } from 'react';\n\nimport { Theme, ToastProps, TypeOptions } from '../types';"
},
{
"path": "src/components/ProgressBar.cy.tsx",
"chars": 2149,
"preview": "import React from 'react';\nimport { Theme } from '../types';\nimport { ProgressBar } from './ProgressBar';\n\nconst getProp"
},
{
"path": "src/components/ProgressBar.tsx",
"chars": 3054,
"preview": "import React from 'react';\nimport cx from 'clsx';\n\nimport { Default, isFn, Type } from '../utils';\nimport { Theme, Toast"
},
{
"path": "src/components/Toast.cy.tsx",
"chars": 8676,
"preview": "import React from 'react';\nimport { DraggableDirection, ToastProps } from '../types';\nimport { Default } from '../utils'"
},
{
"path": "src/components/Toast.tsx",
"chars": 3272,
"preview": "import cx from 'clsx';\nimport React, { cloneElement, isValidElement } from 'react';\n\nimport { useToast } from '../hooks/"
},
{
"path": "src/components/ToastContainer.tsx",
"chars": 4847,
"preview": "import cx from 'clsx';\nimport React, { useEffect, useRef, useState } from 'react';\n\nimport { toast } from '../core';\nimp"
},
{
"path": "src/components/Transitions.tsx",
"chars": 582,
"preview": "import { cssTransition, Default } from '../utils';\n\nconst getConfig = (animationName: string, appendPosition = false) =>"
},
{
"path": "src/components/index.tsx",
"chars": 194,
"preview": "export * from './CloseButton';\nexport * from './ProgressBar';\nexport { ToastContainer } from './ToastContainer';\nexport "
},
{
"path": "src/core/containerObserver.ts",
"chars": 4660,
"preview": "import {\n Id,\n NotValidatedToastProps,\n OnChangeCallback,\n Toast,\n ToastContainerProps,\n ToastContent,\n ToastProp"
},
{
"path": "src/core/genToastId.ts",
"chars": 68,
"preview": "let TOAST_ID = 1;\n\nexport const genToastId = () => `${TOAST_ID++}`;\n"
},
{
"path": "src/core/index.ts",
"chars": 25,
"preview": "export * from './toast';\n"
},
{
"path": "src/core/store.ts",
"chars": 3609,
"preview": "import {\n ClearWaitingQueueParams,\n Id,\n NotValidatedToastProps,\n OnChangeCallback,\n ToastContainerProps,\n ToastCo"
},
{
"path": "src/core/toast.cy.tsx",
"chars": 18309,
"preview": "import React from 'react';\nimport { ToastContainer } from '../components';\nimport { toast } from './toast';\n\nbeforeEach("
},
{
"path": "src/core/toast.ts",
"chars": 9410,
"preview": "import {\n ClearWaitingQueueFunc,\n Id,\n IdOpts,\n NotValidatedToastProps,\n OnChangeCallback,\n ToastContent,\n ToastO"
},
{
"path": "src/hooks/index.ts",
"chars": 65,
"preview": "export * from './useToastContainer';\nexport * from './useToast';\n"
},
{
"path": "src/hooks/useIsomorphicLayoutEffect.ts",
"chars": 155,
"preview": "import { useEffect, useLayoutEffect } from 'react';\n\nexport const useIsomorphicLayoutEffect = typeof window !== 'undefin"
},
{
"path": "src/hooks/useToast.ts",
"chars": 4996,
"preview": "import { DOMAttributes, useEffect, useRef, useState } from 'react';\n\nimport { ToastProps } from '../types';\nimport { Def"
},
{
"path": "src/hooks/useToastContainer.ts",
"chars": 1006,
"preview": "import { useRef, useSyncExternalStore } from 'react';\nimport { isToastActive, registerContainer } from '../core/store';\n"
},
{
"path": "src/index.ts",
"chars": 720,
"preview": "import './style.css';\n\nexport { cssTransition, collapseToast } from './utils';\nexport { ToastContainer, Bounce, Flip, Sl"
},
{
"path": "src/style.css",
"chars": 17277,
"preview": ":root {\n --toastify-color-light: #fff;\n --toastify-color-dark: #121212;\n --toastify-color-info: #3498db;\n --toastify"
},
{
"path": "src/tests.cy.tsx",
"chars": 1540,
"preview": "import React from 'react';\nimport { ToastContainer } from './components';\nimport { toast } from './core';\nimport { Toast"
},
{
"path": "src/types.ts",
"chars": 8176,
"preview": "import React, { HTMLAttributes } from 'react';\nimport { CloseButtonProps, IconProps } from './components';\nimport { clea"
},
{
"path": "src/utils/collapseToast.ts",
"chars": 565,
"preview": "import { Default } from './constant';\n\n/**\n * Used to collapse toast after exit animation\n */\nexport function collapseTo"
},
{
"path": "src/utils/constant.ts",
"chars": 344,
"preview": "export const enum Type {\n INFO = 'info',\n SUCCESS = 'success',\n WARNING = 'warning',\n ERROR = 'error',\n DEFAULT = '"
},
{
"path": "src/utils/cssTransition.tsx",
"chars": 2955,
"preview": "import React, { useEffect, useLayoutEffect, useRef } from 'react';\nimport { collapseToast } from './collapseToast';\nimpo"
},
{
"path": "src/utils/index.ts",
"chars": 153,
"preview": "export * from './propValidator';\nexport * from './constant';\nexport * from './cssTransition';\nexport * from './collapseT"
},
{
"path": "src/utils/mapper.ts",
"chars": 1157,
"preview": "import { Toast, ToastContentProps, ToastItem, ToastItemStatus, ToastProps } from '../types';\nimport { cloneElement, isVa"
},
{
"path": "src/utils/propValidator.ts",
"chars": 803,
"preview": "import { isValidElement } from 'react';\nimport { Id } from '../types';\n\nexport const isNum = (v: any): v is Number => ty"
},
{
"path": "tsconfig.json",
"chars": 140,
"preview": "{\n \"compilerOptions\": {\n \"jsx\": \"react\",\n \"moduleResolution\": \"node\",\n \"esModuleInterop\": true,\n \"lib\": [\"e"
},
{
"path": "tsup.config.ts",
"chars": 1294,
"preview": "import { defineConfig, Options } from 'tsup';\n\nconst injectFunc = `\nfunction injectStyle(css) {\n if (!css || typeof doc"
},
{
"path": "vite.config.mts",
"chars": 286,
"preview": "import { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport istanbul from 'vite-plugin-istanbu"
}
]
About this extraction
This page contains the full source code of the fkhadra/react-toastify GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 71 files (158.9 KB), approximately 44.7k tokens, and a symbol index with 107 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.