Skip to content

Commit

Permalink
[Feat] Toast 컴포넌트 추가 (#52)
Browse files Browse the repository at this point in the history
* chore(packages/ui): ovarlay-kit 의존성 추가

* feat(packages/ui): Toast 컴포넌트 구현

* test(apps/web): Toast 컴포넌트 사용 예시 추가

* fix(packages/ui): 접근성 개선

* chore(packages/ui): lock 파일 업데이트

* chore(packages/themes): violet 색상 추가

* chore(packages/ui): success일 경우의 색상 변경

* fix(apps/web): Providers 컴포넌트 분리, OverlayProvider 이동
  • Loading branch information
minseong0324 authored Jan 12, 2025
1 parent 49fe65f commit 562478e
Show file tree
Hide file tree
Showing 19 changed files with 482 additions and 12 deletions.
3 changes: 2 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@
"@repo/theme": "workspace:^",
"@repo/ui": "workspace:^",
"next": "14.2.21",
"overlay-kit": "^1.4.1",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@repo/theme": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@repo/ui": "workspace:*",
"@types/node": "^20.11.24",
"@types/react": "18.3.0",
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import localFont from 'next/font/local';
import './globals.css';
import '@repo/theme/styles';
import '@repo/ui/styles';
import { ThemeProvider } from '@repo/theme';
import { Providers } from '../components/Providers/Providers';

const geistSans = localFont({
src: './fonts/GeistVF.woff',
Expand All @@ -29,8 +29,7 @@ export default function RootLayout({
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable}`}>
<ThemeProvider theme="light">{children}</ThemeProvider>{' '}
{/** TODO: 추후 시스템 감지 설정 추가 예정 */}
<Providers>{children}</Providers>
</body>
</html>
);
Expand Down
32 changes: 31 additions & 1 deletion apps/web/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,41 @@
import { Icon, Text } from '@repo/ui';
'use client';

import { Icon, Toast, Text } from '@repo/ui';
import { overlay } from 'overlay-kit';

export default function Home() {
const notify1 = () =>
overlay.open(({ isOpen, close, unmount }) => (
<Toast
open={isOpen}
onClose={close}
leftAddon={<Toast.Icon toastType="success" />}
onExited={unmount}
>
생성된 본문이 업데이트 됐어요!
</Toast>
));

const notify2 = () =>
overlay.open(({ isOpen, close, unmount }) => (
<Toast
open={isOpen}
onClose={close}
leftAddon={<Toast.Icon toastType="error" />}
onExited={unmount}
>
1개 이상의 게시물을 선택해주세요
</Toast>
));

return (
<div>
웹 1팀 파이팅!
<Icon size={24} name="stack" type="stroke" />
<Icon size={24} name="stack" type="fill" />
<Icon size={24} name="stack" type="stroke" color="warning300" />
<button onClick={notify1}>success 토스트 열기</button>
<button onClick={notify2}>warning 토스트 열기</button>
<Text.H1 color="grey950" fontSize={28} fontWeight="semibold">
hihi
</Text.H1>
Expand Down
17 changes: 17 additions & 0 deletions apps/web/src/components/Providers/Providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client';

import { ThemeProvider } from '@repo/theme';
import { OverlayProvider } from 'overlay-kit';
import { ReactNode } from 'react';

type ProvidersProps = {
children: ReactNode;
};

export function Providers({ children }: ProvidersProps) {
return (
<ThemeProvider theme="light">
<OverlayProvider>{children}</OverlayProvider>
</ThemeProvider>
);
}
6 changes: 6 additions & 0 deletions packages/theme/src/themes/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ export type ThemeContract = {
blue200: string;
blue400: string;
blue800: string;

violet0: string;
violet100: string;
violet200: string;
violet400: string;
violet800: string;
};
space: {
[K in keyof typeof tokens.spacing]: string;
Expand Down
6 changes: 6 additions & 0 deletions packages/theme/src/themes/dark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ export const darkTheme: ThemeContract = {
blue200: tokens.colors.blue200,
blue400: tokens.colors.blue400,
blue800: tokens.colors.blue800,

violet0: tokens.colors.violet0,
violet100: tokens.colors.violet100,
violet200: tokens.colors.violet200,
violet400: tokens.colors.violet400,
violet800: tokens.colors.violet800,
},
space: tokens.spacing,
borderRadius: tokens.radius,
Expand Down
6 changes: 6 additions & 0 deletions packages/theme/src/themes/light.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ export const lightTheme: ThemeContract = {
blue200: tokens.colors.blue200,
blue400: tokens.colors.blue400,
blue800: tokens.colors.blue800,

violet0: tokens.colors.violet0,
violet100: tokens.colors.violet100,
violet200: tokens.colors.violet200,
violet400: tokens.colors.violet400,
violet800: tokens.colors.violet800,
},
space: tokens.spacing,
borderRadius: tokens.radius,
Expand Down
6 changes: 6 additions & 0 deletions packages/theme/src/tokens/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ export const colors = {
blue400: '#7698E2',
blue800: '#153C66',

violet0: '#F2F3FF',
violet100: '#DFE1FE',
violet200: '#B7B4FF',
violet400: '#817ED5',
violet800: '#4A46A3',

grey0: '#FFFFFF',
grey25: '#F7F9FB',
grey50: '#EAEFF4',
Expand Down
1 change: 1 addition & 0 deletions packages/ui/esbuild.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const buildOptions = {
}),
],
loader: { '.css': 'file' },
allowOverwrite: true,
outdir,
external: ['react'],
};
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
},
"dependencies": {
"@repo/theme": "workspace:^",
"@types/react-dom": "18.3.1",
"@vanilla-extract/css": "^1.17.0",
"@vanilla-extract/recipes": "^0.5.5",
"motion": "^11.17.0",
"react": "^18"
},
"devDependencies": {
Expand Down
24 changes: 24 additions & 0 deletions packages/ui/src/components/Toast/Toast.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { style } from '@vanilla-extract/css';
import { tokens } from '@repo/theme';

export const container = style({
position: 'fixed',
bottom: 40,
right: 40,
padding: `${tokens.spacing[20]} ${tokens.spacing[32]}`,
borderRadius: 100,
backgroundColor: tokens.colors.grey700,
color: tokens.colors.grey0,
});

export const content = style({
display: 'flex',
alignItems: 'center',
gap: tokens.spacing[8],
});

export const message = style({
fontSize: tokens.typography.fontSize[20],
fontWeight: tokens.typography.fontWeight.semibold,
lineHeight: '24px',
});
141 changes: 141 additions & 0 deletions packages/ui/src/components/Toast/Toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { motion, AnimatePresence, HTMLMotionProps } from 'motion/react';
import {
ForwardRefExoticComponent,
ReactNode,
forwardRef,
useEffect,
KeyboardEvent,
useRef,
} from 'react';
import { ToastIcon } from './compounds/Icon/Icon';
import * as styles from './Toast.css';
import { useTimer } from './hooks/useTimer';
import { mergeRefs } from '@/utils';

export type ToastType = 'default' | 'success' | 'error';

export type ToastProps = {
/**
* 토스트 타입
* @default 'default'
*/
toastType?: ToastType;
/**
* 왼쪽 추가 요소 (아이콘 등)
*/
leftAddon?: ReactNode;
/**
* 토스트 지속 시간
* @default 2000
*/
duration?: number;
/**
* 자식 요소
*/
children?: ReactNode;
/**
* 토스트 열기 여부
*/
open: boolean;
/**
* 토스트가 열릴 때 호출되는 함수
*/
onOpen?: VoidFunction;
/**
* 토스트가 닫힐 때 호출되는 함수
*/
onClose?: VoidFunction;
/**
* 토스트가 완전히 닫힌 후 호출되는 함수
*/
onExited?: VoidFunction;
} & Omit<HTMLMotionProps<'div'>, 'children'>;

const ToastComponent = forwardRef<HTMLDivElement, ToastProps>(
(
{
toastType = 'default',
leftAddon,
duration = 2000,
children,
open,
onOpen,
onClose,
onExited,
style: toastStyle,
...restProps
},
ref
) => {
const { startCurrentTimer, clearCurrentTimeout } = useTimer({
onTimerEnd: onClose,
timeoutSecond: duration,
});
const toastRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (open) {
onOpen?.();
startCurrentTimer();
toastRef.current?.focus();
}
}, [open, onOpen, startCurrentTimer]);

const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose?.();
}
};

return (
<AnimatePresence onExitComplete={onExited}>
{open && (
<motion.div
ref={mergeRefs(ref, toastRef)}
className={styles.container}
initial={{ y: '120%', opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: '120%', opacity: 0 }}
transition={{
type: 'spring',
damping: 25,
stiffness: 400,
opacity: {
duration: 0.15,
ease: 'easeInOut',
},
}}
onPointerEnter={clearCurrentTimeout}
onPointerLeave={startCurrentTimer}
style={{
...toastStyle,
}}
role="alert"
aria-live="polite"
tabIndex={0}
onKeyDown={handleKeyDown}
{...restProps}
>
<div className={styles.content}>
{leftAddon ?? (
<ToastIcon toastType={toastType} aria-hidden="true" />
)}
<span className={styles.message}>{children}</span>
</div>
</motion.div>
)}
</AnimatePresence>
);
}
);

type ToastComposition = {
Icon: typeof ToastIcon;
};

export const Toast: ForwardRefExoticComponent<ToastProps> & ToastComposition =
Object.assign(ToastComponent, {
Icon: ToastIcon,
});

Toast.displayName = 'Toast';
38 changes: 38 additions & 0 deletions packages/ui/src/components/Toast/compounds/Icon/Icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Icon } from '@repo/ui';
import type { IconProps } from '@repo/ui';
import { ToastType } from '../../Toast';

export type ToastIconProps = Omit<IconProps, 'name'> & {
toastType?: ToastType;
};

export function ToastIcon({
toastType = 'default',
...restProps
}: ToastIconProps) {
const iconName = (() => {
switch (toastType) {
case 'success':
return 'check';
case 'error':
return 'notice';
default:
return null;
}
})();

if (!iconName) {
return null;
}

const iconColor = (() => {
switch (toastType) {
case 'success':
return 'violet200';
case 'error':
return 'warning300';
}
})();

return <Icon type="fill" name={iconName} color={iconColor} {...restProps} />;
}
Loading

0 comments on commit 562478e

Please sign in to comment.