diff --git a/packages/react-docs/pages/components/table/index.page.mdx b/packages/react-docs/pages/components/table/index.page.mdx
index 4d43226c84..870fab887c 100644
--- a/packages/react-docs/pages/components/table/index.page.mdx
+++ b/packages/react-docs/pages/components/table/index.page.mdx
@@ -82,7 +82,10 @@ Below are the code snippets that demonstrate how to render the expanded row base
Cell 3
{row.getCanExpand() && (
-
+
{renderExpandedRow({ row })}
)}
@@ -102,7 +105,10 @@ Below are the code snippets that demonstrate how to render the expanded row base
borderBottom={0}
colSpan={row.getVisibleCells().length}
>
-
+
{renderExpandedRow({ row })}
diff --git a/packages/react-docs/pages/components/table/row-expanding.js b/packages/react-docs/pages/components/table/row-expanding.js
index f941f2b27b..a8c26e0720 100644
--- a/packages/react-docs/pages/components/table/row-expanding.js
+++ b/packages/react-docs/pages/components/table/row-expanding.js
@@ -240,7 +240,10 @@ const App = () => {
})}
{(row.getCanExpand() && layout === 'flexbox') && (
-
+
{renderExpandedRow({ row })}
)}
@@ -251,7 +254,10 @@ const App = () => {
borderBottom={0}
colSpan={row.getVisibleCells().length}
>
-
+
{renderExpandedRow({ row })}
diff --git a/packages/react-docs/pages/components/toast-manager/index.page.mdx b/packages/react-docs/pages/components/toast-manager/index.page.mdx
index 428ba5b441..791011fa4b 100644
--- a/packages/react-docs/pages/components/toast-manager/index.page.mdx
+++ b/packages/react-docs/pages/components/toast-manager/index.page.mdx
@@ -44,7 +44,7 @@ import { useToastManager } from '@tonic-ui/react';
function MyComponent() {
const toast = useToastManager();
const handleClickOpenToast = () => {
- const render = ({ onClose, placement }) => {
+ const render = ({ id, data, onClose, placement }) => {
const isTop = placement.includes('top');
const toastSpacingKey = isTop ? 'pb' : 'pt';
const styleProps = {
@@ -54,7 +54,7 @@ function MyComponent() {
return (
-
+
This is a toast notification
@@ -63,6 +63,7 @@ function MyComponent() {
const options = {
placement: 'bottom-right',
duration: 5000,
+ data: {}, // user-defined data
};
toast(render, options);
};
@@ -75,19 +76,19 @@ function MyComponent() {
}
```
-The `toast` method takes a function that returns the toast element to be displayed. The function can also receive an `onClose` function and the `placement` string. The `onClose` function can be used to remove the toast when the user clicks on a close button or after a certain time period.
+The `toast` method accepts a function that generates the toast element to display. This function receives `id`, `data`, `onClose`, and `placement` as arguments.
```jsx disabled
-const id = toast(({ onClose, placement }) => (
+toast(({ id, data, onClose, placement }) => (
This is a toast notification
));
```
-To remove a toast, you can either call the `onClose` function or use the `toast.remove` method, which takes the toast's unique id as an argument.
+To remove a toast, you can use the `onClose` function, triggered by the user clicking a close button. Alternatively, you can use the `toast.remove` method, passing the toast's unique id as an argument.
-```jsx disabled
+```js
toast.remove(id);
```
diff --git a/packages/react-docs/pages/components/toast-manager/useToastManager.js b/packages/react-docs/pages/components/toast-manager/useToastManager.js
index aa25040118..b7e3ae43eb 100644
--- a/packages/react-docs/pages/components/toast-manager/useToastManager.js
+++ b/packages/react-docs/pages/components/toast-manager/useToastManager.js
@@ -10,7 +10,7 @@ import React, { useCallback } from 'react';
const App = () => {
const toast = useToastManager();
const handleClickOpenToast = useCallback(() => {
- const render = ({ onClose, placement }) => {
+ const render = ({ id, data, onClose, placement }) => {
const isTop = placement.includes('top');
const toastSpacingKey = isTop ? 'pb' : 'pt';
const styleProps = {
@@ -20,7 +20,7 @@ const App = () => {
return (
-
+ This is a toast notification
@@ -28,6 +28,9 @@ const App = () => {
};
const options = {
placement: 'bottom-right',
+ data: {
+ foo: 'bar',
+ },
duration: 5000,
};
toast(render, options);
diff --git a/packages/react-docs/pages/components/toast-manager/useToastManager.page.mdx b/packages/react-docs/pages/components/toast-manager/useToastManager.page.mdx
index 7c040940c3..9ce0d71f6f 100644
--- a/packages/react-docs/pages/components/toast-manager/useToastManager.page.mdx
+++ b/packages/react-docs/pages/components/toast-manager/useToastManager.page.mdx
@@ -16,24 +16,24 @@ const toast = useToastManager();
The `useToastManager` Hook returns an object with the following methods and properties:
-### toast(message, [options={'{}'}])
+### toast(content, [options={'{}'}])
Create a toast at the specified placement and return the toast id.
#### Aliases
-
*toast.notify(message, [options={'{}'}])*
+
*toast.notify(content, [options={'{}'}])*
#### Parameters
-
`message` *(Function|string)*: The toast message to render.
+
`content` *(Function)*: The toast content to render.
`[options={}]` *(Object)*: The options object.
`[options.data]` *(any)*: The user-defined data supplied to the toast.
`[options.duration=null]` *(number)*: The duration (in milliseconds) that the toast should remain on the screen. If set to null, toast will never dismiss.
-
`[options.id]` *(string)*: A unique ID of the toast.
+
`[options.id]` *(string)*: A unique identifier that represents the toast.
`[options.placement]` *(string)*: The placement of the toast.
@@ -117,8 +117,9 @@ Update a specific toast with new options based on the given toast id.
`id` *(string)*: The id to update the toast.
`[options={}]` *(Object)*: The options object.
+
`[options.content]` *(Function)*: The toast content to render.
+
`[options.data]` *(any)*: The user-defined data supplied to the toast.
`[options.duration=null]` *(number)*: The duration (in milliseconds) that the toast should remain on the screen. If set to null, toast will never dismiss.
-
`[options.message]` *(Function|string)*: The toast message to render.
#### Returns
@@ -133,7 +134,7 @@ Specify the placement to place the toast. The default placement will be used if
### toast.setState(state | updater)
-The `setState` method is used to modify the internal state of the toast manager. It provides the ability to add, remove, or update toast messages.
+The `setState` method is used to modify the internal state of the toast manager. It provides the ability to add, remove, or update toast content.
#### Parameters
@@ -155,10 +156,13 @@ Example usage with a state object:
toast.setState({
'top': [
{
- id: '2',
+ id: 1,
+ content: ({ id, data, onClose, placement }) => (
+
+ This is a toast
+
+ ),
duration: 3000,
- message: 'New toast message',
- onClose: () => toast.close('2'),
placement: 'top',
}
],
@@ -178,10 +182,13 @@ toast.setState(prevState => ({
'top': [
...prevState['top'],
{
- id: '2',
+ id: 1,
+ content: ({ id, data, onClose, placement }) => (
+
+ This is a toast
+
+ ),
duration: null,
- message: 'New toast message',
- onClose: () => toast.close('2', 'top'),
placement: 'top',
},
],
@@ -196,10 +203,16 @@ The toast state is a placement object, each placement contains an array of objec
{
'top': [
{
- id: '1', // A unique identifier that represents the toast message
+ id: 1, // A unique identifier that represents the toast
+ content: ({ id, data, onClose, placement }) => (
+
+ This is a toast
+
+ ),
+ data: {
+ // The user-defined data supplied to the toast
+ },
duration: null, // The duration (in milliseconds) that the toast should remain on the screen. If set to null, toast will never dismiss.
- message: ({ id, onClose, placement }) => , // The toast message to render
- onClose: () => toast.close(id, placement), // The function to close the toast
placement: 'top', // The placement of the toast
},
],
diff --git a/packages/react-docs/pages/components/toast/drawer-toast.js b/packages/react-docs/pages/components/toast/drawer-toast.js
deleted file mode 100644
index 1187f5a45c..0000000000
--- a/packages/react-docs/pages/components/toast/drawer-toast.js
+++ /dev/null
@@ -1,208 +0,0 @@
-import {
- Box,
- Button,
- Divider,
- Flex,
- Grid,
- DrawerContent,
- DrawerHeader,
- DrawerBody,
- DrawerFooter,
- Skeleton,
- Space,
- Stack,
- Text,
- Toast,
- ToastController,
- ToastTransition,
- useColorStyle,
-} from '@tonic-ui/react';
-import { CloseSIcon } from '@tonic-ui/react-icons';
-import React, { useState } from 'react';
-import { TransitionGroup } from 'react-transition-group';
-
-const MAX_TOASTS = 1;
-
-const CustomToastContainer = (props) => (
-
-);
-
-let autoIncrementIndex = 0;
-
-const App = () => {
- const [colorStyle] = useColorStyle();
- const [toasts, setToasts] = useState([]);
-
- const notify = (options) => {
- const {
- appearance,
- content,
- duration = null,
- isClosable = true,
- } = { ...options };
-
- setToasts(prevState => {
- const id = ++autoIncrementIndex;
- const onClose = () => {
- setToasts(toasts => toasts.filter(x => x.id !== id));
- };
- // You can decide how many toasts you want to show at the same time depending on your use case
- const nextState = [
- ...prevState.slice(MAX_TOASTS > 1 ? -(MAX_TOASTS - 1) : prevState.length),
- {
- id,
- appearance,
- content,
- duration,
- isClosable,
- onClose,
- },
- ];
- return nextState;
- });
- };
-
- const closeAll = () => {
- setToasts([]);
- };
-
- const handleClickAddToastByAppearance = (appearance) => (event) => {
- const content = {
- success: (
- <>
- This is a success message.
- The toast will be automatically dismissed after 5 seconds.
- >
- ),
- info: (
- <>
- This is an info message.
- The toast will remain visible until the user dismisses it.
- >
- ),
- warning: (
- <>
- This is a warning message.
- The toast will remain visible until the user dismisses it.
- >
- ),
- error: (
- <>
- This is an error message.
- The toast will remain visible until the user dismisses it.
- >
- ),
- }[appearance];
-
- notify({
- appearance,
- content,
- duration: (appearance === 'success') ? 5000 : undefined,
- });
- };
-
- const handleClickCloseToasts = () => {
- closeAll();
- };
-
- return (<>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ` element
- >
- {toasts.map(toast => (
-
-
-
- {toast?.content}
-
-
-
- ))}
-
-
-
- Drawer
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >);
-};
-
-export default App;
diff --git a/packages/react-docs/pages/components/toast/drawer-with-toast-notification.js b/packages/react-docs/pages/components/toast/drawer-with-toast-notification.js
new file mode 100644
index 0000000000..d3412d9082
--- /dev/null
+++ b/packages/react-docs/pages/components/toast/drawer-with-toast-notification.js
@@ -0,0 +1,213 @@
+import {
+ Box,
+ Button,
+ Divider,
+ Flex,
+ Grid,
+ DrawerContent,
+ DrawerHeader,
+ DrawerBody,
+ DrawerFooter,
+ Skeleton,
+ Space,
+ Stack,
+ Text,
+ Toast,
+ ToastController,
+ ToastTransition,
+ ToastTransitionGroup,
+ useColorStyle,
+} from '@tonic-ui/react';
+import { CloseSIcon } from '@tonic-ui/react-icons';
+import React, { useState } from 'react';
+
+const MAX_TOASTS = 1;
+
+const InlineToastContainer = (props) => (
+
+);
+
+let autoIncrementIndex = 0;
+
+const App = () => {
+ const [colorStyle] = useColorStyle();
+ const [toasts, setToasts] = useState([]);
+
+ const notify = (options) => {
+ const {
+ appearance,
+ content,
+ duration,
+ } = { ...options };
+
+ setToasts(prevState => {
+ const id = ++autoIncrementIndex;
+ // You can decide how many toasts you want to show at the same time depending on your use case
+ const nextState = [
+ ...prevState.slice(MAX_TOASTS > 1 ? -(MAX_TOASTS - 1) : prevState.length),
+ {
+ id,
+ appearance,
+ content,
+ duration,
+ },
+ ];
+ return nextState;
+ });
+ };
+
+ const closeToast = (id) => {
+ setToasts(toasts => toasts.filter(x => x.id !== id));
+ };
+
+ const closeAll = () => {
+ setToasts([]);
+ };
+
+ const handleClickAddToastByAppearance = (appearance) => (event) => {
+ const content = {
+ success: (
+ <>
+ This is a success message.
+ The toast will be automatically dismissed after 5 seconds.
+ >
+ ),
+ info: (
+ <>
+ This is an info message.
+ The toast will be automatically dismissed after 5 seconds.
+ >
+ ),
+ warning: (
+ <>
+ This is a warning message.
+ The toast will remain visible until the user dismisses it.
+ >
+ ),
+ error: (
+ <>
+ This is an error message.
+ The toast will remain visible until the user dismisses it.
+ >
+ ),
+ }[appearance];
+
+ notify({
+ appearance,
+ content,
+ duration: (appearance === 'success' || appearance === 'info') ? 5000 : undefined,
+ });
+ };
+
+ const createCloseToastHandler = (id) => () => {
+ closeToast(id);
+ };
+
+ const handleClickCloseToasts = () => {
+ closeAll();
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {toasts.map(toast => {
+ const onClose = createCloseToastHandler(toast.id);
+ return (
+
+
+
+ {toast.content}
+
+
+
+ );
+ })}
+
+
+
+ Drawer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default App;
diff --git a/packages/react-docs/pages/components/toast/index.page.mdx b/packages/react-docs/pages/components/toast/index.page.mdx
index ec00760efd..dadc44a378 100644
--- a/packages/react-docs/pages/components/toast/index.page.mdx
+++ b/packages/react-docs/pages/components/toast/index.page.mdx
@@ -10,6 +10,7 @@ import {
ToastCloseButton,
ToastController,
ToastTransition,
+ ToastTransitionGroup,
} from '@tonic-ui/react';
```
@@ -78,11 +79,137 @@ The placement and size of toasts are typically determined by the design of the a
In this example, the toast will be positioned 48 pixels from the top of the modal or drawer, and has a minimum width of 280 pixels. If the content of the toast message is wider than 280 pixels, the toast will expand to fit the content without exceeding 80% of the width of the modal or drawer in which it is being displayed.
-To animate the toast when it is displayed or dismissed, you can use the `ToastTransition` component. The `ToastController` component can also be used to control the duration for which the toast will be displayed before it is automatically dismissed. This allows you to set a specific amount of time for the toast to be visible, ensuring that it does not interrupt the user's workflow for too long.
+```jsx
+const InlineToastContainer = (props) => (
+
+);
+```
+
+```jsx
+
+ This is a success message.
+
+```
+
+To animate the toast when it is displayed or dismissed, you can utilize `ToastTransitionController` to manage the duration the toast is shown before it is automatically dismissed. This enables you to specify a set amount of time for the toast to remain visible. To implement this, define an array of toasts with `const [toasts, setToasts] = useState([])`, where each toast object can include the following properties:
+
+| Name | Type | Description |
+| :--- | :--- | :---------- |
+| id | string \| number | A unique identifier for the toast. |
+| appearance | string | The appearance of the toast. |
+| content | ReactNode | The content displayed within the toast. |
+| duration | number | (Optional) Defines how long (in milliseconds) the toast should remain visible before automatically dismissing. Pass `null` to keep the toast visible indefinitely. |
+
+Example toast object:
+```jsx
+{
+ id: 1,
+ appearance: 'success',
+ content: (
+ This is a success message.
+ ),
+ duration: 5000, // auto dismiss after 5 seconds
+}
+```
+
+Here is a complete example:
+```js
+const idRef = useRef(0);
+const [toasts, setToasts] = useState([]);
+const notify = (options) => {
+ const {
+ appearance,
+ content,
+ duration,
+ } = { ...options };
+
+ setToasts(prevState => {
+ const id = ++(idRef.current);
+ // You can decide how many toasts you want to show at the same time depending on your use case
+ const nextState = [
+ ...prevState.slice(MAX_TOASTS > 1 ? -(MAX_TOASTS - 1) : prevState.length),
+ {
+ id,
+ appearance,
+ content,
+ duration,
+ },
+ ];
+ return nextState;
+ });
+};
+const closeToast = (id) => {
+ setToasts(toasts => toasts.filter(x => x.id !== id));
+};
+const createCloseToastHandler = (id) => () => {
+ closeToast(id);
+};
+```
-{render('./modal-toast')}
+```jsx
+
+
+ {toasts.map(toast => {
+ const onClose = createCloseToastHandler(toast.id);
+ return (
+
+
+
+ {toast.content}
+
+
+
+ );
+ })}
+
+
+```
+
+#### Modal with toast notification
+
+{render('./modal-with-toast-notification')}
-{render('./drawer-toast')}
+#### Drawer with multiple toasts
+
+{render('./drawer-with-toast-notification')}
## Props
@@ -106,7 +233,7 @@ To animate the toast when it is displayed or dismissed, you can use the `ToastTr
| Name | Type | Default | Description |
| :--- | :--- | :------ | :---------- |
-| children | ReactNode | | |
+| children | ReactNode | A function child can be used intead of a React element. This function is invoked with an object that includes the `onClose` prop. |
| duration | number | null | The duration in milliseconds after which the toast will be automatically closed. Set to `null` to disable auto-closing. |
| onClose | function | | A callback called when the toast is being closed. |
@@ -121,3 +248,12 @@ To animate the toast when it is displayed or dismissed, you can use the `ToastTr
| mountOnEnter | boolean | | If `true`, it will "lazy mount" the component on the first `in={true}`. After the first enter transition the component will stay mounted, even on the 'exited' state, unless you also specify `unmountOnExit`. By default the child component is mounted immediately along with the parent transition component. |
| timeout | number \| `{ appear?: number, enter?: number, exit?: number }` | `{ enter: duration.standard, exit: duration.standard }` | The duration for the transition, in milliseconds. You may specify a single timeout for all transitions, or individually with an object. |
| unmountOnExit | boolean | | If `true`, it will unmount the child component when `in={false}` and the animation has finished. By default the child component stays mounted after it reaches the 'exited' state. |
+
+### ToastTransitionGroup
+
+| Name | Type | Default | Description |
+| :--- | :--- | :------ | :---------- |
+| children | any | | A set of `` components, that are toggled `in` and `out` as they leave. |
+| appear | boolean | | A convenience prop that enables or disables appear animations for all children. Note that specifying this will override any defaults set on individual children Transitions. |
+| enter | boolean | | A convenience prop that enables or disables enter animations for all children. Note that specifying this will override any defaults set on individual children Transitions. |
+| exit | boolean | | A convenience prop that enables or disables exit animations for all children. Note that specifying this will override any defaults set on individual children Transitions. |
diff --git a/packages/react-docs/pages/components/toast/modal-toast.js b/packages/react-docs/pages/components/toast/modal-toast.js
deleted file mode 100644
index 5e3c5009b9..0000000000
--- a/packages/react-docs/pages/components/toast/modal-toast.js
+++ /dev/null
@@ -1,208 +0,0 @@
-import {
- Box,
- Button,
- Divider,
- Flex,
- Grid,
- ModalContent,
- ModalHeader,
- ModalBody,
- ModalFooter,
- Skeleton,
- Space,
- Stack,
- Text,
- Toast,
- ToastController,
- ToastTransition,
- useColorStyle,
-} from '@tonic-ui/react';
-import { CloseSIcon } from '@tonic-ui/react-icons';
-import React, { useState } from 'react';
-import { TransitionGroup } from 'react-transition-group';
-
-const MAX_TOASTS = 1;
-
-const CustomToastContainer = (props) => (
-
-);
-
-let autoIncrementIndex = 0;
-
-const App = () => {
- const [colorStyle] = useColorStyle();
- const [toasts, setToasts] = useState([]);
-
- const notify = (options) => {
- const {
- appearance,
- content,
- duration = null,
- isClosable = true,
- } = { ...options };
-
- setToasts(prevState => {
- const id = ++autoIncrementIndex;
- const onClose = () => {
- setToasts(toasts => toasts.filter(x => x.id !== id));
- };
- // You can decide how many toasts you want to show at the same time depending on your use case
- const nextState = [
- ...prevState.slice(MAX_TOASTS > 1 ? -(MAX_TOASTS - 1) : prevState.length),
- {
- id,
- appearance,
- content,
- duration,
- isClosable,
- onClose,
- },
- ];
- return nextState;
- });
- };
-
- const closeAll = () => {
- setToasts([]);
- };
-
- const handleClickAddToastByAppearance = (appearance) => (event) => {
- const content = {
- success: (
- <>
- This is a success message.
- The toast will be automatically dismissed after 5 seconds.
- >
- ),
- info: (
- <>
- This is an info message.
- The toast will remain visible until the user dismisses it.
- >
- ),
- warning: (
- <>
- This is a warning message.
- The toast will remain visible until the user dismisses it.
- >
- ),
- error: (
- <>
- This is an error message.
- The toast will remain visible until the user dismisses it.
- >
- ),
- }[appearance];
-
- notify({
- appearance,
- content,
- duration: (appearance === 'success') ? 5000 : undefined,
- });
- };
-
- const handleClickCloseToasts = () => {
- closeAll();
- };
-
- return (<>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ` element
- >
- {toasts.map(toast => (
-
-
-
- {toast?.content}
-
-
-
- ))}
-
-
-
- Modal
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >);
-};
-
-export default App;
diff --git a/packages/react-docs/pages/components/toast/modal-with-toast-notification.js b/packages/react-docs/pages/components/toast/modal-with-toast-notification.js
new file mode 100644
index 0000000000..abe9969b61
--- /dev/null
+++ b/packages/react-docs/pages/components/toast/modal-with-toast-notification.js
@@ -0,0 +1,213 @@
+import {
+ Box,
+ Button,
+ Divider,
+ Flex,
+ Grid,
+ ModalContent,
+ ModalHeader,
+ ModalBody,
+ ModalFooter,
+ Skeleton,
+ Space,
+ Stack,
+ Text,
+ Toast,
+ ToastController,
+ ToastTransition,
+ ToastTransitionGroup,
+ useColorStyle,
+} from '@tonic-ui/react';
+import { CloseSIcon } from '@tonic-ui/react-icons';
+import React, { useState } from 'react';
+
+const MAX_TOASTS = 1;
+
+const InlineToastContainer = (props) => (
+
+);
+
+let autoIncrementIndex = 0;
+
+const App = () => {
+ const [colorStyle] = useColorStyle();
+ const [toasts, setToasts] = useState([]);
+
+ const notify = (options) => {
+ const {
+ appearance,
+ content,
+ duration,
+ } = { ...options };
+
+ setToasts(prevState => {
+ const id = ++autoIncrementIndex;
+ // You can decide how many toasts you want to show at the same time depending on your use case
+ const nextState = [
+ ...prevState.slice(MAX_TOASTS > 1 ? -(MAX_TOASTS - 1) : prevState.length),
+ {
+ id,
+ appearance,
+ content,
+ duration,
+ },
+ ];
+ return nextState;
+ });
+ };
+
+ const closeToast = (id) => {
+ setToasts(toasts => toasts.filter(x => x.id !== id));
+ };
+
+ const closeAll = () => {
+ setToasts([]);
+ };
+
+ const handleClickAddToastByAppearance = (appearance) => (event) => {
+ const content = {
+ success: (
+ <>
+ This is a success message.
+ The toast will be automatically dismissed after 5 seconds.
+ >
+ ),
+ info: (
+ <>
+ This is an info message.
+ The toast will be automatically dismissed after 5 seconds.
+ >
+ ),
+ warning: (
+ <>
+ This is a warning message.
+ The toast will remain visible until the user dismisses it.
+ >
+ ),
+ error: (
+ <>
+ This is an error message.
+ The toast will remain visible until the user dismisses it.
+ >
+ ),
+ }[appearance];
+
+ notify({
+ appearance,
+ content,
+ duration: (appearance === 'success' || appearance === 'info') ? 5000 : undefined,
+ });
+ };
+
+ const createCloseToastHandler = (id) => () => {
+ closeToast(id);
+ };
+
+ const handleClickCloseToasts = () => {
+ closeAll();
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {toasts.map(toast => {
+ const onClose = createCloseToastHandler(toast.id);
+ return (
+
+
+
+ {toast.content}
+
+
+
+ );
+ })}
+
+
+
+ Modal
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default App;
diff --git a/packages/react/__tests__/index.test.js b/packages/react/__tests__/index.test.js
index 0d2c1f5c2e..f83db6a4b4 100644
--- a/packages/react/__tests__/index.test.js
+++ b/packages/react/__tests__/index.test.js
@@ -242,6 +242,7 @@ test('should match expected exports', () => {
'ToastManager',
'ToastMessage',
'ToastTransition',
+ 'ToastTransitionGroup',
'useToastManager',
// tooltip
diff --git a/packages/react/src/menu/MenuToggleIcon.js b/packages/react/src/menu/MenuToggleIcon.js
index 781e52ee1e..905aa4641c 100644
--- a/packages/react/src/menu/MenuToggleIcon.js
+++ b/packages/react/src/menu/MenuToggleIcon.js
@@ -7,7 +7,6 @@ import {
} from '@tonic-ui/react-icons';
import { ariaAttr, createTransitionStyle, getEnterTransitionProps, getExitTransitionProps, reflow, transitionEasing } from '@tonic-ui/utils';
import React, { forwardRef, useEffect, useRef } from 'react';
-import { isValidElementType } from 'react-is';
import { Transition } from 'react-transition-group';
import { Box } from '../box';
import { useDefaultProps } from '../default-props';
@@ -129,7 +128,7 @@ const MenuToggleIcon = forwardRef((inProps, ref) => {
{...childProps}
style={style}
>
- {children ?? (isValidElementType(IconComponent) && )}
+ {children ?? (!!IconComponent && )}
);
}}
diff --git a/packages/react/src/stack/Stack.js b/packages/react/src/stack/Stack.js
index 969c838465..aca56c61f4 100644
--- a/packages/react/src/stack/Stack.js
+++ b/packages/react/src/stack/Stack.js
@@ -1,4 +1,4 @@
-import React, { forwardRef } from 'react';
+import React, { forwardRef, isValidElement } from 'react';
import { Box } from '../box';
import { useDefaultProps } from '../default-props';
import StackItem from './StackItem';
@@ -21,13 +21,13 @@ const Stack = forwardRef((inProps, ref) => {
// Filter only the valid children of a component, and ignore any nullish or falsy child.
const validChildren = React.Children
.toArray(children)
- .filter(c => React.isValidElement(c));
+ .filter(c => isValidElement(c));
let clones = validChildren;
if (shouldWrapChildren) {
clones = validChildren.map((child, index) => {
// Use the provided child key, otherwise use the index as fallback
- const key = (typeof child.key !== 'undefined') ? child.key : index;
+ const key = (child?.key) ?? index;
return (
diff --git a/packages/react/src/stack/__tests__/Stack.test.js b/packages/react/src/stack/__tests__/Stack.test.js
new file mode 100644
index 0000000000..51e9e28866
--- /dev/null
+++ b/packages/react/src/stack/__tests__/Stack.test.js
@@ -0,0 +1,97 @@
+import { screen } from '@testing-library/react';
+import { testA11y } from '@tonic-ui/react/test-utils/accessibility';
+import { render } from '@tonic-ui/react/test-utils/render';
+import { Box, Stack } from '@tonic-ui/react/src';
+import React from 'react';
+
+describe('Stack', () => {
+ it('should render correctly', async () => {
+ const renderOptions = {
+ useCSSVariables: true,
+ };
+ const { container } = render((
+
+
+
+
+
+ ), renderOptions);
+
+ expect(container).toMatchSnapshot();
+
+ await testA11y(container);
+ });
+
+ it('should apply the default direction of column', () => {
+ render(
+
+
+
+
+ );
+
+ const stack = screen.getByTestId('stack');
+ expect(stack).toHaveStyle('flex-direction: column');
+ });
+
+ it('should apply the custom direction when specified', () => {
+ render(
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+
+ expect(screen.getByTestId('stack-row')).toHaveStyle('flex-direction: row');
+ expect(screen.getByTestId('stack-column')).toHaveStyle('flex-direction: column');
+ });
+
+ it('should apply spacing between child elements', () => {
+ const renderOptions = {
+ useCSSVariables: true,
+ };
+ render((
+
+
+
+
+ ), renderOptions);
+
+ const stack = screen.getByTestId('stack');
+ expect(stack).toHaveStyle('row-gap: var(--tonic-sizes-2x)');
+ });
+
+ it('should not wrap each child by default when shouldWrapChildren is false', () => {
+ render(
+
+
+
+
+ );
+
+ const stack = screen.getByTestId('stack');
+ expect(stack.children).toHaveLength(2);
+ expect(stack.children[0]).toHaveAttribute('data-testid', 'stack-item-1');
+ expect(stack.children[1]).toHaveAttribute('data-testid', 'stack-item-2');
+ });
+
+ it('should wrap each child when shouldWrapChildren is true', () => {
+ render(
+
+
+
+
+ );
+
+ const stack = screen.getByTestId('stack');
+ expect(stack.children).toHaveLength(2);
+ expect(stack.children[0].firstChild).toHaveAttribute('data-testid', 'stack-item-1');
+ expect(stack.children[1].firstChild).toHaveAttribute('data-testid', 'stack-item-2');
+ });
+});
diff --git a/packages/react/src/stack/__tests__/__snapshots__/Stack.test.js.snap b/packages/react/src/stack/__tests__/__snapshots__/Stack.test.js.snap
new file mode 100644
index 0000000000..5d6950ee58
--- /dev/null
+++ b/packages/react/src/stack/__tests__/__snapshots__/Stack.test.js.snap
@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Stack should render correctly 1`] = `
+.emotion-0 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: column;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ row-gap: var(--tonic-sizes-4x);
+}
+
+
+
+
+
+
+
+
+`;
diff --git a/packages/react/src/toast/ToastController.js b/packages/react/src/toast/ToastController.js
index 9d4ff05614..b8ca927b93 100644
--- a/packages/react/src/toast/ToastController.js
+++ b/packages/react/src/toast/ToastController.js
@@ -7,24 +7,23 @@ import useTimeout from '../utils/useTimeout';
const ToastController = forwardRef((inProps, ref) => {
const {
- children,
- duration = null,
- onClose,
+ duration: durationProp = null,
+ onClose: onCloseProp,
onMouseEnter: onMouseEnterProp,
onMouseLeave: onMouseLeaveProp,
...rest
} = useDefaultProps({ props: inProps, name: 'ToastController' });
const nodeRef = useRef();
const combinedRef = useMergeRefs(nodeRef, ref);
- const [delay, setDelay] = useState(duration);
+ const [delay, setDelay] = useState(durationProp);
const onMouseEnter = useCallback((event) => {
setDelay(null);
}, []);
const onMouseLeave = useCallback((event) => {
- setDelay(duration);
- }, [duration]);
+ setDelay(durationProp);
+ }, [durationProp]);
- useTimeout(onClose, delay);
+ useTimeout(onCloseProp, delay);
return (
{
onMouseEnter={callEventHandlers(onMouseEnterProp, onMouseEnter)}
onMouseLeave={callEventHandlers(onMouseLeaveProp, onMouseLeave)}
{...rest}
- >
- {children}
-
+ />
);
});
diff --git a/packages/react/src/toast/ToastManager.js b/packages/react/src/toast/ToastManager.js
index 2d8b121452..cc4f82042a 100644
--- a/packages/react/src/toast/ToastManager.js
+++ b/packages/react/src/toast/ToastManager.js
@@ -1,17 +1,15 @@
import { useHydrated } from '@tonic-ui/react-hooks';
-import { runIfFn } from '@tonic-ui/utils';
+import { isNullish, runIfFn } from '@tonic-ui/utils';
import { ensureArray, ensureString } from 'ensure-type';
import memoize from 'micro-memoize';
import React, { useCallback, useState } from 'react';
-import { isElement, isValidElementType } from 'react-is';
-import {
- TransitionGroup,
-} from 'react-transition-group';
import { useDefaultProps } from '../default-props';
import { Portal } from '../portal';
+import { renderComponentOrValue } from '../utils/renderComponentOrValue';
import ToastContainer from './ToastContainer';
import ToastController from './ToastController';
import ToastTransition from './ToastTransition';
+import ToastTransitionGroup from './ToastTransitionGroup';
import { ToastManagerContext } from './context';
const uniqueId = (() => {
@@ -43,6 +41,8 @@ const getToastPlacementByState = (state, id) => {
const ToastManager = (inProps) => {
const {
+ ToastContainerComponent = ToastContainer,
+ ToastContainerProps,
TransitionComponent = ToastTransition,
TransitionProps,
children,
@@ -57,37 +57,6 @@ const ToastManager = (inProps) => {
}, {})
));
- /**
- * Create properties for a new toast
- */
- const createToast = useCallback((message, options) => {
- const id = options?.id ?? uniqueId();
- const data = options?.data;
- const duration = options?.duration;
- const placement = ensureString(options?.placement ?? placementProp);
- const onClose = () => close(id, placement);
-
- return {
- // A unique identifier that represents the toast message
- id,
-
- // The user-defined data supplied to the toast
- data,
-
- // The toast message to render
- message,
-
- // The placement of the toast
- placement,
-
- // The duration (in milliseconds) that the toast should remain on the screen. If set to null, toast will never dismiss.
- duration,
-
- // The function to close the toast
- onClose,
- };
- }, [close, placementProp]);
-
/**
* Close a toast record at its placement
*/
@@ -145,11 +114,43 @@ const ToastManager = (inProps) => {
return ensureArray(state[placement]).findIndex((toast) => toast.id === id);
}, [state]);
+ /**
+ * Update a specific toast with new options based on the given id. Returns `true` if the toast exists, else `false`.
+ */
+ const update = useCallback((id, options) => {
+ const placement = find(id)?.placement;
+ const index = findIndex(id);
+
+ if (!placement || index === -1) {
+ return false;
+ }
+
+ setState((prevState) => {
+ const nextState = { ...prevState };
+ nextState[placement][index] = {
+ ...nextState[placement][index],
+ ...options,
+ };
+ return nextState;
+ });
+
+ return true;
+ }, [find, findIndex]);
+
/**
* Create a toast at the specified placement and return the id
*/
- const notify = useCallback((message, options) => {
- const toast = createToast(message, options);
+ const notify = useCallback((content, options) => {
+ // A unique identifier that represents the toast
+ const id = options?.id ?? uniqueId();
+ // The user-defined data supplied to the toast
+ const data = options?.data;
+ // The duration (in milliseconds) that the toast should remain on the screen. If set to null, toast will never dismiss.
+ const duration = options?.duration;
+ // The placement of the toast
+ const placement = ensureString(options?.placement ?? placementProp);
+
+ const toast = Object.freeze({ id, content, data, duration, placement });
if (!placements.includes(toast.placement)) {
console.error(`[ToastManager] Error: Invalid toast placement "${toast.placement}". Please provide a valid placement from the following options: ${placements.join(', ')}.`);
@@ -190,30 +191,7 @@ const ToastManager = (inProps) => {
});
return toast.id;
- }, [createToast]);
-
- /**
- * Update a specific toast with new options based on the given id. Returns `true` if the toast exists, else `false`.
- */
- const update = useCallback((id, options) => {
- const placement = find(id)?.placement;
- const index = findIndex(id);
-
- if (!placement || index === -1) {
- return false;
- }
-
- setState((prevState) => {
- const nextState = { ...prevState };
- nextState[placement][index] = {
- ...nextState[placement][index],
- ...options,
- };
- return nextState;
- });
-
- return true;
- }, [find, findIndex]);
+ }, [placementProp]);
const context = getMemoizedState({
// Methods
@@ -221,8 +199,8 @@ const ToastManager = (inProps) => {
closeAll,
find,
findIndex,
- notify,
update,
+ notify,
// Properties
placement: placementProp,
@@ -232,6 +210,10 @@ const ToastManager = (inProps) => {
setState,
});
+ const createCloseToastHandler = (id, placement) => () => {
+ close(id, placement);
+ };
+
return (
{runIfFn(children, context)}
@@ -240,45 +222,39 @@ const ToastManager = (inProps) => {
containerRef={containerRef}
>
{Object.keys(state).map((placement) => {
- const toasts = ensureArray(state[placement]);
+ const toasts = ensureArray(state[placement]).filter(toast => !isNullish(toast));
return (
-
-
- {toasts.map((toast) => (
-
-
+ {toasts.map((toast) => {
+ const onClose = createCloseToastHandler(toast.id, placement);
+ return (
+
- {(() => {
- if (isElement(toast.message)) {
- return toast.message;
- }
- if (isValidElementType(toast.message)) {
- return (
-
- );
- }
- return null;
- })()}
-
-
- ))}
-
-
+
+ {renderComponentOrValue(toast.content, {
+ id: toast.id,
+ data: toast.data,
+ onClose,
+ placement: toast.placement,
+ })}
+
+
+ );
+ })}
+
+
);
})}
diff --git a/packages/react/src/toast/ToastTransitionGroup.js b/packages/react/src/toast/ToastTransitionGroup.js
new file mode 100644
index 0000000000..2b0feadfe5
--- /dev/null
+++ b/packages/react/src/toast/ToastTransitionGroup.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import {
+ TransitionGroup,
+} from 'react-transition-group';
+import { useDefaultProps } from '../default-props';
+
+const ToastTransitionGroup = (inProps) => {
+ const props = useDefaultProps({ props: inProps, name: 'ToastTransitionGroup' });
+
+ return (
+ element
+ {...props}
+ />
+ );
+};
+
+ToastTransitionGroup.displayName = 'ToastTransitionGroup';
+
+export default ToastTransitionGroup;
diff --git a/packages/react/src/toast/__tests__/Toast.test.js b/packages/react/src/toast/__tests__/Toast.test.js
index 63e0fe72da..e925ec5c38 100644
--- a/packages/react/src/toast/__tests__/Toast.test.js
+++ b/packages/react/src/toast/__tests__/Toast.test.js
@@ -1,10 +1,43 @@
import { screen, waitForElementToBeRemoved } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from '@tonic-ui/react/test-utils/render';
-import { Collapse, Toast } from '@tonic-ui/react/src';
+import {
+ Button,
+ Collapse,
+ Flex,
+ Grid,
+ ModalContent,
+ ModalHeader,
+ ModalBody,
+ ModalFooter,
+ Skeleton,
+ Stack,
+ Text,
+ Toast,
+ ToastCloseButton,
+ ToastController,
+ ToastTransition,
+ ToastTransitionGroup,
+ useColorStyle,
+} from '@tonic-ui/react/src';
import { useToggle } from '@tonic-ui/react-hooks/src';
import { callEventHandlers, transitionDuration } from '@tonic-ui/utils/src';
-import React from 'react';
+import React, { useState } from 'react';
+
+const InlineToastContainer = (props) => (
+
+);
describe('Toast', () => {
it('should render correctly', async () => {
@@ -42,4 +75,106 @@ describe('Toast', () => {
timeout: transitionDuration.standard + 100, // see "transitions/Collapse.js"
});
});
+
+ test('should display a toast within a modal', async () => {
+ const user = userEvent.setup();
+
+ const TestComponent = ({ onClose }) => {
+ const [colorStyle] = useColorStyle();
+ const [toasts, setToasts] = useState([
+ {
+ id: 1,
+ appearance: 'success',
+ content: 'This is a success message.',
+ duration: 5000,
+ },
+ ]);
+ const closeToast = (id) => {
+ setToasts(toasts => toasts.filter(x => x.id !== id));
+ };
+ const createCloseToastHandler = (id) => () => {
+ closeToast(id);
+ };
+
+ return (
+
+
+
+ {toasts.map(toast => {
+ const onClose = createCloseToastHandler(toast.id);
+ return (
+
+
+
+ {toast.content}
+
+
+
+
+ );
+ })}
+
+
+
+ Modal
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ };
+
+ render(
+
+ );
+
+ // Verify the toast is displayed
+ expect(screen.getByText('This is a success message.')).toBeInTheDocument();
+
+ // Simulate closing the toast
+ const toastCloseButton = screen.getByTestId('toast-close-button');
+ await user.click(toastCloseButton);
+
+ await waitForElementToBeRemoved(() => screen.getByTestId('toast'), {
+ timeout: transitionDuration.standard + 100, // see "transitions/Collapse.js"
+ });
+ });
});
diff --git a/packages/react/src/toast/__tests__/ToastController.test.js b/packages/react/src/toast/__tests__/ToastController.test.js
new file mode 100644
index 0000000000..1cfe865e03
--- /dev/null
+++ b/packages/react/src/toast/__tests__/ToastController.test.js
@@ -0,0 +1,45 @@
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { render } from '@tonic-ui/react/test-utils/render';
+import { Toast, ToastController } from '@tonic-ui/react/src';
+
+jest.useFakeTimers();
+
+describe('ToastController', () => {
+ test('pauses timeout on mouse enter and resumes on mouse leave', async () => {
+ const user = userEvent.setup({
+ // Disable action delay to allow `await user.hover(element)` to complete immediately
+ // Reference: https://github.com/testing-library/user-event/issues/833
+ delay: null,
+ });
+ const onClose = jest.fn();
+ const duration = 5000;
+
+ render(
+
+
+ This is a toast
+
+
+ );
+
+ const toast = screen.getByTestId('toast');
+
+ // Simulate mouse enter, which should pause the timeout
+ await user.hover(toast);
+
+ // Timeout should be paused
+ jest.advanceTimersByTime(duration);
+ expect(onClose).not.toHaveBeenCalled();
+
+ // Simulate mouse leave, which should resume the timeout
+ await user.unhover(toast);
+
+ // Timeout should be resumed
+ jest.advanceTimersByTime(duration);
+ expect(onClose).toHaveBeenCalled();
+ });
+});
diff --git a/packages/react/src/toast/__tests__/ToastManager.test.js b/packages/react/src/toast/__tests__/ToastManager.test.js
index b0d624614e..0bea08958f 100644
--- a/packages/react/src/toast/__tests__/ToastManager.test.js
+++ b/packages/react/src/toast/__tests__/ToastManager.test.js
@@ -1,7 +1,7 @@
import { act, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from '@tonic-ui/react/test-utils/render';
-import { Box, Button, Toast, ToastManager, useToastManager } from '@tonic-ui/react/src';
+import { Box, Button, Toast, ToastCloseButton, ToastManager, useToastManager } from '@tonic-ui/react/src';
import { transitionDuration } from '@tonic-ui/utils/src';
import React, { useCallback, useRef } from 'react';
@@ -164,7 +164,77 @@ describe('ToastManager', () => {
});
});
- it('should close all toasts when `closeAll` is called', async () => {
+ it('should close a toast', async () => {
+ const user = userEvent.setup();
+
+ const placement = 'bottom-right';
+ const message = 'This is a toast message';
+
+ const WrapperComponent = (props) => (
+
+ );
+ const TestComponent = () => {
+ const toastCounterRef = useRef(0);
+ const toast = useToastManager();
+ const handleClickAddToast = useCallback(() => {
+ const testId = ++toastCounterRef.current;
+ const toastTestId = `toast-${testId}`;
+ const toastCloseButtonTestId = `toast-close-button-${testId}`;
+ toast(({ onClose, placement }) => {
+ return (
+
+ {message}
+
+
+ );
+ }, { placement });
+ }, [toast]);
+
+ return (
+
+ );
+ };
+
+ render(
+
+
+
+ );
+
+ const addToastButton = await screen.findByText('Add Toast');
+
+ // Generate more than the maximum number of toasts
+ await user.click(addToastButton);
+ await user.click(addToastButton);
+ await user.click(addToastButton);
+
+ const toastPlacementElement = document.querySelector(`[data-toast-placement="${placement}"]`);
+
+ await waitFor(() => {
+ expect(toastPlacementElement.childNodes.length).toBe(3);
+ });
+
+ // Close the second toast
+ const toastCloseButton = screen.getByTestId('toast-close-button-2');
+ expect(toastCloseButton).toBeInTheDocument();
+ await user.click(toastCloseButton);
+
+ await waitFor(() => {
+ expect(toastPlacementElement.childNodes.length).toBe(2);
+ });
+
+ expect(screen.getByTestId('toast-close-button-1')).toBeInTheDocument();
+ expect(screen.queryByTestId('toast-close-button-2')).not.toBeInTheDocument(); // use queryBy* only when an element is not present
+ expect(screen.getByTestId('toast-close-button-3')).toBeInTheDocument();
+ });
+
+ it('should close all toasts', async () => {
const user = userEvent.setup();
const placement = 'bottom-right';
@@ -288,6 +358,354 @@ describe('ToastManager', () => {
});
const toastPlacementElement = document.querySelector(`[data-toast-placement="${placement}"]`);
+
expect(toastPlacementElement.childNodes.length).toBe(maxToasts);
});
+
+ it('should find a toast by ID using find()', async () => {
+ const user = userEvent.setup();
+ const toastId = 'toast-id';
+ const message = 'This is a toast message';
+
+ const WrapperComponent = (props) => (
+
+ );
+
+ const TestComponent = () => {
+ const toast = useToastManager();
+ const handleClickAddToast = useCallback(() => {
+ toast(({ onClose, placement }) => (
+
+ {message}
+
+ ), { id: toastId });
+ }, [toast]);
+ const handleClickFindToast = useCallback(() => {
+ const foundToast = toast.find(toastId);
+ expect(foundToast).toBeDefined();
+ expect(foundToast.id).toBe(toastId);
+ }, [toast]);
+
+ return (
+ <>
+
+
+ >
+ );
+ };
+
+ render(
+
+
+
+ );
+
+ // Add the toast
+ await user.click(screen.getByText('Add Toast'));
+
+ // Find the toast
+ await user.click(screen.getByText('Find Toast'));
+
+ // Check if the toast message is displayed correctly
+ const toastElement = screen.getByTestId(toastId);
+ expect(toastElement).toHaveTextContent(message);
+ });
+
+ it('should find the index of a toast by ID using findIndex()', async () => {
+ const user = userEvent.setup();
+ const toastId = 'toast-id';
+ const message = 'This is a toast message';
+
+ const WrapperComponent = (props) => (
+
+ );
+
+ const TestComponent = () => {
+ const toast = useToastManager();
+ const handleClickAddToast = useCallback(() => {
+ toast(({ onClose, placement }) => (
+
+ {message}
+
+ ), { id: toastId });
+ }, [toast]);
+ const handleClickFindToast = useCallback(() => {
+ const toastIndex = toast.findIndex(toastId);
+ expect(toastIndex).toBeGreaterThan(-1);
+ }, [toast]);
+
+ return (
+ <>
+
+
+ >
+ );
+ };
+
+ render(
+
+
+
+ );
+
+ // Add the toast
+ await user.click(screen.getByText('Add Toast'));
+
+ // Find the toast
+ await user.click(screen.getByText('Find Toast'));
+
+ // Check if the toast message is displayed correctly
+ const toastElement = screen.getByTestId(toastId);
+ expect(toastElement).toHaveTextContent(message);
+ });
+
+ it('should update an existing toast by ID using update()', async () => {
+ const user = userEvent.setup();
+ const toastId = 'toast-id';
+ const initialMessage = 'Initial toast message';
+ const updatedMessage = 'Updated toast message';
+
+ const WrapperComponent = (props) => (
+
+ );
+
+ const TestComponent = () => {
+ const toast = useToastManager();
+ const handleClickAddToast = useCallback(() => {
+ toast(({ onClose }) => (
+
+ {initialMessage}
+
+ ), { id: toastId });
+ }, [toast]);
+ const handleClickUpdateToast = useCallback(() => {
+ const updateSuccess = toast.update(toastId, {
+ content: ({ onClose }) => (
+
+ {updatedMessage}
+
+ ),
+ });
+ expect(updateSuccess).toBe(true);
+ }, [toast]);
+ const handleClickUpdateInvalidToast = useCallback(() => {
+ const updateSuccess = toast.update(null, {});
+ expect(updateSuccess).toBe(false);
+ }, [toast]);
+
+ return (
+ <>
+
+
+
+ >
+ );
+ };
+
+ render(
+
+
+
+ );
+
+ // Add the toast
+ await user.click(screen.getByText('Add Toast'));
+
+ // Update the toast
+ await user.click(screen.getByText('Update Toast'));
+ await user.click(screen.getByText('Update Invalid Toast'));
+
+ // Check if the content has been updated
+ const toastElement = screen.getByTestId(toastId);
+ expect(toastElement).toHaveTextContent(updatedMessage);
+ });
+
+ it('should not create a toast and return false for invalid placement', async () => {
+ const user = userEvent.setup();
+ const toastId = 'toast-id';
+ const placement = 'center'; // "center" is not a supported placement
+ const message = 'This is a toast message';
+
+ // Spy on console.error to capture and check the error message
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ const WrapperComponent = (props) => (
+
+ );
+
+ const TestComponent = () => {
+ const toast = useToastManager();
+ const handleClick = useCallback(() => {
+ const result = toast(({ onClose }) => (
+
+ {message}
+
+ ), { placement });
+ expect(result).toBe(false);
+ }, [toast]);
+
+ return (
+
+ );
+ };
+
+ render(
+
+
+
+ );
+
+ const button = await screen.findByText('Add Toast');
+ await user.click(button);
+
+ // Check that console.error was called with the expected error message
+ const placements = [
+ 'bottom',
+ 'bottom-right',
+ 'bottom-left',
+ 'top',
+ 'top-left',
+ 'top-right',
+ ];
+ const expectedErrorMessage = `[ToastManager] Error: Invalid toast placement "${placement}". Please provide a valid placement from the following options: ${placements.join(', ')}.`;
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expectedErrorMessage);
+
+ // Assert that no toast element with the invalid placement was created
+ const toastElement = screen.queryByTestId(toastId);
+ expect(toastElement).not.toBeInTheDocument();
+
+ // Restore console.error to its original implementation
+ consoleErrorSpy.mockRestore();
+ });
+
+ it('should create toasts in the correct order for top and bottom placements in the state', async () => {
+ const user = userEvent.setup();
+ const topPlacement = 'top';
+ const bottomPlacement = 'bottom';
+ const message = 'This is a toast message';
+
+ const WrapperComponent = (props) => (
+
+ );
+
+ const TestComponent = () => {
+ const toast = useToastManager();
+ const handleClickAddToasts = useCallback(() => {
+ // Add toast for top-right placement
+ toast(({ onClose }) => (
+
+ {message}
+
+ ), { placement: topPlacement });
+
+ // Add toast for bottom-right placement
+ toast(({ onClose }) => (
+
+ {message}
+
+ ), { placement: bottomPlacement });
+ }, [toast]);
+
+ return (
+ <>
+
+ {/* Access toast.state here to check order */}
+ {toast.state && (
+
{JSON.stringify(toast.state, null, 2)}
+ )}
+ >
+ );
+ };
+
+ render(
+
+
+
+ );
+
+ const button = await screen.findByText('Add Toasts');
+ await user.click(button);
+ await user.click(button);
+
+ // Wait for the state to be updated with toasts
+ await screen.findByTestId('toast-state');
+
+ // Get the state of the toasts
+ const toastState = JSON.parse(screen.getByTestId('toast-state').textContent);
+
+ // Check that toasts with top-right and bottom-right placements exist in the state
+ const topToasts = toastState[topPlacement];
+ const bottomToasts = toastState[bottomPlacement];
+
+ // top-right
+ //
+ // ```js
+ // [
+ // { id: '3', placement: 'top-right' },
+ // { id: '1', placement: 'top-right' },
+ // ]
+ // ```
+ expect(topToasts).toHaveLength(2);
+ expect(topToasts[0].id > topToasts[1].id).toBeTruthy();
+
+ // bottom-right
+ //
+ // ```js
+ // [
+ // { id: '2', placement: 'bottom-right' },
+ // { id: '4', placement: 'bottom-right' },
+ // ]
+ // ```
+ expect(bottomToasts).toHaveLength(2);
+ expect(bottomToasts[0].id < bottomToasts[1].id).toBeTruthy();
+ });
});
diff --git a/packages/react/src/toast/index.js b/packages/react/src/toast/index.js
index e5b0cdc479..cf8fa938ff 100644
--- a/packages/react/src/toast/index.js
+++ b/packages/react/src/toast/index.js
@@ -6,6 +6,7 @@ import ToastIcon from './ToastIcon';
import ToastManager from './ToastManager';
import ToastMessage from './ToastMessage';
import ToastTransition from './ToastTransition';
+import ToastTransitionGroup from './ToastTransitionGroup';
import useToastManager from './useToastManager';
export {
@@ -17,5 +18,6 @@ export {
ToastManager,
ToastMessage,
ToastTransition,
+ ToastTransitionGroup,
useToastManager,
};
diff --git a/packages/react/src/utils/__tests__/renderComponentOrValue.test.js b/packages/react/src/utils/__tests__/renderComponentOrValue.test.js
new file mode 100644
index 0000000000..ec06c0d1bf
--- /dev/null
+++ b/packages/react/src/utils/__tests__/renderComponentOrValue.test.js
@@ -0,0 +1,41 @@
+import { screen } from '@testing-library/react';
+import { render } from '@tonic-ui/react/test-utils/render';
+import React from 'react';
+import { renderComponentOrValue } from '../renderComponentOrValue';
+
+class ReactClassComponent extends React.Component {
+ render() {
+ return (
+