183 lines
4.5 KiB
TypeScript
183 lines
4.5 KiB
TypeScript
import { useCallback, useMemo, useRef } from 'react';
|
|
import { useTheme } from '@emotion/react';
|
|
import styled from '@emotion/styled';
|
|
|
|
import { IconAlertTriangle, IconX } from '@/ui/icon';
|
|
import {
|
|
ProgressBar,
|
|
ProgressBarControls,
|
|
} from '@/ui/progress-bar/components/ProgressBar';
|
|
import { rgba } from '@/ui/theme/constants/colors';
|
|
|
|
import { usePausableTimeout } from '../hooks/usePausableTimeout';
|
|
|
|
const StyledMotionContainer = styled.div<Pick<SnackbarProps, 'variant'>>`
|
|
align-items: center;
|
|
background-color: ${({ theme, variant }) => {
|
|
switch (variant) {
|
|
case 'error':
|
|
return theme.snackBar.error.background;
|
|
case 'success':
|
|
return theme.snackBar.success.background;
|
|
case 'info':
|
|
default:
|
|
return theme.color.gray80;
|
|
}
|
|
}};
|
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
|
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
|
color: ${({ theme, variant }) => {
|
|
switch (variant) {
|
|
case 'error':
|
|
return theme.snackBar.error.color;
|
|
case 'success':
|
|
return theme.snackBar.success.color;
|
|
case 'info':
|
|
default:
|
|
return theme.grayScale.gray0;
|
|
}
|
|
}};
|
|
cursor: pointer;
|
|
display: flex;
|
|
height: 40px;
|
|
overflow: hidden;
|
|
padding: ${({ theme }) => theme.spacing(2)};
|
|
pointer-events: auto;
|
|
position: relative;
|
|
`;
|
|
|
|
const StyledIconContainer = styled.div`
|
|
display: flex;
|
|
margin-right: ${({ theme }) => theme.spacing(2)};
|
|
`;
|
|
|
|
const StyledProgressBarContainer = styled.div`
|
|
height: 5px;
|
|
left: 0;
|
|
position: absolute;
|
|
right: 0;
|
|
top: 0;
|
|
`;
|
|
|
|
const StyledCloseButton = styled.button<Pick<SnackbarProps, 'variant'>>`
|
|
align-items: center;
|
|
background-color: transparent;
|
|
border: none;
|
|
border-radius: 12px;
|
|
color: ${({ theme, variant }) => {
|
|
switch (variant) {
|
|
case 'error':
|
|
return theme.color.red20;
|
|
case 'success':
|
|
return theme.color.turquoise20;
|
|
case 'info':
|
|
default:
|
|
return theme.grayScale.gray0;
|
|
}
|
|
}};
|
|
cursor: pointer;
|
|
display: flex;
|
|
height: 24px;
|
|
justify-content: center;
|
|
margin-left: ${({ theme }) => theme.spacing(6)};
|
|
padding-left: ${({ theme }) => theme.spacing(1)};
|
|
padding-right: ${({ theme }) => theme.spacing(1)};
|
|
width: 24px;
|
|
|
|
&:hover {
|
|
background-color: ${({ theme }) => rgba(theme.grayScale.gray0, 0.1)};
|
|
}
|
|
`;
|
|
|
|
export type SnackbarVariant = 'info' | 'error' | 'success';
|
|
|
|
export interface SnackbarProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
role?: 'alert' | 'status';
|
|
icon?: React.ReactNode;
|
|
message?: string;
|
|
allowDismiss?: boolean;
|
|
duration?: number;
|
|
variant?: SnackbarVariant;
|
|
children?: React.ReactNode;
|
|
onClose?: () => void;
|
|
}
|
|
|
|
export function SnackBar({
|
|
role = 'status',
|
|
icon: iconComponent,
|
|
message,
|
|
allowDismiss = true,
|
|
duration = 6000,
|
|
variant = 'info',
|
|
children,
|
|
onClose,
|
|
...rootProps
|
|
}: SnackbarProps) {
|
|
const theme = useTheme();
|
|
|
|
const progressBarRef = useRef<ProgressBarControls | null>(null);
|
|
|
|
const closeSnackbar = useCallback(() => {
|
|
onClose && onClose();
|
|
}, [onClose]);
|
|
|
|
const { pauseTimeout, resumeTimeout } = usePausableTimeout(
|
|
closeSnackbar,
|
|
duration,
|
|
);
|
|
|
|
const icon = useMemo(() => {
|
|
if (iconComponent) {
|
|
return iconComponent;
|
|
}
|
|
|
|
switch (variant) {
|
|
case 'error':
|
|
return (
|
|
<IconAlertTriangle aria-label="Error" size={theme.icon.size.md} />
|
|
);
|
|
case 'success':
|
|
case 'info':
|
|
default:
|
|
return null;
|
|
}
|
|
}, [iconComponent, theme.icon.size.md, variant]);
|
|
|
|
const onMouseEnter = () => {
|
|
progressBarRef.current?.pause();
|
|
pauseTimeout();
|
|
};
|
|
|
|
const onMouseLeave = () => {
|
|
progressBarRef.current?.start();
|
|
resumeTimeout();
|
|
};
|
|
|
|
return (
|
|
<StyledMotionContainer
|
|
aria-live={role === 'alert' ? 'assertive' : 'polite'}
|
|
role={role}
|
|
onMouseEnter={onMouseEnter}
|
|
onMouseLeave={onMouseLeave}
|
|
variant={variant}
|
|
{...rootProps}
|
|
>
|
|
<StyledProgressBarContainer>
|
|
<ProgressBar
|
|
ref={progressBarRef}
|
|
barHeight={5}
|
|
barColor={rgba(theme.grayScale.gray0, 0.3)}
|
|
duration={duration}
|
|
/>
|
|
</StyledProgressBarContainer>
|
|
{icon && <StyledIconContainer>{icon}</StyledIconContainer>}
|
|
{children ? children : message}
|
|
{allowDismiss && (
|
|
<StyledCloseButton variant={variant} onClick={closeSnackbar}>
|
|
<IconX aria-label="Close" size={theme.icon.size.md} />
|
|
</StyledCloseButton>
|
|
)}
|
|
</StyledMotionContainer>
|
|
);
|
|
}
|