UNPKG

@axeptio/design-system

Version:
425 lines (364 loc) 8.53 kB
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;