Skip to content

Commit

Permalink
Merge pull request #143 from osstotalsoft/actionsOnToast
Browse files Browse the repository at this point in the history
Optimizations on Toast Component.
Refactor toast with our own icons.
Added actions for passing ReactNode code to the footer of the toast.
  • Loading branch information
elena-dumitrescu authored Dec 9, 2024
2 parents 088789c + 218326c commit 6135bea
Show file tree
Hide file tree
Showing 16 changed files with 652 additions and 164 deletions.
1 change: 0 additions & 1 deletion .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ const preview: Preview = {
},
decorators: [withThemeProvider],
tags: ['autodocs']

}

export default preview
42 changes: 39 additions & 3 deletions src/components/feedback/Toast/Toast.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { fireEvent, renderHook, screen, waitFor } from '@testing-library/react'
import React, { act } from 'react'
import useToast from './useToast'
import { render } from 'testingUtils'
import Button from 'components/buttons/Button'
import usePromiseToast from './usePromiseToast'
import { Button, useToast, usePromiseToast, Typography } from '../../index'
import { emptyFunction } from '../../utils/constants'
import { Stack } from '@mui/material'

describe('Toast', () => {
it.each([
Expand Down Expand Up @@ -115,3 +115,39 @@ describe('Promise toast', () => {
})
})
})

describe('Actions toast', () => {
it('Should render a toast with actions', async () => {
const { result } = renderHook(() => useToast())

const CustomMessageWithActions = () => (
<Stack direction="row" alignItems="flex-end" justifyContent="flex-end" gap={1}>
<Button size={'small'} onClick={emptyFunction} variant="text" capitalize={false}>
<Typography>{'Button 1'}</Typography>
</Button>
<Button size={'small'} onClick={emptyFunction} variant="text" capitalize={false}>
<Typography>{'Button 2'}</Typography>
</Button>
</Stack>
)

render(
<Button
onClick={() =>
result.current('This is a custom toast with actions!', 'success', {
actions: <CustomMessageWithActions />
})
}
/>
)

await act(async () => fireEvent.click(screen.getByRole('button')))
await waitFor(() => expect(screen.getByRole('alert')).toBeInTheDocument())

const button1 = screen.getByRole('button', { name: 'Button 1' })
const button2 = screen.getByRole('button', { name: 'Button 2' })

expect(button1).toBeInTheDocument()
expect(button2).toBeInTheDocument()
})
})
21 changes: 19 additions & 2 deletions src/components/feedback/Toast/ToastContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,22 @@ const ToastContainer: React.FC<ToastContainerProps> = ({
position = 'top-center',
newestOnTop = true,
transitionType = 'Slide',
closeOnClick = true,
closeButton = false,
icon = false,
limit = 5,
textSize = 'small',
...rest
}) => {
return (
<Container>
<Container textSize={textSize}>
<ReactToastify
className={classes.toastWrapper}
position={position}
closeOnClick={closeOnClick}
closeButton={closeButton}
newestOnTop={newestOnTop}
icon={icon}
transition={transitionType as any}
theme="colored"
limit={limit}
Expand All @@ -45,6 +52,11 @@ ToastContainer.propTypes = {
* @default 5
*/
limit: PropTypes.number,
/**
* Close the toast when clicked.
* @default true
*/
closeOnClick: PropTypes.bool,
/**
* Set the position to use.
* @default 'top-center'
Expand All @@ -61,7 +73,12 @@ ToastContainer.propTypes = {
* Whether or not to display the newest toast on top.
* @default true
*/
newestOnTop: PropTypes.bool
newestOnTop: PropTypes.bool,
/**
* @default 'small'
* The content font size
*/
textSize: PropTypes.oneOf(['small', 'medium', 'large'])
}

export default ToastContainer
93 changes: 52 additions & 41 deletions src/components/feedback/Toast/ToastStyles.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { styled } from '@mui/material/styles'
import { always, cond, equals, includes, T } from 'ramda'

const PREFIX = 'StyledToast'

Expand All @@ -11,47 +12,57 @@ export const classes = {
toastWrapper: `${PREFIX}-toastWrapper`
}

const Container = styled('div')(({ theme }) => ({
[`& .${classes.default}`]: { borderRadius: '6px', padding: '6px 20px' },
[`& .${classes.success}`]: {
'--toastify-color-success': theme.palette.success.main,
'--toastify-text-color-success': theme.palette.success.contrastText,
'--toastify-icon-color-success': theme.palette.success.main,
'--toastify-color-progress-success': theme.palette.success.main
},
[`& .${classes.info}`]: {
'--toastify-color-info': theme.palette.info.main,
'--toastify-text-color-info': theme.palette.info.contrastText,
'--toastify-icon-color-info': theme.palette.info.main,
'--toastify-color-progress-info': theme.palette.info.main
},
[`& .${classes.error}`]: {
'--toastify-color-error': theme.palette.error.main,
'--toastify-text-color-error': theme.palette.error.contrastText,
'--toastify-icon-color-error': theme.palette.error.main,
'--toastify-color-progress-error': theme.palette.error.main
},
[`& .${classes.warning}`]: {
'--toastify-color-warning': theme.palette.warning.main,
'--toastify-text-color-warning': theme.palette.warning.contrastText,
'--toastify-icon-color-warning': theme.palette.warning.main,
'--toastify-color-progress-warning': theme.palette.warning.main
},
[`& .${classes.toastWrapper}`]: {
borderRadius: '6px',
width: '350px',
overflowWrap: 'anywhere'
},
['.Toastify__close-button']: {
background: 'transparent',
outline: 'none',
border: 'none',
padding: 0,
cursor: 'pointer',
opacity: 1,
transition: '0.3s ease',
alignSelf: 'auto'
const Container: any = styled('div', {
shouldForwardProp: prop => !includes(prop, ['textSize'])
})(({ theme, textSize }: any) => {
const fontSize = cond([
[equals('medium'), always(theme.typography.body2.fontSize)],
[equals('large'), always(theme.typography.h6.fontSize)],
[T, always(theme.typography.defaultFont.fontSize)]
])(textSize)

return {
[`& .${classes.default}`]: { borderRadius: '6px' },
[`& .${classes.success}`]: {
'--toastify-color-success': theme.palette.success.main,
'--toastify-text-color-success': theme.palette.success.contrastText,
'--toastify-icon-color-success': theme.palette.success.main,
'--toastify-color-progress-success': theme.palette.success.main
},
[`& .${classes.info}`]: {
'--toastify-color-info': theme.palette.info.main,
'--toastify-text-color-info': theme.palette.info.contrastText,
'--toastify-icon-color-info': theme.palette.info.main,
'--toastify-color-progress-info': theme.palette.info.main
},
[`& .${classes.error}`]: {
'--toastify-color-error': theme.palette.error.main,
'--toastify-text-color-error': theme.palette.error.contrastText,
'--toastify-icon-color-error': theme.palette.error.main,
'--toastify-color-progress-error': theme.palette.error.main
},
[`& .${classes.warning}`]: {
'--toastify-color-warning': theme.palette.warning.main,
'--toastify-text-color-warning': theme.palette.warning.contrastText,
'--toastify-icon-color-warning': theme.palette.warning.main,
'--toastify-color-progress-warning': theme.palette.warning.main
},
['.Toastify__close-button']: {
background: 'transparent',
outline: 'none',
border: 'none',
padding: 0,
cursor: 'pointer',
opacity: 1,
transition: '0.3s ease',
alignSelf: 'auto',
marginRight: '6px'
},
['.Toastify__toast']: {
...theme.typography.defaultFont,
fontSize
}
}
}))
})

export default Container
3 changes: 2 additions & 1 deletion src/components/feedback/Toast/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './types'
export { default as ToastContainer } from './ToastContainer'
export { default as useToast } from './useToast'
export { default as usePromiseToast } from './usePromiseToast'
export { default as usePromiseToast } from './usePromiseToast'
27 changes: 24 additions & 3 deletions src/components/feedback/Toast/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
// Copyright (c) TotalSoft.
// This source code is licensed under the MIT license.
import { ToastContainerProps as ReactToastifyProps } from 'react-toastify'
import { always, cond, equals, T } from 'ramda'
import { Bounce, Flip, ToastOptions as ToastOptionsBase, ToastContainerProps as ToastContainerPropsBase, Slide, Zoom } from 'react-toastify'

export interface ToastContainerProps extends Omit<ReactToastifyProps, 'transition'> {
export type TextFontSize = 'small' | 'medium' | 'large'

export interface ToastContainerProps extends Omit<ToastContainerPropsBase, 'transition' | 'textSize'> {
/**
* The appearance effect.
* @default Slide
*/
transitionType?: 'Slide' | 'Bounce' | 'Zoom' | 'Flip'
transitionType?: 'Slide' | 'Bounce' | 'Zoom' | 'Flip',
/**
* The size of the toast content text.
* @default 'small'
*/
textSize?: TextFontSize
}

export type ToastOptions = Omit<ToastOptionsBase, 'transition'> & {
transitionType?: 'Slide' | 'Bounce' | 'Zoom' | 'Flip',
actions?: React.ReactNode
}

export const getTransitionType = cond([
[equals('Slide'), always(Slide)],
[equals('Bounce'), always(Bounce)],
[equals('Flip'), always(Flip)],
[equals('Zoom'), always(Zoom)],
[T, always(Slide)]
])
21 changes: 18 additions & 3 deletions src/components/feedback/Toast/usePromiseToast.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
// Copyright (c) TotalSoft.
// This source code is licensed under the MIT license.
import { useCallback } from 'react'
import { toast } from 'react-toastify'
import { toast, ToastOptions as ToastOptionsBase } from 'react-toastify'
import { classes } from './ToastStyles'
import { getTransitionType } from './types'

type ToastOptions = Omit<ToastOptionsBase, 'transition'> & {
transitionType?: 'Slide' | 'Bounce' | 'Zoom' | 'Flip'
}

const usePromiseToast = () => {
return useCallback(
Expand All @@ -10,13 +17,21 @@ const usePromiseToast = () => {
* @param {(String|Object)} pending The message to be shown while promise in pending or the entire object with all the configurations
* @param {(String|Object)} success The message to be shown when promise completed successfully or the entire object with all the configurations
* @param {(String|Object)} error The message to be shown when promise was rejected or the entire object with all the configurations
* @param {ToastOptions} options Additional options passed to the toast
*/
(promise: Promise<unknown>, pending: string | object, success: string | object, error: string | object) => {
(promise: Promise<unknown>, pending: string | object, success: string | object, error: string | object, { transitionType, ...restOptions } = {} as ToastOptions) => {

const localOptions: ToastOptionsBase = {
...restOptions,
transition: getTransitionType(transitionType)
}
toast.promise(
promise,
{ pending, success, error },
{
className: `${classes.success} ${classes.default} ${classes.info} ${classes.error} ${classes.error} ${classes.default} `
className: `${classes.default} ${classes.success} ${classes.info} ${classes.error} ${classes.warning} ${classes.toastWrapper} `,
closeButton: true,
...localOptions,
}
)
},
Expand Down
69 changes: 0 additions & 69 deletions src/components/feedback/Toast/useToast.ts

This file was deleted.

Loading

0 comments on commit 6135bea

Please sign in to comment.