mirror of https://github.com/acid-info/lsd.git
fix: implements PR comment changes
This commit is contained in:
parent
24ccbd04b2
commit
7041359c44
|
@ -2,23 +2,23 @@ export const toastClasses = {
|
||||||
root: `lsd-toast`,
|
root: `lsd-toast`,
|
||||||
|
|
||||||
inlineContainer: 'lsd-toast__inline-container',
|
inlineContainer: 'lsd-toast__inline-container',
|
||||||
blockContainer: 'lsd-toast__block-container',
|
columnContainer: 'lsd-toast__column-container',
|
||||||
|
|
||||||
large: 'lsd-toast--large',
|
large: 'lsd-toast--large',
|
||||||
medium: 'lsd-toast--medium',
|
medium: 'lsd-toast--medium',
|
||||||
small: 'lsd-toast--small',
|
small: 'lsd-toast--small',
|
||||||
|
|
||||||
|
icon: 'lsd-toast__icon',
|
||||||
|
|
||||||
textContainer: 'lsd-toast__text-container',
|
textContainer: 'lsd-toast__text-container',
|
||||||
|
columnIconContainer: 'lsd-toast__column-icon-container',
|
||||||
|
inlineIconContainer: 'lsd-toast__inline-icon-container',
|
||||||
title: 'lsd-toast__title',
|
title: 'lsd-toast__title',
|
||||||
information: 'lsd-toast__information',
|
information: 'lsd-toast__information',
|
||||||
|
|
||||||
inlineButtonContainer: 'lsd-toast__inline-button-container',
|
inlineButtonContainer: 'lsd-toast__inline-button-container',
|
||||||
hiddenButtonContainer: 'lsd-toast__hidden-button-container',
|
columnButtonContainer: 'lsd-toast__column-button-container',
|
||||||
blockButton: 'lsd-toast__block-button',
|
buttonContainer: 'lsd-toast__button-container',
|
||||||
|
|
||||||
closeButton: 'lsd-toast__close-button',
|
closeButton: 'lsd-toast__close-button',
|
||||||
actionButton: 'lsd-toast__action-button',
|
|
||||||
|
|
||||||
errorIcon: 'lsd-toast__error-icon',
|
|
||||||
errorIconContainer: 'lsd-toast__error-icon-container',
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Meta, Story } from '@storybook/react'
|
import { Meta, Story } from '@storybook/react'
|
||||||
import { Toast, ToastProps } from './Toast'
|
import { Toast, ToastProps } from './Toast'
|
||||||
|
import { Button } from '../Button'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Toast',
|
title: 'Toast',
|
||||||
|
@ -15,13 +16,11 @@ export default {
|
||||||
} as Meta
|
} as Meta
|
||||||
|
|
||||||
export const Root: Story<ToastProps> = (args) => {
|
export const Root: Story<ToastProps> = (args) => {
|
||||||
return <Toast {...args} />
|
return <Toast {...args} actions={<Button>Button</Button>} />
|
||||||
}
|
}
|
||||||
|
|
||||||
Root.args = {
|
Root.args = {
|
||||||
title: 'Toast Title',
|
title: 'Toast Title',
|
||||||
information: '',
|
information: '',
|
||||||
size: 'large',
|
size: 'large',
|
||||||
buttonText: 'Click me',
|
|
||||||
inline: true,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,51 +4,42 @@ import { toastClasses } from './Toast.classes'
|
||||||
export const ToastStyles = css`
|
export const ToastStyles = css`
|
||||||
.${toastClasses.root} {
|
.${toastClasses.root} {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
position: fixed;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: rgb(var(--lsd-surface-primary));
|
background: rgb(var(--lsd-surface-primary));
|
||||||
border: 1px solid rgb(var(--lsd-border-primary));
|
border: 1px solid rgb(var(--lsd-border-primary));
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
|
||||||
z-index: 9999;
|
|
||||||
|
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.${toastClasses.inlineButtonContainer} {
|
.${toastClasses.inlineButtonContainer} {
|
||||||
margin: 0 8px;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.${toastClasses.hiddenButtonContainer} {
|
.${toastClasses.columnButtonContainer} {
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.${toastClasses.blockButton} {
|
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.${toastClasses.inlineContainer} {
|
.${toastClasses.inlineContainer} {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
width: 100%;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.${toastClasses.blockContainer} {
|
.${toastClasses.columnContainer} {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.${toastClasses.textContainer} {
|
.${toastClasses.textContainer} {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-grow: 1;
|
|
||||||
color: rgb(var(--lsd-text-secondary));
|
color: rgb(var(--lsd-text-secondary));
|
||||||
margin-left: 32px;
|
padding-left: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.${toastClasses.title} {
|
.${toastClasses.title} {
|
||||||
|
@ -59,11 +50,11 @@ export const ToastStyles = css`
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.${toastClasses.actionButton} {
|
.${toastClasses.buttonContainer} {
|
||||||
height: 28px;
|
min-height: 28px;
|
||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
padding: 6px 12px;
|
padding: 0px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.${toastClasses.closeButton} {
|
.${toastClasses.closeButton} {
|
||||||
|
@ -71,39 +62,43 @@ export const ToastStyles = css`
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
width: 28px;
|
width: 28px;
|
||||||
|
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.${toastClasses.errorIconContainer} {
|
.${toastClasses.columnIconContainer} {
|
||||||
width: 26px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
justify-content: center;
|
||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
top: 4px;
|
||||||
|
padding-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.${toastClasses.errorIcon} {
|
.${toastClasses.inlineIconContainer} {
|
||||||
position: absolute;
|
display: flex;
|
||||||
top: 3px;
|
align-items: center;
|
||||||
left: -26px;
|
justify-content: center;
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.${toastClasses.icon} {
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.${toastClasses.large} {
|
.${toastClasses.large} {
|
||||||
.${toastClasses.textContainer} {
|
width: 364px;
|
||||||
min-width: 204px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.${toastClasses.medium} {
|
.${toastClasses.medium} {
|
||||||
.${toastClasses.textContainer} {
|
width: 336px;
|
||||||
min-width: 184px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.${toastClasses.small} {
|
.${toastClasses.small} {
|
||||||
.${toastClasses.textContainer} {
|
width: 296px;
|
||||||
min-width: 144px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.${toastClasses.errorIcon} {
|
.${toastClasses.icon} {
|
||||||
top: 0px;
|
top: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,60 +5,63 @@ import {
|
||||||
useCommonProps,
|
useCommonProps,
|
||||||
} from '../../utils/useCommonProps'
|
} from '../../utils/useCommonProps'
|
||||||
import { toastClasses } from './Toast.classes'
|
import { toastClasses } from './Toast.classes'
|
||||||
import { CloseIcon, ErrorIcon } from '../Icons'
|
import { CloseIcon, ErrorIcon, LsdIconProps } from '../Icons'
|
||||||
import { IconButton } from '../IconButton'
|
import { IconButton } from '../IconButton'
|
||||||
import { Typography } from '../Typography'
|
import { Typography } from '../Typography'
|
||||||
import { Button } from '../Button'
|
|
||||||
|
|
||||||
export type ToastProps = CommonProps &
|
export type ToastProps = CommonProps &
|
||||||
Omit<React.HTMLAttributes<HTMLDivElement>, 'label'> & {
|
Omit<React.HTMLAttributes<HTMLDivElement>, 'label'> & {
|
||||||
isOpen?: boolean
|
|
||||||
title: string
|
title: string
|
||||||
information?: string
|
information?: string
|
||||||
inline?: boolean
|
|
||||||
buttonText?: string
|
|
||||||
onButtonClick?: () => void
|
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
size?: 'large' | 'medium' | 'small'
|
size?: 'large' | 'medium' | 'small'
|
||||||
toastRefFunction?: (el: HTMLDivElement | null) => void
|
toastRef?: React.Ref<HTMLDivElement>
|
||||||
|
icon?: React.ComponentType<LsdIconProps> | null | false
|
||||||
|
actions?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Toast: React.FC<ToastProps> & {
|
export const Toast: React.FC<ToastProps> & {
|
||||||
classes: typeof toastClasses
|
classes: typeof toastClasses
|
||||||
} = ({
|
} = ({
|
||||||
isOpen,
|
|
||||||
title,
|
title,
|
||||||
information,
|
information,
|
||||||
inline = true,
|
|
||||||
buttonText,
|
|
||||||
onButtonClick,
|
|
||||||
onClose,
|
onClose,
|
||||||
size = 'large',
|
size = 'large',
|
||||||
toastRefFunction,
|
toastRef,
|
||||||
children,
|
children,
|
||||||
|
icon,
|
||||||
|
actions,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const commonProps = useCommonProps(props)
|
const commonProps = useCommonProps(props)
|
||||||
const isInlineButtonHidden = !inline || !!information || !buttonText
|
const isInline = !information
|
||||||
|
|
||||||
// If the toast is not open, do not render anything.
|
const Icon = typeof icon === 'undefined' ? ErrorIcon : icon
|
||||||
if (isOpen === false) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={toastRefFunction}
|
ref={toastRef}
|
||||||
{...omitCommonProps(props)}
|
{...omitCommonProps(props)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
props.className,
|
||||||
commonProps.className,
|
commonProps.className,
|
||||||
toastClasses.root,
|
toastClasses.root,
|
||||||
toastClasses[size],
|
toastClasses[size],
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
isInline
|
||||||
|
? toastClasses.inlineIconContainer
|
||||||
|
: toastClasses.columnIconContainer,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{Icon && <Icon color="primary" className={toastClasses.icon} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
inline ? toastClasses.inlineContainer : toastClasses.blockContainer
|
isInline ? toastClasses.inlineContainer : toastClasses.columnContainer
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className={clsx(toastClasses.textContainer)}>
|
<div className={clsx(toastClasses.textContainer)}>
|
||||||
|
@ -68,11 +71,6 @@ export const Toast: React.FC<ToastProps> & {
|
||||||
component="div"
|
component="div"
|
||||||
variant={size === 'small' ? 'label2' : 'label1'}
|
variant={size === 'small' ? 'label2' : 'label1'}
|
||||||
>
|
>
|
||||||
<ErrorIcon
|
|
||||||
color="primary"
|
|
||||||
className={toastClasses.errorIcon}
|
|
||||||
style={{ width: '16px' }}
|
|
||||||
/>
|
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
@ -86,30 +84,20 @@ export const Toast: React.FC<ToastProps> & {
|
||||||
{information}
|
{information}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!inline && !!buttonText && (
|
|
||||||
<Button
|
|
||||||
onClick={onButtonClick}
|
|
||||||
className={clsx(
|
|
||||||
toastClasses.actionButton,
|
|
||||||
toastClasses.blockButton,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{buttonText}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
{!!actions && (
|
||||||
className={clsx(
|
<div
|
||||||
toastClasses.inlineButtonContainer,
|
className={clsx(
|
||||||
isInlineButtonHidden && toastClasses.hiddenButtonContainer,
|
toastClasses.buttonContainer,
|
||||||
)}
|
isInline
|
||||||
>
|
? toastClasses.inlineButtonContainer
|
||||||
<Button onClick={onButtonClick} className={toastClasses.actionButton}>
|
: toastClasses.columnButtonContainer,
|
||||||
{`${isInlineButtonHidden ? '' : buttonText}`}
|
)}
|
||||||
</Button>
|
>
|
||||||
</div>
|
{actions}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
export const toastProviderClasses = {
|
export const toastProviderClasses = {
|
||||||
toastsContainer: `lsd-toast-provider__toasts-container`,
|
toastContainer: `lsd-toast-provider__toast-container`,
|
||||||
|
topLeft: `lsd-toast-provider__toast--top-left`,
|
||||||
|
topCenter: `lsd-toast-provider__toast--top-center`,
|
||||||
|
topRight: `lsd-toast-provider__toast--top-right`,
|
||||||
|
bottomLeft: `lsd-toast-provider__toast--bottom-left`,
|
||||||
|
bottomCenter: `lsd-toast-provider__toast--bottom-center`,
|
||||||
|
bottomRight: `lsd-toast-provider__toast--bottom-right`,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { Meta, Story } from '@storybook/react'
|
import { Meta, Story } from '@storybook/react'
|
||||||
import { ToastProps } from '../Toast'
|
import {
|
||||||
import { ToastProvider, useLSDToast } from './ToastProvider'
|
ToastContantAndOptions,
|
||||||
|
ToastProvider,
|
||||||
|
useToast,
|
||||||
|
} from './ToastProvider'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { Button } from '../Button'
|
import { Button } from '../Button'
|
||||||
import { ToastOptions } from 'react-hot-toast'
|
|
||||||
import { pickCommonProps } from '../../utils/useCommonProps'
|
import { pickCommonProps } from '../../utils/useCommonProps'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -16,31 +18,43 @@ export default {
|
||||||
value: ['small', 'medium', 'large'],
|
value: ['small', 'medium', 'large'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
position: {
|
||||||
|
type: {
|
||||||
|
name: 'enum',
|
||||||
|
value: [
|
||||||
|
'top-left',
|
||||||
|
'top-center',
|
||||||
|
'top-right',
|
||||||
|
'bottom-left',
|
||||||
|
'bottom-center',
|
||||||
|
'bottom-right',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as Meta
|
} as Meta
|
||||||
|
|
||||||
type ToastButtonProps = {
|
const ToastButton: FC<ToastContantAndOptions> = ({
|
||||||
toastArgs: ToastProps & ToastOptions
|
information,
|
||||||
}
|
title,
|
||||||
|
...toastArgs
|
||||||
const ToastButton: FC<ToastButtonProps> = ({ toastArgs }) => {
|
}) => {
|
||||||
const showToast = useLSDToast()
|
const showToast = useToast()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
{...pickCommonProps(toastArgs)}
|
{...pickCommonProps(toastArgs)}
|
||||||
onClick={() => showToast(toastArgs, { duration: toastArgs.duration })}
|
onClick={() => showToast({ title, information }, { ...toastArgs })}
|
||||||
style={{ marginBottom: 8 }}
|
|
||||||
>
|
>
|
||||||
Show Toast
|
Show Toast
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Root: Story<ToastProps & ToastOptions> = (args) => {
|
export const Root: Story<ToastContantAndOptions> = (args) => {
|
||||||
return (
|
return (
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<ToastButton toastArgs={args} />
|
<ToastButton {...args} />
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -49,7 +63,6 @@ Root.args = {
|
||||||
title: 'Toast Title',
|
title: 'Toast Title',
|
||||||
information: '',
|
information: '',
|
||||||
size: 'large',
|
size: 'large',
|
||||||
buttonText: 'Click me',
|
position: 'top-center',
|
||||||
inline: true,
|
|
||||||
duration: 4000,
|
duration: 4000,
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,31 @@ import { css } from '@emotion/react'
|
||||||
import { toastProviderClasses } from './ToastProvider.classes'
|
import { toastProviderClasses } from './ToastProvider.classes'
|
||||||
|
|
||||||
export const ToastProviderStyles = css`
|
export const ToastProviderStyles = css`
|
||||||
.${toastProviderClasses.toastsContainer} {
|
.${toastProviderClasses.toastContainer} {
|
||||||
|
position: fixed;
|
||||||
|
|
||||||
|
transition: all 230ms cubic-bezier(0.21, 1.02, 0.73, 1);
|
||||||
|
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.${toastProviderClasses.topLeft},
|
||||||
|
.${toastProviderClasses.topCenter},
|
||||||
|
.${toastProviderClasses.topRight} {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.${toastProviderClasses.bottomLeft},
|
||||||
|
.${toastProviderClasses.bottomCenter},
|
||||||
|
.${toastProviderClasses.bottomRight} {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.${toastProviderClasses.topCenter}, .${toastProviderClasses.bottomCenter} {
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.${toastProviderClasses.topRight}, .${toastProviderClasses.bottomRight} {
|
||||||
|
right: 0;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
|
@ -1,41 +1,94 @@
|
||||||
import React, { createContext, useContext, ReactNode } from 'react'
|
import React, { createContext, useContext, ReactNode } from 'react'
|
||||||
import {
|
import {
|
||||||
ToastOptions,
|
ToastOptions as HotToastOptions,
|
||||||
useToaster,
|
useToaster,
|
||||||
toast as hotToast,
|
toast as hotToast,
|
||||||
} from 'react-hot-toast/headless'
|
} from 'react-hot-toast/headless'
|
||||||
import { Toast, ToastProps } from '../Toast'
|
import { Toast, ToastProps } from '../Toast'
|
||||||
import { toastProviderClasses } from './ToastProvider.classes'
|
import { toastProviderClasses } from './ToastProvider.classes'
|
||||||
|
import { ToastPosition } from 'react-hot-toast'
|
||||||
|
import { Portal } from '../PortalProvider/Portal'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
type ShowToastType = (props: ToastProps, options?: ToastOptions) => void
|
export type ToastContent = {
|
||||||
|
title: ToastProps['title']
|
||||||
export const ToastContext = createContext<null | ShowToastType>(null)
|
information: ToastProps['information']
|
||||||
|
|
||||||
type ToastsProps = {
|
|
||||||
toastsPropsMap: Map<string, ToastProps>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Toasts: React.FC<ToastsProps> = ({ toastsPropsMap }) => {
|
export type ToastOptions = Pick<HotToastOptions, 'position' | 'duration'> &
|
||||||
|
Omit<ToastProps, 'title' | 'information'>
|
||||||
|
|
||||||
|
export type ToastContantAndOptions = ToastContent & ToastOptions
|
||||||
|
|
||||||
|
type ShowToastType = (content: ToastContent, options?: ToastOptions) => void
|
||||||
|
|
||||||
|
const getPositionStyle = (
|
||||||
|
position: ToastPosition | undefined,
|
||||||
|
offset: number,
|
||||||
|
): { positionClassName: string; transform: string } => {
|
||||||
|
if (!position)
|
||||||
|
return { positionClassName: '', transform: `translateY(${offset}px)` }
|
||||||
|
|
||||||
|
let positionClassName = ''
|
||||||
|
const isCenter = position.includes('center')
|
||||||
|
const isBottom = position.includes('bottom')
|
||||||
|
// Dynamic style part, not included in CSS classes.
|
||||||
|
const transform = `translateY(${isBottom ? -offset : offset}px) translateX(${
|
||||||
|
isCenter ? '-50%' : '0'
|
||||||
|
})`
|
||||||
|
|
||||||
|
if (position === 'top-left') {
|
||||||
|
positionClassName = toastProviderClasses.topLeft
|
||||||
|
} else if (position === 'top-center') {
|
||||||
|
positionClassName = toastProviderClasses.topCenter
|
||||||
|
} else if (position === 'top-right') {
|
||||||
|
positionClassName = toastProviderClasses.topRight
|
||||||
|
} else if (position === 'bottom-left') {
|
||||||
|
positionClassName = toastProviderClasses.bottomLeft
|
||||||
|
} else if (position === 'bottom-center') {
|
||||||
|
positionClassName = toastProviderClasses.bottomCenter
|
||||||
|
} else if (position === 'bottom-right') {
|
||||||
|
positionClassName = toastProviderClasses.bottomRight
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
positionClassName,
|
||||||
|
transform,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ToastContextType = ShowToastType | null
|
||||||
|
|
||||||
|
export const ToastContext = createContext<ToastContextType>(null)
|
||||||
|
|
||||||
|
type ToastContainerProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||||
|
toastsPropsMap: Map<string, ToastContantAndOptions>
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToastContainer: React.FC<ToastContainerProps> = ({
|
||||||
|
toastsPropsMap,
|
||||||
|
className,
|
||||||
|
...containerProps
|
||||||
|
}) => {
|
||||||
const { toasts, handlers } = useToaster()
|
const { toasts, handlers } = useToaster()
|
||||||
const { startPause, endPause, calculateOffset, updateHeight } = handlers
|
const { startPause, endPause, calculateOffset, updateHeight } = handlers
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Portal id="toast">
|
||||||
onMouseEnter={startPause}
|
|
||||||
onMouseLeave={endPause}
|
|
||||||
className={toastProviderClasses.toastsContainer}
|
|
||||||
>
|
|
||||||
{toasts.map((toast) => {
|
{toasts.map((toast) => {
|
||||||
const customProps = toastsPropsMap.get(toast.id)
|
const propsMapValue = toastsPropsMap.get(toast.id)
|
||||||
|
|
||||||
if (!customProps) {
|
if (!propsMapValue) {
|
||||||
console.error('Could not find toast with id', toast.id)
|
console.warn('Could not find toast with id', toast.id)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { position, duration, ...customProps } = propsMapValue
|
||||||
|
|
||||||
const offset = calculateOffset(toast, {
|
const offset = calculateOffset(toast, {
|
||||||
reverseOrder: false,
|
reverseOrder: false,
|
||||||
gutter: 8,
|
gutter: 8,
|
||||||
|
defaultPosition: position,
|
||||||
})
|
})
|
||||||
|
|
||||||
const ref = (el: HTMLDivElement | null) => {
|
const ref = (el: HTMLDivElement | null) => {
|
||||||
|
@ -45,29 +98,47 @@ const Toasts: React.FC<ToastsProps> = ({ toastsPropsMap }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { transform: positionTransform, positionClassName } =
|
||||||
|
getPositionStyle(position, offset)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Toast
|
<div
|
||||||
key={toast.id}
|
key={`container-${toast.id}`}
|
||||||
toastRefFunction={ref}
|
onMouseEnter={startPause}
|
||||||
|
onMouseLeave={endPause}
|
||||||
|
{...containerProps}
|
||||||
|
className={clsx(
|
||||||
|
toastProviderClasses.toastContainer,
|
||||||
|
positionClassName,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
transition: 'all 0.5s ease-out',
|
transform: positionTransform,
|
||||||
opacity: toast.visible ? 1 : 0,
|
...containerProps.style,
|
||||||
transform: `translateY(${offset}px)`,
|
|
||||||
}}
|
}}
|
||||||
{...toast.ariaProps}
|
>
|
||||||
{...customProps}
|
<Toast
|
||||||
onClose={() => {
|
key={toast.id}
|
||||||
hotToast.dismiss(toast.id)
|
className={clsx(customProps.className)}
|
||||||
customProps.onClose?.()
|
toastRef={ref}
|
||||||
}}
|
{...customProps}
|
||||||
/>
|
style={{
|
||||||
|
opacity: toast.visible ? 1 : 0,
|
||||||
|
...customProps.style,
|
||||||
|
}}
|
||||||
|
onClose={() => {
|
||||||
|
hotToast.dismiss(toast.id)
|
||||||
|
customProps.onClose?.()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</Portal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useLSDToast() {
|
export function useToast() {
|
||||||
const context = useContext(ToastContext)
|
const context = useContext(ToastContext)
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('useToast must be used within a ToastProvider')
|
throw new Error('useToast must be used within a ToastProvider')
|
||||||
|
@ -75,25 +146,39 @@ export function useLSDToast() {
|
||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToastProviderProps = {
|
type ToastProviderProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||||
|
providerToastOptions?: ToastOptions
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ToastProvider: React.FC<ToastProviderProps> = ({ children }) => {
|
export const ToastProvider: React.FC<ToastProviderProps> = ({
|
||||||
|
providerToastOptions,
|
||||||
|
children,
|
||||||
|
...toastsContainerProps
|
||||||
|
}) => {
|
||||||
const [toastsPropsMap, setToastsPropsMap] = React.useState<
|
const [toastsPropsMap, setToastsPropsMap] = React.useState<
|
||||||
Map<string, ToastProps>
|
Map<string, ToastContantAndOptions>
|
||||||
>(new Map())
|
>(new Map())
|
||||||
|
|
||||||
const showToast: ShowToastType = (toastProps, options) => {
|
const showToast: ShowToastType = (content, showToastOptions) => {
|
||||||
|
// There are 2 ways to define the toast options:
|
||||||
|
// 1. Globally, in the ToastProvider component's props.
|
||||||
|
// 2. Per-toast, in the showToast function's second argument.
|
||||||
|
// The per-toast options override the global options.
|
||||||
|
const options = {
|
||||||
|
...providerToastOptions,
|
||||||
|
...showToastOptions,
|
||||||
|
}
|
||||||
|
|
||||||
// The toast function displays the toast, and returns its ID.
|
// The toast function displays the toast, and returns its ID.
|
||||||
// The message is '' because we're not using it - currently
|
// The message is '' because we're not using it - currently
|
||||||
// we use the Toast component's 'title' and 'information' props to display info.
|
// we use the Toast component's 'title' and 'information' props to display info.
|
||||||
const toastId = hotToast('', options)
|
const toastId = hotToast('', { duration: options?.duration })
|
||||||
|
|
||||||
if (toastProps) {
|
if (content) {
|
||||||
setToastsPropsMap((prev) => {
|
setToastsPropsMap((prev) => {
|
||||||
const newMap = new Map(prev)
|
const newMap = new Map(prev)
|
||||||
newMap.set(toastId, toastProps)
|
newMap.set(toastId, { ...content, ...options })
|
||||||
return newMap
|
return newMap
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -102,7 +187,10 @@ export const ToastProvider: React.FC<ToastProviderProps> = ({ children }) => {
|
||||||
return (
|
return (
|
||||||
<ToastContext.Provider value={showToast}>
|
<ToastContext.Provider value={showToast}>
|
||||||
{children}
|
{children}
|
||||||
<Toasts toastsPropsMap={toastsPropsMap} />
|
<ToastContainer
|
||||||
|
toastsPropsMap={toastsPropsMap}
|
||||||
|
{...toastsContainerProps}
|
||||||
|
/>
|
||||||
</ToastContext.Provider>
|
</ToastContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue