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',
|
errorIcon: 'lsd-toast__error-icon',
|
||||||
errorIconContainer: 'lsd-toast__error-icon-container',
|
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'],
|
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
|
} as Meta
|
||||||
|
|
||||||
export const Root: Story<ToastProps> = ({ buttonText, ...args }) => {
|
export const Root: Story<ToastProps> = ({ buttonText, ...args }) => {
|
||||||
const [isVisible, setIsVisible] = useState(args.isOpen)
|
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 (
|
return (
|
||||||
<div style={{ width: 400, position: 'relative' }}>
|
<div style={{ width: 400, position: 'relative' }}>
|
||||||
<Button onClick={() => setIsVisible(!isVisible)}>Show Toast</Button>
|
<Button onClick={handleClick}>Show Toast</Button>
|
||||||
<Toast
|
<Toast
|
||||||
{...args}
|
{...args}
|
||||||
isOpen={isVisible}
|
isOpen={isVisible}
|
||||||
|
@ -37,7 +65,12 @@ Root.args = {
|
||||||
title: 'Toast Title',
|
title: 'Toast Title',
|
||||||
information: '',
|
information: '',
|
||||||
size: 'large',
|
size: 'large',
|
||||||
|
xOffset: 20,
|
||||||
|
yOffset: 20,
|
||||||
buttonText: 'Click me',
|
buttonText: 'Click me',
|
||||||
|
animation: 'auto',
|
||||||
|
position: 'top',
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
inline: true,
|
inline: true,
|
||||||
|
openTimeMilliseconds: 0,
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,13 +6,14 @@ export const ToastStyles = css`
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 20px;
|
align-items: center;
|
||||||
left: 20px;
|
|
||||||
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;
|
||||||
align-items: center;
|
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
|
|
||||||
|
height: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.${toastClasses.inlineButtonContainer} {
|
.${toastClasses.inlineButtonContainer} {
|
||||||
|
@ -86,6 +87,47 @@ export const ToastStyles = css`
|
||||||
left: -26px;
|
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.large} {
|
||||||
.${toastClasses.textContainer} {
|
.${toastClasses.textContainer} {
|
||||||
min-width: 204px;
|
min-width: 204px;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import React from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
CommonProps,
|
CommonProps,
|
||||||
omitCommonProps,
|
omitCommonProps,
|
||||||
|
@ -10,6 +10,10 @@ import { CloseIcon, ErrorIcon } from '../Icons'
|
||||||
import { IconButton } from '../IconButton'
|
import { IconButton } from '../IconButton'
|
||||||
import { Typography } from '../Typography'
|
import { Typography } from '../Typography'
|
||||||
import { Button } from '../Button'
|
import { Button } from '../Button'
|
||||||
|
import {
|
||||||
|
getToastAnimationClass,
|
||||||
|
setToastPosition,
|
||||||
|
} from '../../utils/toast.utils'
|
||||||
|
|
||||||
export type ToastProps = CommonProps &
|
export type ToastProps = CommonProps &
|
||||||
Omit<React.HTMLAttributes<HTMLDivElement>, 'label'> & {
|
Omit<React.HTMLAttributes<HTMLDivElement>, 'label'> & {
|
||||||
|
@ -21,6 +25,17 @@ export type ToastProps = CommonProps &
|
||||||
onButtonClick?: () => void
|
onButtonClick?: () => void
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
size?: 'large' | 'medium' | 'small'
|
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> & {
|
export const Toast: React.FC<ToastProps> & {
|
||||||
|
@ -34,11 +49,57 @@ export const Toast: React.FC<ToastProps> & {
|
||||||
onButtonClick,
|
onButtonClick,
|
||||||
onClose,
|
onClose,
|
||||||
size = 'large',
|
size = 'large',
|
||||||
|
position = 'top',
|
||||||
|
animation = 'auto',
|
||||||
|
openTimeMilliseconds,
|
||||||
|
xOffset = 20,
|
||||||
|
yOffset = 20,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const commonProps = useCommonProps(props)
|
const commonProps = useCommonProps(props)
|
||||||
const isInlineButtonHidden = !inline || !!information || !buttonText
|
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 the toast is not open, do not render anything.
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
|
@ -47,12 +108,16 @@ export const Toast: React.FC<ToastProps> & {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={toastRef}
|
||||||
{...omitCommonProps(props)}
|
{...omitCommonProps(props)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
commonProps.className,
|
commonProps.className,
|
||||||
toastClasses.root,
|
toastClasses.root,
|
||||||
toastClasses[size],
|
toastClasses[size],
|
||||||
|
getToastAnimationClass(position, animation),
|
||||||
|
isClosing && toastClasses.closingAnimation,
|
||||||
)}
|
)}
|
||||||
|
onAnimationEnd={handleAnimationEnd}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
|
@ -103,7 +168,7 @@ export const Toast: React.FC<ToastProps> & {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={onClose}
|
onClick={handleClose}
|
||||||
className={toastClasses.closeButton}
|
className={toastClasses.closeButton}
|
||||||
size="medium"
|
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