mirror of https://github.com/acid-info/lsd.git
feat: adds animation and position props to toast component
This commit is contained in:
parent
dc9ce1f4a8
commit
1f249490ce
|
@ -21,4 +21,9 @@ export const toastClasses = {
|
|||
|
||||
errorIcon: 'lsd-toast__error-icon',
|
||||
errorIconContainer: 'lsd-toast__error-icon-container',
|
||||
|
||||
moveUp: 'lsd-toast--move-up',
|
||||
moveDown: 'lsd-toast--move-down',
|
||||
|
||||
closingAnimation: 'lsd-toast--closing-animation',
|
||||
}
|
||||
|
|
|
@ -13,15 +13,43 @@ export default {
|
|||
value: ['small', 'medium', 'large'],
|
||||
},
|
||||
},
|
||||
position: {
|
||||
type: {
|
||||
name: 'enum',
|
||||
value: [
|
||||
'top-left',
|
||||
'top',
|
||||
'top-right',
|
||||
'bottom-left',
|
||||
'bottom',
|
||||
'bottom-right',
|
||||
],
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
type: {
|
||||
name: 'enum',
|
||||
value: ['auto', 'moveDown', 'moveUp'],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Meta
|
||||
|
||||
export const Root: Story<ToastProps> = ({ buttonText, ...args }) => {
|
||||
const [isVisible, setIsVisible] = useState(args.isOpen)
|
||||
|
||||
// When the "Show toast" button is clicked, close the toast and then open it again.
|
||||
const handleClick = () => {
|
||||
setIsVisible(false)
|
||||
|
||||
setTimeout(() => {
|
||||
setIsVisible(true)
|
||||
}, 200)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ width: 400, position: 'relative' }}>
|
||||
<Button onClick={() => setIsVisible(!isVisible)}>Show Toast</Button>
|
||||
<Button onClick={handleClick}>Show Toast</Button>
|
||||
<Toast
|
||||
{...args}
|
||||
isOpen={isVisible}
|
||||
|
@ -37,7 +65,12 @@ Root.args = {
|
|||
title: 'Toast Title',
|
||||
information: '',
|
||||
size: 'large',
|
||||
xOffset: 20,
|
||||
yOffset: 20,
|
||||
buttonText: 'Click me',
|
||||
animation: 'auto',
|
||||
position: 'top',
|
||||
isOpen: false,
|
||||
inline: true,
|
||||
openTimeMilliseconds: 0,
|
||||
}
|
||||
|
|
|
@ -6,13 +6,14 @@ export const ToastStyles = css`
|
|||
box-sizing: border-box;
|
||||
display: flex;
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
align-items: center;
|
||||
background: rgb(var(--lsd-surface-primary));
|
||||
border: 1px solid rgb(var(--lsd-border-primary));
|
||||
padding: 8px;
|
||||
align-items: center;
|
||||
|
||||
z-index: 9999;
|
||||
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.${toastClasses.inlineButtonContainer} {
|
||||
|
@ -86,6 +87,47 @@ export const ToastStyles = css`
|
|||
left: -26px;
|
||||
}
|
||||
|
||||
@keyframes moveDown {
|
||||
from {
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveUp {
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOutAnimation {
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
}
|
||||
|
||||
.${toastClasses.moveDown} {
|
||||
animation: moveDown 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
.${toastClasses.moveUp} {
|
||||
animation: moveUp 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
.${toastClasses.closingAnimation} {
|
||||
animation: fadeOutAnimation 0.3s forwards;
|
||||
}
|
||||
|
||||
.${toastClasses.large} {
|
||||
.${toastClasses.textContainer} {
|
||||
min-width: 204px;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import clsx from 'clsx'
|
||||
import React from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
CommonProps,
|
||||
omitCommonProps,
|
||||
|
@ -10,6 +10,10 @@ import { CloseIcon, ErrorIcon } from '../Icons'
|
|||
import { IconButton } from '../IconButton'
|
||||
import { Typography } from '../Typography'
|
||||
import { Button } from '../Button'
|
||||
import {
|
||||
getToastAnimationClass,
|
||||
setToastPosition,
|
||||
} from '../../utils/toast.utils'
|
||||
|
||||
export type ToastProps = CommonProps &
|
||||
Omit<React.HTMLAttributes<HTMLDivElement>, 'label'> & {
|
||||
|
@ -21,6 +25,17 @@ export type ToastProps = CommonProps &
|
|||
onButtonClick?: () => void
|
||||
onClose?: () => void
|
||||
size?: 'large' | 'medium' | 'small'
|
||||
position?:
|
||||
| 'top-left'
|
||||
| 'top'
|
||||
| 'top-right'
|
||||
| 'bottom-left'
|
||||
| 'bottom'
|
||||
| 'bottom-right'
|
||||
animation?: 'auto' | 'moveDown' | 'moveUp'
|
||||
openTimeMilliseconds?: number
|
||||
xOffset?: number
|
||||
yOffset?: number
|
||||
}
|
||||
|
||||
export const Toast: React.FC<ToastProps> & {
|
||||
|
@ -34,11 +49,57 @@ export const Toast: React.FC<ToastProps> & {
|
|||
onButtonClick,
|
||||
onClose,
|
||||
size = 'large',
|
||||
position = 'top',
|
||||
animation = 'auto',
|
||||
openTimeMilliseconds,
|
||||
xOffset = 20,
|
||||
yOffset = 20,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const commonProps = useCommonProps(props)
|
||||
const isInlineButtonHidden = !inline || !!information || !buttonText
|
||||
const toastRef = useRef<HTMLDivElement | null>(null)
|
||||
// Used for the closing animation.
|
||||
const [isClosing, setIsClosing] = useState(false)
|
||||
|
||||
// Closing flow:
|
||||
// 1. handleClose is called.
|
||||
// 2. handleClose sets isClosing to true.
|
||||
// 3. isClosing triggers the closing animation.
|
||||
// 4. When the closing animation ends, handleAnimationEnd is called.
|
||||
const handleClose = () => {
|
||||
setIsClosing(true)
|
||||
}
|
||||
|
||||
const handleAnimationEnd = () => {
|
||||
if (isClosing && onClose) {
|
||||
onClose()
|
||||
setIsClosing(false) // reset the state for potential reuse of the component
|
||||
}
|
||||
}
|
||||
|
||||
// Close the timer after the specified time.
|
||||
useEffect(() => {
|
||||
if (!isOpen || !openTimeMilliseconds) return
|
||||
|
||||
let timer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
if (openTimeMilliseconds && onClose) {
|
||||
timer = setTimeout(onClose, openTimeMilliseconds)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timer) clearTimeout(timer)
|
||||
}
|
||||
}, [isOpen, openTimeMilliseconds, onClose])
|
||||
|
||||
// Sets the toast position.
|
||||
useEffect(() => {
|
||||
if (isOpen && toastRef.current) {
|
||||
setToastPosition(toastRef.current, position, xOffset, yOffset)
|
||||
}
|
||||
}, [isOpen, position])
|
||||
|
||||
// If the toast is not open, do not render anything.
|
||||
if (!isOpen) {
|
||||
|
@ -47,12 +108,16 @@ export const Toast: React.FC<ToastProps> & {
|
|||
|
||||
return (
|
||||
<div
|
||||
ref={toastRef}
|
||||
{...omitCommonProps(props)}
|
||||
className={clsx(
|
||||
commonProps.className,
|
||||
toastClasses.root,
|
||||
toastClasses[size],
|
||||
getToastAnimationClass(position, animation),
|
||||
isClosing && toastClasses.closingAnimation,
|
||||
)}
|
||||
onAnimationEnd={handleAnimationEnd}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
|
@ -103,7 +168,7 @@ export const Toast: React.FC<ToastProps> & {
|
|||
</div>
|
||||
|
||||
<IconButton
|
||||
onClick={onClose}
|
||||
onClick={handleClose}
|
||||
className={toastClasses.closeButton}
|
||||
size="medium"
|
||||
>
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import { ToastProps } from '../components/Toast'
|
||||
import { toastClasses } from '../components/Toast/Toast.classes'
|
||||
|
||||
export const setToastPosition = (
|
||||
toastElement: HTMLDivElement,
|
||||
position: ToastProps['position'],
|
||||
xOffset: number = 0,
|
||||
yOffset: number = 0,
|
||||
) => {
|
||||
switch (position) {
|
||||
case 'top-left':
|
||||
toastElement.style.top = `${yOffset}px`
|
||||
toastElement.style.left = `${xOffset}px`
|
||||
break
|
||||
case 'top':
|
||||
toastElement.style.top = `${yOffset}px`
|
||||
toastElement.style.left = `${
|
||||
(window.innerWidth - toastElement.offsetWidth) / 2 + xOffset
|
||||
}px`
|
||||
break
|
||||
case 'top-right':
|
||||
toastElement.style.top = `${yOffset}px`
|
||||
toastElement.style.right = `${xOffset}px`
|
||||
break
|
||||
case 'bottom-left':
|
||||
toastElement.style.bottom = `${yOffset}px`
|
||||
toastElement.style.left = `${xOffset}px`
|
||||
break
|
||||
case 'bottom':
|
||||
toastElement.style.bottom = `${yOffset}px`
|
||||
toastElement.style.left = `${
|
||||
(window.innerWidth - toastElement.offsetWidth) / 2 + xOffset
|
||||
}px`
|
||||
break
|
||||
case 'bottom-right':
|
||||
toastElement.style.bottom = `${yOffset}px`
|
||||
toastElement.style.right = `${xOffset}px`
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
export const getToastAnimationClass = (
|
||||
position: ToastProps['position'],
|
||||
animation: ToastProps['animation'],
|
||||
) => {
|
||||
switch (animation) {
|
||||
case 'moveDown':
|
||||
return toastClasses.moveDown
|
||||
case 'moveUp':
|
||||
return toastClasses.moveUp
|
||||
case 'auto':
|
||||
return position!.startsWith('top')
|
||||
? toastClasses.moveDown
|
||||
: toastClasses.moveUp
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue