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

fea(ui-kit): Toast 추가 #33

Merged
merged 12 commits into from
Jan 18, 2021
Merged
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ module.exports = {
],
rules: {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-empty-function': 'off',
},
};
7 changes: 5 additions & 2 deletions ui-kit/.storybook/preview.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import '../src/sass/index.scss';
import React from 'react';
import { LubyconUIKitProvider } from '../src/components';

export const decorators = [(Story => (
<Story />
))]
<LubyconUIKitProvider>
<Story />
</LubyconUIKitProvider>
))];
11 changes: 6 additions & 5 deletions ui-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"dependencies": {
"@types/classnames": "^2.2.11",
"classnames": "^2.2.6",
"ionicons": "^5.2.3"
"ionicons": "^5.2.3",
"react-spring": "^8.0.27"
},
"scripts": {
"start": "start-storybook -p 6006 --no-dll",
Expand Down Expand Up @@ -61,11 +62,11 @@
"@babel/core": "^7.12.3",
"@rollup/plugin-commonjs": "^16.0.0",
"@rollup/plugin-node-resolve": "^10.0.0",
"@storybook/addon-actions": "^6.0.28",
"@storybook/addon-essentials": "^6.0.28",
"@storybook/node-logger": "^6.0.28",
"@storybook/addon-actions": "^6.1.14",
"@storybook/addon-essentials": "^6.1.14",
"@storybook/node-logger": "^6.1.14",
"@storybook/preset-create-react-app": "^3.1.5",
"@storybook/react": "^6.0.28",
"@storybook/react": "^6.1.14",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
Expand Down
7 changes: 0 additions & 7 deletions ui-kit/rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import path from 'path';

import autoprefixer from 'autoprefixer';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import postcss from 'rollup-plugin-postcss';
import typescript from 'rollup-plugin-typescript2';
// import babel from 'rollup-plugin-babel';

const extensions = ['.js', '.jsx', '.ts', '.tsx'];

Expand All @@ -29,11 +27,6 @@ function buildJS(input, output, format) {
typescript({
tsconfig: 'tsconfig.json',
}),
// babel({
// extensions,
// runtimeHelpers: true,
// include: ['src/**'],
// }),
resolve({ extensions }),
commonjs({
namedExports: {
Expand Down
17 changes: 17 additions & 0 deletions ui-kit/src/components/LubyconUIKitProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React, { ReactNode } from 'react';
import { ToastProvider } from 'contexts/Toast';
import { PortalProvider } from 'contexts/Portal';

interface Props {
children: ReactNode;
}

function LubyconUIKitProvider({ children }: Props) {
return (
<PortalProvider>
<ToastProvider>{children}</ToastProvider>
</PortalProvider>
);
}

export default LubyconUIKitProvider;
17 changes: 17 additions & 0 deletions ui-kit/src/components/Toast/ToastBody.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import classnames from 'classnames';
import Text from 'components/Text';

interface Props {
message: string;
}

const ToastBody = ({ message }: Props) => {
return (
<div className={classnames('lubycon-toast--inbox', 'lubycon-shadow--3')}>
<Text typography="p2">{message}</Text>
</div>
);
};

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

export interface ToastProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
show: boolean;
message: string;
autoHideDuration?: number;
onShow?: () => void;
onHide?: () => void;
}
const Toast = ({
show,
message,
autoHideDuration,
onShow,
onHide,
className,
style,
...rest
}: ToastProps) => {
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-toast', className)}
style={{
...style,
...props,
}}
{...rest}
>
<div>
<ToastBody message={message} />
</div>
</animated.div>
) : null;
})}
</>
);
};

export default Toast;
2 changes: 2 additions & 0 deletions ui-kit/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export { default as Radio } from './Radio';
export { default as Selection } from './Selection';
export { default as Switch } from './Switch';
export { default as Text } from './Text';
export { default as LubyconUIKitProvider } from './LubyconUIKitProvider';
export { default as Toast } from './Toast';
36 changes: 36 additions & 0 deletions ui-kit/src/contexts/Portal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { createContext, ReactNode, useContext, useState } from 'react';
import { createPortal } from 'react-dom';

export const PortalContext = createContext<HTMLDivElement | null>(null);

interface PortalProviderProps {
children: ReactNode;
}

export function PortalProvider({ children }: PortalProviderProps) {
const [portalRef, setPortalRef] = useState<HTMLDivElement | null>(null);

return (
<PortalContext.Provider value={portalRef}>
{children}
<div
id="lubycon-portal-container"
ref={(element) => {
if (portalRef !== null || element === null) {
return;
}

setPortalRef(element);
}}
/>
</PortalContext.Provider>
);
}

interface PortalConsumerProps {
children: ReactNode;
}
export function Portal({ children }: PortalConsumerProps) {
const portalRef = useContext(PortalContext);
return portalRef == null ? null : createPortal(children, portalRef);
}
78 changes: 78 additions & 0 deletions ui-kit/src/contexts/Toast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React, { ReactNode, createContext, useState, useCallback, useContext } from 'react';
import classnames from 'classnames';
import Toast, { ToastProps } from 'components/Toast';
import { generateID } from 'src/utils';
import { Portal } from './Portal';

interface ToastOptions extends Omit<ToastProps, 'show'> {
duration?: number;
}
interface ToastGlobalState {
openToast: (option: ToastOptions) => void;
closeToast: (toastId: string) => void;
}
const ToastContext = createContext<ToastGlobalState>({
openToast: () => {},
closeToast: () => {},
});

interface ToastProviderProps {
children: ReactNode;
maxStack?: number;
}
export function ToastProvider({ children, maxStack = 3 }: ToastProviderProps) {
const [openedToastsQueue, setOpenedToastsQueue] = useState<ToastOptions[]>([]);

const openToast = useCallback(
(option: ToastOptions) => {
const id = option.id ?? generateID('lubycon-toast');
const toast = { id, ...option };
const [, ...rest] = openedToastsQueue;

if (openedToastsQueue.length >= maxStack) {
setOpenedToastsQueue([...rest, toast]);
} else {
setOpenedToastsQueue([...openedToastsQueue, toast]);
}
},
[openedToastsQueue]
);

const closeToast = useCallback(
(closedToastId: string) => {
setOpenedToastsQueue(openedToastsQueue.filter((toast) => toast.id !== closedToastId));
},
[openedToastsQueue]
);

return (
<ToastContext.Provider
value={{
openToast,
closeToast,
}}
>
{children}
<Portal>
<div className={classnames('lubycon-toast--context-container')}>
{openedToastsQueue.map(({ id, onHide, duration = 3000, ...toastProps }) => (
<Toast
key={id}
show={true}
autoHideDuration={duration}
onHide={() => {
closeToast(id ?? '');
onHide?.();
}}
{...toastProps}
/>
))}
</div>
</Portal>
</ToastContext.Provider>
);
}

export function useToast() {
return useContext(ToastContext);
}
32 changes: 32 additions & 0 deletions ui-kit/src/sass/components/_Toast.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.lubycon-toast {
overflow: visible;

.lubycon-toast--inbox {
display: inline-block;
padding: 8px 16px;
min-width: 336px;
border-radius: 4px;
background-color: white;
margin: 12px 0;
}

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

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

.lubycon-toast--context-container {
display: flex;
position: fixed;
flex-direction: column-reverse;
top: auto;
right: auto;
bottom: 40px;
left: 40px;
}
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 @@ -7,3 +7,4 @@
@import './Switch';
@import './Selection';
@import './Icon';
@import './Toast';
Loading