diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..ebe51d3
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,12 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = false
+insert_final_newline = false
\ No newline at end of file
diff --git a/.eslintignore b/.eslintignore
index b512c09..97008e5 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1 +1,2 @@
-node_modules
\ No newline at end of file
+node_modules
+yarn.lock
\ No newline at end of file
diff --git a/.eslintrc.json b/.eslintrc.json
index 37dbb1b..bc4e694 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -6,7 +6,7 @@
"extends": [
"eslint:recommended",
"plugin:react/recommended",
- "plgin:react-hooks/recommended",
+ "plugin:react-hooks/recommended",
"plugin:storybook/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
diff --git a/.prettierignore b/.prettierignore
index b512c09..97008e5 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1 +1,2 @@
-node_modules
\ No newline at end of file
+node_modules
+yarn.lock
\ No newline at end of file
diff --git a/README.md b/README.md
index 3768ecb..037f308 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,8 @@ To use WAP UI, Install the `@wap-ui/react` package and its peer dependencies.
(`@emotion/react`, `@emotion/styled`, `framer-motion`)
```sh
+pnpm add @wap-ui/react @emotion/react @emotion/styled framer-motion
+# or
yarn add @wap-ui/react @emotion/react @emotion/styled framer-motion
# or
npm i @wap-ui/react @emotion/react @emotion/styled framer-motion
diff --git a/docs/components/Portal.md b/docs/components/Portal.md
deleted file mode 100644
index c6e85a9..0000000
--- a/docs/components/Portal.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# `[Component] Portal`
-
-## `type`
-
-```tsx
-interface Portal {
- children: React.ReactNode;
-
- /**
- * @description DOM의 id 혹은 element를 지정합니다.
- * @default 'portal' - className으로 지정됨
- *
- */
- target?: HTMLElement | string;
-}
-```
-
-## `example`
-
-```tsx
-
-
-
-
-
-
-
-
-
-
-
-```
diff --git a/packages/react/.storybook/preview-head.html b/packages/react/.storybook/preview-head.html
index 2e3ae13..e551040 100644
--- a/packages/react/.storybook/preview-head.html
+++ b/packages/react/.storybook/preview-head.html
@@ -1 +1,3 @@
-
+
diff --git a/packages/react/README.md b/packages/react/README.md
index 796374a..037f308 100644
--- a/packages/react/README.md
+++ b/packages/react/README.md
@@ -30,10 +30,10 @@
-
+
|
-
+
|
@@ -45,6 +45,8 @@ To use WAP UI, Install the `@wap-ui/react` package and its peer dependencies.
(`@emotion/react`, `@emotion/styled`, `framer-motion`)
```sh
+pnpm add @wap-ui/react @emotion/react @emotion/styled framer-motion
+# or
yarn add @wap-ui/react @emotion/react @emotion/styled framer-motion
# or
npm i @wap-ui/react @emotion/react @emotion/styled framer-motion
@@ -141,7 +143,7 @@ export default Home;
## `Links`
- #### [Documents](https://github.com/pknu-wap/wap-ui/tree/main/docs)
-- #### [NPM](https://www.npmjs.com/package/wap-ui)
+- #### [NPM](https://www.npmjs.com/package/@wap-ui/react)
- #### [Playground](https://wap-ui.vercel.app/)
- #### [Presentations](https://github.com/pknu-wap/wap-ui/tree/main/ppt)
- #### [Example](https://github.com/pknu-wap/wap-ui/tree/main/example)
diff --git a/packages/react/package.json b/packages/react/package.json
index db6b5a9..e275ff4 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -1,6 +1,6 @@
{
"name": "@wap-ui/react",
- "version": "1.3.3",
+ "version": "1.4.0",
"description": "WAP UI / React UI Components Library",
"repository": "https://github.com/pknu-wap/2022_2_WAP_WEB_TEAM1.git",
"author": "neko113 ",
diff --git a/packages/react/src/components/Dropdown/Dropdown.tsx b/packages/react/src/components/Dropdown/Dropdown.tsx
index 730bd6a..9454ac7 100644
--- a/packages/react/src/components/Dropdown/Dropdown.tsx
+++ b/packages/react/src/components/Dropdown/Dropdown.tsx
@@ -1,10 +1,10 @@
import React, { useRef, useState } from 'react';
-import useOnClickOutside from '../../hooks/useOnClickOutside';
import { DropdownMenu } from './DropdownMenu';
import { DropdownMenuItem } from './DropdownMenuItem';
import { DropdownButton } from './DropdownButton/DropdownButton';
import { DropdownContext } from './DropdownContext';
-import { Portal } from '../Portal';
+import { usePortal, useOnClickOutside } from '../../hooks';
+import { createPortal } from 'react-dom';
export interface DropdownProps {
children: React.ReactNode[];
@@ -27,6 +27,7 @@ export interface DropdownProps {
* ```
*/
export const Dropdown = ({ children }: DropdownProps) => {
+ const el = usePortal('dropdown');
/** triggerRef은 Context를 타고 DropdownButton으로 전달됨 */
const triggerRef = useRef(null);
const contentRef = useRef(null);
@@ -42,6 +43,8 @@ export const Dropdown = ({ children }: DropdownProps) => {
}
});
+ if (!el) return null;
+
return (
{
{/* tirgger button Actions */}
{trigger}
{/* content menu ... */}
- {content}
+ {createPortal(content, el)}
);
};
diff --git a/packages/react/src/components/Modal/Modal.tsx b/packages/react/src/components/Modal/Modal.tsx
index 5446237..1ed1c8f 100644
--- a/packages/react/src/components/Modal/Modal.tsx
+++ b/packages/react/src/components/Modal/Modal.tsx
@@ -1,10 +1,11 @@
import { AnimatePresence, useWillChange, type Variants } from 'framer-motion';
import React, { useLayoutEffect } from 'react';
-import { Portal } from '../Portal';
import * as S from './Modal.styles';
import { ModalBody } from './ModalBody/ModalBody';
import { ModalFooter } from './ModalFooter/ModalFooter';
import { ModalHeader } from './ModalHeader/ModalHeader';
+import { usePortal } from '../../hooks';
+import { createPortal } from 'react-dom';
export interface ModalProps {
/**
@@ -52,6 +53,7 @@ export const Modal = ({
children,
}: ModalProps) => {
const willChange = useWillChange();
+ const el = usePortal('modal');
useLayoutEffect(() => {
if (isOpen) {
@@ -88,37 +90,38 @@ export const Modal = ({
},
};
- return (
-
-
- {isOpen && (
- <>
-
+ {isOpen && (
+ <>
+
+
+
-
-
- {children}
-
-
- >
- )}
-
-
+ >
+ {children}
+
+
+ >
+ )}
+ ,
+ el,
);
};
diff --git a/packages/react/src/components/Portal/Portal.stories.tsx b/packages/react/src/components/Portal/Portal.stories.tsx
deleted file mode 100644
index 6ca77a2..0000000
--- a/packages/react/src/components/Portal/Portal.stories.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { ComponentStory, ComponentMeta } from '@storybook/react';
-import React from 'react';
-import { Button } from '../Button';
-import styled from '@emotion/styled';
-import { Modal } from '../Modal';
-import { Portal, PortalProps } from './Portal';
-import useDisclosure from '../../hooks/useDisclosure';
-
-export default {
- title: 'Components/Portal',
- component: Portal,
-} as ComponentMeta;
-
-const Template: ComponentStory = (args: PortalProps) => {
- const { isOpen, onOpen, onClose } = useDisclosure();
-
- return (
- <>
-
-
-
-
- Header
-
-
- Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
- eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
- enim ad minim veniam, quis nostrud exercitation ullamco laboris
- nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
- in reprehenderit in voluptate velit esse cillum dolore eu fugiat
- nulla pariatur. Excepteur sint occaecat cupidatat non proident,
- sunt in culpa qui officia deserunt mollit anim id est laborum.
-
-
-
-
-
-
-
-
-
- >
- );
-};
-
-const Container = styled.div`
- height: 200vh;
-`;
-
-export const Default = Template.bind({});
-
-export const WithId = Template.bind({});
-WithId.args = {
- target: 'modal',
-};
-
-export const WithElement = Template.bind({});
-WithElement.args = {
- target: document.getElementById('modal') as HTMLElement,
-};
diff --git a/packages/react/src/components/Portal/Portal.tsx b/packages/react/src/components/Portal/Portal.tsx
deleted file mode 100644
index 9c1a024..0000000
--- a/packages/react/src/components/Portal/Portal.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import React, { useEffect, useRef } from 'react';
-import ReactDOM from 'react-dom';
-
-export interface PortalProps {
- children: React.ReactNode;
-
- /**
- * @description DOM의 id 혹은 element를 지정합니다.
- * @default 'portal' - className으로 지정됨
- *
- */
- target?: HTMLElement | string;
-}
-
-/**
- * 참고
- * @see https://github.com/mantinedev/mantine/blob/master/src/mantine-core/src/Portal/Portal.tsx
- *
- * @example
- *
- *
- *
- *
- *
- *
- *
- *
- *
- *
- *
- */
-
-export const Portal = ({ children, target }: PortalProps) => {
- const portalContainer = useRef();
-
- if (typeof target === 'string') {
- /**
- * @description target이 string일 경우, 해당 id를 가진 element를 찾는다
- */
- if (document.getElementById(target) as HTMLElement) {
- portalContainer.current = document.getElementById(target) as HTMLElement;
- } else {
- /**
- * 찾지 못할 경우 target을 className으로 갖는 element를 만든다
- */
- if (!portalContainer.current) {
- portalContainer.current = document.createElement('div');
- portalContainer.current.className = target;
- document.body.appendChild(portalContainer.current);
- }
- }
- } else if (target instanceof HTMLElement) {
- /**
- * @description target이 HTMLElement일 경우, 해당 element를 찾는다
- */
- portalContainer.current = target;
- document.body.appendChild(portalContainer.current);
- } else {
- /**
- * @description target이 없을 경우, body에 portal라는 className를 가진 div element를 생성한다
- */
- if (!portalContainer.current) {
- const div = document.createElement('div');
- div.className = 'portal';
- portalContainer.current = div;
- }
- document.body.appendChild(portalContainer.current);
- }
-
- useEffect(() => {
- return () => {
- if (target === undefined && portalContainer.current) {
- document.body.removeChild(portalContainer.current);
- }
- };
- }, [target]);
-
- return ReactDOM.createPortal(
- children,
- portalContainer.current as HTMLElement,
- );
-};
diff --git a/packages/react/src/components/Portal/index.ts b/packages/react/src/components/Portal/index.ts
deleted file mode 100644
index ca6e556..0000000
--- a/packages/react/src/components/Portal/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { Portal } from './Portal';
diff --git a/packages/react/src/components/Toast/Toaster/Toaster.tsx b/packages/react/src/components/Toast/Toaster/Toaster.tsx
index 21866c2..10b61db 100644
--- a/packages/react/src/components/Toast/Toaster/Toaster.tsx
+++ b/packages/react/src/components/Toast/Toaster/Toaster.tsx
@@ -1,9 +1,10 @@
import { AnimatePresence } from 'framer-motion';
import React, { useEffect, useState } from 'react';
-import { Portal } from '../../Portal';
import { ToastBar } from '../ToastBar/ToastBar';
import { getToastList } from '../useToast';
import * as S from './Toaster.styles';
+import { usePortal } from '../../../hooks';
+import { createPortal } from 'react-dom';
type ToastPosition =
| 'top-left'
@@ -85,6 +86,7 @@ const getPositionStyles = (position: ToastPosition): React.CSSProperties => {
*/
export const Toaster = ({ position = 'bottom-center' }: ToasterProps) => {
+ const el = usePortal('toast');
const positionStyles = getPositionStyles(position);
const [isBottom, setIsBottom] = useState(true);
const toastList = getToastList();
@@ -97,21 +99,20 @@ export const Toaster = ({ position = 'bottom-center' }: ToasterProps) => {
}
}, [position, setIsBottom]);
- return (
- <>
-
-
-
-
- {toastList.map((toast) => (
-
- {toast.message}
-
- ))}
-
-
-
-
- >
+ if (!el) return null;
+
+ return createPortal(
+
+
+
+ {toastList.map((toast) => (
+
+ {toast.message}
+
+ ))}
+
+
+ ,
+ el,
);
};
diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts
index 0342e28..7e5f3f5 100644
--- a/packages/react/src/components/index.ts
+++ b/packages/react/src/components/index.ts
@@ -3,7 +3,6 @@ export * from './Card';
export * from './Checkbox';
export * from './Radio';
export * from './Modal';
-export * from './Portal';
export * from './Accordion';
export * from './TextInput';
export * from './Toggle';
diff --git a/packages/react/src/hooks/index.ts b/packages/react/src/hooks/index.ts
index 8aed404..23f0d09 100644
--- a/packages/react/src/hooks/index.ts
+++ b/packages/react/src/hooks/index.ts
@@ -3,3 +3,5 @@ export { default as useDisclosure } from './useDisclosure';
export { default as useThrottle } from './useThrottle';
export { default as useDebounce } from './useDebounce';
export { default as useIntersectionObserver } from './useIntersectionObserver';
+export { default as usePortal } from './usePortal';
+export { default as useOnClickOutside } from './useOnClickOutside';
diff --git a/packages/react/src/hooks/usePortal.ts b/packages/react/src/hooks/usePortal.ts
new file mode 100644
index 0000000..aea60df
--- /dev/null
+++ b/packages/react/src/hooks/usePortal.ts
@@ -0,0 +1,50 @@
+import { useEffect, useState } from 'react';
+
+const createElement = (id: string): HTMLElement => {
+ const el = document.createElement('div');
+ el.setAttribute('id', id);
+ return el;
+};
+
+const isBrowser = (): boolean => {
+ return Boolean(
+ typeof window !== 'undefined' &&
+ window.document &&
+ window.document.createElement,
+ );
+};
+
+/**
+@description
+usePortal은 문서의 본문에 div 요소를 만들고 반환하는 훅입니다.
+이는 모달, 드롭다운 및 툴팁을 만드는 데 유용합니다.
+index.html에 div 요소를 추가하지 않아도 됩니다.
+@example
+const Modal = () => {
+ const el = usePortal('modal');
+ if (!el) return null;
+ return createPortal(, el);
+};
+ */
+const usePortal = (selectId: string): HTMLElement | null => {
+ const id = `${selectId}`;
+
+ const [elSnapshot, setElSnapshot] = useState(
+ isBrowser() ? createElement(id) : null,
+ );
+
+ useEffect(() => {
+ const parentElement = document.body;
+ const hasElement = parentElement?.querySelector(`#${id}`);
+ const el = hasElement || createElement(id);
+
+ if (!hasElement) {
+ parentElement.appendChild(el);
+ }
+ setElSnapshot(el);
+ }, []);
+
+ return elSnapshot;
+};
+
+export default usePortal;