Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui-kit): 스낵바 컴포넌트 추가 #41

Merged
merged 5 commits into from
Feb 7, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion ui-kit/src/components/LubyconUIKitProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { ReactNode } from 'react';
import { ToastProvider } from 'contexts/Toast';
import { PortalProvider } from 'contexts/Portal';
import { SnackbarProvider } from 'src/contexts/Snackbar';

interface Props {
children: ReactNode;
Expand All @@ -9,7 +10,9 @@ interface Props {
function LubyconUIKitProvider({ children }: Props) {
return (
<PortalProvider>
<ToastProvider>{children}</ToastProvider>
<SnackbarProvider>
<ToastProvider>{children}</ToastProvider>
</SnackbarProvider>
</PortalProvider>
);
}
Expand Down
26 changes: 26 additions & 0 deletions ui-kit/src/components/Snackbar/SnackbarBody.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, { ReactNode, isValidElement } from 'react';
import classnames from 'classnames';
import Text from 'components/Text';
import {} from 'react';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요거 삭제되면 좋을것 같습니당...!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fbce4f1 에서 반영되었습니다!

import Button from '../Button';

interface Props {
message: string;
button: ReactNode;
onClick?: () => void;
}

const SnackbarBody = ({ message, button, onClick }: Props) => {
return (
<div className={classnames('lubycon-snackbar__body', 'lubycon-shadow--3')}>
<Text typography="p2" className="lubycon-snackbar__text">
{message}
</Text>
<div className="lubycon-snackbar__body__buttons">
{isValidElement(button) ? button : <Button onClick={onClick}>{button}</Button>}
</div>
</div>
);
};

export default SnackbarBody;
93 changes: 93 additions & 0 deletions ui-kit/src/components/Snackbar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React, { HTMLAttributes, useEffect, useState, ReactNode } from 'react';
import { animated, useTransition } from 'react-spring';
import classnames from 'classnames';
import SnackbarBody from './SnackbarBody';
import { Combine } from 'src/types/utils';

export type SnackbarProps = Combine<
{
show: boolean;
message: string;
button: ReactNode;
autoHideDuration?: number;
onShow?: () => void;
onHide?: () => void;
onClick?: () => void;
},
Omit<HTMLAttributes<HTMLDivElement>, 'children'>
>;

const Snackbar = ({
show,
message,
button,
autoHideDuration,
onShow,
onHide,
onClick,
className,
style,
...rest
}: SnackbarProps) => {
const [isOpen, setOpen] = useState(show);
const transition = useTransition(isOpen, null, {
from: {
opacity: 0,
transform: 'translateX(-100%)',
height: 60,
},
enter: [
{ height: 60 },
{
opacity: 1,
transform: 'translateX(0)',
},
],
leave: [
{
opacity: 0,
transform: 'translateX(-100%)',
},
{ height: 0 },
],
onStart: () => {
onShow?.();
},
onDestroyed: () => {
onHide?.();
},
});

useEffect(() => {
let timer: NodeJS.Timeout;
if (autoHideDuration != null && isOpen === true) {
timer = setTimeout(() => {
setOpen(false);
}, autoHideDuration);
}

return () => clearTimeout(timer);
}, []);

return (
<>
{transition.map(({ item, key, props }) => {
return item ? (
<animated.div
key={key}
className={classnames('lubycon-snackbar', className)}
style={{
...style,
...props,
}}
{...rest}
>
<SnackbarBody message={message} button={button} onClick={onClick} />
</animated.div>
) : null;
})}
</>
);
};

export default Snackbar;
1 change: 1 addition & 0 deletions ui-kit/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export { default as LubyconUIKitProvider } from './LubyconUIKitProvider';
export { default as Toast } from './Toast';
export { default as Tooltip } from './Tooltip';
export { Tabs, TabPane } from './Tabs';
export { default as Snackbar } from './Snackbar';
79 changes: 79 additions & 0 deletions ui-kit/src/contexts/Snackbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React, { ReactNode, createContext, useState, useCallback, useContext } from 'react';
import classnames from 'classnames';
import Snackbar, { SnackbarProps } from 'components/Snackbar';
import { generateID } from 'src/utils';
import { Portal } from './Portal';
interface SnackbarOptions extends Omit<SnackbarProps, 'show'> {
duration?: number;
}

interface SnackbarGlobalState {
openSnackbar: (option: SnackbarOptions) => void;
closeSnackbar: (toastId: string) => void;
}
const SnackbarContext = createContext<SnackbarGlobalState>({
openSnackbar: () => {},
closeSnackbar: () => {},
});

interface SnackbarProviderProps {
children: ReactNode;
maxStack?: number;
}
export function SnackbarProvider({ children, maxStack = 1 }: SnackbarProviderProps) {
const [openedSnackbarQueue, setOpenedSnackbarQueue] = useState<SnackbarOptions[]>([]);

const openSnackbar = useCallback(
({ id = generateID('lubycon-snackbar'), ...option }: SnackbarOptions) => {
const snackbar = { id, ...option };
const [, ...rest] = openedSnackbarQueue;

if (openedSnackbarQueue.length >= maxStack) {
setOpenedSnackbarQueue([...rest, snackbar]);
} else {
setOpenedSnackbarQueue([...openedSnackbarQueue, snackbar]);
}
},
[openedSnackbarQueue]
);

const closeSnackbar = useCallback(
(closedSnackbarId: string) => {
setOpenedSnackbarQueue(
openedSnackbarQueue.filter((snackbar) => snackbar.id !== closedSnackbarId)
);
},
[openedSnackbarQueue]
);

return (
<SnackbarContext.Provider
value={{
openSnackbar,
closeSnackbar,
}}
>
{children}
<Portal>
<div className={classnames('lubycon-snackbar__context-container')}>
{openedSnackbarQueue.map(({ id, onHide, duration = 3000, ...snackbarProps }) => (
<Snackbar
key={id}
show={true}
autoHideDuration={duration}
onHide={() => {
closeSnackbar(id ?? '');
onHide?.();
}}
{...snackbarProps}
/>
))}
</div>
</Portal>
</SnackbarContext.Provider>
);
}

export function useSnackbar() {
return useContext(SnackbarContext);
}
3 changes: 0 additions & 3 deletions ui-kit/src/sass/components/_Button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,12 @@
transition: background-color 0.2s ease-in-out;

&__small {
height: 32px;
padding: 4px 16px;
}
&__medium {
height: 40px;
padding: 8px 16px;
}
&__large {
height: 56px;
padding: 12px 32px;
border-radius: 8px;
}
Expand Down
41 changes: 41 additions & 0 deletions ui-kit/src/sass/components/_Snackbar.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
.lubycon-snackbar {
overflow: visible;

.lubycon-snackbar__body {
display: inline-flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
min-width: 400px;
border-radius: 4px;
background-color: white;
margin: 12px 0;
}

.lubycon-snackbar__text {
white-space: pre;
}

.lubycon-snackbar__body__buttons {
margin-left: 16px;
.lubycon-button + .lubycon-button {
margin-left: 8px;
}
}

&:first-of-type {
.lubycon-snackbar__body {
margin-bottom: 0;
}
}
}

.lubycon-snackbar__context-container {
position: fixed;
display: flex;
flex-direction: column-reverse;
top: auto;
bottom: 40px;
left: 40px;
right: auto;
}
1 change: 1 addition & 0 deletions ui-kit/src/sass/components/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
@import './Toast';
@import './Tooltip';
@import './Tabs';
@import './Snackbar';
99 changes: 99 additions & 0 deletions ui-kit/src/stories/Snackbar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React, { useState } from 'react';
import { Meta } from '@storybook/react/types-6-0';
import Snackbar from 'components/Snackbar';
import Button from 'components/Button';
import { useSnackbar } from 'contexts/Snackbar';

export default {
title: 'Lubycon UI Kit/Snackbar',
component: Snackbar,
} as Meta;

export const Default = () => {
return (
<div>
<Snackbar show={true} message="데이터 전송이 완료되었습니다." button="실행취소" />
<Snackbar
show={true}
message={`16개의 이미지가\n“동물" 폴더에 추가되었습니다.`}
button="실행취소"
/>
</div>
);
};

export const AutoHide = () => {
const [show, setShow] = useState(true);
return (
<div>
<Snackbar show={true} message="데이터 전송이 완료되었습니다." button="실행취소" />
<Snackbar
show={show}
autoHideDuration={3000}
onHide={() => setShow(true)}
message={`16개의 이미지가\n“동물" 폴더에 추가되었습니다.`}
button="실행취소"
/>
</div>
);
};

export const SnackbarHooks = () => {
const { openSnackbar } = useSnackbar();
return (
<div>
<Button
onClick={() =>
openSnackbar({
message: `파일이 휴지통으로 이동되었습니다.`,
button: '실행취소',
})
}
>
스낵바 열기
</Button>
</div>
);
};

export const onClick = () => {
const { openSnackbar } = useSnackbar();
return (
<div>
<Button
onClick={() =>
openSnackbar({
message: `파일이 휴지통으로 이동되었습니다.`,
button: '실행취소',
onClick: () => alert('실행 취소 완료'),
})
}
>
스낵바 열기
</Button>
</div>
);
};

export const multipleButton = () => {
const { openSnackbar } = useSnackbar();
return (
<div>
<Button
onClick={() =>
openSnackbar({
message: '메세지가 전송되었습니다.',
button: (
<>
<Button onClick={() => alert('실행 취소 완료')}>실행취소</Button>
<Button onClick={() => alert('메세지 보기 클릭')}>메세지 보기</Button>
</>
),
})
}
>
스낵바 열기
</Button>
</div>
);
};
Loading