@axeptio/design-system
Version:
Design System for Axeptio
425 lines (364 loc) • 8.53 kB
JSX
import React, { useState, useMemo, createContext, useContext } from 'react';
import { createPortal } from 'react-dom';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import Icon from '../Icon';
const ProgressBar = styled.div`
@keyframes progressBar {
0% {
transform: scaleX(1);
}
100% {
transform: scaleX(0);
}
}
background: ${props => props.progressBarColor};
position: absolute;
border-radius: 0px 0px 0px 6px;
overflow: hidden;
bottom: 0;
left: 0;
width: 100%;
height: 5px;
z-index: 9999;
transform-origin: left;
animation-duration: 5000ms;
opacity: 1;
animation-name: progressBar;
transform: scaleX(0);
`;
const Root = styled.div`
position: relative;
${props =>
props.absolute &&
`
position: absolute;
`}
${props =>
props.position === 'top-left' &&
`
top: 20px;
left: 20px;
`}
${props =>
props.position === 'top-right' &&
`
top: 20px;
right: 20px;
`}
${props =>
props.position === 'top-middle' &&
`
top: 20px;
left: 50%;
transform: translate(-50%);
`}
${props =>
props.position === 'bottom-left' &&
`
bottom: 20px;
left: 20px;
`}
${props =>
props.position === 'bottom-right' &&
`
bottom: 20px;
right: 20px;
`}
${props =>
props.position === 'bottom-middle' &&
`
bottom: 20px;
left: 50%;
transform: translate(-50%);
`}
margin-top: 16px;
margin-bottom: 16px;
display: flex;
flex-direction: row;
padding: 15px;
width: 290px;
max-width: 100vw;
z-index: 2147483647;
outline: none;
box-shadow:
hsl(206 22% 7% / 35%) 0px 10px 38px -10px,
hsl(206 22% 7% / 20%) 0px 10px 20px -15px;
border-radius: 6px;
align-items: center;
cursor: pointer;
background-color: ${props => props.backgroundColor};
&:hover ${ProgressBar} {
animation-play-state: paused;
}
@keyframes bounceInMiddle {
from,
20%,
40%,
60%,
80%,
to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
transform: scale3d(0.3, 0.3, 0.3) translate(-50%);
}
20% {
transform: scale3d(1.1, 1.1, 1.1) translate(-50%);
}
40% {
transform: scale3d(0.9, 0.9, 0.9) translate(-50%);
}
60% {
opacity: 1;
transform: scale3d(1.03, 1.03, 1.03) translate(-50%);
}
80% {
transform: scale3d(0.97, 0.97, 0.97) translate(-50%);
}
to {
opacity: 1;
transform: scale3d(1, 1, 1) translate(-50%);
}
}
@keyframes bounceIn {
from,
20%,
40%,
60%,
80%,
to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
transform: scale3d(0.3, 0.3, 0.3);
}
20% {
transform: scale3d(1.1, 1.1, 1.1);
}
40% {
transform: scale3d(0.9, 0.9, 0.9);
}
60% {
opacity: 1;
transform: scale3d(1.03, 1.03, 1.03);
}
80% {
transform: scale3d(0.97, 0.97, 0.97);
}
to {
opacity: 1;
transform: scale3d(1, 1, 1);
}
}
animation-duration: 1s;
${props =>
(props.position === 'top-middle' || props.position === 'bottom-middle') &&
`
animation-name: ${props.open ? 'bounceInMiddle' : 'bounceOutMiddle'};
`}
${props =>
props.position !== 'top-middle' &&
props.position !== 'bottom-middle' &&
`
animation-name: ${props.open ? 'bounceIn' : 'bounceOut'};
`}
@keyframes bounceOut {
20% {
transform: scale3d(0.9, 0.9, 0.9);
}
50%,
55% {
opacity: 1;
transform: scale3d(1.1, 1.1, 1.1);
}
to {
opacity: 0;
transform: scale3d(0.3, 0.3, 0.3);
}
}
@keyframes bounceOutMiddle {
20% {
transform: scale3d(0.9, 0.9, 0.9) translate(-50%);
}
50%,
55% {
opacity: 1;
transform: scale3d(1.1, 1.1, 1.1) translate(-50%);
}
to {
opacity: 0;
transform: scale3d(0.3, 0.3, 0.3) translate(-50%);
}
}
`;
const ToastWrapper = styled.div`
position: absolute;
${props =>
props.position === 'top-left' &&
`
top: 20px;
left: 20px;
`}
${props =>
props.position === 'top-right' &&
`
top: 20px;
right: 20px;
`}
${props =>
props.position === 'top-middle' &&
`
top: 20px;
left: 50%;
transform: translate(-50%);
`}
${props =>
props.position === 'bottom-left' &&
`
bottom: 20px;
left: 20px;
`}
${props =>
props.position === 'bottom-right' &&
`
bottom: 20px;
right: 20px;
`}
${props =>
props.position === 'bottom-middle' &&
`
bottom: 20px;
left: 50%;
transform: translate(-50%);
`}
`;
const TextContainer = styled.div`
display: flex;
flex-direction: column;
margin-left: 20px;
`;
const Title = styled.span`
font-family: ${props => props.theme.fonts.text};
font-style: normal;
font-weight: 700;
font-size: 18px;
color: ${props => props.color};
`;
const Description = styled.span`
font-family: ${props => props.theme.fonts.text};
font-style: normal;
font-weight: 400;
font-size: 16px;
color: ${props => props.color};
`;
export const ToastContext = createContext();
export const useToast = () => useContext(ToastContext);
function generateUEID() {
let first = (Math.random() * 46656) | 0;
let second = (Math.random() * 46656) | 0;
first = ('000' + first.toString(36)).slice(-3);
second = ('000' + second.toString(36)).slice(-3);
return first + second;
}
export const ToastProvider = props => {
const [toasts, setToasts] = useState([]);
const open = content => {
setToasts(currentToasts => [...currentToasts, { id: generateUEID(), content }]);
};
const close = id => setToasts(currentToasts => currentToasts.filter(toast => toast.id !== id));
const contextValue = useMemo(() => ({ open }), []);
return (
<ToastContext.Provider value={contextValue}>
{props.children}
{createPortal(
<ToastWrapper position={toasts.length > 0 ? toasts[0].content.position : ''}>
{toasts.map(toast => (
<Toast key={toast.id} {...toast.content} close={() => close(toast.id)} />
))}
</ToastWrapper>,
document.body
)}
</ToastContext.Provider>
);
};
ToastProvider.propTypes = {
children: PropTypes.node
};
const Toast = props => {
const { color, position, absolute, backgroundColor, title, description, progressBarColor, icon } = props;
const [open, setOpen] = useState(true);
const [startTime, setStartTime] = useState(null);
const [timeout, setOurTimeout] = useState(null);
const [remaining, setRemaining] = useState(null);
const close = () => {
setOpen(false);
setTimeout(props.close, 1000);
};
const launchTimeout = (time = 4000) => {
if (startTime === null) {
setStartTime(Date.now());
}
let timeout = setTimeout(close, time);
setOurTimeout(timeout);
return timeout;
};
if (!timeout) {
launchTimeout();
}
const onMouseHover = () => {
if (remaining === null) {
const elapsed = Date.now() - startTime;
const remaining = 4000 - elapsed;
setRemaining(remaining);
}
clearTimeout(timeout);
};
const onMouseLeave = () => {
launchTimeout(remaining);
};
return (
<Root
absolute={absolute}
position={position}
onMouseOver={onMouseHover}
onMouseEnter={onMouseHover}
onMouseLeave={onMouseLeave}
open={open}
onClick={close}
backgroundColor={backgroundColor}
>
<Icon strokeColor={color} name={icon} />
<TextContainer>
<Title color={color}>{title}</Title>
<Description color={color}>{description}</Description>
</TextContainer>
<ProgressBar progressBarColor={progressBarColor} />
</Root>
);
};
Toast.propTypes = {
absolute: PropTypes.bool,
position: PropTypes.oneOf[('top-left', 'top-middle', 'top-right', 'bottom-left', 'bottom-middle', 'bottom-right')],
backgroundColor: PropTypes.string,
progressBarColor: PropTypes.string,
color: PropTypes.string,
title: PropTypes.string,
description: PropTypes.string,
icon: PropTypes.string,
close: PropTypes.func
};
Toast.defaultProps = {
absolute: false,
position: 'top-middle',
color: '#212121',
title: '',
description: '',
backgroundColor: '#5cb85c',
progressBarColor: '#ffce43',
icon: null,
close: null
};
export default Toast;