Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: refactor css modules to global BEM class #98

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Elevate your React applications with ultra-sleek toast notifications! With React
</p>

## Table of Contents

- [Installation](#Installation)
- [Usage](#Usage)
- [Key Features](#Key-Features)
Expand All @@ -24,37 +25,35 @@ Elevate your React applications with ultra-sleek toast notifications! With React
- [License](#License)

<a name="Installation"></a>

## Installation 📦

Get started in seconds!



```bash
npm install react-simple-toasts
```

<a name="Usage"></a>

## Usage 💡

Integrate with ease. Customize with flair.

```jsx
import toast, { toastConfig } from 'react-simple-toasts';
import 'react-simple-toasts/dist/style.css';
import 'react-simple-toasts/dist/theme/dark.css';

toastConfig({ theme: 'dark' });

function MyComponent() {
return (
<button onClick={() => toast('Your toast is ready! 🍞')}>
Show Toast
</button>
);
return <button onClick={() => toast('Your toast is ready! 🍞')}>Show Toast</button>;
}
```

<a name="Key-Features"></a>

## Key Features 🌟

- **Ease of Use**: Set up in minutes, not hours!
Expand All @@ -64,31 +63,37 @@ function MyComponent() {
- **Multi-Toast Control**: Manage multiple notifications effortlessly.

<a name="Themes"></a>

## Themes 🎨

Your style, your toast. Choose from built-in themes or create your own.

### Standard Theme

<p align="center">
<img src="https://raw.githubusercontent.com/almond-bongbong/react-simple-toasts/master/docs/theme_standard.gif" alt="standard theme showcase" />
</p>

### Creative Theme

<p align="center">
<img src="https://raw.githubusercontent.com/almond-bongbong/react-simple-toasts/master/docs/theme_creative.gif" alt="creative theme showcase" />
</p>

<a name="Documentation"></a>

## Documentation 📘

Explore full [documentation](https://almond-bongbong.github.io/react-simple-toasts/) for in-depth guides, examples, and API details.

<a name="Contribute"></a>

## Contribute 🤝

Join our growing community! [Star us on GitHub](https://github.com/almond-bongbong/react-simple-toasts/stargazers) and contribute to making React Simple Toasts better.

<a name="License"></a>

## License 📜

React Simple Toasts is MIT licensed.
30 changes: 18 additions & 12 deletions __tests__/create-toast.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { createToast } from '../src';
import { ToastPosition } from '../src/lib/constants';
import { act, screen, waitForElementToBeRemoved } from '@testing-library/react';
import { act, fireEvent, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
import { generateMessage } from '../src/lib/utils';

const EXIT_ANIMATION_DURATION = 310;
const EXIT_ANIMATION_DURATION = 300;

const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

describe('createToast', () => {
it('allows creating custom toast instances with specified classNames and overriding className', async () => {
Expand All @@ -27,25 +29,27 @@ describe('createToast', () => {

it('allows creating custom toast instances with specified durations and overriding duration', async () => {
const TOAST_TEXT = generateMessage();
const DURATION = 1000;
const DURATION = 500;
const myToast = createToast({
duration: DURATION,
});
await act(() => myToast(TOAST_TEXT));

const toastElement = screen.getByText(TOAST_TEXT);
await waitForElementToBeRemoved(toastElement, {
timeout: DURATION + EXIT_ANIMATION_DURATION,
});
await waitFor(() => delay(DURATION + EXIT_ANIMATION_DURATION));
fireEvent.transitionEnd(toastElement);

expect(toastElement).not.toBeInTheDocument();

const TOAST_TEXT_2 = generateMessage();
const DURATION_2 = 500;
const DURATION_2 = 300;
await act(() => myToast(TOAST_TEXT_2, { duration: DURATION_2 }));

const toastElement2 = screen.getByText(TOAST_TEXT_2);
await waitForElementToBeRemoved(toastElement2, {
timeout: DURATION_2 + EXIT_ANIMATION_DURATION,
});
await waitFor(() => delay(DURATION_2 + EXIT_ANIMATION_DURATION));
fireEvent.transitionEnd(toastElement2);

expect(toastElement2).not.toBeInTheDocument();
});

it('allows creating custom toast instances with specified positions and overriding position', async () => {
Expand All @@ -57,14 +61,16 @@ describe('createToast', () => {
await act(() => myToast(TOAST_TEXT));

const toastElement = screen.getByText(TOAST_TEXT);
expect(toastElement.parentElement).toHaveClass(POSITION);
expect(toastElement.parentElement).toHaveClass(`toast__message--${POSITION}`);

const TOAST_TEXT_2 = generateMessage();
const POSITION_2 = ToastPosition.BOTTOM_RIGHT;
await act(() => myToast(TOAST_TEXT_2, { position: POSITION_2 }));

const overridenPositionToastElement = screen.getByText(TOAST_TEXT_2);
expect(overridenPositionToastElement.parentElement).toHaveClass(POSITION_2);
expect(overridenPositionToastElement.parentElement).toHaveClass(
`toast__message--${POSITION_2}`,
);
});

it('allows creating custom toast instances with specified clickClosable and overriding clickClosable', async () => {
Expand Down
89 changes: 52 additions & 37 deletions __tests__/toast.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import React from 'react';
import { act, render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
import themeModuleClassNames from '../src/theme/theme-classnames.json';
import toast, { toast as toastNamed } from '../src';
import { generateMessage } from '../src/lib/utils';

const EXIT_ANIMATION_DURATION = 320;
const EXIT_ANIMATION_DURATION = 300;

const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

describe('toast', () => {
it('renders a toast when the show button is clicked', async () => {
Expand All @@ -27,9 +28,11 @@ describe('toast', () => {
await act(() => toast(TOAST_TEXT, DURATION));

const toastElement = screen.getByText(TOAST_TEXT);
await waitForElementToBeRemoved(toastElement, {
timeout: DURATION + EXIT_ANIMATION_DURATION,
});

await waitFor(() => delay(DURATION + EXIT_ANIMATION_DURATION));
fireEvent.transitionEnd(toastElement);

expect(toastElement).not.toBeInTheDocument();
});

it('renders toast with infinite duration until manually closed', async () => {
Expand All @@ -38,9 +41,11 @@ describe('toast', () => {

const toastElement = screen.getByText(TOAST_TEXT);
await act(() => infiniteToast.close());
await waitForElementToBeRemoved(toastElement, {
timeout: EXIT_ANIMATION_DURATION,
});

await act(() => delay(EXIT_ANIMATION_DURATION));
fireEvent.transitionEnd(toastElement);

expect(toastElement).not.toBeInTheDocument();
});

it('renders and removes toast based on specified duration in options', async () => {
Expand All @@ -49,9 +54,10 @@ describe('toast', () => {
await act(() => toast(TOAST_TEXT, { duration: DURATION }));

const toastElement = screen.getByText(TOAST_TEXT);
await waitForElementToBeRemoved(toastElement, {
timeout: DURATION + EXIT_ANIMATION_DURATION,
});
await waitFor(() => delay(DURATION + EXIT_ANIMATION_DURATION));
fireEvent.transitionEnd(toastElement);

expect(toastElement).not.toBeInTheDocument();
});

it('applies custom className to toast container', async () => {
Expand All @@ -70,35 +76,37 @@ describe('toast', () => {
const toastDOM = screen.getByText(TOAST_TEXT);
await act(() => toastDOM.click());

await waitForElementToBeRemoved(toastDOM, {
timeout: EXIT_ANIMATION_DURATION,
});
await waitFor(() => delay(EXIT_ANIMATION_DURATION));
fireEvent.transitionEnd(toastDOM);

expect(toastDOM).not.toBeInTheDocument();
});

it('renders toast with specified position', async () => {
const TOAST_TEXT = generateMessage();
await act(() => toast(TOAST_TEXT, { position: 'top-center' }));
const toastDOM1 = screen.getByText(TOAST_TEXT);

expect(toastDOM1.parentElement).toHaveClass('top-center');
expect(toastDOM1.parentElement).toHaveClass('toast__message--top-center');
});

it('limits visible toasts based on maxVisibleToasts', async () => {
const TOAST_TEXT = generateMessage();
const DURATION = 500;

await act(() => {
toast(TOAST_TEXT);
toast(TOAST_TEXT, { maxVisibleToasts: 1 });
toast(TOAST_TEXT, DURATION);
toast(TOAST_TEXT, { duration: DURATION, maxVisibleToasts: 1 });
});

await waitFor(
() => {
const toasts = screen.getAllByText(TOAST_TEXT);
expect(toasts.length).toBe(1);
},
{
timeout: EXIT_ANIMATION_DURATION,
},
);
const toastDOM = screen.getAllByText(TOAST_TEXT);

toastDOM.forEach((toast) => {
fireEvent.transitionEnd(toast);
});

const toasts = screen.getAllByText(TOAST_TEXT);
expect(toasts.length).toBe(1);
});

it('renders custom toast content with render prop', async () => {
Expand All @@ -122,12 +130,17 @@ describe('toast', () => {

it('calls onCloseStart and onClose when toast is clicked with clickClosable set to true', async () => {
const TOAST_TEXT = generateMessage();
const DURATION = 500;
const onCloseStart = jest.fn();
const onClose = jest.fn();
await act(() => toast(TOAST_TEXT, { onCloseStart, onClose, clickClosable: true }));
await act(() =>
toast(TOAST_TEXT, { onCloseStart, onClose, clickClosable: true, duration: DURATION }),
);

const toastDOM = screen.getByText(TOAST_TEXT);
await act(() => toastDOM.click());
fireEvent.transitionEnd(toastDOM);

expect(onCloseStart).toHaveBeenCalled();
await waitFor(() => expect(onClose).toHaveBeenCalled());
});
Expand All @@ -141,9 +154,10 @@ describe('toast', () => {
toastInstance.updateDuration(NEW_DURATION);
const toastElement = screen.getByText(TOAST_TEXT);

await waitForElementToBeRemoved(toastElement, {
timeout: NEW_DURATION + EXIT_ANIMATION_DURATION,
});
await act(() => delay(NEW_DURATION));
fireEvent.transitionEnd(toastElement);

expect(toastElement).not.toBeInTheDocument();
});

it('updates the content of the displayed toast', async () => {
Expand All @@ -161,7 +175,7 @@ describe('toast', () => {
await act(() => toast(TOAST_TEXT, { theme: 'light' }));
const toastElement = screen.getByText(TOAST_TEXT);

expect(toastElement).toHaveClass(themeModuleClassNames['toast-light']);
expect(toastElement).toHaveClass('toast__light');
});

it('applies the specified zIndex to the toast', async () => {
Expand All @@ -182,9 +196,10 @@ describe('toast', () => {
);
const toastElement = screen.getByText(TOAST_TEXT);
await act(() => toastElement.click());
await waitForElementToBeRemoved(toastElement, {
timeout: EXIT_ANIMATION_DURATION,
});
await waitFor(() => delay(EXIT_ANIMATION_DURATION));
fireEvent.transitionEnd(toastElement);

expect(toastElement).not.toBeInTheDocument();
});

it('renders second toast upper than first toast when isReversedOrder set to true', async () => {
Expand All @@ -198,7 +213,7 @@ describe('toast', () => {

it('applies theme class to toast when theme is specified and does not apply it when theme is not specified', async () => {
const TOAST_TEXT = generateMessage();
const toastContentClassName = 'toast-theme-content';
const toastContentClassName = 'toast__content toast__theme-content';
await act(() => toast(TOAST_TEXT, { theme: 'dark' }));

const toastDOM = screen.getByText(TOAST_TEXT);
Expand Down Expand Up @@ -261,7 +276,7 @@ describe('toast', () => {
it('should retain the theme when updating the toast with an options object', async () => {
const TOAST_TEXT = generateMessage();
const UPDATED_TEXT = generateMessage();
const toastContentClassName = 'toast-theme-content';
const toastContentClassName = 'toast__theme-content';

const toastInstance = await act(() => toast(TOAST_TEXT, { theme: 'dark' }));

Expand Down
1 change: 1 addition & 0 deletions example/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client';
import App from './app';
import './index.css';
import { toastConfig } from 'react-simple-toasts';
import 'react-simple-toasts/dist/style.css';

import.meta.globEager('/node_modules/react-simple-toasts/dist/theme/*.css');

Expand Down
2 changes: 1 addition & 1 deletion example/src/page/api/api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function Api() {
</p>
<div className={styles.code}>
<CommonHighlighter>{`import toast from 'react-simple-toasts';

import 'react-simple-toasts/dist/style.css';
export function MyComponent() {
return (
<button onClick={() => toast('Hello, world!')}>
Expand Down
Loading