diff --git a/packages/lsd-react/src/components/Toast/Toast.classes.ts b/packages/lsd-react/src/components/Toast/Toast.classes.ts index b5c49e2..7f181af 100644 --- a/packages/lsd-react/src/components/Toast/Toast.classes.ts +++ b/packages/lsd-react/src/components/Toast/Toast.classes.ts @@ -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', } diff --git a/packages/lsd-react/src/components/Toast/Toast.stories.tsx b/packages/lsd-react/src/components/Toast/Toast.stories.tsx index 438ed18..4738684 100644 --- a/packages/lsd-react/src/components/Toast/Toast.stories.tsx +++ b/packages/lsd-react/src/components/Toast/Toast.stories.tsx @@ -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 = ({ 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 (
- + , '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 & { @@ -34,11 +49,57 @@ export const Toast: React.FC & { 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(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 | 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 & { return (
& {
diff --git a/packages/lsd-react/src/utils/toast.utils.ts b/packages/lsd-react/src/utils/toast.utils.ts new file mode 100644 index 0000000..f742bdf --- /dev/null +++ b/packages/lsd-react/src/utils/toast.utils.ts @@ -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 '' + } +}